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 2025/11/05 05:48:53 UTC
(causeway) 01/03: CAUSEWAY-3939: removal of IdStringifier for Serializable
This is an automated email from the ASF dual-hosted git repository.
ahuber pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/causeway.git
commit e6bf00c63c33cfa894a19d6122526a1aec227d14
Author: Andi Huber <ah...@apache.org>
AuthorDate: Thu Oct 30 14:27:54 2025 +0100
CAUSEWAY-3939: removal of IdStringifier for Serializable
- id de-serialization is exposed to untrusted input
CAUSEWAY-3939: adds HmacAuthority interface
- designated for devs to opt in on secure Serialization fallbacks
- yet only implemented for ViewModelFacetForSerializableInterface
CAUSEWAY-3939: adds default impl. f. HmacAuthority
CAUSEWAY-3939: more java doc and test fixes
CAUSEWAY-3939: hardening HmacAuthority
CAUSEWAY-3939: introduces HmacUtils
refactors verifaction and signing of byte arrays into HmacUtils
CAUSEWAY-3939: moves Mementos from commons to metamodel artifact
such that we can later make use of the HmacAuthority that is in Applib
CAUSEWAY-3939: makes HMAC mandatory for viewmodels and composite values
CAUSEWAY-3939: HMAC support for
ViewModelFacetForXmlRootElementAnnotation
CAUSEWAY-3939: fixes to prev. commit
CAUSEWAY-3939: HMAC support for ViewModelFacetForViewModelInterface
CAUSEWAY-3939: consolidates all encoding/decoding into shared abstract
ViewmodelFacet
CAUSEWAY-3939: ViewModel java-doc update
CAUSEWAY-3939: refactors SerializingAdapter to ValueCodec (from Service
to just a Bean)
(tried to encapsulate, however that made test setup hard)
CAUSEWAY-3939: removes temporary code
CAUSEWAY-3939: introduces DigitalVerificationException
CAUSEWAY-3939: on DigitalVerificationException disable error page
tickets
---
.../java/org/apache/causeway/applib/ViewModel.java | 23 ++-
.../unrecoverable/BookmarkNotFoundException.java | 4 -
...tion.java => DigitalVerificationException.java} | 22 +--
.../applib/layout/grid/bootstrap/BSUtil.java | 9 +-
.../applib/services/bookmark/HmacAuthority.java | 121 +++++++++++++
.../services/urlencoding/UrlEncodingService.java | 26 +--
.../causeway/applib/value/semantics/Converter.java | 3 -
.../applib/value/semantics/DefaultsProvider.java | 3 -
.../applib/value/semantics/OrderRelation.java | 3 -
commons/src/main/java/module-info.java | 1 -
.../causeway/commons/internal/base/_Bytes.java | 6 +
.../commons/internal/memento/_MementoDefault.java | 125 -------------
.../commons/internal/memento/_Mementos.java | 193 ---------------------
.../commons/internal/memento/package-info.java | 28 ---
.../commons/internal/resources/_Serializables.java | 71 ++++++--
core/metamodel/src/main/java/module-info.java | 1 +
.../metamodel/CausewayModuleCoreMetamodel.java | 23 ++-
.../DomainObjectAnnotationFacetFactory.java | 23 ++-
.../object/viewmodel/SecureViewModelFacet.java | 167 ++++++++++++++++++
.../object/viewmodel/ViewModelFacetAbstract.java | 119 -------------
.../object/viewmodel/ViewModelFacetFactory.java | 38 +++-
.../ViewModelFacetForDomainObjectAnnotation.java | 74 +++-----
.../viewmodel/ViewModelFacetForJavaRecord.java | 79 +++------
.../ViewModelFacetForSerializableInterface.java | 68 +++-----
.../ViewModelFacetForViewModelInterface.java | 82 +++++----
.../ViewModelFacetForXmlRootElementAnnotation.java | 77 +++-----
.../core/metamodel/util/hmac/HmacUrlCodec.java | 67 +++++++
.../core/metamodel/util/hmac/HmacUtils.java | 96 ++++++++++
.../causeway/core/metamodel/util/hmac/Memento.java | 60 +++++++
.../metamodel/util/hmac/MementoHmacContext.java | 55 ++++++
.../core/metamodel/util/hmac/SecureMemento.java | 119 +++++++++++++
.../IdStringifierForSerializable.java | 113 ------------
.../valuesemantics/TreeNodeValueSemantics.java | 51 ++++--
.../core/metamodel/valuesemantics/ValueCodec.java} | 65 +++----
.../IdStringifierForSerializable_Test.java | 71 --------
.../mmtestsupport/MetaModelContext_forTesting.java | 11 +-
.../runtimeservices/src/main/java/module-info.java | 4 +-
.../CausewayModuleCoreRuntimeServices.java | 19 +-
.../runtimeservices/urlencoding/MementosTest.java | 36 ++--
.../security/EncodabilityContractTest.java | 26 +--
.../domainmodel/MetaModelRegressionTest.java | 14 +-
.../testdomain/factory/ViewModelFactoryTest.java | 9 +-
.../wicket/ui/errors/ExceptionStackTracePanel.java | 27 +--
.../integration/WebRequestCycleForCauseway.java | 21 ++-
44 files changed, 1152 insertions(+), 1101 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 4fc820bbc1d..435dffe0cd2 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
@@ -27,10 +27,10 @@
/**
* 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 (at least) one public constructor.
- * <p>
- * Contract:
+ *
+ * <p> Instances of {@link ViewModel} must include (at least) one public constructor.
+ *
+ * <p> Contract:
* <ul>
* <li>there is either exactly one public constructor or if there are more than one,
* then only one of these is annotated with any of {@code @Inject} or {@code @Autowired(required=true)}
@@ -44,8 +44,8 @@
* Naturally this also allows for the idiom of passing in the {@link ServiceInjector} as an argument
* and programmatically resolve any field-style injection points via {@link ServiceInjector#injectServicesInto(Object)},
* that is, if already required during <i>construction</i>.
- * <p>
- * After a view-model got new-ed up by the framework (or programmatically via the {@link FactoryService}),
+ *
+ * <p> After a view-model got new-ed up by the framework (or programmatically via the {@link FactoryService}),
* {@link ServiceInjector#injectServicesInto(Object)} is called on the viewmodel instance,
* regardless of what happened during <i>construction</i>.
*
@@ -55,13 +55,12 @@ public interface ViewModel {
/**
* Obtain a memento of the view-model. (Optional)
- * <p>
- * Captures the state of this view-model as {@link String},
+ *
+ * <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.
+ *
+ * <p> The framework automatically takes care of non-URL-safe strings, null and the empty String.
+ * Those view-model mementos are also digitally signed, before the are handed out to clients.
*/
@Programmatic
String viewModelMemento();
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/BookmarkNotFoundException.java b/api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/BookmarkNotFoundException.java
index 5372c4f16ba..cad9163fdb6 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/BookmarkNotFoundException.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/BookmarkNotFoundException.java
@@ -25,12 +25,8 @@
* Indicates that a bookmark cannot be found.
*
* @since 2.1, 3.1 {@index}
- *
*/
public class BookmarkNotFoundException extends UnrecoverableException {
- /**
- *
- */
private static final long serialVersionUID = 1L;
public BookmarkNotFoundException(final String msg) {
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/BookmarkNotFoundException.java b/api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/DigitalVerificationException.java
similarity index 72%
copy from api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/BookmarkNotFoundException.java
copy to api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/DigitalVerificationException.java
index 5372c4f16ba..3bd5c7cddb8 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/BookmarkNotFoundException.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/DigitalVerificationException.java
@@ -22,36 +22,36 @@
import org.apache.causeway.applib.services.i18n.TranslatableString;
/**
- * Indicates that a bookmark cannot be found.
+ * Indicates that a digital verification failed.
*
- * @since 2.1, 3.1 {@index}
+ * <p>E.g. could be an invalid (or no longer valid) bookmark.
+ *
+ * @since 3.5 {@index}
*
*/
-public class BookmarkNotFoundException extends UnrecoverableException {
- /**
- *
- */
+public class DigitalVerificationException extends UnrecoverableException {
private static final long serialVersionUID = 1L;
- public BookmarkNotFoundException(final String msg) {
+ public DigitalVerificationException(final String msg) {
super(msg);
}
- public BookmarkNotFoundException(final TranslatableString translatableMessage,
+ public DigitalVerificationException(final TranslatableString translatableMessage,
final Class<?> translationContextClass, final String translationContextMethod) {
super(translatableMessage, translationContextClass, translationContextMethod);
}
- public BookmarkNotFoundException(final Throwable cause) {
+ public DigitalVerificationException(final Throwable cause) {
super(cause);
}
- public BookmarkNotFoundException(final String msg, final Throwable cause) {
+ public DigitalVerificationException(final String msg, final Throwable cause) {
super(msg, cause);
}
- public BookmarkNotFoundException(final TranslatableString translatableMessage,
+ public DigitalVerificationException(final TranslatableString translatableMessage,
final Class<?> translationContextClass, final String translationContextMethod, final Throwable cause) {
super(translatableMessage, translationContextClass, translationContextMethod, cause);
}
+
}
\ No newline at end of file
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/layout/grid/bootstrap/BSUtil.java b/api/applib/src/main/java/org/apache/causeway/applib/layout/grid/bootstrap/BSUtil.java
index ff1ef2165fe..9d207ef9da7 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/layout/grid/bootstrap/BSUtil.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/layout/grid/bootstrap/BSUtil.java
@@ -21,6 +21,8 @@
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
+import org.springframework.util.SerializationUtils;
+
import org.apache.causeway.applib.layout.component.ActionLayoutData;
import org.apache.causeway.applib.layout.component.ActionLayoutDataOwner;
import org.apache.causeway.applib.layout.component.CollectionLayoutData;
@@ -30,9 +32,6 @@
import org.apache.causeway.applib.layout.component.FieldSet;
import org.apache.causeway.applib.layout.component.PropertyLayoutData;
import org.apache.causeway.applib.layout.grid.bootstrap.BSElement.BSElementVisitor;
-import org.apache.causeway.commons.internal.base._Casts;
-import org.apache.causeway.commons.internal.resources._Serializables;
-
import lombok.experimental.UtilityClass;
@UtilityClass
@@ -76,9 +75,7 @@ public void visit(final CollectionLayoutData collectionLayoutData) {
* Creates a deep copy of given original grid.
*/
public BSGrid deepCopy(final BSGrid orig) {
- var bytes = _Serializables.write(orig);
- return _Casts.uncheckedCast(
- _Serializables.read(BSGrid.class, bytes));
+ return SerializationUtils.clone(orig);
}
public BSGrid resolveOwners(final BSGrid grid) {
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/bookmark/HmacAuthority.java b/api/applib/src/main/java/org/apache/causeway/applib/services/bookmark/HmacAuthority.java
new file mode 100644
index 00000000000..45a8fa78752
--- /dev/null
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/bookmark/HmacAuthority.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.applib.services.bookmark;
+
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Objects;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.jspecify.annotations.Nullable;
+
+import lombok.SneakyThrows;
+
+/**
+ * Can be registered with Spring, to override the build in default, which has an application scoped random secret.
+ *
+ * <pre>
+ * {@code @Configuration}
+ * class EnableHmacAuthority {
+ * {@code @Bean}
+ * public HmacAuthority hmacAuthority() {
+ * return HmacAuthority.HmacSHA256.randomInstance();
+ * }
+ * }
+ * </pre>
+ *
+ * <p>Note that bookmark's validity is bound to the (server-side) secret key of the {@link HmacAuthority}.
+ * Once the secret changes, bookmarks that are stored client-side for later use, will be rendered invalid.
+ *
+ * @apiNote the default bean is auto-configured with 'CausewayModuleCoreRuntimeServices.HmacAutorityAutoconfigure'
+ *
+ * @since 3.5
+ */
+public interface HmacAuthority {
+
+ /**
+ * HMAC as byte array, for given input data.
+ */
+ byte[] generateHmac(byte[] data);
+
+ /**
+ * Verifies that given dataToVerify when passed to {@link #generateHmac(byte[])} yields given hmacToVerify.
+ *
+ * <p>If any of the arguments is null returns false.
+ *
+ * <p>If hmacToVerify does not conform with {@link #isValidHmacLength(int)} returns false.
+ */
+ default boolean verifyHmac(final @Nullable byte[] data, final @Nullable byte[] hmacToVerify) {
+ if(data == null || hmacToVerify == null) return false; // invalid by definition
+ if(!isValidHmacLength(hmacToVerify.length)) return false; // shortcut
+ return Arrays.equals(generateHmac(data), hmacToVerify);
+ }
+
+ /**
+ * Whether HMAC length in bytes is expected/valid.
+ */
+ boolean isValidHmacLength(final int hmacLength);
+
+ // -- IMPL
+
+ record HmacSHA256(
+ SecretKeySpec secretKey) implements HmacAuthority {
+
+ private final static String ALGORITHM = "HmacSHA256";
+
+ public HmacSHA256(final byte[] secret) {
+ this(new SecretKeySpec(secret, ALGORITHM));
+ }
+
+ @SneakyThrows
+ public static HmacSHA256 randomInstance() {
+ var secret = new byte[32]; // double the minimum requirement of 16
+ SecureRandom.getInstanceStrong().nextBytes(secret);
+ return new HmacSHA256(secret);
+ }
+
+ @Override
+ public byte[] generateHmac(final byte[] data) {
+ Objects.requireNonNull(data);
+ var mac = newMac();
+ return mac.doFinal(data);
+ }
+
+ @Override
+ public boolean isValidHmacLength(final int hmacLength) {
+ return 32 == hmacLength;
+ }
+
+ // -- HELPER
+
+ @SneakyThrows
+ private Mac newMac() {
+ var mac = Mac.getInstance(ALGORITHM);
+ mac.init(secretKey);
+ return mac;
+ }
+ }
+
+ // JUNIT SUPPORT
+
+ static HmacAuthority forTesting() {
+ return new HmacSHA256("secret for testing onyl".getBytes());
+ }
+}
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/urlencoding/UrlEncodingService.java b/api/applib/src/main/java/org/apache/causeway/applib/services/urlencoding/UrlEncodingService.java
index 70105be3824..41d3acf1f44 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/services/urlencoding/UrlEncodingService.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/urlencoding/UrlEncodingService.java
@@ -22,39 +22,32 @@
import org.apache.causeway.commons.internal.base._Bytes;
import org.apache.causeway.commons.internal.base._Strings;
-import org.apache.causeway.commons.internal.memento._Mementos.EncoderDecoder;
/**
* Defines a consistent way to convert strings to/from a form safe for use
* within a URL.
*
- * <p>
- * The service is used by the framework to map mementos (derived from the
- * state of the view model itself) into a form that can be used as a view
- * model. When the framework needs to recreate the view model (for example
- * to invoke an action on it), this URL is converted back into a view model
- * memento, from which the view model can be hydrated.
- * </p>
+ * <p>The service is used by the framework to map mementos (derived from the
+ * state of the view model itself) into a form that can be used as a view
+ * model. When the framework needs to recreate the view model (for example
+ * to invoke an action on it), this URL is converted back into a view model
+ * memento, from which the view model can be hydrated.
*
* @since 1.x {@index}
*/
-public interface UrlEncodingService extends EncoderDecoder {
+public interface UrlEncodingService {
/**
- * Converts the string (eg view model memento) into a string safe for use
+ * Converts given data bytes (eg view model memento) into a string safe for use
* within an URL
*/
- @Override
String encode(final byte[] bytes);
/**
- * Unconverts the string from its URL form into its original form URL.
+ * Converts the plain URL safe string back to its original data bytes.
*
- * <p>
- * Reciprocal of {@link #encode(byte[])}.
- * </p>
+ * <p>Inverse of {@link #encode(byte[])}.
*/
- @Override
byte[] decode(String str);
default String encodeString(final String str) {
@@ -101,7 +94,6 @@ public byte[] decode(final String str) {
return _Bytes.ofUrlBase64.apply(_Strings.toBytes(str, StandardCharsets.UTF_8));
}
};
-
}
}
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/Converter.java b/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/Converter.java
index 40974b26014..c108b4ed889 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/Converter.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/Converter.java
@@ -18,8 +18,6 @@
*/
package org.apache.causeway.applib.value.semantics;
-import org.apache.causeway.commons.internal.memento._Mementos.EncoderDecoder;
-
/**
* Provides forth and back conversion between 2 types.
*
@@ -28,7 +26,6 @@
*
* @see DefaultsProvider
* @see Parser
- * @see EncoderDecoder
* @see ValueSemanticsProvider
*
* @since 2.x {@index}
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/DefaultsProvider.java b/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/DefaultsProvider.java
index aca71505e9d..443e835d3ed 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/DefaultsProvider.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/DefaultsProvider.java
@@ -18,8 +18,6 @@
*/
package org.apache.causeway.applib.value.semantics;
-import org.apache.causeway.commons.internal.memento._Mementos.EncoderDecoder;
-
/**
* Provides a mechanism for providing a default value for an object.
*
@@ -40,7 +38,6 @@
* the object reflectively.
*
* @see Parser
- * @see EncoderDecoder
* @see ValueSemanticsProvider
*
* @since 1.x {@index}
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/OrderRelation.java b/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/OrderRelation.java
index feac2561a00..4275a394374 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/OrderRelation.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/OrderRelation.java
@@ -18,8 +18,6 @@
*/
package org.apache.causeway.applib.value.semantics;
-import org.apache.causeway.commons.internal.memento._Mementos.EncoderDecoder;
-
/**
* Provides an ordering relation for a given value-type.
* <p>
@@ -36,7 +34,6 @@
*
* @see DefaultsProvider
* @see Parser
- * @see EncoderDecoder
* @see ValueSemanticsProvider
*
* @since 2.x {@index}
diff --git a/commons/src/main/java/module-info.java b/commons/src/main/java/module-info.java
index 1da63b2ab4a..7b19feb18c3 100644
--- a/commons/src/main/java/module-info.java
+++ b/commons/src/main/java/module-info.java
@@ -50,7 +50,6 @@
exports org.apache.causeway.commons.internal.html;
exports org.apache.causeway.commons.internal.image;
exports org.apache.causeway.commons.internal.ioc;
- exports org.apache.causeway.commons.internal.memento;
exports org.apache.causeway.commons.internal.os;
exports org.apache.causeway.commons.internal.primitives;
exports org.apache.causeway.commons.internal.proxy;
diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/base/_Bytes.java b/commons/src/main/java/org/apache/causeway/commons/internal/base/_Bytes.java
index 451b8c0b854..8e1d5a47714 100644
--- a/commons/src/main/java/org/apache/causeway/commons/internal/base/_Bytes.java
+++ b/commons/src/main/java/org/apache/causeway/commons/internal/base/_Bytes.java
@@ -195,6 +195,12 @@ public static byte[] ofHexDump(final @Nullable String hexDump) {
return ofHexDump(hexDump, " ");
}
+ // -- NULLABLE
+
+ public static byte[] nullToEmpty(final byte[] bytes) {
+ return bytes!=null ? bytes : new byte[0];
+ }
+
// -- PREPEND/APPEND
/**
diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/memento/_MementoDefault.java b/commons/src/main/java/org/apache/causeway/commons/internal/memento/_MementoDefault.java
deleted file mode 100644
index 5afd2bb40df..00000000000
--- a/commons/src/main/java/org/apache/causeway/commons/internal/memento/_MementoDefault.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.causeway.commons.internal.memento;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.io.ObjectStreamClass;
-import java.io.Serializable;
-import java.util.HashMap;
-import java.util.Set;
-
-import org.jspecify.annotations.Nullable;
-
-import org.apache.causeway.commons.internal.base._Casts;
-import org.apache.causeway.commons.internal.base._NullSafe;
-import org.apache.causeway.commons.internal.base._Strings;
-import org.apache.causeway.commons.internal.collections._Maps;
-import org.apache.causeway.commons.internal.collections._Sets;
-import org.apache.causeway.commons.internal.context._Context;
-import org.apache.causeway.commons.internal.exceptions._Exceptions;
-import org.apache.causeway.commons.internal.memento._Mementos.EncoderDecoder;
-import org.apache.causeway.commons.internal.memento._Mementos.Memento;
-import org.apache.causeway.commons.internal.memento._Mementos.SerializingAdapter;
-
-import org.jspecify.annotations.NonNull;
-
-/**
- * package private helper for utility class {@link _Mementos}
- *
- * Memento default implementation.
- */
-record _MementoDefault(
- @NonNull EncoderDecoder codec,
- @NonNull SerializingAdapter serializer,
- // we need a Serializable Map
- @NonNull HashMap<String, Serializable> valuesByKey
- ) implements _Mementos.Memento {
-
- _MementoDefault(final EncoderDecoder codec, final SerializingAdapter serializer) {
- this(codec, serializer, _Maps.newHashMap());
- }
-
- @Override
- public Memento put(final @NonNull String name, final Object value) {
- if(value==null) {
- return this; //no-op, there is no point in storing null values
- }
- valuesByKey.put(name, serializer.write(value));
- return this;
- }
-
- @Override
- public <T> T get(final String name, final Class<T> cls) {
- final Serializable value = valuesByKey.get(name);
- if(value==null) {
- return null;
- }
- return serializer.read(cls, value);
- }
-
- @Override
- public Set<String> keySet() {
- return _Sets.unmodifiable(valuesByKey.keySet());
- }
-
- @Override
- public String asString() {
- final ByteArrayOutputStream os = new ByteArrayOutputStream(16*1024); // 16k initial size
- try(ObjectOutputStream oos = new ObjectOutputStream(os)){
- oos.writeObject(valuesByKey); // write the entire map to the byte-buffer
- } catch (Exception e) {
- throw new IllegalArgumentException("failed to serialize memento", e);
- }
- return codec.encode(os.toByteArray()); // convert bytes from byte-buffer to encoded string
- }
-
- // -- PARSER
-
- static Memento parse(
- final @NonNull EncoderDecoder codec,
- final SerializingAdapter serializer,
- final @Nullable String str) {
- if(_NullSafe.isEmpty(str)) {
- return null;
- }
- try(ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(codec.decode(str))) {
- //override ObjectInputStream's class-loading
- @Override
- protected Class<?> resolveClass(ObjectStreamClass desc)
- throws IOException, ClassNotFoundException
- {
- String name = desc.getName();
- return Class.forName(name, false, _Context.getDefaultClassLoader());
- }
- }) {
- // read in the entire map
- final HashMap<String, Serializable> valuesByKey = _Casts.uncheckedCast(ois.readObject());
- return new _MementoDefault(codec, serializer, valuesByKey);
- } catch (Exception e) {
- throw _Exceptions.illegalArgument(e,
- "failed to parse memento from serialized string '%s'",
- _Strings.ellipsifyAtEnd(str, 200, "..."));
- }
- }
-
-}
diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/memento/_Mementos.java b/commons/src/main/java/org/apache/causeway/commons/internal/memento/_Mementos.java
deleted file mode 100644
index 2bad8cfe42b..00000000000
--- a/commons/src/main/java/org/apache/causeway/commons/internal/memento/_Mementos.java
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.causeway.commons.internal.memento;
-
-import java.io.ObjectInput;
-import java.io.ObjectOutput;
-import java.io.Serializable;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-
-import org.apache.causeway.commons.internal.base._Strings;
-import org.apache.causeway.commons.internal.exceptions._Exceptions;
-
-import org.jspecify.annotations.NonNull;
-
-/**
- * <h1>- internal use only -</h1>
- * <p>
- * Provides framework internal memento support.
- * </p>
- * <p>
- * <b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this package! <br/>
- * These may be changed or removed without notice!
- * </p>
- *
- * @since 2.0
- */
-public final class _Mementos {
-
- private _Mementos(){}
-
- // -- ENCODE-DECODER INTERFACE
-
- public static interface EncoderDecoder {
- public String encode(final byte[] bytes);
- public byte[] decode(String str);
- }
-
- // -- MEMENTO INTERFACE
-
- /**
- * Similar to a {@link Map}<String, Object> for key/value pairs,
- * but in addition allows to-String <em>serialization</em> and
- * from-String <em>de-serialization</em> of the entire map.
- */
- public static interface Memento {
-
- /**
- * Returns the Object associated with {@code name}
- * @param name
- * @param cls the expected type which to cast the retrieved value to (required)
- */
- public <T> T get(String name, Class<T> cls);
-
- /**
- * Behaves like a {@link HashMap}, but returns the Memento itself.
- * @param name
- * @param value
- * @return self
- */
- public Memento put(String name, Object value);
-
- /**
- * @return an unmodifiable key-set of this map
- */
- public Set<String> keySet();
-
- /**
- * @return to-String <em>serialization</em> of this map
- */
- public String asString();
- }
-
- // -- SERIALIZER INTERFACE
-
- /**
- * Coder/Decoder from {@link Object} to {@link Serializable}
- */
- public static interface SerializingAdapter {
-
- /**
- * Converts the value into a {@link Serializable} that is write-able to an {@link ObjectOutput}.<br/>
- * Note: write and read are complementary operators.
- * @param value
- */
- public Serializable write(@NonNull Object value);
-
- /**
- * Converts the {@link Serializable} {@code value} as read from an {@link ObjectInput} back into its
- * original (typically a Pojo).<br/>
- * Note: write and read are complementary operators.
- * @param cls the expected type which to cast the {@code value} to (required)
- * @param value
- */
- public <T> T read(@NonNull Class<T> cls, @NonNull Serializable value);
- }
-
- // -- MEMENTO CONSTRUCTION
-
- /**
- * Creates an empty {@link Memento}.
- *
- * <p>
- * Typically followed by {@link Memento#put(String, Object)} for each of the data values to
- * add to the {@link Memento}, then {@link Memento#asString()} to convert to a string format.
- * </p>
- *
- * @param codec (required)
- * @param serializer (required)
- * @return non-null
- */
- public static Memento create(final EncoderDecoder codec, final SerializingAdapter serializer) {
- return new _MementoDefault(codec, serializer);
- }
-
- /**
- * Parse string returned from {@link Memento#asString()}
- *
- * <p>
- * Typically followed by {@link Memento#get(String, Class)} for each of the data values held
- * in the {@link Memento}.
- * </p>
- *
- * @param codec (required)
- * @param serializer (required)
- * @param input
- * @return {@code empty()} if {@code input} is empty
- *
- * @throws IllegalArgumentException if parsing fails
- *
- */
- public static Memento parse(
- final EncoderDecoder codec,
- final SerializingAdapter serializer,
- final String input) {
-
- if(_Strings.isNullOrEmpty(input)) {
- return empty();
- }
- return _MementoDefault.parse(codec, serializer, input);
- }
-
- // -- EMPTY MEMENTO
-
- private static final class EmptyMemento implements Memento {
-
- @Override
- public <T> T get(final String name, final Class<T> cls) {
- return null;
- }
-
- @Override
- public Memento put(final String name, final Object value) {
- throw _Exceptions.notImplemented();
- }
-
- @Override
- public Set<String> keySet() {
- return Collections.emptySet();
- }
-
- @Override
- public String asString() {
- return "EmptyMemento";
- }
-
- }
-
- private static final Memento EMPTY_MEMENTO = new EmptyMemento();
-
- public static Memento empty() {
- return EMPTY_MEMENTO;
- }
-
-}
diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/memento/package-info.java b/commons/src/main/java/org/apache/causeway/commons/internal/memento/package-info.java
deleted file mode 100644
index 3246904976f..00000000000
--- a/commons/src/main/java/org/apache/causeway/commons/internal/memento/package-info.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-/**
- * <h1>Internal API</h1>
- * Internal classes, contributing to the internal proprietary API.
- * These may be changed or removed without notice!
- * <p>
- * <b>WARNING</b>:
- * Do NOT use any of the classes provided by this package!
- * </p>
- */
-package org.apache.causeway.commons.internal.memento;
\ No newline at end of file
diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/resources/_Serializables.java b/commons/src/main/java/org/apache/causeway/commons/internal/resources/_Serializables.java
index b623f79f1f5..5048c58ce18 100644
--- a/commons/src/main/java/org/apache/causeway/commons/internal/resources/_Serializables.java
+++ b/commons/src/main/java/org/apache/causeway/commons/internal/resources/_Serializables.java
@@ -20,32 +20,38 @@
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
+import java.io.ObjectStreamClass;
import java.io.Serializable;
+import org.jspecify.annotations.NonNull;
+
import org.apache.causeway.commons.internal.base._Casts;
import org.apache.causeway.commons.internal.exceptions._Exceptions;
-import org.jspecify.annotations.NonNull;
import lombok.SneakyThrows;
+import lombok.experimental.UtilityClass;
/**
* <h1>- internal use only -</h1>
- * <p>
- * Utilities for marshalling Serializable.
- * </p>
- * <p>
- * <b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this package! <br/>
+ * <p>Utilities for marshalling {@link Serializable}.
+ *
+ * <p><b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this package! <br/>
* These may be changed or removed without notice!
- * </p>
+ *
+ * @apiNote Every code path within the framework, that requires {@link java.io.ObjectInputStream#readObject()}
+ * should use this utility class to access it indirectly. This allows for easier security related code reviews.
+ *
* @since 2.0
*/
+@UtilityClass
public class _Serializables {
@SneakyThrows
- public static byte[] write(
+ public byte[] write(
final @NonNull Serializable object) {
var bos = new ByteArrayOutputStream(16*4096); // 16k initial buffer size
try(var oos = new ObjectOutputStream(bos)) {
@@ -53,14 +59,18 @@ public static byte[] write(
oos.flush();
}
return bos.toByteArray();
-
}
+ /**
+ * This utility uses Java Object Serialization, which allows
+ * arbitrary code to be run and is known for being the source of many Remote
+ * Code Execution (RCE) vulnerabilities.
+ */
@SneakyThrows
- public static <T extends Serializable> T read(
+ public <T extends Serializable> T read(
final @NonNull Class<T> requiredClass,
- final @NonNull InputStream content) {
- try(var ois = new ObjectInputStream(content)){
+ final @NonNull InputStream trustedContent) {
+ try(var ois = new ObjectInputStream(trustedContent)){
var pojo = ois.readObject();
if(!(requiredClass.isAssignableFrom(pojo.getClass()))) {
throw _Exceptions.unrecoverable(
@@ -71,13 +81,44 @@ public static <T extends Serializable> T read(
}
}
+ /**
+ * This utility uses Java Object Serialization, which allows
+ * arbitrary code to be run and is known for being the source of many Remote
+ * Code Execution (RCE) vulnerabilities.
+ */
@SneakyThrows
- public static <T extends Serializable> T read(
+ public <T extends Serializable> T read(
final @NonNull Class<T> requiredClass,
- final @NonNull byte[] input) {
- try(var bis = new ByteArrayInputStream(input)) {
+ final @NonNull byte[] trustedBytes) {
+ try(var bis = new ByteArrayInputStream(trustedBytes)) {
return read(requiredClass, bis);
}
}
+ /**
+ * This utility uses Java Object Serialization, which allows
+ * arbitrary code to be run and is known for being the source of many Remote
+ * Code Execution (RCE) vulnerabilities.
+ */
+ @SneakyThrows
+ public <T extends Serializable> T readWithCustomClassLoader(
+ final @NonNull Class<T> requiredClass,
+ final @NonNull ClassLoader classLoader,
+ final @NonNull byte[] trustedBytes) {
+ try(ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(trustedBytes)) {
+ @Override
+ protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
+ return Class.forName(desc.getName(), false, classLoader);
+ }
+ }) {
+ var pojo = ois.readObject();
+ if(!(requiredClass.isAssignableFrom(pojo.getClass()))) {
+ throw _Exceptions.unrecoverable(
+ "de-serializion of input stream did not yield an object of required type %s",
+ requiredClass.getName());
+ }
+ return _Casts.uncheckedCast(pojo);
+ }
+ }
+
}
diff --git a/core/metamodel/src/main/java/module-info.java b/core/metamodel/src/main/java/module-info.java
index 49ed9a4f34a..a5d00a19d97 100644
--- a/core/metamodel/src/main/java/module-info.java
+++ b/core/metamodel/src/main/java/module-info.java
@@ -130,6 +130,7 @@
exports org.apache.causeway.core.metamodel.services.grid.spi;
exports org.apache.causeway.core.metamodel.specloader.validator;
exports org.apache.causeway.core.metamodel.util;
+ exports org.apache.causeway.core.metamodel.util.hmac;
exports org.apache.causeway.core.metamodel.util.pchain;
exports org.apache.causeway.core.metamodel.util.snapshot;
exports org.apache.causeway.core.metamodel.valuesemantics;
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java
index ed3a517e9ff..51e2202f8fa 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java
@@ -19,6 +19,7 @@
package org.apache.causeway.core.metamodel;
import java.util.List;
+import java.util.Objects;
import java.util.stream.Stream;
import jakarta.inject.Provider;
@@ -34,8 +35,10 @@
import org.apache.causeway.applib.graph.tree.TreeAdapter;
import org.apache.causeway.applib.layout.resource.LayoutResourceLoader;
import org.apache.causeway.applib.services.appfeat.ApplicationFeatureSort;
+import org.apache.causeway.applib.services.bookmark.BookmarkService;
import org.apache.causeway.applib.services.grid.GridMarshaller;
import org.apache.causeway.applib.services.message.MessageService;
+import org.apache.causeway.applib.value.semantics.ValueSemanticsResolver;
import org.apache.causeway.commons.functional.Either;
import org.apache.causeway.commons.functional.Railway;
import org.apache.causeway.commons.functional.Try;
@@ -84,7 +87,6 @@
import org.apache.causeway.core.metamodel.valuesemantics.CommandDtoValueSemantics;
import org.apache.causeway.core.metamodel.valuesemantics.DoubleValueSemantics;
import org.apache.causeway.core.metamodel.valuesemantics.FloatValueSemantics;
-import org.apache.causeway.core.metamodel.valuesemantics.IdStringifierForSerializable;
import org.apache.causeway.core.metamodel.valuesemantics.IntValueSemantics;
import org.apache.causeway.core.metamodel.valuesemantics.InteractionDtoValueSemantics;
import org.apache.causeway.core.metamodel.valuesemantics.LocalResourcePathValueSemantics;
@@ -99,6 +101,7 @@
import org.apache.causeway.core.metamodel.valuesemantics.TreePathValueSemantics;
import org.apache.causeway.core.metamodel.valuesemantics.URLValueSemantics;
import org.apache.causeway.core.metamodel.valuesemantics.UUIDValueSemantics;
+import org.apache.causeway.core.metamodel.valuesemantics.ValueCodec;
import org.apache.causeway.core.metamodel.valuesemantics.temporal.LocalDateTimeValueSemantics;
import org.apache.causeway.core.metamodel.valuesemantics.temporal.LocalDateValueSemantics;
import org.apache.causeway.core.metamodel.valuesemantics.temporal.LocalTimeValueSemantics;
@@ -174,8 +177,6 @@
JavaUtilDateValueSemantics.class,
// Value Semantics (meta-model)
ApplicationFeatureIdValueSemantics.class,
- // fallback IdStringifier
- IdStringifierForSerializable.class,
// @Service's
ColumnOrderTxtFileServiceDefault.class,
@@ -239,15 +240,21 @@ public PreloadableTypes collectionTypes() {
@Bean
public PreloadableTypes treeAdapterTypes() {
- return ()->Stream.of(
- TreeAdapter.class);
+ return ()->Stream.of(TreeAdapter.class);
}
@Bean
public PreloadableTypes metamodelViewTypes() {
- return ()->Stream.of(
- MetamodelInspectView.class
- );
+ return ()->Stream.of(MetamodelInspectView.class);
+ }
+
+ @Bean
+ public ValueCodec valueCodec(
+ final BookmarkService bookmarkService,
+ final Provider<ValueSemanticsResolver> valueSemanticsResolverProvider) {
+ Objects.requireNonNull(bookmarkService);
+ Objects.requireNonNull(valueSemanticsResolverProvider);
+ return new ValueCodec(bookmarkService, valueSemanticsResolverProvider);
}
}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactory.java
index e7e7a16c5c5..dfb6fef4e2b 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactory.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactory.java
@@ -20,6 +20,7 @@
import java.lang.reflect.Modifier;
import java.util.List;
+import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -46,6 +47,8 @@
import org.apache.causeway.applib.events.lifecycle.ObjectUpdatedEvent;
import org.apache.causeway.applib.events.lifecycle.ObjectUpdatingEvent;
import org.apache.causeway.applib.id.LogicalType;
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
+import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.commons.internal.collections._Multimaps;
import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod;
@@ -80,6 +83,9 @@
import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
import org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidatorAbstract;
import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;
+import org.apache.causeway.core.metamodel.util.hmac.HmacUrlCodec;
+import org.apache.causeway.core.metamodel.util.hmac.MementoHmacContext;
+import org.apache.causeway.core.metamodel.valuesemantics.ValueCodec;
import static org.apache.causeway.commons.internal.base._NullSafe.stream;
@@ -95,10 +101,24 @@ public class DomainObjectAnnotationFacetFactory
private final MetaModelValidatorForMixinTypes mixinTypeValidator =
new MetaModelValidatorForMixinTypes("@DomainObject#nature=MIXIN");
- @Inject
+ // self-managed injection point resolving via constructor ..
+ @Inject HmacAuthority hmacAuthority;
+ @Inject UrlEncodingService urlCodec;
+ @Inject ValueCodec valueCodec;
+
+ private final MementoHmacContext mementoContext;
+
public DomainObjectAnnotationFacetFactory(
final MetaModelContext mmc) {
super(mmc, FeatureType.OBJECTS_ONLY);
+
+ mmc.getServiceInjector().injectServicesInto(this);
+ Objects.requireNonNull(hmacAuthority);
+ Objects.requireNonNull(urlCodec);
+ Objects.requireNonNull(valueCodec);
+
+ this.mementoContext = new MementoHmacContext(
+ new HmacUrlCodec(hmacAuthority, urlCodec), valueCodec);
}
@Override
@@ -332,6 +352,7 @@ void processNature(
ViewModelFacetForDomainObjectAnnotation
.create(
domainObjectIfAny,
+ mementoContext,
facetHolder))
.isPresent()) {
return;
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/SecureViewModelFacet.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/SecureViewModelFacet.java
new file mode 100644
index 00000000000..80f77472844
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/SecureViewModelFacet.java
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.core.metamodel.facets.object.viewmodel;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.util.StringUtils;
+
+import org.apache.causeway.applib.exceptions.unrecoverable.DigitalVerificationException;
+import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.apache.causeway.commons.internal.assertions._Assert;
+import org.apache.causeway.commons.internal.reflection._ClassCache;
+import org.apache.causeway.core.metamodel.commons.CanonicalInvoker;
+import org.apache.causeway.core.metamodel.commons.ClassExtensions;
+import org.apache.causeway.core.metamodel.facetapi.Facet;
+import org.apache.causeway.core.metamodel.facetapi.FacetAbstract;
+import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.util.hmac.HmacUrlCodec;
+
+sealed abstract class SecureViewModelFacet
+extends FacetAbstract implements ViewModelFacet
+permits
+ ViewModelFacetForDomainObjectAnnotation,
+ ViewModelFacetForJavaRecord,
+ ViewModelFacetForSerializableInterface,
+ ViewModelFacetForViewModelInterface,
+ ViewModelFacetForXmlRootElementAnnotation {
+
+ private static final Class<? extends Facet> type() {
+ return ViewModelFacet.class;
+ }
+
+ private final HmacUrlCodec hmacUrlCodec;
+
+ protected SecureViewModelFacet(
+ final HmacUrlCodec hmacUrlCodec,
+ final FacetHolder holder) {
+ super(type(), holder);
+ this.hmacUrlCodec = Objects.requireNonNull(hmacUrlCodec);
+ }
+
+ protected SecureViewModelFacet(
+ final HmacUrlCodec hmacUrlCodec,
+ final FacetHolder holder,
+ final Facet.Precedence precedence) {
+ super(type(), holder, precedence);
+ this.hmacUrlCodec = Objects.requireNonNull(hmacUrlCodec);
+ }
+
+ @Override
+ public final ManagedObject instantiate(
+ final ObjectSpecification viewmodelSpec,
+ final Optional<Bookmark> untrustedBookmarkOpt) {
+
+ Objects.requireNonNull(viewmodelSpec);
+
+ if(untrustedBookmarkOpt==null || untrustedBookmarkOpt.isEmpty()) {
+ // this code path needs NO bookmark checking
+
+ var viewModel = createViewmodel(viewmodelSpec);
+ initialize(viewModel.getPojo());
+
+ viewModel.getBookmark(); // trigger bookmark memoization, if not memoized already
+
+ _Assert.assertTrue(viewModel.isBookmarkMemoized(),
+ ()->"Framework Bug: Viewmodel should have its bookmark memoized once initialized.");
+ return viewModel;
+ }
+
+ // this code path needs untrusted bookmark checking ..
+
+ var untrustedBookmark = untrustedBookmarkOpt.orElseThrow();
+
+ byte[] trustedBookmarkIdAsBytes = Optional.ofNullable(untrustedBookmark.identifier())
+ .filter(StringUtils::hasText)
+ .map(untrustedBookmarkId->hmacUrlCodec.decodeFromUrl(untrustedBookmarkId).orElse(null))
+ .orElse(null);
+
+ if(trustedBookmarkIdAsBytes==null) {
+ // verification failed
+ throw new DigitalVerificationException("invalid request for " + viewmodelSpec.logicalTypeName());
+ }
+
+ var viewModel = ManagedObject.bookmarked(
+ viewmodelSpec,
+ createViewmodelPojo(viewmodelSpec, trustedBookmarkIdAsBytes),
+ untrustedBookmark /* now trusted */);
+
+ initialize(viewModel.getPojo());
+
+ viewModel.getBookmark(); // trigger bookmark memoization, if not memoized already
+
+ _Assert.assertTrue(viewModel.isBookmarkMemoized(),
+ ()->"Framework Bug: Viewmodel should have its bookmark memoized once initialized.");
+ return viewModel;
+ }
+
+ @Override
+ public final void initialize(final @Nullable Object pojo) {
+ if(pojo==null) return;
+ getServiceInjector().injectServicesInto(pojo);
+ invokePostConstructMethod(pojo);
+ }
+
+ @Override
+ public final Bookmark serializeToBookmark(final @NonNull ManagedObject managedObject) {
+ var digitallySignedBookmarId = hmacUrlCodec.encodeForUrl(encodeState(managedObject));
+ return managedObject.createBookmark(digitallySignedBookmarId);
+ }
+
+ // -- ABSTRACT
+
+ /**
+ * Create viewmodel instance from given validated viewmodel state data.
+ */
+ protected abstract Object createViewmodelPojo(
+ @NonNull ObjectSpecification viewmodelSpec,
+ @NonNull byte[] trustedViewmodelState);
+
+ /**
+ * Encodes given viewmodel's state into a byte array for further processing
+ * (digital signing and url-safe encoding).
+ *
+ * <p> The resulting byte array is eventually fed into {@link #createViewmodelPojo(ObjectSpecification, byte[])}
+ * for re-creaton of the viewmodel instance.
+ */
+ protected abstract @NonNull byte[] encodeState(
+ @NonNull ManagedObject viewmodel);
+
+ /**
+ * Create default viewmodel instance (without any {@link Bookmark} available).
+ */
+ protected ManagedObject createViewmodel(final @NonNull ObjectSpecification spec) {
+ return ManagedObject.viewmodel(spec, ClassExtensions.newInstance(spec.getCorrespondingClass()),
+ Optional.empty());
+ }
+
+ // -- HELPER
+
+ private void invokePostConstructMethod(final Object viewModel) {
+ _ClassCache.getInstance().streamPostConstructMethods(viewModel.getClass())
+ .forEach(postConstructMethod->
+ CanonicalInvoker.invoke(postConstructMethod, viewModel));
+ }
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetAbstract.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetAbstract.java
deleted file mode 100644
index dc1425ea964..00000000000
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetAbstract.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.causeway.core.metamodel.facets.object.viewmodel;
-
-import java.util.Optional;
-
-import org.jspecify.annotations.Nullable;
-
-import org.apache.causeway.applib.services.bookmark.Bookmark;
-import org.apache.causeway.commons.internal.assertions._Assert;
-import org.apache.causeway.commons.internal.base._Strings;
-import org.apache.causeway.commons.internal.reflection._ClassCache;
-import org.apache.causeway.core.metamodel.commons.CanonicalInvoker;
-import org.apache.causeway.core.metamodel.commons.ClassExtensions;
-import org.apache.causeway.core.metamodel.facetapi.Facet;
-import org.apache.causeway.core.metamodel.facetapi.FacetAbstract;
-import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
-import org.apache.causeway.core.metamodel.object.ManagedObject;
-import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
-
-import org.jspecify.annotations.NonNull;
-
-public abstract class ViewModelFacetAbstract
-extends FacetAbstract
-implements ViewModelFacet {
-
- private static final Class<? extends Facet> type() {
- return ViewModelFacet.class;
- }
-
- protected ViewModelFacetAbstract(
- final FacetHolder holder) {
- super(type(), holder);
- }
-
- protected ViewModelFacetAbstract(
- final FacetHolder holder,
- final Facet.Precedence precedence) {
- super(type(), holder, precedence);
- }
-
- @Override
- public final ManagedObject instantiate(
- final ObjectSpecification spec,
- final Optional<Bookmark> bookmarkIfAny) {
-
- var bookmark = bookmarkIfAny.orElse(null);
- var isBookmarkAvailable = bookmarkIfAny.map(Bookmark::identifier)
- .map(_Strings::isNotEmpty)
- .orElse(false);
-
- var viewModel = !isBookmarkAvailable
- ? createViewmodel(spec)
- : createViewmodel(spec, bookmark);
-
- initialize(viewModel.getPojo());
-
- viewModel.getBookmark(); // trigger bookmark memoization, if not memoized already
-
- _Assert.assertTrue(viewModel.isBookmarkMemoized(),
- ()->"Framework Bug: Viewmodel should have its bookmark memoized once initialized.");
- return viewModel;
- }
-
- @Override
- public final void initialize(final @Nullable Object pojo) {
- if(pojo==null) return;
- getServiceInjector().injectServicesInto(pojo);
- invokePostConstructMethod(pojo);
- }
-
- /**
- * Create default viewmodel instance (without any {@link Bookmark} available).
- */
- protected ManagedObject createViewmodel(final @NonNull ObjectSpecification spec) {
- return ManagedObject.viewmodel(spec, ClassExtensions.newInstance(spec.getCorrespondingClass()),
- Optional.empty());
- }
-
- /**
- * Create viewmodel instance from a given valid {@link Bookmark}.
- * <p>
- * The resulting {@link ManagedObject} is not required to have its bookmark memoized.
- * We trigger bookmark memoization later in {@link #instantiate(ObjectSpecification, Optional)}.
- */
- protected abstract ManagedObject createViewmodel(
- @NonNull ObjectSpecification spec,
- @NonNull Bookmark bookmark);
-
- private void invokePostConstructMethod(final Object viewModel) {
- _ClassCache.getInstance().streamPostConstructMethods(viewModel.getClass())
- .forEach(postConstructMethod->
- CanonicalInvoker.invoke(postConstructMethod, viewModel));
- }
-
- @Override
- public final Bookmark serializeToBookmark(final @NonNull ManagedObject managedObject) {
- return managedObject.createBookmark(serialize(managedObject));
- }
-
- protected abstract @NonNull String serialize(@NonNull ManagedObject managedObject);
-
-}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetFactory.java
index 06aa4ccaa13..99185f20688 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetFactory.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetFactory.java
@@ -18,8 +18,13 @@
*/
package org.apache.causeway.core.metamodel.facets.object.viewmodel;
+import java.util.Objects;
+
import jakarta.inject.Inject;
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
+import org.apache.causeway.applib.services.jaxb.JaxbService;
+import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
import org.apache.causeway.commons.internal.reflection._ClassCache;
import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants;
import org.apache.causeway.core.metamodel.context.MetaModelContext;
@@ -29,16 +34,37 @@
import org.apache.causeway.core.metamodel.facets.FacetFactoryAbstract;
import org.apache.causeway.core.metamodel.progmodel.ProgrammingModel;
import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;
+import org.apache.causeway.core.metamodel.util.hmac.HmacUrlCodec;
+import org.apache.causeway.core.metamodel.util.hmac.MementoHmacContext;
+import org.apache.causeway.core.metamodel.valuesemantics.ValueCodec;
public class ViewModelFacetFactory
extends FacetFactoryAbstract
implements
MetaModelRefiner {
- @Inject
+ // self-managed injection point resolving via constructor ..
+ @Inject HmacAuthority hmacAuthority;
+ @Inject UrlEncodingService urlCodec;
+ @Inject JaxbService jaxbService;
+ @Inject ValueCodec valueCodec;
+
+ private final HmacUrlCodec hmacUrlCodec;
+ private final MementoHmacContext mementoHmacContext;
+
public ViewModelFacetFactory(
final MetaModelContext mmc) {
super(mmc, FeatureType.OBJECTS_ONLY);
+
+ mmc.getServiceInjector().injectServicesInto(this);
+ Objects.requireNonNull(hmacAuthority);
+ Objects.requireNonNull(urlCodec);
+
+ Objects.requireNonNull(jaxbService);
+ Objects.requireNonNull(valueCodec);
+
+ this.hmacUrlCodec = new HmacUrlCodec(hmacAuthority, urlCodec);
+ this.mementoHmacContext = new MementoHmacContext(hmacUrlCodec, valueCodec);
}
/**
@@ -58,23 +84,23 @@ public void process(final ProcessClassContext processClassContext) {
FacetUtil
.addFacetIfPresent(
ViewModelFacetForXmlRootElementAnnotation
- .create(hasXmlRootElementAnnotation, facetHolder));
+ .create(hasXmlRootElementAnnotation, hmacUrlCodec, jaxbService, facetHolder));
// (with high precedence)
FacetUtil
.addFacetIfPresent(
// either ViewModel interface (highest precedence)
- ViewModelFacetForViewModelInterface.create(type, facetHolder)
+ ViewModelFacetForViewModelInterface.create(type, hmacUrlCodec, facetHolder)
// or Serializable interface (if any)
- .or(()->ViewModelFacetForSerializableInterface.create(type, facetHolder))
+ .or(()->ViewModelFacetForSerializableInterface.create(type, hmacUrlCodec, facetHolder))
// or else Java record (if any)
- .or(()->ViewModelFacetForJavaRecord.create(type, facetHolder))
+ .or(()->ViewModelFacetForJavaRecord.create(type, mementoHmacContext, facetHolder))
);
// DomainObject(nature=VIEW_MODEL) is managed by the DomainObjectAnnotationFacetFactory as a fallback strategy
}
- // //////////////////////////////////////
+ // --
@Override
public void refineProgrammingModel(final ProgrammingModel programmingModel) {
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForDomainObjectAnnotation.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForDomainObjectAnnotation.java
index f5a93d6ec47..65d2e8509a3 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForDomainObjectAnnotation.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForDomainObjectAnnotation.java
@@ -22,13 +22,11 @@
import java.util.Optional;
import java.util.stream.Stream;
+import org.jspecify.annotations.NonNull;
+
import org.apache.causeway.applib.annotation.DomainObject;
-import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.applib.services.metamodel.BeanSort;
-import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
import org.apache.causeway.commons.internal.base._Casts;
-import org.apache.causeway.commons.internal.memento._Mementos;
-import org.apache.causeway.commons.internal.memento._Mementos.SerializingAdapter;
import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
import org.apache.causeway.core.metamodel.facets.properties.update.modify.PropertySetterFacet;
@@ -36,17 +34,19 @@
import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
+import org.apache.causeway.core.metamodel.util.hmac.Memento;
+import org.apache.causeway.core.metamodel.util.hmac.MementoHmacContext;
-import org.jspecify.annotations.NonNull;
-
-public class ViewModelFacetForDomainObjectAnnotation
-extends ViewModelFacetAbstract {
+public final class ViewModelFacetForDomainObjectAnnotation
+extends SecureViewModelFacet {
public static Optional<ViewModelFacetForDomainObjectAnnotation> create(
final Optional<DomainObject> domainObjectIfAny,
+ final MementoHmacContext mementoContext,
final FacetHolder holder) {
- return domainObjectIfAny
+ return mementoContext!=null
+ ? domainObjectIfAny
.map(DomainObject::nature)
.map(nature -> {
switch (nature) {
@@ -70,36 +70,36 @@ public static Optional<ViewModelFacetForDomainObjectAnnotation> create(
}
// else fall through
case VIEW_MODEL:
- return new ViewModelFacetForDomainObjectAnnotation(holder);
+ return new ViewModelFacetForDomainObjectAnnotation(mementoContext, holder);
}
// shouldn't happen, the above switch should match all cases
throw new IllegalArgumentException("nature of '" + nature + "' not recognized");
})
- .filter(Objects::nonNull);
+ .filter(Objects::nonNull)
+ : Optional.empty();
}
- private UrlEncodingService codec;
- private SerializingAdapter serializer;
+ private final MementoHmacContext mementoContext;
protected ViewModelFacetForDomainObjectAnnotation(
- final FacetHolder holder) {
+ final MementoHmacContext mementoContext, final FacetHolder holder) {
// is overruled by any other ViewModelFacet type
- super(holder, Precedence.LOW);
+ super(mementoContext.hmacUrlCodec(), holder, Precedence.LOW);
+ this.mementoContext = mementoContext;
}
@Override
- protected ManagedObject createViewmodel(
+ protected Object createViewmodelPojo(
final @NonNull ObjectSpecification viewmodelSpec,
- final @NonNull Bookmark bookmark) {
+ final @NonNull byte[] trustedBookmarkIdAsBytes) {
- var viewmodel = viewmodelSpec.createObject();
+ // throws on de-marshalling failure
+ var memento = mementoContext.parseTrustedMemento(trustedBookmarkIdAsBytes);
- var memento = parseMemento(bookmark);
+ var viewmodel = viewmodelSpec.createObject();
var mementoKeys = memento.keySet();
- if(mementoKeys.isEmpty()) {
- return viewmodel;
- }
+ if(mementoKeys.isEmpty()) return viewmodel.getPojo();
var objectManager = super.getObjectManager();
@@ -119,13 +119,13 @@ protected ManagedObject createViewmodel(
property.set(viewmodel, propertyValue, InteractionInitiatedBy.PASS_THROUGH);
});
- return viewmodel;
+ return viewmodel.getPojo();
}
@Override
- public String serialize(final ManagedObject viewModel) {
+ public byte[] encodeState(final ManagedObject viewModel) {
- final _Mementos.Memento memento = newMemento();
+ final Memento memento = mementoContext.newMemento();
var viewmodelSpec = viewModel.objSpec();
@@ -141,7 +141,7 @@ public String serialize(final ManagedObject viewModel) {
}
});
- return memento.asString();
+ return memento.stateAsBytes();
}
// -- HELPER
@@ -155,26 +155,4 @@ private Stream<OneToOneAssociation> streamPersistableProperties(
.filter(property->property.isIncludedWithSnapshots());
}
- private void initDependencies() {
- var serviceRegistry = getServiceRegistry();
- this.codec = serviceRegistry.lookupServiceElseFail(UrlEncodingService.class);
- this.serializer = serviceRegistry.lookupServiceElseFail(SerializingAdapter.class);
- }
-
- private void ensureDependenciesInited() {
- if(codec==null) {
- initDependencies();
- }
- }
-
- private _Mementos.Memento newMemento() {
- ensureDependenciesInited();
- return _Mementos.create(codec, serializer);
- }
-
- private _Mementos.Memento parseMemento(final Bookmark bookmark) {
- ensureDependenciesInited();
- return _Mementos.parse(codec, serializer, bookmark.identifier());
- }
-
}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForJavaRecord.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForJavaRecord.java
index 5e8b1234bbf..617d70b3ad0 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForJavaRecord.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForJavaRecord.java
@@ -24,76 +24,73 @@
import java.util.Optional;
import java.util.stream.Stream;
-import org.apache.causeway.applib.services.bookmark.Bookmark;
-import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
+import org.jspecify.annotations.NonNull;
+
import org.apache.causeway.commons.collections.Can;
-import org.apache.causeway.commons.internal.memento._Mementos;
-import org.apache.causeway.commons.internal.memento._Mementos.SerializingAdapter;
import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
import org.apache.causeway.core.metamodel.object.ManagedObject;
import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation;
+import org.apache.causeway.core.metamodel.util.hmac.Memento;
+import org.apache.causeway.core.metamodel.util.hmac.MementoHmacContext;
-import org.jspecify.annotations.NonNull;
import lombok.SneakyThrows;
/**
* @since 3.0.0
*/
-public class ViewModelFacetForJavaRecord
-extends ViewModelFacetAbstract {
+public final class ViewModelFacetForJavaRecord
+extends SecureViewModelFacet {
- public static Optional<ViewModelFacetForJavaRecord> create(
+ static Optional<ViewModelFacetForJavaRecord> create(
final Class<?> cls,
+ final MementoHmacContext mementoContext,
final FacetHolder holder) {
- return cls.isRecord()
- ? Optional.of(new ViewModelFacetForJavaRecord(cls, holder))
- : Optional.empty();
+ return mementoContext!=null
+ && cls.isRecord()
+ ? Optional.of(new ViewModelFacetForJavaRecord(cls, mementoContext, holder))
+ : Optional.empty();
}
- private UrlEncodingService codec;
- private SerializingAdapter serializer;
-
+ private final MementoHmacContext mementoContext;
private final Constructor<?> canonicalConstructor;
protected ViewModelFacetForJavaRecord(
final Class<?> recordClass,
+ final MementoHmacContext mementoContext,
final FacetHolder holder) {
// is overruled by ViewModel interface semantics
- super(holder, Precedence.DEFAULT);
+ super(mementoContext.hmacUrlCodec(), holder, Precedence.DEFAULT);
+ this.mementoContext = mementoContext;
this.canonicalConstructor = canonicalConstructor(recordClass);
}
@Override @SneakyThrows
- protected ManagedObject createViewmodel(
+ protected Object createViewmodelPojo(
final @NonNull ObjectSpecification viewmodelSpec,
- final @NonNull Bookmark bookmark) {
+ final @NonNull byte[] trustedBookmarkIdAsBytes) {
- var memento = parseMemento(bookmark);
+ // throws on de-marshalling failure
+ var memento = mementoContext.parseTrustedMemento(trustedBookmarkIdAsBytes);
var recordComponentPojos = streamRecordComponents(viewmodelSpec)
.map(association->{
- var associationId = association.getId();
- var elementType = association.getElementType();
- var elementClass = elementType.getCorrespondingClass();
var associationPojo = association.isProperty()
- ? memento.get(associationId, elementClass)
- //TODO collection values not yet supported by memento (as workaround use Serializable record)
- : null;
+ ? memento.get(association.getId(), association.getElementType().getCorrespondingClass())
+ //TODO collection values not yet supported by memento (as workaround use Serializable record)
+ : null;
return associationPojo;
}).toArray();
- return ManagedObject.viewmodel(viewmodelSpec,
- canonicalConstructor.newInstance(recordComponentPojos),
- Optional.of(bookmark));
+ return canonicalConstructor.newInstance(recordComponentPojos);
}
@Override
- public String serialize(final ManagedObject viewModel) {
+ public byte[] encodeState(final ManagedObject viewModel) {
- final _Mementos.Memento memento = newMemento();
+ final Memento memento = mementoContext.newMemento();
var viewmodelSpec = viewModel.objSpec();
@@ -111,7 +108,7 @@ public String serialize(final ManagedObject viewModel) {
}
});
- return memento.asString();
+ return memento.stateAsBytes();
}
// -- HELPER
@@ -125,28 +122,6 @@ private Stream<ObjectAssociation> streamRecordComponents(
return recordComponentsAsAssociations.stream();
}
- private void initDependencies() {
- var serviceRegistry = getServiceRegistry();
- this.codec = serviceRegistry.lookupServiceElseFail(UrlEncodingService.class);
- this.serializer = serviceRegistry.lookupServiceElseFail(SerializingAdapter.class);
- }
-
- private void ensureDependenciesInited() {
- if(codec==null) {
- initDependencies();
- }
- }
-
- private _Mementos.Memento newMemento() {
- ensureDependenciesInited();
- return _Mementos.create(codec, serializer);
- }
-
- private _Mementos.Memento parseMemento(final Bookmark bookmark) {
- ensureDependenciesInited();
- return _Mementos.parse(codec, serializer, bookmark.identifier());
- }
-
private static Can<ObjectAssociation> recordComponentsAsAssociations(
final @NonNull ObjectSpecification viewmodelSpec) {
return Arrays.stream(viewmodelSpec.getCorrespondingClass().getRecordComponents())
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForSerializableInterface.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForSerializableInterface.java
index 6f7321006ba..be90964f04c 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForSerializableInterface.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForSerializableInterface.java
@@ -18,79 +18,59 @@
*/
package org.apache.causeway.core.metamodel.facets.object.viewmodel;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
import java.io.Serializable;
-import java.nio.charset.StandardCharsets;
import java.util.Optional;
-import org.apache.causeway.applib.services.bookmark.Bookmark;
-import org.apache.causeway.commons.internal.base._Bytes;
-import org.apache.causeway.commons.internal.base._Strings;
+import org.jspecify.annotations.NonNull;
+
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
+import org.apache.causeway.commons.internal.base._Casts;
+import org.apache.causeway.commons.internal.resources._Serializables;
import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
import org.apache.causeway.core.metamodel.object.ManagedObject;
import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
-
-import org.jspecify.annotations.NonNull;
+import org.apache.causeway.core.metamodel.util.hmac.HmacUrlCodec;
import lombok.SneakyThrows;
/**
* Corresponds to {@link Serializable} interface.
+ *
+ * <p> requires a {@link HmacAuthority}, otherwise disabled.
*/
-public class ViewModelFacetForSerializableInterface
-extends ViewModelFacetAbstract {
+public final class ViewModelFacetForSerializableInterface
+extends SecureViewModelFacet {
- public static Optional<ViewModelFacet> create(
+ static Optional<ViewModelFacet> create(
final Class<?> cls,
+ final HmacUrlCodec hmacUrlCodec,
final FacetHolder holder) {
- return Serializable.class.isAssignableFrom(cls)
- ? Optional.of(new ViewModelFacetForSerializableInterface(holder))
+ return hmacUrlCodec!=null
+ && Serializable.class.isAssignableFrom(cls)
+ ? Optional.of(new ViewModelFacetForSerializableInterface(hmacUrlCodec, holder))
: Optional.empty();
}
protected ViewModelFacetForSerializableInterface(
+ final HmacUrlCodec hmacUrlCodec,
final FacetHolder holder) {
- super(holder, Precedence.HIGH);
+ super(hmacUrlCodec, holder, Precedence.HIGH);
}
@SneakyThrows
@Override
- protected ManagedObject createViewmodel(
+ protected Object createViewmodelPojo(
final @NonNull ObjectSpecification viewmodelSpec,
- final @NonNull Bookmark bookmark) {
- return ManagedObject.bookmarked(
- viewmodelSpec,
- deserialize(viewmodelSpec, bookmark.identifier()),
- bookmark);
- }
+ final @NonNull byte[] trustedBookmarkIdAsBytes) {
- @SneakyThrows
- @Override
- public String serialize(final ManagedObject viewModel) {
- var baos = new ByteArrayOutputStream();
- try(var oos = new ObjectOutputStream(baos)) {
- oos.writeObject(viewModel.getPojo());
- var mementoStr = _Strings.ofBytes(
- _Bytes.asUrlBase64.apply(baos.toByteArray()),
- StandardCharsets.UTF_8);
- return mementoStr;
- }
+ Class<? extends Serializable> expectedType = _Casts.uncheckedCast(viewmodelSpec.getCorrespondingClass());
+ return _Serializables.read(expectedType, trustedBookmarkIdAsBytes);
}
- // -- HELPER
-
@SneakyThrows
- private Object deserialize(
- final @NonNull ObjectSpecification viewmodelSpec,
- final @NonNull String memento) {
- var bytes = _Bytes.ofUrlBase64.apply(_Strings.toBytes(memento, StandardCharsets.UTF_8));
- try(var ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
- var viewModelPojo = ois.readObject();
- return viewModelPojo;
- }
+ @Override
+ public byte[] encodeState(final ManagedObject viewModel) {
+ return _Serializables.write((Serializable) viewModel.getPojo());
}
}
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 c06cc5b9113..bae3845c90a 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
@@ -21,25 +21,25 @@
import java.lang.reflect.Constructor;
import java.net.URLDecoder;
import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.apache.causeway.applib.ViewModel;
-import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.applib.services.registry.ServiceRegistry;
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;
import org.apache.causeway.core.metamodel.object.ManagedObject;
import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;
+import org.apache.causeway.core.metamodel.util.hmac.HmacUrlCodec;
import lombok.Getter;
import org.jspecify.annotations.NonNull;
@@ -50,16 +50,16 @@
/**
* Corresponds to {@link ViewModel} interface.
*/
-public class ViewModelFacetForViewModelInterface
-extends ViewModelFacetAbstract {
+public final class ViewModelFacetForViewModelInterface
+extends SecureViewModelFacet {
public static <T> Optional<ViewModelFacet> create(
final Class<T> cls,
+ final HmacUrlCodec hmacUrlCodec,
final FacetHolder holder) {
- if(!ViewModel.class.isAssignableFrom(cls)) {
- return Optional.empty();
- }
+ if(!ViewModel.class.isAssignableFrom(cls)) return Optional.empty();
+ if(hmacUrlCodec==null) return Optional.empty();
ResolvedConstructor pickedConstructor = null; // not used for abstract types
@@ -103,15 +103,16 @@ public static <T> Optional<ViewModelFacet> create(
}
- return Optional.of(new ViewModelFacetForViewModelInterface(holder, pickedConstructor));
+ return Optional.of(new ViewModelFacetForViewModelInterface(pickedConstructor, hmacUrlCodec, holder));
}
- private ResolvedConstructor constructorAnyArgs;
+ private final ResolvedConstructor constructorAnyArgs;
protected ViewModelFacetForViewModelInterface(
- final FacetHolder holder,
- final @Nullable ResolvedConstructor constructorAnyArgs) {
- super(holder, Precedence.HIGH);
+ final @Nullable ResolvedConstructor constructorAnyArgs,
+ final HmacUrlCodec hmacUrlCodec,
+ final FacetHolder holder) {
+ super(hmacUrlCodec, holder, Precedence.HIGH);
this.constructorAnyArgs = constructorAnyArgs;
}
@@ -126,19 +127,31 @@ protected ManagedObject createViewmodel(
@SneakyThrows
@Override
- protected ManagedObject createViewmodel(
+ protected Object createViewmodelPojo(
final @NonNull ObjectSpecification viewmodelSpec,
- final @NonNull Bookmark bookmark) {
- return ManagedObject.bookmarked(
- viewmodelSpec,
- deserialize(viewmodelSpec, bookmark.identifier()),
- bookmark);
+ final @NonNull byte[] trustedBookmarkIdAsBytes) {
+
+ var mementoDecoded = new String(trustedBookmarkIdAsBytes, StandardCharsets.UTF_8);
+
+ if(SpecialMemento.NULL.matches(mementoDecoded)) {
+ mementoDecoded = null;
+ } else if(SpecialMemento.EMPTY.matches(mementoDecoded)) {
+ mementoDecoded = "";
+ }
+
+ return deserialize(viewmodelSpec, mementoDecoded);
}
@Override
- public String serialize(final ManagedObject viewModel) {
+ public byte[] encodeState(final ManagedObject viewModel) {
final ViewModel viewModelPojo = (ViewModel) viewModel.getPojo();
- return SpecialMemento.encode(viewModelPojo.viewModelMemento());
+ String memento = viewModelPojo.viewModelMemento();
+ if(memento==null) {
+ memento = SpecialMemento.NULL.representationInUrl();
+ } else if(memento.isEmpty()) {
+ memento = SpecialMemento.EMPTY.representationInUrl();
+ }
+ return memento.getBytes(StandardCharsets.UTF_8);
}
// -- HELPER
@@ -153,25 +166,11 @@ public String serialize(final ManagedObject viewModel) {
* hence gets processed by {@link URLEncoder} and {@link URLDecoder},
* which makes it safe for us to use with special meaning
*/
- @Getter @Accessors(fluent=true)
@RequiredArgsConstructor
- enum SpecialMemento {
+ private enum SpecialMemento {
EMPTY("||"),
NULL("|");
- static String encode(final @Nullable String memento) {
- return memento==null
- ? NULL.representationInUrl()
- : memento.isEmpty()
- ? EMPTY.representationInUrl()
- : UrlUtils.urlEncodeUtf8(memento);
- }
- static String decode(final @Nullable String memento) {
- return NULL.matches(memento)
- ? null
- : EMPTY.matches(memento)
- ? ""
- : UrlUtils.urlDecodeUtf8(memento);
- }
+ @Getter @Accessors(fluent=true)
final String representationInUrl;
boolean matches(final String other) {
return representationInUrl.equals(other);
@@ -181,13 +180,12 @@ boolean matches(final String other) {
@SneakyThrows
private Object deserialize(
final @NonNull ObjectSpecification viewmodelSpec,
- final @Nullable String mementoEncoded) {
+ final @Nullable String trustedMemento) {
- _Assert.assertNotNull(constructorAnyArgs, ()->"framework bug: required non-null, "
- + "this can only happen, if we try to deserialize an abstract type");
+ Objects.requireNonNull(constructorAnyArgs, ()->"framework bug: required non-null, "
+ + "this can only happen, if we try to deserialize an abstract type");
- var memento = SpecialMemento.decode(mementoEncoded);
- var resolvedArgs = resolveArgsForConstructor(constructorAnyArgs, getServiceRegistry(), memento);
+ var resolvedArgs = resolveArgsForConstructor(constructorAnyArgs, getServiceRegistry(), trustedMemento);
var viewmodelPojo = constructorAnyArgs.constructor().newInstance(resolvedArgs);
return viewmodelPojo;
}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForXmlRootElementAnnotation.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForXmlRootElementAnnotation.java
index 2960f16c26d..9fef84b58a1 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForXmlRootElementAnnotation.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForXmlRootElementAnnotation.java
@@ -18,83 +18,58 @@
*/
package org.apache.causeway.core.metamodel.facets.object.viewmodel;
+import java.nio.charset.StandardCharsets;
import java.util.Optional;
-import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.jspecify.annotations.NonNull;
+
import org.apache.causeway.applib.services.jaxb.JaxbService;
-import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
-import org.apache.causeway.commons.internal.debug._Debug;
-import org.apache.causeway.commons.internal.debug.xray.XrayUi;
import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
import org.apache.causeway.core.metamodel.object.ManagedObject;
import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.util.hmac.HmacUrlCodec;
-import lombok.Getter;
-import org.jspecify.annotations.NonNull;
-
-public class ViewModelFacetForXmlRootElementAnnotation
-extends ViewModelFacetAbstract {
+public final class ViewModelFacetForXmlRootElementAnnotation
+extends SecureViewModelFacet {
public static Optional<ViewModelFacet> create(
final boolean hasRootElementAnnotation,
+ final HmacUrlCodec hmacUrlCodec,
+ final JaxbService jaxbService,
final FacetHolder facetHolder) {
return hasRootElementAnnotation
- ? Optional.of(new ViewModelFacetForXmlRootElementAnnotation(facetHolder))
- : Optional.empty();
+ && hmacUrlCodec!=null
+ && jaxbService!=null
+ ? Optional.of(new ViewModelFacetForXmlRootElementAnnotation(hmacUrlCodec, jaxbService, facetHolder))
+ : Optional.empty();
}
+ private final JaxbService jaxbService;
+
private ViewModelFacetForXmlRootElementAnnotation(
+ final HmacUrlCodec hmacUrlCodec,
+ final JaxbService jaxbService,
final FacetHolder facetHolder) {
// overruled by other non fallback ViewModelFacet types
- super(facetHolder, Precedence.DEFAULT);
+ super(hmacUrlCodec, facetHolder, Precedence.DEFAULT);
+ this.jaxbService = jaxbService;
}
@Override
- protected ManagedObject createViewmodel(
+ protected Object createViewmodelPojo(
final @NonNull ObjectSpecification viewmodelSpec,
- final @NonNull Bookmark bookmark) {
- final String xmlStr = getUrlEncodingService().decodeToString(bookmark.identifier());
+ final @NonNull byte[] trustedBookmarkIdAsBytes) {
- _Debug.onCondition(XrayUi.isXrayEnabled(), ()->{
- _Debug.log("[JAXB] de-serializing viewmodel %s\n"
- + "--- XML ---\n"
- + "%s"
- + "-----------\n",
- viewmodelSpec.logicalTypeName(),
- xmlStr);
- });
-
- var viewmodelPojo = getJaxbService().fromXml(viewmodelSpec.getCorrespondingClass(), xmlStr);
- return viewmodelPojo!=null
- ? ManagedObject.bookmarked(viewmodelSpec, viewmodelPojo, bookmark)
- : ManagedObject.empty(viewmodelSpec);
+ var trustedXml = new String(trustedBookmarkIdAsBytes, StandardCharsets.UTF_8);
+ var viewmodelPojo = jaxbService.fromXml(viewmodelSpec.getCorrespondingClass(), trustedXml);
+ return viewmodelPojo;
}
@Override
- protected String serialize(final ManagedObject managedObject) {
-
- final String xml = getJaxbService().toXml(managedObject.getPojo());
- final String encoded = getUrlEncodingService().encodeString(xml);
- _Debug.onCondition(XrayUi.isXrayEnabled(), ()->{
- _Debug.log("[JAXB] serializing viewmodel %s\n"
- + "--- XML ---\n"
- + "%s"
- + "-----------\n",
- managedObject.objSpec().logicalTypeName(),
- xml);
- });
- return encoded;
+ protected byte[] encodeState(final ManagedObject managedObject) {
+ final String xml = jaxbService.toXml(managedObject.getPojo());
+ return xml.getBytes(StandardCharsets.UTF_8);
}
- // -- DEPENDENCIES
-
- @Getter(lazy=true)
- private final JaxbService jaxbService =
- getServiceRegistry().lookupServiceElseFail(JaxbService.class);
-
- @Getter(lazy=true)
- private final UrlEncodingService urlEncodingService =
- getServiceRegistry().lookupServiceElseFail(UrlEncodingService.class);
-
}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/HmacUrlCodec.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/HmacUrlCodec.java
new file mode 100644
index 00000000000..3959c617cd8
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/HmacUrlCodec.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.core.metamodel.util.hmac;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.jspecify.annotations.NonNull;
+
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
+import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
+
+/**
+ * Thread-safe coder/decoder with digital signing and validity verification support.
+ *
+ * <p> can be used as an application scoped singleton
+ *
+ * @since 3.5
+ */
+public record HmacUrlCodec(
+ HmacAuthority hmacAuthority,
+ UrlEncodingService urlSafeCodec) {
+
+ public HmacUrlCodec {
+ Objects.requireNonNull(hmacAuthority);
+ Objects.requireNonNull(urlSafeCodec);
+ }
+
+ public String encodeForUrl(final @NonNull byte[] byteArray) {
+ var digitallySignedPayload = HmacUtils.digitallySign(hmacAuthority, byteArray);
+ return urlSafeCodec.encode(digitallySignedPayload);
+ }
+
+ public Optional<byte[]> decodeFromUrl(final @NonNull String untrustedUrlEncodedString) {
+ var trustedBytes = HmacUtils.verify(hmacAuthority, urlSafeCodec.decode(untrustedUrlEncodedString));
+ return Optional.ofNullable(trustedBytes);
+ }
+
+ // -- STRING SUPPORT
+
+ public String encodeForUrlAsUtf8(final @NonNull String string) {
+ return encodeForUrl(string.getBytes(StandardCharsets.UTF_8));
+ }
+
+ public Optional<String> decodeFromUrlAsUtf8(final @NonNull String untrustedUrlEncodedString) {
+ return decodeFromUrl(untrustedUrlEncodedString)
+ .map(trustedBytes->new String(trustedBytes, StandardCharsets.UTF_8));
+ }
+
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/HmacUtils.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/HmacUtils.java
new file mode 100644
index 00000000000..2803243b331
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/HmacUtils.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.core.metamodel.util.hmac;
+
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+import org.springframework.util.Assert;
+
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
+
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * @since 3.5
+ */
+@UtilityClass
+@Slf4j
+public class HmacUtils {
+
+ /**
+ * Returns an output byte array, as a concatenation of:<br>
+ * (1) the HMAC length as 2 byte short value<br>
+ * (2) HMAC as byte array<br>
+ * (3) input data as byte array<br>
+ */
+ public byte[] digitallySign(final HmacAuthority hmacAuthority, final byte[] data) {
+ Objects.requireNonNull(hmacAuthority);
+ Objects.requireNonNull(data);
+
+ var hmac = hmacAuthority.generateHmac(data);
+
+ // safety guard on the cast to short
+ Assert.isTrue(hmac.length<=Short.MAX_VALUE, ()->"unexpected HMAC length encountered: " + hmac.length);
+ short hmacLengthAsShort = (short) hmac.length;
+
+ var buf = ByteBuffer.allocate(2 + hmac.length + data.length);
+
+ var digitallySignedPayload = buf
+ .putShort(hmacLengthAsShort)
+ .put(hmac)
+ .put(data)
+ .array();
+
+ return digitallySignedPayload;
+ }
+
+ /**
+ * Interprets an input byte array containing:<br>
+ * (1) the HMAC length as 2 byte short value<br>
+ * (2) HMAC as byte array<br>
+ * (3) actual data as byte array<br>
+ * then returns the actual data if it can be verified against given {@link HmacAuthority}, otherwise returns null.
+ */
+ public byte[] verify(final HmacAuthority hmacAuthority, final byte[] untrustedPayload) {
+ Objects.requireNonNull(hmacAuthority);
+ Objects.requireNonNull(untrustedPayload);
+
+ var buf = ByteBuffer.wrap(untrustedPayload);
+
+ int hmacLength = buf.getShort();
+
+ if(!hmacAuthority.isValidHmacLength(hmacLength)) return null; // invalid HMAC length encountered in untrusted payload
+
+ var hmacToVerify = new byte[hmacLength];
+ buf.get(hmacToVerify);
+
+ var data = new byte[untrustedPayload.length - hmacLength - 2];
+ buf.get(data);
+
+ if(!hmacAuthority.verifyHmac(data, hmacToVerify)) {
+ log.info("digital verification failed (HMAC either expired or forgery detected)");
+ return null; // validation failed
+ }
+
+ return data;
+ }
+
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/Memento.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/Memento.java
new file mode 100644
index 00000000000..755a5b0542c
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/Memento.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.core.metamodel.util.hmac;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Similar to a {@link Map}<String, Object> for key/value pairs,
+ * but in addition allows to-String <em>serialization</em> and
+ * from-String <em>de-serialization</em> of the entire map.
+ */
+public sealed interface Memento
+permits SecureMemento {
+
+ /**
+ * Returns the Object associated with {@code name}
+ * @param name
+ * @param cls the expected type which to cast the retrieved value to (required)
+ */
+ <T> T get(String name, Class<T> cls);
+
+ /**
+ * Behaves like a {@link HashMap}, but returns the Memento itself.
+ * @param name
+ * @param value
+ * @return self
+ */
+ Memento put(String name, Object value);
+
+ /**
+ * @return an unmodifiable key-set of this map
+ */
+ Set<String> keySet();
+
+ /**
+ * @return to-String <em>serialization</em> of this map (digitally signed)
+ */
+ String toExternalForm();
+
+ byte[] stateAsBytes();
+
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/MementoHmacContext.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/MementoHmacContext.java
new file mode 100644
index 00000000000..a4aaa38700d
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/MementoHmacContext.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.core.metamodel.util.hmac;
+
+import java.util.Objects;
+
+import org.jspecify.annotations.Nullable;
+
+import org.apache.causeway.applib.exceptions.unrecoverable.DigitalVerificationException;
+import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.apache.causeway.core.metamodel.valuesemantics.ValueCodec;
+
+public record MementoHmacContext(
+ HmacUrlCodec hmacUrlCodec,
+ ValueCodec valueCodec) {
+
+ public MementoHmacContext {
+ Objects.requireNonNull(hmacUrlCodec);
+ Objects.requireNonNull(valueCodec);
+ }
+
+ public Memento newMemento() {
+ return new SecureMemento(this);
+ }
+
+ public Memento parseTrustedMemento(final byte[] trustedInput) {
+ return SecureMemento.parseTrustedMemento(this, trustedInput);
+ }
+
+ public Memento parseDigitallySignedMemento(final String untrustedInput) {
+ return SecureMemento.parseDigitallySignedMemento(this, untrustedInput);
+ }
+
+ public Memento parseMemento(final @Nullable Bookmark untrustedBookmark) {
+ if(untrustedBookmark==null) throw new DigitalVerificationException("invalid memento data");
+ return parseDigitallySignedMemento(untrustedBookmark.identifier());
+ }
+
+}
\ No newline at end of file
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/SecureMemento.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/SecureMemento.java
new file mode 100644
index 00000000000..dc5855025ac
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/SecureMemento.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.core.metamodel.util.hmac;
+
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import org.apache.causeway.applib.exceptions.unrecoverable.DigitalVerificationException;
+import org.apache.causeway.commons.internal.base._Casts;
+import org.apache.causeway.commons.internal.base._Strings;
+import org.apache.causeway.commons.internal.collections._Sets;
+import org.apache.causeway.commons.internal.context._Context;
+import org.apache.causeway.commons.internal.exceptions._Exceptions;
+import org.apache.causeway.commons.internal.resources._Serializables;
+
+record SecureMemento(
+ MementoHmacContext context,
+ // we need a Serializable Map
+ Map<String, Serializable> valuesByKey
+ ) implements Memento {
+
+ SecureMemento(final MementoHmacContext context) {
+ this(context, new HashMap<>());
+ }
+
+ SecureMemento {
+ Objects.requireNonNull(context);
+ Objects.requireNonNull(valuesByKey);
+ Assert.isTrue(Serializable.class.isInstance(valuesByKey), ()->"map is expected to be serializable");
+ }
+
+ @Override
+ public Memento put(final @NonNull String name, final Object value) {
+ if(value==null) return this; //no-op, there is no point in storing null values
+
+ valuesByKey.put(name, context.valueCodec().encode(value));
+ return this;
+ }
+
+ @Override
+ public <T> T get(final String name, final Class<T> cls) {
+ final Serializable value = valuesByKey.get(name);
+ if(value==null) return null;
+
+ return context.valueCodec().decode(cls, value);
+ }
+
+ @Override
+ public Set<String> keySet() {
+ return _Sets.unmodifiable(valuesByKey.keySet());
+ }
+
+ @Override
+ public byte[] stateAsBytes() {
+ return _Serializables.write((Serializable) valuesByKey);
+ }
+
+ @Override
+ public String toExternalForm() {
+ try {
+ return context.hmacUrlCodec().encodeForUrl(stateAsBytes());
+ } catch (Exception e) {
+ throw new IllegalArgumentException("failed to serialize memento", e);
+ }
+ }
+
+ // -- PARSER
+
+ static Memento parseDigitallySignedMemento(
+ final MementoHmacContext context,
+ final @Nullable String untrustedEncodedString) {
+ if(!StringUtils.hasText(untrustedEncodedString)) throw new DigitalVerificationException("invalid memento data");
+
+ var trustedBytes = context.hmacUrlCodec().decodeFromUrl(untrustedEncodedString).orElse(null);
+ if(trustedBytes==null) throw new DigitalVerificationException("invalid memento data");
+
+ return parseTrustedMemento(context, trustedBytes);
+ }
+
+ static Memento parseTrustedMemento(
+ final MementoHmacContext context,
+ final byte[] trustedBytes) {
+ try {
+ final HashMap<String, Serializable> valuesByKey = _Casts.uncheckedCast(
+ _Serializables.readWithCustomClassLoader(HashMap.class, _Context.getDefaultClassLoader(), trustedBytes));
+ return new SecureMemento(context, valuesByKey);
+ } catch (Exception e) {
+ throw _Exceptions.illegalArgument(e,
+ "failed to parse memento from serialized string '%s'",
+ _Strings.ellipsifyAtEnd(new String(trustedBytes, StandardCharsets.UTF_8) , 200, "..."));
+ }
+ }
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/IdStringifierForSerializable.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/IdStringifierForSerializable.java
deleted file mode 100644
index 5abf4957859..00000000000
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/IdStringifierForSerializable.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.causeway.core.metamodel.valuesemantics;
-
-import java.io.Serializable;
-
-import jakarta.annotation.Priority;
-import jakarta.inject.Inject;
-import jakarta.inject.Named;
-
-import org.springframework.stereotype.Component;
-
-import org.apache.causeway.applib.annotation.PriorityPrecedence;
-import org.apache.causeway.applib.services.bookmark.IdStringifier;
-import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
-import org.apache.causeway.applib.value.semantics.ValueSemanticsProvider;
-import org.apache.causeway.commons.internal.base._Strings;
-import org.apache.causeway.commons.internal.resources._Serializables;
-
-import org.jspecify.annotations.NonNull;
-
-/**
- * Used as a fallback if no other {@link ValueSemanticsProvider}
- * is available to handle the corresponding value type.
- */
-@Component
-@Named("causeway.metamodel.value.IdStringifierForSerializable")
-@Priority(PriorityPrecedence.LAST)
-public class IdStringifierForSerializable
-implements
- IdStringifier.EntityAgnostic<Serializable>{
-
- private final UrlEncodingService codec;
-
- @Inject
- public IdStringifierForSerializable(
- final @NonNull UrlEncodingService codec) {
- this.codec = codec;
- }
-
-// @Override
-// public ValueType getSchemaValueType() {
-// return ValueType.STRING;
-// }
-
- @Override
- public Class<Serializable> getCorrespondingClass() {
- return Serializable.class;
- }
-
-// // -- COMPOSER
-//
-// @Override
-// public ValueDecomposition decompose(final Serializable value) {
-// return decomposeAsString(value, this::enstring, ()->null);
-// }
-//
-// @Override
-// public Serializable compose(final ValueDecomposition decomposition) {
-// return composeFromString(decomposition, this::destring, ()->null);
-// }
-
- // -- ID STRINGIFIER
-
- @Override
- public String enstring(final @NonNull Serializable id) {
- // even though null case is guarded by lombok - keep null check for symmetry
- return id != null
- ? codec.encode(_Serializables.write(id))
- : null;
- }
-
- @Override
- public Serializable destring(
- final @NonNull String stringified) {
- return destringAs(stringified, Serializable.class);
- }
-
-// @Override
-// public Can<Serializable> getExamples() {
-// return Can.of(
-// Integer.MAX_VALUE,
-// "Hallo World",
-// new BigDecimal("3.1415"));
-// }
-
- // -- HELPER
-
- private <T extends Serializable> T destringAs(
- final @NonNull String stringified,
- final @NonNull Class<T> requiredClass) {
- return _Strings.isNotEmpty(stringified)
- ? _Serializables.read(requiredClass, codec.decode(stringified))
- : null;
- }
-
-}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/TreeNodeValueSemantics.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/TreeNodeValueSemantics.java
index 2311284d236..40a9e2fbd74 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/TreeNodeValueSemantics.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/TreeNodeValueSemantics.java
@@ -18,11 +18,13 @@
*/
package org.apache.causeway.core.metamodel.valuesemantics;
+import java.util.Objects;
import java.util.stream.Stream;
import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.inject.Named;
+import jakarta.inject.Provider;
import org.springframework.stereotype.Component;
@@ -31,16 +33,19 @@
import org.apache.causeway.applib.graph.tree.TreeNode;
import org.apache.causeway.applib.graph.tree.TreePath;
import org.apache.causeway.applib.graph.tree.TreeState;
+import org.apache.causeway.applib.services.bookmark.BookmarkService;
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
import org.apache.causeway.applib.services.factory.FactoryService;
import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
import org.apache.causeway.applib.value.semantics.Renderer;
import org.apache.causeway.applib.value.semantics.ValueDecomposition;
import org.apache.causeway.applib.value.semantics.ValueSemanticsAbstract;
+import org.apache.causeway.applib.value.semantics.ValueSemanticsResolver;
import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.commons.internal.base._Casts;
-import org.apache.causeway.commons.internal.memento._Mementos;
-import org.apache.causeway.commons.internal.memento._Mementos.Memento;
-import org.apache.causeway.commons.internal.memento._Mementos.SerializingAdapter;
+import org.apache.causeway.core.metamodel.util.hmac.HmacUrlCodec;
+import org.apache.causeway.core.metamodel.util.hmac.Memento;
+import org.apache.causeway.core.metamodel.util.hmac.MementoHmacContext;
import org.apache.causeway.schema.common.v2.ValueType;
@Component
@@ -51,9 +56,27 @@ public class TreeNodeValueSemantics
implements
Renderer<TreeNode<?>> {
- @Inject UrlEncodingService urlEncodingService;
- @Inject SerializingAdapter serializingAdapter;
- @Inject FactoryService factoryService;
+ private final FactoryService factoryService;
+ private final MementoHmacContext mementoContext;
+
+ @Inject
+ public TreeNodeValueSemantics(
+ final HmacAuthority hmacAuthority,
+ final UrlEncodingService urlEncodingService,
+ final FactoryService factoryService,
+ final BookmarkService bookmarkService,
+ final Provider<ValueSemanticsResolver> valueSemanticsResolverProvider,
+ final ValueCodec valueCodec) {
+
+ Objects.requireNonNull(hmacAuthority);
+ Objects.requireNonNull(urlEncodingService);
+ Objects.requireNonNull(bookmarkService);
+ Objects.requireNonNull(valueSemanticsResolverProvider);
+ this.factoryService = Objects.requireNonNull(factoryService);
+
+ this.mementoContext = new MementoHmacContext(
+ new HmacUrlCodec(hmacAuthority, urlEncodingService), valueCodec);
+ }
@Override
public Class<TreeNode<?>> getCorrespondingClass() {
@@ -78,17 +101,17 @@ public TreeNode<?> compose(final ValueDecomposition decomposition) {
}
private String toEncodedString(final TreeNode<?> treeNode) {
- final Memento memento = newMemento();
+ final Memento memento = mementoContext.newMemento();
memento.put("rootValue", treeNode.rootValue());
memento.put("adapterClass", treeNode.treeAdapter().getClass());
memento.put("treeState", treeNode.treeState());
memento.put("treePath", treeNode.treePath());
- return memento.asString();
+ return memento.toExternalForm();
}
@SuppressWarnings("unchecked")
private TreeNode<?> fromEncodedString(final String input) {
- final Memento memento = parseMemento(input);
+ final Memento memento = mementoContext.parseDigitallySignedMemento(input);
final TreeNode<?> rootNode = TreeNode.root(
memento.get("rootValue", Object.class),
memento.get("adapterClass", Class.class),
@@ -127,14 +150,4 @@ class TreeAdapterString implements TreeAdapter<String> {
TreeNode.root("another TreeRoot", new TreeAdapterString(), TreeState.rootCollapsed()));
}
- // -- HELPER
-
- private _Mementos.Memento newMemento(){
- return _Mementos.create(urlEncodingService, serializingAdapter);
- }
-
- private _Mementos.Memento parseMemento(final String input){
- return _Mementos.parse(urlEncodingService, serializingAdapter, input);
- }
-
}
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/serializing/SerializingAdapterDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/ValueCodec.java
similarity index 74%
rename from core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/serializing/SerializingAdapterDefault.java
rename to core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/ValueCodec.java
index a6e7d66f939..c58e2862774 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/serializing/SerializingAdapterDefault.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/ValueCodec.java
@@ -16,20 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.causeway.core.runtimeservices.serializing;
+package org.apache.causeway.core.metamodel.valuesemantics;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
import java.io.Serializable;
import java.util.Optional;
-import jakarta.annotation.Priority;
-import jakarta.inject.Inject;
-import jakarta.inject.Named;
+import jakarta.inject.Provider;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.context.annotation.Lazy;
-import org.springframework.stereotype.Service;
+import org.jspecify.annotations.NonNull;
-import org.apache.causeway.applib.annotation.PriorityPrecedence;
import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.applib.services.bookmark.BookmarkService;
import org.apache.causeway.applib.services.bookmark.idstringifiers.PredefinedSerializables;
@@ -37,38 +34,35 @@
import org.apache.causeway.applib.value.semantics.ValueSemanticsResolver;
import org.apache.causeway.commons.internal.base._Casts;
import org.apache.causeway.commons.internal.exceptions._Exceptions;
-import org.apache.causeway.commons.internal.memento._Mementos.SerializingAdapter;
-import org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices;
-
-import org.jspecify.annotations.NonNull;
/**
- * Default implementation of {@link SerializingAdapter}, intended as an 'internal' service.
+ * Coder/Decoder from {@link Object} to {@link Serializable}
+ * As used to encode values to bookmarks.
*
- * <p>
* @implNote uses {@link Bookmark} or {@link ValueDecomposition}
* for identifiable objects or value types,
* while some predefined serializable types that implement {@link Serializable}
* are written/read directly
- * </p>
*
* @see PredefinedSerializables
*
- * @since 2.0 {@index}
+ * @since 3.5 (refactored from SerializingAdapter)
*/
-@Service
-@Named(CausewayModuleCoreRuntimeServices.NAMESPACE + ".SerializingAdapterDefault")
-@Priority(PriorityPrecedence.MIDPOINT)
-@Qualifier("Default")
-public class SerializingAdapterDefault implements SerializingAdapter {
+public record ValueCodec(
+ BookmarkService bookmarkService,
+ Provider<ValueSemanticsResolver> valueSemanticsResolverProvider) {
- @Inject private BookmarkService bookmarkService;
-
- @Lazy
- @Inject private ValueSemanticsResolver valueSemanticsResolver;
+ /** JUnit testing default */
+ public static ValueCodec forTesting() {
+ return new ValueCodec(null, () -> null);
+ }
- @Override
- public Serializable write(final @NonNull Object value) {
+ /**
+ * Converts the value into a {@link Serializable} that is write-able to an {@link ObjectOutput}.
+ *
+ * <p>Note: write and read are complementary operations
+ */
+ public Serializable encode(final @NonNull Object value) {
if(PredefinedSerializables.isPredefinedSerializable(value.getClass())) {
// the value can be stored/written directly without conversion to a bookmark
return (Serializable) value;
@@ -83,8 +77,15 @@ public Serializable write(final @NonNull Object value) {
_Exceptions.unrecoverable("cannot create a memento for object of type %s", value.getClass()));
}
- @Override
- public <T> T read(final @NonNull Class<T> valueClass, final @NonNull Serializable value) {
+ /**
+ * Converts the {@link Serializable} {@code value} as read from an {@link ObjectInput} back into its
+ * original (typically a Pojo).
+ *
+ * <p>Note: write and read are complementary operations
+ *
+ * @param valueClass the expected type which to cast the {@code value} to (required)
+ */
+ public <T> T decode(final @NonNull Class<T> valueClass, final @NonNull Serializable value) {
// see if required/desired value-class is Bookmark, then just cast
if(Bookmark.class.equals(valueClass)) {
@@ -120,7 +121,7 @@ private <T> Optional<ValueDecomposition> toValueDecomposition(
final Class<T> valueClass = _Casts.uncheckedCast(value.getClass());
- return valueSemanticsResolver.streamValueSemantics(valueClass)
+ return valueSemanticsResolverProvider().get().streamValueSemantics(valueClass)
.findFirst()
.map(vs->vs.decompose(value));
}
@@ -129,9 +130,9 @@ private <T> Optional<T> fromValueDecomposition(
final @NonNull Class<T> valueClass,
final @NonNull ValueDecomposition decomposition) {
- return valueSemanticsResolver.streamValueSemantics(valueClass)
+ return valueSemanticsResolverProvider().get().streamValueSemantics(valueClass)
.findFirst()
.map(vs->vs.compose(decomposition));
}
-}
+}
\ No newline at end of file
diff --git a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/valuesemantics/IdStringifierForSerializable_Test.java b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/valuesemantics/IdStringifierForSerializable_Test.java
deleted file mode 100644
index 1facb53f69a..00000000000
--- a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/valuesemantics/IdStringifierForSerializable_Test.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- *
- */
-
-package org.apache.causeway.core.metamodel.valuesemantics;
-
-import java.io.Serializable;
-import java.util.stream.Stream;
-
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
-
-class IdStringifierForSerializable_Test {
-
- private UrlEncodingService codec = UrlEncodingService.forTesting();
-
- // -- SCENARIO
-
- static record CustomerPK(
- int lower,
- int upper) implements Serializable{
- }
-
- // -- TEST
-
- static Stream<Arguments> roundtrip() {
- return Stream.of(
- Arguments.of(Byte.MAX_VALUE),
- Arguments.of(Byte.MIN_VALUE),
- Arguments.of((byte)0),
- Arguments.of((byte)12345),
- Arguments.of((byte)-12345),
- Arguments.of(new CustomerPK(5,6))
- // Arguments.of((Serializable)null) ... throws NPE as expected
- );
- }
-
- @ParameterizedTest
- @MethodSource()
- void roundtrip(final Serializable value) {
-
- var stringifier = new IdStringifierForSerializable(codec);
-
- String stringified = stringifier.enstring(value);
- Serializable parse = stringifier.destring(stringified);
-
- assertThat(parse).isEqualTo(value);
- }
-
-}
diff --git a/core/mmtestsupport/src/main/java/org/apache/causeway/core/mmtestsupport/MetaModelContext_forTesting.java b/core/mmtestsupport/src/main/java/org/apache/causeway/core/mmtestsupport/MetaModelContext_forTesting.java
index a59d0be3483..4e0f8eef81d 100644
--- a/core/mmtestsupport/src/main/java/org/apache/causeway/core/mmtestsupport/MetaModelContext_forTesting.java
+++ b/core/mmtestsupport/src/main/java/org/apache/causeway/core/mmtestsupport/MetaModelContext_forTesting.java
@@ -30,10 +30,10 @@
import static java.util.Objects.requireNonNull;
import org.jspecify.annotations.NonNull;
-
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.util.ClassUtils;
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
import org.apache.causeway.applib.services.factory.FactoryService;
import org.apache.causeway.applib.services.grid.GridMarshaller;
import org.apache.causeway.applib.services.grid.GridService;
@@ -50,6 +50,7 @@
import org.apache.causeway.applib.services.render.PlaceholderRenderService;
import org.apache.causeway.applib.services.repository.RepositoryService;
import org.apache.causeway.applib.services.title.TitleService;
+import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
import org.apache.causeway.applib.services.wrapper.WrapperFactory;
import org.apache.causeway.applib.services.xactn.TransactionService;
import org.apache.causeway.applib.services.xactn.TransactionState;
@@ -103,6 +104,7 @@
import org.apache.causeway.core.metamodel.valuesemantics.TreePathValueSemantics;
import org.apache.causeway.core.metamodel.valuesemantics.URLValueSemantics;
import org.apache.causeway.core.metamodel.valuesemantics.UUIDValueSemantics;
+import org.apache.causeway.core.metamodel.valuesemantics.ValueCodec;
import org.apache.causeway.core.metamodel.valuetypes.ValueSemanticsResolverDefault;
import org.apache.causeway.core.security.authentication.manager.AuthenticationManager;
import org.apache.causeway.core.security.authorization.manager.AuthorizationManager;
@@ -301,8 +303,11 @@ Stream<SingletonBeanProvider> streamBeanAdapters() {
SingletonBeanProvider.forTestingLazy(JaxbService.class, this::getJaxbService),
SingletonBeanProvider.forTestingLazy(MenuBarsService.class, this::getMenuBarsService),
SingletonBeanProvider.forTestingLazy(LayoutService.class, this::getLayoutService),
- SingletonBeanProvider.forTestingLazy(SpecificationLoader.class, this::getSpecificationLoader)
- )
+ SingletonBeanProvider.forTestingLazy(SpecificationLoader.class, this::getSpecificationLoader),
+ SingletonBeanProvider.forTestingLazy(HmacAuthority.class, HmacAuthority::forTesting),
+ SingletonBeanProvider.forTestingLazy(UrlEncodingService.class, UrlEncodingService::forTestingNoCompression),
+ SingletonBeanProvider.forTestingLazy(ValueCodec.class, ()->ValueCodec.forTesting())
+ )
);
}
diff --git a/core/runtimeservices/src/main/java/module-info.java b/core/runtimeservices/src/main/java/module-info.java
index de4ab855e19..23e9b93a01e 100644
--- a/core/runtimeservices/src/main/java/module-info.java
+++ b/core/runtimeservices/src/main/java/module-info.java
@@ -38,7 +38,6 @@
exports org.apache.causeway.core.runtimeservices.recognizer.dae;
exports org.apache.causeway.core.runtimeservices.routing;
exports org.apache.causeway.core.runtimeservices.scratchpad;
- exports org.apache.causeway.core.runtimeservices.serializing;
exports org.apache.causeway.core.runtimeservices.session;
exports org.apache.causeway.core.runtimeservices.sitemap;
exports org.apache.causeway.core.runtimeservices.spring;
@@ -79,7 +78,8 @@
requires org.apache.causeway.core.codegen.bytebuddy;
requires spring.aop;
requires java.management;
-
+ requires spring.boot.autoconfigure;
+
opens org.apache.causeway.core.runtimeservices;
opens org.apache.causeway.core.runtimeservices.wrapper;
opens org.apache.causeway.core.runtimeservices.wrapper.handlers; //to org.apache.causeway.core.codegen.bytebuddy
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java
index 513e4dcf80a..3a619bb9cd9 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java
+++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java
@@ -18,6 +18,8 @@
*/
package org.apache.causeway.core.runtimeservices;
+import org.springframework.boot.autoconfigure.AutoConfigureOrder;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@@ -25,6 +27,8 @@
import org.springframework.core.OrderComparator;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
+import org.apache.causeway.applib.annotation.PriorityPrecedence;
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
import org.apache.causeway.core.codegen.bytebuddy.CausewayModuleCoreCodegenByteBuddy;
import org.apache.causeway.core.runtime.CausewayModuleCoreRuntime;
import org.apache.causeway.core.runtimeservices.bookmarks.BookmarkServiceDefault;
@@ -57,7 +61,6 @@
import org.apache.causeway.core.runtimeservices.render.PlaceholderRenderServiceDefault;
import org.apache.causeway.core.runtimeservices.routing.RoutingServiceDefault;
import org.apache.causeway.core.runtimeservices.scratchpad.ScratchpadDefault;
-import org.apache.causeway.core.runtimeservices.serializing.SerializingAdapterDefault;
import org.apache.causeway.core.runtimeservices.session.InteractionIdGeneratorDefault;
import org.apache.causeway.core.runtimeservices.session.InteractionServiceDefault;
import org.apache.causeway.core.runtimeservices.sitemap.SitemapServiceDefault;
@@ -109,7 +112,6 @@
LifecycleCallbackNotifier.class,
SchemaValueMarshallerDefault.class,
ScratchpadDefault.class,
- SerializingAdapterDefault.class,
SitemapServiceDefault.class,
SpringBeansService.class,
TransactionServiceSpring.class,
@@ -129,6 +131,9 @@
// Exception Recognizers
ExceptionRecognizerForDataAccessException.class,
+ // auto configuration
+ CausewayModuleCoreRuntimeServices.HmacAutorityAutoconfigure.class
+
})
@ComponentScan(basePackages = "org.apache.causeway.core.runtimeservices.icons")
public class CausewayModuleCoreRuntimeServices {
@@ -140,4 +145,14 @@ public OrderComparator orderComparator() {
return new AnnotationAwareOrderComparator();
}
+ @AutoConfigureOrder(PriorityPrecedence.LATE)
+ @Configuration
+ static class HmacAutorityAutoconfigure {
+ @Bean(NAMESPACE + ".fallbackHmacAuthority")
+ @ConditionalOnMissingBean(HmacAuthority.class)
+ public HmacAuthority fallbackHmacAuthority() {
+ return HmacAuthority.HmacSHA256.randomInstance();
+ }
+ }
+
}
diff --git a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/urlencoding/MementosTest.java b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/urlencoding/MementosTest.java
index c2f4a4fedec..05eb3363203 100644
--- a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/urlencoding/MementosTest.java
+++ b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/urlencoding/MementosTest.java
@@ -18,7 +18,6 @@
*/
package org.apache.causeway.core.runtimeservices.urlencoding;
-import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
@@ -32,11 +31,12 @@
import static org.hamcrest.MatcherAssert.assertThat;
import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
-import org.apache.causeway.commons.internal.base._Casts;
-import org.apache.causeway.commons.internal.memento._Mementos;
-import org.apache.causeway.commons.internal.memento._Mementos.Memento;
-import org.apache.causeway.commons.internal.memento._Mementos.SerializingAdapter;
+import org.apache.causeway.core.metamodel.util.hmac.HmacUrlCodec;
+import org.apache.causeway.core.metamodel.util.hmac.Memento;
+import org.apache.causeway.core.metamodel.util.hmac.MementoHmacContext;
+import org.apache.causeway.core.metamodel.valuesemantics.ValueCodec;
class MementosTest {
@@ -46,26 +46,13 @@ static enum DOW {
UrlEncodingServiceWithCompression serviceWithCompression;
UrlEncodingService serviceBaseEncoding;
- SerializingAdapter serializingAdapter;
+ ValueCodec valueCodec;
@BeforeEach
void setUp() throws Exception {
serviceWithCompression = new UrlEncodingServiceWithCompression();
- serviceBaseEncoding = UrlEncodingService.forTestingNoCompression();;
-
- serializingAdapter = new SerializingAdapter() {
-
- @Override
- public Serializable write(final Object value) {
- return (Serializable) value;
- }
-
- @Override
- public <T> T read(final Class<T> cls, final Serializable value) {
- return _Casts.castToOrElseNull(value, cls);
- }
- };
-
+ serviceBaseEncoding = UrlEncodingService.forTestingNoCompression();
+ valueCodec = ValueCodec.forTesting();
}
@Test
@@ -79,7 +66,8 @@ void roundtrip_with_compression() {
}
private void roundtrip(final UrlEncodingService codec) {
- final Memento memento = _Mementos.create(codec, serializingAdapter);
+ final var mementoContext = new MementoHmacContext(new HmacUrlCodec(HmacAuthority.HmacSHA256.randomInstance(), codec), valueCodec);
+ final Memento memento = mementoContext.newMemento();
memento.put("someString", "a string");
memento.put("someStringWithDoubleSpaces", "a string");
@@ -101,9 +89,9 @@ private void roundtrip(final UrlEncodingService codec) {
memento.put("someEnum", DOW.Wed);
- final String str = memento.asString();
+ final String str = memento.toExternalForm();
- final Memento memento2 = _Mementos.parse(codec, serializingAdapter, str);
+ final Memento memento2 = mementoContext.parseDigitallySignedMemento(str);
assertThat(memento2.get("someString", String.class), is("a string"));
assertThat(memento2.get("someStringWithDoubleSpaces", String.class), is("a string"));
diff --git a/core/security/src/test/java/org/apache/causeway/security/EncodabilityContractTest.java b/core/security/src/test/java/org/apache/causeway/security/EncodabilityContractTest.java
index 80b091357b5..f31c975d729 100644
--- a/core/security/src/test/java/org/apache/causeway/security/EncodabilityContractTest.java
+++ b/core/security/src/test/java/org/apache/causeway/security/EncodabilityContractTest.java
@@ -18,11 +18,7 @@
*/
package org.apache.causeway.security;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
import java.io.Serializable;
import org.junit.jupiter.api.BeforeEach;
@@ -32,11 +28,10 @@
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
+import org.springframework.util.SerializationUtils;
+
import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
import org.apache.causeway.applib.services.user.UserMemento;
-import org.apache.causeway.commons.internal.base._Casts;
-
-import lombok.SneakyThrows;
public abstract class EncodabilityContractTest {
@@ -67,25 +62,10 @@ public void shouldImplementEncodeable() throws Exception {
@Test
public void shouldRoundTrip() throws IOException, ClassNotFoundException {
- var decodedObject = doRoundTrip(serializable);
+ var decodedObject = SerializationUtils.clone(serializable);
assertRoundtripped(decodedObject, serializable);
}
- @SneakyThrows
- private static <T extends Serializable> T doRoundTrip(final T serializable) {
-
- var buffer = new ByteArrayOutputStream();
-
- try(var out = new ObjectOutputStream(buffer)) {
- out.writeObject(serializable);
- }
-
- try(var in = new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray()))) {
- var decodedObject = in.readObject();
- return _Casts.uncheckedCast(decodedObject);
- }
- }
-
protected abstract void assertRoundtripped(Object decodedEncodable, Object originalEncodable);
}
\ No newline at end of file
diff --git a/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/MetaModelRegressionTest.java b/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/MetaModelRegressionTest.java
index 58214b032f6..a11b730ce89 100644
--- a/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/MetaModelRegressionTest.java
+++ b/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/MetaModelRegressionTest.java
@@ -32,14 +32,18 @@
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.TestPropertySource;
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
import org.apache.causeway.applib.services.factory.FactoryService;
import org.apache.causeway.applib.services.metamodel.MetaModelServiceMenu;
import org.apache.causeway.applib.services.metamodel.MetaModelServiceMenu.ExportFormat;
import org.apache.causeway.applib.value.Clob;
import org.apache.causeway.core.config.presets.CausewayPresets;
import org.apache.causeway.testdomain.conf.Configuration_headless;
+import org.apache.causeway.testdomain.domainmodel.MetaModelRegressionTest.EnableHmacAuthority;
import org.apache.causeway.testdomain.model.good.Configuration_usingValidDomain;
import org.apache.causeway.testing.integtestsupport.applib.ApprovalsOptions;
@@ -49,7 +53,7 @@
classes = {
Configuration_headless.class,
Configuration_usingValidDomain.class,
-
+ EnableHmacAuthority.class
},
properties = {
"causeway.core.meta-model.introspector.mode=FULL",
@@ -65,6 +69,14 @@ class MetaModelRegressionTest {
@Inject MetaModelServiceMenu metaModelServiceMenu;
@Inject FactoryService factoryService;
+ @Configuration(proxyBeanMethods = false)
+ static class EnableHmacAuthority {
+ @Bean
+ public HmacAuthority hmacAuthority() {
+ return HmacAuthority.HmacSHA256.randomInstance();
+ }
+ }
+
@BeforeEach
void setUp() {
assertNotNull(metaModelServiceMenu);
diff --git a/regressiontests/factory/src/test/java/org/apache/causeway/testdomain/factory/ViewModelFactoryTest.java b/regressiontests/factory/src/test/java/org/apache/causeway/testdomain/factory/ViewModelFactoryTest.java
index 5e0fde8013a..a5ac1852971 100644
--- a/regressiontests/factory/src/test/java/org/apache/causeway/testdomain/factory/ViewModelFactoryTest.java
+++ b/regressiontests/factory/src/test/java/org/apache/causeway/testdomain/factory/ViewModelFactoryTest.java
@@ -32,8 +32,11 @@
import org.apache.causeway.applib.annotation.DomainObject;
import org.apache.causeway.applib.annotation.Nature;
import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
import org.apache.causeway.applib.services.registry.ServiceRegistry;
import org.apache.causeway.applib.services.repository.RepositoryService;
+import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
+import org.apache.causeway.core.metamodel.util.hmac.HmacUrlCodec;
import org.apache.causeway.testdomain.conf.Configuration_headless;
import org.apache.causeway.testing.integtestsupport.applib.CausewayIntegrationTestAbstract;
@@ -121,6 +124,9 @@ void onPostConstruct() {
// -- TESTS
+ @Inject HmacAuthority hmacAuthority;
+ @Inject UrlEncodingService urlEncodingService;
+
@Test
void sampleViewModel_shouldHave_injectionPointsResolved() {
var sampleViewModel = factoryService.viewModel(SimpleViewModel.class);
@@ -138,7 +144,8 @@ void viewModel_shouldHave_constructorArgsResolved() {
ViewModelWithInjectableConstructorArgs viewModel = factoryService.viewModel(ViewModelWithInjectableConstructorArgs.class,
Bookmark.forLogicalTypeNameAndIdentifier(
ViewModelWithInjectableConstructorArgs.class.getName(),
- "aName"));
+ new HmacUrlCodec(hmacAuthority, urlEncodingService).encodeForUrlAsUtf8("aName")
+ ));
viewModel.assertInitialized();
}
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/errors/ExceptionStackTracePanel.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/errors/ExceptionStackTracePanel.java
index 8e2b34f34a5..1e40c83c7ac 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/errors/ExceptionStackTracePanel.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/errors/ExceptionStackTracePanel.java
@@ -19,6 +19,7 @@
package org.apache.causeway.viewer.wicket.ui.errors;
import java.util.Objects;
+import java.util.Optional;
import java.util.stream.Stream;
import org.apache.wicket.AttributeModifier;
@@ -88,16 +89,22 @@ public ExceptionStackTracePanel(
super(id, Model.of(exceptionModel));
- var ticketOptional = lookupService(ErrorReportingService.class)
- .map(errorReportingService->
- errorReportingService.reportError(exceptionModel.asErrorDetails()));
+ final boolean unautorizedOrRecognized =
+ exceptionModel.isAuthorizationException()
+ || exceptionModel.isRecognized();
- var mainMessage = ticketOptional
+ var ticketOpt = unautorizedOrRecognized
+ ? Optional.<Ticket>empty()
+ : lookupService(ErrorReportingService.class)
+ .map(errorReportingService->
+ errorReportingService.reportError(exceptionModel.asErrorDetails()));
+
+ var mainMessage = ticketOpt
.map(Ticket::getUserMessage)
.orElseGet(exceptionModel::getMainMessage);
Wkt.labelAdd(this, ID_MAIN_MESSAGE, mainMessage);
- ticketOptional
+ ticketOpt
.map(Ticket::getMarkup)
.ifPresentOrElse(ticketMarkup->{
Wkt.markupAdd(this, ID_TICKET_MARKUP, ticketMarkup);
@@ -105,10 +112,8 @@ public ExceptionStackTracePanel(
WktComponents.permanentlyHide(this, ID_TICKET_MARKUP);
});
- final boolean suppressExceptionDetail =
- exceptionModel.isAuthorizationException()
- || exceptionModel.isRecognized()
- || ticketOptional
+ final boolean suppressExceptionDetail = unautorizedOrRecognized
+ || ticketOpt
.map(Ticket::getStackTracePolicy)
.map(Ticket.StackTracePolicy.HIDE::equals)
.orElse(false);
@@ -150,7 +155,7 @@ public void renderHead(final IHeaderResponse response) {
// -- HELPER
- private static String convertToHtml(ExceptionModel exceptionModel) {
+ private static String convertToHtml(final ExceptionModel exceptionModel) {
var html = new StringBuilder();
_NullSafe.stream(exceptionModel.causalChain())
.filter(Objects::nonNull)
@@ -175,7 +180,7 @@ private static String convertToHtml(ExceptionModel exceptionModel) {
return html.toString();
}
- private static String convertMessageToHtml(Throwable cause) {
+ private static String convertMessageToHtml(final Throwable cause) {
return StringUtils.hasLength(cause.getMessage())
? org.springframework.web.util.HtmlUtils.htmlEscape(cause.getMessage().replace("\n", "<br>"))
: "";
diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java
index c9fbabead88..6b04e2e1a0f 100644
--- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java
+++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java
@@ -46,6 +46,8 @@
import org.jspecify.annotations.Nullable;
import org.apache.causeway.applib.exceptions.unrecoverable.BookmarkNotFoundException;
+import org.apache.causeway.applib.exceptions.unrecoverable.DigitalVerificationException;
+import org.apache.causeway.applib.services.exceprecog.Category;
import org.apache.causeway.applib.services.exceprecog.ExceptionRecognizer;
import org.apache.causeway.applib.services.exceprecog.ExceptionRecognizerForType;
import org.apache.causeway.applib.services.exceprecog.ExceptionRecognizerService;
@@ -412,13 +414,17 @@ protected PageProvider errorPageProviderFor(final Exception ex) {
// special case handling for PageExpiredException, otherwise infinite loop
private static final ExceptionRecognizerForType pageExpiredExceptionRecognizer =
- new ExceptionRecognizerForType(
- PageExpiredException.class,
- __->"Requested page is no longer available.");
+ new ExceptionRecognizerForType(
+ PageExpiredException.class, __->"Requested page is no longer available.");
+
+ private static final ExceptionRecognizerForType exceptionRecognizerForDigitalVerificationException =
+ new ExceptionRecognizerForType(Category.NOT_FOUND,
+ DigitalVerificationException.class, __->"Digital verification failed for this request. "
+ + "Perhaps bookmark is not (or no longer) valid.");
private static final ExceptionRecognizerForType exceptionRecognizerForBookmarkNotFoundException =
- new ExceptionRecognizerForType(
- BookmarkNotFoundException.class, __ -> "Bookmark is not found.");
+ new ExceptionRecognizerForType(Category.NOT_FOUND,
+ BookmarkNotFoundException.class, __->"Bookmark is not found.");
protected IRequestablePage errorPageFor(final Exception ex) {
@@ -430,20 +436,21 @@ protected IRequestablePage errorPageFor(final Exception ex) {
}
// using side-effect free access to MM validation result
- var validationResult = getMetaModelContext().getSpecificationLoader().getValidationResult()
+ var validationResult = mmc.getSpecificationLoader().getValidationResult()
.orElse(null);
if(validationResult!=null
&& validationResult.hasFailures()) {
return new MmvErrorPage(validationResult.getMessages("[%d] %s"));
}
- var exceptionRecognizerService = getMetaModelContext().getServiceRegistry()
+ var exceptionRecognizerService = mmc.getServiceRegistry()
.lookupServiceElseFail(ExceptionRecognizerService.class);
final Optional<Recognition> recognition = exceptionRecognizerService
.recognizeFromSelected(
Can.<ExceptionRecognizer>of(
pageExpiredExceptionRecognizer,
+ exceptionRecognizerForDigitalVerificationException,
exceptionRecognizerForBookmarkNotFoundException)
.addAll(exceptionRecognizerService.getExceptionRecognizers()),
ex);