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())); }