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);
}
}