diff --git a/README.md b/README.md
index 4864106a..396fe67a 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ Both of these features ensure that our library can be seamlessly integrated into
To include in your project:
##### Gradle
```
-implementation 'com.cedarsoftware:java-util:2.8.0'
+implementation 'com.cedarsoftware:java-util:2.9.0'
```
##### Maven
@@ -33,7 +33,7 @@ implementation 'com.cedarsoftware:java-util:2.8.0'
com.cedarsoftware
java-util
- 2.8.0
+ 2.9.0
```
---
@@ -51,6 +51,8 @@ same class.
* **CompactCIHashSet** - Small memory footprint `Set` that expands to a case-insensitive `HashSet` when `size() > compactSize()`.
* **CaseInsensitiveSet** - `Set` that ignores case for `Strings` contained within.
* **ConcurrentHashSet** - A thread-safe `Set` which does not require each element to be `Comparable` like `ConcurrentSkipListSet` which is a `NavigableSet,` which is a `SortedSet.`
+ * **SealableSet** - Provides a `Set` (or `Set` wrapper) that will make it read-only (sealed) or read-write (unsealed), controllable via a `Supplier.` This moves the immutability control outside the `Set` and ensures that all views on the `Set` respect the sealed-ness. One master supplier can control the immutability of many collections.
+ * **SealableNavigableSet** - Provides a `NavigableSet` (or `NavigableSet` wrapper) that will make it read-only (sealed) or read-write (unsealed), controllable via a `Supplier.` This moves the immutability control outside the `NavigableSet` and ensures that all views on the `NavigableSet` respect the sealed-ness. One master supplier can control the immutability of many collections.
* **Maps**
* **CompactMap** - Small memory footprint `Map` that expands to a `HashMap` when `size() > compactSize()` entries.
* **CompactLinkedMap** - Small memory footprint `Map` that expands to a `LinkedHashMap` when `size() > compactSize()` entries.
@@ -59,8 +61,12 @@ same class.
* **CaseInsensitiveMap** - `Map` that ignores case when `Strings` are used as keys.
* **LRUCache** - Thread safe LRUCache that implements the full Map API and supports a maximum capacity. Once max capacity is reached, placing another item in the cache will cause the eviction of the item that was the least recently used (LRU).
* **TrackingMap** - `Map` class that tracks when the keys are accessed via `.get()` or `.containsKey()`. Provided by @seankellner
+ * **SealableMap** - Provides a `Map` (or `Map` wrapper) that will make it read-only (sealed) or read-write (unsealed), controllable via a `Supplier.` This moves the immutability control outside the `Map` and ensures that all views on the `Map` respect the sealed-ness. One master supplier can control the immutability of many collections.
+ * **SealableNavigbableMap** - Provides a `NavigableMap` (or `NavigableMap` wrapper) that will make it read-only (sealed) or read-write (unsealed), controllable via a `Supplier.` This moves the immutability control outside the `NavigableMap` and ensures that all views on the `NavigableMap` respect the sealed-ness. One master supplier can control the immutability of many collections.
* **Lists**
- * **ConcurrentList** - Provides a thread-safe `List` with all API support except for `listIterator(),` however, it implements `iterator()` which returns an iterator to a snapshot copy of the `List.`
+ * **ConcurrentList** - Provides a thread-safe `List` (or `List` wrapper). Use the no-arg constructor for a thread-safe `List,` use the constructor that takes a `List` to wrap another `List` instance and make it thread-safe (no elements are copied).
+ * **SealableList** - Provides a `List` (or `List` wrapper) that will make it read-only (sealed) or read-write (unsealed), controllable via a `Supplier.` This moves the immutability control outside the `List` and ensures that all views on the `List` respect the sealed-ness. One master supplier can control the immutability of many collections.
+
* **Converter** - Convert from one instance to another. For example, `convert("45.3", BigDecimal.class)` will convert the `String` to a `BigDecimal`. Works for all primitives, primitive wrappers, `Date`, `java.sql.Date`, `String`, `BigDecimal`, `BigInteger`, `AtomicBoolean`, `AtomicLong`, etc. The method is very generous on what it allows to be converted. For example, a `Calendar` instance can be input for a `Date` or `Long`. Call the method `Converter.getSupportedConversions()` or `Converter.allSupportedConversions()` to get a list of all source/target conversions. Currently, there are 680+ conversions.
* **DateUtilities** - Robust date String parser that handles date/time, date, time, time/date, string name months or numeric months, skips comma, etc. English month names only (plus common month name abbreviations), time with/without seconds or milliseconds, `y/m/d` and `m/d/y` ordering as well.
* **DeepEquals** - Compare two object graphs and return 'true' if they are equivalent, 'false' otherwise. This will handle cycles in the graph, and will call an `equals()` method on an object if it has one, otherwise it will do a field-by-field equivalency check for non-transient fields. Has options to turn on/off using `.equals()` methods that may exist on classes.
diff --git a/changelog.md b/changelog.md
index f1cd2a74..9ef86dab 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,4 +1,11 @@
### Revision History
+* 2.9.0
+ * Added `SealableList` which provides a `List` (or `List` wrapper) that will make it read-only (sealed) or read-write (unsealed), controllable via a `Supplier.` This moves the immutability control outside the list and ensures that all views on the `List` respect the sealed-ness. One master supplier can control the immutability of many collections.
+ * Added `SealableSet` similar to SealableList but with `Set` nature.
+ * Added `SealableMap` similar to SealableList but with `Map` nature.
+ * Added `SealableNavigableSet` similar to SealableList but with `NavigableSet` nature.
+ * Added `SealableNavigableMap` similar to SealableList but with `NavigableMap` nature.
+ * Updated `ConcurrentList` to support wrapping any `List` and making it thread-safe, including all view APIs: `iterator(),` `listIterator(),` `listIterator(index).` The no-arg constructor creates a `ConcurrentList` ready-to-go. The constructor that takes a `List` parameter constructor wraps the passed in list and makes it thread-safe.
* 2.8.0
* Added `ClassUtilities.doesOneWrapTheOther()` API so that it is easy to test if one class is wrapping the other.
* Added `StringBuilder` and `StringBuffer` to `Strings` to the `Converter.` Eliminates special cases for `.toString()` calls where generalized `convert(src, type)` is being used.
diff --git a/pom.xml b/pom.xml
index 4967e13e..ef7bec18 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
com.cedarsoftware
java-util
bundle
- 2.8.0
+ 2.9.0
Java Utilities
https://github.com/jdereg/java-util
diff --git a/src/main/java/com/cedarsoftware/util/ConcurrentList.java b/src/main/java/com/cedarsoftware/util/ConcurrentList.java
index 4dfb3740..cbf4ba11 100644
--- a/src/main/java/com/cedarsoftware/util/ConcurrentList.java
+++ b/src/main/java/com/cedarsoftware/util/ConcurrentList.java
@@ -9,6 +9,11 @@
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
+ * ConcurrentList provides a List or List wrapper that is thread-safe, usable in highly concurrent
+ * environments. It provides a no-arg constructor that will directly return a ConcurrentList that is
+ * thread-safe. It has a constructor that takes a List argument, which will wrap that List and make it
+ * thread-safe (no elements are duplicated).
+ *
* @author John DeRegnaucourt (jdereg@gmail.com)
*
* Copyright (c) Cedar Software LLC
@@ -26,9 +31,28 @@
* limitations under the License.
*/
public class ConcurrentList implements List {
- private final List list = new ArrayList<>();
+ private final List list;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
+ /**
+ * Use this no-arg constructor to create a ConcurrentList.
+ */
+ public ConcurrentList() {
+ this.list = new ArrayList<>();
+ }
+
+ /**
+ * Use this constructor to wrap a List (any kind of List) and make it a ConcurrentList.
+ * No duplicate of the List is created and the original list is operated on directly.
+ * @param list List instance to protect.
+ */
+ public ConcurrentList(List list) {
+ if (list == null) {
+ throw new IllegalArgumentException("list cannot be null");
+ }
+ this.list = list;
+ }
+
public int size() {
lock.readLock().lock();
try {
@@ -47,6 +71,24 @@ public boolean isEmpty() {
}
}
+ public boolean equals(Object obj) {
+ lock.readLock().lock();
+ try {
+ return list.equals(obj);
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ public int hashCode() {
+ lock.readLock().lock();
+ try {
+ return list.hashCode();
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
public boolean contains(Object o) {
lock.readLock().lock();
try {
@@ -209,20 +251,90 @@ public int lastIndexOf(Object o) {
}
}
+ public List subList(int fromIndex, int toIndex) { return new ConcurrentList<>(list.subList(fromIndex, toIndex)); }
+
public ListIterator listIterator() {
- throw new UnsupportedOperationException("ListIterator is not supported with thread-safe iteration.");
+ return createLockHonoringListIterator(list.listIterator());
}
public ListIterator listIterator(int index) {
- throw new UnsupportedOperationException("ListIterator is not supported with thread-safe iteration.");
+ return createLockHonoringListIterator(list.listIterator(index));
}
- public List subList(int fromIndex, int toIndex) {
- lock.readLock().lock();
- try {
- return new ArrayList<>(list.subList(fromIndex, toIndex)); // Return a snapshot of the sublist
- } finally {
- lock.readLock().unlock();
- }
+ private ListIterator createLockHonoringListIterator(ListIterator iterator) {
+ return new ListIterator() {
+ public boolean hasNext() {
+ lock.readLock().lock();
+ try {
+ return iterator.hasNext();
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+ public E next() {
+ lock.readLock().lock();
+ try {
+ return iterator.next();
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+ public boolean hasPrevious() {
+ lock.readLock().lock();
+ try {
+ return iterator.hasPrevious();
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+ public E previous() {
+ lock.readLock().lock();
+ try {
+ return iterator.previous();
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+ public int nextIndex() {
+ lock.readLock().lock();
+ try {
+ return iterator.nextIndex();
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+ public int previousIndex() {
+ lock.readLock().lock();
+ try {
+ return iterator.previousIndex();
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+ public void remove() {
+ lock.writeLock().lock();
+ try {
+ iterator.remove();
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+ public void set(E e) {
+ lock.writeLock().lock();
+ try {
+ iterator.set(e);
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+ public void add(E e) {
+ lock.writeLock().lock();
+ try {
+ iterator.add(e);
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+ };
}
}
diff --git a/src/main/java/com/cedarsoftware/util/SealableList.java b/src/main/java/com/cedarsoftware/util/SealableList.java
new file mode 100644
index 00000000..473a9c20
--- /dev/null
+++ b/src/main/java/com/cedarsoftware/util/SealableList.java
@@ -0,0 +1,130 @@
+package com.cedarsoftware.util;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.function.Supplier;
+
+/**
+ * SealableList provides a List or List wrapper that can be 'sealed' and 'unsealed.' When
+ * sealed, the List is mutable, when unsealed it is immutable (read-only). The iterator(),
+ * listIterator(), and subList() return views that honor the Supplier's sealed state.
+ * The sealed state can be changed as often as needed.
+ *
+ * NOTE: Please do not reformat this code as the current format makes it easy to see the overall structure.
+ *
+ * @author John DeRegnaucourt (jdereg@gmail.com)
+ *
+ * Copyright (c) Cedar Software LLC
+ *
+ * Licensed 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
+ *
+ * License
+ *
+ * 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.
+ */
+public class SealableList implements List {
+ private final List list;
+ private final Supplier sealedSupplier;
+
+ /**
+ * Create a SealableList. Since no List is being supplied, this will use an ConcurrentList internally. If you
+ * want to use an ArrayList for example, use SealableList constructor that takes a List and pass it the instance
+ * you want it to wrap.
+ * @param sealedSupplier {@code Supplier} that returns 'true' to indicate sealed, 'false' for mutable.
+ */
+ public SealableList(Supplier sealedSupplier) {
+ this.list = new ConcurrentList<>();
+ this.sealedSupplier = sealedSupplier;
+ }
+
+ /**
+ * Create a SealableList. Since a List is not supplied, the elements from the passed in Collection will be
+ * copied to an internal ConcurrentList. If you want to use an ArrayList for example, use SealableList
+ * constructor that takes a List and pass it the instance you want it to wrap.
+ * @param col Collection to supply initial elements. These are copied to an internal ConcurrentList.
+ * @param sealedSupplier {@code Supplier} that returns 'true' to indicate sealed, 'false' for mutable.
+ */
+ public SealableList(Collection col, Supplier sealedSupplier) {
+ this.list = new ConcurrentList<>();
+ this.list.addAll(col);
+ this.sealedSupplier = sealedSupplier;
+ }
+
+ /**
+ * Use this constructor to wrap a List (any kind of List) and make it a SealableList.
+ * No duplicate of the List is created and the original list is operated on directly if unsealed, or protected
+ * from changes if sealed.
+ * @param list List instance to protect.
+ * @param sealedSupplier {@code Supplier} that returns 'true' to indicate sealed, 'false' for mutable.
+ */
+ public SealableList(List list, Supplier sealedSupplier) {
+ this.list = list;
+ this.sealedSupplier = sealedSupplier;
+ }
+
+ private void throwIfSealed() {
+ if (sealedSupplier.get()) {
+ throw new UnsupportedOperationException("This list has been sealed and is now immutable");
+ }
+ }
+
+ // Immutable APIs
+ public boolean equals(Object other) { return list.equals(other); }
+ public int hashCode() { return list.hashCode(); }
+ public int size() { return list.size(); }
+ public boolean isEmpty() { return list.isEmpty(); }
+ public boolean contains(Object o) { return list.contains(o); }
+ public boolean containsAll(Collection> col) { return new HashSet<>(list).containsAll(col); }
+ public int indexOf(Object o) { return list.indexOf(o); }
+ public int lastIndexOf(Object o) { return list.lastIndexOf(o); }
+ public T get(int index) { return list.get(index); }
+ public Object[] toArray() { return list.toArray(); }
+ public T1[] toArray(T1[] a) { return list.toArray(a);}
+ public Iterator iterator() { return createSealHonoringIterator(list.iterator()); }
+ public ListIterator listIterator() { return createSealHonoringListIterator(list.listIterator()); }
+ public ListIterator listIterator(final int index) { return createSealHonoringListIterator(list.listIterator(index)); }
+ public List subList(int fromIndex, int toIndex) { return new SealableList<>(list.subList(fromIndex, toIndex), sealedSupplier); }
+
+ // Mutable APIs
+ public boolean add(T t) { throwIfSealed(); return list.add(t); }
+ public boolean remove(Object o) { throwIfSealed(); return list.remove(o); }
+ public boolean addAll(Collection extends T> col) { throwIfSealed(); return list.addAll(col); }
+ public boolean addAll(int index, Collection extends T> col) { throwIfSealed(); return list.addAll(index, col); }
+ public boolean removeAll(Collection> col) { throwIfSealed(); return list.removeAll(col); }
+ public boolean retainAll(Collection> col) { throwIfSealed(); return list.retainAll(col); }
+ public void clear() { throwIfSealed(); list.clear(); }
+ public T set(int index, T element) { throwIfSealed(); return list.set(index, element); }
+ public void add(int index, T element) { throwIfSealed(); list.add(index, element); }
+ public T remove(int index) { throwIfSealed(); return list.remove(index); }
+
+ private Iterator createSealHonoringIterator(Iterator iterator) {
+ return new Iterator() {
+ public boolean hasNext() { return iterator.hasNext(); }
+ public T next() { return iterator.next(); }
+ public void remove() { throwIfSealed(); iterator.remove(); }
+ };
+ }
+
+ private ListIterator createSealHonoringListIterator(ListIterator iterator) {
+ return new ListIterator() {
+ public boolean hasNext() { return iterator.hasNext();}
+ public T next() { return iterator.next(); }
+ public boolean hasPrevious() { return iterator.hasPrevious(); }
+ public T previous() { return iterator.previous(); }
+ public int nextIndex() { return iterator.nextIndex(); }
+ public int previousIndex() { return iterator.previousIndex(); }
+ public void remove() { throwIfSealed(); iterator.remove(); }
+ public void set(T e) { throwIfSealed(); iterator.set(e); }
+ public void add(T e) { throwIfSealed(); iterator.add(e);}
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/cedarsoftware/util/SealableMap.java b/src/main/java/com/cedarsoftware/util/SealableMap.java
new file mode 100644
index 00000000..c874babc
--- /dev/null
+++ b/src/main/java/com/cedarsoftware/util/SealableMap.java
@@ -0,0 +1,83 @@
+package com.cedarsoftware.util;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
+
+/**
+ * SealableMap provides a Map or Map wrapper that can be 'sealed' and 'unsealed.' When sealed, the
+ * Map is mutable, when unsealed it is immutable (read-only). The view methods iterator(), keySet(),
+ * values(), and entrySet() return a view that honors the Supplier's sealed state. The sealed state
+ * can be changed as often as needed.
+ *
+ * NOTE: Please do not reformat this code as the current format makes it easy to see the overall structure.
+ *
+ * @author John DeRegnaucourt (jdereg@gmail.com)
+ *
+ * Copyright (c) Cedar Software LLC
+ *
+ * Licensed 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
+ *
+ * License
+ *
+ * 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.
+ */
+public class SealableMap implements Map {
+ private final Map map;
+ private final Supplier sealedSupplier;
+
+ /**
+ * Create a SealableMap. Since a Map is not supplied, this will use a ConcurrentHashMap internally. If you
+ * want a HashMap to be used internally, use the SealableMap constructor that takes a Map and pass it the
+ * instance you want it to wrap.
+ * @param sealedSupplier {@code Supplier} that returns 'true' to indicate sealed, 'false' for mutable.
+ */
+ public SealableMap(Supplier sealedSupplier) {
+ this.sealedSupplier = sealedSupplier;
+ this.map = new ConcurrentHashMap<>();
+ }
+
+ /**
+ * Use this constructor to wrap a Map (any kind of Map) and make it a SealableMap. No duplicate of the Map is
+ * created and the original map is operated on directly if unsealed, or protected from changes if sealed.
+ * @param map Map instance to protect.
+ * @param sealedSupplier {@code Supplier} that returns 'true' to indicate sealed, 'false' for mutable.
+ */
+ public SealableMap(Map map, Supplier sealedSupplier) {
+ this.sealedSupplier = sealedSupplier;
+ this.map = map;
+ }
+
+ private void throwIfSealed() {
+ if (sealedSupplier.get()) {
+ throw new UnsupportedOperationException("This map has been sealed and is now immutable");
+ }
+ }
+
+ // Immutable
+ public boolean equals(Object obj) { return map.equals(obj); }
+ public int hashCode() { return map.hashCode(); }
+ public int size() { return map.size(); }
+ public boolean isEmpty() { return map.isEmpty(); }
+ public boolean containsKey(Object key) { return map.containsKey(key); }
+ public boolean containsValue(Object value) { return map.containsValue(value); }
+ public V get(Object key) { return map.get(key); }
+ public Set keySet() { return new SealableSet<>(map.keySet(), sealedSupplier); }
+ public Collection values() { return new SealableList<>(new ArrayList<>(map.values()), sealedSupplier); }
+ public Set> entrySet() { return new SealableSet<>(map.entrySet(), sealedSupplier); }
+
+ // Mutable
+ public V put(K key, V value) { throwIfSealed(); return map.put(key, value); }
+ public V remove(Object key) { throwIfSealed(); return map.remove(key); }
+ public void putAll(Map extends K, ? extends V> m) { throwIfSealed(); map.putAll(m); }
+ public void clear() { throwIfSealed(); map.clear(); }
+}
\ No newline at end of file
diff --git a/src/main/java/com/cedarsoftware/util/SealableNavigableMap.java b/src/main/java/com/cedarsoftware/util/SealableNavigableMap.java
new file mode 100644
index 00000000..71641925
--- /dev/null
+++ b/src/main/java/com/cedarsoftware/util/SealableNavigableMap.java
@@ -0,0 +1,130 @@
+package com.cedarsoftware.util;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.NavigableSet;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.concurrent.ConcurrentSkipListMap;
+import java.util.function.Supplier;
+
+/**
+ * SealableNavigableMap provides a NavigableMap or NavigableMap wrapper that can be 'sealed' and 'unsealed.'
+ * When sealed, the Map is mutable, when unsealed it is immutable (read-only). The view methods keySet(), entrySet(),
+ * values(), navigableKeySet(), descendingMap(), descendingKeySet(), subMap(), headMap(), and tailMap() return a view
+ * that honors the Supplier's sealed state. The sealed state can be changed as often as needed.
+ *
+ * NOTE: Please do not reformat this code as the current format makes it easy to see the overall structure.
+ *
+ * @author John DeRegnaucourt
+ *
+ * Copyright (c) Cedar Software LLC
+ *
+ * Licensed 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
+ *
+ * License
+ *
+ * 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.
+ */
+public class SealableNavigableMap implements NavigableMap {
+ private final NavigableMap map;
+ private final Supplier sealedSupplier;
+
+ /**
+ * Create a SealableNavigableMap. Since a Map is not supplied, this will use a ConcurrentSkipListMap internally.
+ * If you want a TreeMap to be used internally, use the SealableMap constructor that takes a NavigableMap and pass
+ * it the instance you want it to wrap.
+ * @param sealedSupplier {@code Supplier} that returns 'true' to indicate sealed, 'false' for mutable.
+ */
+ public SealableNavigableMap(Supplier sealedSupplier) {
+ this.sealedSupplier = sealedSupplier;
+ this.map = new ConcurrentSkipListMap<>();
+ }
+
+ /**
+ * Create a NavigableSealableMap. Since NavigableMap is not supplied, the elements from the passed in SortedMap
+ * will be copied to an internal ConcurrentSkipListMap. If you want to use a TreeMap for example, use the
+ * SealableNavigableMap constructor that takes a NavigableMap and pass it the instance you want it to wrap.
+ * @param map SortedMap to supply initial elements. These are copied to an internal ConcurrentSkipListMap.
+ * @param sealedSupplier {@code Supplier} that returns 'true' to indicate sealed, 'false' for mutable.
+ */
+ public SealableNavigableMap(SortedMap map, Supplier sealedSupplier) {
+ this.sealedSupplier = sealedSupplier;
+ this.map = new ConcurrentSkipListMap<>(map);
+ }
+
+ /**
+ * Use this constructor to wrap a NavigableMap (any kind of NavigableMap) and make it a SealableNavigableMap.
+ * No duplicate of the Map is created and the original map is operated on directly if unsealed, or protected
+ * from changes if sealed.
+ * @param map NavigableMap instance to protect.
+ * @param sealedSupplier {@code Supplier} that returns 'true' to indicate sealed, 'false' for mutable.
+ */
+ public SealableNavigableMap(NavigableMap map, Supplier sealedSupplier) {
+ this.sealedSupplier = sealedSupplier;
+ this.map = map;
+ }
+
+ private void throwIfSealed() {
+ if (sealedSupplier.get()) {
+ throw new UnsupportedOperationException("This map has been sealed and is now immutable");
+ }
+ }
+
+ // Immutable APIs
+ public boolean equals(Object o) { return map.equals(o); }
+ public int hashCode() { return map.hashCode(); }
+ public boolean isEmpty() { return map.isEmpty(); }
+ public boolean containsKey(Object key) { return map.containsKey(key); }
+ public boolean containsValue(Object value) { return map.containsValue(value); }
+ public int size() { return map.size(); }
+ public V get(Object key) { return map.get(key); }
+ public Comparator super K> comparator() { return map.comparator(); }
+ public K firstKey() { return map.firstKey(); }
+ public K lastKey() { return map.lastKey(); }
+ public Set keySet() { return new SealableSet<>(map.keySet(), sealedSupplier); }
+ public Collection values() { return new SealableList<>(new ArrayList<>(map.values()), sealedSupplier); }
+ public Set> entrySet() { return new SealableSet<>(map.entrySet(), sealedSupplier); }
+ public Map.Entry lowerEntry(K key) { return map.lowerEntry(key); }
+ public K lowerKey(K key) { return map.lowerKey(key); }
+ public Map.Entry floorEntry(K key) { return map.floorEntry(key); }
+ public K floorKey(K key) { return map.floorKey(key); }
+ public Map.Entry ceilingEntry(K key) { return map.ceilingEntry(key); }
+ public K ceilingKey(K key) { return map.ceilingKey(key); }
+ public Map.Entry higherEntry(K key) { return map.higherEntry(key); }
+ public K higherKey(K key) { return map.higherKey(key); }
+ public Map.Entry firstEntry() { return map.firstEntry(); }
+ public Map.Entry lastEntry() { return map.lastEntry(); }
+ public NavigableMap descendingMap() { return new SealableNavigableMap<>(map.descendingMap(), sealedSupplier); }
+ public NavigableSet navigableKeySet() { return new SealableNavigableSet<>(map.navigableKeySet(), sealedSupplier); }
+ public NavigableSet descendingKeySet() { return new SealableNavigableSet<>(map.descendingKeySet(), sealedSupplier); }
+ public SortedMap subMap(K fromKey, K toKey) { return subMap(fromKey, true, toKey, false); }
+ public NavigableMap subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) {
+ return new SealableNavigableMap<>(map.subMap(fromKey, fromInclusive, toKey, toInclusive), sealedSupplier);
+ }
+ public SortedMap headMap(K toKey) { return headMap(toKey, false); }
+ public NavigableMap headMap(K toKey, boolean inclusive) {
+ return new SealableNavigableMap<>(map.headMap(toKey, inclusive), sealedSupplier);
+ }
+ public SortedMap tailMap(K fromKey) { return tailMap(fromKey, true); }
+ public NavigableMap tailMap(K fromKey, boolean inclusive) {
+ return new SealableNavigableMap<>(map.tailMap(fromKey, inclusive), sealedSupplier);
+ }
+
+ // Mutable APIs
+ public Map.Entry pollFirstEntry() { throwIfSealed(); return map.pollFirstEntry(); }
+ public Map.Entry pollLastEntry() { throwIfSealed(); return map.pollLastEntry(); }
+ public V put(K key, V value) { throwIfSealed(); return map.put(key, value); }
+ public V remove(Object key) { throwIfSealed(); return map.remove(key); }
+ public void putAll(Map extends K, ? extends V> m) { throwIfSealed(); map.putAll(m); }
+ public void clear() { throwIfSealed(); map.clear(); }
+}
\ No newline at end of file
diff --git a/src/main/java/com/cedarsoftware/util/SealableNavigableSet.java b/src/main/java/com/cedarsoftware/util/SealableNavigableSet.java
new file mode 100644
index 00000000..16219d26
--- /dev/null
+++ b/src/main/java/com/cedarsoftware/util/SealableNavigableSet.java
@@ -0,0 +1,175 @@
+package com.cedarsoftware.util;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NavigableSet;
+import java.util.SortedSet;
+import java.util.concurrent.ConcurrentSkipListSet;
+import java.util.function.Supplier;
+
+/**
+ * SealableNavigableSet provides a NavigableSet or NavigableSet wrapper that can be 'sealed' and
+ * 'unsealed.' When sealed, the NavigableSet is mutable, when unsealed it is immutable (read-only).
+ * The view methods iterator(), descendingIterator(), descendingSet(), subSet(), headSet(), and
+ * tailSet(), return a view that honors the Supplier's sealed state. The sealed state can be
+ * changed as often as needed.
+ *
+ * NOTE: Please do not reformat this code as the current format makes it easy to see the overall structure.
+ *
+ * @author John DeRegnaucourt
+ *
+ * Copyright (c) Cedar Software LLC
+ *
+ * Licensed 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
+ *
+ * License
+ *
+ * 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.
+ */
+public class SealableNavigableSet implements NavigableSet {
+ private final NavigableSet navigableSet;
+ private final Supplier sealedSupplier;
+
+ /**
+ * Create a NavigableSealableSet. Since a NavigableSet is not supplied, this will use a ConcurrentSkipListSet
+ * internally. If you want to use a TreeSet for example, use the SealableNavigableSet constructor that takes
+ * a NavigableSet and pass it the instance you want it to wrap.
+ * @param sealedSupplier {@code Supplier} that returns 'true' to indicate sealed, 'false' for mutable.
+ */
+ public SealableNavigableSet(Supplier sealedSupplier) {
+ this.sealedSupplier = sealedSupplier;
+ navigableSet = new ConcurrentSkipListSet<>();
+ }
+
+ /**
+ * Create a NavigableSealableSet. Since a NavigableSet is not supplied, this will use a ConcurrentSkipListSet
+ * internally. If you want to use a TreeSet for example, use the SealableNavigableSet constructor that takes
+ * a NavigableSet and pass it the instance you want it to wrap.
+ * @param comparator {@code Comparator} A comparison function, which imposes a total ordering on some
+ * collection of objects.
+ * @param sealedSupplier {@code Supplier} that returns 'true' to indicate sealed, 'false' for mutable.
+ */
+ public SealableNavigableSet(Comparator super T> comparator, Supplier sealedSupplier) {
+ this.sealedSupplier = sealedSupplier;
+ navigableSet = new ConcurrentSkipListSet<>(comparator);
+ }
+
+ /**
+ * Create a NavigableSealableSet. Since NavigableSet is not supplied, the elements from the passed in Collection
+ * will be copied to an internal ConcurrentSkipListSet. If you want to use a TreeSet for example, use the
+ * SealableNavigableSet constructor that takes a NavigableSet and pass it the instance you want it to wrap.
+ * @param col Collection to supply initial elements. These are copied to an internal ConcurrentSkipListSet.
+ * @param sealedSupplier {@code Supplier} that returns 'true' to indicate sealed, 'false' for mutable.
+ */
+ public SealableNavigableSet(Collection extends T> col, Supplier sealedSupplier) {
+ this(sealedSupplier);
+ addAll(col);
+ }
+
+ /**
+ * Create a NavigableSealableSet. Since NavigableSet is not supplied, the elements from the passed in SortedSet
+ * will be copied to an internal ConcurrentSkipListSet. If you want to use a TreeSet for example, use the
+ * SealableNavigableSet constructor that takes a NavigableSet and pass it the instance you want it to wrap.
+ * @param set SortedSet to supply initial elements. These are copied to an internal ConcurrentSkipListSet.
+ * @param sealedSupplier {@code Supplier} that returns 'true' to indicate sealed, 'false' for mutable.
+ */
+ public SealableNavigableSet(SortedSet set, Supplier sealedSupplier) {
+ this.sealedSupplier = sealedSupplier;
+ navigableSet = new ConcurrentSkipListSet<>(set);
+ }
+
+ /**
+ * Use this constructor to wrap a NavigableSet (any kind of NavigableSet) and make it a SealableNavigableSet.
+ * No duplicate of the Set is created, the original set is operated on directly if unsealed, or protected
+ * from changes if sealed.
+ * @param set NavigableSet instance to protect.
+ * @param sealedSupplier {@code Supplier} that returns 'true' to indicate sealed, 'false' for mutable.
+ */
+ public SealableNavigableSet(NavigableSet set, Supplier sealedSupplier) {
+ this.sealedSupplier = sealedSupplier;
+ navigableSet = set;
+ }
+
+ private void throwIfSealed() {
+ if (sealedSupplier.get()) {
+ throw new UnsupportedOperationException("This set has been sealed and is now immutable");
+ }
+ }
+
+ // Immutable APIs
+ public boolean equals(Object o) { return o == this || navigableSet.equals(o); }
+ public int hashCode() { return navigableSet.hashCode(); }
+ public int size() { return navigableSet.size(); }
+ public boolean isEmpty() { return navigableSet.isEmpty(); }
+ public boolean contains(Object o) { return navigableSet.contains(o); }
+ public boolean containsAll(Collection> col) { return navigableSet.containsAll(col);}
+ public Comparator super T> comparator() { return navigableSet.comparator(); }
+ public T first() { return navigableSet.first(); }
+ public T last() { return navigableSet.last(); }
+ public Object[] toArray() { return navigableSet.toArray(); }
+ public T[] toArray(T[] a) { return navigableSet.toArray(a); }
+ public T lower(T e) { return navigableSet.lower(e); }
+ public T floor(T e) { return navigableSet.floor(e); }
+ public T ceiling(T e) { return navigableSet.ceiling(e); }
+ public T higher(T e) { return navigableSet.higher(e); }
+ public Iterator iterator() {
+ return createSealHonoringIterator(navigableSet.iterator());
+ }
+ public Iterator descendingIterator() {
+ return createSealHonoringIterator(navigableSet.descendingIterator());
+ }
+ public NavigableSet descendingSet() {
+ return new SealableNavigableSet<>(navigableSet.descendingSet(), sealedSupplier);
+ }
+ public SortedSet subSet(T fromElement, T toElement) {
+ return subSet(fromElement, true, toElement, false);
+ }
+ public NavigableSet subSet(T fromElement, boolean fromInclusive, T toElement, boolean toInclusive) {
+ return new SealableNavigableSet<>(navigableSet.subSet(fromElement, fromInclusive, toElement, toInclusive), sealedSupplier);
+ }
+ public SortedSet headSet(T toElement) {
+ return headSet(toElement, false);
+ }
+ public NavigableSet headSet(T toElement, boolean inclusive) {
+ return new SealableNavigableSet<>(navigableSet.headSet(toElement, inclusive), sealedSupplier);
+ }
+ public SortedSet tailSet(T fromElement) {
+ return tailSet(fromElement, false);
+ }
+ public NavigableSet tailSet(T fromElement, boolean inclusive) {
+ return new SealableNavigableSet<>(navigableSet.tailSet(fromElement, inclusive), sealedSupplier);
+ }
+
+ // Mutable APIs
+ public boolean add(T e) { throwIfSealed(); return navigableSet.add(e); }
+ public boolean addAll(Collection extends T> col) { throwIfSealed(); return navigableSet.addAll(col); }
+ public void clear() { throwIfSealed(); navigableSet.clear(); }
+ public boolean remove(Object o) { throwIfSealed(); return navigableSet.remove(o); }
+ public boolean removeAll(Collection> col) { throwIfSealed(); return navigableSet.removeAll(col); }
+ public boolean retainAll(Collection> col) { throwIfSealed(); return navigableSet.retainAll(col); }
+ public T pollFirst() { throwIfSealed(); return navigableSet.pollFirst(); }
+ public T pollLast() { throwIfSealed(); return navigableSet.pollLast(); }
+
+ private Iterator createSealHonoringIterator(Iterator iterator) {
+ return new Iterator() {
+ public boolean hasNext() { return iterator.hasNext(); }
+ public T next() {
+ T item = iterator.next();
+ if (item instanceof Map.Entry) {
+ Map.Entry, ?> entry = (Map.Entry, ?>) item;
+ return (T) new SealableSet.SealAwareEntry<>(entry, sealedSupplier);
+ }
+ return item;
+ }
+ public void remove() { throwIfSealed(); iterator.remove();}
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/cedarsoftware/util/SealableSet.java b/src/main/java/com/cedarsoftware/util/SealableSet.java
new file mode 100644
index 00000000..bff1f550
--- /dev/null
+++ b/src/main/java/com/cedarsoftware/util/SealableSet.java
@@ -0,0 +1,134 @@
+package com.cedarsoftware.util;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
+
+/**
+ * SealableSet provides a Set or Set wrapper that can be 'sealed' and 'unsealed.' When sealed, the
+ * Set is mutable, when unsealed it is immutable (read-only). The iterator() returns a view that
+ * honors the Supplier's sealed state. The sealed state can be changed as often as needed.
+ *
+ * NOTE: Please do not reformat this code as the current format makes it easy to see the overall structure.
+ *
+ * @author John DeRegnaucourt
+ *
+ * Copyright Cedar Software LLC
+ *
+ * Licensed 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
+ *
+ * License
+ *
+ * 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.
+ */
+public class SealableSet implements Set {
+ private final Set set;
+ private final Supplier sealedSupplier;
+
+ /**
+ * Create a SealableSet. Since a Set is not supplied, this will use a ConcurrentHashMap.newKeySet internally.
+ * If you want a HashSet to be used internally, use SealableSet constructor that takes a Set and pass it the
+ * instance you want it to wrap.
+ * @param sealedSupplier {@code Supplier} that returns 'true' to indicate sealed, 'false' for mutable.
+ */
+ public SealableSet(Supplier sealedSupplier) {
+ this.sealedSupplier = sealedSupplier;
+ this.set = ConcurrentHashMap.newKeySet();
+ }
+
+ /**
+ * Create a SealableSet. Since a Set is not supplied, the elements from the passed in Collection will be
+ * copied to an internal ConcurrentHashMap.newKeySet. If you want to use a HashSet for example, use SealableSet
+ * constructor that takes a Set and pass it the instance you want it to wrap.
+ * @param col Collection to supply initial elements. These are copied to an internal ConcurrentHashMap.newKeySet.
+ * @param sealedSupplier {@code Supplier} that returns 'true' to indicate sealed, 'false' for mutable.
+ */
+ public SealableSet(Collection col, Supplier sealedSupplier) {
+ this.sealedSupplier = sealedSupplier;
+ this.set = ConcurrentHashMap.newKeySet(col.size());
+ this.set.addAll(col);
+ }
+
+ /**
+ * Use this constructor to wrap a Set (any kind of Set) and make it a SealableSet. No duplicate of the Set is
+ * created and the original set is operated on directly if unsealed, or protected from changes if sealed.
+ * @param set Set instance to protect.
+ * @param sealedSupplier {@code Supplier} that returns 'true' to indicate sealed, 'false' for mutable.
+ */
+ public SealableSet(Set set, Supplier sealedSupplier) {
+ this.sealedSupplier = sealedSupplier;
+ this.set = set;
+ }
+
+ private void throwIfSealed() {
+ if (sealedSupplier.get()) {
+ throw new UnsupportedOperationException("This set has been sealed and is now immutable");
+ }
+ }
+
+ // Immutable APIs
+ public int size() { return set.size(); }
+ public boolean isEmpty() { return set.isEmpty(); }
+ public boolean contains(Object o) { return set.contains(o); }
+ public Object[] toArray() { return set.toArray(); }
+ public T1[] toArray(T1[] a) { return set.toArray(a); }
+ public boolean containsAll(Collection> col) { return set.containsAll(col); }
+ public boolean equals(Object o) { return set.equals(o); }
+ public int hashCode() { return set.hashCode(); }
+
+ // Mutable APIs
+ public boolean add(T t) { throwIfSealed(); return set.add(t); }
+ public boolean remove(Object o) { throwIfSealed(); return set.remove(o); }
+ public boolean addAll(Collection extends T> col) { throwIfSealed(); return set.addAll(col); }
+ public boolean removeAll(Collection> col) { throwIfSealed(); return set.removeAll(col); }
+ public boolean retainAll(Collection> col) { throwIfSealed(); return set.retainAll(col); }
+ public void clear() { throwIfSealed(); set.clear(); }
+ public Iterator iterator() {
+ Iterator iterator = set.iterator();
+ return new Iterator() {
+ public boolean hasNext() { return iterator.hasNext(); }
+ public T next() {
+ T item = iterator.next();
+ if (item instanceof Map.Entry) {
+ Map.Entry, ?> entry = (Map.Entry, ?>) item;
+ return (T) new SealAwareEntry<>(entry, sealedSupplier);
+ }
+ return item;
+ }
+ public void remove() { throwIfSealed(); iterator.remove(); }
+ };
+ }
+
+ // Must enforce immutability after the Map.Entry was "handed out" because
+ // it could have been handed out when the map was unsealed or sealed.
+ static class SealAwareEntry implements Map.Entry {
+ private final Map.Entry entry;
+ private final Supplier sealedSupplier;
+
+ SealAwareEntry(Map.Entry entry, Supplier sealedSupplier) {
+ this.entry = entry;
+ this.sealedSupplier = sealedSupplier;
+ }
+
+ public K getKey() { return entry.getKey(); }
+ public V getValue() { return entry.getValue(); }
+ public V setValue(V value) {
+ if (sealedSupplier.get()) {
+ throw new UnsupportedOperationException("Cannot modify, set is sealed");
+ }
+ return entry.setValue(value);
+ }
+
+ public boolean equals(Object o) { return entry.equals(o); }
+ public int hashCode() { return entry.hashCode(); }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/cedarsoftware/util/ConcurrentListTest.java b/src/test/java/com/cedarsoftware/util/ConcurrentListTest.java
index 1cf607b0..1010569a 100644
--- a/src/test/java/com/cedarsoftware/util/ConcurrentListTest.java
+++ b/src/test/java/com/cedarsoftware/util/ConcurrentListTest.java
@@ -13,7 +13,6 @@
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ConcurrentListTest {
@@ -245,20 +244,4 @@ void testAddAllAtIndex() {
list.addAll(7, Arrays.asList(6, 7));
assert deepEquals(Arrays.asList(-1, 0, 2, 3, 4, 1, 5, 6, 7), list);
}
-
- @Test
- void testListIeratorBlows() {
- List list = new ConcurrentList<>();
- list.addAll(Arrays.asList(1, 5));
-
- assertThrows(UnsupportedOperationException.class, () -> list.listIterator());
- }
-
- @Test
- void testListIerator2Blows() {
- List list = new ConcurrentList<>();
- list.addAll(Arrays.asList(1, 5));
-
- assertThrows(UnsupportedOperationException.class, () -> list.listIterator(1));
- }
}
diff --git a/src/test/java/com/cedarsoftware/util/SealableListTest.java b/src/test/java/com/cedarsoftware/util/SealableListTest.java
new file mode 100644
index 00000000..641c7b23
--- /dev/null
+++ b/src/test/java/com/cedarsoftware/util/SealableListTest.java
@@ -0,0 +1,203 @@
+package com.cedarsoftware.util;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.NoSuchElementException;
+import java.util.function.Supplier;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @author John DeRegnaucourt (jdereg@gmail.com)
+ *
+ * Copyright (c) Cedar Software LLC
+ *
+ * Licensed 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
+ *
+ * License
+ *
+ * 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.
+ */
+class SealableListTest {
+
+ private SealableList list;
+ private volatile boolean sealedState = false;
+ private Supplier sealedSupplier = () -> sealedState;
+
+ @BeforeEach
+ void setUp() {
+ sealedState = false;
+ list = new SealableList<>(sealedSupplier);
+ list.add(10);
+ list.add(20);
+ list.add(30);
+ }
+
+ @Test
+ void testAdd() {
+ assertFalse(list.isEmpty());
+ assertEquals(3, list.size());
+ list.add(40);
+ assertTrue(list.contains(40));
+ assertEquals(4, list.size());
+ }
+
+ @Test
+ void testRemove() {
+ assertTrue(list.remove(Integer.valueOf(20)));
+ assertFalse(list.contains(20));
+ assertEquals(2, list.size());
+ }
+
+ @Test
+ void testAddWhenSealed() {
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> list.add(50));
+ }
+
+ @Test
+ void testRemoveWhenSealed() {
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> list.remove(Integer.valueOf(10)));
+ }
+
+ @Test
+ void testIteratorWhenSealed() {
+ Iterator it = list.iterator();
+ sealedState = true;
+ assertTrue(it.hasNext());
+ assertEquals(10, it.next());
+ assertThrows(UnsupportedOperationException.class, it::remove);
+ }
+
+ @Test
+ void testListIteratorSetWhenSealed() {
+ ListIterator it = list.listIterator();
+ sealedState = true;
+ it.next();
+ assertThrows(UnsupportedOperationException.class, () -> it.set(100));
+ }
+
+ @Test
+ void testSubList() {
+ List sublist = list.subList(0, 2);
+ assertEquals(2, sublist.size());
+ assertTrue(sublist.contains(10));
+ assertTrue(sublist.contains(20));
+ assertFalse(sublist.contains(30));
+ sublist.add(25);
+ assertTrue(sublist.contains(25));
+ assertEquals(3, sublist.size());
+ }
+
+ @Test
+ void testSubListWhenSealed() {
+ List sublist = list.subList(0, 2);
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> sublist.add(35));
+ assertThrows(UnsupportedOperationException.class, () -> sublist.remove(Integer.valueOf(10)));
+ }
+
+ @Test
+ void testClearWhenSealed() {
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, list::clear);
+ }
+
+ @Test
+ void testSetWhenSealed() {
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> list.set(1, 100));
+ }
+
+ @Test
+ void testListIteratorAddWhenSealed() {
+ ListIterator it = list.listIterator();
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> it.add(45));
+ }
+
+ @Test
+ void testAddAllWhenSealed() {
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> list.addAll(Arrays.asList(50, 60)));
+ }
+
+ @Test
+ void testIteratorTraversal() {
+ Iterator it = list.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(Integer.valueOf(10), it.next());
+ assertEquals(Integer.valueOf(20), it.next());
+ assertEquals(Integer.valueOf(30), it.next());
+ assertFalse(it.hasNext());
+ assertThrows(NoSuchElementException.class, it::next);
+ }
+
+ @Test
+ void testListIteratorPrevious() {
+ ListIterator it = list.listIterator(2);
+ assertEquals(Integer.valueOf(20), it.previous());
+ assertTrue(it.hasPrevious());
+
+ Iterator it2 = list.listIterator(0);
+ assertEquals(Integer.valueOf(10), it2.next());
+ assertEquals(Integer.valueOf(20), it2.next());
+ assertEquals(Integer.valueOf(30), it2.next());
+ assertThrows(NoSuchElementException.class, () -> it2.next());
+ }
+
+ @Test
+ void testEquals() {
+ SealableList other = new SealableList<>(sealedSupplier);
+ other.add(10);
+ other.add(20);
+ other.add(30);
+ assertEquals(list, other);
+ other.add(40);
+ assertNotEquals(list, other);
+ }
+
+ @Test
+ void testHashCode() {
+ SealableList other = new SealableList<>(sealedSupplier);
+ other.add(10);
+ other.add(20);
+ other.add(30);
+ assertEquals(list.hashCode(), other.hashCode());
+ other.add(40);
+ assertNotEquals(list.hashCode(), other.hashCode());
+ }
+
+ @Test
+ void testNestingHonorsOuterSeal()
+ {
+ List l2 = list.subList(0, list.size());
+ List l3 = l2.subList(0, l2.size());
+ List l4 = l3.subList(0, l3.size());
+ List l5 = l4.subList(0, l4.size());
+ l5.add(40);
+ assertEquals(list.size(), 4);
+ assertEquals(list.get(3), 40);
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> l5.add(50));
+ sealedState = false;
+ l5.add(50);
+ assertEquals(list.size(), 5);
+ }
+}
diff --git a/src/test/java/com/cedarsoftware/util/SealableMapTest.java b/src/test/java/com/cedarsoftware/util/SealableMapTest.java
new file mode 100644
index 00000000..e15a471b
--- /dev/null
+++ b/src/test/java/com/cedarsoftware/util/SealableMapTest.java
@@ -0,0 +1,161 @@
+package com.cedarsoftware.util;
+
+import java.util.AbstractMap;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static com.cedarsoftware.util.MapUtilities.mapOf;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @author John DeRegnaucourt (jdereg@gmail.com)
+ *
+ * Copyright (c) Cedar Software LLC
+ *
+ * Licensed 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
+ *
+ * License
+ *
+ * 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.
+ */
+class SealableMapTest {
+
+ private SealableMap map;
+ private volatile boolean sealedState = false;
+ private Supplier sealedSupplier = () -> sealedState;
+
+ @BeforeEach
+ void setUp() {
+ map = new SealableMap<>(sealedSupplier);
+ map.put("one", 1);
+ map.put("two", 2);
+ map.put("three", 3);
+ }
+
+ @Test
+ void testPutWhenUnsealed() {
+ assertEquals(1, map.get("one"));
+ map.put("four", 4);
+ assertEquals(4, map.get("four"));
+ }
+
+ @Test
+ void testRemoveWhenUnsealed() {
+ assertEquals(1, map.get("one"));
+ map.remove("one");
+ assertNull(map.get("one"));
+ }
+
+ @Test
+ void testPutWhenSealed() {
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> map.put("five", 5));
+ }
+
+ @Test
+ void testRemoveWhenSealed() {
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> map.remove("one"));
+ }
+
+ @Test
+ void testModifyEntrySetWhenSealed() {
+ Set> entries = map.entrySet();
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> entries.removeIf(e -> e.getKey().equals("one")));
+ assertThrows(UnsupportedOperationException.class, () -> entries.iterator().remove());
+ }
+
+ @Test
+ void testModifyKeySetWhenSealed() {
+ Set keys = map.keySet();
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> keys.remove("two"));
+ assertThrows(UnsupportedOperationException.class, keys::clear);
+ }
+
+ @Test
+ void testModifyValuesWhenSealed() {
+ Collection values = map.values();
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> values.remove(3));
+ assertThrows(UnsupportedOperationException.class, values::clear);
+ }
+
+ @Test
+ void testClearWhenSealed() {
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, map::clear);
+ }
+
+ @Test
+ void testPutAllWhenSealed() {
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> map.putAll(mapOf("ten", 10)));
+ }
+
+ @Test
+ void testSealAndUnseal() {
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> map.put("six", 6));
+ sealedState = false;
+ map.put("six", 6);
+ assertEquals(6, map.get("six"));
+ }
+
+ @Test
+ void testEntrySetFunctionality() {
+ Set> entries = map.entrySet();
+ assertNotNull(entries);
+ assertTrue(entries.stream().anyMatch(e -> e.getKey().equals("one") && e.getValue().equals(1)));
+
+ sealedState = true;
+ Map.Entry entry = new AbstractMap.SimpleImmutableEntry<>("five", 5);
+ assertThrows(UnsupportedOperationException.class, () -> entries.add(entry));
+ }
+
+ @Test
+ void testKeySetFunctionality() {
+ Set keys = map.keySet();
+ assertNotNull(keys);
+ assertTrue(keys.contains("two"));
+
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> keys.add("five"));
+ }
+
+ @Test
+ void testValuesFunctionality() {
+ Collection values = map.values();
+ assertNotNull(values);
+ assertTrue(values.contains(3));
+
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> values.add(5));
+ }
+
+ @Test
+ void testMapEquality() {
+ SealableMap anotherMap = new SealableMap<>(sealedSupplier);
+ anotherMap.put("one", 1);
+ anotherMap.put("two", 2);
+ anotherMap.put("three", 3);
+
+ assertEquals(map, anotherMap);
+ }
+}
diff --git a/src/test/java/com/cedarsoftware/util/SealableNavigableMapSubsetTest.java b/src/test/java/com/cedarsoftware/util/SealableNavigableMapSubsetTest.java
new file mode 100644
index 00000000..bb31477e
--- /dev/null
+++ b/src/test/java/com/cedarsoftware/util/SealableNavigableMapSubsetTest.java
@@ -0,0 +1,101 @@
+package com.cedarsoftware.util;
+
+import java.util.NavigableMap;
+import java.util.TreeMap;
+import java.util.function.Supplier;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * @author John DeRegnaucourt (jdereg@gmail.com)
+ *
+ * Copyright (c) Cedar Software LLC
+ *
+ * Licensed 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
+ *
+ * License
+ *
+ * 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.
+ */
+class SealableNavigableMapSubsetTest {
+ private SealableNavigableMap unmodifiableMap;
+ private volatile boolean sealedState = false;
+ private final Supplier sealedSupplier = () -> sealedState;
+
+ @BeforeEach
+ void setUp() {
+ NavigableMap testMap = new TreeMap<>();
+ for (int i = 10; i <= 100; i += 10) {
+ testMap.put(i, String.valueOf(i));
+ }
+ unmodifiableMap = new SealableNavigableMap<>(testMap, sealedSupplier);
+ }
+
+ @Test
+ void testSubMap() {
+ NavigableMap subMap = unmodifiableMap.subMap(30, true, 70, true);
+ assertEquals(5, subMap.size(), "SubMap size should initially include keys 30, 40, 50, 60, 70");
+
+ assertThrows(IllegalArgumentException.class, () -> subMap.put(25, "25"), "Adding key 25 should fail as it is outside the bounds");
+ assertNull(subMap.put(35, "35"), "Adding key 35 should succeed");
+ assertEquals(6, subMap.size(), "SubMap size should now be 6");
+ assertEquals(11, unmodifiableMap.size(), "Enclosing map should reflect the addition");
+
+ assertNull(subMap.remove(10), "Removing key 10 should fail as it is outside the bounds");
+ assertEquals("40", subMap.remove(40), "Removing key 40 should succeed");
+ assertEquals(5, subMap.size(), "SubMap size should be back to 5 after removal");
+ assertEquals(10, unmodifiableMap.size(), "Enclosing map should reflect the removal");
+
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> subMap.put(60, "60"), "Modification should fail when sealed");
+ }
+
+ @Test
+ void testHeadMap() {
+ NavigableMap headMap = unmodifiableMap.headMap(50, true);
+ assertEquals(5, headMap.size(), "HeadMap should include keys up to and including 50");
+
+ assertThrows(IllegalArgumentException.class, () -> headMap.put(55, "55"), "Adding key 55 should fail as it is outside the bounds");
+ assertNull(headMap.put(5, "5"), "Adding key 5 should succeed");
+ assertEquals(6, headMap.size(), "HeadMap size should now be 6");
+ assertEquals(11, unmodifiableMap.size(), "Enclosing map should reflect the addition");
+
+ assertNull(headMap.remove(60), "Removing key 60 should fail as it is outside the bounds");
+ assertEquals("20", headMap.remove(20), "Removing key 20 should succeed");
+ assertEquals(5, headMap.size(), "HeadMap size should be back to 5 after removal");
+ assertEquals(10, unmodifiableMap.size(), "Enclosing map should reflect the removal");
+
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> headMap.put(10, "10"), "Modification should fail when sealed");
+ }
+
+ @Test
+ void testTailMap() {
+ NavigableMap tailMap = unmodifiableMap.tailMap(50, true);
+ assertEquals(6, tailMap.size(), "TailMap should include keys from 50 to 100");
+
+ assertThrows(IllegalArgumentException.class, () -> tailMap.put(45, "45"), "Adding key 45 should fail as it is outside the bounds");
+ assertNull(tailMap.put(110, "110"), "Adding key 110 should succeed");
+ assertEquals(7, tailMap.size(), "TailMap size should now be 7");
+ assertEquals(11, unmodifiableMap.size(), "Enclosing map should reflect the addition");
+
+ assertNull(tailMap.remove(40), "Removing key 40 should fail as it is outside the bounds");
+ assertEquals("60", tailMap.remove(60), "Removing key 60 should succeed");
+ assertEquals(6, tailMap.size(), "TailMap size should be back to 6 after removal");
+ assertEquals(10, unmodifiableMap.size(), "Enclosing map should reflect the removal");
+
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> tailMap.put(80, "80"), "Modification should fail when sealed");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/cedarsoftware/util/SealableNavigableMapTest.java b/src/test/java/com/cedarsoftware/util/SealableNavigableMapTest.java
new file mode 100644
index 00000000..b1324e93
--- /dev/null
+++ b/src/test/java/com/cedarsoftware/util/SealableNavigableMapTest.java
@@ -0,0 +1,148 @@
+package com.cedarsoftware.util;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+import java.util.function.Supplier;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @author John DeRegnaucourt (jdereg@gmail.com)
+ *
+ * Copyright (c) Cedar Software LLC
+ *
+ * Licensed 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
+ *
+ * License
+ *
+ * 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.
+ */
+class SealableNavigableMapTest {
+
+ private NavigableMap map;
+ private SealableNavigableMap unmodifiableMap;
+ private Supplier sealedSupplier;
+ private boolean sealed;
+
+ @BeforeEach
+ void setUp() {
+ sealed = false;
+ sealedSupplier = () -> sealed;
+
+ map = new TreeMap<>();
+ map.put("one", 1);
+ map.put("two", 2);
+ map.put("three", 3);
+
+ unmodifiableMap = new SealableNavigableMap<>(map, sealedSupplier);
+ }
+
+ @Test
+ void testMutationsWhenUnsealed() {
+ assertFalse(sealedSupplier.get(), "Map should start unsealed.");
+ assertEquals(3, unmodifiableMap.size());
+ unmodifiableMap.put("four", 4);
+ assertEquals(Integer.valueOf(4), unmodifiableMap.get("four"));
+ assertTrue(unmodifiableMap.containsKey("four"));
+ }
+
+ @Test
+ void testSealedMutationsThrowException() {
+ sealed = true;
+ assertThrows(UnsupportedOperationException.class, () -> unmodifiableMap.put("five", 5));
+ assertThrows(UnsupportedOperationException.class, () -> unmodifiableMap.remove("one"));
+ assertThrows(UnsupportedOperationException.class, unmodifiableMap::clear);
+ }
+
+ @Test
+ void testEntrySetValueWhenSealed() {
+ Map.Entry entry = unmodifiableMap.firstEntry();
+ sealed = true;
+ assertThrows(UnsupportedOperationException.class, () -> entry.setValue(10));
+ }
+
+ @Test
+ void testKeySetViewReflectsChanges() {
+ unmodifiableMap.put("five", 5);
+ assertTrue(unmodifiableMap.keySet().contains("five"));
+ sealed = true;
+ assertThrows(UnsupportedOperationException.class, () -> unmodifiableMap.keySet().remove("five"));
+ }
+
+ @Test
+ void testValuesViewReflectsChanges() {
+ unmodifiableMap.put("six", 6);
+ assertTrue(unmodifiableMap.values().contains(6));
+ sealed = true;
+ assertThrows(UnsupportedOperationException.class, () -> unmodifiableMap.values().remove(6));
+ }
+
+ @Test
+ void testSubMapViewReflectsChanges2() {
+ // SubMap from "one" to "three", only includes "one" and "three"
+ NavigableMap subMap = unmodifiableMap.subMap("one", true, "three", true);
+ assertEquals(2, subMap.size()); // Should only include "one" and "three"
+ assertTrue(subMap.containsKey("one") && subMap.containsKey("three"));
+ assertFalse(subMap.containsKey("two")); // "two" should not be included
+
+ // Adding a key that's lexicographically after "three"
+ unmodifiableMap.put("two-and-half", 2);
+ assertFalse(subMap.containsKey("two-and-half")); // Should not be visible in the submap
+ assertEquals(2, subMap.size()); // Size should remain as "two-and-half" is out of range
+ unmodifiableMap.put("pop", 93);
+ assertTrue(subMap.containsKey("pop"));
+
+ subMap.put("poop", 37);
+ assertTrue(unmodifiableMap.containsKey("poop"));
+
+ // Sealing and testing immutability
+ sealed = true;
+ assertThrows(UnsupportedOperationException.class, () -> subMap.put("zero", 0)); // Immutable (and outside range)
+ sealed = false;
+ assertThrows(java.lang.IllegalArgumentException.class, () -> subMap.put("zero", 0)); // outside range
+ }
+
+ @Test
+ void testIteratorsThrowWhenSealed() {
+ Iterator keyIterator = unmodifiableMap.navigableKeySet().iterator();
+ Iterator> entryIterator = unmodifiableMap.entrySet().iterator();
+
+ while (keyIterator.hasNext()) {
+ keyIterator.next();
+ sealed = true;
+ assertThrows(UnsupportedOperationException.class, keyIterator::remove);
+ sealed = false;
+ }
+
+ while (entryIterator.hasNext()) {
+ Map.Entry entry = entryIterator.next();
+ sealed = true;
+ assertThrows(UnsupportedOperationException.class, () -> entry.setValue(999));
+ sealed = false;
+ }
+ }
+
+ @Test
+ void testDescendingMapReflectsChanges() {
+ unmodifiableMap.put("zero", 0);
+ NavigableMap descendingMap = unmodifiableMap.descendingMap();
+ assertTrue(descendingMap.containsKey("zero"));
+
+ sealed = true;
+ assertThrows(UnsupportedOperationException.class, () -> descendingMap.put("minus one", -1));
+ }
+}
diff --git a/src/test/java/com/cedarsoftware/util/SealableNavigableSetTest.java b/src/test/java/com/cedarsoftware/util/SealableNavigableSetTest.java
new file mode 100644
index 00000000..b22c04e8
--- /dev/null
+++ b/src/test/java/com/cedarsoftware/util/SealableNavigableSetTest.java
@@ -0,0 +1,127 @@
+package com.cedarsoftware.util;
+
+import java.util.Iterator;
+import java.util.NavigableSet;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * @author John DeRegnaucourt (jdereg@gmail.com)
+ *
+ * Copyright (c) Cedar Software LLC
+ *
+ * Licensed 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
+ *
+ * License
+ *
+ * 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.
+ */
+class SealableNavigableSetTest {
+
+ private SealableNavigableSet set;
+ private volatile boolean sealedState = false;
+ private Supplier sealedSupplier = () -> sealedState;
+
+ @BeforeEach
+ void setUp() {
+ set = new SealableNavigableSet<>(sealedSupplier);
+ set.add(10);
+ set.add(20);
+ set.add(30);
+ }
+
+ @Test
+ void testIteratorModificationException() {
+ Iterator iterator = set.iterator();
+ sealedState = true;
+ assertDoesNotThrow(iterator::next);
+ assertThrows(UnsupportedOperationException.class, iterator::remove);
+ }
+
+ @Test
+ void testDescendingIteratorModificationException() {
+ Iterator iterator = set.descendingIterator();
+ sealedState = true;
+ assertDoesNotThrow(iterator::next);
+ assertThrows(UnsupportedOperationException.class, iterator::remove);
+ }
+
+ @Test
+ void testTailSetModificationException() {
+ NavigableSet tailSet = set.tailSet(20, true);
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> tailSet.add(40));
+ assertThrows(UnsupportedOperationException.class, tailSet::clear);
+ }
+
+ @Test
+ void testHeadSetModificationException() {
+ NavigableSet headSet = set.headSet(20, false);
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> headSet.add(5));
+ assertThrows(UnsupportedOperationException.class, headSet::clear);
+ }
+
+ @Test
+ void testSubSetModificationException() {
+ NavigableSet subSet = set.subSet(10, true, 30, true);
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> subSet.add(25));
+ assertThrows(UnsupportedOperationException.class, subSet::clear);
+ }
+
+ @Test
+ void testDescendingSetModificationException() {
+ NavigableSet descendingSet = set.descendingSet();
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> descendingSet.add(5));
+ assertThrows(UnsupportedOperationException.class, descendingSet::clear);
+ }
+
+ @Test
+ void testSealAfterModification() {
+ Iterator iterator = set.iterator();
+ NavigableSet tailSet = set.tailSet(20, true);
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, iterator::remove);
+ assertThrows(UnsupportedOperationException.class, () -> tailSet.add(40));
+ }
+
+ @Test
+ void testSubset()
+ {
+ Set subset = set.subSet(5, true, 25, true);
+ assertEquals(subset.size(), 2);
+ subset.add(5);
+ assertEquals(subset.size(), 3);
+ subset.add(25);
+ assertEquals(subset.size(), 4);
+ assertThrows(IllegalArgumentException.class, () -> subset.add(26));
+ assertEquals(set.size(), 5);
+ }
+
+ @Test
+ void testSubset2()
+ {
+ Set subset = set.subSet(5, 25);
+ assertEquals(subset.size(), 2);
+ assertThrows(IllegalArgumentException.class, () -> subset.add(4));
+ subset.add(5);
+ assertEquals(subset.size(), 3);
+ assertThrows(IllegalArgumentException.class, () -> subset.add(25));
+ assertEquals(4, set.size());
+ }
+}
diff --git a/src/test/java/com/cedarsoftware/util/SealableNavigableSubsetTest.java b/src/test/java/com/cedarsoftware/util/SealableNavigableSubsetTest.java
new file mode 100644
index 00000000..98965d47
--- /dev/null
+++ b/src/test/java/com/cedarsoftware/util/SealableNavigableSubsetTest.java
@@ -0,0 +1,103 @@
+package com.cedarsoftware.util;
+
+import java.util.NavigableSet;
+import java.util.TreeSet;
+import java.util.function.Supplier;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @author John DeRegnaucourt (jdereg@gmail.com)
+ *
+ * Copyright (c) Cedar Software LLC
+ *
+ * Licensed 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
+ *
+ * License
+ *
+ * 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.
+ */
+class SealableNavigableSubsetTest {
+ private SealableNavigableSet unmodifiableSet;
+ private volatile boolean sealedState = false;
+ private final Supplier sealedSupplier = () -> sealedState;
+
+ @BeforeEach
+ void setUp() {
+ NavigableSet testSet = new TreeSet<>();
+ for (int i = 10; i <= 100; i += 10) {
+ testSet.add(i);
+ }
+ unmodifiableSet = new SealableNavigableSet<>(testSet, sealedSupplier);
+ }
+
+ @Test
+ void testSubSet() {
+ NavigableSet subSet = unmodifiableSet.subSet(30, true, 70, true);
+ assertEquals(5, subSet.size(), "SubSet size should initially include 30, 40, 50, 60, 70");
+
+ assertThrows(IllegalArgumentException.class, () -> subSet.add(25), "Adding 25 should fail as it is outside the bounds");
+ assertTrue(subSet.add(35), "Adding 35 should succeed");
+ assertEquals(6, subSet.size(), "SubSet size should now be 6");
+ assertEquals(11, unmodifiableSet.size(), "Enclosing set should reflect the addition");
+
+ assertFalse(subSet.remove(10), "Removing 10 should fail as it is outside the bounds");
+ assertTrue(subSet.remove(40), "Removing 40 should succeed");
+ assertEquals(5, subSet.size(), "SubSet size should be back to 5 after removal");
+ assertEquals(10, unmodifiableSet.size(), "Enclosing set should reflect the removal");
+
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> subSet.add(60), "Modification should fail when sealed");
+
+ }
+
+ @Test
+ void testHeadSet() {
+ NavigableSet headSet = unmodifiableSet.headSet(50, true);
+ assertEquals(5, headSet.size(), "HeadSet should include 10, 20, 30, 40, 50");
+
+ assertThrows(IllegalArgumentException.class, () -> headSet.add(55), "Adding 55 should fail as it is outside the bounds");
+ assertTrue(headSet.add(5), "Adding 5 should succeed");
+ assertEquals(6, headSet.size(), "HeadSet size should now be 6");
+ assertEquals(11, unmodifiableSet.size(), "Enclosing set should reflect the addition");
+
+ assertFalse(headSet.remove(60), "Removing 60 should fail as it is outside the bounds");
+ assertTrue(headSet.remove(20), "Removing 20 should succeed");
+ assertEquals(5, headSet.size(), "HeadSet size should be back to 5 after removal");
+ assertEquals(10, unmodifiableSet.size(), "Enclosing set should reflect the removal");
+
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> headSet.add(10), "Modification should fail when sealed");
+ }
+
+ @Test
+ void testTailSet() {
+ NavigableSet tailSet = unmodifiableSet.tailSet(50, true);
+ assertEquals(6, tailSet.size(), "TailSet should include 50, 60, 70, 80, 90, 100");
+
+ assertThrows(IllegalArgumentException.class, () -> tailSet.add(45), "Adding 45 should fail as it is outside the bounds");
+ assertTrue(tailSet.add(110), "Adding 110 should succeed");
+ assertEquals(7, tailSet.size(), "TailSet size should now be 7");
+ assertEquals(11, unmodifiableSet.size(), "Enclosing set should reflect the addition");
+
+ assertFalse(tailSet.remove(40), "Removing 40 should fail as it is outside the bounds");
+ assertTrue(tailSet.remove(60), "Removing 60 should succeed");
+ assertEquals(6, tailSet.size(), "TailSet size should be back to 6 after removal");
+ assertEquals(10, unmodifiableSet.size(), "Enclosing set should reflect the removal");
+
+ sealedState = true;
+ assertThrows(UnsupportedOperationException.class, () -> tailSet.add(80), "Modification should fail when sealed");
+ }
+}
diff --git a/src/test/java/com/cedarsoftware/util/SealableSetTest.java b/src/test/java/com/cedarsoftware/util/SealableSetTest.java
new file mode 100644
index 00000000..f1db4bfa
--- /dev/null
+++ b/src/test/java/com/cedarsoftware/util/SealableSetTest.java
@@ -0,0 +1,205 @@
+package com.cedarsoftware.util;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.function.Supplier;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static com.cedarsoftware.util.CollectionUtilities.setOf;
+import static com.cedarsoftware.util.DeepEquals.deepEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @author John DeRegnaucourt (jdereg@gmail.com)
+ *
+ * Copyright (c) Cedar Software LLC
+ *
+ * Licensed 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
+ *
+ * License
+ *
+ * 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.
+ */
+class SealableSetTest {
+
+ private SealableSet set;
+ private volatile boolean sealed = false;
+ private Supplier sealedSupplier = () -> sealed;
+
+ @BeforeEach
+ void setUp() {
+ set = new SealableSet<>(sealedSupplier);
+ set.add(10);
+ set.add(20);
+ }
+
+ @Test
+ void testAdd() {
+ assertTrue(set.add(30));
+ assertTrue(set.contains(30));
+ }
+
+ @Test
+ void testRemove() {
+ assertTrue(set.remove(20));
+ assertFalse(set.contains(20));
+ }
+
+ @Test
+ void testAddWhenSealed() {
+ sealed = true;
+ assertThrows(UnsupportedOperationException.class, () -> set.add(40));
+ }
+
+ @Test
+ void testRemoveWhenSealed() {
+ sealed = true;
+ assertThrows(UnsupportedOperationException.class, () -> set.remove(10));
+ }
+
+ @Test
+ void testIteratorRemoveWhenSealed() {
+ Iterator iterator = set.iterator();
+ sealed = true;
+ iterator.next(); // Move to first element
+ assertThrows(UnsupportedOperationException.class, iterator::remove);
+ }
+
+ @Test
+ void testClearWhenSealed() {
+ sealed = true;
+ assertThrows(UnsupportedOperationException.class, set::clear);
+ }
+
+ @Test
+ void testIterator() {
+ // Set items could be in any order
+ Iterator iterator = set.iterator();
+ assertTrue(iterator.hasNext());
+ Integer value = iterator.next();
+ assert value == 10 || value == 20;
+ value = iterator.next();
+ assert value == 10 || value == 20;
+ assertFalse(iterator.hasNext());
+ assertThrows(NoSuchElementException.class, iterator::next);
+ }
+
+ @Test
+ void testRootSealStateHonored() {
+ Iterator iterator = set.iterator();
+ iterator.next();
+ sealed = true;
+ assertThrows(UnsupportedOperationException.class, () -> iterator.remove());
+ sealed = false;
+ iterator.remove();
+ assertEquals(set.size(), 1);
+ }
+
+ @Test
+ void testContainsAll() {
+ assertTrue(set.containsAll(Arrays.asList(10, 20)));
+ assertFalse(set.containsAll(Arrays.asList(10, 30)));
+ }
+
+ @Test
+ void testRetainAll() {
+ set.retainAll(Arrays.asList(10));
+ assertTrue(set.contains(10));
+ assertFalse(set.contains(20));
+ }
+
+ @Test
+ void testRetainAllWhenSealed() {
+ sealed = true;
+ assertThrows(UnsupportedOperationException.class, () -> set.retainAll(Arrays.asList(10)));
+ }
+
+ @Test
+ void testAddAll() {
+ set.addAll(Arrays.asList(30, 40));
+ assertTrue(set.containsAll(Arrays.asList(30, 40)));
+ }
+
+ @Test
+ void testAddAllWhenSealed() {
+ sealed = true;
+ assertThrows(UnsupportedOperationException.class, () -> set.addAll(Arrays.asList(30, 40)));
+ }
+
+ @Test
+ void testRemoveAll() {
+ set.removeAll(Arrays.asList(10, 20));
+ assertTrue(set.isEmpty());
+ }
+
+ @Test
+ void testRemoveAllWhenSealed() {
+ sealed = true;
+ assertThrows(UnsupportedOperationException.class, () -> set.removeAll(Arrays.asList(10, 20)));
+ }
+
+ @Test
+ void testSize() {
+ assertEquals(2, set.size());
+ }
+
+ @Test
+ void testIsEmpty() {
+ assertFalse(set.isEmpty());
+ set.clear();
+ assertTrue(set.isEmpty());
+ }
+
+ @Test
+ void testToArray() {
+ assert deepEquals(setOf(10, 20), set);
+ }
+
+ @Test
+ void testToArrayGenerics() {
+ Integer[] arr = set.toArray(new Integer[0]);
+ boolean found10 = false;
+ boolean found20 = false;
+ for (int i = 0; i < arr.length; i++) {
+ if (arr[i] == 10) {
+ found10 = true;
+ }
+ if (arr[i] == 20) {
+ found20 = true;
+ }
+ }
+ assertTrue(found10);
+ assertTrue(found20);
+ assert arr.length == 2;
+ }
+
+ @Test
+ void testEquals() {
+ SealableSet other = new SealableSet<>(sealedSupplier);
+ other.add(10);
+ other.add(20);
+ assertEquals(set, other);
+ other.add(30);
+ assertNotEquals(set, other);
+ }
+
+ @Test
+ void testHashCode() {
+ int expectedHashCode = set.hashCode();
+ set.add(30);
+ assertNotEquals(expectedHashCode, set.hashCode());
+ }
+}