ch.inftec.ju.util.JuCollectionUtils Maven / Gradle / Ivy
package ch.inftec.ju.util;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ObjectUtils;
import ch.inftec.ju.util.comparison.DefaultComparator;
import ch.inftec.ju.util.comparison.EqualityTester;
import ch.inftec.ju.util.function.Function;
/**
* Contains utility methods regarding collections, maps and the like.
* @author Martin
*
*/
public final class JuCollectionUtils {
/**
* Don't instantiate.
*/
private JuCollectionUtils() {
throw new AssertionError("use only statically");
}
/**
* Compares two maps. The maps are considered equal if they contain the same keys
* and the corresponding keys have the same values (using equals on the keys
* and the specified EqualityTester on the values for comparison).
* @param m1 Map 1
* @param m2 Map 2
* @param equalityTester EqualityTester instance to test for equality. If null, a
* DefaultComparator will be used
* @return True if the maps are equal, false otherwise
*/
public static boolean mapEquals(Map m1, Map m2, EqualityTester equalityTester) {
// Handle null
if (m1 == null || m2 == null) return m1 == m2;
if (m1.size() != m2.size()) return false;
// Use DefaultComparator if tester is null
if (equalityTester == null) equalityTester = new DefaultComparator<>();
for (K k : m1.keySet()) {
// Make sure m2 contains the key too
if (!m2.containsKey(k)) return false;
// Make sure m2 has the same value for the key
if (!equalityTester.equals(m1.get(k), m2.get(k))) return false;
}
return true;
}
/**
* Compares two maps. The maps are considered equal if they contain the same keys
* and the corresponding keys have the same values (using equals on the keys
* and values for comparison).
* @param m1 Map 1
* @param m2 Map 2
* @return True if the maps are equal, false otherwise
*/
public static boolean mapEquals(Map m1, Map m2) {
return JuCollectionUtils.mapEquals(m1, m2, null);
}
/**
* Compares to collections. The collections are considered equal if they contain
* the same elements in the same order.
* @param c1 Collection 1
* @param c2 Collection 2
* @return True if the collections are equal, false otherwise
*/
public static boolean collectionEquals(Collection extends T> c1, Collection extends T> c2) {
// Handle special cases
if (c1 == c2) {
return true;
} else if (c1 == null || c2 == null) {
// Collections are not identical, so one of them will not be null...
return false;
} else if (c1.size() != c2.size()) {
return false;
} else {
// Collections are not null and have same length
// Convert the collections to List to have indexed access...
List extends T> l1 = asList(c1);
List extends T> l2 = asList(c2);
for (int i = 0; i < l1.size(); i++) {
if (!ObjectUtils.equals(l1.get(i), l2.get(i))) return false;
}
// All elements are equal
return true;
}
}
/**
* Checks if two collections are equal ignoring the order of the elements, i.e. contain
* the same elements, regardless of their order.
* @param c1 Collection 1
* @param c2 Collection 2
* @return True if both collections contain the same elements in arbitrary order
*/
public static boolean collectionEqualsIgnoreOrder(List c1, List c2) {
return intersection(JuCollectionUtils.asList(c1), JuCollectionUtils.asList(c2)).size() == c1.size();
}
/**
* Returns the intersection of the two specified collections, i.e. a new collection that contains all elements
* that are present in both collections.
* @return List containing all elements present in both collections. In case of a no match (or null parameters given),
* an empty list is returned. This method always returns a new collection, even if the result is equal to one of the input collections
*/
public static List intersection(Collection extends T> c1, Collection extends T> c2) {
// Null handling
if (c1 == null || c2 == null) {
return Collections.emptyList();
}
List intersection = new ArrayList<>();
for (T item : c1) {
if (collectionContains(c2, item)) {
intersection.add(item);
}
}
return intersection;
}
/**
* Checks if all specified values are part of the specified collection.
*
* The collection may contain more than the specified values
* @param cCollection Collection
* @param values Values the collection must contain, in arbitrary order
*/
@SafeVarargs
public static boolean collectionContains(Collection extends T> cCollection, T... values) {
for (T val : values) {
if (!cCollection.contains(val)) return false;
}
return true;
}
/**
* Checks if all specified values are part of the specified collection. Comparison
* is done case insensitively.
*
* The collection may contain more than the specified values.
* @param cCollection Collection
* @param values Values the collection must contain, in arbitrary order
*/
@SafeVarargs
public static boolean collectionContainsIgnoreCase(Collection cCollection, String... values) {
for (String val : values) {
boolean containsVal = false;
for (String colVal : cCollection) {
if (colVal.equalsIgnoreCase(val)) {
containsVal = true;
break;
}
}
if (!containsVal) return false;
}
return true;
}
/**
* Returns a map using the provided key (String or Object.toString) and
* values (Object) pairs.
* @param keyValuePairs Key value pairs
*/
public static HashMap stringMap(Object... keyValuePairs) {
HashMap map = new HashMap<>();
for (int i = 1; i < keyValuePairs.length; i+=2) {
Object obj = keyValuePairs[i-1];
String key = obj instanceof String ? (String)obj : obj.toString();
map.put(key, keyValuePairs[i]);
}
return map;
}
/**
* Returns a new ArrayList containing the specified elements.
*
* This method is annotated with SafeVarargs to indicate that no faulty
* type conversions are performed.
* @param elements Elements
* @return ArrayList containing the specified elements
*/
@SafeVarargs
public static ArrayList arrayList(T... elements) {
ArrayList list = new ArrayList<>();
Collections.addAll(list, elements);
return list;
}
/**
* Returns an unmodifiable list with the specified elements.
*
* This method is annotated with SafeVarargs to indicate that no faulty
* type conversions are performed.
* @param elements Elements
* @return Unmodifiable list containing the specified elements
*/
@SafeVarargs
public static List unmodifiableList(T... elements) {
return Collections.unmodifiableList(JuCollectionUtils.arrayList(elements));
}
/**
* Returns an empty collection if the specified collection is null,
* otherwise the same reference is returned.
*
* Can be used to avoid null pointer issues.
* @param col Collection
* @return Empty collection is specified collection is null, the same collection otherwise
*/
public static Collection emptyForNull(Collection col) {
@SuppressWarnings("unchecked")
Collection emptyList = Collections.EMPTY_LIST;
return col == null ? emptyList : col;
}
/**
* Checks if the source array is a subset of the destination array, i.e. all
* elements of the source array are contained in the destination array.
* @param srcArray Source array
* @param dstArray Destination array
* @return True if the source array is a subset of the destination array
*/
public static boolean isSubsetOf(Object[] srcArray, Object[] dstArray) {
if (dstArray == null || srcArray == null) {
return dstArray == srcArray;
}
for (Object o : srcArray) {
if (!ArrayUtils.contains(dstArray, o)) return false;
}
return true;
}
/**
* Checks if the the to arrays are equal, i.e. contain the same elements.
* Note that the type of the array is not compared, so an empty Long array will be equal to an empty Float
* array...
* @param a1 Array 1
* @param a2 Array 2
* @return True if the array are equal, false otherwise
*/
public static boolean arrayEquals(Object[] a1, Object[] a2) {
if (a1 == null || a2 == null) return a1 == a2;
// If the array have the same dimension and type, compare all elements within
if (a1.length == a2.length) {
for (int i = 0; i < a1.length; i++) {
if (!ObjectUtils.equals(a1[i], a2[i])) return false;
}
return true;
} else {
return false;
}
}
/**
* Selects all items of the collection that start with the specified start string and returns them as a new collection.
* @param inputCollection Input collection
* @param startString Start string. If null, null elements are returned
* @param caseSensitive Whether the comparison should be case sensitive
* @return Collection of strings starting with the specified start strings
*/
public static Collection selectStartingWith(Collection inputCollection, String startString, final boolean caseSensitive) {
final String startStringConv = startString == null ? null : (caseSensitive ? startString.toUpperCase() : startString);
return null;
// return CollectionUtils.select(inputCollection, new Predicate() {
// @Override
// public boolean evaluate(String object) {
// if (object == null || startStringConv == null) return object == startStringConv;
//
// String str = caseSensitive ? object.toUpperCase() : object;
// return str.startsWith(startStringConv);
// }
// });
}
/**
* Converts the specified collection to a list.
*
* If the collection already is a list, it is casted. Otherwise, a new ArrayList is created.
* @param collection Collection
* @return Collection as List
*/
public static List asList(Collection collection) {
if (collection instanceof List) return (List)collection;
else return new ArrayList<>(collection);
}
/**
* Adds all elements returned by the specified Iterable to a list.
* @param iterable Iterable
* @return List
*/
public static List asList(Iterable iterable) {
List l = new ArrayList<>();
for (T e: iterable) {
l.add(e);
}
return l;
}
/**
* Converts the specified collection to an ArrayList.
*
* If the collection already is an ArrayList, it is casted. Otherwise, a new ArrayList is created.
* @param collection Collection
* @return Collection as List
*/
public static ArrayList asArrayList(Collection collection) {
if (collection instanceof ArrayList) return (ArrayList)collection;
else return new ArrayList<>(collection);
}
/**
* Converts the specified values to an ArrayList.
* @param values List of values / array
* @return ArrayList
*/
@SafeVarargs
public static ArrayList asArrayList(T... values) {
ArrayList list = new ArrayList<>();
for (T value : values) list.add(value);
return list;
}
/**
* Converts the specified values to an ArrayList of the specified type.
* @param type Explicit type
* @param values List of values / array
* @return ArrayList
*/
@SafeVarargs
public static ArrayList asTypedArrayList(Class type, S... values) {
ArrayList list = new ArrayList<>();
for (T value : values) list.add(value);
return list;
}
/**
* Returns a sorted set with all distinct values of the specified collection in their
* natural order
* @param collection Collection containing elements
* @return Sorted set
*/
public static Set asSortedSet(Collection collection) {
return new TreeSet<>(collection);
}
/**
* Returns a sorted set with all distinct values of the specified collection in the order
* they first appear.
* @param collection Collection containing elements
* @return Sorted set
*/
public static Set asSameOrderSet(Collection collection) {
return new LinkedHashSet<>(collection);
}
/**
* Gets a set of all keys in the specified Properties instance, converted to Strings.
*
* This method will return the keys sorted alphabetically as Properties returns their keys non deterministically
* @param props Properties
* @return Set of key Strings
*/
public static Set getKeyStrings(Properties props) {
Set set = new TreeSet<>();
for (Object key : props.keySet()) {
if (key != null) set.add(key.toString());
}
return set;
}
/**
* Gets all items from an iterator, stores them in a list and returns it.
*/
public static List iteratorToList(Iterator iterator) {
List list = new ArrayList<>();
while (iterator.hasNext()) {
T item = iterator.next();
list.add(item);
}
return list;
}
/**
* Returns a new iterator backed by the specified iterator that applies the specified transformation on all the iterated values.
* @param sourceIterator Source iterator
* @param transformation Transformation function to be applied to all items
* @param Source type
* @param Result type
* @return Iterator that returns all items of the source iterator after being transformed using the specified transformation
*/
public static Iterator iteratorTransformed(Iterator sourceIterator, Function transformation) {
return new TransformingIterator<>(sourceIterator, transformation);
}
private static class TransformingIterator implements Iterator {
private final Iterator sourceIterator;
private final Function transformer;
public TransformingIterator(Iterator sourceIterator, Function transformer) {
this.sourceIterator = sourceIterator;
this.transformer = transformer;
}
@Override
public boolean hasNext() {
return sourceIterator.hasNext();
}
@Override
public E next() {
S nextVal = sourceIterator.next();
return transformer.apply(nextVal);
}
@Override
public void remove() {
sourceIterator.remove();
}
}
/**
* Returns a new iterable backed by the specified iterable that applies the specified transformation on all the iterated values.
* @param sourceIterable Source iterator
* @param transformation Transformation function to be applied to all items
* @param Source type
* @param Result type
* @return Iterator that returns all items of the source iterator after being transformed using the specified transformation
*/
public static Iterable iterableTransformed(final Iterable sourceIterable, final Function transformation) {
return new Iterable() {
@Override
public Iterator iterator() {
Iterator sourceIterator = sourceIterable.iterator();
return new TransformingIterator<>(sourceIterator, transformation);
}
};
}
/**
* Creates a new instance of a WeakReferenceIterable.
*/
public static WeakReferenceIterable newWeakReferenceIterable() {
return new WeakReferenceIterableImpl<>();
}
static final class WeakReferenceIterableImpl implements WeakReferenceIterable {
private final List> list = new ArrayList<>();
private final ReferenceQueue queue = new ReferenceQueue<>();
private int listVersion = 0;
private WeakReferenceIterableImpl() {
}
/**
* Gets the size of the internal list. This is package protected to allow unit tests
* to check whether empty reference objects are disposed correctly.
* @return Size of internal list (which may contain empty references that are not
* returned by the iterator)
*/
int getInternalListSize() {
return this.list.size();
}
@Override
public Iterator iterator() {
return new Iter();
}
@Override
public void add(E element) {
this.list.add(new ReferenceWrapper<>(element, false, this.queue, this.list));
this.listVersion++;
}
@Override
public void addWeak(E element) {
if (element == null) throw new NullPointerException("Null cannot be weak referenced");
this.list.add(new ReferenceWrapper<>(element, true, this.queue, this.list));
this.listVersion++;
}
@Override
public void remove(E element) {
for (Iterator> iter = this.list.iterator(); iter.hasNext(); ) {
ReferenceWrapper wrapper = iter.next();
if (ObjectUtils.equals(element, wrapper.get())) {
iter.remove();
break;
}
}
if (this.list.size() > 0) this.list.get(0).removeDeadReferences();
this.listVersion++;
}
@Override
public void clear() {
this.list.clear();
this.listVersion++;
}
private final static class ReferenceWrapper {
private final T strongRef;
private final WeakReference weakRef;
private final ReferenceQueue queue;
private final List> list;
ReferenceWrapper(T ref, boolean weak, ReferenceQueue queue, List> list) {
if (weak) {
this.weakRef = new WeakReference<>(ref, queue);
this.strongRef = null;
} else {
this.strongRef = ref;
this.weakRef = null;
}
this.queue = queue;
this.list = list;
this.removeDeadReferences();
}
public T get() {
return this.weakRef != null ? this.weakRef.get() : this.strongRef;
}
private void removeDeadReferences() {
Reference extends T> ref;
while ((ref = this.queue.poll()) != null) {
this.list.remove(ref);
}
}
}
private final class Iter implements Iterator {
/**
* Index of the current element, i.e. index the remove method will use.
* If -1, next hasn't been called yet.
*/
private int index = -1;
/**
* If null, hasNext hasn't been called yet. If -1, there is no
* next element. Otherwise, the index of the next element.
*/
private Integer nextIndex = null;
/**
* Index of the last element that was removed.
*/
private Integer removedIndex = null;
/**
* List version to detect concurrent modification problems
*/
private int lastListVersion = listVersion;
/**
* Strong reference to the next element to make sure it doesn't get garbage collected.
*/
private E nextElement;
@Override
public boolean hasNext() {
this.checkConcurrentUpdate();
if (this.nextIndex == null || this.index == this.nextIndex) {
// Try to get next element, i.e. set the nextIndex and nextElement
for (int i = this.index + 1; i < list.size(); i++) {
ReferenceWrapper wrapper = list.get(i);
this.nextElement = wrapper.get();
if (this.nextElement == null && wrapper.weakRef != null) {
// WeakRef that is null, so skip
} else {
// Found next element. Set index
this.nextIndex = i;
return true;
}
}
// No next element found
this.nextIndex = -1;
}
return this.nextIndex >= 0;
}
@Override
public E next() {
this.checkConcurrentUpdate();
if (this.nextIndex == null || this.index == this.nextIndex) {
this.hasNext();
}
if (this.nextIndex == -1) {
throw new NoSuchElementException("No more elements in Iterable");
}
this.index = this.nextIndex;
return list.get(this.index).get();
}
@Override
public void remove() {
this.checkConcurrentUpdate();
if (this.removedIndex != null && this.removedIndex == this.index) {
throw new IllegalStateException("Element was already removed");
}
if (this.index < 0) {
throw new IllegalStateException("Next hasn't been called yet");
}
list.remove(this.index);
this.index--;
this.removedIndex = index;
this.nextIndex--;
this.lastListVersion = listVersion;
}
private void checkConcurrentUpdate() {
if (this.lastListVersion != listVersion) {
throw new ConcurrentModificationException("WeakReferenceIterable has been modified oustide of Iterator");
}
}
}
}
}