This is an automated email from the ASF dual-hosted git repository.
ahuber pushed a commit to branch spring6
in repository https://gitbox.apache.org/repos/asf/causeway.git
The following commit(s) were added to refs/heads/spring6 by this push:
new 10df0582c7 CAUSEWAY-3690: adds ViewModelFacet for Java record, to
support bookmarking
10df0582c7 is described below
commit 10df0582c738302fbe1cc427e26265a44367bd04
Author: Andi Huber <[email protected]>
AuthorDate: Tue Mar 5 10:43:05 2024 +0100
CAUSEWAY-3690: adds ViewModelFacet for Java record, to support
bookmarking
---
.../object/viewmodel/ViewModelFacetFactory.java | 5 +-
.../viewmodel/ViewModelFacetForJavaRecord.java | 153 +++++++++++++++++++++
.../DomainModelTest_usingGoodDomain.java | 21 ++-
3 files changed, 174 insertions(+), 5 deletions(-)
diff --git
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetFactory.java
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetFactory.java
index 684ea192a0..028a48e193 100644
---
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetFactory.java
+++
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetFactory.java
@@ -68,7 +68,10 @@ implements
// either ViewModel interface (highest precedence)
ViewModelFacetForViewModelInterface.create(type, facetHolder)
// or Serializable interface (if any)
- .or(()->ViewModelFacetForSerializableInterface.create(type,
facetHolder)));
+ .or(()->ViewModelFacetForSerializableInterface.create(type,
facetHolder))
+ // or else Java record (if any)
+ .or(()->ViewModelFacetForJavaRecord.create(type, facetHolder))
+ );
// DomainObject(nature=VIEW_MODEL) is managed by the
DomainObjectAnnotationFacetFactory as a fallback strategy
}
diff --git
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForJavaRecord.java
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForJavaRecord.java
new file mode 100644
index 0000000000..e94b61cca3
--- /dev/null
+++
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForJavaRecord.java
@@ -0,0 +1,153 @@
+/*
+ * 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.lang.reflect.Constructor;
+import java.lang.reflect.RecordComponent;
+import java.util.Arrays;
+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.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 lombok.NonNull;
+import lombok.SneakyThrows;
+import lombok.val;
+
+/**
+ * @since 3.0.0
+ */
+public class ViewModelFacetForJavaRecord
+extends ViewModelFacetAbstract {
+
+ public static Optional<ViewModelFacetForJavaRecord> create(
+ final Class<?> cls,
+ final FacetHolder holder) {
+ return cls.isRecord()
+ ? Optional.of(new ViewModelFacetForJavaRecord(cls, holder))
+ : Optional.empty();
+ }
+
+ private UrlEncodingService codec;
+ private SerializingAdapter serializer;
+ private Constructor<?> canonicalConstructor;
+
+ protected ViewModelFacetForJavaRecord(
+ final Class<?> recordClass,
+ final FacetHolder holder) {
+ // is overruled by ViewModel interface semantics
+ super(holder, Precedence.DEFAULT);
+ this.canonicalConstructor = canonicalConstructor(recordClass);
+ }
+
+ @Override @SneakyThrows
+ protected ManagedObject createViewmodel(
+ @NonNull final ObjectSpecification viewmodelSpec,
+ @NonNull final Bookmark bookmark) {
+
+ val memento = parseMemento(bookmark);
+
+ var recordComponentPojos = streamRecordComponents(viewmodelSpec)
+ .map(association->{
+ val associationId = association.getId();
+ val elementType = association.getElementType();
+ val elementClass = elementType.getCorrespondingClass();
+ val associationPojo = association.isProperty()
+ ? memento.get(associationId, elementClass)
+ //TODO collection values not yet supported by memento (as
workaround use Serializable record)
+ : null;
+ return associationPojo;
+ }).toArray();
+
+ return
getObjectManager().adapt(canonicalConstructor.newInstance(recordComponentPojos));
+ }
+
+ @Override
+ public String serialize(final ManagedObject viewModel) {
+
+ final _Mementos.Memento memento = newMemento();
+
+ val viewmodelSpec = viewModel.getSpecification();
+
+ streamRecordComponents(viewmodelSpec)
+ .forEach(association->{
+
+ final ManagedObject associationValue =
+ association.get(viewModel,
InteractionInitiatedBy.PASS_THROUGH);
+
+ if(association != null
+ //TODO collection values not yet supported by memento (as
workaround use Serializable record)
+ && association.isProperty()
+ && associationValue.getPojo()!=null) {
+ memento.put(association.getId(), associationValue.getPojo());
+ }
+ });
+
+ return memento.asString();
+ }
+
+ // -- HELPER
+
+ private Stream<ObjectAssociation> streamRecordComponents(
+ final @NonNull ObjectSpecification viewmodelSpec) {
+ return
Stream.of(viewmodelSpec.getCorrespondingClass().getRecordComponents())
+ .map(RecordComponent::getName)
+ .map(memberId->viewmodelSpec.getAssociationElseFail(memberId,
MixedIn.EXCLUDED));
+ }
+
+ private void initDependencies() {
+ val 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.getIdentifier());
+ }
+
+ @SneakyThrows
+ private static <T> Constructor<T> canonicalConstructor(final @NonNull
Class<T> recordClass) {
+ val constructorParamTypes =
Arrays.stream(recordClass.getRecordComponents())
+ .map(RecordComponent::getType)
+ .toArray(Class[]::new);
+ return recordClass.getDeclaredConstructor(constructorParamTypes);
+ }
+
+}
diff --git
a/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/DomainModelTest_usingGoodDomain.java
b/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/DomainModelTest_usingGoodDomain.java
index 48b8660a47..deebbe4930 100644
---
a/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/DomainModelTest_usingGoodDomain.java
+++
b/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/DomainModelTest_usingGoodDomain.java
@@ -19,6 +19,7 @@
package org.apache.causeway.testdomain.domainmodel;
import java.util.List;
+import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -55,6 +56,7 @@ import org.apache.causeway.commons.internal.base._Strings;
import org.apache.causeway.commons.internal.exceptions._Exceptions;
import org.apache.causeway.core.config.CausewayConfiguration;
import org.apache.causeway.core.config.presets.CausewayPresets;
+import org.apache.causeway.core.metamodel.context.MetaModelContext;
import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
import org.apache.causeway.core.metamodel.facets.all.named.MemberNamedFacet;
import
org.apache.causeway.core.metamodel.facets.members.publish.execution.ExecutionPublishingFacet;
@@ -62,6 +64,7 @@ import
org.apache.causeway.core.metamodel.facets.object.icon.IconFacet;
import
org.apache.causeway.core.metamodel.facets.object.introspection.IntrospectionPolicyFacet;
import org.apache.causeway.core.metamodel.facets.object.title.TitleFacet;
import
org.apache.causeway.core.metamodel.facets.object.viewmodel.ViewModelFacet;
+import
org.apache.causeway.core.metamodel.facets.object.viewmodel.ViewModelFacetForJavaRecord;
import
org.apache.causeway.core.metamodel.facets.objectvalue.mandatory.MandatoryFacet;
import
org.apache.causeway.core.metamodel.facets.objectvalue.mandatory.MandatoryFacet.Semantics;
import
org.apache.causeway.core.metamodel.facets.param.choices.ActionParameterChoicesFacet;
@@ -1018,7 +1021,21 @@ class DomainModelTest_usingGoodDomain extends
CausewayIntegrationTestAbstract {
@ParameterizedTest
@EnumSource(RecordScenario.class)
void javaRecordAsViewModel(final RecordScenario scenario) {
+ final Class<?> classUnderTest = scenario.recordClass;
final Object sample = scenario.samples.getFirstElseFail();
+ val viewModel =
MetaModelContext.instanceElseFail().getObjectManager().adapt(sample);
+ val elementType = viewModel.getSpecification();
+ val viewmodelFacet = elementType.getFacet(ViewModelFacet.class);
+
+ assertEquals(BeanSort.VIEW_MODEL, elementType.getBeanSort());
+ assertEquals(classUnderTest.getName(),
elementType.getFeatureIdentifier().getLogicalTypeName());
+
assertTrue(ViewModelFacetForJavaRecord.class.isInstance(viewmodelFacet),
+ ()->"Record is expected to have a
ViewModelFacetForJavaRecord");
+
+ val bookmark = viewmodelFacet.serializeToBookmark(viewModel);
+ val viewModelAfterRoundTrip = viewmodelFacet.instantiate(elementType,
Optional.of(bookmark));
+ assertEquals(viewModel.getPojo(), viewModelAfterRoundTrip.getPojo());
+
val isExpectedExplicitlyAnnotated = scenario ==
RecordScenario.ANNOTATED;
val additionalString = testerFactory
@@ -1060,13 +1077,9 @@ class DomainModelTest_usingGoodDomain extends
CausewayIntegrationTestAbstract {
final Class<?> classUnderTest = scenario.recordClass;
var dataTable = DataTable.forDomainType(classUnderTest);
- var spec = dataTable.getElementType();
dataTable.setDataElementPojos(scenario.samples);
- assertEquals(BeanSort.VIEW_MODEL, spec.getBeanSort());
- assertEquals(classUnderTest.getName(),
spec.getFeatureIdentifier().getLogicalTypeName());
assertEquals(scenario.classFriendlyName(),
dataTable.getTableFriendlyName());
-
assertEquals(scenario.samples.size(), dataTable.getDataRows().size());
assertEquals(4, dataTable.getDataColumns().size());