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

Reply via email to