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 col) { throwIfSealed(); return list.addAll(col); } + public boolean addAll(int index, Collection 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 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 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 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 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 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 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 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 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()); + } +}