You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by ah...@apache.org on 2023/09/17 09:28:22 UTC

[causeway] 02/02: CAUSEWAY-3586: allows for ViewModel.viewModelMemento() to return non-url-safe Strings

This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 53d23488c01d1b59c369da208b68c9be183a5985
Author: andi-huber <ah...@apache.org>
AuthorDate: Sun Sep 17 11:28:14 2023 +0200

    CAUSEWAY-3586: allows for ViewModel.viewModelMemento() to return
    non-url-safe Strings
---
 .../java/org/apache/causeway/applib/ViewModel.java |  6 ++-
 .../org/apache/causeway/commons/io/UrlUtils.java   | 62 ++++++++++++++++++++++
 .../ViewModelFacetForViewModelInterface.java       |  6 ++-
 .../RecreatableObjectFacetFactoryTest.java         | 51 +++++++++++++++---
 .../applib/util/UrlEncodingUtils.java              | 47 +++++-----------
 5 files changed, 128 insertions(+), 44 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/causeway/applib/ViewModel.java b/api/applib/src/main/java/org/apache/causeway/applib/ViewModel.java
index 7005deda65..4fc820bbc1 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/ViewModel.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/ViewModel.java
@@ -28,7 +28,7 @@ import org.apache.causeway.applib.services.registry.ServiceRegistry;
 /**
  * Indicates that an object belongs to the UI/application layer and is intended to be used as a view-model.
  * <p>
- * Instances of {@link ViewModel} must include (exactly) one public constructor.
+ * Instances of {@link ViewModel} must include (at least) one public constructor.
  * <p>
  * Contract:
  * <ul>
@@ -58,6 +58,10 @@ public interface ViewModel {
      * <p>
      * Captures the state of this view-model as {@link String},
      * which can be passed in to this view-model's constructor for later re-construction.
+     * <p>
+     * The framework automatically takes care of non-URL-safe strings,
+     * by passing them through {@link java.net.URLEncoder}/
+     * {@link java.net.URLDecoder} for encoding and decoding respectively.
      */
     @Programmatic
     String viewModelMemento();
diff --git a/commons/src/main/java/org/apache/causeway/commons/io/UrlUtils.java b/commons/src/main/java/org/apache/causeway/commons/io/UrlUtils.java
new file mode 100644
index 0000000000..52eba32faa
--- /dev/null
+++ b/commons/src/main/java/org/apache/causeway/commons/io/UrlUtils.java
@@ -0,0 +1,62 @@
+package org.apache.causeway.commons.io;
+
+import java.net.URL;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+import org.springframework.lang.Nullable;
+
+import lombok.SneakyThrows;
+import lombok.experimental.UtilityClass;
+
+/**
+ * Utilities for {@link URL}s.
+ *
+ * @since 2.0 {@index}
+ */
+@UtilityClass
+public class UrlUtils {
+
+    // -- DECODE
+
+    /**
+     * @see URLDecoder
+     */
+    @SneakyThrows
+    public static String urlDecodeUtf8(final @Nullable String input) {
+        return input!=null
+             ? URLDecoder.decode(input, StandardCharsets.UTF_8)
+             : input;
+    }
+
+    // -- ENCODE
+
+    /**
+     * @see URLEncoder
+     */
+    @SneakyThrows
+    public static String urlEncodeUtf8(final @Nullable String input) {
+        return input!=null
+                ? URLEncoder.encode(input, StandardCharsets.UTF_8)
+                : input;
+    }
+
+    // -- VALIDITY
+
+//XXX non optimized, so not published
+//    /**
+//     * Whether given url string is URL safe.
+//     * @implNote this check is potentially expensive,
+//     *      as it generates stacktraces for the non-happy cases
+//     */
+//    public boolean isUrlSafe(final String url) {
+//        try {
+//            new URL(url);
+//            return true;
+//        } catch (MalformedURLException e) {
+//            return false;
+//        }
+//    }
+
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForViewModelInterface.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForViewModelInterface.java
index a225ab1061..e2dd9ce65f 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForViewModelInterface.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForViewModelInterface.java
@@ -31,6 +31,7 @@ import org.apache.causeway.commons.collections.Can;
 import org.apache.causeway.commons.functional.IndexedConsumer;
 import org.apache.causeway.commons.internal.assertions._Assert;
 import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedConstructor;
+import org.apache.causeway.commons.io.UrlUtils;
 import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants;
 import org.apache.causeway.core.metamodel.commons.ClassExtensions;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
@@ -134,7 +135,7 @@ extends ViewModelFacetAbstract {
     @Override
     public String serialize(final ManagedObject viewModel) {
         final ViewModel viewModelPojo = (ViewModel) viewModel.getPojo();
-        return viewModelPojo.viewModelMemento();
+        return UrlUtils.urlEncodeUtf8(viewModelPojo.viewModelMemento());
     }
 
     // -- HELPER
@@ -142,11 +143,12 @@ extends ViewModelFacetAbstract {
     @SneakyThrows
     private Object deserialize(
             @NonNull final ObjectSpecification viewmodelSpec,
-            @Nullable final String memento) {
+            @Nullable final String mementoEncoded) {
 
         _Assert.assertNotNull(constructorAnyArgs, ()->"framework bug: required non-null, "
                 + "this can only happen, if we try to deserialize an abstract type");
 
+        val memento = UrlUtils.urlDecodeUtf8(mementoEncoded);
         val resolvedArgs = resolveArgsForConstructor(constructorAnyArgs, getServiceRegistry(), memento);
         val viewmodelPojo = constructorAnyArgs.constructor().newInstance(resolvedArgs);
         return viewmodelPojo;
diff --git a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/object/recreatable/RecreatableObjectFacetFactoryTest.java b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/object/recreatable/RecreatableObjectFacetFactoryTest.java
index 1b5978d9f8..1881a94bc2 100644
--- a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/object/recreatable/RecreatableObjectFacetFactoryTest.java
+++ b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/object/recreatable/RecreatableObjectFacetFactoryTest.java
@@ -18,20 +18,30 @@
  */
 package org.apache.causeway.core.metamodel.facets.object.recreatable;
 
+import java.util.Optional;
+
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.springframework.lang.Nullable;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import org.apache.causeway.applib.ViewModel;
-import org.apache.causeway.core.metamodel.facetapi.Facet;
+import org.apache.causeway.commons.internal.base._Strings;
+import org.apache.causeway.commons.io.UrlUtils;
 import org.apache.causeway.core.metamodel.facets.FacetFactoryTestAbstract;
 import org.apache.causeway.core.metamodel.facets.object.viewmodel.ViewModelFacet;
 import org.apache.causeway.core.metamodel.facets.object.viewmodel.ViewModelFacetFactory;
 import org.apache.causeway.core.metamodel.facets.object.viewmodel.ViewModelFacetForViewModelInterface;
 
+import lombok.Getter;
+import lombok.Setter;
+import lombok.val;
+
 class RecreatableObjectFacetFactoryTest
 extends FacetFactoryTestAbstract {
 
@@ -47,10 +57,17 @@ extends FacetFactoryTestAbstract {
         facetFactory = null;
     }
 
-    static class Customer implements ViewModel {
+    // public, so org.apache.causeway.core.metamodel.facets.object.viewmodel.ViewModelFacetForViewModelInterface
+    // can reflectively access it for the roundtrip below
+    public static class Customer implements ViewModel {
+
+        public Customer(final String memento) { setName(memento);}
+
+        @Getter @Setter
+        private String name;
+
         @Override
-        public String viewModelMemento() { return null; }
-        public Customer(final String memento) { }
+        public String viewModelMemento() { return name; }
     }
 
     @Test
@@ -60,12 +77,32 @@ extends FacetFactoryTestAbstract {
             //when
             facetFactory.process(processClassContext);
             //then
-            final Facet facet = facetHolder.getFacet(ViewModelFacet.class);
-            assertNotNull(facet);
-            assertTrue(facet instanceof ViewModelFacetForViewModelInterface);
+            final ViewModelFacet viewModelFacet = facetHolder.getFacet(ViewModelFacet.class);
+            assertNotNull(viewModelFacet);
+            assertTrue(viewModelFacet instanceof ViewModelFacetForViewModelInterface);
 
             assertNoMethodsRemoved();
+
+            // do a little roundtrip test
+            {
+                val customerPojo = new Customer("Hallo World!:/?|");
+                assertFalse(isUrlSafeChunk(customerPojo.viewModelMemento()));
+
+                val customer = facetFactory.getObjectManager().adapt(customerPojo);
+                val customerBookmark = viewModelFacet.serializeToBookmark(customer);
+
+                val customerRecreated = viewModelFacet.instantiate(
+                        customer.getSpecification(), Optional.of(customerBookmark));
+
+                assertEquals(customerPojo.getName(), ((Customer)(customerRecreated.getPojo())).getName());
+            }
+
         });
     }
 
+    private static boolean isUrlSafeChunk(final @Nullable String urlChunck) {
+        if(_Strings.isEmpty(urlChunck)) return true;
+        return UrlUtils.urlEncodeUtf8(urlChunck).equals(urlChunck);
+    }
+
 }
diff --git a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/UrlEncodingUtils.java b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/UrlEncodingUtils.java
index 9c2e526260..595c321bac 100644
--- a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/UrlEncodingUtils.java
+++ b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/UrlEncodingUtils.java
@@ -18,67 +18,46 @@
  */
 package org.apache.causeway.viewer.restfulobjects.applib.util;
 
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.List;
-import java.util.function.Function;
 
 import com.fasterxml.jackson.databind.JsonNode;
 
 import org.apache.causeway.commons.internal.collections._Lists;
+import org.apache.causeway.commons.io.UrlUtils;
 import org.apache.causeway.viewer.restfulobjects.applib.JsonRepresentation;
 
+import lombok.experimental.UtilityClass;
+
 /**
  * @since 1.x {@index}
  */
+@UtilityClass
 public final class UrlEncodingUtils {
 
-    public static final Function<String, String> FUNCTION = new Function<String, String>() {
-
-        @Override
-        public String apply(final String input) {
-            try {
-                return URLDecoder.decode(input, "UTF-8");
-            } catch (final UnsupportedEncodingException e) {
-                return "";
-            }
-        }
-    };
-
-    private UrlEncodingUtils() {
-    }
-
-    public static String urlDecode(final String string) {
-        return FUNCTION.apply(string);
+    public String urlDecode(final String string) {
+        return UrlUtils.urlDecodeUtf8(string);
     }
 
-    public static List<String> urlDecode(final List<String> values) {
-        return _Lists.map(values, FUNCTION);
+    public List<String> urlDecode(final List<String> values) {
+        return _Lists.map(values, UrlUtils::urlDecodeUtf8);
     }
 
-    public static String[] urlDecode(final String[] values) {
+    public String[] urlDecode(final String[] values) {
         final List<String> asList = Arrays.asList(values);
         return urlDecode(asList).toArray(new String[] {});
     }
 
-    public static String urlEncode(final JsonNode jsonNode) {
+    public String urlEncode(final JsonNode jsonNode) {
         return urlEncode(jsonNode.toString());
     }
 
-    public static String urlEncode(final JsonRepresentation jsonRepresentation ) {
+    public String urlEncode(final JsonRepresentation jsonRepresentation ) {
         return urlEncode(jsonRepresentation.toString());
     }
 
-    public static String urlEncode(final String str) {
-        try {
-            return URLEncoder.encode(str, StandardCharsets.UTF_8.name());
-        } catch (final UnsupportedEncodingException e) {
-            // shouldn't happen
-            throw new RuntimeException(e);
-        }
+    public String urlEncode(final String str) {
+        return UrlUtils.urlEncodeUtf8(str);
     }
 
 }