Apache Causeway: Java deserialization vulnerability to authenticated attackers
Description
Apache Causeway faces Java deserialization vulnerabilities that allow remote code execution (RCE) through user-controllable URL parameters. These vulnerabilities affect all applications using Causeway's ViewModel functionality and can be exploited by authenticated attackers to execute arbitrary code with application privileges.
This issue affects all current versions.
Users are recommended to upgrade to version 3.5.0, which fixes the issue.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Apache Causeway's ViewModel functionality contains a Java deserialization vulnerability allowing authenticated RCE via URL parameters; fixed in 3.5.0.
Vulnerability
Overview Apache Causeway, a framework for rapidly developing domain-driven Java apps, is vulnerable to Java deserialization attacks through user-controllable URL parameters in its ViewModel functionality. The root cause is that view model bookmarks could be forged or tampered with, enabling deserialization of arbitrary objects [1][2].
Exploitation
An authenticated attacker can exploit this by crafting malicious serialized objects in URL parameters that are deserialized by the application. No special network position is required beyond standard user access; the attacker must be authenticated [2].
Impact
Successful exploitation allows remote code execution (RCE) with application privileges, potentially leading to full compromise of the application and its data [2].
Mitigation
The vulnerability affects all versions prior to 3.5.0. The fix introduces HMAC-based verification for view model bookmarks, preventing forgery and deserialization attacks [3][4]. Users should upgrade to version 3.5.0 immediately [2].
- GitHub - apache/causeway: Use Apache Causeway™ to rapidly develop domain-driven apps or modular monoliths in Java, on top of the Spring Boot platform. Write your business logic in entities, domain services or view models, and the framework dynamically generates a representation of that domain model as a webapp, GraphQL or RESTful API. For prototyping or production.
- NVD - CVE-2025-64408
- CAUSEWAY-3939: Viewmodel Bookmark Overhaul · apache/causeway@bef00f5
- CAUSEWAY-3939: removal of IdStringifier for Serializable · apache/causeway@e6bf00c
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.apache.causeway.commons:causeway-commonsMaven | < 3.5.0 | 3.5.0 |
org.apache.causeway.core:causeway-applibMaven | < 3.5.0 | 3.5.0 |
org.apache.causeway.core:causeway-coreMaven | < 3.5.0 | 3.5.0 |
org.apache.causeway.viewer:causeway-viewer-wicketMaven | < 3.5.0 | 3.5.0 |
Affected products
2- Apache Software Foundation/Apache Causewayv5Range: 2.0.0
Patches
3bef00f58d2a2CAUSEWAY-3939: Viewmodel Bookmark Overhaul
44 files changed · +1200 −1090
api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/BookmarkNotFoundException.java+0 −4 modified@@ -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) {
api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/DigitalVerificationException.java+57 −0 added@@ -0,0 +1,57 @@ +/* + * 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.exceptions.unrecoverable; + +import org.apache.causeway.applib.exceptions.UnrecoverableException; +import org.apache.causeway.applib.services.i18n.TranslatableString; + +/** + * Indicates that a digital verification failed. + * + * <p>E.g. could be an invalid (or no longer valid) bookmark. + * + * @since 3.5 {@index} + * + */ +public class DigitalVerificationException extends UnrecoverableException { + private static final long serialVersionUID = 1L; + + public DigitalVerificationException(final String msg) { + super(msg); + } + + public DigitalVerificationException(final TranslatableString translatableMessage, + final Class<?> translationContextClass, final String translationContextMethod) { + super(translatableMessage, translationContextClass, translationContextMethod); + } + + public DigitalVerificationException(final Throwable cause) { + super(cause); + } + + public DigitalVerificationException(final String msg, final Throwable cause) { + super(msg, cause); + } + + 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
api/applib/src/main/java/org/apache/causeway/applib/layout/grid/bootstrap/BSUtil.java+3 −6 modified@@ -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) {
api/applib/src/main/java/org/apache/causeway/applib/services/bookmark/HmacAuthority.java+123 −0 added@@ -0,0 +1,123 @@ +/* + * 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 built 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>Used to generate the bookmark of view models so that they are not susceptible to forgery or de-serialization attacks. + * (Bookmarks of entities are not susceptible). + * Note though, that the bookmarks' 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.HmacAuthorityAutoconfigure' + * + * @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(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 only".getBytes()); + } +}
api/applib/src/main/java/org/apache/causeway/applib/services/urlencoding/UrlEncodingService.java+9 −17 modified@@ -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)); } }; - } }
api/applib/src/main/java/org/apache/causeway/applib/value/semantics/Converter.java+0 −3 modified@@ -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}
api/applib/src/main/java/org/apache/causeway/applib/value/semantics/DefaultsProvider.java+0 −3 modified@@ -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}
api/applib/src/main/java/org/apache/causeway/applib/value/semantics/OrderRelation.java+0 −3 modified@@ -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}
api/applib/src/main/java/org/apache/causeway/applib/ViewModel.java+11 −12 modified@@ -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();
commons/src/main/java/module-info.java+0 −1 modified@@ -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;
commons/src/main/java/org/apache/causeway/commons/internal/base/_Bytes.java+6 −0 modified@@ -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 /**
commons/src/main/java/org/apache/causeway/commons/internal/memento/_MementoDefault.java+0 −125 removed@@ -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, "...")); - } - } - -}
commons/src/main/java/org/apache/causeway/commons/internal/memento/_Mementos.java+0 −193 removed@@ -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; - } - -}
commons/src/main/java/org/apache/causeway/commons/internal/memento/package-info.java+0 −28 removed@@ -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
commons/src/main/java/org/apache/causeway/commons/internal/resources/_Serializables.java+56 −15 modified@@ -20,47 +20,57 @@ 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)) { oos.writeObject(object); 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); + } + } + }
core/metamodel/src/main/java/module-info.java+1 −0 modified@@ -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;
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java+15 −8 modified@@ -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); } }
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactory.java+22 −1 modified@@ -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;
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/SecureViewModelFacet.java+167 −0 added@@ -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)); + } +}
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetAbstract.java+0 −119 removed@@ -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); - -}
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetFactory.java+32 −6 modified@@ -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) {
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForDomainObjectAnnotation.java+26 −48 modified@@ -22,31 +22,31 @@ 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; 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.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()); - } - }
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForJavaRecord.java+27 −52 modified@@ -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())
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForSerializableInterface.java+24 −44 modified@@ -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()); } }
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForViewModelInterface.java+40 −42 modified@@ -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; }
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForXmlRootElementAnnotation.java+26 −51 modified@@ -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); - }
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/HmacUrlCodec.java+67 −0 added@@ -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)); + } + +}
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/HmacUtils.java+96 −0 added@@ -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; + } + +}
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/MementoHmacContext.java+55 −0 added@@ -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
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/Memento.java+60 −0 added@@ -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(); + +}
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/SecureMemento.java+119 −0 added@@ -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, "...")); + } + } +}
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/IdStringifierForSerializable.java+0 −113 removed@@ -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; - } - -}
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/TreeNodeValueSemantics.java+32 −19 modified@@ -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); - } - }
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/ValueCodec.java+33 −32 renamed@@ -16,59 +16,53 @@ * 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; import org.apache.causeway.applib.value.semantics.ValueDecomposition; 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
core/mmtest/src/test/java/org/apache/causeway/core/metamodel/valuesemantics/IdStringifierForSerializable_Test.java+0 −71 removed@@ -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); - } - -}
core/mmtestsupport/src/main/java/org/apache/causeway/core/mmtestsupport/MetaModelContext_forTesting.java+8 −3 modified@@ -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()) + ) ); }
core/runtimeservices/src/main/java/module-info.java+2 −2 modified@@ -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
core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java+17 −2 modified@@ -18,13 +18,17 @@ */ 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; import org.springframework.context.annotation.Import; 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.HmacAuthorityAutoconfigure.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 HmacAuthorityAutoconfigure { + @Bean(NAMESPACE + ".fallbackHmacAuthority") + @ConditionalOnMissingBean(HmacAuthority.class) + public HmacAuthority fallbackHmacAuthority() { + return HmacAuthority.HmacSHA256.randomInstance(); + } + } + }
core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/urlencoding/MementosTest.java+12 −24 modified@@ -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"));
core/security/src/test/java/org/apache/causeway/security/EncodabilityContractTest.java+3 −23 modified@@ -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
regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/MetaModelRegressionTest.java+13 −1 modified@@ -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);
regressiontests/factory/src/test/java/org/apache/causeway/testdomain/factory/ViewModelFactoryTest.java+8 −1 modified@@ -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(); }
viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/errors/ExceptionStackTracePanel.java+16 −11 modified@@ -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,27 +89,31 @@ 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); }, ()->{ 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>")) : "";
viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java+14 −7 modified@@ -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);
e66290fe9be8CAUSEWAY-3939: java-doc and typo fixes
2 files changed · +9 −7
api/applib/src/main/java/org/apache/causeway/applib/services/bookmark/HmacAuthority.java+7 −5 modified@@ -29,7 +29,7 @@ import lombok.SneakyThrows; /** - * Can be registered with Spring, to override the build in default, which has an application scoped random secret. + * Can be registered with Spring, to override the built in default, which has an application scoped random secret. * * <pre> * {@code @Configuration} @@ -41,10 +41,12 @@ * } * </pre> * - * <p>Note that bookmark's validity is bound to the (server-side) secret key of the {@link HmacAuthority}. + * <p>Used to generate the bookmark of view models so that they are not susceptible to forgery or de-serialization attacks. + * (Bookmarks of entities are not susceptible). + * Note though, that the bookmarks' 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' + * @apiNote the default bean is auto-configured with 'CausewayModuleCoreRuntimeServices.HmacAuthorityAutoconfigure' * * @since 3.5 */ @@ -71,7 +73,7 @@ default boolean verifyHmac(final @Nullable byte[] data, final @Nullable byte[] h /** * Whether HMAC length in bytes is expected/valid. */ - boolean isValidHmacLength(final int hmacLength); + boolean isValidHmacLength(int hmacLength); // -- IMPL @@ -116,6 +118,6 @@ private Mac newMac() { // JUNIT SUPPORT static HmacAuthority forTesting() { - return new HmacSHA256("secret for testing onyl".getBytes()); + return new HmacSHA256("secret for testing only".getBytes()); } }
core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java+2 −2 modified@@ -132,7 +132,7 @@ ExceptionRecognizerForDataAccessException.class, // auto configuration - CausewayModuleCoreRuntimeServices.HmacAutorityAutoconfigure.class + CausewayModuleCoreRuntimeServices.HmacAuthorityAutoconfigure.class }) @ComponentScan(basePackages = "org.apache.causeway.core.runtimeservices.icons") @@ -147,7 +147,7 @@ public OrderComparator orderComparator() { @AutoConfigureOrder(PriorityPrecedence.LATE) @Configuration - static class HmacAutorityAutoconfigure { + static class HmacAuthorityAutoconfigure { @Bean(NAMESPACE + ".fallbackHmacAuthority") @ConditionalOnMissingBean(HmacAuthority.class) public HmacAuthority fallbackHmacAuthority() {
e6bf00c63c33CAUSEWAY-3939: removal of IdStringifier for Serializable
44 files changed · +1198 −1090
api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/BookmarkNotFoundException.java+0 −4 modified@@ -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) {
api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/DigitalVerificationException.java+57 −0 added@@ -0,0 +1,57 @@ +/* + * 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.exceptions.unrecoverable; + +import org.apache.causeway.applib.exceptions.UnrecoverableException; +import org.apache.causeway.applib.services.i18n.TranslatableString; + +/** + * Indicates that a digital verification failed. + * + * <p>E.g. could be an invalid (or no longer valid) bookmark. + * + * @since 3.5 {@index} + * + */ +public class DigitalVerificationException extends UnrecoverableException { + private static final long serialVersionUID = 1L; + + public DigitalVerificationException(final String msg) { + super(msg); + } + + public DigitalVerificationException(final TranslatableString translatableMessage, + final Class<?> translationContextClass, final String translationContextMethod) { + super(translatableMessage, translationContextClass, translationContextMethod); + } + + public DigitalVerificationException(final Throwable cause) { + super(cause); + } + + public DigitalVerificationException(final String msg, final Throwable cause) { + super(msg, cause); + } + + 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
api/applib/src/main/java/org/apache/causeway/applib/layout/grid/bootstrap/BSUtil.java+3 −6 modified@@ -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) {
api/applib/src/main/java/org/apache/causeway/applib/services/bookmark/HmacAuthority.java+121 −0 added@@ -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()); + } +}
api/applib/src/main/java/org/apache/causeway/applib/services/urlencoding/UrlEncodingService.java+9 −17 modified@@ -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)); } }; - } }
api/applib/src/main/java/org/apache/causeway/applib/value/semantics/Converter.java+0 −3 modified@@ -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}
api/applib/src/main/java/org/apache/causeway/applib/value/semantics/DefaultsProvider.java+0 −3 modified@@ -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}
api/applib/src/main/java/org/apache/causeway/applib/value/semantics/OrderRelation.java+0 −3 modified@@ -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}
api/applib/src/main/java/org/apache/causeway/applib/ViewModel.java+11 −12 modified@@ -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();
commons/src/main/java/module-info.java+0 −1 modified@@ -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;
commons/src/main/java/org/apache/causeway/commons/internal/base/_Bytes.java+6 −0 modified@@ -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 /**
commons/src/main/java/org/apache/causeway/commons/internal/memento/_MementoDefault.java+0 −125 removed@@ -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, "...")); - } - } - -}
commons/src/main/java/org/apache/causeway/commons/internal/memento/_Mementos.java+0 −193 removed@@ -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; - } - -}
commons/src/main/java/org/apache/causeway/commons/internal/memento/package-info.java+0 −28 removed@@ -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
commons/src/main/java/org/apache/causeway/commons/internal/resources/_Serializables.java+56 −15 modified@@ -20,47 +20,57 @@ 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)) { oos.writeObject(object); 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); + } + } + }
core/metamodel/src/main/java/module-info.java+1 −0 modified@@ -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;
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java+15 −8 modified@@ -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); } }
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactory.java+22 −1 modified@@ -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;
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/SecureViewModelFacet.java+167 −0 added@@ -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)); + } +}
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetAbstract.java+0 −119 removed@@ -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); - -}
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetFactory.java+32 −6 modified@@ -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) {
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForDomainObjectAnnotation.java+26 −48 modified@@ -22,31 +22,31 @@ 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; 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.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()); - } - }
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForJavaRecord.java+27 −52 modified@@ -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())
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForSerializableInterface.java+24 −44 modified@@ -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()); } }
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForViewModelInterface.java+40 −42 modified@@ -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; }
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForXmlRootElementAnnotation.java+26 −51 modified@@ -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); - }
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/HmacUrlCodec.java+67 −0 added@@ -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)); + } + +}
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/HmacUtils.java+96 −0 added@@ -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; + } + +}
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/MementoHmacContext.java+55 −0 added@@ -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
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/Memento.java+60 −0 added@@ -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(); + +}
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/SecureMemento.java+119 −0 added@@ -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, "...")); + } + } +}
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/IdStringifierForSerializable.java+0 −113 removed@@ -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; - } - -}
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/TreeNodeValueSemantics.java+32 −19 modified@@ -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); - } - }
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/ValueCodec.java+33 −32 renamed@@ -16,59 +16,53 @@ * 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; import org.apache.causeway.applib.value.semantics.ValueDecomposition; 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
core/mmtest/src/test/java/org/apache/causeway/core/metamodel/valuesemantics/IdStringifierForSerializable_Test.java+0 −71 removed@@ -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); - } - -}
core/mmtestsupport/src/main/java/org/apache/causeway/core/mmtestsupport/MetaModelContext_forTesting.java+8 −3 modified@@ -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()) + ) ); }
core/runtimeservices/src/main/java/module-info.java+2 −2 modified@@ -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
core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java+17 −2 modified@@ -18,13 +18,17 @@ */ 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; import org.springframework.context.annotation.Import; 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(); + } + } + }
core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/urlencoding/MementosTest.java+12 −24 modified@@ -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"));
core/security/src/test/java/org/apache/causeway/security/EncodabilityContractTest.java+3 −23 modified@@ -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
regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/MetaModelRegressionTest.java+13 −1 modified@@ -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);
regressiontests/factory/src/test/java/org/apache/causeway/testdomain/factory/ViewModelFactoryTest.java+8 −1 modified@@ -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(); }
viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/errors/ExceptionStackTracePanel.java+16 −11 modified@@ -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,27 +89,31 @@ 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); }, ()->{ 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>")) : "";
viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java+14 −7 modified@@ -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);
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-wq4c-57mh-5f7gghsaADVISORY
- lists.apache.org/thread/rjlg4spqhmgy1xgq9wq5h2tfnq4pm70bghsavendor-advisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2025-64408ghsaADVISORY
- www.openwall.com/lists/oss-security/2025/11/19/1ghsaWEB
- github.com/apache/causeway/commit/bef00f58d2a2cba9a45230c9d117a0327e4c7038ghsaWEB
- github.com/apache/causeway/commit/e66290fe9be87aa0e4c5dc55bce1993a54330624ghsaWEB
- github.com/apache/causeway/commit/e6bf00c63c33cfa894a19d6122526a1aec227d14ghsaWEB
- issues.apache.org/jira/browse/CAUSEWAY-3939ghsaWEB
News mentions
0No linked articles in our index yet.