/* * Copyright (C) 2014 Intel Corporation * All rights reserved. */ package com.intel.mtwilson.patch; import com.fasterxml.jackson.databind.PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.apache.commons.beanutils.PropertyUtils; /** * * @author jbuhacoff */ public class PatchUtil { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(PatchUtil.class); /** * If replaceAttrs contains a key that is not found in the target, it will * be ignored. * @param <T> * @param replaceAttrs a map containing key-value pairs that should be set on the target object * @param target the object which should be updated with the provided key-value pairs in replaceAttrs * @return target as a convenience for chaining method calls but can be ignored since the target parameter is modified * @throws PatchException */ public static <T> T apply(Map<String,Object> replaceAttrs, T target) throws PatchException { try { Map<String,Object> targetAttrs = PropertyUtils.describe(target);// throws IllegalAccessException, InvocationTargetException, NoSuchMethodException // ReverseLowerCaseWithUnderscoresStrategy reverseNamingStrategy = new ReverseLowerCaseWithUnderscoresStrategy(target); for(Map.Entry<String,Object> attr : replaceAttrs.entrySet()) { // there are attributes we skip, like "class" from getClass() if( attr.getKey().equals("class") ) { continue; } // find the corresponding property in the object (reverse of naming strategy) // String key = reverseNamingStrategy.translate(attr.getKey()); // log.debug("patch replace attr {} -> {} value {}", attr.getKey(), key, attr.getValue()); log.debug("patch replace attr {} value {}", attr.getKey(), attr.getValue()); if( targetAttrs.containsKey(attr.getKey()) ) { PropertyUtils.setSimpleProperty(target, attr.getKey(), attr.getValue()); } } return target; // can be ignored by caller since we modify the argument } catch(IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { throw new PatchException(e); } } /** * All keys in replaceAttrs are assumed to exist in the target so if one * is missing an exception will be thrown. * * @param <T> * @param replaceAttrs a map containing key-value pairs that should be set on the target object * @param target the object which should be updated with the provided key-value pairs in replaceAttrs * @return target as a convenience for chaining method calls but can be ignored since the target parameter is modified * @throws PatchException */ public static <T> T applyAll(Map<String,Object> replaceAttrs, T target) throws PatchException { try { // ReverseLowerCaseWithUnderscoresStrategy reverseNamingStrategy = new ReverseLowerCaseWithUnderscoresStrategy(target); for(Map.Entry<String,Object> attr : replaceAttrs.entrySet()) { // there are attributes we skip, like "class" from getClass() if( attr.getKey().equals("class") ) { continue; } log.debug("patch replace attr {} value {}", attr.getKey(), attr.getValue()); // find the corresponding property in the object (reverse of naming strategy) // String key = reverseNamingStrategy.translate(attr.getKey()); PropertyUtils.setSimpleProperty(target, attr.getKey(), attr.getValue()); } return target; // can be ignored by caller since we modify the argument } catch(IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { throw new PatchException(e); } } /** * Returns a "replace" map showing which attributes change from * o1 to o2 such that creating a clone of o1 and applying the diff * map would result in o2. The value of using diff and apply * instead of just copy is if you want to see what are the differences * and possibly let the user approve or reject individual changes. * If you're not using the diff in an intermediate step you can * just use copy(source,target) instead and it will copy all * properties from the source to the target (even if they are null), or * use merge(source,target) to copy all non-null properties from the * source to the target. * * This method assumes the objects are flat -- it does not support * objects having arrays, lists, etc. maybe a future version will. * so currently any object taht is present will replace the previous * value completely, which means changes to arrays or maps require the * full arra/map to be sent */ public static <T> Map<String,Object> diff(T o1, T o2) throws PatchException { try { Map<String,Object> result = new HashMap<>(); Map<String,Object> replaceAttrs = PropertyUtils.describe(o1);// throws IllegalAccessException, InvocationTargetException, NoSuchMethodException for(Map.Entry<String,Object> attr : replaceAttrs.entrySet()) { // there are attributes we skip, like "class" from getClass() if( attr.getKey().equals("class") ) { continue; } Object a1 = PropertyUtils.getSimpleProperty(o1, attr.getKey()); Object a2 = PropertyUtils.getSimpleProperty(o2, attr.getKey()); if( a1 == null && a2 == null ) { continue; } else if( a1 != null && a2 == null ) { result.put(attr.getKey(), null); } else if( a1 == null && a2 != null ) { result.put(attr.getKey(), a2); } else if( a1 != null && a2 != null && !a1.equals(a2)) { result.put(attr.getKey(), a2); } } return result; } catch(IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { throw new PatchException(e); } } /** * Creates a new map instance containing the same key-value pairs as the * input instance but with the keys renamed using the lowercase with underscores * naming strategy * @param input * @return */ public static Map<String,Object> toLowercaseWithUnderscores(Map<String,Object> input) { LowerCaseWithUnderscoresStrategy namingStrategy = new LowerCaseWithUnderscoresStrategy(); HashMap<String,Object> result = new HashMap<>(); for(Map.Entry<String,Object> kv : input.entrySet()) { String translatedKey = namingStrategy.translate(kv.getKey()); result.put(translatedKey, kv.getValue()); } return result; } /** * Returns a "replace" map showing which attributes changes from * o1 to o2 * * This method assumes the objects are flat -- it does not support * objects having arrays, lists, etc. maybe a future version will. * so currently any object that is present will replace the previous * value completely, which means changes to arrays or maps require the * full array/map to be sent * * This function wraps PropertyUtils.describe and excludes the "class" * attribute which ends up in the described object. * */ public static <T> Map<String,Object> toMap(T source) throws PatchException { try { Map<String,Object> result = new HashMap<>(); Map<String,Object> sourceAttrs = PropertyUtils.describe(source);// throws IllegalAccessException, InvocationTargetException, NoSuchMethodException for(Map.Entry<String,Object> attr : sourceAttrs.entrySet()) { // there are attributes we skip, like "class" from getClass() if( attr.getKey().equals("class") ) { continue; } Object value = PropertyUtils.getSimpleProperty(source, attr.getKey()); result.put(attr.getKey(), value); } return result; } catch(IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { throw new PatchException(e); } } /** * Returns a set of key names that have null values in the map * @param map * @return */ public static <T> Set<String> nullValueKeySet(Map<String,T> map) { HashSet<String> nullValueKeys = new HashSet<>(); for(Map.Entry<String,T> attr : map.entrySet()) { if( attr.getValue() == null ) { nullValueKeys.add(attr.getKey()); } } return nullValueKeys; } /** * Modifies the input map by removing keys that have null values * @param map */ public static <T> void removeNullValues(Map<String,T> map) { Set<String> toRemove = nullValueKeySet(map); for(String key : toRemove) { map.remove(key); } } /** * Copies all non-null source properties to the target. * Source and target need not be of the same type; only properties * that are available in the target would be copied from the source. * * @param source * @param target * @throws PatchException */ public static void merge(Object source, Object target) throws PatchException { Map<String,Object> properties = toMap(source); removeNullValues(properties); apply(properties, target); } /** * Copies all null and non-null source properties to the target. * Source and target need not be of the same type; only properties * that are available in the target would be copied from the source. * * @param source * @param target * @throws PatchException */ public static void copy(Object source, Object target) throws PatchException { Map<String,Object> properties = toMap(source); apply(properties, target); } }