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}