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:24 UTC

[isis] branch 3204-bounded.generics created (now db21865fa0)

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

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


      at db21865fa0 ISIS-3204: adds TypeOfAnyCardinality

This branch includes the following new commits:

     new db21865fa0 ISIS-3204: adds TypeOfAnyCardinality

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



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

Posted by ah...@apache.org.
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));
+
+    }
+
+}