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 0b0b1242aee CAUSEWAY-3837: Java built-in Map type to support 
collection semantics
0b0b1242aee is described below

commit 0b0b1242aee14d4f03fb918fc6a95942108617db
Author: andi-huber <ahu...@apache.org>
AuthorDate: Mon Dec 16 08:00:42 2024 +0100

    CAUSEWAY-3837: Java built-in Map type to support collection semantics
---
 .../applib/annotation/CollectionLayout.java        |   2 +
 .../causeway/applib/annotation/PropertyLayout.java |  12 ++
 .../commons/internal/collections/_Lists.java       |   2 +-
 .../commons/semantics/CollectionSemantics.java     | 198 ++++++++++++---------
 .../CollectionAccessorFacetViaAccessor.java        |  10 +-
 .../facets/object/navchild/TreeTraversalTest.java  |  29 +--
 .../handlers/PluralInvocationHandlerAbstract.java  |   4 +-
 7 files changed, 149 insertions(+), 108 deletions(-)

diff --git 
a/api/applib/src/main/java/org/apache/causeway/applib/annotation/CollectionLayout.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/annotation/CollectionLayout.java
index b89b9d7b550..2960ede4afa 100644
--- 
a/api/applib/src/main/java/org/apache/causeway/applib/annotation/CollectionLayout.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/annotation/CollectionLayout.java
@@ -115,6 +115,8 @@ public @interface CollectionLayout {
      * <p>
      * The order of appearance of this tree branch in the UI relative to other 
branches of the same tree node,
      * is given in <i>Dewey-decimal</i> notation.
+     *
+     * @see PropertyLayout#navigableSubtree()
      */
     String navigableSubtree()
         default "";
diff --git 
a/api/applib/src/main/java/org/apache/causeway/applib/annotation/PropertyLayout.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/annotation/PropertyLayout.java
index b2734b6aa87..09cc39a5166 100644
--- 
a/api/applib/src/main/java/org/apache/causeway/applib/annotation/PropertyLayout.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/annotation/PropertyLayout.java
@@ -201,6 +201,18 @@ public @interface PropertyLayout {
     Navigable navigable()
             default Navigable.NOT_SPECIFIED;
 
+    /**
+     * When set, identifies a logical child, that is navigable via the UI.
+     * <p>
+     * The order of appearance of this tree branch in the UI relative to other 
branches of the same tree node,
+     * is given in <i>Dewey-decimal</i> notation.
+     *
+     * @see CollectionLayout#navigableSubtree()
+     */
+    String navigableSubtree()
+        default "";
+
+
     /**
      * How the properties of this domain object are be edited, either {@link 
PromptStyle#DIALOG dialog} or {@link PromptStyle#INLINE inline}.
      */
diff --git 
a/commons/src/main/java/org/apache/causeway/commons/internal/collections/_Lists.java
 
b/commons/src/main/java/org/apache/causeway/commons/internal/collections/_Lists.java
index 32e5d9b3d2e..bf2d34ab6e1 100644
--- 
a/commons/src/main/java/org/apache/causeway/commons/internal/collections/_Lists.java
+++ 
b/commons/src/main/java/org/apache/causeway/commons/internal/collections/_Lists.java
@@ -103,7 +103,7 @@ public final class _Lists {
      * Returns an unmodifiable list containing all elements from given lists
      * list1 and list2.
      */
-    public <T> List<T> concat(final @Nullable List<T> list1, final @Nullable 
List<T> list2) {
+    public <T> List<T> concat(final @Nullable Collection<T> list1, final 
@Nullable Collection<T> list2) {
         var isEmpty1 = _NullSafe.isEmpty(list1);
         var isEmpty2 = _NullSafe.isEmpty(list2);
 
diff --git 
a/commons/src/main/java/org/apache/causeway/commons/semantics/CollectionSemantics.java
 
b/commons/src/main/java/org/apache/causeway/commons/semantics/CollectionSemantics.java
index 797c77f963e..ee26b07ef16 100644
--- 
a/commons/src/main/java/org/apache/causeway/commons/semantics/CollectionSemantics.java
+++ 
b/commons/src/main/java/org/apache/causeway/commons/semantics/CollectionSemantics.java
@@ -22,6 +22,7 @@ import java.lang.reflect.Array;
 import java.lang.reflect.Method;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -107,27 +108,24 @@ public enum CollectionSemantics {
         }
     },
     /**
-     * not supported, however holds a usable {@link InvocationHandlingPolicy}
+     * Supported as collection provider, but not as collection receiver.
      */
     MAP(Map.class, MethodSets.MAP){
         @Override public Object asContainerType(
                 final Class<?> elementType, final @NonNull List<?> plural) {
-            throw new UnsupportedOperationException("Map is not a collection");
+            throw new UnsupportedOperationException("A Map cannot be 
reconstructed from a Collection");
         }
-    }
-    ;
+    };
 
     public static interface InvocationHandlingPolicy {
         /**
-         * Methods which will trigger CollectionMethodEvent(s)
-         * on invocation.
+         * Whether on invocation given method will trigger a 
CollectionMethodEvent.
          */
-        List<Method> getIntercepted();
+        boolean intercepts(Method method);
         /**
-         * Methods which will cause an {@link UnsupportedOperationException}
-         * on invocation.
+         * Whether on invocation given method will cause an {@link 
UnsupportedOperationException}.
          */
-        List<Method> getVetoed();
+        boolean vetoes(Method method);
     }
 
     public boolean isArray() {return this == ARRAY;}
@@ -141,6 +139,14 @@ public enum CollectionSemantics {
     public boolean isMap() {return this == MAP;}
     //
     public boolean isSetAny() {return isSet() || isSortedSet(); }
+
+    @Nullable
+    public static Object toIterable(@Nullable final Object pojo) {
+        return pojo instanceof Map map
+                ? map.values()
+                : pojo;
+    }
+
     @Getter private final Class<?> containerType;
     @Getter private final InvocationHandlingPolicy invocationHandlingPolicy;
 
@@ -155,7 +161,6 @@ public enum CollectionSemantics {
                 ? Optional.of(CollectionSemantics.ARRAY)
                 : all.stream()
                     
.filter(collType->collType.getContainerType().isAssignableFrom(type))
-                    .filter(t->!t.isMap()) // not supported
                     .findFirst();
     }
 
@@ -176,82 +181,99 @@ public enum CollectionSemantics {
 }
 
 //TODO perhaps needs an update to reflect Java 7->11 Language changes
-@RequiredArgsConstructor
 enum MethodSets implements InvocationHandlingPolicy {
-  EMPTY(List.of(), List.of()),
-  COLLECTION(
-          // intercepted ...
-          List.of(
-                  getMethod(Collection.class, "contains", Object.class),
-                  getMethod(Collection.class, "size"),
-                  getMethod(Collection.class, "isEmpty")
-          ),
-          // vetoed ...
-          List.of(
-                  getMethod(Collection.class, "add", Object.class),
-                  getMethod(Collection.class, "remove", Object.class),
-                  getMethod(Collection.class, "addAll", Collection.class),
-                  getMethod(Collection.class, "removeAll", Collection.class),
-                  getMethod(Collection.class, "retainAll", Collection.class),
-                  getMethod(Collection.class, "clear")
-          )),
-  LIST(
-          // intercepted ...
-          _Lists.concat(
-                  COLLECTION.intercepted,
-                  List.of(
-                          getMethod(List.class, "get", int.class)
-                  )
-          ),
-          // vetoed ...
-          _Lists.concat(
-                  COLLECTION.vetoed,
-                  List.of(
-                  )
-          )),
-  CAN(
-          // intercepted ...
-          _Lists.concat(
-                  COLLECTION.intercepted,
-                  List.of(
-                          getMethod(Can.class, "get", int.class),
-                          getMethod(Can.class, "getElseFail", int.class),
-                          getMethod(Can.class, "getFirst"),
-                          getMethod(Can.class, "getFirstElseFail"),
-                          getMethod(Can.class, "getLast"),
-                          getMethod(Can.class, "getLastElseFail")
-                  )
-          ),
-          // vetoed ...
-          _Lists.concat(
-                  COLLECTION.vetoed,
-                  List.of(
-                  )
-          )),
-  MAP(
-          // intercepted ...
-          List.of(
-                  getMethod(Map.class, "containsKey", Object.class),
-                  getMethod(Map.class, "containsValue", Object.class),
-                  getMethod(Map.class, "size"),
-                  getMethod(Map.class, "isEmpty")
-          ),
-          // vetoed ...
-          List.of(
-                  getMethod(Map.class, "put", Object.class, Object.class),
-                  getMethod(Map.class, "remove", Object.class),
-                  getMethod(Map.class, "putAll", Map.class),
-                  getMethod(Map.class, "clear")
-          ))
-  ;
-  @Getter private final List<Method> intercepted;
-  @Getter private final List<Method> vetoed;
-  // -- HELPER
-  @SneakyThrows
-  private static Method getMethod(
-          final Class<?> cls,
-          final String methodName,
-          final Class<?>... parameterClass) {
-      return cls.getMethod(methodName, parameterClass);
-  }
+    EMPTY(List.of(), List.of()),
+    COLLECTION(
+            // intercepted ...
+            List.of(
+                    getMethod(Collection.class, "contains", Object.class),
+                    getMethod(Collection.class, "size"),
+                    getMethod(Collection.class, "isEmpty")
+                    ),
+            // vetoed ...
+            List.of(
+                    getMethod(Collection.class, "add", Object.class),
+                    getMethod(Collection.class, "remove", Object.class),
+                    getMethod(Collection.class, "addAll", Collection.class),
+                    getMethod(Collection.class, "removeAll", Collection.class),
+                    getMethod(Collection.class, "retainAll", Collection.class),
+                    getMethod(Collection.class, "clear")
+                    )),
+    LIST(
+            // intercepted ...
+            _Lists.concat(
+                    COLLECTION.intercepted.values(),
+                    List.of(
+                            getMethod(List.class, "get", int.class)
+                            )
+                    ),
+            // vetoed ...
+            _Lists.concat(
+                    COLLECTION.vetoed.values(),
+                    List.of(
+                            )
+                    )),
+    CAN(
+            // intercepted ...
+            _Lists.concat(
+                    COLLECTION.intercepted.values(),
+                    List.of(
+                            getMethod(Can.class, "get", int.class),
+                            getMethod(Can.class, "getElseFail", int.class),
+                            getMethod(Can.class, "getFirst"),
+                            getMethod(Can.class, "getFirstElseFail"),
+                            getMethod(Can.class, "getLast"),
+                            getMethod(Can.class, "getLastElseFail")
+                            )
+                    ),
+            // vetoed ...
+            _Lists.concat(
+                    COLLECTION.vetoed.values(),
+                    List.of()
+                    )),
+    MAP(
+            // intercepted ...
+            List.of(
+                    getMethod(Map.class, "containsKey", Object.class),
+                    getMethod(Map.class, "containsValue", Object.class),
+                    getMethod(Map.class, "size"),
+                    getMethod(Map.class, "isEmpty")
+                    ),
+            // vetoed ...
+            List.of(
+                    getMethod(Map.class, "put", Object.class, Object.class),
+                    getMethod(Map.class, "remove", Object.class),
+                    getMethod(Map.class, "putAll", Map.class),
+                    getMethod(Map.class, "clear")
+                    ))
+    ;
+
+    private MethodSets(final List<Method> intercepted, final List<Method> 
vetoed) {
+        intercepted.forEach(method->this.intercepted.put(method.getName(), 
method));
+        vetoed.forEach(method->this.vetoed.put(method.getName(), method));
+    }
+
+    private final Map<String, Method> intercepted = new HashMap<>();
+    private final Map<String, Method> vetoed = new HashMap<>();
+    // -- HELPER
+    @SneakyThrows
+    private static Method getMethod(
+            final Class<?> cls,
+            final String methodName,
+            final Class<?>... parameterClass) {
+        return cls.getMethod(methodName, parameterClass);
+    }
+
+    @Override
+    public boolean intercepts(@Nullable final Method method) {
+        return method!=null
+                ? intercepted.containsKey(method.getName())
+                : false;
+    }
+    @Override
+    public boolean vetoes(@Nullable final Method method) {
+        return method!=null
+                ? vetoed.containsKey(method.getName())
+                : false;
+    }
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/collections/accessor/CollectionAccessorFacetViaAccessor.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/collections/accessor/CollectionAccessorFacetViaAccessor.java
index 050cae091a5..a3066113116 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/collections/accessor/CollectionAccessorFacetViaAccessor.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/collections/accessor/CollectionAccessorFacetViaAccessor.java
@@ -23,6 +23,7 @@ import java.util.function.BiConsumer;
 import org.apache.causeway.commons.collections.Can;
 import 
org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod;
 import 
org.apache.causeway.commons.internal.reflection._MethodFacades.MethodFacade;
+import org.apache.causeway.commons.semantics.CollectionSemantics;
 import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
 import org.apache.causeway.core.metamodel.facets.ImperativeFacet;
@@ -60,11 +61,12 @@ implements ImperativeFacet {
             final InteractionInitiatedBy interactionInitiatedBy) {
 
         var method = methods.getFirstElseFail().asMethodElseFail(); // 
expected regular
-        final Object collectionOrArray = 
MmInvokeUtils.invokeNoArg(method.method(), owningAdapter);
-        if(collectionOrArray == null) return null;
+        final Object pojo = MmInvokeUtils.invokeNoArg(method.method(), 
owningAdapter);
+        var iterableOrArray = CollectionSemantics.toIterable(pojo);
+        if(iterableOrArray == null) return null;
 
         if(isConfiguredToFilterForVisibility()) {
-            var collectionAdapter = 
getObjectManager().adapt(collectionOrArray);
+            var collectionAdapter = getObjectManager().adapt(iterableOrArray);
             var autofittedObjectContainer = MmVisibilityUtils
                     .visiblePojosAutofit(collectionAdapter, 
interactionInitiatedBy, method.returnType());
 
@@ -75,7 +77,7 @@ implements ImperativeFacet {
         }
 
         // either no filtering, or was unable to filter (unable to take copy 
due to unrecognized type)
-        return collectionOrArray;
+        return iterableOrArray;
     }
 
     @Override
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
index e3d01e2dfad..7f26873b136 100644
--- 
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
@@ -75,6 +75,11 @@ extends FacetFactoryTestAbstract {
         assertTrue(assocAB.isCollection());
         assertTrue(assocAB.containsFacet(NavigableSubtreeSequenceFacet.class));
 
+        // java map support
+        var assocAC = specA.getAssociationElseFail("childrenC");
+        assertTrue(assocAC.isCollection());
+        assertTrue(assocAC.containsFacet(NavigableSubtreeSequenceFacet.class));
+
         // second: post-processor should generate NavigableSubtreeFacet
         assertTrue(specA.containsFacet(NavigableSubtreeFacet.class));
         assertTrue(specB.containsFacet(NavigableSubtreeFacet.class));
@@ -86,14 +91,14 @@ extends FacetFactoryTestAbstract {
         assertTrue(tree.isRoot());
         assertFalse(tree.isLeaf());
 
-        // node a is expected to have 2 children
+        // node a is expected to have 4 children
         var navigableSubtreeFacet = 
specA.getFacet(NavigableSubtreeFacet.class);
-        assertEquals(2, navigableSubtreeFacet.childCountOf(a));
-        assertEquals(2, navigableSubtreeFacet.childrenOf(a).toList().size());
-        assertEquals(2, treeAdapter.childCountOf(a));
-        assertEquals(2, treeAdapter.childrenOf(a).toList().size());
-        assertEquals(2, tree.childCount());
-        assertEquals(2, tree.streamChildren().toList().size());
+        assertEquals(4, navigableSubtreeFacet.childCountOf(a));
+        assertEquals(4, navigableSubtreeFacet.childrenOf(a).toList().size());
+        assertEquals(4, treeAdapter.childCountOf(a));
+        assertEquals(4, treeAdapter.childrenOf(a).toList().size());
+        assertEquals(4, tree.childCount());
+        assertEquals(4, tree.streamChildren().toList().size());
 
         var firstChildOfA = 
treeAdapter.childrenOf(a).findFirst().orElseThrow();
 
@@ -101,10 +106,10 @@ extends FacetFactoryTestAbstract {
         assertEquals(3, treeAdapter.childCountOf(firstChildOfA));
         assertEquals(3, treeAdapter.childrenOf(firstChildOfA).toList().size());
 
-        //TODO[causeway-core-metamodel-CAUSEWAY-2297] add map support -> 
expected 17
+        //TODO[causeway-core-metamodel-CAUSEWAY-2297] add property support
         // count all nodes
-        assertEquals(9, Can.ofIterable(tree::iteratorDepthFirst).size());
-        assertEquals(9, Can.ofIterable(tree::iteratorBreadthFirst).size());
+        assertEquals(17, Can.ofIterable(tree::iteratorDepthFirst).size());
+        assertEquals(17, Can.ofIterable(tree::iteratorBreadthFirst).size());
     }
 
     @Test
@@ -115,9 +120,7 @@ extends FacetFactoryTestAbstract {
             .collect(Collectors.joining(", "));
 
         assertEquals(
-                "a, b1, d1, d2, d3, b2, d1, d2, d3",
-                //TODO[causeway-core-metamodel-CAUSEWAY-2297] add map support
-                //"a, b1, d1, d2, d3, b2, d1, d2, d3, c1, d1, d2, d3, c2, d1, 
d2, d3",
+                "a, b1, d1, d2, d3, b2, d1, d2, d3, c1, d1, d2, d3, c2, d1, 
d2, d3",
                 nodeNames);
     }
 
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/PluralInvocationHandlerAbstract.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/PluralInvocationHandlerAbstract.java
index 20d4a5cff0c..186546deed9 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/PluralInvocationHandlerAbstract.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/PluralInvocationHandlerAbstract.java
@@ -69,7 +69,7 @@ extends DelegatingInvocationHandlerDefault<P> {
 
         var policy = collectionSemantics.getInvocationHandlingPolicy();
 
-        if (policy.getIntercepted().contains(method)) {
+        if (policy.intercepts(method)) {
 
             resolveIfRequired(domainObject);
 
@@ -85,7 +85,7 @@ extends DelegatingInvocationHandlerDefault<P> {
             return returnValueObj;
         }
 
-        if (policy.getVetoed().contains(method)) {
+        if (policy.vetoes(method)) {
             throw new UnsupportedOperationException(
                     String.format("Method '%s' may not be called directly.", 
method.getName()));
         }

Reply via email to