ilscipio/scipio-erp

View on GitHub
framework/base/src/org/ofbiz/base/util/UtilMisc.java

Summary

Maintainability
F
5 days
Test Coverage
/*******************************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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.
 *******************************************************************************/
package org.ofbiz.base.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.commons.collections4.iterators.EnumerationIterator;
import org.ofbiz.base.util.collections.MapComparator;

/**
 * General and most commonly used language and utility functions for common types such as collections, maps, booleans,
 * locales, generics and casts ({@link #cast(Object)}).
 *
 * <p>SCIPIO: NOTE: This is a misnomer for a general language and utility function namespace class and would have been
 * more accurate as UtilLang. Although split classes for utilities (UtilMap, UtilList, UtilBoolean, etc.) are sometimes
 * clearer, this is a longstanding legacy class conveniently shortly named that is already automatically and manually
 * imported throughout the codebase; not all methods are helpful or appropriate for all contexts, and many come from
 * different development histories, so individual descriptions should be consulted. Currently, some generic language
 * and type methods are also found in {@link UtilNumber}, {@link UtilValidate} and others - some may be moved or
 * duplicated into UtilMisc in the future as facades; meanwhile alternative enhanced type-specific classes already exist
 * (such as ScipioMap and others) and may be used for more advanced purposes and additional helpers.</p>
 *
 * <p>SCIPIO: 3.0.0: Added several new utils such as {@link #constMap(Object...)} and deprecated code in transition;
 *  moving toward super-generic helpers such as {@link #put(Map, Object...)} (eliminates need for tons of overloads).</p>
 * <p>SCIPIO: 2.1.0: Now contains {@link #cast(Object)} as more succinct and UtilGenerics is headed toward deprecation.</p>
 */
public class UtilMisc {

    private static final Debug.OfbizLogger module = Debug.getOfbizLogger(java.lang.invoke.MethodHandles.lookup().lookupClass());

    private static final UtilMisc INSTANCE = new UtilMisc(); // SCIPIO: This is for FreeMarkerWorker (only!)

    @Deprecated
    public static final BigDecimal ZERO_BD = BigDecimal.ZERO;

    /**
     * Casts object to given type.
     *
     * <p>SCIPIO: 3.0.0: Added as more succinct than {@link UtilGenerics#cast}.</p>
     */
    @SuppressWarnings("unchecked")
    public static <V> V cast(Object object) {
        return (V) object;
    }

    public static <T extends Throwable> T initCause(T throwable, Throwable cause) {
        throwable.initCause(cause);
        return throwable;
    }

    public static <T> int compare(Comparable<T> obj1, T obj2) {
        if (obj1 == null) {
            if (obj2 == null) {
                return 0;
            }
            return 1;
        }
        return obj1.compareTo(obj2);
    }

    public static <E> int compare(List<E> obj1, List<E> obj2) {
        if (obj1 == obj2) {
            return 0;
        }
        try {
            if (obj1.size() == obj2.size() && obj1.containsAll(obj2) && obj2.containsAll(obj1)) {
                return 0;
            }
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            Debug.log(e, module);
        }
        return 1;
    }

    /**
     * Get an iterator from a collection, returning null if collection is null
     * @param col The collection to be turned in to an iterator
     * @return The resulting Iterator
     */
    public static <T> Iterator<T> toIterator(Collection<T> col) {
        if (col == null) {
            return null;
        }
        return col.iterator();
    }

    /**
     * Gets (casts) a map from the given value.
     *
     * <p>NOTE: This is for consistency but asList, asSet have serious uses.</p>
     *
     * @return The resulting Map
     */
    @SuppressWarnings("unchecked")
    public static <K, V> Map<K, V> asMap(Object map) {
        if (map instanceof Map) {
            return  (Map<K, V>) map;
        } else if (map == null) {
            return null;
        } else {
            throw new IllegalArgumentException("asList: Not a map-compatible type: " + map.getClass().getName());
        }
    }

    /**
     * Create a map from passed nameX, valueX parameters
     *
     * <p>SCIPIO: TODO: This should ideally return insert-order-preserving maps (LinkedHashMap), which was the FastMap behavior.</p>
     *
     * @return The resulting Map
     */
    public static <K, V> Map<K, V> toMap(Object... keyValuePairs) {
        if (keyValuePairs.length == 1 && keyValuePairs[0] instanceof Map) {
            return cast(keyValuePairs[0]);
        }
        return put(new HashMap<>(), keyValuePairs);
    }

    /**
     * Create a map from passed nameX, valueX parameters, as ordered map (currently LinkedHashMap).
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     *
     * @return The resulting Map
     */
    @SuppressWarnings("unchecked")
    public static <K, V> Map<K, V> orderedMap(Object... keyValuePairs) {
        if (keyValuePairs.length == 1 && keyValuePairs[0] instanceof Map) {
            return cast(keyValuePairs[0]);
        }
        return put(new LinkedHashMap<>(), keyValuePairs);
    }

    /**
     * Create a map from passed nameX, valueX parameters, as ordered map (currently LinkedHashMap).
     * @deprecated SCIPIO: 3.0.0: Use {@link #orderedMap(Object...)}.
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    public static <K, V> Map<K, V> toOrderedMap(Object... keyValuePairs) {
        return orderedMap(keyValuePairs);
    }

    /**
     * Create a map from passed nameX, valueX parameters, as fast random order map (currently HashMap).
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     *
     * @return The resulting Map
     */
    @SuppressWarnings("unchecked")
    public static <K, V> Map<K, V> randomMap(Object... keyValuePairs) {
        if (keyValuePairs.length == 1 && keyValuePairs[0] instanceof Map) {
            return cast(keyValuePairs[0]);
        }
        return put(new HashMap<>(), keyValuePairs);
    }

    /**
     * Create a map from passed nameX, valueX parameters, as umodifiable/read-only ordered map (currently LinkedHashMap).
     *
     * @return The resulting Map
     */
    @SuppressWarnings("unchecked")
    public static <K, V> Map<K, V> constMap(Object... keyValuePairs) {
        return Collections.unmodifiableMap(orderedMap(keyValuePairs));
        //return Map.of(keyValuePairs); // Unordered
    }

    /**
     * Create a map from passed nameX, valueX parameters, as umodifiable/read-only ordered map (currently LinkedHashMap).
     *
     * @return The resulting Map
     */
    @SuppressWarnings("unchecked")
    public static <K, V> Map<K, V> constMapCopy(Map<? extends K, ? extends V> map) {
        return Collections.unmodifiableMap(new LinkedHashMap<>(map));
        //return Map.copyOf(map); // Unordered
    }

    /**
     * Puts the given key-value pairs into the map and returns the map, for chaining.
     *
     * <p>SCIPIO: 3.0.0: Made this method prominent because it's extremely short, convenient and works with all map types.</p>
     * <p>SCIPIO: 2.1.0: Added.</p>
     *
     * @return The same map (for chaining)
     */
    public static <M extends Map<K, V>, K, V> M put(M map, Object... keyValuePairs) {
        if ((keyValuePairs.length % 2) != 0) {
            throw new IllegalArgumentException("Uneven number of key-value pair arguments");
        }
        for (int i = 0; i < keyValuePairs.length; i += 2) {
            @SuppressWarnings("unchecked")
            K key = (K) keyValuePairs[i];
            @SuppressWarnings("unchecked")
            V value = (V) keyValuePairs[i + 1];
            map.put(key, value);
        }
        return map;
    }

    /**
     * Create a map from passed nameX, valueX parameters, into provided map (currently LinkedHashMap).
     *
     * <p>TODO: Deprecate in favor of superior shorthand {@link #put(Map, Object...)} as this hasn't been in use for long.</p>
     *
     * <p>SCIPIO: 2.1.0: Added.</p>
     *
     * @return The same map (for chaining)
     * @see #put(Map, Object...)
     */
    @SuppressWarnings("unchecked")
    public static <K, V, M extends Map<K, V>> M putAll(M map, Object... keyValuePairs) {
        return put(map, keyValuePairs);
    }

    /**
     * SCIPIO: For an inMap with generics Map&lt;K, V&gt;, populates and returns the opposite mapping outMap, Map&lt;V, K&gt;
     * Added 2017-07-12.
     */
    public static <K, V> Map<V, K> putAllReverseMapping(Map<V, K> map, Map<? extends K, ? extends V> inMap) {
        for (Map.Entry<? extends K, ? extends V> entry : inMap.entrySet()) {
            map.put(entry.getValue(), entry.getKey());
        }
        return map;
    }

    /**
     * Transfers specified keys from in to out map.
     *
     * <p>SCIPIO: 2017-12-04: Added.</p>
     */
    public static <M extends Map<K, V>, K, V> M putKeys(M map, Map<? extends K, ? extends V> inMap, Collection<? extends K> keys) {
        for (K key : keys) {
            map.put(key, inMap.get(key));
        }
        return map;
    }

    /**
     * Transfers all or part of the specified keys from in to out (this) map, with null-treatment options.
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     *
     * @param preserveNull If null, transfer all keys even if missing from source; if false, transfer only non-null values from source;
     *                     if true, transfer only non-null and keys present in source ({@link Map#containsKey}
     */
    public static <K, V, M extends Map<K, V>> M putKeys(M map, Map<? extends K, ? extends V> inMap, Collection<? extends K> keys, Boolean preserveNull) {
        if (keys == null) {
            keys = inMap.keySet();
        }
        if (preserveNull == null) {
            for (K key : keys) {
                map.put(key, inMap.get(key));
            }
        } else if (preserveNull) {
            for (K key : keys) {
                V value = inMap.get(key);
                if (value != null || inMap.containsKey(key)) { // NOTE: Optimized to leave containsKey() last because most values are non-null
                    map.put(key, value);
                }
            }
        } else {
            for (K key : keys) {
                V value = inMap.get(key);
                if (value != null) {
                    map.put(key, value);
                }
            }
        }
        return map;
    }

    /**
     * Transfers specified keys from in to out map.
     *
     * <p>SCIPIO: 3.0.0: Changed unused overload signature to fix compilation ambiguity.</p>
     *
     * @param preserveNull If null, transfer all keys even if missing from source; if false, transfer only non-null values from source;
     *                     if true, transfer only non-null and keys present in source ({@link Map#containsKey}
     */
    @SafeVarargs
    public static <M extends Map<K, V>, K, V> M putKeys(M map, Map<? extends K, ? extends V> inMap, Boolean preserveNull, K... keys) {
        return putKeys(map, inMap, Arrays.asList(keys), preserveNull);
    }

    /**
     * Sets all specified keys to null.
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     */
    public static <M extends Map<K, V>, K, V> M putNull(M map, Collection<? extends K> keys) {
        for (K key : keys) {
            map.put(key, null);
        }
        return map;
    }

    /**
     * Sets all specified keys to null.
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     */
    public static <M extends Map<K, V>, K, V> M putNull(M map, K... keys) {
        return putNull(map, Arrays.asList(keys));
    }

    /**
     * Removes all specified keys.
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     */
    public static <M extends Map<K, V>, K, V> M removeKeys(M map, Collection<? extends K> keys) {
        for (K key : keys) {
            map.remove(key);
        }
        return map;
    }

    /**
     * Removes all specified keys.
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     */
    public static <M extends Map<K, V>, K, V> M removeKeys(M map, K... keys) {
        return removeKeys(map, Arrays.asList(keys));
    }

    public static <K, V> String printMap(Map<? extends K, ? extends V> theMap) {
        StringBuilder theBuf = new StringBuilder();
        for (Map.Entry<? extends K, ? extends V> entry: theMap.entrySet()) {
            theBuf.append(entry.getKey());
            theBuf.append(" --> ");
            theBuf.append(entry.getValue());
            theBuf.append(System.getProperty("line.separator"));
        }
        return theBuf.toString();
    }

    public static <T> List<T> makeListWritable(Collection<? extends T> col) {
        return (col != null) ? new ArrayList<>(col) : new ArrayList<>();
    }

    public static <K, V> Map<K, V> makeMapWritable(Map<K, ? extends V> map) {
        return (map != null) ? new HashMap<>(map) : new HashMap<>();
    }

    public static <T> Set<T> makeSetWritable(Collection<? extends T> col) {
        return (col != null) ? new LinkedHashSet<>(col) : new LinkedHashSet<>();
    }

    /**
     * This change a Map to be Serializable by removing all entries with values that are not Serializable.
     *
     * @param <V>
     * @param map
     */
    public static <V> void makeMapSerializable(Map<String, V> map) {
        // now filter out all non-serializable values
        Set<String> keysToRemove = new LinkedHashSet<>();
        for (Map.Entry<String, V> mapEntry: map.entrySet()) {
            Object entryValue = mapEntry.getValue();
            if (entryValue != null && !(entryValue instanceof Serializable)) {
                keysToRemove.add(mapEntry.getKey());
                if (Debug.verboseOn()) {
                    Debug.logVerbose("Found Map value that is not Serializable: " + mapEntry.getKey() + "=" + mapEntry.getValue(), module);
                }

            }
        }
        for (String keyToRemove: keysToRemove) { map.remove(keyToRemove); }
    }

    /**
     * Sort a List of Maps by specified consistent keys.
     * @param listOfMaps List of Map objects to sort.
     * @param sortKeys List of Map keys to sort by.
     * @return a new List of sorted Maps.
     */
    public static List<Map<Object, Object>> sortMaps(List<Map<Object, Object>> listOfMaps, List<? extends String> sortKeys) {
        if (listOfMaps == null || sortKeys == null) {
            return null;
        }
        List<Map<Object, Object>> toSort = new ArrayList<>(listOfMaps.size());
        toSort.addAll(listOfMaps);
        try {
            MapComparator mc = new MapComparator(sortKeys);
            toSort.sort(mc);
        } catch (Exception e) {
            Debug.logError(e, "Problems sorting list of maps; returning null.", module);
            return null;
        }
        return toSort;
    }

    /**
     * Assuming outerMap not null; if null will throw a NullPointerException
     */
    public static <K, IK, V> Map<IK, V> getMapFromMap(Map<K, Object> outerMap, K key) {
        Map<IK, V> innerMap = UtilGenerics.<IK, V>checkMap(outerMap.get(key));
        if (innerMap == null) {
            innerMap = new HashMap<>();
            outerMap.put(key, innerMap);
        }
        return innerMap;
    }

    /**
     * Assuming outerMap not null; if null will throw a NullPointerException
     */
    public static <K, V> List<V> getListFromMap(Map<K, Object> outerMap, K key) {
        List<V> innerList = UtilGenerics.<V>checkList(outerMap.get(key));
        if (innerList == null) {
            innerList = new ArrayList<>(); // SCIPIO: switched to ArrayList
            outerMap.put(key, innerList);
        }
        return innerList;
    }

    /**
     * Assuming theMap not null; if null will throw a NullPointerException
     */
    public static <K> BigDecimal addToBigDecimalInMap(Map<K, Object> theMap, K mapKey, BigDecimal addNumber) {
        Object currentNumberObj = theMap.get(mapKey);
        BigDecimal currentNumber = null;
        if (currentNumberObj == null) {
            currentNumber = BigDecimal.ZERO;
        } else if (currentNumberObj instanceof BigDecimal) {
            currentNumber = (BigDecimal) currentNumberObj;
        } else if (currentNumberObj instanceof Double) {
            currentNumber = new BigDecimal((Double) currentNumberObj);
        } else if (currentNumberObj instanceof Long) {
            currentNumber = new BigDecimal((Long) currentNumberObj);
        } else {
            throw new IllegalArgumentException("In addToBigDecimalInMap found a Map value of a type not supported: " + currentNumberObj.getClass().getName());
        }

        if (addNumber == null || BigDecimal.ZERO.compareTo(addNumber) == 0) {
            return currentNumber;
        }
        currentNumber = currentNumber.add(addNumber);
        theMap.put(mapKey, currentNumber);
        return currentNumber;
    }

    public static <T> T removeFirst(List<T> lst) {
        return lst.remove(0);
    }

    public static <T> Set<T> collectionToSet(Collection<T> c) {
        if (c == null) {
            return null;
        }
        Set<T> theSet;
        if (c instanceof Set<?>) {
            theSet = (Set<T>) c;
        } else {
            // SCIPIO: 2.1.0: Fixed erroneous modification of input collection
            theSet = new LinkedHashSet<>();
            //c.remove(null);
            //theSet.addAll(c);
            for(T val : c) {
                if (val != null) {
                    theSet.add(val);
                }
            }
        }
        return theSet;
    }

    /**
     * Generates a String from given values delimited by delimiter.
     *
     * @param values
     * @param delimiter
     * @return String
     */
    public static String collectionToString(Collection<? extends Object> values, String delimiter) {
        if (UtilValidate.isEmpty(values)) {
            return null;
        }
        if (delimiter == null) {
            delimiter = "";
        }
        StringBuilder out = new StringBuilder();

        for (Object val : values) {
            out.append(UtilFormatOut.safeToString(val)).append(delimiter);
        }
        return out.toString();
    }

    /**
     * Gets set or creates an ordered set copy from the passed collection.
     *
     * <p>NOTE: This can now always be expected to return an insert-order-preserving set, because this
     * generally increases the predictibility and reliability of various operations (security, stability, queries,
     * logging, etc.).</p>
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     */
    public static <T> Set<T> asSet(Object collection) {
        if (collection instanceof Set) {
            return cast(collection);
        } else if (collection instanceof Collection) {
            return new LinkedHashSet<>(UtilMisc.<Collection<T>>cast(collection));
        } else if (collection == null) {
            return null;
            // TODO: arrays
        } else {
            throw new IllegalArgumentException("asSet: Not a collection-compatible type: " + collection.getClass().getName());
        }
    }

    /**
     * Create an ordered set copy from the passed objects.
     *
     * <p>SCIPIO: 3.0.0: NOTE: This can now always be expected to return an insert-order-preserving set, because this
     * generally increases the predictibility and reliability of various operations (security, stability, queries,
     * logging, etc.).</p>
     *
     * @param elems
     * @return theSet
     */
    @SafeVarargs
    public static <T> Set<T> toSet(T... elems) {
        return new LinkedHashSet<>(Arrays.asList(elems));
    }

    /**
     * Gets or creates an ordered set copy from the passed collection.
     *
     * <p>NOTE: This can now always be expected to return an insert-order-preserving set when one is created, because this
     * generally increases the predictibility and reliability of various operations (security, stability, queries,
     * logging, etc.).</p>
     *
     * <p>TODO: Should be deprecated in favor of {@link #asSet} because much clearer and this one conflicts with {@link #toSet(Object...)}.</p>
     *
     * <p>SCIPIO: 3.0.0: Made order-preserving and now always creates a copy for safety and caller modification.</p>
     *
     * @param collection
     * @return theSet
     */
    public static <T> Set<T> toSet(Collection<T> collection) {
        return (collection != null) ? (collection instanceof Set ? (Set<T>) collection : new LinkedHashSet<>(collection)) : null;
    }

    /**
     * Create a fast random set copy from the passed objects.
     *
     * <p>SCIPIO: 3.0.0: Added because {@link #toSet} is no longer random and sometimes this is explicitly desired.
     * You can also use {@link Set#of} and {@link Set#copyOf} for immutable versions.</p>
     *
     * @see Set#of
     */
    @SafeVarargs
    public static <T> Set<T> randomSet(T... elems) {
        return (elems != null) ? new HashSet<>(Arrays.asList(elems)) : null;
    }

    /**
     * Gets from or creates an unmodifiable ordered set copy from the passed elements.
     *
     * <p>NOTE: This can now always be expected to return an insert-order-preserving set, because this
     * generally increases the predictibility and reliability of various operations (security, stability, queries,
     * logging, etc.).</p>
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     */
    @SuppressWarnings("unchecked")
    public static <T> Set<T> constSet(T... elems) {
        return Collections.unmodifiableSet(toSet(elems));
    }

    /**
     * Gets from or creates an unmodifiable ordered set copy from the passed collection.
     *
     * <p>NOTE: This can now always be expected to return an insert-order-preserving set, because this
     * generally increases the predictibility and reliability of various operations (security, stability, queries,
     * logging, etc.).</p>
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     */
    public static <T> Set<T> constSetCopy(Collection<? extends T> coll) {
        return Collections.unmodifiableSet(new LinkedHashSet<>(coll));
    }

    /**
     * Creates a set from the passed array.
     * <p>SCIPIO: 3.0.0: NOTE: This can now always be expected to return an insert-order-preserving set, because this
     * generally increases the predictibility and reliability of various operations (security, stability, queries,
     * logging, etc.).</p>
     */
    public static <T> Set<T> toSetArray(T[] data) {
        if (data == null) {
            return null;
        }
        return new LinkedHashSet<>(Arrays.asList(data));
    }

    /**
     * SCIPIO: Create a HashSet from passed objX parameters.
     *
     * <p>TODO: Deprecate in favor of {@link #randomSet}.</p>
     *
     * @return The resulting HashSet
     */
    @SafeVarargs
    public static <T> Set<T> toHashSet(T... obj) {
        return new HashSet<T>(Arrays.asList(obj));
    }

    /**
     * SCIPIO: Create a HashSet from passed objX parameters.
     *
     * <p>TODO: Deprecate in favor of {@link #randomSet}.</p>
     *
     * @return The resulting HashSet
     */
    @Deprecated
    public static <T> Set<T> toHashSet(Collection<? extends T> collection) {
        return new HashSet<T>(collection);
    }

    /**
     * Gets list or creates a list copy from the passed collection.
     *
     * <p>NOTE: This can now always be expected to return an insert-order-preserving set, because this
     * generally increases the predictibility and reliability of various operations (security, stability, queries,
     * logging, etc.).</p>
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     *
     * @param collection
     * @return The list
     */
    @SuppressWarnings("unchecked")
    public static <T> List<T> asList(Object collection) {
        if (collection instanceof List) {
            return (List<T>) collection;
        } else if (collection instanceof Collection) {
            return new ArrayList<>((Collection<T>) collection);
            // TODO: arrays
        } else if (collection == null) {
            return null;
        } else {
            throw new IllegalArgumentException("asList: Not a collection-compatible type: " + collection.getClass().getName());
        }
    }

    /**
     * Creates a list from passed objects.
     * @param data
     * @return list
     */
    @SafeVarargs
    public static <T> List<T> toList(T... data) {
        return new ArrayList<>(Arrays.asList(data));
    }

    /**
     * Gets or creates a list copy from the passed collection.
     *
     * <p>TODO: Should be deprecated in favor of {@link #asList} because much clearer and this one conflicts with {@link #toList(Object...)}.</p>
     */
    @SuppressWarnings("unchecked")
    public static <T> List<T> toList(Collection<? extends T> collection) {
        return asList(collection);
    }

    /**
     * Creates an unmodifiable ordered list copy from the passed elements.
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     *
     * @param elems
     * @return theSet
     */
    @SuppressWarnings("unchecked")
    public static <T> List<T> constList(T... elems) {
        return List.copyOf(Arrays.asList(elems));
    }

    /**
     * Creates an unmodifiable ordered list copy from the passed collection.
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     *
     * @param collection
     * @return theSet
     */
    public static <T> List<T> constListCopy(Collection<? extends T> collection) {
        return List.copyOf(collection);
    }

    public static <T> List<T> toListArray(T[] data) {
        return (data != null) ? new ArrayList<T>(Arrays.asList(data)) : null;
    }

    public static <K, V> void addToListInMap(V element, Map<K, ?> theMap, K listKey) { // SCIPIO: Generalized this: Map<K, Object>
        List<V> theList = UtilGenerics.checkList(theMap.get(listKey));
        if (theList == null) {
            theList = new ArrayList<>(); // SCIPIO: switched to ArrayList
            UtilGenerics.<Map<K, List<V>>>cast(theMap).put(listKey, theList); // SCIPIO: cast
        }
        theList.add(element);
    }

    /**
     * SCIPIO: Adds element to the list in the in map having given key; if no set yet, listSupplier provides a new one.
     */
    public static <K, V> void addToListInMap(V element, Map<K, ?> theMap, K listKey, Supplier<List<V>> listSupplier) { // SCIPIO: Generalized this: Map<K, Object>
        List<V> theList = UtilGenerics.checkList(theMap.get(listKey));
        if (theList == null) {
            theList = listSupplier.get();
            UtilGenerics.<Map<K, List<V>>>cast(theMap).put(listKey, theList); // SCIPIO: cast
        }
        theList.add(element);
    }

    public static <K, V> void addToSetInMap(V element, Map<K, Set<V>> theMap, K setKey) {
        Set<V> theSet = UtilGenerics.checkSet(theMap.get(setKey));
        if (theSet == null) {
            theSet = new LinkedHashSet<>();
            theMap.put(setKey, theSet);
        }
        theSet.add(element);
    }

    public static <K, V> void addToSortedSetInMap(V element, Map<K, Set<V>> theMap, K setKey) {
        Set<V> theSet = UtilGenerics.checkSet(theMap.get(setKey));
        if (theSet == null) {
            theSet = new TreeSet<>();
            theMap.put(setKey, theSet);
        }
        theSet.add(element);
    }

    /**
     * SCIPIO: Adds element to the set in the in map having given key; if no set yet, setSupplier provides a new one.
     */
    public static <K, V> void addToSetInMap(V element, Map<K, Set<V>> theMap, K setKey, Supplier<Set<V>> setSupplier) {
        Set<V> theSet = UtilGenerics.checkSet(theMap.get(setKey));
        if (theSet == null) {
            theSet = setSupplier.get();
            theMap.put(setKey, theSet);
        }
        theSet.add(element);
    }

    /** Converts an <code>Object</code> to a <code>double</code>. Returns
     * zero if conversion is not possible.
     * @param obj Object to convert
     * @return double value
     */
    public static double toDouble(Object obj) {
        Double result = toDoubleObject(obj);
        return result == null ? 0.0 : result;
    }

    /** Converts an <code>Object</code> to a <code>Double</code>. Returns
     * defaultValue if conversion is not possible (SCIPIO).
     * @param obj Object to convert
     * @param defaultValue default value
     * @return Double
     */
    public static Double toDouble(Object obj, Double defaultValue) {
        Double result = toDoubleObject(obj);
        return result == null ? defaultValue : result;
    }

    /** Converts an <code>Object</code> to a <code>Double</code>. Returns
     * <code>null</code> if conversion is not possible.
     * @param obj Object to convert
     * @return Double
     */
    public static Double toDoubleObject(Object obj) {
        if (obj == null) {
            return null;
        }
        if (obj instanceof Double) {
            return (Double) obj;
        }
        if (obj instanceof Number) {
            return ((Number) obj).doubleValue();
        }
        Double result = null;
        try {
            result = Double.parseDouble(obj.toString());
        } catch (Exception e) {}
        return result;
    }

    /** Converts an <code>Object</code> to a <code>float</code>. Returns
     * zero if conversion is not possible (SCIPIO).
     * @param obj Object to convert
     * @return float value
     */
    public static float toFloat(Object obj) {
        Float result = toFloatObject(obj);
        return result == null ? 0.0f : result;
    }

    /** Converts an <code>Object</code> to a <code>Float</code>. Returns
     * defaultValue if conversion is not possible (SCIPIO).
     * @param obj Object to convert
     * @param defaultValue default value
     * @return Float
     */
    public static Float toFloat(Object obj, Float defaultValue) {
        Float result = toFloatObject(obj);
        return result == null ? defaultValue : result;
    }

    /** Converts an <code>Object</code> to a <code>Float</code>. Returns
     * <code>null</code> if conversion is not possible (SCIPIO).
     * @param obj Object to convert
     * @return Float
     */
    public static Float toFloatObject(Object obj) {
        if (obj == null) {
            return null;
        }
        if (obj instanceof Float) {
            return (Float) obj;
        }
        if (obj instanceof Number) {
            return ((Number) obj).floatValue();
        }
        Float result = null;
        try {
            result = Float.parseFloat(obj.toString());
        } catch (Exception e) {}
        return result;
    }

    /** Converts an <code>Object</code> to an <code>int</code>. Returns
     * zero if conversion is not possible.
     * @param obj Object to convert
     * @return int value
     */
    public static int toInteger(Object obj) {
        Integer result = toIntegerObject(obj);
        return result == null ? 0 : result;
    }

    /** Converts an <code>Object</code> to an <code>Integer</code>. Returns
     * default value if conversion is not possible (SCIPIO).
     * @param obj Object to convert
     * @return Integer
     */
    public static Integer toInteger(Object obj, Integer defaultValue) {
        Integer result = toIntegerObject(obj);
        return result == null ? defaultValue : result;
    }

    /** Converts an <code>Object</code> to an <code>Integer</code>. Returns
     * <code>null</code> if conversion is not possible.
     * @param obj Object to convert
     * @return Integer
     */
    public static Integer toIntegerObject(Object obj) {
        if (obj == null) {
            return null;
        }
        if (obj instanceof Integer) {
            return (Integer) obj;
        }
        if (obj instanceof Number) {
            return ((Number)obj).intValue();
        }
        Integer result = null;
        try {
            result = Integer.parseInt(obj.toString());
        } catch (Exception e) {}
        return result;
    }

    /** Converts an <code>Object</code> to a <code>long</code>. Returns
     * zero if conversion is not possible.
     * @param obj Object to convert
     * @return long value
     */
    public static long toLong(Object obj) {
        Long result = toLongObject(obj);
        return result == null ? 0 : result;
    }

    /** Converts an <code>Object</code> to a <code>Long</code>. Returns
     * default value if conversion is not possible (SCIPIO).
     * @param obj Object to convert
     * @return Long
     */
    public static Long toLong(Object obj, Long defaultValue) {
        Long result = toLongObject(obj);
        return result == null ? defaultValue : result;
    }

    /** Returns true if the number is null or zero.
     * Not reliable for all floating-point values.
     * <p>SCIPIO: 2.1.0: Added.</p>
     */
    public static boolean nullOrZero(Number number) { // SCIPIO
        return (number == null || number.longValue() == 0L);
    }

    /** Returns true if the number is null or zero.
     * <p>SCIPIO: 2.1.0: Added.</p>
     */
    public static boolean nullOrZero(Integer number) { // SCIPIO
        return (number == null || number == 0);
    }

    /** Returns true if the number is null or zero.
     * <p>SCIPIO: 2.1.0: Added.</p>
     */
    public static boolean nullOrZero(Long number) { // SCIPIO
        return (number == null || number == 0);
    }

    /** Converts an <code>Object</code> to a <code>Long</code>. Returns
     * <code>null</code> if conversion is not possible.
     * @param obj Object to convert
     * @return Long
     */
    public static Long toLongObject(Object obj) {
        if (obj == null) {
            return null;
        }
        if (obj instanceof Long) {
            return (Long) obj;
        }
        if (obj instanceof Number) {
            return ((Number) obj).longValue();
        }
        Long result = null;
        try {
            result = Long.parseLong(obj.toString());
        } catch (Exception e) {}
        return result;
    }

    /**
     * Adds value to the key entry in theMap, or creates a new one if not already there
     * @param theMap
     * @param key
     * @param value
     */
    public static <K> void addToDoubleInMap(Map<K, Object> theMap, K key, Double value) {
        Double curValue = (Double) theMap.get(key);
        if (curValue != null) {
            theMap.put(key, curValue + value);
        } else {
            theMap.put(key, value);
        }
    }

    /**
     * Parse a locale string Locale object
     * @param localeString The locale string (en_US)
     * @return Locale The new Locale object or null if no valid locale can be interpreted
     */
    public static Locale parseLocale(String localeString) {
        if (UtilValidate.isEmpty(localeString)) {
            return null;
        }

        Locale locale = null;
        if (localeString.length() == 2) {
            // two letter language code
            locale = new Locale.Builder().setLanguage(localeString).build();
        } else if (localeString.length() == 5) {
            // positions 0-1 language, 3-4 are country
            String language = localeString.substring(0, 2);
            String country = localeString.substring(3, 5);
            locale = new Locale.Builder().setLanguage(language).setRegion(country).build();
        } else if (localeString.length() > 6) {
            // positions 0-1 language, 3-4 are country, 6 and on are special extensions
            String language = localeString.substring(0, 2);
            String country = localeString.substring(3, 5);
            String extension = localeString.substring(6);
            locale = new Locale(language, country, extension);
        } else {
            Debug.logWarning("Do not know what to do with the localeString [" + localeString + "], should be length 2, 5, or greater than 6, returning null", module);
        }

        return locale;
    }

    /**
     * Parse a locale string Locale objects (SCIPIO).
     * @param localeStrings The locale strings (en_US)
     * @return The locales
     */
    public static List<Locale> parseLocales(Collection<String> localeStrings) {
        List<Locale> locales = new ArrayList<>(localeStrings.size());
        for(String localeString : localeStrings) {
            Locale locale = parseLocale(localeString);
            if (locale != null) {
                locales.add(locale);
            }
        }
        return locales;
    }

    /**
     * Parse a locale string Locale objects (SCIPIO).
     * @param localeString The comma-separated locales string (en_US)
     * @return The locales
     */
    public static List<Locale> parseLocales(String localeString) {
        if (UtilValidate.isEmpty(localeString)) {
            return Collections.emptyList();
        }
        return parseLocales(Arrays.asList(localeString.split("\\s*,\\s*")));
    }

    /**
     * Return the object converted to a Locale per framework conversion rules.
     *
     * <p>The input can be a String, Locale, or even null and a valid Locale will always be returned; if nothing else works, returns the default locale.</p>
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     *
     * @param localeObject An Object representing the locale
     */
    public static Locale asLocale(Object localeObject) {
        if (localeObject instanceof Locale) {
            return (Locale) localeObject;
        } else if (localeObject instanceof String) {
            return parseLocale((String) localeObject);
        } else if (localeObject == null) {
            return null;
        } else {
            throw new IllegalArgumentException("Invalid locale type: " + localeObject.getClass().getName());
        }
    }

    /**
     * Return the object converted to a Locale per framework conversion rules.
     *
     * <p>The input can be a String, Locale, or even null and a valid Locale will always be returned; if nothing else works, returns the default locale.</p>
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     *
     * @param localeObject An Object representing the locale
     * @param defaultLocaleObject Object representing the default locale to use
     */
    public static Locale asLocale(Object localeObject, Object defaultLocaleObject) {
        Locale locale = asLocale(localeObject);
        return (locale != null) ? locale : asLocale(defaultLocaleObject);
    }

    /** The input can be a String, Locale, or even null and a valid Locale will always be returned; if nothing else works, returns the default locale.
     * @param localeObject An Object representing the locale
     */
    public static Locale ensureLocale(Object localeObject) {
        try {
            Locale locale = asLocale(localeObject);
            return (locale != null) ? locale : Locale.getDefault();
        } catch (IllegalArgumentException e) {
            Debug.logWarning("ensureLocale: " + e.toString(), module);
            return null;
        }
    }

    /**
     * Converts a Locale instance to a candidate Locale list. The list
     * is ordered most-specific to least-specific. Example:
     * <code>localeToCandidateList(Locale.US)</code> would return
     * a list containing <code>en_US</code> and <code>en</code>.
     *
     * <p>SCIPIO: 3.0.0: Added, previously as {@link UtilProperties#localeToCandidateList}.</p>
     *
     * @return A list of candidate locales.
     */
    public static List<Locale> makeLocaleCandidateList(Locale locale) {
        return makeLocaleCandidateList(locale, true, new ArrayList<>());
    }

    /**
     * Converts a Locale instance to a candidate Locale list. The list
     * is ordered most-specific to least-specific. Example:
     * <code>localeToCandidateList(Locale.US)</code> would return
     * a list containing <code>en_US</code> and <code>en</code>.
     *
     * <p>SCIPIO: 3.0.0: Added, previously as {@link UtilProperties#localeToCandidateList}.</p>
     *
     * @return A list of candidate locales.
     */
    public static List<Locale> makeLocaleCandidateList(Locale locale, boolean includeSelf) {
        return makeLocaleCandidateList(locale, includeSelf, new ArrayList<>());
    }

    /**
     * Converts a Locale instance to a candidate Locale list. The list
     * is ordered most-specific to least-specific. Example:
     * <code>localeToCandidateList(Locale.US)</code> would return
     * a list containing <code>en_US</code> and <code>en</code>.
     *
     * <p>SCIPIO: 3.0.0: Added, previously as {@link UtilProperties#localeToCandidateList}.</p>
     *
     * @return A list of candidate locales.
     */
    public static List<Locale> makeLocaleCandidateList(Locale locale, boolean includeSelf, List<Locale> outList) {
        if (includeSelf) {
            outList.add(locale);
        }
        String localeString = locale.toString();
        int pos = localeString.lastIndexOf("_");
        while (pos != -1) {
            localeString = localeString.substring(0, pos);
            outList.add(new Locale(localeString));
            pos = localeString.lastIndexOf("_");
        }
        return outList;
    }

    /**
     * Converts a Locale instance to a candidate Locale list. The list
     * is ordered most-specific to least-specific. Example:
     * <code>localeToCandidateList(Locale.US)</code> would return
     * a list containing <code>en_US</code> and <code>en</code>.
     *
     * <p>SCIPIO: 3.0.0: Added, previously as {@link UtilProperties#localeToCandidateList}.</p>
     *
     * @return A list of candidate locales.
     */
    public static List<String> makeLocaleCandidateStringList(String localeString) {
        return makeLocaleCandidateStringList(localeString, true, new ArrayList<>());
    }

    /**
     * Converts a Locale instance to a candidate Locale list. The list
     * is ordered most-specific to least-specific. Example:
     * <code>localeToCandidateList(Locale.US)</code> would return
     * a list containing <code>en_US</code> and <code>en</code>.
     *
     * <p>SCIPIO: 3.0.0: Added, previously as {@link UtilProperties#localeToCandidateList}.</p>
     *
     * @return A list of candidate locales.
     */
    public static List<String> makeLocaleCandidateStringList(String localeString, boolean includeSelf) {
        return makeLocaleCandidateStringList(localeString, includeSelf, new ArrayList<>());
    }

    /**
     * Converts a Locale instance to a candidate Locale list. The list
     * is ordered most-specific to least-specific. Example:
     * <code>localeToCandidateList(Locale.US)</code> would return
     * a list containing <code>en_US</code> and <code>en</code>.
     *
     * <p>SCIPIO: 3.0.0: Added, previously as {@link UtilProperties#localeToCandidateList}.</p>
     *
     * @return A list of candidate locales.
     */
    public static List<String> makeLocaleCandidateStringList(String localeString, boolean includeSelf, List<String> outList) {
        if (includeSelf) {
            outList.add(localeString);
        }
        int pos = localeString.lastIndexOf("_");
        while (pos != -1) {
            localeString = localeString.substring(0, pos);
            outList.add(localeString);
            pos = localeString.lastIndexOf("_");
        }
        return outList;
    }

    /**
     * Returns matching locale candidate to filter locales or null if no
     * candidate matching criteria; if availableLocales is empty itself, the (first) locale from localeObject is returned.
     *
     * <p>filterLocalesAllString may be set to true as optimization.</p>
     *
     * <p>SCIPIO: 3.0.0: Added to filter UserLogin.lastLocale and client request accept headers.</p>
     *
     * @param exact If true, performs simple candidate locale matching between input and filter locales
     */
    public static Locale getCandidateLocale(Object localeObject, List<?> filterLocales, Boolean filterLocalesAllString, Boolean exact) {
        if (localeObject == null) {
            return null;
        }
        Collection<?> locales;
        if (localeObject instanceof Collection) {
            locales = (Collection<?>) localeObject;
            if (locales.isEmpty()) {
                return null;
            }
            if (UtilValidate.isEmpty(filterLocales)) {
                return asLocale(first(locales));
            }
        } else {
            if (UtilValidate.isEmpty(filterLocales)) {
                return asLocale(localeObject);
            }
            locales = List.of(localeObject);
        }

        if (exact == null) {
            exact = true;
        }

        List<String> filterLocaleStrings;
        if (!Boolean.TRUE.equals(filterLocalesAllString)) {
            filterLocaleStrings = filterLocales.stream().map(Object::toString).collect(Collectors.toList());
        } else {
            filterLocaleStrings = UtilGenerics.cast(filterLocales);
        }

        // TODO: REVIEW: Locales may sometimes come from servlet container and it might be a good idea
        //  to pass them through parseLocale(locale.toString()) to make sure they match framework specs

        // Match one of the requested locales to exact filter locale, if possible
        for (Object currentLocaleObject : locales) {
            if (currentLocaleObject instanceof String) {
                if (filterLocaleStrings.contains(currentLocaleObject)) {
                    return asLocale(currentLocaleObject);
                }
            } else {
                Locale currentLocale = asLocale(currentLocaleObject);
                if (currentLocale != null && filterLocaleStrings.contains(currentLocale.toString())) {
                    return currentLocale;
                }
            }
        }

        if (!exact) {
            // Use candidate locales of both requested and filter locales

            // If the client passed a list as localeObject, do a second pass with candidate locales of each (excluding the first of each, already checked);
            // this will usually only work if the filter locales are in simplified general language form: "en", "de", etc.
            List<Object> localesAndCandidates = new ArrayList<>(locales); // put exact locales first, for next section
            for (Object currentLocaleObject : locales) {
                Locale currentLocale = asLocale(currentLocaleObject);
                if (currentLocale != null) {
                    List<Locale> currentCandidateLocales = UtilProperties.localeToCandidateList(currentLocale, false);
                    for (Locale currentCandidateLocale : currentCandidateLocales) {
                        if (filterLocaleStrings.contains(currentLocale.toString())) {
                            return currentCandidateLocale;
                        }
                    }
                    localesAndCandidates.addAll(currentCandidateLocales);
                }
            }

            // Try to match the client locale against the candidate locales of the filter locales;
            // this will usually only work if the filter locales are in precise mode: "en_US", "de_DE", etc.
            // NOTE: We have to match back to the more precise locale to respect the filter locales
            Map<Locale, Locale> candidateFilterLocalesMap = makeReverseCandidateLocaleMap(filterLocales, false, new LinkedHashMap<>());
            for (Object currentCandidateLocaleObj : localesAndCandidates) {
                Locale origFilterLocale = candidateFilterLocalesMap.get(asLocale(currentCandidateLocaleObj));
                if (origFilterLocale != null) {
                    return origFilterLocale;
                }
            }
        }
        return null;
    }

    public static Map<Locale, Locale> makeReverseCandidateLocaleMap(List<?> locales, boolean includeSelf, Map<Locale, Locale> outMap) {
        if (includeSelf) {
            List<Locale> convertedLocales = new ArrayList<>(locales.size());
            for (Object localeObj : locales) { // Put the locales themselves for order preservation (pass LinkedHashMap for this)
                Locale locale = UtilMisc.asLocale(localeObj);
                if (locale != null) {
                    outMap.put(locale, locale);
                    convertedLocales.add(locale);
                }
            }
            for (Locale locale : convertedLocales) {
                for (Locale candidateLocale : UtilProperties.localeToCandidateList(locale, false)) {
                    if (!outMap.containsKey(candidateLocale)) {
                        outMap.put(candidateLocale, locale);
                    }
                }
            }
        } else {
            for (Object localeObj : locales) {
                Locale locale = UtilMisc.asLocale(localeObj);
                if (locale != null) {
                    for (Locale candidateLocale : UtilProperties.localeToCandidateList(locale, false)) {
                        if (!outMap.containsKey(candidateLocale)) {
                            outMap.put(candidateLocale, locale);
                        }
                    }
                }
            }
        }
        return outMap;
    }

    // Private lazy-initializer class
    private static class LocaleHolder {
        private static final List<Locale> availableLocaleList = getAvailableLocaleList();

        /** SCIPIO: SPECIAL: Available locales automatically expanded to include countries (country required) (added 2017-10-11) */
        private static final List<Locale> availableLocaleExpandedCountryRequiredList = getAvailableLocaleExpandedCountryRequiredList();

        /** SCIPIO: SPECIAL: Available locales automatically expanded to include countries, but will also show locales not having countries (added 2017-10-11) */
        private static final List<Locale> availableLocaleExpandedCountryOptionalList = getAvailableLocaleExpandedCountryOptionalList();

        private static List<Locale> getAvailableLocaleList() {
            TreeMap<String, Locale> localeMap = new TreeMap<>();
            String localesString = UtilProperties.getPropertyValue("general", "locales.available");
            if (UtilValidate.isNotEmpty(localesString)) {
                List<String> idList = StringUtil.split(localesString, ",");
                for (String id : idList) {
                    Locale curLocale = parseLocale(id);
                    localeMap.put(curLocale.getDisplayName(), curLocale);
                }
            } else {
                Locale[] locales = Locale.getAvailableLocales();
                for (int i = 0; i < locales.length && locales[i] != null; i++) {
                    String displayName = locales[i].getDisplayName();
                    if (!displayName.isEmpty()) {
                        localeMap.put(displayName, locales[i]);
                    }
                }
            }
            return Collections.unmodifiableList(new ArrayList<>(localeMap.values()));
        }

        /** SCIPIO: SPECIAL: Returns a List of available locales sorted by display name expanded to include country codes (added 2017-10-11) */
        private static List<Locale> getAvailableLocaleExpandedCountryRequiredList() {
            List<Locale> list = getAvailableLocaleExpandedCountryOptionalList();
            ArrayList<Locale> filtered = new ArrayList<>();
            for(Locale locale : list) {
                if (UtilValidate.isNotEmpty(locale.getCountry())) {
                    filtered.add(locale);
                }
            }
            filtered.trimToSize();
            return Collections.unmodifiableList(filtered);
        }

        /** SCIPIO: SPECIAL: Returns a List of available locales sorted by display name expanded to include country codes and also without country codes (added 2017-10-11) */
        private static List<Locale> getAvailableLocaleExpandedCountryOptionalList() {
            TreeMap<String, Locale> localeMap = new TreeMap<>();
            String localesString = UtilProperties.getPropertyValue("general", "locales.available");
            if (UtilValidate.isNotEmpty(localesString)) {
                List<String> idList = StringUtil.split(localesString, ",");
                Set<String> genericLangs = new HashSet<>();
                for (String id : idList) {
                    Locale curLocale = parseLocale(id);
                    localeMap.put(curLocale.getDisplayName(), curLocale);
                    //if (UtilValidate.isEmpty(curLocale.getCountry())) { // TODO: REVIEW: don't restrict the countries with this list for now...
                    genericLangs.add(curLocale.getLanguage());
                    //}
                }
                Locale[] locales = Locale.getAvailableLocales();
                for (int i = 0; i < locales.length && locales[i] != null; i++) {
                    if (genericLangs.contains(locales[i].getLanguage())) {
                        String displayName = locales[i].getDisplayName();
                        if (!displayName.isEmpty()) {
                            localeMap.put(displayName, locales[i]);
                        }
                    }
                }
            } else {
                Locale[] locales = Locale.getAvailableLocales();
                for (int i = 0; i < locales.length && locales[i] != null; i++) {
                    String displayName = locales[i].getDisplayName();
                    if (!displayName.isEmpty()) {
                        localeMap.put(displayName, locales[i]);
                    }
                }
            }
            return Collections.unmodifiableList(new ArrayList<>(localeMap.values()));
        }
    }

    /** Returns a List of available locales sorted by display name */
    public static List<Locale> availableLocales() {
        return LocaleHolder.availableLocaleList;
    }

    /** SCIPIO: SPECIAL: Returns a List of available locales sorted by display name expanded to include country codes (added 2017-10-11)
     * NOTE: This list may be subject to restrictions by user configuration (now or in the future) - do not rely on this to get a list of all existing countries. */
    public static List<Locale> availableLocalesExpandedCountryRequired() {
        return LocaleHolder.availableLocaleExpandedCountryRequiredList;
    }

    /** SCIPIO: SPECIAL: Returns a List of available locales sorted by display name expanded to include country codes and also without country codes (added 2017-10-11).
     * NOTE: This list may be subject to restrictions by user configuration (now or in the future) - do not rely on this to get a list of all existing countries. */
    public static List<Locale> availableLocalesExpandedCountryOptional() {
        return LocaleHolder.availableLocaleExpandedCountryOptionalList;
    }

    /** @deprecated use Thread.sleep() */
    @Deprecated
    public static void staticWait(long timeout) throws InterruptedException {
        Thread.sleep(timeout);
    }

    public static void copyFile(File sourceLocation , File targetLocation) throws IOException {
        if (sourceLocation.isDirectory()) {
            throw new IOException("File is a directory, not a file, cannot copy") ;
        }
        try (
                InputStream in = new FileInputStream(sourceLocation);
                OutputStream out = new FileOutputStream(targetLocation);
        ) {
            // Copy the bits from instream to outstream
            byte[] buf = new byte[1024];
            int len;
            while ((len = in.read(buf)) > 0) {
                out.write(buf, 0, len);
            }
        }
    }

    public static int getViewLastIndex(int listSize, int viewSize) {
        return (int)Math.ceil(listSize / (float) viewSize) - 1;
    }

    // SCIPIO: 2018-08-30: these methods cannot exist here because Delegator
    // is not available from base component; they are moved to:
    // org.ofbiz.common.address.AddressUtil
//    public static Map<String, String> splitPhoneNumber(String phoneNumber, Delegator delegator) {
//        Map<String, String> result = new HashMap<>();
//        try {
//            PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
//            String defaultCountry = EntityUtilProperties.getPropertyValue("general", "country.geo.id.default", delegator);
//            GenericValue defaultGeo = EntityQuery.use(delegator).from("Geo").where("geoId", defaultCountry).cache().queryOne();
//            String defaultGeoCode = defaultGeo != null ? defaultGeo.getString("geoCode") : "US";
//            PhoneNumber phNumber = phoneUtil.parse(phoneNumber, defaultGeoCode);
//            if (phoneUtil.isValidNumber(phNumber) || phoneUtil.isPossibleNumber(phNumber)) {
//                String nationalSignificantNumber = phoneUtil.getNationalSignificantNumber(phNumber);
//                int areaCodeLength = phoneUtil.getLengthOfGeographicalAreaCode(phNumber);
//                result.put("countryCode", Integer.toString(phNumber.getCountryCode()));
//                if (areaCodeLength > 0) {
//                    result.put("areaCode", nationalSignificantNumber.substring(0, areaCodeLength));
//                    result.put("contactNumber", nationalSignificantNumber.substring(areaCodeLength));
//                } else {
//                    result.put("areaCode", "");
//                    result.put("contactNumber", nationalSignificantNumber);
//                }
//            } else {
//                Debug.logError("Invalid phone number " + phoneNumber, module);
//                result.put(ModelService.ERROR_MESSAGE, "Invalid phone number");
//            }
//        } catch (GenericEntityException | NumberParseException ex) {
//            Debug.logError(ex, module);
//            result.put(ModelService.ERROR_MESSAGE, ex.getMessage());
//        }
//        return result;
//    }

    /**
     * SCIPIO: Gets map entries matching the given prefix.
     */
    public static Map<String, Object> getPrefixedMapEntries(Map<String, Object> map, String prefix) {
        Map<String, Object> res = new HashMap<>();
        for(Map.Entry<String, Object> entry : map.entrySet()) {
            String name = entry.getKey();
            if (name != null && name.startsWith(prefix)) {
                res.put(name.substring(prefix.length()), entry.getValue());
            }
        }
        return res;
    }

    /**
     * SCIPIO: Creates a new, empty map (abstraction).
     * <p>
     * This returns a general-purpose map type, such as HashMap, and does not
     * guarantee insertion order.
     * <p>
     * This and the other methods below are useful for Freemarker workarounds and to guarantee a map
     * is of the same type as the other toMap calls in this class.
     */
    public static <K, V> Map<K, V> newMap() {
        return new HashMap<>();
    }

    /**
     * SCIPIO: Creates a new map initialized from the given map (abstraction).
     * <p>
     * This returns a general-purpose map type, such as HashMap, and does not
     * guarantee insertion order.
     * @see #newMap()
     */
    public static <K, V> Map<K, V> newMap(Map<? extends K, ? extends V> map) {
        return new HashMap<>(map);
    }

    /**
     * SCIPIO: Creates a new map with given initial capacity hint (abstraction).
     * <p>
     * This returns a general-purpose map type, such as HashMap, and does not
     * guarantee insertion order. The initial capacity hint may or may not
     * be honored.
     * @see #newMap()
     */
    public static <K, V> Map<K, V> newMap(int initialCapacity) {
        return new HashMap<>(initialCapacity);
    }

    /**
     * SCIPIO: Creates a new, insert-order-preserving empty map (abstraction).
     * <p>
     * This returns a general-purpose insert-order-preserving map type, such as LinkedHashMap.
     * <p>
     * This is useful for Freemarker workarounds and to guarantee a map
     * is of the same type as the other toMap calls in this class.
     */
    public static <K, V> Map<K, V> newInsertOrderMap() {
        return new LinkedHashMap<>();
    }

    /**
     * SCIPIO: Creates a new, insert-order-preserving empty map initialized from the
     * given collection (abstraction).
     * <p>
     * This returns a general-purpose insert-order-preserving map type, such as LinkedHashMap.
     * @see #newInsertOrderMap()
     */
    public static <K, V> Map<K, V> newInsertOrderMap(Map<? extends K, ? extends V> map) {
        return new LinkedHashMap<>(map);
    }

    /**
     * SCIPIO: Creates a new, empty list (abstraction).
     * <p>
     * This returns a general-purpose list type with no specific initial capacity or structure,
     * appropriate for general use in most Scipio code and services - it may be ArrayList,
     * LinkedList, or even another.
     * <p>
     * NOTE: Often it is better to choose a specific List type such as ArrayList or LinkedList
     * for a given situation; this method is for code which has not been performance written or analyzed.
     * In particular, it may be used to replace instances of javolution FastList usage in old
     * code. This type is likely - but not guaranteed - to remain ArrayList.
     * <p>
     * This is useful for Freemarker workarounds and to guarantee a list
     * is of a type common used by Scipio code.
     */
    public static <V> List<V> newList() {
        return new ArrayList<>(); // new LinkedList<V>()
    }

    /**
     * SCIPIO: Creates a new list initialized from the given collection (abstraction).
     * <p>
     * This returns a general-purpose list type with no specific structure,
     * with contents duplicated from the passed collection,
     * appropriate for general use in most Scipio code and services - it may be ArrayList,
     * LinkedList, or even another.
     * <p>
     * NOTE: Usually it is better to choose a specific List type such as ArrayList or LinkedList
     * for a given situation; this method is for code which has not been performance analyzed.
     * In particular, it may be used to replace instances of javolution FastList usage in old
     * code. This type is likely - but not guaranteed - to remain ArrayList.
     * @see #newList()
     */
    public static <V> List<V> newList(Collection<? extends V> c) {
        return new ArrayList<>(c); // new LinkedList<V>(c)
    }

    /**
     * SCIPIO: Creates a new, empty list, with given initial capacity hint (abstraction).
     * <p>
     * This returns a general-purpose list type with no specific structure,
     * and which may or may not honor the passed initialCapacity,
     * appropriate for general use in most Scipio code and services - it may be ArrayList,
     * LinkedList, or even another.
     * <p>
     * NOTE: Usually it is better to choose a specific List type such as ArrayList or LinkedList
     * for a given situation; this method is for code which has not been performance analyzed.
     * In particular, it may be used to replace instances of javolution FastList usage in old
     * code. This type is likely - but not guaranteed - to remain ArrayList.
     * @see #newList()
     */
    public static <V> List<V> newList(int initialCapacity) {
        return new ArrayList<>(initialCapacity); // new LinkedList<V>()
    }

    /**
     * SCIPIO: Creates a new, empty set (abstraction).
     * <p>
     * This returns a general-purpose set type with no specific initial capacity, structure,
     * and not guaranteed to preserve order,
     * appropriate for general use in most Scipio code and services.
     * <p>
     * This is useful for Freemarker workarounds and to guarantee a list
     * is of a type common used by Scipio code.
     */
    public static <V> Set<V> newSet() {
        return new HashSet<>();
    }

    /**
     * SCIPIO: Creates a new set initialized from the given collection (abstraction).
     * <p>
     * This returns a general-purpose set type with no specific initial capacity, structure,
     * and not guaranteed to preserve order,
     * appropriate for general use in most Scipio code and services.
     * @see #newSet()
     */
    public static <V> Set<V> newSet(Collection<? extends V> c) {
        return new HashSet<>(c);
    }

    /**
     * SCIPIO: Creates a new, empty insert-order-preserving set (abstraction).
     * <p>
     * This returns a general-purpose insert-order-preserving set type with no specific
     * initial capacity or structure,
     * appropriate for general use in most Scipio code and services.
     * <p>
     * This is useful for Freemarker workarounds and to guarantee a list
     * is of a type common used by Scipio code.
     */
    public static <V> Set<V> newInsertOrderSet() {
        return new LinkedHashSet<>();
    }

    /**
     * SCIPIO: Creates a new insert-order-preserving set initialized from the given collection (abstraction).
     * <p>
     * This returns a general-purpose insert-order-preserving set type with no specific
     * initial capacity or structure, appropriate for general use in most Scipio code and services.
     * @see #newInsertOrderSet()
     */
    public static <V> Set<V> newInsertOrderSet(Collection<? extends V> c) {
        return new LinkedHashSet<>(c);
    }

    /**
     * SCIPIO: Returns a new list of entries, where each entry corresponds to the value
     * for the given key for each map in the collection.
     */
    public static <K, V extends R, R> List<R> collectMapValuesForKey(Collection<Map<K, V>> collection, K key) {
        List<R> res = new ArrayList<>(collection.size());
        for(Map<K, V> map : collection) {
            res.add(map.get(key));
        }
        return res;
    }

    /**
     * Returns the first non-null value, or null if none.
     *
     * <p>SCIPIO: 2.x.x: Added.</p>
     */
    @SafeVarargs
    public static <T> T firstNonNull(T... values) {
        return UtilObject.firstNonNull(values);
    }

    /**
     * Returns the first non-null value, or null.
     *
     * <p>SCIPIO: 2.x.x: Added.</p>
     */
    public static <T> T firstNonNull(Collection<?> values) {
        return UtilObject.firstNonNull(values);
    }

    /**
     * Returns the first element of collection or list using get() if possible.
     *
     * <p>SCIPIO: 2.x.x: Added.</p>
     */
    public static <T> T first(Collection<?> values) {
        return (values instanceof List) ? UtilGenerics.<List<T>>cast(values).get(0) : UtilGenerics.cast(values.iterator().next());
    }

    /**
     * Returns the first element of collection or list using get() if possible, or null if empty.
     *
     * <p>SCIPIO: 2.x.x: Added.</p>
     */
    public static <T> T firstSafe(Collection<?> values) {
        return UtilValidate.isNotEmpty(values) ? first(values) : null;
    }

    /**
     * Returns the first value in a collection, array, or the value itself, throwing
     * an exception if collections are empty.
     *
     * <p>NOTE: This method does not handle maps (too poorly defined - entry vs value).</p>
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     *
     * @see #firstSafe(Object)
     * @see #firstValue(Object)
     */
    public static <T> T first(Object values) throws IllegalArgumentException {
        if (values instanceof Collection) {
            return UtilMisc.first(UtilGenerics.<Collection<String>>cast(values));
        } else if (values != null && values.getClass().isArray()) {
            return firstInArray(values);
        } else {
            return UtilGenerics.cast(values);
        }
    }

    /**
     * Returns the first value in a collection, array, or the value itself, or null if empty collection.
     *
     * <p>NOTE: This method does not handle maps (too poorly defined - entry vs value).</p>
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     *
     * @see #firstSafe(Object)
     * @see #firstValueSafe(Object)
     */
    public static <T> T firstSafe(Object values) throws IllegalArgumentException {
        if (values instanceof Collection) {
            return UtilMisc.firstSafe(UtilGenerics.<Collection<String>>cast(values));
        } else if (values != null && values.getClass().isArray()) {
            return firstInArraySafe(values);
        } else {
            return UtilGenerics.cast(values);
        }
    }

    /**
     * Returns the first value (as opposed to entry) of non-empty map.
     *
     * <p>NOTE: If map is unordered or greater than size 1, result may be arbitrary.</p>
     *
     * <p>SCIPIO: 3.0.0: Added to replace ambiguous first(Map).</p>
     */
    public static <T> T firstValue(Map<?, ?> values) {
        return UtilGenerics.cast(values.entrySet().iterator().next().getValue());
    }

    /**
     * Returns the first value of map (as opposed to entry), or null if empty map.
     *
     * <p>NOTE: If map is unordered or greater than size 1, result may be arbitrary.</p>
     *
     * <p>SCIPIO: 3.0.0: Added to replace ambiguous firstSafe(Map).</p>
     */
    public static <T> T firstValueSafe(Map<?, ?> values) {
        return UtilValidate.isNotEmpty(values) ? firstValue(values) : null;
    }

    /**
     * Returns the first value of map (as opposed to entry) or collection, or the value itself.
     *
     * <p>NOTE: If map is unordered or greater than size 1, result may be arbitrary.</p>
     *
     * <p>SCIPIO: 3.0.0: Added to replace ambiguous firstOrSelfSafe(Object).</p>
     */
    public static <T> T firstValue(Object value) {
        if (value instanceof Map) {
            return UtilMisc.firstValue(UtilGenerics.<Map<String, ?>>cast(value));
        } else {
            return UtilMisc.first(value);
        }
    }

    /**
     * Returns the first value (as opposed to entry) of map, or null if empty collection or map.
     *
     * <p>NOTE: If map is unordered or greater than size 1, result may be arbitrary.</p>
     *
     * <p>SCIPIO: 3.0.0: Added to replace ambiguous firstOrSelfSafe(Object).</p>
     */
    public static <T> T firstValueSafe(Object value) {
        if (value instanceof Map) {
            return UtilMisc.firstValueSafe(UtilGenerics.<Map<String, ?>>cast(value));
        } else {
            return UtilMisc.firstSafe(value);
        }
    }

    private static <T> T firstInArray(Object values) {
        Object value;
        if (values instanceof Object[]) {
            value = ((Object[]) values)[0];
        } else if (values instanceof boolean[]) {
            value = ((boolean[]) values)[0];
        } else if (values instanceof byte[]) {
            value = ((byte[]) values)[0];
        } else if (values instanceof short[]) {
            value = ((short[]) values)[0];
        } else if (values instanceof char[]) {
            value = ((char[]) values)[0];
        } else if (values instanceof int[]) {
            value = ((int[]) values)[0];
        } else if (values instanceof long[]) {
            value = ((long[]) values)[0];
        } else if (values instanceof float[]) {
            value = ((float[]) values)[0];
        } else if (values instanceof double[]) {
            value = ((double[]) values)[0];
        } else {
            throw new IllegalArgumentException("Unknown array type: " + values.getClass().getName());
        }
        return UtilGenerics.cast(value);
    }

    private static <T> T firstInArraySafe(Object values) {
        Object value;
        if (values instanceof Object[]) {
            value = (((Object[]) values).length > 0) ? ((Object[]) values)[0] : null;
        } else if (values instanceof boolean[]) {
            value = (((boolean[]) values).length > 0) ? ((boolean[]) values)[0] : null;
        } else if (values instanceof byte[]) {
            value = (((byte[]) values).length > 0) ? ((byte[]) values)[0] : null;
        } else if (values instanceof short[]) {
            value = (((short[]) values).length > 0) ? ((short[]) values)[0] : null;
        } else if (values instanceof char[]) {
            value = (((char[]) values).length > 0) ? ((char[]) values)[0] : null;
        } else if (values instanceof int[]) {
            value = (((int[]) values).length > 0) ? ((int[]) values)[0] : null;
        } else if (values instanceof long[]) {
            value = (((long[]) values).length > 0) ? ((long[]) values)[0] : null;
        } else if (values instanceof float[]) {
            value = (((float[]) values).length > 0) ? ((float[]) values)[0] : null;
        } else if (values instanceof double[]) {
            value = (((double[]) values).length > 0) ? ((double[]) values)[0] : null;
        } else {
            throw new IllegalArgumentException("Unknown array type: " + values.getClass().getName());
        }
        return UtilGenerics.cast(value);
    }

    /**
     * SCIPIO: Returns the first element of non-empty map.
     * @deprecated SCIPIO: 3.0.0: Use {@link #firstValueSafe(Map)}, too ambiguous - TODO: REMOVE
     */
    @Deprecated
    public static <T> T first(Map<?, ?> values) {
        return UtilGenerics.cast(values.entrySet().iterator().next().getValue());
    }

    /**
     * SCIPIO: Returns the first element of map, with empty map check.
     * @deprecated SCIPIO: 3.0.0: Use {@link #firstValueSafe(Map)}, too ambiguous - TODO: REMOVE
     */
    @Deprecated
    public static <T> T firstSafe(Map<?, ?> values) {
        return UtilValidate.isNotEmpty(values) ? first(values) : null;
    }

    /**
     * SCIPIO: Returns the first element of map or collection, or other the value itself.
     * @deprecated SCIPIO: 3.0.0: Use {@link #firstValueSafe(Object)} - TODO: REMOVE
     */
    @Deprecated
    public static <T> T firstOrSelfSafe(Object values) {
        return firstValueSafe(values);
    }

    /**
     * SCIPIO: Returns Boolean.TRUE if value is Boolean.TRUE or "true", or Boolean.FALSE
     * if value is Boolean.FALSE or "false", or null if anything else (case-sensitive).
     */
    public static Boolean booleanValue(Object value) {
        return UtilValidate.booleanValue(value);
    }

    /**
     * SCIPIO: Returns Boolean.TRUE if value is "true", or Boolean.FALSE
     * if value is "false", or null if anything else (case-sensitive).
     */
    public static Boolean booleanValue(String value) {
        return UtilValidate.booleanValue(value);
    }

    /**
     * SCIPIO: Returns true if value is Boolean.TRUE or "true", or false
     * if value is Boolean.FALSE or "false", or defaultValue if anything else (case-sensitive).
     */
    public static boolean booleanValue(Object value, boolean defaultValue) {
        return UtilValidate.booleanValue(value, defaultValue);
    }

    /**
     * SCIPIO: Returns true if value is "true", or false
     * if value is "false", or defaultValue if anything else (case-sensitive).
     */
    public static boolean booleanValue(String value, boolean defaultValue) {
        return UtilValidate.booleanValue(value, defaultValue);
    }

    /**
     * SCIPIO: Returns Boolean.TRUE if value is Boolean.TRUE or "Y", or Boolean.FALSE
     * if value is Boolean.FALSE or "N", or null if anything else (case-sensitive).
     */
    public static Boolean booleanValueIndicator(Object value) {
        return UtilValidate.booleanValueIndicator(value);
    }

    /**
     * SCIPIO: Returns Boolean.TRUE if value is "Y", or Boolean.FALSE
     * if value is "N", or null if anything else (case-sensitive).
     */
    public static Boolean booleanValueIndicator(String value) {
        return UtilValidate.booleanValueIndicator(value);
    }

    /**
     * SCIPIO: Returns true if value is Boolean.TRUE or "Y", or false
     * if value is Boolean.FALSE or "N", or defaultValue if anything else (case-sensitive).
     */
    public static boolean booleanValueIndicator(Object value, boolean defaultValue) {
        return UtilValidate.booleanValueIndicator(value, defaultValue);
    }

    /**
     * SCIPIO: Returns true if value is "Y", or false
     * if value is "N", or defaultValue if anything else (case-sensitive).
     */
    public static boolean booleanValueIndicator(String value, boolean defaultValue) {
        return UtilValidate.booleanValueIndicator(value, defaultValue);
    }

    /**
     * SCIPIO: Returns Boolean.TRUE if value is Boolean.TRUE, "true" or "Y", or Boolean.FALSE
     * if value is Boolean.FALSE, "false" or "N", or null if anything else (case-sensitive).
     */
    public static Boolean booleanValueVersatile(Object value) {
        return UtilValidate.booleanValueVersatile(value);
    }

    /**
     * SCIPIO: Returns Boolean.TRUE if value is "true" or "Y", or Boolean.FALSE
     * if value is "false" or "N", or null if anything else (case-sensitive).
     */
    public static Boolean booleanValueVersatile(String value) {
        return UtilValidate.booleanValueVersatile(value);
    }

    /**
     * SCIPIO: Returns true if value is Boolean.TRUE, "true" or "Y", or false
     * if value is Boolean.FALSE, "false" or "N", or defaultValue if anything else (case-sensitive).
     */
    public static boolean booleanValueVersatile(Object value, boolean defaultValue) {
        return UtilValidate.booleanValueVersatile(value, defaultValue);
    }

    /**
     * SCIPIO: Returns true if value is "true" or "Y", or false
     * if value is "false" or "N", or defaultValue if anything else (case-sensitive).
     */
    public static boolean booleanValueVersatile(String value, boolean defaultValue) {
        return UtilValidate.booleanValueVersatile(value, defaultValue);
    }

    /**
     * SCIPIO: Returns "Y" if value is Boolean.TRUE or "Y", or "N"
     * if value is Boolean.FALSE or "N", or null if anything else (case-sensitive).
     */
    public static String indicatorValue(Object value) {
        return UtilValidate.indicatorValue(value);
    }

    /**
     * SCIPIO: Returns "Y" if value is Boolean.TRUE or "Y", or "N"
     * if value is Boolean.FALSE or "N", or defaultValue if anything else (case-sensitive).
     */
    public static String indicatorValue(Object value, String defaultValue) {
        return UtilValidate.indicatorValue(value, defaultValue);
    }

    /**
     * SCIPIO: Returns "Y" if value is Boolean.TRUE, "true" or "Y", or "N"
     * if value is Boolean.FALSE, "false" or "N", or null if anything else (case-sensitive).
     */
    public static String indicatorValueVersatile(Object value) {
        return UtilValidate.indicatorValueVersatile(value);
    }

    /**
     * SCIPIO: Returns "Y" if value is Boolean.TRUE, "true" or "Y", or "N"
     * if value is Boolean.FALSE, "false" or "N", or defaultValue if anything else (case-sensitive).
     */
    public static String indicatorValueVersatile(Object value, String defaultValue) {
        return UtilValidate.indicatorValueVersatile(value, defaultValue);
    }

    /**
     * SCIPIO: Returns an unmodifiable hash set.
     * (We use this pattern constantly.)
     *
     * TODO: Deprecate in favor of {@link #constSet}
     */
    @SuppressWarnings("unchecked")
    public static <T> Set<T> unmodifiableHashSet(T... elems) {
        return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(elems)));
    }

    /**
     * SCIPIO: Returns an unmodifiable hash set copied from the given collection.
     *
     * TODO: Deprecate in favor of {@link #constSetCopy}
     */
    public static <T> Set<T> unmodifiableHashSetCopy(Collection<? extends T> collection) {
        return Collections.unmodifiableSet(new HashSet<T>(collection));
    }

    /**
     * SCIPIO: Returns an unmodifiable hash set copied from the given collection, with support for extra elements.
     * If collection is null, a new one is created.
     */
    @Deprecated
    public static <T> Set<T> unmodifiableHashSetCopyAdd(Collection<? extends T> collection, T... addValues) {
        Set<T> set = (collection != null) ? new HashSet<>(collection) : new HashSet<>();
        for (T value : addValues) {
            set.add(value);
        }
        return Collections.unmodifiableSet(set);
    }

    /**
     * SCIPIO: Returns an unmodifiable hash set copied from the given collection, with support for extra elements.
     */
    @Deprecated
    public static <T> Set<T> unmodifiableHashSetCopyRemove(Collection<? extends T> collection, T... removeValues) {
        Set<T> set = new HashSet<>(collection);
        for (T value : removeValues) {
            set.remove(value);
        }
        return Collections.unmodifiableSet(set);
    }

    /**
     * SCIPIO: Returns an unmodifiable linked hash set.
     *
     * TODO: Deprecate in favor of {@link #constSet}
     */
    @SuppressWarnings("unchecked")
    public static <T> Set<T> unmodifiableLinkedHashSet(T... elems) {
        return Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(elems)));
    }

    /**
     * SCIPIO: Returns an unmodifiable linked hash set.
     *
     * TODO: Deprecate in favor of {@link #constSetCopy}
     */
    @SuppressWarnings("unchecked")
    public static <T> Set<T> unmodifiableLinkedHashSetCopy(Collection<? extends T> collection) {
        return Collections.unmodifiableSet(new LinkedHashSet<>(collection));
    }

    /**
     * SCIPIO: Returns an unmodifiable linked hash set copied from the given collection, with support for extra elements.
     * If collection is null, a new one is created.
     */
    @Deprecated
    public static <T> Set<T> unmodifiableLinkedHashSetCopyAdd(Collection<? extends T> collection, T... addValues) {
        Set<T> set = (collection != null) ? new LinkedHashSet<>(collection) : new LinkedHashSet<>();
        for(T value : addValues) {
            set.add(value);
        }
        return Collections.unmodifiableSet(set);
    }

    /**
     * SCIPIO: Returns an unmodifiable linked hash set copied from the given collection, with support for extra elements.
     */
    @Deprecated
    public static <T> Set<T> unmodifiableLinkedHashSetCopyRemove(Collection<? extends T> collection, T... removeValues) {
        Set<T> set = new LinkedHashSet<>(collection);
        for(T value : removeValues) {
            set.remove(value);
        }
        return Collections.unmodifiableSet(set);
    }

    /**
     * SCIPIO: Returns an unmodifiable array list.
     *
     * TODO: Deprecate in favor of List.of
     */
    @SuppressWarnings("unchecked")
    public static <T> List<T> unmodifiableArrayList(T... elems) {
        return Collections.unmodifiableList(new ArrayList<>(Arrays.asList(elems)));
    }

    /**
     * SCIPIO: Returns an unmodifiable array list copied from the given collection.
     *
     * TODO: Deprecate in favor of List.copyOf
     */
    public static <T> List<T> unmodifiableArrayListCopy(Collection<? extends T> collection) {
        return Collections.unmodifiableList(new ArrayList<>(collection));
    }

    /**
     * SCIPIO: Returns an unmodifiable array list copied from the given collection, with support for extra elements.
     * If collection is null, a new one is created.
     */
    @Deprecated
    public static <T> List<T> unmodifiableArrayListCopyAdd(Collection<? extends T> collection, T... addValues) {
        List<T> list = (collection != null) ? new ArrayList<>(collection) : new ArrayList<>();
        for(T value : addValues) {
            list.add(value);
        }
        return Collections.unmodifiableList(list);
    }

    /**
     * SCIPIO: Returns an unmodifiable array list copied from the given collection, with support for extra elements.
     */
    @Deprecated
    public static <T> List<T> unmodifiableArrayListCopyRemove(Collection<? extends T> collection, T... removeValues) {
        List<T> list = new ArrayList<>(collection);
        for(T value : removeValues) {
            list.remove(value);
        }
        return Collections.unmodifiableList(list);
    }

    /**
     * SCIPIO: Returns an unmodifiable linked list.
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    public static <T> List<T> unmodifiableLinkedList(T... elems) {
        return Collections.unmodifiableList(new LinkedList<>(Arrays.asList(elems)));
    }

    /**
     * SCIPIO: Returns an unmodifiable linked list copied from the given collection.
     */
    @Deprecated
    public static <T> List<T> unmodifiableLinkedListCopy(Collection<? extends T> collection) {
        return Collections.unmodifiableList(new LinkedList<>(collection));
    }

    /**
     * SCIPIO: Returns an unmodifiable linked list copied from the given collection, with support for extra elements.
     * If collection is null, a new one is created.
     */
    @Deprecated
    public static <T> List<T> unmodifiableLinkedListCopyAdd(Collection<? extends T> collection, T... addValues) {
        List<T> list = (collection != null) ? new LinkedList<>(collection) : new LinkedList<>();
        for(T value : addValues) {
            list.add(value);
        }
        return Collections.unmodifiableList(list);
    }

    /**
     * SCIPIO: Returns an unmodifiable linked list copied from the given collection, with support for extra elements.
     */
    @Deprecated
    public static <T> List<T> unmodifiableLinkedListCopyRemove(Collection<? extends T> collection, T... removeValues) {
        List<T> list = new LinkedList<>(collection);
        for(T value : removeValues) {
            list.remove(value);
        }
        return Collections.unmodifiableList(list);
    }

    /**
     * SCIPIO: Creates a hash map copy with specified keys.
     * Added 2017-12-04.
     */
    @Deprecated
    public static <K, V> Map<K, V> toHashMapWithKeys(Map<? extends K, ? extends V> inMap, Collection<K> keys) {
        Map<K, V> outMap = new HashMap<>();
        for(K key : keys) { outMap.put(key, inMap.get(key)); }
        return outMap;
    }

    /**
     * SCIPIO: Creates a hash map copy including specified keys.
     * Added 2017-12-04.
     */
    @Deprecated
    @SafeVarargs
    public static <K, V> Map<K, V> toHashMapWithKeys(Map<? extends K, ? extends V> inMap, K... keys) {
        Map<K, V> outMap = new HashMap<>();
        for(K key : keys) { outMap.put(key, inMap.get(key)); }
        return outMap;
    }

    /**
     * SCIPIO: Creates a hash map copy excluding specified keys.
     * Added 2017-12-04.
     */
    @Deprecated
    public static <K, V> Map<K, V> toHashMapWithoutKeys(Map<? extends K, ? extends V> inMap, Collection<K> keys) {
        Map<K, V> outMap = new HashMap<>();
        for(Map.Entry<? extends K, ? extends V> entry : inMap.entrySet()) {
            if (!keys.contains(entry.getKey())) { outMap.put(entry.getKey(), entry.getValue()); }
        }
        return outMap;
    }

    /**
     * SCIPIO: Creates a hash map copy excluding specified keys.
     * Added 2017-12-04.
     */
    @Deprecated
    @SafeVarargs
    public static <K, V> Map<K, V> toHashMapWithoutKeys(Map<? extends K, ? extends V> inMap, K... keys) {
        return toHashMapWithoutKeys(inMap, new HashSet<>(Arrays.asList(keys)));
    }

    /**
     * SCIPIO: Creates a linked (ordered) hash map copy with specified keys, preserving
     * the original insertion order.
     * NOTE: this is slower than {@link #toLinkedHashMapWithKeysNewOrder}.
     * Added 2017-12-04.
     */
    @Deprecated
    public static <K, V> Map<K, V> toLinkedHashMapWithKeysOrigOrder(Map<? extends K, ? extends V> inMap, Collection<K> keys) {
        Map<K, V> outMap = new LinkedHashMap<>();
        for(Map.Entry<? extends K, ? extends V> entry : inMap.entrySet()) {
            if (keys.contains(entry.getKey())) { outMap.put(entry.getKey(), entry.getValue()); }
        }
        return outMap;
    }

    /**
     * SCIPIO: Creates a linked (ordered) hash map copy with specified keys, preserving
     * the original insertion order.
     * NOTE: this is slower than {@link #toLinkedHashMapWithKeysNewOrder}.
     * Added 2017-12-04.
     */
    @Deprecated
    @SafeVarargs
    public static <K, V> Map<K, V> toLinkedHashMapWithKeysOrigOrder(Map<? extends K, ? extends V> inMap, K... keys) {
        return toLinkedHashMapWithKeysOrigOrder(inMap, new HashSet<>(Arrays.asList(keys)));
    }

    /**
     * SCIPIO: Creates a linked (ordered) hash map copy including specified keys, with the
     * key order determined by the order of the passed keys collection parameter.
     * Added 2017-12-04.
     */
    @Deprecated
    public static <K, V> Map<K, V> toLinkedHashMapWithKeysNewOrder(Map<? extends K, ? extends V> inMap, Collection<K> keys) {
        Map<K, V> outMap = new HashMap<>();
        for(K key : keys) { outMap.put(key, inMap.get(key)); }
        return outMap;
    }

    /**
     * SCIPIO: Creates a linked (ordered) hash map copy including specified keys, with the
     * key order determined by the order of the passed keys collection parameter.
     * Added 2017-12-04.
     */
    @Deprecated
    @SafeVarargs
    public static <K, V> Map<K, V> toLinkedHashMapWithKeysNewOrder(Map<? extends K, ? extends V> inMap, K... keys) {
        Map<K, V> outMap = new HashMap<>();
        for(K key : keys) { outMap.put(key, inMap.get(key)); }
        return outMap;
    }

    /**
     * SCIPIO: Creates a linked (ordered) hash map copy excluding specified keys.
     * The original key order is preserved.
     * Added 2017-12-04.
     */
    @Deprecated
    public static <K, V> Map<K, V> toLinkedHashMapWithoutKeys(Map<? extends K, ? extends V> inMap, Collection<K> keys) {
        Map<K, V> outMap = new LinkedHashMap<>();
        for(Map.Entry<? extends K, ? extends V> entry : inMap.entrySet()) {
            if (!keys.contains(entry.getKey())) { outMap.put(entry.getKey(), entry.getValue()); }
        }
        return outMap;
    }

    /**
     * SCIPIO: Creates a linked (ordered) hash map copy excluding specified keys.
     * The original key order is preserved.
     * Added 2017-12-04.
     */
    @Deprecated
    @SafeVarargs
    public static <K, V> Map<K, V> toLinkedHashMapWithoutKeys(Map<? extends K, ? extends V> inMap, K... keys) {
        return toLinkedHashMapWithoutKeys(inMap, new HashSet<>(Arrays.asList(keys)));
    }

    /**
     * SCIPIO: For each map in the given collection, extracts the value for the specified key and adds
     * it to the given outValueCollection.
     * Added 2018-05-29.
     */
    @SuppressWarnings("unchecked")
    public static <K, V, T extends V, C extends Collection<T>> C extractValuesForKey(Collection<? extends Map<K, V>> mapList,
            K key, C outValueCollection) {
        for(Map<K, V> map : mapList) {
            outValueCollection.add((T) map.get(key));
        }
        return outValueCollection;
    }

    /**
     * SCIPIO: For each map in the given collection, extracts the value for the specified key and adds
     * it to the given outValueCollection; skips null values.
     * Added 2018-05-29.
     */
    @SuppressWarnings("unchecked")
    public static <K, V, T extends V, C extends Collection<T>> C extractValuesForKeyNonNull(Collection<? extends Map<K, V>> mapList,
            K key, C outValueCollection) {
        for(Map<K, V> map : mapList) {
            V value = map.get(key);
            if (value != null) {
                outValueCollection.add((T) map.get(key));
            }
        }
        return outValueCollection;
    }

    /**
     * SCIPIO: For each map in the given collection, extracts the value for the specified key and
     * maps it to the original map value in the outValueMap.
     * Added 2018-05-29.
     */
    @SuppressWarnings("unchecked")
    public static <K, V, T extends V, E extends Map<K, V>, M extends Map<T, E>> M extractValuesForKeyAsMap(Collection<E> mapList,
            K key, M outValueMap) {
        for(E map : mapList) {
            outValueMap.put((T) map.get(key), map);
        }
        return outValueMap;
    }

    /**
     * SCIPIO: For each map in the given collection, extracts the value for the specified key and
     * maps it to the original map value in the outValueMap; skips null values.
     * Added 2018-05-29.
     */
    @SuppressWarnings("unchecked")
    public static <K, V, T extends V, E extends Map<K, V>, M extends Map<T, E>> M extractValuesForKeyAsMapNonNull(Collection<E> mapList,
            K key, M outValueMap) {
        for(E map : mapList) {
            V value = map.get(key);
            if (value != null) {
                outValueMap.put((T) map.get(key), map);
            }
        }
        return outValueMap;
    }

    /**
     * SCIPIO: Returns true if and only if all of the test values are in the given collection.
     * Added 2018-09-26.
     */
    public static <T> boolean containsAll(Collection<T> collection, Iterable<? extends T> testValues) {
        if (collection == null) {
            return false;
        }
        for (T value : testValues) {
            if (!collection.contains(value)) {
                return false;
            }
        }
        return true;
    }

    /**
     * SCIPIO: If the given value is already an ArrayList or null, returns it as-is; otherwise returns a copy as an ArrayList.
     * Added 2018-11-23.
     */
    @Deprecated
    public static <T> List<T> asArrayList(Collection<T> value) {
        return (value instanceof ArrayList || value == null) ? (List<T>) value : new ArrayList<>(value);
    }

    /**
     * SCIPIO: If the given value is an ArrayList, returns it as-is; if Collection, returns a new
     * ArrayList copy from it; if null, returns null; if other type, throws ClassCastException.
     * Added 2019-01-23.
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    public static <T> List<T> asArrayList(Object value) {
        return (value instanceof ArrayList || value == null) ? (List<T>) value : new ArrayList<T>((Collection<T>) value);
    }

    /**
     * SCIPIO: If the given value is not already a HashSet or null, returns it as-is; otherwise returns a copy as a HashSet.
     * Added 2019-01-23.
     */
    @Deprecated
    public static <T> Set<T> asHashSet(Collection<T> value) {
        return (value instanceof HashSet || value == null) ? (Set<T>) value : new HashSet<>(value);
    }

    /**
     * SCIPIO: If the given value is an HashSet, returns it as-is; if Collection, returns a new
     * HashSet copy from it; if null, returns null; if other type, throws ClassCastException.
     * Added 2019-01-23.
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    public static <T> Set<T> asHashSet(Object value) {
        return (value instanceof HashSet || value == null) ? (Set<T>) value : new HashSet<T>((Collection<T>) value);
    }

    /**
     * SCIPIO: If the given value is not already a LinkedHashSet or null, returns it as-is; otherwise returns a copy as a LinkedHashSet.
     * Added 2019-01-23.
     */
    @Deprecated
    public static <T> Set<T> asLinkedHashSet(Collection<T> value) {
        return (value instanceof LinkedHashSet || value == null) ? (Set<T>) value : new LinkedHashSet<>(value);
    }

    /**
     * SCIPIO: If the given value is an LinkedHashSet, returns it as-is; if Collection, returns a new
     * LinkedHashSet copy from it; if null, returns null; if other type, throws ClassCastException.
     * Added 2019-01-23.
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    public static <T> Set<T> asLinkedHashSet(Object value) {
        return (value instanceof LinkedHashSet || value == null) ? (Set<T>) value : new LinkedHashSet<T>((Collection<T>) value);
    }

    /**
     * SCIPIO: If the given value is not already a HashMap or null, returns it as-is; otherwise returns a copy as a HashMap.
     * Added 2019-01-23.
     */
    @Deprecated
    public static <K, V> Map<K, V> asHashMap(Map<K, V> value) {
        return (value instanceof HashMap || value == null) ? (Map<K, V>) value : new HashMap<>(value);
    }

    /**
     * SCIPIO: If the given value is an HashMap, returns it as-is; if another Map, returns a new
     * HashMap copy from it; if null, returns null; if other type, throws ClassCastException.
     * Added 2019-01-23.
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    public static <K, V> Map<K, V> asHashMap(Object value) {
        return (value instanceof HashMap || value == null) ? (Map<K, V>) value : new HashMap<K, V>((Map<K, V>) value);
    }

    /**
     * SCIPIO: If the given value is not already a LinkedHashMap or null, returns it as-is; otherwise returns a copy as a LinkedHashMap.
     * Added 2019-01-23.
     */
    @Deprecated
    public static <K, V> Map<K, V> asLinkedHashMap(Map<K, V> value) {
        return (value instanceof LinkedHashMap || value == null) ? (Map<K, V>) value : new LinkedHashMap<>(value);
    }

    /**
     * SCIPIO: If the given value is an LinkedHashMap, returns it as-is; if another Map, returns a new
     * LinkedHashMap copy from it; if null, returns null; if other type, throws ClassCastException.
     * Added 2019-01-23.
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    public static <K, V> Map<K, V> asLinkedHashMap(Object value) {
        return (value instanceof LinkedHashMap || value == null) ? (Map<K, V>) value : new LinkedHashMap<K, V>((Map<K, V>) value);
    }

    /**
     * SCIPIO: If the given value is not already an ArrayList, returns it as-is; otherwise creates a copy as an ArrayList;
     * if null, returns empty.
     * Added 2018-11-23.
     */
    @Deprecated
    public static <T> List<T> asArrayListNonNull(Collection<T> value) {
        return (value instanceof ArrayList) ? (List<T>) value : ((value == null) ? new ArrayList<>() : new ArrayList<>(value));
    }

    /**
     * SCIPIO: If the given value is an ArrayList, returns it as-is; if Collection, returns a new
     * ArrayList copy from it; if null, returns empty; if other type, throws ClassCastException.
     * Added 2019-01-23.
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    public static <T> List<T> asArrayListNonNull(Object value) {
        return (value instanceof ArrayList) ? (List<T>) value : ((value == null) ? new ArrayList<>() : new ArrayList<>((Collection<T>) value));
    }

    /**
     * SCIPIO: If the given value is not already a HashSet, returns it as-is; otherwise returns a copy as a HashSet;
     * if null, returns empty.
     * Added 2019-01-23.
     */
    @Deprecated
    public static <T> Set<T> asHashSetNonNull(Collection<T> value) {
        return (value instanceof HashSet) ? (Set<T>) value : ((value == null) ? new HashSet<>() : new HashSet<>(value));
    }

    /**
     * SCIPIO: If the given value is an HashSet, returns it as-is; if Collection, returns a new
     * HashSet copy from it; if null, returns empty; if other type, throws ClassCastException.
     * Added 2019-01-23.
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    public static <T> Set<T> asHashSetNonNull(Object value) {
        return (value instanceof HashSet) ? (Set<T>) value : ((value == null) ? new HashSet<>() : new HashSet<>((Collection<T>) value));
    }

    /**
     * SCIPIO: If the given value is not already a LinkedHashSet, returns it as-is; otherwise returns a copy as a HashSet;
     * if null, returns empty.
     * Added 2019-01-23.
     */
    @Deprecated
    public static <T> Set<T> asLinkedHashSetNonNull(Collection<T> value) {
        return (value instanceof LinkedHashSet) ? (Set<T>) value : ((value == null) ? new LinkedHashSet<>() : new LinkedHashSet<>(value));
    }

    /**
     * SCIPIO: If the given value is an LinkedHashSet, returns it as-is; if Collection, returns a new
     * LinkedHashSet copy from it; if null, returns empty; if other type, throws ClassCastException.
     * Added 2019-01-23.
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    public static <T> Set<T> asLinkedHashSetNonNull(Object value) {
        return (value instanceof LinkedHashSet) ? (Set<T>) value : ((value == null) ? new LinkedHashSet<>() : new LinkedHashSet<>((Collection<T>) value));
    }

    /**
     * SCIPIO: If the given value is not already a HashMap, returns it as-is; otherwise returns a copy as a HashMap;
     * if null, returns empty.
     * Added 2019-01-23.
     */
    @Deprecated
    public static <K, V> Map<K, V> asHashMapNonNull(Map<K, V> value) {
        return (value instanceof HashMap) ? (Map<K, V>) value : ((value == null) ? new HashMap<>() : new HashMap<>(value));
    }

    /**
     * SCIPIO: If the given value is an HashMap, returns it as-is; if another Map, returns a new
     * HashMap copy from it; if null, returns empty; if other type, throws ClassCastException.
     * Added 2019-01-23.
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    public static <K, V> Map<K, V> asHashMapNonNull(Object value) {
        return (value instanceof HashMap) ? (Map<K, V>) value : ((value == null) ? new HashMap<>() : new HashMap<>((Map<K, V>) value));
    }

    /**
     * SCIPIO: If the given value is not already a LinkedHashMap, returns it as-is; otherwise returns a copy as a LinkedHashMap;
     * if null, returns empty.
     * Added 2019-01-23.
     */
    @Deprecated
    public static <K, V> Map<K, V> asLinkedHashMapNonNull(Map<K, V> value) {
        return (value instanceof LinkedHashMap) ? (Map<K, V>) value : ((value == null) ? new LinkedHashMap<>() : new LinkedHashMap<>(value));
    }

    /**
     * SCIPIO: If the given value is an LinkedHashMap, returns it as-is; if another Map, returns a new
     * LinkedHashMap copy from it; if null, returns empty; if other type, throws ClassCastException.
     * Added 2019-01-23.
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    public static <K, V> Map<K, V> asLinkedHashMapNonNull(Object value) {
        return (value instanceof LinkedHashMap) ? (Map<K, V>) value : ((value == null) ? new LinkedHashMap<>() : new LinkedHashMap<>((Map<K, V>) value));
    }

    /**
     * SCIPIO: Returns a new list composed of the given collection but reversed.
     * Added 2018-12-03.
     */
    public static <T> List<T> asReversedList(Collection<T> list) {
        List<T> newList = new ArrayList<>(list);
        Collections.reverse(newList);
        return newList;
    }

    /**
     * SCIPIO: Returns a new list composed of the given collection but reversed.
     * Added 2018-12-03.
     */
    public static <T> List<T> unmodifiableReversedList(Collection<T> list) {
        return Collections.unmodifiableList(asReversedList(list));
    }

    /**
     * SCIPIO: Takes a list of maps and groups them using the specified key value.
     */
    public static <K, V, M extends Map<K, V>> Map<K, List<M>> groupMapsByKey(Iterable<M> records, K groupByKey) {
        Map<K, List<M>> map = new HashMap<>();
        for(M elem : records) {
            addToListInMap(elem, map, groupByKey);
        }
        return map;
    }

    /**
     * SCIPIO: A small helper to extra all the values for a given field from each map.
     * Convenience method to avoid null pointers and stream API.
     * Does nothing if either mapList or out are null.
     */
    @SuppressWarnings("unchecked")
    public static <K, V, T extends V, C extends Collection<T>> C getMapValuesForKey(Collection<? extends Map<K, V>> mapList, K key, C out) {
        if (mapList != null && out != null) {
            for(Map<K, V> map : mapList) {
                out.add((T) map.get(key));
            }
        }
        return out;
    }

    /**
     * SCIPIO: A small helper to extra all the values for a given field from each map, as a list of same length.
     * Convenience method to avoid null pointers and stream API.
     * If mapList is null or mapList is empty, returns unmodifiable empty list.
     * Use {@link #getMapValuesForKeyOrNewList} to force new list in all cases.
     */
    @SuppressWarnings("unchecked")
    public static <K, V, T extends V> List<T> getMapValuesForKey(Collection<? extends Map<K, V>> mapList, K key) {
        if (mapList == null || mapList.isEmpty()) {
            return Collections.emptyList();
        }
        List<T> out = new ArrayList<>(mapList.size());
        for(Map<K, V> map : mapList) {
            out.add((T) map.get(key));
        }
        return out;
    }

    /**
     * SCIPIO: A small helper to extra all the values for a given field from each map, as a same-sized ArrayList.
     * Convenience method to avoid null pointers and stream API.
     * NOTE: Always creates a new ArrayList even if mapList is null or empty.
     */
    @SuppressWarnings("unchecked")
    public static <K, V, T extends V> List<T> getMapValuesForKeyOrNewList(Collection<? extends Map<K, V>> mapList, K key) {
        if (mapList == null || mapList.isEmpty()) {
            return new ArrayList<>();
        }
        List<T> out = new ArrayList<>(mapList.size());
        for(Map<K, V> map : mapList) {
            out.add((T) map.get(key));
        }
        return out;
    }

    /**
     * SCIPIO: Wraps an Enumeration in an Iterator.
     */
    public static <T> Iterator<T> toIterator(Enumeration<T> enumeration) {
        return new EnumerationIterator<T>(enumeration);
    }

    /**
     * SCIPIO: Wraps an Iterator in an Iterable.
     * Useful when you have an API that returns only Iterator.
     */
    public static <T> Iterable<T> toIterable(Iterator<T> iterator) {
        return new Iterable<T>() {
            @Override
            public Iterator<T> iterator() {
                return iterator;
            }
        };
    }

    /**
     * SCIPIO: Wraps an Enumeration in an Iterable.
     * NOTE: This could become inefficient if you are dealing with a large number of collections.
     */
    public static <T> Iterable<T> toIterable(Enumeration<T> enumeration) {
        return toIterable(toIterator(enumeration));
    }

    /**
     * Adds the given values pairs to the collection and returns the collection, for chaining (SCIPIO).
     * @deprecated SCIPIO: 3.0.0: Redundant/misnamed; use {@link #addAll(Collection, Collection)}
     */
    @Deprecated
    public static <C extends Collection<E>, E> C add(C collection, Collection<? extends E> newElems) {
        if (collection != null && newElems != null) {
            collection.addAll(newElems);
        }
        return collection;
    }

    /**
     * Adds the given values pairs to the collection and returns the collection, for chaining (SCIPIO).
     */
    @SafeVarargs
    public static <C extends Collection<E>, E> C add(C collection, E... newElems) {
        if (collection != null && newElems != null) {
            collection.addAll(List.of(newElems));
        }
        return collection;
    }

    /**
     * Calls out.addAll for every passed collection on the outCollection.
     *
     * <p>SCIPIO: 3.0.0: More robust; removed varargs array due to compiler warnings.</p>
     *
     * @param outCollection The collection to add the inCollection values to
     * @param inCollection The source collection
     * @return The outCollection, for convenience
     */
    public static <C extends Collection<O>, O> C addAll(C outCollection, Collection<? extends O> inCollection) {
        if (inCollection != null && outCollection != null) {
            outCollection.addAll(inCollection);
        }
        return outCollection;
    }

    /**
     * SCIPIO: Adds the given enumeration's elements to the given out collection.
     *
     * @param outCollection The collection to add the inEnumeration values to
     * @param inEnumeration The source enumeration
     * @return The outCollection, for convenience
     */
    public static <T, C extends Collection<T>> C addAll(C outCollection, Enumeration<T> inEnumeration) {
        if (inEnumeration != null && outCollection != null) {
            while (inEnumeration.hasMoreElements()) {
                outCollection.add(inEnumeration.nextElement());
            }
        }
        return outCollection;
    }

    /**
     * Calls out.removeAll for every passed collection on the outCollection.
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     *
     * @param outCollection The collection to remove the inCollection values from
     * @param inCollection The source collection
     * @return The outCollection, for convenience
     */
    public static <C extends Collection<O>, O> C removeAll(C outCollection, Collection<? extends O> inCollection) {
        if (inCollection != null && outCollection != null) {
            outCollection.removeAll(inCollection);
        }
        return outCollection;
    }

    /**
     * Adds the given enumeration's elements to the given out collection.
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     *
     * @param outCollection The collection to remove the inEnumeration values from
     * @param inEnumeration The source enumeration
     * @return The outCollection, for convenience
     */
    public static <T, C extends Collection<T>> C removeAll(C outCollection, Enumeration<T> inEnumeration) {
        if (inEnumeration != null && outCollection != null) {
            while (inEnumeration.hasMoreElements()) {
                outCollection.remove(inEnumeration.nextElement());
            }
        }
        return outCollection;
    }

    /**
     * SCIPIO: Returns a new List containing the enumeration's elements.
     */
    public static <T> List<T> toList(Enumeration<T> enumeration) {
        return Collections.list(enumeration);
    }

    /**
     * SCIPIO: Returns a new Set containing the enumeration's elements.
     */
    public static <T> Set<T> toSet(Enumeration<T> enumeration) {
        return addAll(new HashSet<>(), enumeration);
    }

    /**
     * SCIPIO: Returns a new HashSet containing the enumeration's elements.
     */
    public static <T> Set<T> toHashSet(Enumeration<T> enumeration) {
        return addAll(new HashSet<>(), enumeration);
    }

    /**
     * SCIPIO: Returns a new LinkedHashSet containing the enumeration's elements.
     */
    public static <T> Set<T> toLinkedHashSet(Enumeration<T> enumeration) {
        return addAll(new HashSet<>(), enumeration);
    }

    /**
     * SCIPIO: Checks if the given enumeration contains the given object
     * using the {@link #equals(Object)} method.
     */
    public static boolean contains(Enumeration<?> enumeration, Object object) {
        if (enumeration == null) {
            return false;
        }
        if (object != null) {
            while(enumeration.hasMoreElements()) {
                if (object.equals(enumeration.nextElement())) {
                    return true;
                }
            }
        } else {
            while(enumeration.hasMoreElements()) {
                if (enumeration.nextElement() == null) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * SCIPIO: DO NOT USE: Returns a "dummy" static instance, for use by <code>FreeMarkerWorker</code>.
     * Subject to change without notice.
     * Added 2019-01-31.
     */
    public static UtilMisc getStaticInstance() {
        return INSTANCE;
    }

    /**
     * SCIPIO: Returns true if either the passed value is non-null or if the given map contains the passed key.
     * This can be used to optimize Map accesses when the values are biased toward predominantly non-null.
     */
    public static <K, V> boolean containsKey(Map<K, V> map, K key, Object value) {
        return (value != null || map.containsKey(key));
    }

    /**
     * SCIPIO: Transfers the specified keys from srcMap to dstMap and returns dstMap.
     */
    public static <K, V> Map<K, V> copyKeys(Map<K, ? extends V> srcMap, Map<K, V> dstMap, Iterable<K> keys) {
        for(K key : keys) {
            dstMap.put(key, srcMap.get(key));
        }
        return dstMap;
    }

    /**
     * SCIPIO: Copy the collection to an ArrayList of same capacity plus the given members.
     */
    public static <T> List<T> copyExtendList(Collection<? extends T> collection, Collection<? extends T> membersToAppend) {
        List<T> list = new ArrayList<>(collection.size() + membersToAppend.size());
        list.addAll(collection);
        list.addAll(membersToAppend);
        return list;
    }

    /**
     * SCIPIO: Copy the collection to an ArrayList of same capacity plus the given members.
     */
    public static <T, A extends T> List<T> copyExtendList(Collection<? extends T> collection, A... membersToAppend) {
        List<T> list = new ArrayList<>(collection.size() + membersToAppend.length);
        list.addAll(collection);
        for(T member : membersToAppend) {
            list.add(member);
        }
        return list;
    }

    /**
     * SCIPIO: Returns an ordered (currently tree) map where all keys are converted to integers and sorted naturally.
     */
    public static <V> Map<Integer, V> asOrderedIntegerMap(Map<String, V> map) {
        Map<Integer, V> orderedMap = new TreeMap<>();
        for(Map.Entry<String, V> entry : map.entrySet()) {
            orderedMap.put(Integer.parseInt(entry.getKey()), entry.getValue());
        }
        return orderedMap;
    }

    /**
     * SCIPIO: Returns null if collection is empty, otherwise the collection itself.
     */
    public static <C extends Collection> C nullIfEmpty(C collection) {
        return (collection == null) || !collection.isEmpty() ? collection : null;
    }

    /**
     * SCIPIO: Returns null if map is empty, otherwise the map itself.
     */
    public static <M extends Map<?, ?>> M nullIfEmpty(M map) {
        return (map == null) || !map.isEmpty() ? map : null;
    }

    /**
     * SCIPIO: Simple method to extract keys having prefix.
     */
    @SuppressWarnings("unchecked")
    public static <K, V, W extends V> Map<K, V> getEntriesWithKeyPrefix(Map<K, V> out, Map<String, W> map, String prefix) {
        for(Map.Entry<String, W> entry : map.entrySet()) {
            if (entry.getKey().startsWith(prefix)) {
                out.put((K) entry.getKey(), entry.getValue());
            }
        }
        return out;
    }

    /**
     * SCIPIO: Simple method to extract keys having prefix.
     */
    @SuppressWarnings("unchecked")
    public static <K, V, W extends V> Map<K, V> getEntriesWithKeyPrefix(Map<String, W> map, String prefix) {
        return getEntriesWithKeyPrefix(new LinkedHashMap<>(), map, prefix);
    }

    /**
     * SCIPIO: Simple method to extract keys having prefix, with ability to strip prefix whole or partial.
     */
    @SuppressWarnings("unchecked")
    public static <K, V, W extends V> Map<K, V> getStripEntriesWithKeyPrefix(Map<K, V> out, Map<String, W> map, String prefix, String stripPrefix) {
        for(Map.Entry<String, W> entry : map.entrySet()) {
            if (entry.getKey().startsWith(prefix)) {
                out.put((K) entry.getKey().substring(stripPrefix.length()), entry.getValue());
            }
        }
        return out;
    }

    public static <V, T extends V> V getFirst(Iterable<T> iterable) {
        if (iterable instanceof List) {
            return getFirst(UtilGenerics.<List<T>>cast(iterable));
        } else {
            return (iterable != null) ? iterable.iterator().next() : null;
        }
    }

    public static <V, T extends V> V getFirst(List<T> list) {
        return (list != null) ? list.get(0) : null;
    }

    /**
     * Returns an Iterator as-is or from an Iterable (SCIPIO).
     */
    public static <T, I extends Iterator<T>> Iterator<T> asIterator(Object object) {
        if (object instanceof Iterator) {
            return UtilGenerics.cast(object);
        } else if (object instanceof Iterable) {
            return UtilGenerics.<Iterable<T>>cast(object).iterator();
        } else {
            throw new IllegalArgumentException("Not iterable or iterator");
        }
    }

    /**
     * Static next() method that can handle any iterator type including NextOnlyIterator/EntityListIterator (SCIPIO),
     * returns null when done.
     * Example:
     * <pre>{@code
     *     Iterator<GenericValue> iterator = ...;
     *     GenericValue entity;
     *     while((entity = UtilMisc.next(iterator)) != null) {
     *         ...
     *     }
     * }</pre>
     * NOTE: Will not work properly if the iterator contains null entries.
     * @return the next value or null if no more elements.
     */
    public static <T> T next(Iterator<T> iterator) {
        return (iterator instanceof NextOnlyIterator) ? iterator.next() : (iterator.hasNext() ? iterator.next() : null);
    }

    public static <K, V> Map<K, V> subMap(Map<K, V> out, Map<K, V> map, Collection<? extends K> keys) { // SCIPIO
        for(K key : keys) {
            out.put(key, map.get(key));
        }
        return out;
    }

    public static <K, V> Map<K, V> subMap(Map<K, V> map, Collection<? extends K> keys) { // SCIPIO
        return subMap(new LinkedHashMap<>(), map, keys);
    }

    public static <K, V> Map<K, V> subMapNonNull(Map<K, V> out, Map<K, V> map, Collection<? extends K> keys) { // SCIPIO
        for(K key : keys) {
            V value = map.get(key);
            if (value != null) {
                out.put(key, value);
            }
        }
        return out;
    }

    public static <K, V> Map<K, V> subMapNonNull(Map<K, V> map, Collection<? extends K> keys) { // SCIPIO
        return subMapNonNull(new LinkedHashMap<>(), map, keys);
    }

    /**
     * SCIPIO: For each record extracts a shortPk-like string that maps to it.
     */
    public static <M extends Map<String, Object>> Map<String, M> makeShortKeyRecordMap(Collection<? extends M> records,
                                                                                       Collection<String> keyNames,
                                                                                       String keyDelim,
                                                                                       Map<String, M> outMap) throws IllegalArgumentException {
        if (records == null) {
            return null;
        }
        if (keyNames.size() == 1) {
            String pkFieldName = UtilMisc.first(keyNames);
            for (M record : records) {
                outMap.put((String) record.get(pkFieldName), record);
            }
        } else {
            for (M record : records) {
                StringBuilder key = new StringBuilder();
                for(String pkFieldName : keyNames) {
                    if (key.length() > 0) {
                        key.append(keyDelim);
                    }
                    key.append((String) record.get(pkFieldName));
                }
                outMap.put(key.toString(), record);
            }
        }
        return outMap;
    }

    /**
     * SCIPIO: For each record extracts a shortPk-like string that maps to it.
     */
    public static <M extends Map<String, Object>> Map<String, M> makeShortKeyRecordMap(Collection<? extends M> records,
                                                                                       Collection<String> keyNames,
                                                                                       String keyDelim) throws IllegalArgumentException {
        return makeShortKeyRecordMap(records, keyNames, keyDelim, new LinkedHashMap<>());
    }

    /**
     * SCIPIO: Returns the Collections.unmodifiableXxx or Collections.emptyXxx instance corresponding to the underlying type of collection, best-effort.
     */
    public static <C extends Collection<V>, V> C unmodifiableAdapted(C collection) {
        if (collection == null) {
            return null;
        } else if (collection instanceof List) {
            return UtilGenerics.cast(!collection.isEmpty() ? Collections.unmodifiableList(UtilGenerics.cast(collection)) : Collections.emptyList());
        } else if (collection instanceof NavigableSet) {
            return UtilGenerics.cast(!collection.isEmpty() ? Collections.unmodifiableNavigableSet(UtilGenerics.cast(collection)) : Collections.emptyNavigableSet());
        } else if (collection instanceof SortedSet) {
            return UtilGenerics.cast(!collection.isEmpty() ? Collections.unmodifiableSortedSet(UtilGenerics.cast(collection)) : Collections.emptySortedSet());
        } else if (collection instanceof Set) {
            return UtilGenerics.cast(!collection.isEmpty() ? Collections.unmodifiableSet(UtilGenerics.cast(collection)) : Collections.emptySet());
        } else {
            // NOTE: This may produce ClassCastException if wrong result type, but let's let caller handles this case, for simplicity
            return UtilGenerics.cast(Collections.unmodifiableCollection(collection));
        }
    }

    /**
     * SCIPIO: Returns the Collections.unmodifiableXxx or Collections.emptyXxx instance corresponding to the underlying type of map, best-effort.
     */
    public static <K, V> Map<K, V> unmodifiableAdapted(Map<K, V> map) {
        if (map == null) {
            return null;
        } else if (map instanceof NavigableMap) {
            return UtilGenerics.cast(!map.isEmpty() ? Collections.unmodifiableNavigableMap(UtilGenerics.cast(map)) : Collections.emptyNavigableMap());
        } else if (map instanceof SortedMap) {
            return UtilGenerics.cast(!map.isEmpty() ? Collections.unmodifiableSortedMap(UtilGenerics.cast(map)) : Collections.emptySortedMap());
        } else {
            return UtilGenerics.cast(!map.isEmpty() ? Collections.unmodifiableMap(map) : Collections.emptyMap());
        }
    }

    /**
     * SCIPIO: Returns the Collections.unmodifiableXxx or Collections.emptyXxx instance corresponding to the underlying type of map, best-effort.
     */
    public static <T> T unmodifiableGeneric(Object collOrMap) {
        if (collOrMap == null) {
            return null;
        } else if (collOrMap instanceof Collection) {
            return UtilGenerics.cast(unmodifiableAdapted((Collection<?>) collOrMap));
        } else if (collOrMap instanceof Map) {
            return UtilGenerics.cast(unmodifiableAdapted((Map<?, ?>) collOrMap));
        } else {
            throw new IllegalArgumentException("Unsupported type for unmodifiable collections: " + collOrMap.getClass().getName());
        }
    }

    /**
     * SCIPIO: Returns the Collections.emptyXxx instance corresponding to the specified type, best-effort.
     */
    public static <T> T emptyGeneric(Class<?> type) {
        if (List.class.isAssignableFrom(type)) {
            return UtilGenerics.cast(Collections.emptyList());
        } else if (NavigableSet.class.isAssignableFrom(type)) {
            return UtilGenerics.cast(Collections.emptyNavigableSet());
        } else if (SortedSet.class.isAssignableFrom(type)) {
            return UtilGenerics.cast(Collections.emptySortedSet());
        } else if (Set.class.isAssignableFrom(type)) {
            return UtilGenerics.cast(Collections.emptySet());
        } else if (NavigableMap.class.isAssignableFrom(type)) {
            return UtilGenerics.cast(Collections.emptyNavigableMap());
        } else if (SortedMap.class.isAssignableFrom(type)) {
            return UtilGenerics.cast(Collections.emptySortedMap());
        } else if (Map.class.isAssignableFrom(type)) {
            return UtilGenerics.cast(Collections.emptyMap());
        } else if (Collection.class.isAssignableFrom(type)) {
            return UtilGenerics.cast(Collections.emptyList());
        } else {
            throw new IllegalArgumentException("Unrecognized collection type: " + type);
        }
    }

    /**
     * SCIPIO: Attempts to optimize the given collection.
     * Currently simply trims ArrayLists.
     * NOTE: future versions may return a different instance, so the result must be used.
     */
    public static <C extends Collection<V>, V> C optimized(C collection) {
        if (collection instanceof ArrayList) {
            UtilGenerics.<ArrayList<V>>cast(collection).trimToSize();
        }
        return collection;
    }

    /**
     * SCIPIO: Attempts to optimize the given collection and returns a read-only version.
     */
    public static <C extends Collection<V>, V> C unmodifiableOptimized(C collection) {
        return unmodifiableAdapted(optimized(collection));
    }

    /**
     * SCIPIO: Attempts to optimize the given collection and returns a read-only version and produces null for empty.
     */
    public static <C extends Collection<V>, V> C unmodifiableOptimizedOrNull(C collection) {
        if (collection == null || collection.isEmpty()) {
            return null;
        } else {
            return unmodifiableAdapted(optimized(collection));
        }
    }

    /**
     * SCIPIO: Attempts to optimize the given map.
     * NOTE: future versions may return a different instance, so the result must be used.
     */
    public static <K, V> Map<K, V> optimized(Map<K, V> map) {
        return map;
    }

    /**
     * SCIPIO: Attempts to optimize the given map and returns a read-only version.
     */
    public static <K, V> Map<K, V> unmodifiableOptimized(Map<K, V> map) {
        return unmodifiableAdapted(optimized(map));
    }

    /**
     * SCIPIO: Attempts to optimize the given map and returns a read-only version and produces null for empty.
     */
    public static <K, V> Map<K, V> unmodifiableOptimizedOrNull(Map<K, V> map) {
        if (map == null || map.isEmpty()) {
            return null;
        } else {
            return unmodifiableAdapted(optimized(map));
        }
    }

    /**
     * SCIPIO: Returns a best-possible optimized views of the combined keys of both maps, unless one is not set.
     */
    public static <K> Set<K> keySet(Map<K, ?> firstMap, Map<K, ?> secondMap) { // SCIPIO
        if (UtilValidate.isNotEmpty(firstMap)) {
            if (UtilValidate.isNotEmpty(secondMap)) {
                Set<K> keys = new LinkedHashSet<>(firstMap.keySet());
                keys.addAll(secondMap.keySet());
                return keys;
            } else {
                return firstMap.keySet();
            }
        } else {
            if (UtilValidate.isNotEmpty(secondMap)) {
                return secondMap.keySet();
            } else {
                return Collections.emptySet();
            }
        }
    }

    /**
     * Returns the given parameter as a collection if it already is, otherwise puts it in a new collection;
     * if null, returns null.
     *
     * <p>WARN: If param is wrong type, collection type may be violated.</p>
     *
     * <p>SCIPIO: 3.0.0: Renamed.</p>
     */
    public static <T, C extends Collection<T>> C toCollection(Object param, Supplier<? extends Collection<? extends T>> containerFactory) {
        if (param == null) {
            return null;
        } else if (param instanceof Collection) {
            return UtilGenerics.cast(param);
        } else {
            Collection<? extends T> collection = containerFactory.get();
            collection.add(UtilGenerics.cast(param));
            return UtilGenerics.cast(collection);
        }
    }

    /**
     * Returns the given parameter as a collection if it already is, otherwise puts it in a new collection;
     * if null, returns null.
     *
     * <p>WARN: If param is wrong type, collection type may be violated.</p>
     *
     * <p>SCIPIO: 3.0.0: Renamed.</p>
     */
    public static <T, C extends Collection<T>> C toCollection(Object param) {
        return toCollection(param, ArrayList::new);
    }

    /**
     * Returns the given parameter as a collection if it already is, otherwise puts it in a new collection;
     * if null, returns null.
     * @deprecated SCIPIO: 3.0.0: Use {@link #toCollection(Object, Supplier)}
     *
     * <p>WARN: If param is wrong type, collection type may be violated.</p>
     *
     * <p>SCIPIO: 2.x.x: Added.</p>
     */
    @Deprecated
    public static <T> Collection<T> asCollectionNonNull(Object param, Supplier<? extends Collection<? extends T>> containerFactory) {
        return toCollection(param, containerFactory);
    }

    /**
     * SCIPIO: Returns the given parameter as a collection if it already is, otherwise puts it in a new collection;
     * if null, returns null.
     * @deprecated SCIPIO: 3.0.0: Use {@link #toCollection(Object)}
     *
     * <p>WARN: If param is wrong type, collection type may be violated.</p>
     *
     * <p>SCIPIO: 2.x.x: Added.</p>
     */
    @Deprecated
    public static <T> Collection<T> asCollectionNonNull(Object param) {
        return toCollection(param);
    }

    public static <C extends Collection<Pattern>> C getPatterns(Object collectionOrPat, Supplier<C> containerFactory) {
        C out = null;
        if (collectionOrPat == null) {
            ;
        } else if (collectionOrPat instanceof Collection) {
            Collection<?> collection = UtilGenerics.cast(collectionOrPat);
            if (!collection.isEmpty()) {
                out = containerFactory.get();
                for (Object elem : collection) {
                    if (elem instanceof Pattern) {
                        out.add((Pattern) elem);
                    } else if (elem instanceof String) {
                        out.add(Pattern.compile((String) elem));
                    } else {
                        throw new IllegalArgumentException("Not a Pattern or String: " + (elem != null ? elem.getClass().getName() : null));
                    }
                }
            }
        } else if (collectionOrPat instanceof Pattern) {
            out = containerFactory.get();
            out.add((Pattern) collectionOrPat);
        } else if (collectionOrPat instanceof String) {
            out = containerFactory.get();
            out.add(Pattern.compile((String) collectionOrPat));
        } else {
            throw new IllegalArgumentException("Unknown pattern or pattern collection argument type: " +
                    collectionOrPat.getClass().getName());
        }
        return out;
    }

    public static List<Pattern> getPatterns(Object collectionOrPat) {
        return getPatterns(collectionOrPat, ArrayList::new);
    }

    public static <V> Set<V> excludedSet(Collection<? extends V> in, Collection<? extends V> excludes) {
        Set<V> set = (in != null) ? new LinkedHashSet<>(in) : new LinkedHashSet<>();
        if (excludes != null) {
            set.removeAll(excludes);
        }
        return set;
    }

    public static <V> Set<V> excludedSet(Collection<? extends V> in, V... excludes) {
        return excludedSet(in, Arrays.asList(excludes));
    }

    /**
     * Returns the size of the passed collection or map, or IllegalArgumentException.
     * <p>SCIPIO: 2.1.0: Added.</p>
     */
    public static int size(Object coll) throws IllegalArgumentException {
        if (coll instanceof Collection) {
            return UtilGenerics.<Collection<?>>cast(coll).size();
        } else if (coll instanceof Map) {
            return UtilGenerics.<Map<?, Map>>cast(coll).size();
        } else if (coll == null) {
            throw new IllegalArgumentException("Missing container for size");
        } else {
            throw new IllegalArgumentException("Unknown container type for size: " + coll.getClass().getName());
        }
    }

    /**
     * Returns the size of the passed collection or map, or null instead of exception.
     * <p>SCIPIO: 2.1.0: Added.</p>
     */
    public static Integer sizeOrNull(Object coll) {
        try {
            return size(coll);
        } catch(IllegalArgumentException e) {
            return null;
        }
    }

    /**
     * Collection returns as-is; map returns key set; otherwise IllegalArgumentException.
     * <p>SCIPIO: 2.1.0: Added.</p>
     */
    public static <C extends Collection<?>> C keys(Object coll) throws IllegalArgumentException {
        if (coll instanceof Collection) {
            return UtilGenerics.cast(coll);
        } else if (coll instanceof Map) {
            return UtilGenerics.cast(UtilGenerics.<Map<?, ?>>cast(coll).keySet());
        } else if (coll == null) {
            throw new IllegalArgumentException("Missing container for size");
        } else {
            throw new IllegalArgumentException("Unknown container type for size: " + coll.getClass().getName());
        }
    }

    /**
     * Collection returns as-is; map returns key set; otherwise null.
     * <p>SCIPIO: 2.1.0: Added.</p>
     */
    public static <C extends Collection<?>> C keysOrNull(Object coll) {
        try {
            return keys(coll);
        } catch(IllegalArgumentException e) {
            return null;
        }
    }

    /**
     * Collection returns as-is; map returns key set; otherwise immutable empty.
     * <p>SCIPIO: 2.1.0: Added.</p>
     */
    public static <C extends Collection<?>> C keysOrEmpty(Object coll) {
        try {
            return keys(coll);
        } catch(IllegalArgumentException e) {
            return UtilGenerics.cast(Collections.emptySet());
        }
    }

    /**
     * Collection returns as-is; map returns values; otherwise IllegalArgumentException.
     * <p>SCIPIO: 2.1.0: Added.</p>
     */
    public static <C extends Collection<?>> C values(Object coll) throws IllegalArgumentException {
        if (coll instanceof Collection) {
            return UtilGenerics.cast(coll);
        } else if (coll instanceof Map) {
            return UtilGenerics.cast(UtilGenerics.<Map<?, ?>>cast(coll).values());
        } else if (coll == null) {
            throw new IllegalArgumentException("Missing container for size");
        } else {
            throw new IllegalArgumentException("Unknown container type for size: " + coll.getClass().getName());
        }
    }

    /**
     * Collection returns as-is; map returns values; otherwise null.
     * <p>SCIPIO: 2.1.0: Added.</p>
     */
    public static <C extends Collection<?>> C valuesOrNull(Object coll) {
        try {
            return values(coll);
        } catch(IllegalArgumentException e) {
            return null;
        }
    }

    /**
     * Collection returns as-is; map returns values; otherwise immutable empty.
     * <p>SCIPIO: 2.1.0: Added.</p>
     */
    public static <C extends Collection<?>> C valuesOrEmpty(Object coll) {
        try {
            return values(coll);
        } catch(IllegalArgumentException e) {
            return UtilGenerics.cast(Collections.emptySet());
        }
    }

    /**
     * Specific index lookup for non-lists.
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     */
    public static <T> T getAtIndex(Iterable<?> collection, int index) {
        if (collection instanceof List) {
            return cast(((List<?>) collection).get(index));
        } else {
            Iterator<?> it = collection.iterator();
            Object value = null;
            for (int i = 0; i <= index; i++) {
                value = it.next();
            }
            return cast(value);
        }
    }

    /**
     * Returns the given map key value as string or, if already a string, the string itself.
     *
     * <p>SCIPIO: 3.0.0: Added.</p>
     */
    public static String asStringForKey(Object mapOrValue, Object key) {
        if (mapOrValue instanceof String) {
            return (String) mapOrValue;
        } else if (mapOrValue instanceof Map) {
            Object result = ((Map<?, ?>) mapOrValue).get(key);
            return (result != null) ? result.toString() : null;
        } else if (mapOrValue == null) {
            return null;
        } else {
            throw new IllegalArgumentException("Invalid type: " + mapOrValue.getClass().getName());
        }
    }
}