package org.jboss.seam.wiki.util; import java.util.*; /** * Compares two collections, returning a list of the additions, changes, and * deletions between them. A <code>Comparator</code> may be passed as an * argument to the constructor, and will thus be used. If not provided, the * initial value in the <code>a</code> ("from") collection will be looked at to * see if it supports the <code>Comparable</code> interface. If so, its * <code>equals</code> and <code>compareTo</code> methods will be invoked on the * instances in the "from" and "to" collections; otherwise, for speed, hash * codes from the objects will be used instead for comparison. * * <p>The file FileDiff.java shows an example usage of this class, in an * application similar to the Unix "diff" program.</p> * * LGPL, Jeff Pace <jpace at incava dot org> * */ public class Diff { public static final int NONE = -1; /** * Represents a difference, as used in <code>Diff</code>. A difference consists * of two pairs of starting and ending points, each pair representing either the * "from" or the "to" collection passed to <code>Diff</code>. If an ending point * is -1, then the difference was either a deletion or an addition. For example, * if <code>getDeletedEnd()</code> returns -1, then the difference represents an * addition. */ public class Difference { /** * The point at which the deletion starts. */ private int delStart = NONE; /** * The point at which the deletion ends. */ private int delEnd = NONE; /** * The point at which the addition starts. */ private int addStart = NONE; /** * The point at which the addition ends. */ private int addEnd = NONE; /** * Creates the difference for the given start and end points for the * deletion and addition. */ public Difference(int delStart, int delEnd, int addStart, int addEnd) { this.delStart = delStart; this.delEnd = delEnd; this.addStart = addStart; this.addEnd = addEnd; } /** * The point at which the deletion starts, if any. A value equal to * <code>NONE</code> means this is an addition. */ public int getDeletedStart() { return delStart; } /** * The point at which the deletion ends, if any. A value equal to * <code>NONE</code> means this is an addition. */ public int getDeletedEnd() { return delEnd; } /** * The point at which the addition starts, if any. A value equal to * <code>NONE</code> means this must be an addition. */ public int getAddedStart() { return addStart; } /** * The point at which the addition ends, if any. A value equal to * <code>NONE</code> means this must be an addition. */ public int getAddedEnd() { return addEnd; } /** * Sets the point as deleted. The start and end points will be modified to * include the given line. */ public void setDeleted(int line) { delStart = Math.min(line, delStart); delEnd = Math.max(line, delEnd); } /** * Sets the point as added. The start and end points will be modified to * include the given line. */ public void setAdded(int line) { addStart = Math.min(line, addStart); addEnd = Math.max(line, addEnd); } /** * Compares this object to the other for equality. Both objects must be of * type Difference, with the same starting and ending points. */ public boolean equals(Object obj) { if (obj instanceof Difference) { Difference other = (Difference) obj; return (delStart == other.delStart && delEnd == other.delEnd && addStart == other.addStart && addEnd == other.addEnd); } else { return false; } } /** * Returns a string representation of this difference. */ public String toString() { StringBuffer buf = new StringBuffer(); buf.append("del: [" + delStart + ", " + delEnd + "]"); buf.append(" "); buf.append("add: [" + addStart + ", " + addEnd + "]"); return buf.toString(); } } /** * The source array, AKA the "from" values. */ protected Object[] a; /** * The target array, AKA the "to" values. */ protected Object[] b; /** * The list of differences, as <code>Difference</code> instances. */ protected List diffs = new ArrayList(); /** * The pending, uncommitted difference. */ private Difference pending; /** * The comparator used, if any. */ private Comparator comparator; /** * The thresholds. */ private TreeMap thresh; /** * Constructs the Diff object for the two arrays, using the given comparator. */ public Diff(Object[] a, Object[] b, Comparator comp) { this.a = a; this.b = b; this.comparator = comp; this.thresh = null; // created in getLongestCommonSubsequences } /** * Constructs the Diff object for the two arrays, using the default * comparison mechanism between the objects, such as <code>equals</code> and * <code>compareTo</code>. */ public Diff(Object[] a, Object[] b) { this(a, b, null); } /** * Constructs the Diff object for the two collections, using the given * comparator. */ public Diff(Collection a, Collection b, Comparator comp) { this(a.toArray(), b.toArray(), comp); } /** * Constructs the Diff object for the two collections, using the default * comparison mechanism between the objects, such as <code>equals</code> and * <code>compareTo</code>. */ public Diff(Collection a, Collection b) { this(a, b, null); } /** * Runs diff and returns the results. */ public List<Difference> diff() { traverseSequences(); // add the last difference, if pending: if (pending != null) { diffs.add(pending); } return diffs; } /** * Traverses the sequences, seeking the longest common subsequences, * invoking the methods <code>finishedA</code>, <code>finishedB</code>, * <code>onANotB</code>, and <code>onBNotA</code>. */ protected void traverseSequences() { Integer[] matches = getLongestCommonSubsequences(); int lastA = a.length - 1; int lastB = b.length - 1; int bi = 0; int ai; int lastMatch = matches.length - 1; for (ai = 0; ai <= lastMatch; ++ai) { Integer bLine = matches[ai]; if (bLine == null) { onANotB(ai, bi); } else { while (bi < bLine.intValue()) { onBNotA(ai, bi++); } onMatch(ai, bi++); } } boolean calledFinishA = false; boolean calledFinishB = false; while (ai <= lastA || bi <= lastB) { // last A? if (ai == lastA + 1 && bi <= lastB) { if (!calledFinishA && callFinishedA()) { finishedA(lastA); calledFinishA = true; } else { while (bi <= lastB) { onBNotA(ai, bi++); } } } // last B? if (bi == lastB + 1 && ai <= lastA) { if (!calledFinishB && callFinishedB()) { finishedB(lastB); calledFinishB = true; } else { while (ai <= lastA) { onANotB(ai++, bi); } } } if (ai <= lastA) { onANotB(ai++, bi); } if (bi <= lastB) { onBNotA(ai, bi++); } } } /** * Override and return true in order to have <code>finishedA</code> invoked * at the last element in the <code>a</code> array. */ protected boolean callFinishedA() { return false; } /** * Override and return true in order to have <code>finishedB</code> invoked * at the last element in the <code>b</code> array. */ protected boolean callFinishedB() { return false; } /** * Invoked at the last element in <code>a</code>, if * <code>callFinishedA</code> returns true. */ protected void finishedA(int lastA) { } /** * Invoked at the last element in <code>b</code>, if * <code>callFinishedB</code> returns true. */ protected void finishedB(int lastB) { } /** * Invoked for elements in <code>a</code> and not in <code>b</code>. */ protected void onANotB(int ai, int bi) { if (pending == null) { pending = new Difference(ai, ai, bi, -1); } else { pending.setDeleted(ai); } } /** * Invoked for elements in <code>b</code> and not in <code>a</code>. */ protected void onBNotA(int ai, int bi) { if (pending == null) { pending = new Difference(ai, -1, bi, bi); } else { pending.setAdded(bi); } } /** * Invoked for elements matching in <code>a</code> and <code>b</code>. */ protected void onMatch(int ai, int bi) { if (pending == null) { // no current pending } else { diffs.add(pending); pending = null; } } /** * Compares the two objects, using the comparator provided with the * constructor, if any. */ protected boolean equals(Object x, Object y) { return comparator == null ? x.equals(y) : comparator.compare(x, y) == 0; } /** * Returns an array of the longest common subsequences. */ public Integer[] getLongestCommonSubsequences() { int aStart = 0; int aEnd = a.length - 1; int bStart = 0; int bEnd = b.length - 1; TreeMap matches = new TreeMap(); while (aStart <= aEnd && bStart <= bEnd && equals(a[aStart], b[bStart])) { matches.put(new Integer(aStart++), new Integer(bStart++)); } while (aStart <= aEnd && bStart <= bEnd && equals(a[aEnd], b[bEnd])) { matches.put(new Integer(aEnd--), new Integer(bEnd--)); } Map bMatches = null; if (comparator == null) { if (a.length > 0 && a[0] instanceof Comparable) { // this uses the Comparable interface bMatches = new TreeMap(); } else { // this just uses hashCode() bMatches = new HashMap(); } } else { // we don't really want them sorted, but this is the only Map // implementation (as of JDK 1.4) that takes a comparator. bMatches = new TreeMap(comparator); } for (int bi = bStart; bi <= bEnd; ++bi) { Object element = b[bi]; Object key = element; List positions = (List)bMatches.get(key); if (positions == null) { positions = new ArrayList(); bMatches.put(key, positions); } positions.add(new Integer(bi)); } thresh = new TreeMap(); Map links = new HashMap(); for (int i = aStart; i <= aEnd; ++i) { Object aElement = a[i]; // keygen here. List positions = (List)bMatches.get(aElement); if (positions != null) { Integer k = new Integer(0); ListIterator pit = positions.listIterator(positions.size()); while (pit.hasPrevious()) { Integer j = (Integer)pit.previous(); k = insert(j, k); if (k == null) { // nothing } else { Object value = k.intValue() > 0 ? links.get(new Integer(k.intValue() - 1)) : null; links.put(k, new Object[] { value, new Integer(i), j }); } } } } if (thresh.size() > 0) { Integer ti = (Integer)thresh.lastKey(); Object[] link = (Object[])links.get(ti); while (link != null) { Integer x = (Integer)link[1]; Integer y = (Integer)link[2]; matches.put(x, y); link = (Object[])link[0]; } } return toArray(matches); } /** * Converts the map (indexed by java.lang.Integers) into an array. */ protected static Integer[] toArray(TreeMap map) { int size = map.size() == 0 ? 0 : 1 + ((Integer)map.lastKey()).intValue(); Integer[] ary = new Integer[size]; Iterator it = map.keySet().iterator(); while (it.hasNext()) { Integer idx = (Integer)it.next(); Integer val = (Integer)map.get(idx); ary[idx.intValue()] = val; } return ary; } /** * Returns whether the integer is not zero (including if it is not null). */ protected static boolean isNonzero(Integer i) { return i != null && i.intValue() != 0; } /** * Returns whether the value in the map for the given index is greater than * the given value. */ protected boolean isGreaterThan(Integer index, Integer val) { Integer lhs = (Integer)thresh.get(index); return lhs != null && val != null && lhs.compareTo(val) > 0; } /** * Returns whether the value in the map for the given index is less than * the given value. */ protected boolean isLessThan(Integer index, Integer val) { Integer lhs = (Integer)thresh.get(index); return lhs != null && (val == null || lhs.compareTo(val) < 0); } /** * Returns the value for the greatest key in the map. */ protected Integer getLastValue() { return (Integer)thresh.get(thresh.lastKey()); } /** * Adds the given value to the "end" of the threshold map, that is, with the * greatest index/key. */ protected void append(Integer value) { Integer addIdx = null; if (thresh.size() == 0) { addIdx = new Integer(0); } else { Integer lastKey = (Integer)thresh.lastKey(); addIdx = new Integer(lastKey.intValue() + 1); } thresh.put(addIdx, value); } /** * Inserts the given values into the threshold map. */ protected Integer insert(Integer j, Integer k) { if (isNonzero(k) && isGreaterThan(k, j) && isLessThan(new Integer(k.intValue() - 1), j)) { thresh.put(k, j); } else { int hi = -1; if (isNonzero(k)) { hi = k.intValue(); } else if (thresh.size() > 0) { hi = ((Integer)thresh.lastKey()).intValue(); } // off the end? if (hi == -1 || j.compareTo(getLastValue()) > 0) { append(j); k = new Integer(hi + 1); } else { // binary search for insertion point: int lo = 0; while (lo <= hi) { int index = (hi + lo) / 2; Integer val = (Integer)thresh.get(new Integer(index)); int cmp = j.compareTo(val); if (cmp == 0) { return null; } else if (cmp > 0) { lo = index + 1; } else { hi = index - 1; } } thresh.put(new Integer(lo), j); k = new Integer(lo); } } return k; } }