/* * Copyright (c) 2015 EMC Corporation * All Rights Reserved */ package com.emc.storageos.api.service.impl.resource.utils; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import com.emc.storageos.db.client.model.DataObject; import com.emc.storageos.db.client.model.Name; import com.emc.storageos.db.client.model.StringMap; import com.emc.storageos.db.client.model.StringSet; import com.emc.storageos.db.client.model.StringSetMap; import com.emc.storageos.db.client.util.NullColumnValueGetter; import com.emc.storageos.svcs.errorhandling.resources.APIException; import com.emc.storageos.svcs.errorhandling.resources.ServiceCode; import com.emc.storageos.svcs.errorhandling.resources.ServiceCodeException; public class DataObjectChangeAnalyzer { // Unfortunately, the UI uses NONE all over the place to mean null private static final String NONE = "NONE"; public static class Change { public String _key; // key (field name) public Object _left; // left object public Object _right; // right object public String name; // plain name of the change public Change(String key, Object left, Object right, String name) { _key = key; _left = left; _right = right; this.name = name; } @Override public String toString() { StringBuffer output = new StringBuffer(_key); output.append(" (source is ").append(_left); output.append(" and target is ").append(_right); output.append(" and name is ").append(name); output.append(")"); return output.toString(); } } private static final String NOT_NULL = "not null"; /** * Returns true if the two Objects are equal, that is: * both null, or both non-null and calling a.equals(b) * returns true. * * @param obj1 Object * @param obj2 Object * @return true iff equal */ private static boolean isEqual(Object obj1, Object obj2) { Object a = obj1; Object b = obj2; /* * For some reason, when UI reset a string field, it set * literal "null" string, instead of null field. * Also, once database set nullable number field to 0, it can not reset * back to null. * Thus, provide workaround check by setting null for value of "null" or 0 */ if (b != null && !(b.toString().contains(" ")) && (NullColumnValueGetter.isNullValue(b.toString()) || (b instanceof String && b.equals(NONE)) || (b instanceof Number && ((Number) b).equals(new Integer(0))))) { b = null; } if (a != null && !(a.toString().contains(" ")) && (NullColumnValueGetter.isNullValue(a.toString()) || (a instanceof String && a.equals(NONE)) || (a instanceof Number && ((Number) a).equals(new Integer(0))))) { a = null; } if (a == null && b == null) { return true; } if (a != null && b != null) { return a.equals(b); } return false; } /** * Makes a Change entry for each key that isn't the same in * the two Stringsets a and b. The Change key is of the form * name.key where name is the annotation name of the Stringset, * and key is a key in the StringSet. * * @param a StringSet * @param b StringSet * @param name - The name annotation of this StringSet, used in key * @param changes */ private static void analyzeStringSets(StringSet a, StringSet b, String name, HashMap<String, Change> changes) { if (a != null) { Iterator<String> iter = a.iterator(); while (iter.hasNext()) { String val = iter.next(); if (b != null && b.contains(val)) { continue; } String key = name + "." + val; Change change = new Change(key, val, null, name); changes.put(key, change); } } if (b != null) { Iterator<String> iter = b.iterator(); while (iter.hasNext()) { String val = iter.next(); if (a != null && a.contains(val)) { continue; } String key = name + "." + val; Change change = new Change(key, null, val, name); changes.put(key, change); } } } /** * Makes a Change entry for each key in StringSet a that isn't in * the StringSet b. The Change key is of the form * name.key where name is the annotation name of the Stringset, * and key is a key in the StringSet. * * @param a StringSet * @param b StringSet * @param name - The name annotation of this StringSet, used in key * @param changes */ private static void analyzeNewStringSetContainsOldStringSetValues(StringSet a, StringSet b, String name, HashMap<String, Change> changes) { if (a != null) { Iterator<String> iter = a.iterator(); while (iter.hasNext()) { String val = iter.next(); if (b != null && b.contains(val)) { continue; } String key = name + "." + val; Change change = new Change(key, val, null, name); changes.put(key, change); } } else if (a == null && b != null) { String key = name; Change change = new Change(key, null, NOT_NULL, name); changes.put(key, change); } } /** * Records differences between StringMaps a and b. * Here the Change key is name.key where name is the annotation name * and key is the StringMap key, and the objects are the values in the StringMap. * * @param a StringMap * @param b StringMap * @param name value in the Name Annotation * @param changes Map of changes being built. */ private static void analyzeStringMaps(StringMap a, StringMap b, String name, HashMap<String, Change> changes) { if (a != null) { for (String key : a.keySet()) { if (b != null && b.containsKey(key) && a.get(key).equals(b.get(key))) { continue; } Object bval = (b != null) ? b.get(key) : null; Change change = new Change(name + "." + key, a.get(key), bval, name); changes.put(change._key, change); } } if (b != null) { for (String key : b.keySet()) { if (a != null && a.containsKey(key) && b.get(key).equals(a.get(key))) { continue; } Object aval = (a != null) ? a.get(key) : null; Change change = new Change(name + "." + key, aval, b.get(key), name); changes.put(change._key, change); } } } /** * Records differences between two StringSetMaps. Here the Change keys * are the name.key where name is the name annotation value and key is * the key in the StringSetMap, and the objects are the StringSets. * * @param a StringSetMap * @param b StringSetMap * @param name Name annotation value * @param changes Change set being generated */ private static void analyzeStringSetMaps(StringSetMap a, StringSetMap b, String name, HashMap<String, Change> changes) { if (a != null) { for (String key : a.keySet()) { if (b != null && b.containsKey(key) && a.get(key).equals(b.get(key))) { continue; } Object bval = (b != null) ? b.get(key) : null; Change change = new Change(name + "." + key, a.get(key), bval, name); changes.put(change._key, change); } } if (b != null) { for (String key : b.keySet()) { if (a != null && a.containsKey(key) && b.get(key).equals(a.get(key))) { continue; } Object aval = (a != null) ? a.get(key) : null; Change change = new Change(name + "." + key, aval, b.get(key), name); changes.put(change._key, change); } } } /** * Scans the methods looking for ones annotated with the Name annotation. * When found (if not excluded), invokes the method on each of the DataObjects * and then compares the results. * * @param left * @param right * @param changes * @param included -- If not null, only fields in included are checked. * @param excluded -- Fields that are excluded are not checked. Must not be null. * @param contained -- If not null, values for fields in contained are checked. */ private static void lookForChanges(DataObject left, DataObject right, HashMap<String, Change> changes, Set<String> included, Set<String> excluded, Set<String> contained) { Class refClass = left.getClass(); Method[] methods = refClass.getMethods(); for (Method method : methods) { boolean contain = false; // We only analyze methods that have the "Name" annotation Name nameAnn = method.getAnnotation(Name.class); if (nameAnn == null) { continue; } String key = nameAnn.value(); // If contained is not null and it contains the key set contain flag to true if (contained != null && contained.contains(key)) { contain = true; }// If included is not null, and does not contain the name, exclude it. else if (included != null && !included.contains(key)) { continue; } // Skip any excluded annotation names if (excluded.contains(key)) { continue; } Class type = method.getReturnType(); try { Object obja = method.invoke(left); Object objb = method.invoke(right); if (type == StringSet.class) { if (contain) { analyzeNewStringSetContainsOldStringSetValues((StringSet) obja, (StringSet) objb, key, changes); } else { analyzeStringSets((StringSet) obja, (StringSet) objb, key, changes); } } else if (type == StringMap.class) { analyzeStringMaps((StringMap) obja, (StringMap) objb, key, changes); } else if (type == StringSetMap.class) { analyzeStringSetMaps((StringSetMap) obja, (StringSetMap) objb, key, changes); } else { if (!isEqual(obja, objb)) { Change change = new Change(key, obja, objb, nameAnn.value()); changes.put(key, change); } } } catch (IllegalAccessException ex) { throw new ServiceCodeException(ServiceCode.UNFORSEEN_ERROR, ex, ex.getMessage(), new String[] {}); } catch (InvocationTargetException ex) { throw new ServiceCodeException(ServiceCode.UNFORSEEN_ERROR, ex, ex.getMessage(), new String[] {}); } } } /** * Analyze the two DataObjects left and right, noting the differences * in a Map of field name (as given by the @Name annotation) * (or fieldName.key) to values for the left and right DataObjects. * Any field names included in the excludedNames array will not be processed. * If the includedNames is non-null, and not empty, *only* includedNames will * be processed. * <p> * This routine works by reflection and calls only the gettrs for fields that are annotated with @Name("xxx"). It understands basic * object types, such as Boolean, String, Integer, and StringSet, StringMap, and StringSetMap. * * @param left DataObject * @param right DataObject * @param includedNames -- if non-null and not empty, only included fields are compared * @param excludedNames -- if non-null, contains fields that should not be compared * @param containNames -- if non-null, contains fields that should be compared for containment. * If anything thats in left but not in right is flagged and fails containment check. * @return A Map of field name (or field name . key) to values for left and right. */ public static Map<String, Change> analyzeChanges( DataObject left, DataObject right, String[] includedNames, String[] excludedNames, String[] containNames) { HashSet<String> included = null; if (includedNames != null && includedNames.length > 0) { included = new HashSet<String>(Arrays.asList(includedNames)); } if (excludedNames == null) { excludedNames = new String[] {}; } HashSet<String> contained = null; if (containNames != null && containNames.length > 0) { contained = new HashSet<String>(Arrays.asList(containNames)); } HashSet<String> excluded = new HashSet<String>(Arrays.asList(excludedNames)); HashMap<String, Change> changes = new HashMap<String, Change>(); if (left.getClass() != right.getClass()) { throw APIException.badRequests.unexpectedClass(left.getClass().getSimpleName(), right .getClass().getSimpleName()); } lookForChanges(left, right, changes, included, excluded, contained); return changes; } }