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