001package com.identityworksllc.iiq.common;
002
003import com.identityworksllc.iiq.common.annotation.CoreStable;
004import org.apache.commons.logging.Log;
005import org.apache.commons.logging.LogFactory;
006
007import java.util.Collection;
008import java.util.Date;
009import java.util.HashSet;
010import java.util.Map;
011import java.util.stream.Collectors;
012
013/**
014 * Utility methods for detecting whether two objects are the same, since IIQ is inconsistent about it.
015 * The primary method in here is {@link Sameness#isSame(Object, Object, boolean)}. This class
016 * is heavily used throughout the IIQCommon libraries as well as the IDW plugins.
017 *
018 * @author Devin Rosenbauer
019 * @author Instrumental Identity
020 */
021@CoreStable
022public class Sameness {
023    private static final Log log = LogFactory.getLog(Sameness.class);
024
025    /**
026     * Populates two HashSet collections with the appropriate values, taking into
027     * account case sensitivity, and then returns true if they are the same.
028     *
029     * @param ignoreCase True if the comparison should ignore case
030     * @param newSet The set containing 'new' values
031     * @param oldSet The set containing 'old' values
032     * @return True if the values are the same
033     */
034    private static boolean checkSets(boolean ignoreCase, HashSet<Object> newSet, HashSet<Object> oldSet) {
035        if (ignoreCase) {
036            newSet = newSet.stream().map(e -> String.valueOf(e).toUpperCase()).collect(Collectors.toCollection(HashSet::new));
037            oldSet = oldSet.stream().map(e -> String.valueOf(e).toUpperCase()).collect(Collectors.toCollection(HashSet::new));
038        }
039
040        return newSet.equals(oldSet);
041    }
042
043    /**
044     * Returns true if the given thing is empty in the isSame() sense, i.e.,
045     * if it ought to be the same as null. These are values that are often
046     * optimized out by SailPoint when serializing to XML.
047     *
048     * A thing is empty if it is null or is an empty string, array, collection,
049     * or map. Boolean false and integer 0 are also empty.
050     *
051     * All other values are not empty.
052     *
053     * @param thing The thing to check for emptiness
054     * @return True if the thing is empty; false otherwise
055     */
056    public static boolean isEmpty(Object thing) {
057        if (thing == null) {
058            return true;
059        }
060        if (thing instanceof String) {
061            return thing.equals("");
062        } else if (thing instanceof Boolean) {
063            return !((Boolean)thing);
064        } else if (thing instanceof Number) {
065            // I am questioning this. If this causes problems, it can be removed.
066            int intValue = ((Number) thing).intValue();
067            return (intValue == 0);
068        } else if (thing.getClass().isArray()) {
069            assert thing instanceof Object[];
070            return ((Object[])thing).length == 0;
071        } else if (thing instanceof Collection) {
072            return ((Collection<?>) thing).isEmpty();
073        } else if (thing instanceof Map) {
074            return ((Map<?, ?>) thing).isEmpty();
075        }
076        return false;
077    }
078
079    /**
080     * A typo-friendly inversion of {@link #isSame(Object, Object, boolean)}.
081     *
082     * @param newValue The new value (can be null)
083     * @param oldValue The old value (can be null)
084     * @param ignoreCase True if strings and collections should be compared ignoring case. Maps are always compared case-insensitively.
085     * @return True if the values are NOT "the same" according to our definition
086     */
087    public static boolean isNotSame(final Object newValue, final Object oldValue, boolean ignoreCase) {
088        return !isSame(newValue, oldValue, ignoreCase);
089    }
090
091    /**
092     * Decide whether the two inputs are the same.
093     *
094     * This can be an expensive check and so should be used in concert with existing .equals(), e.g. `o1.equals(o2) || isSame(o1, o2)`.
095     *
096     * 1) Type differences: If the two values are a String and a Boolean (or a String and a Number), but will be stored the same way by Hibernate, they are the same
097     * 2) Null and empty: Null is the same as any empty object (strings, lists, maps, boolean false)
098     * 3) Dates and Longs: If one value is a long and one is a Date, they are the same if {@link Date#getTime()} equals the long value
099     * 4) Collections: Two collections are the same if they have equal elements in any order. If ignoreCase is true, elements will be converted to strings and compared case-insensitively.
100     * 5) String case: Two strings will be compared case-insensitive if the flag is passed as true
101     * 6) String vs. Collection case: A string is the same as collection containing only that string
102     *
103     * @param newValue The new value (can be null)
104     * @param oldValue The old value (can be null)
105     * @param ignoreCase True if strings and collections should be compared ignoring case. Maps are always compared case-insensitively.
106     * @return True if the values are "the same" according to our definition
107     */
108    public static boolean isSame(final Object newValue, final Object oldValue, boolean ignoreCase) {
109        if (log.isTraceEnabled()) {
110            log.trace("isSame() called with: newValue = [" + newValue + "], oldValue = [" + oldValue + "], ignoreCase = [" + ignoreCase + "]");
111        }
112        if (newValue == null || oldValue == null) {
113            return isEmpty(newValue) && isEmpty(oldValue);
114        } else if (newValue == oldValue) {
115            return true;
116        } else if (newValue instanceof Boolean && oldValue instanceof Boolean) {
117            return newValue.equals(oldValue);
118        } else if (newValue instanceof String && oldValue instanceof String) {
119            if (ignoreCase) {
120                return ((String) newValue).equalsIgnoreCase((String) oldValue);
121            }
122            return newValue.equals(oldValue);
123        } else if (newValue instanceof Date && oldValue instanceof Long) {
124            Date oldDate = new Date((Long)oldValue);
125            return newValue.equals(oldDate);
126        } else if (newValue instanceof Long && oldValue instanceof Date) {
127            Date newDate = new Date((Long) newValue);
128            return oldValue.equals(newDate);
129        } else if (newValue.getClass().isArray() && isEmpty(newValue)) {
130            return isEmpty(oldValue);
131        } else if (oldValue.getClass().isArray() && isEmpty(oldValue)) {
132            return isEmpty(newValue);
133        } else if (newValue instanceof Collection && oldValue instanceof Collection) {
134            HashSet<Object> newSet = new HashSet<>((Collection<?>) newValue);
135            HashSet<Object> oldSet = new HashSet<>((Collection<?>) oldValue);
136            return checkSets(ignoreCase, newSet, oldSet);
137        } else if (newValue instanceof Map && oldValue instanceof Map) {
138            return newValue.equals(oldValue);
139        } else if (newValue instanceof String && oldValue instanceof Collection) {
140            HashSet<Object> newSet = new HashSet<>();
141            HashSet<Object> oldSet = new HashSet<>((Collection<?>)oldValue);
142            newSet.add(newValue);
143            return checkSets(ignoreCase, newSet, oldSet);
144        } else if (newValue instanceof Collection && oldValue instanceof String) {
145            HashSet<Object> newSet = new HashSet<>((Collection<?>) newValue);
146            HashSet<Object> oldSet = new HashSet<>();
147            oldSet.add(oldValue);
148            return checkSets(ignoreCase, newSet, oldSet);
149        } else {
150            String ns = String.valueOf(newValue);
151            String os = String.valueOf(oldValue);
152            if (ignoreCase) {
153                return ns.equalsIgnoreCase(os);
154            } else {
155                return ns.equals(os);
156            }
157        }
158    }
159}