/******************************************************************************* * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved. * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * Contributors: * Oracle - initial API and implementation from Oracle TopLink ******************************************************************************/ package org.eclipse.persistence.tools.workbench.utility.diff; import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.ArrayList; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.eclipse.persistence.tools.workbench.utility.CollectionTools; import org.eclipse.persistence.tools.workbench.utility.io.IndentingPrintWriter; import org.eclipse.persistence.tools.workbench.utility.string.StringTools; /** * Given a pair of object graphs to compare, use a variety of user- * specified differentiators to compare the objects in the two graphs. * The differentiators are determined by the classes of the objects * being compared. * * The diff engine logs all of its diffs. By default this log does nothing; * but a client-supplied log can be used to debug any unexpected diffs. */ public class DiffEngine implements Differentiator { /** * This is the differentiator passed to the reflective differentiators * for comparing field values. */ private final RecordingDifferentiator recordingDifferentiator; /** * These are the user-supplied differentiators, keyed by class. * This also holds the default differentiator at the key Object.class. */ private final Map userDifferentiators; /** * This is a temporary cache of the reflective differentiators that * is built up during a diff, keyed by the class of the * objects being compared. * It is cleared out at the end of the diff. */ private final Map reflectiveDifferentiatorCache; /** * This is a temporary cache of the differentiators that * is built up during a diff, keyed by the class of the * objects being compared. * It is cleared out at the end of the diff. */ private final Map differentiatorCache; /** * This flag prevents the same thread from re-starting * the diff engine while it is currently executing a diff. */ private boolean diffInProgress; /** * All the diffs are logged here. By default this is a "null" log. */ private Log log; private static final Class OBJECT_CLASS = Object.class; private static final String[] EMPTY_STRING_ARRAY = new String[0]; // ********** constructors/initialization ********** public DiffEngine() { super(); this.recordingDifferentiator = new RecordingDifferentiator(); this.userDifferentiators = new HashMap(); // the "equality" differentiator works best with primitives, strings, etc. this.userDifferentiators.put(OBJECT_CLASS, EqualityDifferentiator.instance()); this.reflectiveDifferentiatorCache = new HashMap(); this.differentiatorCache = new HashMap(); this.diffInProgress = false; this.log = Log.NULL_INSTANCE; } // ********** Differentiator implementation ********** /** * synchronized so diffs are single-threaded * @see Differentiator#diff(Object, Object) */ public synchronized Diff diff(Object object1, Object object2) { return this.diff(object1, object2, DifferentiatorAdapter.NORMAL); } /** * synchronized so diffs are single-threaded * @see Differentiator#keyDiff(Object, Object) */ public synchronized Diff keyDiff(Object object1, Object object2) { return this.diff(object1, object2, DifferentiatorAdapter.KEY); } private Diff diff(Object object1, Object object2, DifferentiatorAdapter adapter) { this.setUp(); Diff diff = adapter.diff(this.recordingDifferentiator, object1, object2); this.tearDown(); return diff; } /** * @see Differentiator#comparesValueObjects() */ public boolean comparesValueObjects() { return false; } // ********** queries ********** Differentiator differentiatorFor(Object object) { return this.differentiatorForClass((object == null) ? OBJECT_CLASS : object.getClass()); } private Differentiator differentiatorForClass(Class javaClass) { // first look for a cached differentiator Differentiator differentiator = (Differentiator) this.differentiatorCache.get(javaClass); if (differentiator != null) { return differentiator; } // then look for a user differentiator for the class or its superclass Class tempClass = javaClass; while (tempClass != null) { differentiator = (Differentiator) this.userDifferentiators.get(tempClass); if (differentiator != null) { if (differentiator instanceof ReflectiveDifferentiator) { // if we find a reflective differentiator higher in the hierarchy, // extend it down to the current class this.expandReflectiveDifferentiatorCache(javaClass, differentiator.comparesValueObjects()); return (Differentiator) this.differentiatorCache.get(javaClass); } this.differentiatorCache.put(javaClass, differentiator); return differentiator; } tempClass = tempClass.getSuperclass(); } // this shouldn't happen - we should always get a hit on Object.class... throw new IllegalStateException("missing differentiator: " + javaClass.getName()); } public synchronized Differentiator getUserDifferentiator(Class javaClass) { return (Differentiator) this.userDifferentiators.get(javaClass); } public synchronized Differentiator getDefaultDifferentiator() { return (Differentiator) this.userDifferentiators.get(OBJECT_CLASS); } public synchronized Differentiator getRecordingDifferentiator() { return this.recordingDifferentiator; } // ********** behavior ********** private void setUp() { if (this.diffInProgress) { throw new IllegalStateException("Recursive calls to a DiffEngine are not allowed."); } this.diffInProgress = true; this.checkUserReflectiveDifferentiators(); this.expandReflectiveDifferentiatorCache(); this.recordingDifferentiator.setUp(); } /** * verify that none of the user-supplied reflective differentiators * are beneath a user-supplied non-reflective differentiator in * the class hierarchy; also copy the reflective differentiators * to the reflective differentiator cache and the non-reflective * differentiators to the differentiator cache */ private void checkUserReflectiveDifferentiators() { for (Iterator stream = this.userDifferentiators.entrySet().iterator(); stream.hasNext(); ) { Map.Entry entry = (Map.Entry) stream.next(); Class javaClass = (Class) entry.getKey(); Differentiator differentiator = (Differentiator) entry.getValue(); if (differentiator instanceof ReflectiveDifferentiator) { this.checkUserReflectiveDifferentiator(javaClass.getSuperclass()); this.reflectiveDifferentiatorCache.put(javaClass, differentiator); } else { this.differentiatorCache.put(javaClass, differentiator); } } } /** * verify that all the user-specified differentiators for the superclasses * of the specified class are reflective (excepting Object.class) */ private void checkUserReflectiveDifferentiator(Class javaClass) { if (javaClass == OBJECT_CLASS) { return; // Object.class is a special case and is ignored } Differentiator differentiator = (Differentiator) this.userDifferentiators.get(javaClass); if ((differentiator == null) || (differentiator instanceof ReflectiveDifferentiator)) { this.checkUserReflectiveDifferentiator(javaClass.getSuperclass()); // recurse } else { throw new IllegalStateException("The differentiator for " + javaClass.getName() + " must be reflective: " + differentiator); } } /** * go through the reflective differentiator cache, adding * reflective differentiators for any superclasses that don't * have one already */ private void expandReflectiveDifferentiatorCache() { // clone the map because we are going to modify it while we are looping over its entries for (Iterator stream = new HashMap(this.reflectiveDifferentiatorCache).entrySet().iterator(); stream.hasNext(); ) { Map.Entry entry = (Map.Entry) stream.next(); Class javaClass = (Class) entry.getKey(); boolean comparesValueObjects = ((ReflectiveDifferentiator) entry.getValue()).comparesValueObjects(); this.expandReflectiveDifferentiatorCache(javaClass, comparesValueObjects); } } private void expandReflectiveDifferentiatorCache(Class javaClass, boolean comparesValueObjects) { if (javaClass == OBJECT_CLASS) { return; // Object.class is a special case and is ignored } ReflectiveDifferentiator rd = (ReflectiveDifferentiator) this.reflectiveDifferentiatorCache.get(javaClass); if (rd == null) { rd = new ReflectiveDifferentiator(javaClass, this.recordingDifferentiator); rd.setComparesValueObjects(comparesValueObjects); this.reflectiveDifferentiatorCache.put(javaClass, rd); } this.expandReflectiveDifferentiatorCache(javaClass.getSuperclass(), comparesValueObjects); // recurse // once all the superclass reflective differentiators are in place // build the composite differentiator and put it in the cache this.setUpDifferentiatorCache(javaClass); } /** * gather up all the differentiators for the specified class; * there should be one reflective differentiator per class in the * class's hierarchy (except for Object.class) - add them if * necessary; */ private void setUpDifferentiatorCache(Class javaClass) { if (this.differentiatorCache.get(javaClass) != null) { return; } List differentiators = new ArrayList(); for (Class tempClass = javaClass; tempClass != OBJECT_CLASS; tempClass = tempClass.getSuperclass()) { differentiators.add(this.reflectiveDifferentiatorCache.get(tempClass)); } Differentiator differentiator = null; if (differentiators.size() == 1) { differentiator = (Differentiator) differentiators.get(0); } else { // put the top of the hierarchy first differentiator = new CompositeDifferentiator(CollectionTools.reverse(differentiators)); } this.differentiatorCache.put(javaClass, differentiator); } public synchronized void setDefaultDifferentiator(Differentiator defaultDifferentiator) { this.userDifferentiators.put(OBJECT_CLASS, defaultDifferentiator); } public synchronized Differentiator setUserDifferentiator(Class javaClass, Differentiator userDifferentiator) { if (userDifferentiator == null) { throw new NullPointerException(); } if (this.userDifferentiators.put(javaClass, userDifferentiator) != null) { throw new IllegalArgumentException("duplicate differentiator: " + javaClass.getName()); } return userDifferentiator; } public synchronized Differentiator removeUserDifferentiator(Class javaClass) { if (javaClass == OBJECT_CLASS) { throw new IllegalArgumentException("The differentiator for java.lang.Object cannot be removed."); } return (Differentiator) this.userDifferentiators.remove(javaClass); } public synchronized ReflectiveDifferentiator addReflectiveDifferentiator(Class javaClass) { return this.addReflectiveDifferentiator(javaClass, EMPTY_STRING_ARRAY); } public synchronized ReflectiveDifferentiator addReflectiveDifferentiator(Class javaClass, String ignoredFieldName) { return this.addReflectiveDifferentiator(javaClass, new String[] {ignoredFieldName}); } public synchronized ReflectiveDifferentiator addReflectiveDifferentiator(Class javaClass, String ignoredFieldName1, String ignoredFieldName2) { return this.addReflectiveDifferentiator(javaClass, new String[] {ignoredFieldName1, ignoredFieldName2}); } public synchronized ReflectiveDifferentiator addReflectiveDifferentiator(Class javaClass, String ignoredFieldName1, String ignoredFieldName2, String ignoredFieldName3) { return this.addReflectiveDifferentiator(javaClass, new String[] {ignoredFieldName1, ignoredFieldName2, ignoredFieldName3}); } public synchronized ReflectiveDifferentiator addReflectiveDifferentiator(Class javaClass, String ignoredFieldName1, String ignoredFieldName2, String ignoredFieldName3, String ignoredFieldName4) { return this.addReflectiveDifferentiator(javaClass, new String[] {ignoredFieldName1, ignoredFieldName2, ignoredFieldName3, ignoredFieldName4}); } public synchronized ReflectiveDifferentiator addReflectiveDifferentiator(Class javaClass, String[] ignoredFieldNames) { return this.addReflectiveDifferentiator(javaClass, ignoredFieldNames, EMPTY_STRING_ARRAY); } public synchronized ReflectiveDifferentiator addReflectiveDifferentiator(Class javaClass, String[] ignoredFieldNames, String[] keyFieldNames) { ReflectiveDifferentiator rd = new ReflectiveDifferentiator(javaClass, this.recordingDifferentiator); rd.ignoreFieldsNamed(ignoredFieldNames); rd.addKeyFieldsNamed(keyFieldNames); this.setUserDifferentiator(javaClass, rd); return rd; } public void setLog(String fileName) throws FileNotFoundException { this.setLog(new WriterLog(fileName)); } public void setLog(Log log) { this.log = log; } void log(Diff diff) { this.log.log(diff); } private void tearDown() { this.recordingDifferentiator.tearDown(); this.differentiatorCache.clear(); this.reflectiveDifferentiatorCache.clear(); this.diffInProgress = false; this.log.close(); } /** * @see Object#toString() */ public String toString() { return StringTools.buildToStringFor(this); } // ******************** pluggable interface ******************** /** * Interface allowing for a pluggable log. */ public interface Log { /** * Log the specified diff as desired. */ void log(Diff diff); /** * Close the log. */ void close(); Log NULL_INSTANCE = new Log() { // nothing is logged public void log(Diff diff) { // do nothing } public void close() { // do nothing } public String toString() { return "NullLog"; } }; } /** * Implementation of Log that dumps the diffs to either an * output stream or a writer. The log can be configured to * log "value" diffs; by default it will NOT log "value" diffs. */ public static class WriterLog implements Log { private IndentingPrintWriter writer; private boolean logsValueDiffs; public WriterLog() { this(System.out); } public WriterLog(String fileName) throws FileNotFoundException { this(new File(fileName)); } public WriterLog(File file) throws FileNotFoundException { this(new FileOutputStream(file)); } public WriterLog(OutputStream stream) { this(new OutputStreamWriter(stream)); } public WriterLog(Writer writer) { this(new IndentingPrintWriter(new BufferedWriter(writer, 32768))); } public WriterLog(IndentingPrintWriter writer) { super(); this.writer = writer; this.logsValueDiffs = false; } public void log(Diff diff) { if (diff.getDifferentiator().comparesValueObjects() && ! this.logsValueDiffs) { return; } if (diff.identical()) { this.writer.println(Diff.NO_DIFFERENCE_DESCRIPTION); this.writer.print("object 1: "); this.writer.println(diff.getObject1()); this.writer.print("object 2: "); this.writer.println(diff.getObject2()); } else { diff.appendDescription(this.writer); } this.writer.println(); } public void close() { this.writer.close(); } public boolean logsValueDiffs() { return this.logsValueDiffs; } public void setLogsValueDiffs(boolean logsValueDiffs) { this.logsValueDiffs = logsValueDiffs; } } // ******************** helper class ******************** /** * This differentiator records diffs as they occur and throws an * exception if a "reference object" is compared twice. Though * multiple "key diffs" are allowed. */ private class RecordingDifferentiator implements Differentiator { private IdentityHashMap previousDiffs1; private IdentityHashMap previousDiffs2; // ********** constructors ********** RecordingDifferentiator() { super(); this.previousDiffs1 = new IdentityHashMap(); this.previousDiffs2 = new IdentityHashMap(); } // ********** Differentiator implementation ********** /** * @see Differentiator#diff(Object, Object) */ public Diff diff(Object object1, Object object2) { Differentiator differentiator = DiffEngine.this.differentiatorFor(object1); if ( ! differentiator.comparesValueObjects()) { // we should only hit "reference objects" once in each object graph this.checkDiff(object1, this.previousDiffs1); this.checkDiff(object2, this.previousDiffs2); } Diff diff = differentiator.diff(object1, object2); DiffEngine.this.log(diff); return diff; } /** * @see Differentiator#keyDiff(Object, Object) */ public Diff keyDiff(Object object1, Object object2) { Differentiator differentiator = DiffEngine.this.differentiatorFor(object1); Diff diff = differentiator.keyDiff(object1, object2); DiffEngine.this.log(diff); return diff; } /** * @see Differentiator#comparesValueObjects() */ public boolean comparesValueObjects() { return false; } // ********** behavior ********** void setUp() { // do nothing for now } private void checkDiff(Object object, IdentityHashMap previousDiffs) { // "reference objects" should only be diffed once... Object prev = previousDiffs.put(object, object); // "identity set" if (prev != null) { // if this exception is thrown that means you probably have two // fields that claim to "own" the same object - only one of them // can be a "composite" field, the other must be changed to a // "reference" field; // @see ReflectiveDifferentiator#addReferenceFieldNamed(String) throw new IllegalArgumentException("duplicate diff: " + object); } } void tearDown() { // System.out.println("total reference diffs: " + this.previousDiffs1.size()); this.previousDiffs1.clear(); this.previousDiffs2.clear(); } /** * @see Object#toString() */ public String toString() { return StringTools.buildToStringFor(this); } } }