/******************************************************************************* * Copyright (c) 1998, 2016 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.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import org.eclipse.persistence.tools.workbench.utility.ClassTools; import org.eclipse.persistence.tools.workbench.utility.Counter; import org.eclipse.persistence.tools.workbench.utility.string.StringTools; /** * Given a pair of containers to compare, use the element differentiator * to compare the elements' values. The order of the elements is insignificant. * Elements are matched up by using "key diffs": if the "key diff" indicates * that two elements have the same "key", they are then compared with * a "normal diff". */ public class ContainerDifferentiator implements Differentiator { /** the adapter used to adapter the containers */ private Adapter adapter; /** the differentiator used to compare the containers' elements */ private Differentiator elementDifferentiator; // ********** convenience static methods ********** public static ContainerDifferentiator forCollections() { return new ContainerDifferentiator(CollectionAdapter.instance()); } public static ContainerDifferentiator forCollections(Differentiator elementDifferentiator) { return new ContainerDifferentiator(CollectionAdapter.instance(), elementDifferentiator); } public static ContainerDifferentiator forArrays() { return new ContainerDifferentiator(UnorderedArrayAdapter.instance()); } public static ContainerDifferentiator forArrays(Differentiator elementDifferentiator) { return new ContainerDifferentiator(UnorderedArrayAdapter.instance(), elementDifferentiator); } public static ContainerDifferentiator forMaps() { return new ContainerDifferentiator(MapAdapter.instance(), new MapEntryDifferentiator()); } public static ContainerDifferentiator forMaps(Differentiator keyDifferentiator, Differentiator valueDifferentiator) { return new ContainerDifferentiator(MapAdapter.instance(), new MapEntryDifferentiator(keyDifferentiator, valueDifferentiator)); } public static ContainerDifferentiator forMaps(Differentiator entryDifferentiator) { return new ContainerDifferentiator(MapAdapter.instance(), entryDifferentiator); } // ********** constructors ********** /** * by default, use an "equality" element differentiator; * use this constructor if you want to override the various * "adapter" methods instead of building an Adapter */ public ContainerDifferentiator() { this(Adapter.INVALID_INSTANCE); } /** * use this constructor if you want to override the various * "adapter" methods instead of building an Adapter */ public ContainerDifferentiator(Differentiator elementDifferentiator) { this(Adapter.INVALID_INSTANCE, elementDifferentiator); } /** * by default, use an "equality" element differentiator */ public ContainerDifferentiator(Adapter adapter) { this(adapter, EqualityDifferentiator.instance()); } public ContainerDifferentiator(Adapter adapter, Differentiator elementDifferentiator) { super(); this.adapter = adapter; this.elementDifferentiator = elementDifferentiator; } // ********** Differentiator implementation ********** /** * @see Differentiator#diff(Object, Object) */ @Override public Diff diff(Object object1, Object object2) { return this.diff(object1, object2, true); } /** * @see Differentiator#keyDiff(Object, Object) */ @Override public Diff keyDiff(Object object1, Object object2) { return this.diff(object1, object2, false); } /** * oh yeah - this is a pretty spot of code... */ private Diff diff(Object object1, Object object2, boolean fullDiff) { if (object1 == object2) { return new NullDiff(object1, object2, this); } if (this.diffIsFatal(object1, object2)) { return new SimpleDiff(object1, object2, this.fatalDescriptionTitle(), this); } Map counters1 = this.buildCounters(object1); Map counters2 = this.buildCounters(object2); Collection removedElements = new ArrayList(); Collection keyMatchedDiffs = new ArrayList(); for (Iterator stream1 = counters1.entrySet().iterator(); stream1.hasNext(); ) { Map.Entry entry1 = (Map.Entry) stream1.next(); Object element1 = entry1.getKey(); Counter counter1 = (Counter) entry1.getValue(); for (int i = counter1.count(); i-- > 0; ) { boolean keyMatchFound = false; for (Iterator stream2 = counters2.entrySet().iterator(); stream2.hasNext(); ) { Map.Entry entry2 = (Map.Entry) stream2.next(); Object element2 = entry2.getKey(); Diff keyDiff = this.elementDifferentiator.keyDiff(element1, element2); if (keyDiff.identical()) { keyMatchFound = true; Counter counter2 = (Counter) entry2.getValue(); counter2.decrement(); if (counter2.count() == 0) { stream2.remove(); } if (fullDiff) { keyMatchedDiffs.add(this.elementDifferentiator.diff(element1, element2)); } else { keyMatchedDiffs.add(keyDiff); } break; // skip remainder of elements in container 2 and go to the next element in container 1 } } // if a "key" match was not found, the element must have been removed if ( ! keyMatchFound) { removedElements.add(element1); } } } // whatever elements remain in container 2 must have been added Collection addedElements = new ArrayList(); for (Iterator stream2 = counters2.entrySet().iterator(); stream2.hasNext(); ) { Map.Entry entry2 = (Map.Entry) stream2.next(); Object element2 = entry2.getKey(); Counter counter2 = (Counter) entry2.getValue(); for (int i = counter2.count(); i-- > 0; ) { addedElements.add(element2); } } return new ContainerDiff( this.containerClass(), object1, object2, (Diff[]) keyMatchedDiffs.toArray(new Diff[keyMatchedDiffs.size()]), removedElements.toArray(), addedElements.toArray(), this ); } private Map buildCounters(Object container) { Map counters = new LinkedHashMap(this.size(container)); for (Iterator stream = this.iterator(container); stream.hasNext(); ) { Object element = stream.next(); Counter counter = (Counter) counters.get(element); if (counter == null) { counter = new Counter(); counters.put(element, counter); } counter.increment(); } return counters; } private String fatalDescriptionTitle() { return "The two " + ClassTools.shortNameFor(this.containerClass()) + "s cannot be compared"; } /** * this will probably never be called, but we'll try 'false' for now * @see Differentiator#comparesValueObjects() */ @Override public boolean comparesValueObjects() { return false; } // ********** accessors ********** public Differentiator getElementDifferentiator() { return this.elementDifferentiator; } public void setElementDifferentiator(Differentiator elementDifferentiator) { this.elementDifferentiator = elementDifferentiator; } // ********** "adapter" methods ********** /** * return whether the the compared objects can even be * compared (e.g. they both must be non-null and instances * of java.util.Collection) */ protected boolean diffIsFatal(Object object1, Object object2) { return this.adapter.diffIsFatal(object1, object2); } /** * return the class to be passed to any diffs created * during the comparison */ protected Class containerClass() { return this.adapter.containerClass(); } /** * return the size of the specified container */ protected int size(Object container) { return this.adapter.size(container); } /** * return an iterator on the elements of the specified container */ protected Iterator iterator(Object container) { return this.adapter.iterator(container); } // ********** standard override ********** /** * @see Object#toString() */ @Override public String toString() { return StringTools.buildToStringFor(this, this.elementDifferentiator); } // ********** member interface ********** /** * This adapter is used by the differentiator to adapt * access to the various containers to be compared * (e.g. Collection, Array, Map). */ public interface Adapter { /** * Return whether the the specified objects can even be * compared (e.g. they both must be non-null and instances * of java.util.Collection). */ boolean diffIsFatal(Object object1, Object object2); /** * Return the class to be passed to any diffs created * during the comparison. Most of the time this is used * only in user messages. */ Class containerClass(); /** * Return the size of the specified container. */ int size(Object container); /** * Return an iterator on the elements of the specified container. */ Iterator iterator(Object container); Adapter INVALID_INSTANCE = new Adapter() { @Override public boolean diffIsFatal(Object object1, Object object2) { throw new UnsupportedOperationException(); } @Override public Class containerClass() { throw new UnsupportedOperationException(); } @Override public int size(Object container) { throw new UnsupportedOperationException(); } @Override public Iterator iterator(Object container) { throw new UnsupportedOperationException(); } @Override public String toString() { return "InvalidAdapter"; } }; } }