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 <[email protected]> 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; + [email protected](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)); + + } + +}
