This is an automated email from the ASF dual-hosted git repository. ahuber pushed a commit to branch v3 in repository https://gitbox.apache.org/repos/asf/causeway.git
The following commit(s) were added to refs/heads/v3 by this push: new 07afacee957 CAUSEWAY-2297: work on simplified tree model (part 11) 07afacee957 is described below commit 07afacee95714706109fd840d70f561546606209 Author: Andi Huber <ahu...@apache.org> AuthorDate: Sat Dec 14 08:30:00 2024 +0100 CAUSEWAY-2297: work on simplified tree model (part 11) - work on navigable subtree facets (WIP) --- .../causeway/applib/graph/tree/TreeNode.java | 25 ++--- .../commons/semantics/AccessorSemantics.java | 66 +++++++++--- .../core/metamodel/commons/MethodUtil.java | 93 ++++------------- .../core/metamodel/facets/FacetFactory.java | 8 +- .../metamodel/facets/ObjectTypeFacetFactory.java | 2 +- ...ropertyOrCollectionIdentifyingFacetFactory.java | 77 +++++++------- ...rCollectionIdentifyingFacetFactoryAbstract.java | 12 ++- .../CollectionAccessorFacetViaAccessorFactory.java | 48 +-------- .../layout/CollectionLayoutFacetFactory.java | 31 ++++-- .../object/navchild/NavigableSubtreeFacet.java | 55 ++++++++++ .../NavigableSubtreeFacetPostProcessor.java | 51 ++++++++++ .../navchild/NavigableSubtreeFacetRecord.java | 91 +++++++++++++++++ .../navchild/NavigableSubtreeSequenceFacet.java | 60 +++++++++++ .../NavigableSubtreeSequenceFacetRecord.java | 58 +++++++++++ .../facets/object/navchild/ObjectTreeAdapter.java | 53 ++++++++++ .../object/navparent/NavigableParentFacet.java | 2 - .../NavigableParentAnnotationFacetFactory.java | 8 +- .../PropertyAccessorFacetViaAccessorFactory.java | 55 ++-------- .../metamodel/spec/impl/FacetedMethodsBuilder.java | 15 ++- .../spec/impl/ProgrammingModelDefault.java | 3 + .../specloader/facetprocessor/FacetProcessor.java | 28 +++--- .../causeway/core/metamodel/facets/_Utils.java | 5 +- .../navchild/NavigableSubtreeFacetFactoryTest.java | 112 +++++++++++++++++++++ .../facets/object/navchild/TreeTraversalTest.java | 81 +++++++++++++++ .../facets/object/navchild/_TreeSample.java | 86 ++++++++++++++++ 25 files changed, 841 insertions(+), 284 deletions(-) diff --git a/api/applib/src/main/java/org/apache/causeway/applib/graph/tree/TreeNode.java b/api/applib/src/main/java/org/apache/causeway/applib/graph/tree/TreeNode.java index 4a95ca2ea03..b7fac25280f 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/graph/tree/TreeNode.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/graph/tree/TreeNode.java @@ -29,7 +29,6 @@ import java.util.stream.StreamSupport; import org.springframework.lang.Nullable; -import org.apache.causeway.applib.annotation.Programmatic; import org.apache.causeway.applib.graph.Edge; import org.apache.causeway.applib.graph.SimpleEdge; import org.apache.causeway.applib.graph.Vertex; @@ -40,7 +39,7 @@ import org.apache.causeway.commons.internal.base._NullSafe; import lombok.NonNull; /** - * Fundamental building block of Tree structures. + * Fundamental building block for tree structures. * <p> * Wraps a node value and holds references to related nodes. * @@ -72,34 +71,34 @@ implements Vertex<T> { * Creates the root node of a tree structure as inferred from given treeAdapter. */ public static <T> TreeNode<T> root( - final @NonNull T rootNode, + final @NonNull T rootValue, final @NonNull TreeAdapter<T> treeAdapter) { - return TreeNode.root(rootNode, treeAdapter, TreeState.rootCollapsed()); + return TreeNode.root(rootValue, treeAdapter, TreeState.rootCollapsed()); } /** * Creates the root node of a tree structure as inferred from given treeAdapter. */ public static <T> TreeNode<T> root( - final @NonNull T rootNode, + final @NonNull T rootValue, final @NonNull Class<? extends TreeAdapter<T>> treeAdapterClass, final @NonNull FactoryService factoryService) { - return root(rootNode, factoryService.getOrCreate(treeAdapterClass)); + return root(rootValue, factoryService.getOrCreate(treeAdapterClass)); } public static <T> TreeNode<T> root( - final T value, + final T rootValue, final Class<? extends TreeAdapter<T>> treeAdapterClass, final TreeState sharedState, final FactoryService factoryService) { - return root(value, factoryService.getOrCreate(treeAdapterClass)); + return root(rootValue, factoryService.getOrCreate(treeAdapterClass)); } public static <T> TreeNode<T> root( - final T value, + final T rootValue, final TreeAdapter<T> treeAdapter, final TreeState sharedState) { - return new TreeNode<T>(null, TreePath.root(), value, treeAdapter, sharedState); + return new TreeNode<T>(null, TreePath.root(), rootValue, treeAdapter, sharedState); } // -- @@ -224,7 +223,6 @@ implements Vertex<T> { * Adds {@code treePaths} to the set of expanded nodes, as held by this tree's shared state object. * @param treePaths */ - @Programmatic public void expand(final TreePath ... treePaths) { final Set<TreePath> expandedPaths = treeState().expandedNodePaths(); _NullSafe.stream(treePaths).forEach(expandedPaths::add); @@ -233,7 +231,6 @@ implements Vertex<T> { /** * Expands this node and all its parents. */ - @Programmatic public void expand() { final Set<TreePath> expandedPaths = treeState().expandedNodePaths(); streamHierarchyUp() @@ -245,7 +242,6 @@ implements Vertex<T> { * Removes {@code treePaths} from the set of expanded nodes, as held by this tree's shared state object. * @param treePaths */ - @Programmatic public void collapse(final TreePath ... treePaths) { final Set<TreePath> expandedPaths = treeState().expandedNodePaths(); _NullSafe.stream(treePaths).forEach(expandedPaths::remove); @@ -257,7 +253,6 @@ implements Vertex<T> { * Clears all selection markers. * @see #select(TreePath...) */ - @Programmatic public void clearSelection() { treeState().selectedNodePaths().clear(); } @@ -266,7 +261,6 @@ implements Vertex<T> { * Whether node that corresponds to given {@link TreePath} has a selection marker. * @see #select(TreePath...) */ - @Programmatic public boolean isSelected(final TreePath treePath) { final Set<TreePath> selectedPaths = treeState().selectedNodePaths(); return selectedPaths.contains(treePath); @@ -284,7 +278,6 @@ implements Vertex<T> { * } * </pre> */ - @Programmatic public void select(final TreePath ... treePaths) { final Set<TreePath> selectedPaths = treeState().selectedNodePaths(); _NullSafe.stream(treePaths).forEach(selectedPaths::add); diff --git a/commons/src/main/java/org/apache/causeway/commons/semantics/AccessorSemantics.java b/commons/src/main/java/org/apache/causeway/commons/semantics/AccessorSemantics.java index c2f1bfeab0b..56083149030 100644 --- a/commons/src/main/java/org/apache/causeway/commons/semantics/AccessorSemantics.java +++ b/commons/src/main/java/org/apache/causeway/commons/semantics/AccessorSemantics.java @@ -50,7 +50,37 @@ public enum AccessorSemantics { : false; } - // -- UTILITY + // -- HIGH LEVEL PREDICATES + + public static boolean isPropertyAccessor(final ResolvedMethod method) { + return isPropertyAccessorCandidate(method) + ? !hasCollectionSemantics(method.returnType()) + : false; + } + + public static boolean isCollectionAccessor(final ResolvedMethod method) { + return isCollectionAccessorCandidate(method) + ? hasCollectionSemantics(method.returnType()) + : false; + } + + public static boolean hasSupportedNonScalarMethodReturnType(final ResolvedMethod method) { + return isNonBooleanGetter(method, Iterable.class) + && CollectionSemantics.valueOf(method.returnType()).isPresent(); + } + + // -- LOW LEVEL PREDICATES + + public static boolean isPropertyAccessorCandidate(final ResolvedMethod method) { + return isRecordComponentAccessor(method) + || isGetter(method); + } + + public static boolean isCollectionAccessorCandidate(final ResolvedMethod method) { + return isRecordComponentAccessor(method) + || isNonBooleanGetter(method); + } + public static boolean isCandidateGetterName(final @Nullable String name) { return GET.isPrefixOf(name) @@ -65,11 +95,9 @@ public enum AccessorSemantics { || method.returnType() == Boolean.class); } - public static boolean isNonBooleanGetter(final ResolvedMethod method, final Predicate<Class<?>> typeFilter) { - return GET.isPrefixOf(method.name()) - && method.isNoArg() - && !method.isStatic() - && typeFilter.test(method.returnType()); + public static boolean isGetter(final ResolvedMethod method) { + return isBooleanGetter(method) + || isNonBooleanGetter(method); } public static boolean isNonBooleanGetter(final ResolvedMethod method, final Class<?> expectedType) { @@ -77,9 +105,15 @@ public enum AccessorSemantics { expectedType.isAssignableFrom(ClassUtils.resolvePrimitiveIfNecessary(type))); } - public static boolean isGetter(final ResolvedMethod method) { - return isBooleanGetter(method) - || isNonBooleanGetter(method, type->type != void.class); + public static boolean isNonBooleanGetter(final ResolvedMethod method) { + return isNonBooleanGetter(method, type->type != void.class); + } + + private static boolean isNonBooleanGetter(final ResolvedMethod method, final Predicate<Class<?>> typeFilter) { + return GET.isPrefixOf(method.name()) + && method.isNoArg() + && !method.isStatic() + && typeFilter.test(method.returnType()); } /** @@ -89,17 +123,21 @@ public enum AccessorSemantics { var recordClass = method.implementationClass(); if(!recordClass.isRecord()) return false; for(var recordComponent : recordClass.getRecordComponents()) { - if(method.name().equals(recordComponent.getName())) { - return true; - } + if(method.name().equals(recordComponent.getName())) return true; } return false; } public static String associationIdentifierFor(final ResolvedMethod method) { - final String id = AccessorSemantics.isRecordComponentAccessor(method) + return AccessorSemantics.isRecordComponentAccessor(method) ? method.name() : Introspector.decapitalize(_Strings.baseName(method.name())); - return id; + } + + // -- HELPER + + private static boolean hasCollectionSemantics(final Class<?> cls) { + return CollectionSemantics.valueOf(cls) + .isPresent(); } } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/commons/MethodUtil.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/commons/MethodUtil.java index eb59b5eeaeb..18e609759f3 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/commons/MethodUtil.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/commons/MethodUtil.java @@ -24,7 +24,6 @@ import java.util.function.Predicate; import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod; -import org.apache.causeway.commons.semantics.AccessorSemantics; import org.apache.causeway.commons.semantics.CollectionSemantics; import lombok.experimental.UtilityClass; @@ -62,8 +61,8 @@ public class MethodUtil { public static boolean isScalar(final ResolvedMethod method) { return isNotVoid(method) - && CollectionSemantics.valueOf(method.returnType()) - .isEmpty(); + && CollectionSemantics.valueOf(method.returnType()) + .isEmpty(); } @UtilityClass @@ -78,14 +77,8 @@ public class MethodUtil { final Can<Class<?>> matchingParamTypes) { return method -> { // check params (if required) - - if(matchingParamTypes.isEmpty()) { - return true; - } - - if(method.paramCount()<(paramIndexOffset+matchingParamTypes.size())) { - return false; - } + if(matchingParamTypes.isEmpty()) return true; + if(method.paramCount()<(paramIndexOffset+matchingParamTypes.size())) return false; final Class<?>[] parameterTypes = method.paramTypes(); @@ -93,13 +86,9 @@ public class MethodUtil { var left = parameterTypes[paramIndexOffset + c]; var right = matchingParamTypes.getElseFail(paramIndexOffset); - if(!Objects.equals(left, right)) { - return false; - } + if(!Objects.equals(left, right)) return false; } - return true; - }; } @@ -110,87 +99,45 @@ public class MethodUtil { final String methodName, final Class<?> returnType, final Class<?>[] paramTypes) { - return method -> { - - if (!isPublic(method)) { - return false; - } - - if (isStatic(method)) { - return false; - } - + if (!isPublic(method)) return false; + if (isStatic(method)) return false; // check for name - if (!method.name().equals(methodName)) { - return false; - } - + if (!method.name().equals(methodName)) return false; // check for return type - if (returnType != null && returnType != method.returnType()) { - return false; - } - + if (returnType != null && returnType != method.returnType()) return false; // check params (if required) if (paramTypes != null) { final Class<?>[] parameterTypes = method.paramTypes(); - if (paramTypes.length != parameterTypes.length) { - return false; - } + if (paramTypes.length != parameterTypes.length) return false; for (int c = 0; c < paramTypes.length; c++) { - if ((paramTypes[c] != null) && (paramTypes[c] != parameterTypes[c])) { + if ((paramTypes[c] != null) + && (paramTypes[c] != parameterTypes[c])) { return false; } } } - return true; }; - } /** * @return whether the method under test matches the given constraints */ public static Predicate<ResolvedMethod> prefixed( - final String prefix, final Class<?> returnType, final CanBeVoid canBeVoid, final int paramCount) { - + final String prefix, + final Class<?> returnType, + final CanBeVoid canBeVoid, + final int paramCount) { return method -> { - if (MethodUtil.isStatic(method)) { - return false; - } - if(!method.name().startsWith(prefix)) { - return false; - } - if(method.paramCount() != paramCount) { - return false; - } - var type = method.returnType(); - if(!ClassExtensions.isCompatibleAsReturnType(returnType, canBeVoid, type)) { - return false; - } - + if(MethodUtil.isStatic(method)) return false; + if(!method.name().startsWith(prefix)) return false; + if(method.paramCount() != paramCount) return false; + if(!ClassExtensions.isCompatibleAsReturnType(returnType, canBeVoid, method.returnType())) return false; return true; }; - - } - - public static Predicate<ResolvedMethod> booleanGetter() { - return AccessorSemantics::isBooleanGetter; - } - - public static Predicate<ResolvedMethod> nonBooleanGetter(final Class<?> returnType) { - return method->AccessorSemantics.isNonBooleanGetter(method, returnType); } - - public static Predicate<ResolvedMethod> supportedNonScalarMethodReturnType() { - return method-> - AccessorSemantics.isNonBooleanGetter(method, Iterable.class) - && CollectionSemantics.valueOf(method.returnType()) - .isPresent(); - } - } } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/FacetFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/FacetFactory.java index 276fdd7273b..8faf739c798 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/FacetFactory.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/FacetFactory.java @@ -230,13 +230,7 @@ public interface FacetFactory { @Getter private final boolean mixinMain; /** - * @param cls - * @param featureType - * @param method - * @param methodRemover - * @param facetedMethod - * @param isMixinMain - * - Whether we are currently processing a mixin type AND this context's method can be identified + * @param isMixinMain whether we are currently processing a mixin type AND this context's method can be identified * as the main method of the processed mixin class. (since 2.0) */ public ProcessMethodContext( diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/ObjectTypeFacetFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/ObjectTypeFacetFactory.java index e7fea58fa9e..88cc4ff0147 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/ObjectTypeFacetFactory.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/ObjectTypeFacetFactory.java @@ -45,6 +45,6 @@ public interface ObjectTypeFacetFactory extends FacetFactory { } } - void process(ProcessObjectTypeContext processClassContext); + void process(ProcessObjectTypeContext processObjectTypeContext); } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/PropertyOrCollectionIdentifyingFacetFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/PropertyOrCollectionIdentifyingFacetFactory.java index 7ad3144154f..9f4483012e6 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/PropertyOrCollectionIdentifyingFacetFactory.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/PropertyOrCollectionIdentifyingFacetFactory.java @@ -23,61 +23,62 @@ import java.util.List; import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod; import org.apache.causeway.core.metamodel.facetapi.MethodRemover; -import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation; -import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation; /** * A {@link FacetFactory} implementation that is able to identify a property or * collection. - * * <p> * For example, a <i>getter</i> method is most commonly used to represent either * a property (value or reference) or a collection, with the return type * indicating which. - * * <p> * Used by the Java 8 Reflector's <tt>ProgrammingModel</tt> to determine which * facet factories to ask whether a {@link Method} represents a property or a * collection. - * */ public interface PropertyOrCollectionIdentifyingFacetFactory extends FacetFactory { - /** - * Whether (this facet is able to determine that) the supplied - * {@link Method} possibly represents the accessor of either a - * {@link OneToOneAssociation reference property} - * or a {@link OneToManyAssociation collection}. - * - * <p> - * For example, if a method name has a prefix of <tt>get</tt> or - * alternatively has a prefix of <tt>is</tt> and returns a <tt>boolean</tt>, - * then it would be a candidate. - */ - public boolean isPropertyOrCollectionGetterCandidate(ResolvedMethod method); +// /** +// * Whether (this facet is able to determine that) the supplied +// * {@link Method} possibly represents the accessor of either a +// * {@link OneToOneAssociation reference property} +// * or a {@link OneToManyAssociation collection}. +// * <p> +// * For example, if a method name has a prefix of <tt>get</tt> or +// * alternatively has a prefix of <tt>is</tt> and returns a <tt>boolean</tt>, +// * then it would be a candidate. +// */ +// boolean isPropertyOrCollectionGetterCandidate(ResolvedMethod method); - /** - * Whether (this facet is able to determine that) the supplied - * {@link Method} represents <i>either</i> a - * {@link OneToOneAssociation reference property}. - */ - public boolean isPropertyAccessor(ResolvedMethod method); + boolean supportsProperties(); + boolean supportsCollections(); - /** - * Whether (this facet is able to determine that) the supplied - * {@link Method} represents a {@link OneToManyAssociation collection}. - */ - public boolean isCollectionAccessor(ResolvedMethod method); + boolean isAssociationAccessor(ResolvedMethod method); + void findAndRemoveAccessors(MethodRemover methodRemover, List<ResolvedMethod> methodListToAppendTo); - /** - * Use the provided {@link MethodRemover} to remove all reference property - * accessors, and append them to the supplied methodList. - */ - public void findAndRemovePropertyAccessors(MethodRemover methodRemover, List<ResolvedMethod> methodListToAppendTo); - /** - * Use the provided {@link MethodRemover} to remove all collection - * accessors, and append them to the supplied methodList. - */ - public void findAndRemoveCollectionAccessors(MethodRemover methodRemover, List<ResolvedMethod> methodListToAppendTo); +// /** +// * Whether (this facet is able to determine that) the supplied +// * {@link Method} represents a +// * {@link OneToOneAssociation reference property}. +// */ +// boolean isPropertyAccessor(ResolvedMethod method); +// +// /** +// * Whether (this facet is able to determine that) the supplied +// * {@link Method} represents a {@link OneToManyAssociation collection}. +// */ +// boolean isCollectionAccessor(ResolvedMethod method); +// +// /** +// * Use the provided {@link MethodRemover} to remove all reference property +// * accessors, and append them to the supplied methodList. +// */ +// void findAndRemovePropertyAccessors(MethodRemover methodRemover, List<ResolvedMethod> methodListToAppendTo); +// +// /** +// * Use the provided {@link MethodRemover} to remove all collection +// * accessors, and append them to the supplied methodList. +// */ +// void findAndRemoveCollectionAccessors(MethodRemover methodRemover, List<ResolvedMethod> methodListToAppendTo); } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/PropertyOrCollectionIdentifyingFacetFactoryAbstract.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/PropertyOrCollectionIdentifyingFacetFactoryAbstract.java index b699e4117c1..4e0f778abdd 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/PropertyOrCollectionIdentifyingFacetFactoryAbstract.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/PropertyOrCollectionIdentifyingFacetFactoryAbstract.java @@ -20,7 +20,6 @@ package org.apache.causeway.core.metamodel.facets; import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.collections.ImmutableEnumSet; -import org.apache.causeway.commons.semantics.CollectionSemantics; import org.apache.causeway.core.metamodel.context.MetaModelContext; import org.apache.causeway.core.metamodel.facetapi.FeatureType; import org.apache.causeway.core.metamodel.methods.MethodPrefixBasedFacetFactoryAbstract; @@ -37,9 +36,14 @@ implements PropertyOrCollectionIdentifyingFacetFactory { super(mmc, featureTypes, OrphanValidation.DONT_VALIDATE, prefixes); } - protected boolean isNonScalar(final Class<?> cls) { - return CollectionSemantics.valueOf(cls) - .isPresent(); + @Override + public final boolean supportsProperties() { + return super.getFeatureTypes().contains(FeatureType.PROPERTY); + } + + @Override + public final boolean supportsCollections() { + return super.getFeatureTypes().contains(FeatureType.COLLECTION); } } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/collections/accessor/CollectionAccessorFacetViaAccessorFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/collections/accessor/CollectionAccessorFacetViaAccessorFactory.java index ec27ba5eeb3..d33c86620c2 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/collections/accessor/CollectionAccessorFacetViaAccessorFactory.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/collections/accessor/CollectionAccessorFacetViaAccessorFactory.java @@ -25,7 +25,6 @@ import jakarta.inject.Inject; import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod; import org.apache.causeway.commons.semantics.AccessorSemantics; -import org.apache.causeway.core.metamodel.commons.MethodUtil; import org.apache.causeway.core.metamodel.context.MetaModelContext; import org.apache.causeway.core.metamodel.facetapi.FeatureType; import org.apache.causeway.core.metamodel.facetapi.MethodRemover; @@ -43,10 +42,6 @@ extends PropertyOrCollectionIdentifyingFacetFactoryAbstract { @Override public void process(final ProcessMethodContext processMethodContext) { - attachAccessorFacetForAccessorMethod(processMethodContext); - } - - private void attachAccessorFacetForAccessorMethod(final ProcessMethodContext processMethodContext) { var accessorMethod = processMethodContext.getMethod().asMethodElseFail(); // no-arg method, should have a regular facade processMethodContext.removeMethod(accessorMethod); @@ -54,51 +49,18 @@ extends PropertyOrCollectionIdentifyingFacetFactoryAbstract { var typeSpec = getSpecificationLoader().loadSpecification(cls); var facetHolder = processMethodContext.getFacetHolder(); - addFacet( - new CollectionAccessorFacetViaAccessor( - typeSpec, accessorMethod, facetHolder)); - } - - // /////////////////////////////////////////////////////////////// - // PropertyOrCollectionIdentifyingFacetFactory impl. - // /////////////////////////////////////////////////////////////// - - @Override - public boolean isPropertyOrCollectionGetterCandidate(final ResolvedMethod method) { - return AccessorSemantics.GET.isPrefixOf(method.name()); - } - - @Override - public boolean isCollectionAccessor(final ResolvedMethod method) { - if (!isPropertyOrCollectionGetterCandidate(method)) { - return false; - } - final Class<?> methodReturnType = method.returnType(); - return isNonScalar(methodReturnType); - } - - /** - * The method way well represent a reference property, but this facet - * factory does not have any opinion on the matter. - */ - @Override - public boolean isPropertyAccessor(final ResolvedMethod method) { - return false; + addFacet(new CollectionAccessorFacetViaAccessor(typeSpec, accessorMethod, facetHolder)); } @Override - public void findAndRemoveCollectionAccessors( - final MethodRemover methodRemover, - final List<ResolvedMethod> methodListToAppendTo) { - methodRemover.removeMethods( - MethodUtil.Predicates.supportedNonScalarMethodReturnType(), - methodListToAppendTo::add); + public boolean isAssociationAccessor(final ResolvedMethod method) { + return AccessorSemantics.isCollectionAccessor(method); } @Override - public void findAndRemovePropertyAccessors( + public void findAndRemoveAccessors( final MethodRemover methodRemover, final List<ResolvedMethod> methodListToAppendTo) { - // does nothing + methodRemover.removeMethods(AccessorSemantics::hasSupportedNonScalarMethodReturnType, methodListToAppendTo::add); } } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/collections/layout/CollectionLayoutFacetFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/collections/layout/CollectionLayoutFacetFactory.java index ab447e7b820..d8e2619538b 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/collections/layout/CollectionLayoutFacetFactory.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/collections/layout/CollectionLayoutFacetFactory.java @@ -20,12 +20,15 @@ package org.apache.causeway.core.metamodel.facets.collections.layout; import jakarta.inject.Inject; +import org.springframework.util.StringUtils; + import org.apache.causeway.applib.annotation.CollectionLayout; import org.apache.causeway.core.metamodel.context.MetaModelContext; import org.apache.causeway.core.metamodel.facetapi.FeatureType; import org.apache.causeway.core.metamodel.facets.FacetFactoryAbstract; import org.apache.causeway.core.metamodel.facets.collections.layout.tabledec.TableDecoratorFacetForCollectionLayoutAnnotation; import org.apache.causeway.core.metamodel.facets.members.layout.order.LayoutOrderFacetFromCollectionLayoutAnnotation; +import org.apache.causeway.core.metamodel.facets.object.navchild.NavigableSubtreeSequenceFacet; import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailureUtils; public class CollectionLayoutFacetFactory @@ -47,42 +50,48 @@ extends FacetFactoryAbstract { .raiseAmbiguousMixinAnnotations(processMethodContext.getFacetHolder(), CollectionLayout.class)); addFacetIfPresent( - CssClassFacetForCollectionLayoutAnnotation + CssClassFacetForCollectionLayoutAnnotation .create(collectionLayoutIfAny, facetHolder)); addFacet( - DefaultViewFacetForCollectionLayoutAnnotation + DefaultViewFacetForCollectionLayoutAnnotation .create(collectionLayoutIfAny, facetHolder) .orElseGet(()->DefaultViewFacetAsConfigured.create(facetHolder))); addFacetIfPresent( - MemberDescribedFacetForCollectionLayoutAnnotation + MemberDescribedFacetForCollectionLayoutAnnotation .create(collectionLayoutIfAny, facetHolder)); addFacetIfPresent( - HiddenFacetForCollectionLayoutAnnotation + HiddenFacetForCollectionLayoutAnnotation .create(collectionLayoutIfAny, facetHolder)); addFacetIfPresent( - LayoutOrderFacetFromCollectionLayoutAnnotation + LayoutOrderFacetFromCollectionLayoutAnnotation .create(collectionLayoutIfAny, facetHolder)); addFacetIfPresent( - MemberNamedFacetForCollectionLayoutAnnotation + MemberNamedFacetForCollectionLayoutAnnotation .create(collectionLayoutIfAny, facetHolder)); addFacetIfPresent( - TableDecoratorFacetForCollectionLayoutAnnotation - .create(collectionLayoutIfAny, facetHolder)); + TableDecoratorFacetForCollectionLayoutAnnotation + .create(collectionLayoutIfAny, facetHolder)); addFacetIfPresent( - PagedFacetForCollectionLayoutAnnotation + PagedFacetForCollectionLayoutAnnotation .create(collectionLayoutIfAny, facetHolder)); addFacetIfPresent( - SortedByFacetForCollectionLayoutAnnotation + SortedByFacetForCollectionLayoutAnnotation .create(collectionLayoutIfAny, facetHolder)); - + + addFacetIfPresent( + collectionLayoutIfAny + .map(CollectionLayout::navigableSubtree) + .filter(StringUtils::hasLength) + .flatMap(sequence->NavigableSubtreeSequenceFacet.create( + processMethodContext.getCls(), processMethodContext.getMethod().asMethod(), sequence, facetHolder))); } } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navchild/NavigableSubtreeFacet.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navchild/NavigableSubtreeFacet.java new file mode 100644 index 00000000000..c6915241b5f --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navchild/NavigableSubtreeFacet.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.metamodel.facets.object.navchild; + +import java.lang.invoke.MethodHandle; +import java.util.Optional; + +import org.apache.causeway.applib.graph.tree.TreeAdapter; +import org.apache.causeway.commons.collections.Can; +import org.apache.causeway.core.metamodel.facetapi.Facet; +import org.apache.causeway.core.metamodel.facetapi.FacetHolder; +import org.apache.causeway.core.metamodel.util.DeweyOrderComparator; + +/** + * Provides the parent/child relationship information between pojos + * to derive a tree-structure. + * + * @since 3.2 + */ +public sealed interface NavigableSubtreeFacet +extends Facet, TreeAdapter<Object> +permits NavigableSubtreeFacetRecord { + + // -- FACTORY + + static <T> Optional<NavigableSubtreeFacet> create( + final Can<NavigableSubtreeSequenceFacet> navigableSubtreeSequenceFacets, + final FacetHolder facetHolder) { + if(navigableSubtreeSequenceFacets.isEmpty()) return Optional.empty(); + + var comparator = new DeweyOrderComparator(); + Can<MethodHandle> subNodesMethodHandles = navigableSubtreeSequenceFacets + .sorted((a, b)->comparator.compare(a.sequence(), b.sequence())) + .map(NavigableSubtreeSequenceFacet::methodHandle); + + return Optional.of(new NavigableSubtreeFacetRecord(subNodesMethodHandles, facetHolder)); + } + +} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navchild/NavigableSubtreeFacetPostProcessor.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navchild/NavigableSubtreeFacetPostProcessor.java new file mode 100644 index 00000000000..ceca8de2bbd --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navchild/NavigableSubtreeFacetPostProcessor.java @@ -0,0 +1,51 @@ +/* + * 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.navchild; + +import org.apache.causeway.commons.collections.Can; +import org.apache.causeway.core.metamodel.context.MetaModelContext; +import org.apache.causeway.core.metamodel.facetapi.FacetUtil; +import org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessorAbstract; +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.spec.feature.ObjectMember; + +/** + * Installs the {@link NavigableSubtreeFacet} + * as aggregated via {@link NavigableSubtreeSequenceFacet} collected from {@link ObjectAssociation}s. + * {@link ObjectMember}s of the {@link ObjectSpecification}. + */ +public class NavigableSubtreeFacetPostProcessor extends MetaModelPostProcessorAbstract { + + public NavigableSubtreeFacetPostProcessor(final MetaModelContext metaModelContext) { + super(metaModelContext); + } + + @Override + public void postProcessObject(final ObjectSpecification objSpec) { + var navigableSubtreeSequenceFacets = + objSpec.streamAssociations(MixedIn.EXCLUDED) + .flatMap(assoc->assoc.lookupFacet(NavigableSubtreeSequenceFacet.class).stream()) + .collect(Can.toCan()); + + FacetUtil.addFacetIfPresent(NavigableSubtreeFacet.create(navigableSubtreeSequenceFacets, objSpec)); + } + +} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navchild/NavigableSubtreeFacetRecord.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navchild/NavigableSubtreeFacetRecord.java new file mode 100644 index 00000000000..bacbfcc0ff8 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navchild/NavigableSubtreeFacetRecord.java @@ -0,0 +1,91 @@ +/* + * 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.navchild; + +import java.lang.invoke.MethodHandle; +import java.util.function.BiConsumer; +import java.util.stream.Stream; + +import org.springframework.util.ClassUtils; + +import org.apache.causeway.commons.collections.Can; +import org.apache.causeway.commons.internal.base._NullSafe; +import org.apache.causeway.commons.internal.exceptions._Exceptions; +import org.apache.causeway.core.metamodel.facetapi.Facet; +import org.apache.causeway.core.metamodel.facetapi.FacetHolder; + +import lombok.extern.log4j.Log4j2; + +@Log4j2 +record NavigableSubtreeFacetRecord ( + Can<MethodHandle> subNodesMethodHandles, + FacetHolder facetHolder) +implements NavigableSubtreeFacet { + + @Override + public Class<? extends Facet> facetType() { + return NavigableSubtreeFacet.class; + } + + @Override + public Precedence getPrecedence() { + return Precedence.DEFAULT; + } + + @Override + public FacetHolder getFacetHolder() { + return facetHolder; + } + + @Override + public final int childCountOf(final Object node) { + return subNodesMethodHandles.stream() + .mapToInt(mh->{ + try { + return _NullSafe.sizeAutodetect(mh.invoke(node)); + } catch (Throwable e) { + log.error("failed to invoke subNodesMethodHandle {}", + mh.toString(), e); + return 0; + } + }) + .sum(); + } + + @Override + public final Stream<Object> childrenOf(final Object node) { + return subNodesMethodHandles.stream() + .flatMap(mh->{ + try { + return _NullSafe.streamAutodetect(mh.invoke(node)); + } catch (Throwable e) { + throw _Exceptions.unrecoverable(e); + } + }); + } + + @Override + public void visitAttributes(final BiConsumer<String, Object> visitor) { + visitor.accept("facet", ClassUtils.getShortName(getClass())); + visitor.accept("precedence", getPrecedence().name()); + visitor.accept("subNodesMethodHandles", subNodesMethodHandles.map(MethodHandle::toString)); + } + +} + diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navchild/NavigableSubtreeSequenceFacet.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navchild/NavigableSubtreeSequenceFacet.java new file mode 100644 index 00000000000..3fbe37265d8 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navchild/NavigableSubtreeSequenceFacet.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.core.metamodel.facets.object.navchild; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.util.Optional; + +import org.apache.causeway.commons.functional.Try; +import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod; +import org.apache.causeway.core.metamodel.facetapi.Facet; +import org.apache.causeway.core.metamodel.facetapi.FacetHolder; + +/** + * Provides a MethodHandle and its associated Dewey order. + * + * @since 3.2 + */ +public sealed interface NavigableSubtreeSequenceFacet +extends Facet +permits NavigableSubtreeSequenceFacetRecord { + + String sequence(); + MethodHandle methodHandle(); + + // -- FACTORY + + static Optional<NavigableSubtreeSequenceFacet> create( + final Class<?> cls, + final Optional<ResolvedMethod> resolvedMethod, + final String sequence, + final FacetHolder facetHolder) { + + return resolvedMethod + .map(ResolvedMethod::method) + .flatMap(method->Try.call(()->MethodHandles + .privateLookupIn(cls, MethodHandles.lookup()) + .unreflect(method)) + .ifFailure(e->e.printStackTrace()) + .getValue() + .map(mh->new NavigableSubtreeSequenceFacetRecord(sequence, mh, facetHolder))); + } + +} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navchild/NavigableSubtreeSequenceFacetRecord.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navchild/NavigableSubtreeSequenceFacetRecord.java new file mode 100644 index 00000000000..0ffd5a612b7 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navchild/NavigableSubtreeSequenceFacetRecord.java @@ -0,0 +1,58 @@ +/* + * 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.navchild; + +import java.lang.invoke.MethodHandle; +import java.util.function.BiConsumer; + +import org.springframework.util.ClassUtils; + +import org.apache.causeway.core.metamodel.facetapi.Facet; +import org.apache.causeway.core.metamodel.facetapi.FacetHolder; + +record NavigableSubtreeSequenceFacetRecord( + String sequence, + MethodHandle methodHandle, + FacetHolder facetHolder) +implements NavigableSubtreeSequenceFacet { + + @Override + public Class<? extends Facet> facetType() { + return NavigableSubtreeSequenceFacet.class; + } + + @Override + public Precedence getPrecedence() { + return Precedence.DEFAULT; + } + + @Override + public FacetHolder getFacetHolder() { + return facetHolder; + } + + @Override + public void visitAttributes(final BiConsumer<String, Object> visitor) { + visitor.accept("facet", ClassUtils.getShortName(getClass())); + visitor.accept("precedence", getPrecedence().name()); + visitor.accept("sequence", sequence); + visitor.accept("methodHandle", methodHandle); + } + +} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navchild/ObjectTreeAdapter.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navchild/ObjectTreeAdapter.java new file mode 100644 index 00000000000..130abfd9411 --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navchild/ObjectTreeAdapter.java @@ -0,0 +1,53 @@ +/* + * 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.navchild; + +import java.util.Optional; +import java.util.stream.Stream; + +import org.apache.causeway.applib.graph.tree.TreeAdapter; +import org.apache.causeway.commons.internal.base._Casts; +import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; + +public record ObjectTreeAdapter(SpecificationLoader specLoader) +implements + TreeAdapter<Object> { + + @Override + public int childCountOf(final Object node) { + return treeNodeFacet(node) + .map(treeNodeFacet->treeNodeFacet.childCountOf(node)) + .orElse(0); + } + @Override + public Stream<Object> childrenOf(final Object node) { + return treeNodeFacet(node) + .map(treeNodeFacet->treeNodeFacet.childrenOf(node)) + .orElseGet(Stream::empty); + } + + // -- HELPER + + private <T> Optional<NavigableSubtreeFacet> treeNodeFacet(final T node) { + return specLoader().loadSpecification(node.getClass()) + .lookupFacet(NavigableSubtreeFacet.class) + .map(treeNodeFacet->_Casts.<NavigableSubtreeFacet>uncheckedCast(treeNodeFacet)); + } + +} \ No newline at end of file diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navparent/NavigableParentFacet.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navparent/NavigableParentFacet.java index dd970918ba9..4330be1d1f0 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navparent/NavigableParentFacet.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navparent/NavigableParentFacet.java @@ -21,13 +21,11 @@ package org.apache.causeway.core.metamodel.facets.object.navparent; import org.apache.causeway.core.metamodel.facetapi.Facet; /** - * * Mechanism for obtaining the navigable parent (a domain-object or a domain-view-model) * of an instance of a class, used to build a navigable parent chain as required by the * 'where-am-I' feature. * * @since 2.0 - * */ public interface NavigableParentFacet extends Facet { diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navparent/annotation/NavigableParentAnnotationFacetFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navparent/annotation/NavigableParentAnnotationFacetFactory.java index 85c4da17923..c510efa5187 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navparent/annotation/NavigableParentAnnotationFacetFactory.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/navparent/annotation/NavigableParentAnnotationFacetFactory.java @@ -87,12 +87,12 @@ implements MetaModelRefiner { final ResolvedMethod method; // find method that provides the parent ... - if(parentEvaluator instanceof Evaluators.MethodEvaluator) { + if(parentEvaluator instanceof Evaluators.MethodEvaluator methodEvaluator) { // we have a 'parent' annotated method - method = ((Evaluators.MethodEvaluator) parentEvaluator).getMethod(); - } else if(parentEvaluator instanceof Evaluators.FieldEvaluator) { + method = methodEvaluator.getMethod(); + } else if(parentEvaluator instanceof Evaluators.FieldEvaluator fieldEvaluator) { // we have a 'parent' annotated field (useful if one uses lombok's @Getter on a field) - method = ((Evaluators.FieldEvaluator) parentEvaluator).getCorrespondingGetter().orElse(null); + method = fieldEvaluator.getCorrespondingGetter().orElse(null); if(method==null) return; // code should not be reached, since case should be handled by meta-data validation diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/accessor/PropertyAccessorFacetViaAccessorFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/accessor/PropertyAccessorFacetViaAccessorFactory.java index 92a8e6d8510..7779dcbd69b 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/accessor/PropertyAccessorFacetViaAccessorFactory.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/accessor/PropertyAccessorFacetViaAccessorFactory.java @@ -25,14 +25,10 @@ import jakarta.inject.Inject; import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod; import org.apache.causeway.commons.semantics.AccessorSemantics; -import org.apache.causeway.core.metamodel.commons.MethodUtil; import org.apache.causeway.core.metamodel.context.MetaModelContext; -import org.apache.causeway.core.metamodel.facetapi.FacetHolder; -import org.apache.causeway.core.metamodel.facetapi.FacetUtil; import org.apache.causeway.core.metamodel.facetapi.FeatureType; import org.apache.causeway.core.metamodel.facetapi.MethodRemover; import org.apache.causeway.core.metamodel.facets.PropertyOrCollectionIdentifyingFacetFactoryAbstract; -import org.apache.causeway.core.metamodel.spec.ObjectSpecification; public class PropertyAccessorFacetViaAccessorFactory extends PropertyOrCollectionIdentifyingFacetFactoryAbstract { @@ -46,57 +42,26 @@ extends PropertyOrCollectionIdentifyingFacetFactoryAbstract { @Override public void process(final ProcessMethodContext processMethodContext) { - attachPropertyAccessFacetForAccessorMethod(processMethodContext); - } - - private void attachPropertyAccessFacetForAccessorMethod(final ProcessMethodContext processMethodContext) { var accessorMethod = processMethodContext.getMethod().asMethodElseFail(); processMethodContext.removeMethod(accessorMethod); - final Class<?> cls = processMethodContext.getCls(); - final ObjectSpecification typeSpec = getSpecificationLoader().loadSpecification(cls); - - final FacetHolder property = processMethodContext.getFacetHolder(); - FacetUtil.addFacet( - new PropertyAccessorFacetViaAccessor(typeSpec, accessorMethod, property)); - } - - // /////////////////////////////////////////////////////// - // PropertyOrCollectionIdentifying - // /////////////////////////////////////////////////////// - - @Override - public boolean isPropertyOrCollectionGetterCandidate(final ResolvedMethod method) { - return AccessorSemantics.isGetter(method) - || AccessorSemantics.isRecordComponentAccessor(method); - } - - /** - * The method way well represent a collection, but this facet factory does - * not have any opinion on the matter. - */ - @Override - public boolean isCollectionAccessor(final ResolvedMethod method) { - return false; - } + var cls = processMethodContext.getCls(); + var typeSpec = getSpecificationLoader().loadSpecification(cls); + var facetHolder = processMethodContext.getFacetHolder(); - @Override - public boolean isPropertyAccessor(final ResolvedMethod method) { - if (!isPropertyOrCollectionGetterCandidate(method)) { - return false; - } - return isNonScalar(method.returnType()); + addFacet(new PropertyAccessorFacetViaAccessor(typeSpec, accessorMethod, facetHolder)); } @Override - public void findAndRemovePropertyAccessors(final MethodRemover methodRemover, final List<ResolvedMethod> methodListToAppendTo) { - methodRemover.removeMethods(MethodUtil.Predicates.booleanGetter(), methodListToAppendTo::add); - methodRemover.removeMethods(MethodUtil.Predicates.nonBooleanGetter(Object.class), methodListToAppendTo::add); + public boolean isAssociationAccessor(final ResolvedMethod method) { + return AccessorSemantics.isPropertyAccessor(method); } @Override - public void findAndRemoveCollectionAccessors(final MethodRemover methodRemover, final List<ResolvedMethod> methodListToAppendTo) { - // does nothing + public void findAndRemoveAccessors( + final MethodRemover methodRemover, final List<ResolvedMethod> methodListToAppendTo) { + methodRemover.removeMethods(AccessorSemantics::isBooleanGetter, methodListToAppendTo::add); + methodRemover.removeMethods(AccessorSemantics::isNonBooleanGetter, methodListToAppendTo::add); } } diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetedMethodsBuilder.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetedMethodsBuilder.java index 729168454c1..61f45acfa66 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetedMethodsBuilder.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetedMethodsBuilder.java @@ -18,6 +18,7 @@ */ package org.apache.causeway.core.metamodel.spec.impl; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -45,7 +46,6 @@ import org.apache.causeway.commons.internal.reflection._MethodFacades; import org.apache.causeway.commons.internal.reflection._MethodFacades.MethodFacade; import org.apache.causeway.commons.internal.reflection._Reflect; import org.apache.causeway.commons.semantics.AccessorSemantics; -import org.apache.causeway.core.metamodel.commons.MethodUtil; import org.apache.causeway.core.metamodel.commons.ToString; import org.apache.causeway.core.metamodel.context.HasMetaModelContext; import org.apache.causeway.core.metamodel.context.MetaModelContext; @@ -214,7 +214,7 @@ implements private void findAndRemoveCollectionAccessorsAndCreateCorrespondingFacetedMethods( final Consumer<FacetedMethod> onNewAssociationPeer) { - var collectionAccessors = _Lists.<ResolvedMethod>newArrayList(); + var collectionAccessors = new ArrayList<ResolvedMethod>(); getFacetProcessor().findAndRemoveCollectionAccessors(methodRemover, collectionAccessors); createCollectionFacetedMethodsFromAccessors( getMetaModelContext(), collectionAccessors, onNewAssociationPeer); @@ -225,11 +225,11 @@ implements * this will pick up the remaining reference properties. */ private void findAndRemovePropertyAccessorsAndCreateCorrespondingFacetedMethods(final Consumer<FacetedMethod> onNewField) { - var propertyAccessors = _Lists.<ResolvedMethod>newArrayList(); + var propertyAccessors = new ArrayList<ResolvedMethod>(); getFacetProcessor().findAndRemovePropertyAccessors(methodRemover, propertyAccessors); - methodRemover.removeMethods(MethodUtil.Predicates.nonBooleanGetter(Object.class), propertyAccessors::add); - methodRemover.removeMethods(MethodUtil.Predicates.booleanGetter(), propertyAccessors::add); + methodRemover.removeMethods(AccessorSemantics::isNonBooleanGetter, propertyAccessors::add); + methodRemover.removeMethods(AccessorSemantics::isBooleanGetter, propertyAccessors::add); methodRemover.removeMethods(AccessorSemantics::isRecordComponentAccessor, propertyAccessors::add); createPropertyFacetedMethodsFromAccessors(propertyAccessors, onNewField); @@ -372,10 +372,7 @@ implements final ResolvedMethod actionMethod) { var actionMethodFacade = _MethodFacadeAutodetect.autodetect(actionMethod, inspectedTypeSpec); - - if (!isAllParamTypesValid(actionMethodFacade)) { - return null; - } + if (!isAllParamTypesValid(actionMethodFacade)) return null; final FacetedMethod action = FacetedMethod .createForAction(getMetaModelContext(), introspectedClass, actionMethodFacade); diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ProgrammingModelDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ProgrammingModelDefault.java index 2badbb93a04..34a25ee8cf7 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ProgrammingModelDefault.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ProgrammingModelDefault.java @@ -53,6 +53,7 @@ import org.apache.causeway.core.metamodel.facets.object.ignore.javalang.Iterator import org.apache.causeway.core.metamodel.facets.object.ignore.javalang.RemoveMethodsFacetFactory; import org.apache.causeway.core.metamodel.facets.object.logicaltype.LogicalTypeMalformedValidator; import org.apache.causeway.core.metamodel.facets.object.logicaltype.classname.LogicalTypeFacetFromClassNameFactory; +import org.apache.causeway.core.metamodel.facets.object.navchild.NavigableSubtreeFacetPostProcessor; import org.apache.causeway.core.metamodel.facets.object.navparent.annotation.NavigableParentAnnotationFacetFactory; import org.apache.causeway.core.metamodel.facets.object.objectvalidprops.impl.ObjectValidPropertiesFacetImplFactory; import org.apache.causeway.core.metamodel.facets.object.support.ObjectSupportFacetFactory; @@ -252,6 +253,8 @@ extends ProgrammingModelAbstract { addPostProcessor(PostProcessingOrder.A1_BUILTIN, new DisabledFromImmutablePostProcessor(mmc)); addPostProcessor(PostProcessingOrder.A1_BUILTIN, new SynthesizeDomainEventsForMixinPostProcessor(mmc)); addPostProcessor(PostProcessingOrder.A1_BUILTIN, new ProjectionFacetsPostProcessor(mmc)); + + addPostProcessor(PostProcessingOrder.A1_BUILTIN, new NavigableSubtreeFacetPostProcessor(mmc)); addPostProcessor(PostProcessingOrder.A1_BUILTIN, new NavigationFacetFromHiddenTypePostProcessor(mmc)); // must be after all named facets and description facets have been installed diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/facetprocessor/FacetProcessor.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/facetprocessor/FacetProcessor.java index 13bd3859fbc..3ffbd18d70d 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/facetprocessor/FacetProcessor.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/facetprocessor/FacetProcessor.java @@ -165,13 +165,10 @@ implements HasMetaModelContext, AutoCloseable{ public void findAssociationCandidateGetters( final Stream<ResolvedMethod> methodStream, final Consumer<ResolvedMethod> onCandidate) { - var factories = propertyOrCollectionIdentifyingFactories.get(); - - methodStream - .forEach(method->{ + methodStream.forEach(method->{ for (var facetFactory : factories) { - if (facetFactory.isPropertyOrCollectionGetterCandidate(method)) { + if (facetFactory.isAssociationAccessor(method)) { onCandidate.accept(method); } } @@ -179,36 +176,36 @@ implements HasMetaModelContext, AutoCloseable{ } /** - * Use the provided {@link MethodRemover} to have all known + * Use the provided {@link MethodRemover} to call all known * {@link PropertyOrCollectionIdentifyingFacetFactory}s to remove all - * property accessors, and append them to the supplied methodList. - * + * property accessors and append them to the supplied methodList. * <p> - * Intended to be called after {@link #findAndRemovePropertyAccessors(MethodRemover, java.util.List)} once only reference properties remain. + * @see PropertyOrCollectionIdentifyingFacetFactory#findAndRemoveAccessors(MethodRemover, List) */ public void findAndRemovePropertyAccessors( final MethodRemover methodRemover, final List<ResolvedMethod> methodListToAppendTo) { for (var facetFactory : propertyOrCollectionIdentifyingFactories.get()) { - facetFactory.findAndRemovePropertyAccessors(methodRemover, methodListToAppendTo); + if(!facetFactory.supportsProperties()) continue; + facetFactory.findAndRemoveAccessors(methodRemover, methodListToAppendTo); } } /** - * Use the provided {@link MethodRemover} to have all known + * Use the provided {@link MethodRemover} to call all known * {@link PropertyOrCollectionIdentifyingFacetFactory}s to remove all - * property accessors, and append them to the supplied methodList. + * collection accessors and append them to the supplied methodList. * - * @see PropertyOrCollectionIdentifyingFacetFactory#findAndRemoveCollectionAccessors(MethodRemover, - * List) + * @see PropertyOrCollectionIdentifyingFacetFactory#findAndRemoveAccessors(MethodRemover, List) */ public void findAndRemoveCollectionAccessors( final MethodRemover methodRemover, final List<ResolvedMethod> methodListToAppendTo) { for (var facetFactory : propertyOrCollectionIdentifyingFactories.get()) { - facetFactory.findAndRemoveCollectionAccessors(methodRemover, methodListToAppendTo); + if(!facetFactory.supportsCollections()) continue; + facetFactory.findAndRemoveAccessors(methodRemover, methodListToAppendTo); } } @@ -344,7 +341,6 @@ implements HasMetaModelContext, AutoCloseable{ removerElseNoopRemover(methodRemover), facetedMethod, isMixinMain); for (FacetFactory facetFactory : factoryListByFeatureType.get().getOrElseEmpty(featureType)) { - facetFactory.process(processMethodContext); } } diff --git a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/_Utils.java b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/_Utils.java index 0a65fd41bb1..d32f61bb2f3 100644 --- a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/_Utils.java +++ b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/_Utils.java @@ -94,7 +94,10 @@ class _Utils { Optional<ResolvedMethod> findGetter(final Class<?> declaringClass, final String propertyName) { return _Utils.findMethodExact(declaringClass, "get" + _Strings.capitalize(propertyName)) - .or(()->_Utils.findMethodExact(declaringClass, "is" + _Strings.capitalize(propertyName))); + .or(()->_Utils.findMethodExact(declaringClass, "is" + _Strings.capitalize(propertyName))) + .or(()->declaringClass.isRecord() + ? _Utils.findMethodExact(declaringClass, propertyName) + : Optional.empty()); } ResolvedMethod findGetterOrFail(final Class<?> declaringClass, final String propertyName) { diff --git a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/object/navchild/NavigableSubtreeFacetFactoryTest.java b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/object/navchild/NavigableSubtreeFacetFactoryTest.java new file mode 100644 index 00000000000..a083bf622fb --- /dev/null +++ b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/object/navchild/NavigableSubtreeFacetFactoryTest.java @@ -0,0 +1,112 @@ +/* + * 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.navchild; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; + +import org.apache.causeway.commons.collections.Can; +import org.apache.causeway.core.metamodel._testing.MetaModelContext_forTesting; +import org.apache.causeway.core.metamodel.context.MetaModelContext; +import org.apache.causeway.core.metamodel.facets.FacetFactoryTestAbstract; +import org.apache.causeway.core.metamodel.facets.collections.layout.CollectionLayoutFacetFactory; +import org.apache.causeway.core.metamodel.specloader.SpecificationLoader; + +class NavigableSubtreeFacetFactoryTest extends FacetFactoryTestAbstract { + + CollectionLayoutFacetFactory facetFactory; + NavigableSubtreeFacetPostProcessor postProcessor; + SpecificationLoader specLoader; + + @BeforeEach + void setUp() { + var mmc = MetaModelContext_forTesting.buildDefault(); + assertNotNull(MetaModelContext.instanceNullable()); + facetFactory = new CollectionLayoutFacetFactory(mmc); + postProcessor = new NavigableSubtreeFacetPostProcessor(mmc); + specLoader = mmc.getSpecificationLoader(); + } + + @AfterEach + protected void tearDown() { + facetFactory = null; + } + + @Test + void treeNodeFacetShouldBeInstalledWhenNodeHasAnnotations() { + + collectionScenario(_TreeSample.A.class, "childrenB", (processMethodContext, facetHolder, facetedMethod)->{ + facetFactory.process(processMethodContext); + assertNotNull(facetedMethod.getFacet(NavigableSubtreeSequenceFacet.class)); + // copy over facets to spec for testing later + var spec = specLoader.specForType(_TreeSample.A.class).orElseThrow(); + spec.getAssociationElseFail("childrenB") + .addFacet(facetedMethod.getFacet(NavigableSubtreeSequenceFacet.class)); + }); + + collectionScenario(_TreeSample.A.class, "childrenC", (processMethodContext, facetHolder, facetedMethod)->{ + facetFactory.process(processMethodContext); + assertNotNull(facetedMethod.getFacet(NavigableSubtreeSequenceFacet.class)); + // copy over facets to spec for testing later + var spec = specLoader.specForType(_TreeSample.A.class).orElseThrow(); + spec.getAssociationElseFail("childrenC") + .addFacet(facetedMethod.getFacet(NavigableSubtreeSequenceFacet.class)); + }); + + collectionScenario(_TreeSample.B.class, "childrenD", (processMethodContext, facetHolder, facetedMethod)->{ + facetFactory.process(processMethodContext); + assertNotNull(facetedMethod.getFacet(NavigableSubtreeSequenceFacet.class)); + // copy over facets to spec for testing later + var spec = specLoader.specForType(_TreeSample.B.class).orElseThrow(); + spec.getAssociationElseFail("childrenD") + .addFacet(facetedMethod.getFacet(NavigableSubtreeSequenceFacet.class)); + }); + + collectionScenario(_TreeSample.C.class, "childrenD", (processMethodContext, facetHolder, facetedMethod)->{ + facetFactory.process(processMethodContext); + assertNotNull(facetedMethod.getFacet(NavigableSubtreeSequenceFacet.class)); + // copy over facets to spec for testing later + var spec = specLoader.specForType(_TreeSample.C.class).orElseThrow(); + spec.getAssociationElseFail("childrenD") + .addFacet(facetedMethod.getFacet(NavigableSubtreeSequenceFacet.class)); + }); + + var specs = Can.of(_TreeSample.A.class, _TreeSample.B.class, _TreeSample.C.class, _TreeSample.D.class) + .map(specLoader::specForType) + .map(opt->opt.orElse(null)); + // now run the post-processor + specs.forEach(postProcessor::postProcessObject); + + specs.forEach(spec->{ + switch(spec.getCorrespondingClass().getSimpleName()) { + case "A" -> assertNotNull(spec.getFacet(NavigableSubtreeFacet.class)); + case "B" -> assertNotNull(spec.getFacet(NavigableSubtreeFacet.class)); + case "C" -> assertNotNull(spec.getFacet(NavigableSubtreeFacet.class)); + case "D" -> assertNull(spec.getFacet(NavigableSubtreeFacet.class)); + default -> fail("unexpected case"); + } + }); + + } +} diff --git a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/object/navchild/TreeTraversalTest.java b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/object/navchild/TreeTraversalTest.java new file mode 100644 index 00000000000..66239aa1693 --- /dev/null +++ b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/object/navchild/TreeTraversalTest.java @@ -0,0 +1,81 @@ +package org.apache.causeway.core.metamodel.facets.object.navchild; + +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.causeway.applib.graph.tree.TreeNode; +import org.apache.causeway.commons.collections.Can; +import org.apache.causeway.core.metamodel._testing.MetaModelContext_forTesting; +import org.apache.causeway.core.metamodel.context.MetaModelContext; +import org.apache.causeway.core.metamodel.facets.FacetFactoryTestAbstract; + +//TODO[causeway-core-metamodel-CAUSEWAY-2297] WIP +class TreeTraversalTest +extends FacetFactoryTestAbstract { + + MetaModelContext mmc; + ObjectTreeAdapter treeAdapter; + + @BeforeEach + void setUp() { + mmc = MetaModelContext_forTesting.buildDefault(); + treeAdapter = new ObjectTreeAdapter(mmc.getSpecificationLoader()); + } + + //@Test + void preconditions() { + var specLoader = mmc.getSpecificationLoader(); + var specA = specLoader.loadSpecification(_TreeSample.A.class); + var assocAB = specA.getAssociationElseFail("childrenB"); + + System.err.printf("assocA %s%n", assocAB.streamFacets().collect(Can.toCan()).join("\n")); + } + + //@Test + void depthFirstTraversal() { + // instantiate a tree, that we later traverse + var a = _TreeSample.sampleA(); + + // traverse the tree + var tree = TreeNode.root(a, treeAdapter); + + var nodeNames = tree.streamDepthFirst() + .map(TreeNode::value) + .map(_TreeSample::nameOf) + .collect(Collectors.joining(", ")); + + assertEquals( + "a, b1, d1, d2, d3, b2, d1, d2, d3, c1, d1, d2, d3, c2, d1, d2, d3", + nodeNames); + } + + //@Test + void leafToRootTraversal() { + // instantiate a tree and pick an arbitrary leaf value, + // from which we later traverse up to the root + var a = _TreeSample.sampleA(); + var d = a.childrenB().getFirstElseFail().childrenD().getLastElseFail(); + + // traverse the tree + var tree = TreeNode.root(a, treeAdapter); + + // find d's node + var leafNode = tree.streamDepthFirst() + .filter((final TreeNode<Object> treeNode)->d.equals(treeNode.value())) + .findFirst() + .orElseThrow(); + + var nodeNames = leafNode.streamHierarchyUp() + .map(TreeNode::value) + .map(_TreeSample::nameOf) + .collect(Collectors.joining(", ")); + + assertEquals( + "d3, b1, a", + nodeNames); + } + +} + diff --git a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/object/navchild/_TreeSample.java b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/object/navchild/_TreeSample.java new file mode 100644 index 00000000000..c95d49615b4 --- /dev/null +++ b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/object/navchild/_TreeSample.java @@ -0,0 +1,86 @@ +/* + * 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.navchild; + +import java.util.Map; + +import org.apache.causeway.applib.ViewModel; +import org.apache.causeway.applib.annotation.CollectionLayout; +import org.apache.causeway.applib.annotation.Programmatic; +import org.apache.causeway.applib.annotation.Property; +import org.apache.causeway.commons.collections.Can; + +import lombok.Getter; +import lombok.experimental.UtilityClass; + +@UtilityClass +class _TreeSample { + + static interface SampleNode { + String name(); + } + + record A(String name, + @CollectionLayout(navigableSubtree = "1") Can<B> childrenB, + @CollectionLayout(navigableSubtree = "2") Map<String, C> childrenC) implements SampleNode { + } + record B(String name, + @CollectionLayout(navigableSubtree = "1") Can<D> childrenD) implements SampleNode { + } + record C(String name, + @CollectionLayout(navigableSubtree = "1") Can<D> childrenD) implements SampleNode { + } + record D(String name) implements SampleNode { + } + + A sampleA() { + var ds = Can.of(new D("d1"), new D("d2"), new D("d3")); + var cs = Can.of(new C("c1", ds), new C("c2", ds)); + var bs = Can.of(new B("b1", ds), new B("b2", ds)); + var a = new A("a", bs, cs.toMap(C::name)); + return a; + } + + String nameOf(final Object node) { + return node instanceof SampleNode sampleNode + ? sampleNode.name() + : "?"; + } + + public static class SampleNodeView implements ViewModel { + + @Programmatic + final String memento; + + public SampleNodeView(final String memento) { + this.memento = memento; + this.name = "TODO"; + } + + @Override + public String viewModelMemento() { + return memento; + } + + @Property @Getter + final String name; + + } + +}