com.sworddance.util.CUtilities.java Source code

Java tutorial

Introduction

Here is the source code for com.sworddance.util.CUtilities.java

Source

/*
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy
 * of the License at
 *
 *     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 com.sworddance.util;

import static com.sworddance.util.NotNullIterator.newNotNullIterator;
import static org.apache.commons.lang.StringUtils.join;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
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.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Pattern;

import com.sworddance.util.map.MapKeyed;

/**
 * @author patmoore
 *
 */
public class CUtilities {

    /**
     * size of object
     * @param object {@link Map}, {@link Collection}, Array, {@link CharSequence}
     * @return 0 if null
     */
    @SuppressWarnings("unchecked")
    public static int size(Object object) {
        int total = 0;
        if (object != null) {
            if (object instanceof Map) {
                total = ((Map) object).size();
            } else if (object instanceof Collection) {
                total = ((Collection) object).size();
            } else if (object.getClass().isArray()) {
                total = Array.getLength(object);
            } else if (object instanceof CharSequence) {
                total = ((CharSequence) object).length();
            } else if (object instanceof Iterable) {
                Iterator it = ((Iterable) object).iterator();
                while (it.hasNext()) {
                    total++;
                    it.next();
                }
            } else {
                throw new ApplicationIllegalArgumentException(
                        "Unsupported object type: " + object.getClass().getName());
            }
        }
        return total;
    }

    /**
     * Add a value to a collection provided both the collection and the value are not null.
     * @param <T>
     * @param collection
     * @param object
     * @return true if the value was added
     */
    public static <T> boolean add(Collection<T> collection, T object) {
        return object != null && collection != null && collection.add(object);
    }

    /**
     * Add a anotherCollection to a collection provided both the collection and the anotherCollection are not null.
     * @param <T>
     * @param collection
     * @param anotherCollection
     * @return true if the value was added
     */
    public static <T> boolean addAll(Collection<T> collection, Collection<T> anotherCollection) {
        return anotherCollection != null && collection != null && collection.addAll(anotherCollection);
    }

    public static <T> boolean addIfNotContains(Collection<T> collection, T value) {
        if (collection != null && value != null && !collection.contains(value)) {
            return collection.add(value);
        } else {
            return false;
        }
    }

    /**
     * filters out null values
     * @param collection if null then return false
     * @param values
     * @return true collection changed
     */
    public static <T> boolean addAllIfNotContains(Collection<T> collection, T... values) {
        boolean collectionChanged = false;
        if (collection != null && values != null) {
            collectionChanged = addAllIfNotContains(collection, Arrays.asList(values));
        }
        return collectionChanged;
    }

    public static <T> boolean addAllIfNotContains(Collection<T> collection, Collection<T> values) {
        boolean collectionChanged = false;
        if (collection != null) {
            for (T value : NotNullIterator.<T>newNotNullIterator(values)) {
                if (!collection.contains(value)) {
                    collectionChanged |= collection.add(value);
                }
            }
        }
        return collectionChanged;
    }

    /**
     * @param <T>
     * @param collection
     * @param newValues
     * @return true collection changed
     */
    public static <T> boolean addAllNotNull(Collection<T> collection, T... newValues) {
        if (collection != null && newValues != null) {
            return addAllNotNull(collection, Arrays.asList(newValues));
        } else {
            return false;
        }
    }

    public static <T> boolean addAllNotNull(Collection<T> collection, Collection<T> newValues) {
        boolean collectionChanged = false;
        if (collection != null) {
            for (T newValue : NotNullIterator.<T>newNotNullIterator(newValues)) {
                collectionChanged |= collection.add(newValue);
            }
        }
        return collectionChanged;
    }

    /**
     * returns the object at index supplied. returns null if list is null or
     * smaller than the index supplied.
     *
     * @param <T>
     * @param collection
     * @param index
     * @return the index-th item in the collection if collection is a Map, the index-th Map.Entry element is returned.
     */
    @SuppressWarnings("unchecked")
    public static <T> T get(Object collection, int index) {
        if (collection == null) {
            return null;
        } else if (collection instanceof List) {
            List<?> list = (List<?>) collection;
            if (list.size() <= index) {
                return null;
            } else {
                return (T) list.get(index);
            }
        } else if (collection.getClass().isArray()) {
            Object[] list = (Object[]) collection;
            if (list.length <= index) {
                return null;
            } else {
                return (T) list[index];
            }
        } else if (collection instanceof Iterable) {
            int i = 0;
            for (T result : (Iterable<T>) collection) {
                if (i == index) {
                    return result;
                } else {
                    i++;
                }
            }
            return null;
        } else if (collection instanceof Map) {
            return (T) get(((Map) collection).entrySet(), index);
        } else if (index == 0) {
            return (T) collection;
        } else {
            return null;
        }
    }

    @SuppressWarnings("unchecked")
    public static <T> T getFirst(Object collection) {
        return (T) get(collection, 0);
    }

    public static <T> T getFirstNonNull(Object... collection) {
        Object collections;
        switch (collection.length) {
        case 0:
            return null;
        case 1:
            collections = collection[0];
            break;
        default:
            collections = collection;
            break;
        }
        T result = (T) getFirst(collections);
        if (result == null) {
            int size = size(collections);
            for (int i = 1; i < size; i++) {
                result = (T) get(collections, i);
                if (result != null) {
                    break;
                }
            }
        }
        return result;
    }

    /**
     * a universal isEmpty check that can handle arrays, {@link Collection}s, {@link Map}s, {@link CharSequence} or objects.
     * @param object Array, {@link Collection}, {@link Map}s, {@link CharSequence} or object.
     * @return true if the passed object is an array, {@link Collection}, {@link Map}, or {@link CharSequence} that is null or contains no elements.
     * For other objects return true if the object is not null.
     * if the objects is a single object (that is not an array, collection, map) then true is return if the object is null.
     * TODO (Any or All? which is better?)
     */
    public static boolean isEmpty(Object object) {
        if (object == null) {
            return true;
        } else if (object instanceof Map<?, ?>) {
            return ((Map<?, ?>) object).isEmpty();
        } else if (object instanceof Collection<?>) {
            return ((Collection<?>) object).isEmpty();
        } else if (object.getClass().isArray()) {
            return ((Object[]) object).length == 0;
        } else if (object instanceof CharSequence) {
            return ((CharSequence) object).length() == 0;
        } else {
            Method empty;
            try {
                empty = object.getClass().getMethod("isEmpty", new Class<?>[0]);
                return (Boolean) empty.invoke(object);
            } catch (NoSuchMethodException e) {
            } catch (IllegalArgumentException e) {
            } catch (IllegalAccessException e) {
                throw new ApplicationIllegalStateException(e);
            } catch (InvocationTargetException e) {
                throw new ApplicationIllegalStateException(e);
            }
            // singleton object is always "not-empty"
            return false;
        }
    }

    public static boolean isAllEmpty(Object... objects) {
        for (Object object : NotNullIterator.newNotNullIterator(objects)) {
            boolean result = isEmpty(object);
            if (!result) {
                return result;
            }
        }
        return true;
    }

    public static boolean isNotEmpty(Object object) {
        return !isEmpty(object);
    }

    /**
     * Combine multiple arrays into a single array.
     * A ClassCastException will probably result on the return if there is not at least 1 non-null object or array in the objects parameter list.
     * use {@link #combineToSpecifiedClass(Class, Object...)} and supply the expected class for a more certain result.
     * @param <T>
     * @param objects objects of <T> or arrays of <T> objects.
     * @return a single array of <T> objects. nulls are preserved.
     */
    public static <T> T[] combine(Object... objects) {
        Class<T> componentType = null;
        return combineToSpecifiedClass(componentType, objects);
    }

    /**
     * TODO: really should return a List<> but need to know the component type for the toArray to be proper.
     * Example:
     * [ x, [y, x], m] combines to [x,y,x,m]
     * @param <T>
     * @param componentType if null then the component type is attempted to be determined.
     * @param objects objects of <T> or arrays of <T> objects.
     * @return a single array of <T> objects. nulls are preserved.
     */
    public static <T> T[] combineToSpecifiedClass(Class<T> componentType, Object... objects) {
        List<T> list = new ArrayList<T>();
        if (objects != null) {
            if (componentType == null && objects.getClass().getComponentType() != Object.class) {
                componentType = (Class<T>) objects.getClass().getComponentType();
            }

            if (componentType == null) {
                componentType = guessComponentType(objects);
            }

            for (Object object : objects) {
                if (object != null && object.getClass().isArray()) {
                    T[] array = combineToSpecifiedClass(componentType, (Object[]) object);
                    if (array != null) {
                        list.addAll(Arrays.asList(array));
                    }
                } else {
                    list.add((T) object);
                }
            }
        }
        if (componentType == null) {
            // probably will always fail?
            return (T[]) list.toArray();
        } else {
            T[] newArray = (T[]) Array.newInstance(componentType, list.size());
            return list.toArray(newArray);
        }
    }

    private static <T> Class<T> guessComponentType(Object... objects) {
        Class<T> type = null;
        for (Object object : newNotNullIterator(objects)) {
            Class<T> prevType = type;
            if (object.getClass().isArray()) {
                type = (Class<T>) object.getClass().getComponentType();
            } else {
                type = (Class<T>) object.getClass();
            }
            if (prevType != null && !type.isAssignableFrom(prevType)) {
                type = (Class<T>) Object.class;
                break;
            }
        }
        return type;
    }

    /**
     * This is a safe put when using {@link java.util.concurrent.ConcurrentMap} which throw exceptions if key or value is null
     * @param <K>
     * @param <T>
     * @param map if null then nothing happens
     * @param key if null then nothing happens
     * @param value if null then {@link Map#remove(Object)} is called, otherwise
     * @return map.{@link Map#put(Object, Object)}
     */
    public static <K, T> T put(Map<K, T> map, K key, T value) {
        if (map != null && key != null) {
            if (value == null) {
                return map.remove(key);
            } else {
                return map.put(key, value);
            }
        } else {
            return null;
        }
    }

    /**
     * Used when value extends {@link MapKeyed} to add to a map.
     * @param <K>
     * @param <V>
     * @param map may be null.
     * @param value if null then nothing happens ( key to remove is not known )
     * @return {@link #put(Map, Object, Object)}
     */
    public static <K, V extends MapKeyed<K>> V put(Map<K, V> map, V value) {
        if (map == null || value == null) {
            return null;
        } else {
            return put(map, value.getMapKey(), value);
        }
    }

    /**
     * Adds any number of MapKeyed<V> to the map.
     * @param <K>
     * @param <V>
     * @param map map be null.
     * @param values extends V but can't be enforced because generics don't allow for multiple extends bounds when compiler can't enforce that
     * there is only one class specified. (ie. <T extends V & MapKeyed<K>> is not permitted )
     */
    @SuppressWarnings("unchecked")
    public static <K, V extends MapKeyed<K>> void putAll(Map<K, ?> map, Object... values) {
        if (map != null) {
            for (Object value : values) {
                put((Map<K, V>) map, (V) value);
            }
        }
    }

    /**
     * @see #get(Map, Object, Callable)
     * @param <K>
     * @param <T>
     * @param map maybe null
     * @param key maybe null
     * @return null if map or key is null
     */
    public static <K, T> T get(Map<K, T> map, Object key) {
        return get(map, key, (Callable<T>) null);
    }

    /**
     * Get a value from a map. If the value returned is null, then if defaultValue is provided, the {@link Callable#call()} is made and that value is set.
     * If map is {@link ConcurrentMap} then the value supplied by default is set using {@link ConcurrentMap#putIfAbsent(Object, Object)}. Otherwise
     * {@link Map#put(Object, Object)} call is made and the defaultValue-supplied value is returned (and any synchronization issues are handled by the caller).
     * @param <K> key type in map
     * @param <V> value type in map
     * @param map if null then null is returned
     * @param key if null then null is returned
     * @param defaultValue if a {@link ParameterizedCallable} then map and key are passed to {@link ParameterizedCallable#executeCall(Object...)}(map,key)
     * see also {@link com.sworddance.util.AbstractParameterizedCallableImpl}
     * @return the value in the map.
     */
    @SuppressWarnings("unchecked")
    public static <K, V> V get(Map<K, V> map, Object key, Callable<V> defaultValue) {
        V value;
        if (map != null && key != null) {
            value = map.get(key);
        } else {
            value = null;
        }
        if (value == null && defaultValue != null) {
            V callValue;
            try {
                if (defaultValue instanceof ParameterizedCallable<?>) {
                    callValue = ((ParameterizedCallable<V>) defaultValue).executeCall(map, key);
                } else {
                    callValue = defaultValue.call();
                }
            } catch (RuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new ApplicationGeneralException(e);
            }
            if (callValue != null && map != null && key != null) {
                if (map instanceof ConcurrentMap<?, ?>) {
                    ((ConcurrentMap<K, V>) map).putIfAbsent((K) key, callValue);
                } else {
                    map.put((K) key, callValue);
                }
                // another thread may beat us to assigning the value.
                value = map.get(key);
            } else {
                value = callValue;
            }
        }
        return value;
    }

    /**
     * Same as  {@link #get(Map, Object, Callable)} but handles creating a {@link Callable} to wrap the defaultValue.
     * @param <K>
     * @param <V>
     * @param map
     * @param key
     * @param defaultValue
     * @return the value in the map.
     */
    public static <K, V> V get(Map<K, V> map, K key, final V defaultValue) {
        return get(map, key, new Callable<V>() {
            public V call() {
                return defaultValue;
            }
        });
    }

    public static <T> Set<T> asSet(T... values) {
        LinkedHashSet<T> set = new LinkedHashSet<T>();
        addAllNotNull(set, values);
        return set;
    }

    /**
     * Converts object to a list.
     *
     * @param <T>
     * @param object
     * @return null if object is null, object if object is list, new list if object is another collection, list of Map.Entry if object is a map.
     * other wise result of Arrays.asList
     */
    @SuppressWarnings("unchecked")
    public static <T> List<T> convertToList(Object object) {
        List<T> result;
        if (object == null) {
            result = null;
        } else if (object instanceof List) {
            result = (List) object;
        } else if (object.getClass().isArray()) {
            result = Arrays.asList((T[]) object);
        } else if (object instanceof Collection) {
            return new ArrayList<T>((Collection) object);
        } else if (object instanceof Map) {
            return new ArrayList<T>(((Map) object).entrySet());
        } else {
            result = Arrays.asList((T) object);
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    public static <T extends Collection<?>> T cloneCollection(T cloned) {
        Collection result = null;
        if (cloned != null) {
            try {
                result = cloned.getClass().newInstance();
            } catch (InstantiationException e) {
                throw new ApplicationGeneralException(e);
            } catch (IllegalAccessException e) {
                throw new ApplicationGeneralException(e);
            }
            result.addAll(cloned);
        }
        return (T) result;
    }

    /**
     * @param newValues
     * @param <T>
     * @return an new {@link List} populated with the non-null values in newValues
     */
    public static <T> List<T> newList(T... newValues) {
        List<T> l = new ArrayList<T>();
        addAllNotNull(l, newValues);
        return l;
    }

    /**
     * Create a map from alternating keys and values. if a key is null then it (and its
     * corresponding value) are not placed in the map.
     *
     * @param <K>
     * @param <V>
     * @param keysAndValues
     * @return a Map<K,V> of the values.
     */
    public static <K, V> Map<K, V> createMap(Object... keysAndValues) {
        return createMap(false, keysAndValues);
    }

    /**
     * Same as {@link #createMap(Object...)} but also skips pairs with null values.
     *
     * @param <K>
     * @param <V>
     * @param keysAndValues
     * @return map with no null keys or values.
     */
    public static <K, V> Map<K, V> createMapSkipNullValues(Object... keysAndValues) {
        return createMap(true, keysAndValues);
    }

    @SuppressWarnings("unchecked")
    private static <K, V> Map<K, V> createMap(boolean skipNullValues, Object... keysAndValues) {
        Map<K, V> map = new LinkedHashMap<K, V>();
        if (keysAndValues != null && keysAndValues.length != 0) {
            if (keysAndValues.length % 2 != 0) {
                throw new ApplicationIllegalStateException(
                        "Non-even number of parameters to createMap. Need matched set of keys and values. got=",
                        join(keysAndValues, ","));
            }
            for (int i = 0; i < keysAndValues.length; i += 2) {
                if (keysAndValues[i] != null && (!skipNullValues || keysAndValues[i + 1] != null)) {
                    map.put((K) keysAndValues[i], (V) keysAndValues[i + 1]);
                }
            }
        }
        return map;
    }

    /**
     * conceptually equivalent to masterCollection.clear(); masterCollection.addAll(newValues);
     *
     * except that the masterCollection is only modified to the extent needed to bring it into compliance.
     * Useful for avoiding unnecessary db operations.
     * compare the values in the newValues Collection to the masterCollection.
     * @param <T>
     * @param masterCollection
     * @param newValues
     * @return true if a change was made
     */
    public static <T> boolean updateCollectionAsNeeded(Collection<T> masterCollection, Collection<T> newValues) {
        boolean changed = false;
        // avoid updating with self.
        if (masterCollection != newValues) {
            if (isEmpty(newValues)) {
                if (isNotEmpty(masterCollection)) {
                    masterCollection.clear();
                    changed = true;
                }
            } else {
                // may be copying directly from another envelope
                List<T> remainingValues = new ArrayList<T>(newValues);
                // remove topics that are no longer present.
                for (Iterator<T> iterator = masterCollection.iterator(); iterator.hasNext();) {
                    T existingObjectInMasterCollection = iterator.next();
                    if (!remainingValues.contains(existingObjectInMasterCollection)) {
                        iterator.remove();
                        changed = true;
                    } else {
                        remainingValues.remove(existingObjectInMasterCollection);
                    }
                }
                // add any remaining objects that are actually new.
                masterCollection.addAll(remainingValues);
                changed = changed || !remainingValues.isEmpty();
            }
        }
        return changed;
    }

    /**
     * because {@link Collections#reverse(List)} does not return the list.
     * @param <T>
     * @param list
     * @return list
     */
    public static <T extends List<?>> T reverse(T list) {
        Collections.reverse(list);
        return list;
    }

    public static <T> List<T> reverse(T... elements) {
        List<T> list = Arrays.asList(elements);
        Collections.reverse(list);
        return list;
    }

    public static <T> Class<? extends T> getClassIfPossible(String className) {
        Class<? extends T> clazz = null;
        try {
            clazz = (Class<? extends T>) Class.forName(className);
        } catch (ClassNotFoundException e) {
            // we are quiet on purpose - may be should log
        }
        return clazz;
    }

    public static <T> void removeIfNotPresent(Collection<T> collection, Collection<T> permitted) {
        for (Iterator<T> iter = collection.iterator(); iter.hasNext();) {
            T element = iter.next();
            if (!permitted.contains(element)) {
                iter.remove();
            }
        }
    }

    public static Class<?> getClassSafely(Object... objects) {
        if (objects != null) {
            for (Object object : objects) {
                if (object != null) {
                    return object.getClass();
                }
            }
        }
        return null;
    }

    public static Pattern onlyPattern(String regex) {
        return Pattern.compile("^" + regex + "$", Pattern.CASE_INSENSITIVE);
    }

    public static Pattern withinPattern(String regex) {
        // terminator is reluctant so that non-alphanumerics that end regex will still get matched.
        return Pattern.compile("\\b" + regex + "\\b??", Pattern.CASE_INSENSITIVE);
    }

    /**
     * create pattern to search for equivalent javascript. Specifically,
     * <ul><li>All punctuation characters are escaped and leading/trailing spaces are allowed
     * <li>Alnum whitespace Alnum is change to require only a single whitespace ( handles cases like the space between var and ga in : "var ga = []; ")
     * </ul>
     * @param jsScriptStr
     * @return string to supply to {@link Pattern#compile(String)}
     */
    public static String jsQuoteForPattern(String jsScriptStr) {
        String escapeAllPunctuationChars = jsScriptStr.replaceAll("(?:\\s*([\\p{Punct}])\\s*)",
                "\\\\s*\\\\$1\\\\s*");

        String requireAtLeast1WsBetweenWords = escapeAllPunctuationChars.replaceAll("(\\p{Alnum})\\s+(\\p{Alnum})",
                "$1\\\\s+$2");
        String simplifyWsMatching = requireAtLeast1WsBetweenWords.replaceAll("(?:\\Q\\s*\\E)+", "\\\\s*");
        return simplifyWsMatching;
    }

    /**
     * Create a search path list containing:
     * [ fileName, /fileName, /{eachdir}/fileName, /META-INF/fileName, /META-INF/{eachdir}/fileName ]
     * @return a list of locations to look for the file supplied.
     */
    public static List<String> createSearchPath(String fileName, String... alternateDirectories) {
        List<String> searchPath = new ArrayList<String>();
        searchPath.add(fileName);
        String adjustedFilename;
        if (!fileName.startsWith("/")) {
            searchPath.add("/" + fileName);
            adjustedFilename = fileName;
        } else {
            adjustedFilename = fileName.substring(1);
        }
        List<String> workingDirectories = new ArrayList<String>();
        if (isNotEmpty(alternateDirectories)) {
            workingDirectories.addAll(Arrays.asList(alternateDirectories));
        }
        List<String> alternates = new ArrayList<String>();
        for (String alternateRoot : new String[] { "", "META-INF/" }) {
            alternates.add(alternateRoot);
            for (String a : workingDirectories) {
                String full;
                if (a.endsWith("/")) {
                    full = alternateRoot + a;
                } else {
                    full = alternateRoot + a + "/";
                }
                alternates.add(full);
            }
        }
        for (String alternateDirectory : alternates) {
            if (!adjustedFilename.startsWith(alternateDirectory)) {
                searchPath.add("/" + alternateDirectory + adjustedFilename);
            }
        }
        return searchPath;
    }

    public static InputStream getResourceAsStream(Object searchRoot, String fileName,
            String... alternateDirectories) {
        return getResourceAsStream(searchRoot, fileName, false, alternateDirectories);
    }

    public static InputStream getResourceAsStream(Object searchRoot, String fileName, boolean optional,
            String... alternateDirectories) {
        List<String> searchPaths = createSearchPath(fileName, alternateDirectories);
        return getResourceAsStream(searchRoot, fileName, optional, searchPaths);
    }

    public static InputStream getResourceAsStream(Object searchRoot, boolean optional, List<String> searchPaths) {
        return getResourceAsStream(searchRoot, null, optional, searchPaths);
    }

    private static InputStream getResourceAsStream(Object searchRoot, String fileName, boolean optional,
            List<String> searchPaths) {
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        for (String searchPath : searchPaths) {
            InputStream resource = null;
            if (searchRoot != null) {
                resource = searchRoot.getClass().getResourceAsStream(searchPath);
            }

            if (resource == null && contextClassLoader != null) {
                resource = contextClassLoader.getResourceAsStream(searchPath);
            }
            if (resource == null) {
                resource = ClassLoader.getSystemResourceAsStream(searchPath);
            }
            if (resource != null) {
                return resource;
            }
        }
        if (!optional) {
            if (fileName != null) {
                throw new ApplicationNullPointerException(fileName, " not found in ", join(searchPaths, ","),
                        " java.class.path=", System.getProperty("java.class.path"), " java.library.path=",
                        System.getProperty("java.library.path"), " searchRoot =", getClassSafely(searchRoot));
            } else {
                throw new ApplicationNullPointerException("No listed file found ", join(searchPaths, ","),
                        " java.class.path=", System.getProperty("java.class.path"), " java.library.path=",
                        System.getProperty("java.library.path"), " searchRoot =", getClassSafely(searchRoot));
            }
        } else {
            return null;
        }
    }

    public static Collection<URL> getResources(Object searchRoot, String fileName, String... alternateDirectories) {
        return getResources(searchRoot, fileName, false, alternateDirectories);
    }

    public static Collection<URL> getResources(Object searchRoot, String fileName, boolean optional,
            String... alternateDirectories) {
        List<String> searchPaths = createSearchPath(fileName, alternateDirectories);
        return getResources(searchRoot, fileName, optional, searchPaths);
    }

    public static Collection<URL> getResources(Object searchRoot, boolean optional, List<String> searchPaths) {
        return getResources(searchRoot, null, optional, searchPaths);
    }

    /**
     * @param searchRoot
     * @param fileName
     * @param optional
     * @param searchPaths
     * @return a de-duped Enumeration<URL> never returns null.
     */
    private static Collection<URL> getResources(Object searchRoot, String fileName, boolean optional,
            List<String> searchPaths) {
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        ClassLoader searchRootClassLoader = searchRoot != null ? searchRoot.getClass().getClassLoader() : null;
        HashMap<String, URL> results = new HashMap<String, URL>();
        for (String searchPath : searchPaths) {
            Enumeration<URL> resource = null;
            if (searchRootClassLoader != null) {
                try {
                    resource = searchRootClassLoader.getResources(searchPath);
                    for (URL url : NotNullIterator.<URL>newNotNullIterator(resource)) {
                        results.put(url.toString(), url);
                    }
                } catch (IOException e) {
                    // TODO what?
                }
            }
            if (contextClassLoader != null) {
                try {
                    resource = contextClassLoader.getResources(searchPath);
                    for (URL url : NotNullIterator.<URL>newNotNullIterator(resource)) {
                        results.put(url.toString(), url);
                    }
                } catch (IOException e) {
                    // TODO what?
                }
            }
            if (resource == null) {
                try {
                    resource = ClassLoader.getSystemResources(searchPath);
                    for (URL url : NotNullIterator.<URL>newNotNullIterator(resource)) {
                        results.put(url.toString(), url);
                    }
                } catch (IOException e) {
                    // TODO what?
                }
            }
        }
        if (isEmpty(results) && !optional) {
            if (fileName != null) {
                throw new ApplicationNullPointerException(fileName, " not found in ", join(searchPaths, ","),
                        " java.class.path=", System.getProperty("java.class.path"), " java.library.path=",
                        System.getProperty("java.library.path"), " searchRoot =", getClassSafely(searchRoot));
            } else {
                throw new ApplicationNullPointerException("No listed file found ", join(searchPaths, ","),
                        " java.class.path=", System.getProperty("java.class.path"), " java.library.path=",
                        System.getProperty("java.library.path"), " searchRoot =", getClassSafely(searchRoot));
            }
        } else {
            return results.values();
        }
    }

    public static <K, V extends Collection<W>, W> Map<K, V> merge(Map<K, V> first, Map<K, V> second) {
        Map<K, V> result = new HashMap<K, V>();
        if (isNotEmpty(first) || isNotEmpty(second)) {
            if (isEmpty(first)) {
                result.putAll(second);
            } else if (isEmpty(second)) {
                result.putAll(first);
            } else {
                result.putAll(first);
                Set<Map.Entry<K, V>> secondMapEntrySet = second.entrySet();
                for (Map.Entry<K, V> entry : secondMapEntrySet) {
                    K key = entry.getKey();
                    if (result.containsKey(key)) {
                        V resultValue = result.get(key);
                        resultValue.addAll(entry.getValue());
                    } else {
                        result.put(key, entry.getValue());
                    }
                }
            }
        }
        return result;
    }
}