package com.redhat.lightblue.migrator.consistency; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.sql.Time; import java.sql.Timestamp; import java.util.AbstractCollection; import java.util.Date; import java.util.Objects; import org.apache.commons.beanutils.PropertyUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Checks beans for consistency, field by field. Using the checker inside equals method will provide * support for containers and arrays. Behavior: * <ol> * <li>If there is a type mismatch, beans are not consistent.</li> * <li>If both beans are null, beans are consistent.</li> * <li>Fields are compared using <code>Objects.equals(f1, f2)</code>.</li> * <li>Containers and arrays are compared using <code>Objects.equals(f1, f2)</code>.</li> * <li>Use {@link ConsistencyCheck} annotation to ignore certain fields when doing the consistency check.</li> * </ol> * * @author mpatercz * */ public class BeanConsistencyChecker { private static final Logger logger = LoggerFactory.getLogger(BeanConsistencyChecker.class); private boolean logInconsistenciesAsWarnings = true; public BeanConsistencyChecker() {} public BeanConsistencyChecker(boolean logWarnings) { super(); this.logInconsistenciesAsWarnings = logWarnings; } public boolean consistent(final Object o1, final Object o2) { if (logger.isDebugEnabled()) logger.debug("Checking object1="+o1+" against object2="+o2); if (o1 == null && o2 == null) return true; if (o1 == null && o2 != null) return false; for (Field field : o1.getClass().getDeclaredFields()) { if (consistencyCheckRequired(field)) { if (o2 == null) { return false; } if (!o1.getClass().isInstance(o2) || !o2.getClass().isInstance(o1)) { logger.debug("Types do not match"); return false; } if (o1 instanceof Object[] || o1 instanceof AbstractCollection) { logger.debug("This is not a bean, it is a container. Performing standard equals check"); return Objects.equals(o1, o2); } // get value of object 1 Object o1Value; try { o1Value = PropertyUtils.getSimpleProperty(o1, field.getName()); } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { if (logger.isDebugEnabled()) logger.debug("Can't access "+field.getName()+" on object 1. Ignoring."); continue; } // get value of object 2 Object o2Value; try { o2Value = PropertyUtils.getSimpleProperty(o2, field.getName()); } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { if (logger.isDebugEnabled()) logger.debug("Can't access "+field.getName()+" on object 2. Objects are inconsistent."); return false; } // compare values if (o1Value instanceof Date && o2Value instanceof Date) { if (!Objects.equals(((Date)o1Value).getTime(), ((Date)o2Value).getTime())) { logInconsistency(o1.getClass().getSimpleName()+" objects have "+field.getName()+" field inconsistent (checked java.sql.Timestamp against java.util.Date, ignoring nanoseconds)"); return false; } } else { if (!Objects.equals(o1Value, o2Value)) { logInconsistency(o1.getClass().getSimpleName()+" objects have "+field.getName()+" field inconsistent"); return false; } } } } logger.debug("Objects are consistent"); return true; } private boolean consistencyCheckRequired(Field field) { // java 8 has field.getDeclaredAnnotation(clazz) ConsistencyCheck consistencyCheck = null; for (Annotation a: field.getDeclaredAnnotations()) { if (a instanceof ConsistencyCheck) { consistencyCheck = (ConsistencyCheck) a; } } if (consistencyCheck == null) { // check consistency by default return true; } if (consistencyCheck.ignore()) { if(logger.isDebugEnabled()) logger.debug("Ignoring "+field.getName()); return false; } return true; } private void logInconsistency(String message) { if (logInconsistenciesAsWarnings) { logger.warn(message); } else { logger.debug(message); } } private static BeanConsistencyChecker beanConsistencyChecker = new BeanConsistencyChecker(); public static BeanConsistencyChecker getInstance() { return beanConsistencyChecker; } }