You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by ah...@apache.org on 2022/09/06 13:03:25 UTC

[isis] 01/01: ISIS-3204: adds TypeOfAnyCardinality

This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch 3204-bounded.generics
in repository https://gitbox.apache.org/repos/asf/isis.git

commit db21865fa0b8e0e350c8d39259fc50e84cd2e98a
Author: Andi Huber <ah...@apache.org>
AuthorDate: Tue Sep 6 15:03:15 2022 +0200

    ISIS-3204: adds TypeOfAnyCardinality
---
 .../org/apache/isis/commons/collections/Can.java   |  43 +----
 .../commons/collections/ImmutableCollection.java   |  94 +++++++++++
 .../isis/commons/collections/ImmutableEnumSet.java |   9 +
 .../progmodel/ProgrammingModelConstants.java       |  48 ++++++
 .../core/metamodel/spec/TypeOfAnyCardinality.java  | 134 +++++++++++++++
 .../metamodel/spec/TypeOfAnyCardinalityTest.java   | 182 +++++++++++++++++++++
 6 files changed, 469 insertions(+), 41 deletions(-)

diff --git a/commons/src/main/java/org/apache/isis/commons/collections/Can.java b/commons/src/main/java/org/apache/isis/commons/collections/Can.java
index 1c8ac12598..7a278433f4 100644
--- a/commons/src/main/java/org/apache/isis/commons/collections/Can.java
+++ b/commons/src/main/java/org/apache/isis/commons/collections/Can.java
@@ -26,7 +26,6 @@ import java.util.Enumeration;
 import java.util.Iterator;
 import java.util.List;
 import java.util.NoSuchElementException;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.BiConsumer;
@@ -68,17 +67,7 @@ import lombok.val;
  * @since 2.0 {@index}
  */
 public interface Can<T>
-extends Iterable<T>, Comparable<Can<T>>, Serializable {
-
-    /**
-     * @return this Can's cardinality
-     */
-    Cardinality getCardinality();
-
-    /**
-     * @return number of elements this Can contains
-     */
-    int size();
+extends ImmutableCollection<T>, Comparable<Can<T>>, Serializable {
 
     /**
      * Will only ever return an empty Optional, if the elementIndex is out of bounds.
@@ -111,16 +100,6 @@ extends Iterable<T>, Comparable<Can<T>>, Serializable {
     @Override
     int compareTo(final @Nullable Can<T> o);
 
-    /**
-     * @return Stream of elements this Can contains
-     */
-    Stream<T> stream();
-
-    /**
-     * @return possibly concurrent Stream of elements this Can contains
-     */
-    Stream<T> parallelStream();
-
     /**
      * @return this Can's first element or an empty Optional if no such element
      */
@@ -147,25 +126,6 @@ extends Iterable<T>, Comparable<Can<T>>, Serializable {
         return getLast().orElseThrow(_Exceptions::noSuchElement);
     }
 
-    /**
-     * @return this Can's single element or an empty Optional if this Can has any cardinality other than ONE
-     */
-    Optional<T> getSingleton();
-
-    /**
-     * Shortcut for {@code getSingleton().orElseThrow(_Exceptions::noSuchElement)}
-     * @throws NoSuchElementException if result is empty
-     */
-    default T getSingletonOrFail() {
-        return getSingleton().orElseThrow(_Exceptions::noSuchElement);
-    }
-
-    /**
-     * @return whether this Can contains given {@code element}, that is, at least one contained element
-     * passes the {@link Objects#equals(Object, Object)} test with respect to the given element.
-     */
-    boolean contains(@Nullable T element);
-
     // -- FACTORIES
 
     /**
@@ -669,6 +629,7 @@ extends Iterable<T>, Comparable<Can<T>>, Serializable {
 
     // -- SHORTCUTS FOR PREDICATES
 
+    @Override
     default boolean isEmpty() {
         return getCardinality().isZero();
     }
diff --git a/commons/src/main/java/org/apache/isis/commons/collections/ImmutableCollection.java b/commons/src/main/java/org/apache/isis/commons/collections/ImmutableCollection.java
new file mode 100644
index 0000000000..552aac7b52
--- /dev/null
+++ b/commons/src/main/java/org/apache/isis/commons/collections/ImmutableCollection.java
@@ -0,0 +1,94 @@
+/*
+ *  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.isis.commons.collections;
+
+import java.util.Collection;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import org.springframework.lang.Nullable;
+
+import org.apache.isis.commons.internal.exceptions._Exceptions;
+
+/**
+ * Provides a subset of the functionality that the Java {@link Collection}
+ * interface has, focusing on immutability.
+ */
+public interface ImmutableCollection<E>
+extends Iterable<E> {
+
+    /**
+     * Returns the number of elements in this collection.  If this collection
+     * contains more than {@code Integer.MAX_VALUE} elements, returns
+     * {@code Integer.MAX_VALUE}.
+     *
+     * @return the number of elements in this collection
+     */
+    int size();
+
+    /**
+     * Returns {@code true} if this collection contains no elements.
+     *
+     * @return {@code true} if this collection contains no elements
+     */
+    boolean isEmpty();
+
+    /**
+     * @return either 'empty', 'one' or 'multi'
+     */
+    Cardinality getCardinality();
+
+    /**
+     * @return whether this Can contains given {@code element}, that is, at least one contained element
+     * passes the {@link Objects#equals(Object, Object)} test with respect to the given element.
+     */
+    boolean contains(@Nullable E element);
+
+    /**
+     * @return this collection's single element or an empty Optional,
+     * if this collection has any cardinality other than ONE
+     */
+    Optional<E> getSingleton();
+
+    /**
+     * Shortcut for {@code getSingleton().orElseThrow(_Exceptions::noSuchElement)}
+     * @throws NoSuchElementException if result is empty
+     */
+    default E getSingletonOrFail() {
+        return getSingleton().orElseThrow(_Exceptions::noSuchElement);
+    }
+
+    /**
+     * @return Stream of elements this collection contains
+     */
+    default Stream<E> stream() {
+        return StreamSupport.stream(spliterator(), false);
+    }
+
+    /**
+     * @return possibly concurrent Stream of elements this collection contains
+     */
+    default Stream<E> parallelStream() {
+        return StreamSupport.stream(spliterator(), true);
+    }
+
+}
diff --git a/commons/src/main/java/org/apache/isis/commons/collections/ImmutableEnumSet.java b/commons/src/main/java/org/apache/isis/commons/collections/ImmutableEnumSet.java
index 7e7a360ad7..7c60328603 100644
--- a/commons/src/main/java/org/apache/isis/commons/collections/ImmutableEnumSet.java
+++ b/commons/src/main/java/org/apache/isis/commons/collections/ImmutableEnumSet.java
@@ -102,5 +102,14 @@ implements Iterable<E>, java.io.Serializable {
         return from(newEnumSet);
     }
 
+    public ImmutableEnumSet<E> remove(final E entry) {
+        if(!contains(entry)) {
+            return this;
+        }
+        val newEnumSet = delegate.clone();
+        newEnumSet.remove(entry);
+        return from(newEnumSet);
+    }
+
 
 }
diff --git a/core/config/src/main/java/org/apache/isis/core/config/progmodel/ProgrammingModelConstants.java b/core/config/src/main/java/org/apache/isis/core/config/progmodel/ProgrammingModelConstants.java
index 1ffef8a1c4..fbb9c5daf7 100644
--- a/core/config/src/main/java/org/apache/isis/core/config/progmodel/ProgrammingModelConstants.java
+++ b/core/config/src/main/java/org/apache/isis/core/config/progmodel/ProgrammingModelConstants.java
@@ -32,6 +32,9 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.Vector;
 import java.util.function.Function;
 import java.util.function.IntFunction;
 import java.util.function.Predicate;
@@ -49,6 +52,8 @@ import org.apache.isis.applib.annotation.ObjectLifecycle;
 import org.apache.isis.applib.annotation.ObjectSupport;
 import org.apache.isis.applib.services.i18n.TranslatableString;
 import org.apache.isis.commons.collections.Can;
+import org.apache.isis.commons.collections.ImmutableCollection;
+import org.apache.isis.commons.collections.ImmutableEnumSet;
 import org.apache.isis.commons.functional.Try;
 import org.apache.isis.commons.internal.base._Casts;
 import org.apache.isis.commons.internal.base._Refs;
@@ -66,6 +71,7 @@ import lombok.NonNull;
 import lombok.RequiredArgsConstructor;
 import lombok.SneakyThrows;
 import lombok.val;
+import lombok.experimental.Accessors;
 
 public final class ProgrammingModelConstants {
 
@@ -531,6 +537,48 @@ public final class ProgrammingModelConstants {
 
     }
 
+    /**
+     * Supported collection types, including arrays.
+     * Order matters, as class substitution is processed on first matching type.
+     * <p>
+     * Non scalar <i>Action Parameter</i> types cannot be more special than what we offer here.
+     */
+    @RequiredArgsConstructor
+    public static enum CollectionType {
+        ARRAY(Array.class),
+        VECTOR(Vector.class),
+        LIST(List.class),
+        SORTED_SET(SortedSet.class),
+        SET(Set.class),
+        COLLECTION(Collection.class),
+        CAN(Can.class),
+        IMMUTABLE_COLLECTION(ImmutableCollection.class),
+        ;
+        public boolean isArray() {return this == ARRAY;}
+        public boolean isVector() {return this == VECTOR;}
+        public boolean isList() {return this == LIST;}
+        public boolean isSortedSet() {return this == SORTED_SET;}
+        public boolean isSet() {return this == SET;}
+        public boolean isCollection() {return this == COLLECTION;}
+        public boolean isCan() {return this == CAN;}
+        public boolean isImmutableCollection() {return this == IMMUTABLE_COLLECTION;}
+        //
+        public boolean isSetAny() {return isSet() || isSortedSet(); }
+        @Getter private final Class<?> containerType;
+        private static final ImmutableEnumSet<CollectionType> all =
+                ImmutableEnumSet.allOf(CollectionType.class);
+        @Getter @Accessors(fluent = true)
+        private static final ImmutableEnumSet<CollectionType> typeSubstitutors = all.remove(ARRAY);
+        public static Optional<CollectionType> valueOf(final @Nullable Class<?> type) {
+            if(type==null) return Optional.empty();
+            return type.isArray()
+                    ? Optional.of(CollectionType.ARRAY)
+                    : all.stream()
+                        .filter(collType->collType.getContainerType().isAssignableFrom(type))
+                        .findFirst();
+        }
+    }
+
     //TODO perhaps needs an update to reflect Java 7->11 Language changes
     @RequiredArgsConstructor
     public static enum WrapperFactoryProxy {
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/spec/TypeOfAnyCardinality.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/spec/TypeOfAnyCardinality.java
new file mode 100644
index 0000000000..38073fa4b0
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/spec/TypeOfAnyCardinality.java
@@ -0,0 +1,134 @@
+/*
+ *  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.isis.core.metamodel.spec;
+
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.core.ResolvableType;
+
+import org.apache.isis.commons.internal.assertions._Assert;
+import org.apache.isis.core.config.progmodel.ProgrammingModelConstants;
+
+import lombok.NonNull;
+import lombok.val;
+
+@lombok.Value(staticConstructor = "of")
+public class TypeOfAnyCardinality {
+
+    /**
+     * The type either contained or not.
+     */
+    private final @NonNull  Class<?> elementType;
+
+    /**
+     * Optionally the container type, the {@link #getElementType()} is contained in,
+     * such as {@link List}, {@link Collection}, etc.
+     */
+    private final @NonNull Optional<Class<?>> containerType;
+
+    public boolean isScalar() {
+        return containerType.isEmpty();
+    }
+
+    // -- FACTORIES
+
+    public static TypeOfAnyCardinality scalar(final @NonNull Class<?> scalarType) {
+        return of(assertScalar(scalarType), Optional.empty());
+    }
+
+    public static TypeOfAnyCardinality nonScalar(
+            final @NonNull Class<?> elementType,
+            final @NonNull Class<?> nonScalarType) {
+        return of(assertScalar(elementType), Optional.of(assertNonScalar(nonScalarType)));
+    }
+
+    public static TypeOfAnyCardinality forMethodReturn(
+            final Class<?> implementationClass, final Method method) {
+        val methodReturn = method.getReturnType();
+
+        return ProgrammingModelConstants.CollectionType.valueOf(methodReturn)
+        .map(collectionType->
+            nonScalar(
+                    inferElementTypeForMethodReturn(implementationClass, method),
+                    methodReturn)
+        )
+        .orElseGet(()->scalar(methodReturn));
+    }
+
+    public static TypeOfAnyCardinality forParameter(
+            final Class<?> implementationClass, final Method method, final int paramIndex) {
+        val paramType = method.getParameters()[paramIndex].getType();
+
+        return ProgrammingModelConstants.CollectionType.valueOf(paramType)
+        .map(collectionType->
+            nonScalar(
+                    inferElementTypeForMethodParameter(implementationClass, method, paramIndex),
+                    paramType)
+        )
+        .orElseGet(()->scalar(paramType));
+    }
+
+    // -- WITHERS
+
+    public TypeOfAnyCardinality withElementType(final @NonNull Class<?> elementType) {
+        return of(assertScalar(elementType), this.getContainerType());
+    }
+
+    // -- HELPER
+
+    private static Class<?> assertScalar(final @NonNull Class<?> scalarType) {
+        _Assert.assertEquals(
+                Optional.empty(),
+                ProgrammingModelConstants.CollectionType.valueOf(scalarType),
+                ()->String.format("%s should not match any supported non-scalar types", scalarType));
+        return scalarType;
+    }
+
+    private static Class<?> assertNonScalar(final @NonNull Class<?> nonScalarType) {
+        _Assert.assertTrue(
+                ProgrammingModelConstants.CollectionType.valueOf(nonScalarType).isPresent(),
+                ()->String.format("%s should match a supported non-scalar type", nonScalarType));
+        return nonScalarType;
+    }
+
+    /** Return the element type as a resolved Class, falling back to Object if no specific class can be resolved. */
+    private static Class<?> inferElementTypeForMethodReturn(
+            final Class<?> implementationClass, final Method method) {
+        val nonScalar = ResolvableType.forMethodReturnType(method, implementationClass);
+        return toClass(nonScalar);
+    }
+
+    /** Return the element type as a resolved Class, falling back to Object if no specific class can be resolved. */
+    private static Class<?> inferElementTypeForMethodParameter(
+            final Class<?> implementationClass, final Method method, final int paramIndex) {
+        val nonScalar = ResolvableType.forMethodParameter(method, paramIndex, implementationClass);
+        return toClass(nonScalar);
+    }
+
+    private static Class<?> toClass(final ResolvableType nonScalar){
+        val genericTypeArg = nonScalar.isArray()
+                ? nonScalar.getComponentType()
+                : nonScalar.getGeneric(0);
+        return genericTypeArg.toClass();
+    }
+
+}
diff --git a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/spec/TypeOfAnyCardinalityTest.java b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/spec/TypeOfAnyCardinalityTest.java
new file mode 100644
index 0000000000..5e2fa92570
--- /dev/null
+++ b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/spec/TypeOfAnyCardinalityTest.java
@@ -0,0 +1,182 @@
+/*
+ *  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.isis.core.metamodel.spec;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.SortedSet;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.core.ResolvableType;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.apache.isis.commons.internal._Constants;
+import org.apache.isis.core.config.progmodel.ProgrammingModelConstants;
+
+import lombok.SneakyThrows;
+import lombok.val;
+
+class TypeOfAnyCardinalityTest {
+
+    // -- SCENARIO: ARRAY
+
+    static abstract class X {
+        public abstract CharSequence[] someStrings();
+    }
+
+    static class Y extends X {
+        @Override
+        public CharSequence[] someStrings() {
+            return new String[]{};
+        }
+    }
+
+    static class Z extends X {
+        @Override
+        public String[] someStrings() {
+            return new String[]{};
+        }
+    }
+
+    @Test
+    void testArray() {
+
+        val array = new String[]{};
+
+        assertEquals(
+                ProgrammingModelConstants.CollectionType.ARRAY,
+                ProgrammingModelConstants.CollectionType.valueOf(array.getClass())
+                    .orElse(null));
+
+        val arC = new CharSequence[] {};
+        val arS = new String[] {};
+
+        test(X.class, Y.class, Z.class,
+                CharSequence.class, CharSequence.class, String.class,
+                arC.getClass(), arC.getClass(), arS.getClass());
+    }
+
+    // -- SCENARIO: SET vs SORTED_SET
+
+    static abstract class A {
+        public abstract Set<String> someStrings();
+    }
+
+    static class B extends A {
+        @Override
+        public Set<String> someStrings() {
+            return Collections.emptySet();
+        }
+    }
+
+    static class C extends A {
+        @Override
+        public SortedSet<String> someStrings() {
+            return Collections.emptySortedSet();
+        }
+    }
+
+    @Test
+    void testString() {
+        test(A.class, B.class, C.class,
+                String.class, String.class, String.class,
+                Set.class, Set.class, SortedSet.class);
+    }
+
+    // -- SCENARIO: UPPERBOUND
+
+    static abstract class E {
+        public abstract Set<? extends CharSequence> someStrings();
+    }
+
+    static class F extends E {
+        @Override
+        public Set<? extends CharSequence> someStrings() {
+            return Collections.emptySet();
+        }
+    }
+
+    static class G extends E {
+        @Override
+        public SortedSet<String> someStrings() {
+            return Collections.emptySortedSet();
+        }
+    }
+
+    @Test
+    void testUpperBounded() {
+        test(E.class, F.class, G.class,
+                CharSequence.class, CharSequence.class, String.class,
+                Set.class, Set.class, SortedSet.class);
+    }
+
+    // -- HELPER
+
+    @SneakyThrows
+    void test(final Class<?> a, final Class<?> b, final Class<?> c,
+            final Class<?> genericA, final Class<?> genericB, final Class<?> genericC,
+            final Class<?> contA, final Class<?> contB, final Class<?> contC) {
+
+        val methodInA = a.getMethod("someStrings", _Constants.emptyClasses);
+        val methodInB = b.getMethod("someStrings", _Constants.emptyClasses);
+        val methodInC = c.getMethod("someStrings", _Constants.emptyClasses);
+
+        assertNotNull(methodInA);
+        assertNotNull(methodInB);
+        assertNotNull(methodInC);
+
+        val returnA = ResolvableType.forMethodReturnType(methodInA, a);
+        val returnB = ResolvableType.forMethodReturnType(methodInB, b);
+        val returnC = ResolvableType.forMethodReturnType(methodInC, c);
+
+        val genericArgA = returnA.isArray()
+                ? returnA.getComponentType()
+                : returnA.getGeneric(0);
+        val genericArgB = returnB.isArray()
+                ? returnB.getComponentType()
+                : returnB.getGeneric(0);
+        val genericArgC = returnC.isArray()
+                ? returnC.getComponentType()
+                : returnC.getGeneric(0);
+
+        assertNotNull(genericArgA);
+        assertNotNull(genericArgB);
+        assertNotNull(genericArgC);
+
+        assertEquals(genericA, genericArgA.toClass());
+        assertEquals(genericB, genericArgB.toClass());
+        assertEquals(genericC, genericArgC.toClass());
+
+        val typeA = TypeOfAnyCardinality.forMethodReturn(a, methodInA);
+        val typeB = TypeOfAnyCardinality.forMethodReturn(b, methodInB);
+        val typeC = TypeOfAnyCardinality.forMethodReturn(c, methodInC);
+
+        assertEquals(genericA, typeA.getElementType());
+        assertEquals(genericB, typeB.getElementType());
+        assertEquals(genericC, typeC.getElementType());
+
+        assertEquals(contA, typeA.getContainerType().orElse(null));
+        assertEquals(contB, typeB.getContainerType().orElse(null));
+        assertEquals(contC, typeC.getContainerType().orElse(null));
+
+    }
+
+}