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 2019/03/15 12:26:44 UTC
[isis] branch 2033-IoC updated: ISIS-2033: introduces 'Bin' a
framework internal element container
This is an automated email from the ASF dual-hosted git repository.
ahuber pushed a commit to branch 2033-IoC
in repository https://gitbox.apache.org/repos/asf/isis.git
The following commit(s) were added to refs/heads/2033-IoC by this push:
new 960fcd4 ISIS-2033: introduces 'Bin' a framework internal element container
960fcd4 is described below
commit 960fcd4d26252ef0a26d2688f17020ae094268f5
Author: Andi Huber <ah...@apache.org>
AuthorDate: Fri Mar 15 13:26:37 2019 +0100
ISIS-2033: introduces 'Bin' a framework internal element container
immutable, with convenient support for special cases of cardinality:
* ZERO/EMPTY
* ONE
* MULTIPLE
idea extends on Java's built-in idioms: Optional and Collection
Task-Url: https://issues.apache.org/jira/browse/ISIS-2033
---
.../isis/commons/internal/context/_Context.java | 107 +++++++++------
.../internal/context/_Context_ThreadLocal.java | 78 ++++++-----
...al.java => _Context_ThreadLocal_Singleton.java} | 30 +++--
.../apache/isis/core/commons/collections/Bin.java | 150 +++++++++++++++++++++
.../isis/core/commons/collections/Bin_Empty.java | 38 ++++++
.../core/commons/collections/Bin_Multiple.java | 38 ++++++
.../core/commons/collections/Bin_Singleton.java | 37 +++++
.../isis/core/commons/collections/Cardinality.java | 25 ++++
8 files changed, 417 insertions(+), 86 deletions(-)
diff --git a/core/commons/src/main/java/org/apache/isis/commons/internal/context/_Context.java b/core/commons/src/main/java/org/apache/isis/commons/internal/context/_Context.java
index 3c89a34..f6cddd2 100644
--- a/core/commons/src/main/java/org/apache/isis/commons/internal/context/_Context.java
+++ b/core/commons/src/main/java/org/apache/isis/commons/internal/context/_Context.java
@@ -24,15 +24,16 @@ import static org.apache.isis.commons.internal.base._With.ifPresentElseGet;
import static org.apache.isis.commons.internal.base._With.ifPresentElseThrow;
import static org.apache.isis.commons.internal.base._With.requires;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Supplier;
import org.apache.isis.commons.internal.base._Casts;
import org.apache.isis.commons.internal.collections._Lists;
+import org.apache.isis.core.commons.collections.Bin;
import org.apache.isis.core.plugins.environment.IsisSystemEnvironment;
import org.apache.isis.core.plugins.environment.IsisSystemEnvironmentPlugin;
@@ -59,8 +60,9 @@ public final class _Context {
* the first one wins.<br/>
* If synchronization is required it should happen elsewhere, not here!<br/>
*/
- private final static Map<String, Object> singletonMap = new HashMap<>();
+ private final static Map<Class<?>, Object> singletonMap = new ConcurrentHashMap<>();
+ private final static Object $LOCK = new Object[0];
/**
@@ -75,11 +77,11 @@ public final class _Context {
requires(singleton, "singleton");
// let writes to the map be atomic
- synchronized (singletonMap) {
- if(singletonMap.containsKey(toKey(type)))
+ synchronized ($LOCK) {
+ if(singletonMap.containsKey(type))
throw new IllegalStateException(
"there is already a singleton of type '"+type+"' on this context.");
- singletonMap.put(toKey(type), singleton);
+ singletonMap.put(type, singleton);
}
}
@@ -96,12 +98,12 @@ public final class _Context {
requires(singleton, "singleton");
// let writes to the map be atomic
- synchronized (singletonMap) {
- if(singletonMap.containsKey(toKey(type))) {
+ synchronized ($LOCK) {
+ if(singletonMap.containsKey(type)) {
if(!override)
return false;
}
- singletonMap.put(toKey(type), singleton);
+ singletonMap.put(type, singleton);
return true;
}
}
@@ -113,7 +115,7 @@ public final class _Context {
* @return null, if there is no such instance
*/
public static <T> T getIfAny(Class<? super T> type) {
- return _Casts.uncheckedCast(singletonMap.get(toKey(type)));
+ return _Casts.uncheckedCast(singletonMap.get(type));
}
/**
@@ -127,20 +129,27 @@ public final class _Context {
requires(type, "type");
requires(factory, "factory");
- final String key = toKey(type);
+ final T existingIfAny = _Casts.uncheckedCast(singletonMap.get(type));
+ if(existingIfAny!=null) {
+ return existingIfAny;
+ }
+
+ // Note: we don't want to to this inside the synchronized block
+ final T t = factory.apply(type);
// let writes to the map be atomic
- synchronized (singletonMap) {
+ synchronized ($LOCK) {
// Note: cannot just use 'singletonMap.computeIfAbsent(toKey(type), __->factory.apply(type));'
// here because it does not allow for modification of singletonMap inside the factory call
+ // Also we do need a second check for existing key, since it might have been changed by another
+ // thread since.
+ final T existingIfAny2 = _Casts.uncheckedCast(singletonMap.get(type));
+ if(existingIfAny2!=null) {
+ return existingIfAny2;
+ }
- final T existingIfAny = _Casts.uncheckedCast(singletonMap.get(key));
- if(existingIfAny!=null) {
- return existingIfAny;
- }
- final T t = factory.apply(type);
- singletonMap.put(key, t);
+ singletonMap.put(type, t);
return t;
}
}
@@ -207,8 +216,8 @@ public final class _Context {
public static void remove(Class<?> type) {
// let writes to the map be atomic
- synchronized (singletonMap) {
- singletonMap.remove(toKey(type));
+ synchronized ($LOCK) {
+ singletonMap.remove(type);
}
tryClose(type);
}
@@ -222,7 +231,7 @@ public final class _Context {
public static void clear() {
// let writes to the map be atomic
- synchronized (singletonMap) {
+ synchronized ($LOCK) {
closeAnyClosables(_Lists.newArrayList(singletonMap.values()));
@@ -239,31 +248,45 @@ public final class _Context {
/**
* Puts {@code payload} onto the current thread's map.
- * @param type
+ * @param type - the key into the thread-local store
* @param payload
*/
public static <T> void threadLocalPut(Class<? super T> type, T payload) {
- _Context_ThreadLocal.put(toKey(type), payload);
- }
-
- /**
- * Puts {@code payload} onto the current thread's map. The provided {@code onCleanup} is called whenever
- * {@code threadLocalCleanup()} is called.
- * @param type
- * @param payload
- * @param onCleanup
- */
- public static <T> void threadLocalPut(Class<? super T> type, T payload, Runnable onCleanup) {
- _Context_ThreadLocal.put(toKey(type), payload, onCleanup);
+ _Context_ThreadLocal.put(type, payload);
}
+
+//TODO [2033] cleanup comments
+// /**
+// * Puts {@code payload} onto the current thread's map. The provided {@code onCleanup} is called whenever
+// * {@code threadLocalCleanup()} is called.
+// * @param type
+// * @param payload
+// * @param onCleanup
+// */
+// public static <T> void threadLocalPut(Class<? super T> type, T payload, Runnable onCleanup) {
+// _Context_ThreadLocal.put(type, payload, onCleanup);
+// }
+
/**
- * Looks up current thread's value as previously stored with {@link _Context#threadLocalPut(Class, Object)}.
- * @param type
+ * Looks up current thread's values for any instances that match the given type, as previously stored
+ * with {@link _Context#threadLocalPut(Class, Object)}.
+ * @param type - the key into the thread-local store
+ * @return
+ */
+ public static <T> Bin<T> threadLocalGet(Class<? super T> type) {
+ return _Context_ThreadLocal.get(type);
+ }
+
+ /**
+ * Looks up current thread's values for any instances that match the given type, as previously stored
+ * with {@link _Context#threadLocalPut(Class, Object)}.
+ * @param type - the key into the thread-local store
+ * @param requiredType - the required type of the elements in the returned bin
* @return
*/
- public static <T> T threadLocalGetIfAny(Class<? super T> type) {
- return _Casts.uncheckedCast(_Context_ThreadLocal.get(toKey(type)));
+ public static <T> Bin<T> threadLocalSelect(Class<? super T> type, Class<? super T> requiredType) {
+ return _Context_ThreadLocal.select(type, requiredType);
}
/**
@@ -296,8 +319,8 @@ public final class _Context {
final boolean alreadyRegistered = _Context.getIfAny(ClassLoader.class)!=null;
if(!alreadyRegistered || override) {
// let writes to the map be atomic
- synchronized (singletonMap) {
- singletonMap.put(toKey(ClassLoader.class), requires(classLoader, "classLoader"));
+ synchronized ($LOCK) {
+ singletonMap.put(ClassLoader.class, requires(classLoader, "classLoader"));
}
}
}
@@ -345,10 +368,6 @@ public final class _Context {
}
// -- HELPER
-
- private static String toKey(Class<?> type) {
- return type.getName();
- }
private static void tryClose(Object singleton) {
if(singleton==null) {
@@ -363,6 +382,8 @@ public final class _Context {
}
}
+
+
diff --git a/core/commons/src/main/java/org/apache/isis/commons/internal/context/_Context_ThreadLocal.java b/core/commons/src/main/java/org/apache/isis/commons/internal/context/_Context_ThreadLocal.java
index 06bbea1..66379f6 100644
--- a/core/commons/src/main/java/org/apache/isis/commons/internal/context/_Context_ThreadLocal.java
+++ b/core/commons/src/main/java/org/apache/isis/commons/internal/context/_Context_ThreadLocal.java
@@ -20,12 +20,13 @@
package org.apache.isis.commons.internal.context;
import static org.apache.isis.commons.internal.base._With.requires;
-import static org.apache.isis.commons.internal.base._With.requiresNotEmpty;
import java.util.HashMap;
import java.util.Map;
-import lombok.Value;
+import org.apache.isis.commons.internal.base._Casts;
+import org.apache.isis.core.commons.collections.Bin;
+
import lombok.val;
/**
@@ -42,54 +43,71 @@ import lombok.val;
*/
final class _Context_ThreadLocal {
+ //TODO [2033] cleanup comments ...
+
// -- MIXINS
- static void put(String key, Object payload) {
- requiresNotEmpty(key, "key");
- requires(payload, "payload");
- THREAD_LOCAL_MAP.get().put(key, Payload.of(payload, null));
+ static <T> void put(Class<? super T> type, T variant) {
+ requires(type, "type");
+ requires(variant, "variant");
+ THREAD_LOCAL_MAP.get()
+ .compute(type, (k, v) -> v == null
+ ? Bin.<T>ofSingleton(variant)
+ : Bin.<T>concat(_Casts.uncheckedCast(v), variant));
}
- static void put(String key, Object payload, Runnable onCleanup) {
- requiresNotEmpty(key, "key");
- requires(payload, "payload");
- requires(onCleanup, "onCleanup");
- THREAD_LOCAL_MAP.get().put(key, Payload.of(payload, onCleanup));
- }
+// static <T> void put(Class<? super T> type, Object payload, Runnable onCleanup) {
+// requires(type, "type");
+// requires(payload, "payload");
+// requires(onCleanup, "onCleanup");
+// THREAD_LOCAL_MAP.get().put(type, Payload.of(payload, onCleanup));
+// }
+
+ static <T> Bin<T> select(Class<? super T> type, Class<? super T> instanceOf) {
+ val bin = _Context_ThreadLocal.<T>get(type);
+ return bin.filter(t -> isInstanceOf(t, instanceOf));
+ }
+
+ private static boolean isInstanceOf(Object obj, Class<?> type) {
+ return type.isAssignableFrom(obj.getClass());
+ }
- static Object get(String key) {
- val payload = THREAD_LOCAL_MAP.get().get(key);
- if(payload!=null) {
- return payload.pojo;
+ static <T> Bin<T> get(Class<? super T> type) {
+ val bin = THREAD_LOCAL_MAP.get().get(type);
+ if(bin!=null) {
+ return _Casts.uncheckedCast(bin);
}
- return null;
+ return Bin.empty();
}
static void cleanupThread() {
- THREAD_LOCAL_MAP.get().forEach((key, payload)->payload.cleanUp());
+// THREAD_LOCAL_MAP.get().forEach((key, payload)->payload.cleanUp());
THREAD_LOCAL_MAP.remove();
}
// -- HELPER
private _Context_ThreadLocal(){}
-
- @Value(staticConstructor="of")
- private final static class Payload {
- final Object pojo;
- final Runnable onCleanup;
- void cleanUp() {
- if(onCleanup!=null) {
- onCleanup.run();
- }
- }
- }
/**
* Inheritable... allows to have concurrent computations utilizing the ForkJoinPool.
*/
- private final static ThreadLocal<Map<String, Payload>> THREAD_LOCAL_MAP =
+ private final static ThreadLocal<Map<Class<?>, Bin<?>>> THREAD_LOCAL_MAP =
InheritableThreadLocal.withInitial(HashMap::new);
+
+
+// @Value(staticConstructor="of")
+// private final static class Payload<T> {
+// final Instance<T> instance;
+// final Runnable onCleanup;
+// void cleanUp() {
+// if(onCleanup!=null) {
+// onCleanup.run();
+// }
+// }
+// }
+
+
}
diff --git a/core/commons/src/main/java/org/apache/isis/commons/internal/context/_Context_ThreadLocal.java b/core/commons/src/main/java/org/apache/isis/commons/internal/context/_Context_ThreadLocal_Singleton.java
similarity index 73%
copy from core/commons/src/main/java/org/apache/isis/commons/internal/context/_Context_ThreadLocal.java
copy to core/commons/src/main/java/org/apache/isis/commons/internal/context/_Context_ThreadLocal_Singleton.java
index 06bbea1..ebfa716 100644
--- a/core/commons/src/main/java/org/apache/isis/commons/internal/context/_Context_ThreadLocal.java
+++ b/core/commons/src/main/java/org/apache/isis/commons/internal/context/_Context_ThreadLocal_Singleton.java
@@ -20,11 +20,12 @@
package org.apache.isis.commons.internal.context;
import static org.apache.isis.commons.internal.base._With.requires;
-import static org.apache.isis.commons.internal.base._With.requiresNotEmpty;
import java.util.HashMap;
import java.util.Map;
+import org.apache.isis.commons.internal.base._Casts;
+
import lombok.Value;
import lombok.val;
@@ -40,27 +41,28 @@ import lombok.val;
* </p>
* @since 2.0.0-M3
*/
-final class _Context_ThreadLocal {
+@Deprecated //TODO [2033] superseded by _Context_ThreadLocal
+final class _Context_ThreadLocal_Singleton {
// -- MIXINS
- static void put(String key, Object payload) {
- requiresNotEmpty(key, "key");
+ static <T> void put(Class<? super T> type, T payload) {
+ requires(type, "type");
requires(payload, "payload");
- THREAD_LOCAL_MAP.get().put(key, Payload.of(payload, null));
+ THREAD_LOCAL_MAP.get().put(type, Payload.of(payload, null));
}
- static void put(String key, Object payload, Runnable onCleanup) {
- requiresNotEmpty(key, "key");
+ static <T> void put(Class<? super T> type, Object payload, Runnable onCleanup) {
+ requires(type, "type");
requires(payload, "payload");
requires(onCleanup, "onCleanup");
- THREAD_LOCAL_MAP.get().put(key, Payload.of(payload, onCleanup));
+ THREAD_LOCAL_MAP.get().put(type, Payload.of(payload, onCleanup));
}
- static Object get(String key) {
- val payload = THREAD_LOCAL_MAP.get().get(key);
+ static <T> T get(Class<? super T> type) {
+ val payload = THREAD_LOCAL_MAP.get().get(type);
if(payload!=null) {
- return payload.pojo;
+ return _Casts.uncheckedCast(payload.pojo);
}
return null;
}
@@ -72,7 +74,7 @@ final class _Context_ThreadLocal {
// -- HELPER
- private _Context_ThreadLocal(){}
+ private _Context_ThreadLocal_Singleton(){}
@Value(staticConstructor="of")
private final static class Payload {
@@ -88,8 +90,10 @@ final class _Context_ThreadLocal {
/**
* Inheritable... allows to have concurrent computations utilizing the ForkJoinPool.
*/
- private final static ThreadLocal<Map<String, Payload>> THREAD_LOCAL_MAP =
+ private final static ThreadLocal<Map<Class<?>, Payload>> THREAD_LOCAL_MAP =
InheritableThreadLocal.withInitial(HashMap::new);
+
+
}
diff --git a/core/commons/src/main/java/org/apache/isis/core/commons/collections/Bin.java b/core/commons/src/main/java/org/apache/isis/core/commons/collections/Bin.java
new file mode 100644
index 0000000..7cd511c
--- /dev/null
+++ b/core/commons/src/main/java/org/apache/isis/core/commons/collections/Bin.java
@@ -0,0 +1,150 @@
+package org.apache.isis.core.commons.collections;
+
+import static org.apache.isis.commons.internal.base._With.requires;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.annotation.Nullable;
+
+import org.apache.isis.commons.internal.base._NullSafe;
+
+import lombok.val;
+
+/**
+ *
+ * Immutable 'multi-set'.
+ *
+ * @param <T>
+ * @since 2.0.0-M3
+ */
+public interface Bin<T> {
+
+ Cardinality getCardinality();
+ int size();
+
+ Stream<T> stream();
+
+ Optional<T> getFirst();
+ Optional<T> getSingleton();
+
+ // -- FACTORIES
+
+ @SuppressWarnings("unchecked") // this is how the JDK does it for eg. empty lists
+ public static <T> Bin<T> empty() {
+ return (Bin<T>) Bin_Empty.INSTANCE;
+ }
+
+ public static <T> Bin<T> ofNullable(@Nullable T element) {
+ if(element==null) {
+ return empty();
+ }
+ return Bin_Singleton.of(element);
+ }
+
+ public static <T> Bin<T> ofSingleton(T element) {
+ requires(element, "element");
+ return Bin_Singleton.of(element);
+ }
+
+ public static <T> Bin<T> ofCollection(@Nullable Collection<T> collection){
+
+ if(_NullSafe.size(collection)==0) {
+ return empty();
+ }
+
+ val maxSize = collection.size();
+
+ val nonNullElements = collection.stream()
+ .filter(_NullSafe::isPresent)
+ .collect(Collectors.toCollection(()->new ArrayList<>(maxSize)));
+
+ nonNullElements.trimToSize(); // in case we have a 'sparse' collection as input to this method
+
+ val size = nonNullElements.size();
+
+ if(size==0) {
+ return empty();
+ }
+
+ if(size==1) {
+ return ofSingleton(((List<T>)nonNullElements).get(0));
+ }
+
+ return Bin_Multiple.of(nonNullElements);
+ }
+
+ // -- OPERATORS
+
+ public default Bin<T> filter(@Nullable Predicate<? super T> predicate) {
+ if(predicate==null || isEmpty()) {
+ return this;
+ }
+
+ // optimization for the singleton case
+ if(isCardinalityOne()) {
+ val singleton = getSingleton().get();
+ return predicate.test(singleton)
+ ? this
+ : empty();
+ }
+
+ val filteredElements =
+ stream()
+ .filter(predicate)
+ .collect(Collectors.toCollection(ArrayList::new));
+
+ // optimization for the case when the filter accepted all
+ if(filteredElements.size()==size()) {
+ return this;
+ }
+
+ return ofCollection(filteredElements);
+ }
+
+ public static <T> Bin<T> concat(@Nullable Bin<T> bin, @Nullable T variant) {
+ if(bin==null || bin.isEmpty()) {
+ return ofNullable(variant);
+ }
+ if(variant==null) {
+ return bin;
+ }
+ // at this point: bin is not empty and variant is not null
+ val newSize = bin.size() + 1;
+ val union = bin.stream().collect(Collectors.toCollection(()->new ArrayList<>(newSize)));
+ union.add(variant);
+ return Bin_Multiple.of(union);
+ }
+
+
+ // -- SHORTCUTS FOR PREDICATES
+
+ default boolean isEmpty() {
+ return getCardinality().isZero();
+ }
+
+ default boolean isNotEmpty() {
+ return !getCardinality().isZero();
+ }
+
+ default boolean isCardinalityOne() {
+ return getCardinality().isOne();
+ }
+
+ default boolean isCardinalityMultiple() {
+ return getCardinality().isMultiple();
+ }
+
+
+
+
+ // -- SHORTCUTS FOR ELEMENT ACCESS
+
+
+
+}
diff --git a/core/commons/src/main/java/org/apache/isis/core/commons/collections/Bin_Empty.java b/core/commons/src/main/java/org/apache/isis/core/commons/collections/Bin_Empty.java
new file mode 100644
index 0000000..c419fdb
--- /dev/null
+++ b/core/commons/src/main/java/org/apache/isis/core/commons/collections/Bin_Empty.java
@@ -0,0 +1,38 @@
+package org.apache.isis.core.commons.collections;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import lombok.Value;
+
+@Value(staticConstructor="of")
+final class Bin_Empty<T> implements Bin<T> {
+
+ static final Bin_Empty<?> INSTANCE = new Bin_Empty<>();
+
+ @Override
+ public Cardinality getCardinality() {
+ return Cardinality.ZERO;
+ }
+
+ @Override
+ public Stream<T> stream() {
+ return Stream.empty();
+ }
+
+ @Override
+ public Optional<T> getSingleton() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional<T> getFirst() {
+ return Optional.empty();
+ }
+
+ @Override
+ public int size() {
+ return 0;
+ }
+
+}
diff --git a/core/commons/src/main/java/org/apache/isis/core/commons/collections/Bin_Multiple.java b/core/commons/src/main/java/org/apache/isis/core/commons/collections/Bin_Multiple.java
new file mode 100644
index 0000000..47af06d
--- /dev/null
+++ b/core/commons/src/main/java/org/apache/isis/core/commons/collections/Bin_Multiple.java
@@ -0,0 +1,38 @@
+package org.apache.isis.core.commons.collections;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor(staticName="of")
+final class Bin_Multiple<T> implements Bin<T> {
+
+ private final List<T> elements;
+
+ @Getter(lazy=true, onMethod=@__({@Override}))
+ private final Optional<T> first = Optional.of(elements.get(0));
+
+ @Override
+ public Cardinality getCardinality() {
+ return Cardinality.MULTIPLE;
+ }
+
+ @Override
+ public Stream<T> stream() {
+ return elements.stream();
+ }
+
+ @Override
+ public Optional<T> getSingleton() {
+ return Optional.empty();
+ }
+
+ @Override
+ public int size() {
+ return elements.size();
+ }
+
+}
diff --git a/core/commons/src/main/java/org/apache/isis/core/commons/collections/Bin_Singleton.java b/core/commons/src/main/java/org/apache/isis/core/commons/collections/Bin_Singleton.java
new file mode 100644
index 0000000..acb92a6
--- /dev/null
+++ b/core/commons/src/main/java/org/apache/isis/core/commons/collections/Bin_Singleton.java
@@ -0,0 +1,37 @@
+package org.apache.isis.core.commons.collections;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor(staticName="of")
+final class Bin_Singleton<T> implements Bin<T> {
+
+ private final T element;
+
+ @Getter(lazy=true, onMethod=@__({@Override}))
+ private final Optional<T> singleton = Optional.of(element);
+
+ @Override
+ public Cardinality getCardinality() {
+ return Cardinality.ONE;
+ }
+
+ @Override
+ public Stream<T> stream() {
+ return Stream.of(element);
+ }
+
+ @Override
+ public Optional<T> getFirst() {
+ return getSingleton();
+ }
+
+ @Override
+ public int size() {
+ return 1;
+ }
+
+}
diff --git a/core/commons/src/main/java/org/apache/isis/core/commons/collections/Cardinality.java b/core/commons/src/main/java/org/apache/isis/core/commons/collections/Cardinality.java
new file mode 100644
index 0000000..c4ff2a2
--- /dev/null
+++ b/core/commons/src/main/java/org/apache/isis/core/commons/collections/Cardinality.java
@@ -0,0 +1,25 @@
+package org.apache.isis.core.commons.collections;
+
+/**
+ * @since 2.0.0-M3
+ */
+public enum Cardinality {
+
+ ZERO,
+ ONE,
+ MULTIPLE
+ ;
+
+ public boolean isZero() {
+ return this == ZERO;
+ }
+
+ public boolean isOne() {
+ return this == ONE;
+ }
+
+ public boolean isMultiple() {
+ return this == MULTIPLE;
+ }
+
+}