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