diff --git a/licenses/incava-diff.txt b/licenses/incava-diff.txt
new file mode 100644
index 00000000..a9c35ef3
--- /dev/null
+++ b/licenses/incava-diff.txt
@@ -0,0 +1,27 @@
+Copyright (c) 2009, incava.org
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+    * this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above copyright notice,
+    * this list of conditions and the following disclaimer in the documentation
+    * and/or other materials provided with the distribution.
+
+    * Neither the name of incava.org nor the names of its contributors may be
+    * used to endorse or promote products derived from this software without
+    * specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/src/helma/util/Diff.java b/src/helma/util/Diff.java
index aa4d5f0d..4d815fd4 100644
--- a/src/helma/util/Diff.java
+++ b/src/helma/util/Diff.java
@@ -1,863 +1,659 @@
-/* $Log$
-/* Revision 1.1  2002/10/31 08:39:34  hannes
-/* Added GNU Diff class from http://www.bmsi.com/java/#diff
 /*
- * Revision 1.3  2000/03/03  21:58:03  stuart
- * move discard_confusing_lines and shift_boundaries to class file_data
- *
- * Revision 1.2  2000/03/02  16:37:38  stuart
- * Add GPL and copyright
- *
+Copyright (c) 2009, incava.org
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+    * this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above copyright notice,
+    * this list of conditions and the following disclaimer in the documentation
+    * and/or other materials provided with the distribution.
+
+    * Neither the name of incava.org nor the names of its contributors may be
+    * used to endorse or promote products derived from this software without
+    * specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
+
 package helma.util;
 
-import java.util.Hashtable;
+import java.util.*;
 
-/** A class to compare vectors of objects.  The result of comparison
-    is a list of change objects which form an
-    edit script.  The objects compared are traditionally lines
-    of text from two files.  Comparison options such as "ignore
-    whitespace" are implemented by modifying the equals
-    and hashcode methods for the objects compared.
-
- The basic algorithm is described in: - "An O(ND) Difference Algorithm and its Variations", Eugene Myers, - Algorithmica Vol. 1 No. 2, 1986, p 251. -
- This class outputs different results from GNU diff 1.15 on some - inputs. Our results are actually better (smaller change list, smaller - total size of changes), but it would be nice to know why. Perhaps - there is a memory overwrite bug in GNU diff 1.15. - - @author Stuart D. Gathman, translated from GNU diff 1.15 - Copyright (C) 2000 Business Management Systems, Inc. -
- This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 1, or (at your option) - any later version. -
- This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. -
-    You should have received a copy of the 
-    GNU General Public License
-    along with this program; if not, write to the Free Software
-    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 
+/**
+ * Compares two lists, returning a list of the additions, changes, and deletions
+ * between them. A Comparator may be passed as an argument to the
+ * constructor, and will thus be used. If not provided, the initial value in the
+ * a ("from") list will be looked at to see if it supports the
+ * Comparable interface. If so, its equals and
+ * compareTo methods will be invoked on the instances in the "from"
+ * and "to" lists; otherwise, for speed, hash codes from the objects will be
+ * used instead for comparison.
+ *
+ * 
The file FileDiff.java shows an example usage of this class, in an + * application similar to the Unix "diff" program.
*/ +public class Diff +{ + /** + * The source list, AKA the "from" values. + */ + protected List a; -public class Diff { + /** + * The target list, AKA the "to" values. + */ + protected List b; - /** Prepare to find differences between two arrays. Each element of - the arrays is translated to an "equivalence number" based on - the result ofequals.  The original Object arrays
-      are no longer needed for computing the differences.  They will
-      be needed again later to print the results of the comparison as
-      an edit script, if desired.
-   */
-  public Diff(Object[] a,Object[] b) {
-    Hashtable h = new Hashtable(a.length + b.length);
-    filevec[0] = new file_data(a,h);
-    filevec[1] = new file_data(b,h);
-  }
+    /**
+     * The list of differences, as Difference instances.
+     */
+    protected List diffs = new ArrayList();
 
-  /** 1 more than the maximum equivalence value used for this or its
-     sibling file. */
-  private int equiv_max = 1;
+    /**
+     * The pending, uncommitted difference.
+     */
+    private Difference pending;
 
-  /** When set to true, the comparison uses a heuristic to speed it up.
-    With this heuristic, for files with a constant small density
-    of changes, the algorithm is linear in the file size.  */
-  public boolean heuristic = false;
+    /**
+     * The comparator used, if any.
+     */
+    private Comparator comparator;
 
-  /** When set to true, the algorithm returns a guarranteed minimal
-      set of changes.  This makes things slower, sometimes much slower. */
-  public boolean no_discards = false;
+    /**
+     * The thresholds.
+     */
+    private TreeMap thresh;
 
-  private int[] xvec, yvec;	/* Vectors being compared. */
-  private int[] fdiag;		/* Vector, indexed by diagonal, containing
-				   the X coordinate of the point furthest
-				   along the given diagonal in the forward
-				   search of the edit matrix. */
-  private int[] bdiag;		/* Vector, indexed by diagonal, containing
-				   the X coordinate of the point furthest
-				   along the given diagonal in the backward
-				   search of the edit matrix. */
-  private int fdiagoff, bdiagoff;
-  private final file_data[] filevec = new file_data[2];
-  private int cost;
-
-  /** Find the midpoint of the shortest edit script for a specified
-     portion of the two files.
-
-     We scan from the beginnings of the files, and simultaneously from the ends,
-     doing a breadth-first search through the space of edit-sequence.
-     When the two searches meet, we have found the midpoint of the shortest
-     edit sequence.
-
-     The value returned is the number of the diagonal on which the midpoint lies.
-     The diagonal number equals the number of inserted lines minus the number
-     of deleted lines (counting only lines before the midpoint).
-     The edit cost is stored into COST; this is the total number of
-     lines inserted or deleted (counting only lines before the midpoint).
-
-     This function assumes that the first lines of the specified portions
-     of the two files do not match, and likewise that the last lines do not
-     match.  The caller must trim matching lines from the beginning and end
-     of the portions it is going to specify.
-
-     Note that if we return the "wrong" diagonal value, or if
-     the value of bdiag at that diagonal is "wrong",
-     the worst this can do is cause suboptimal diff output.
-     It cannot cause incorrect diff output.  */
-
-  private int diag (int xoff, int xlim, int yoff, int ylim) {
-    final int[] fd = fdiag;	// Give the compiler a chance.
-    final int[] bd = bdiag;	// Additional help for the compiler.
-    final int[] xv = xvec;		// Still more help for the compiler.
-    final int[] yv = yvec;		// And more and more . . .
-    final int dmin = xoff - ylim;	// Minimum valid diagonal.
-    final int dmax = xlim - yoff;	// Maximum valid diagonal.
-    final int fmid = xoff - yoff;	// Center diagonal of top-down search.
-    final int bmid = xlim - ylim;	// Center diagonal of bottom-up search.
-    int fmin = fmid, fmax = fmid;	// Limits of top-down search.
-    int bmin = bmid, bmax = bmid;	// Limits of bottom-up search.
-    /* True if southeast corner is on an odd
-				     diagonal with respect to the northwest. */
-    final boolean odd = (fmid - bmid & 1) != 0;	
-
-    fd[fdiagoff + fmid] = xoff;
-    bd[bdiagoff + bmid] = xlim;
-
-    for (int c = 1;; ++c)
-      {
-	int d;			/* Active diagonal. */
-	boolean big_snake = false;
-
-	/* Extend the top-down search by an edit step in each diagonal. */
-	if (fmin > dmin)
-	  fd[fdiagoff + --fmin - 1] = -1;
-	else
-	  ++fmin;
-	if (fmax < dmax)
-	  fd[fdiagoff + ++fmax + 1] = -1;
-	else
-	  --fmax;
-	for (d = fmax; d >= fmin; d -= 2)
-	  {
-	    int x, y, oldx, tlo = fd[fdiagoff + d - 1], thi = fd[fdiagoff + d + 1];
-
-	    if (tlo >= thi)
-	      x = tlo + 1;
-	    else
-	      x = thi;
-	    oldx = x;
-	    y = x - d;
-	    while (x < xlim && y < ylim && xv[x] == yv[y]) {
-	      ++x; ++y;
-	    }
-	    if (x - oldx > 20)
-	      big_snake = true;
-	    fd[fdiagoff + d] = x;
-	    if (odd && bmin <= d && d <= bmax && bd[bdiagoff + d] <= fd[fdiagoff + d])
-	      {
-		cost = 2 * c - 1;
-		return d;
-	      }
-	  }
-
-	/* Similar extend the bottom-up search. */
-	if (bmin > dmin)
-	  bd[bdiagoff + --bmin - 1] = Integer.MAX_VALUE;
-	else
-	  ++bmin;
-	if (bmax < dmax)
-	  bd[bdiagoff + ++bmax + 1] = Integer.MAX_VALUE;
-	else
-	  --bmax;
-	for (d = bmax; d >= bmin; d -= 2)
-	  {
-	    int x, y, oldx, tlo = bd[bdiagoff + d - 1], thi = bd[bdiagoff + d + 1];
-
-	    if (tlo < thi)
-	      x = tlo;
-	    else
-	      x = thi - 1;
-	    oldx = x;
-	    y = x - d;
-	    while (x > xoff && y > yoff && xv[x - 1] == yv[y - 1]) {
-	      --x; --y;
-	    }
-	    if (oldx - x > 20)
-	      big_snake = true;
-	    bd[bdiagoff + d] = x;
-	    if (!odd && fmin <= d && d <= fmax && bd[bdiagoff + d] <= fd[fdiagoff + d])
-	      {
-		cost = 2 * c;
-		return d;
-	      }
-	  }
-
-	/* Heuristic: check occasionally for a diagonal that has made
-	   lots of progress compared with the edit distance.
-	   If we have any such, find the one that has made the most
-	   progress and return it as if it had succeeded.
-
-	   With this heuristic, for files with a constant small density
-	   of changes, the algorithm is linear in the file size.  */
-
-	if (c > 200 && big_snake && heuristic)
-	  {
-	    int best = 0;
-	    int bestpos = -1;
-
-	    for (d = fmax; d >= fmin; d -= 2)
-	      {
-		int dd = d - fmid;
-		if ((fd[fdiagoff + d] - xoff)*2 - dd > 12 * (c + (dd > 0 ? dd : -dd)))
-		  {
-		    if (fd[fdiagoff + d] * 2 - dd > best
-			&& fd[fdiagoff + d] - xoff > 20
-			&& fd[fdiagoff + d] - d - yoff > 20)
-		      {
-			int k;
-			int x = fd[fdiagoff + d];
-
-			/* We have a good enough best diagonal;
-			   now insist that it end with a significant snake.  */
-			for (k = 1; k <= 20; k++)
-			  if (xvec[x - k] != yvec[x - d - k])
-			    break;
-
-			if (k == 21)
-			  {
-			    best = fd[fdiagoff + d] * 2 - dd;
-			    bestpos = d;
-			  }
-		      }
-		  }
-	      }
-	    if (best > 0)
-	      {
-		cost = 2 * c - 1;
-		return bestpos;
-	      }
-
-	    best = 0;
-	    for (d = bmax; d >= bmin; d -= 2)
-	      {
-		int dd = d - bmid;
-		if ((xlim - bd[bdiagoff + d])*2 + dd > 12 * (c + (dd > 0 ? dd : -dd)))
-		  {
-		    if ((xlim - bd[bdiagoff + d]) * 2 + dd > best
-			&& xlim - bd[bdiagoff + d] > 20
-			&& ylim - (bd[bdiagoff + d] - d) > 20)
-		      {
-			/* We have a good enough best diagonal;
-			   now insist that it end with a significant snake.  */
-			int k;
-			int x = bd[bdiagoff + d];
-
-			for (k = 0; k < 20; k++)
-			  if (xvec[x + k] != yvec[x - d + k])
-			    break;
-			if (k == 20)
-			  {
-			    best = (xlim - bd[bdiagoff + d]) * 2 + dd;
-			    bestpos = d;
-			  }
-		      }
-		  }
-	      }
-	    if (best > 0)
-	      {
-		cost = 2 * c - 1;
-		return bestpos;
-	      }
-	  }
-      }
-  }
-
-  /** Compare in detail contiguous subsequences of the two files
-     which are known, as a whole, to match each other.
-
-     The results are recorded in the vectors filevec[N].changed_flag, by
-     storing a 1 in the element for each line that is an insertion or deletion.
-
-     The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
-
-     Note that XLIM, YLIM are exclusive bounds.
-     All line numbers are origin-0 and discarded lines are not counted.  */
-
-  private void compareseq (int xoff, int xlim, int yoff, int ylim) {
-    /* Slide down the bottom initial diagonal. */
-    while (xoff < xlim && yoff < ylim && xvec[xoff] == yvec[yoff]) {
-      ++xoff; ++yoff;
+    /**
+     * Constructs the Diff object for the two arrays, using the given comparator.
+     */
+    public Diff(Object[] a, Object[] b, Comparator comp)
+    {
+        this(Arrays.asList(a), Arrays.asList(b), comp);
     }
-    /* Slide up the top initial diagonal. */
-    while (xlim > xoff && ylim > yoff && xvec[xlim - 1] == yvec[ylim - 1]) {
-      --xlim; --ylim;
+
+    /**
+     * Constructs the Diff object for the two arrays, using the default
+     * comparison mechanism between the objects, such as equals and
+     * compareTo.
+     */
+    public Diff(Object[] a, Object[] b)
+    {
+        this(a, b, null);
+    }
+
+    /**
+     * Constructs the Diff object for the two lists, using the given comparator.
+     */
+    public Diff(List a, List b, Comparator comp)
+    {
+        this.a = a;
+        this.b = b;
+        this.comparator = comp;
+        this.thresh = null;
+    }
+
+    /**
+     * Constructs the Diff object for the two lists, using the default
+     * comparison mechanism between the objects, such as equals and
+     * compareTo.
+     */
+    public Diff(List a, List b)
+    {
+        this(a, b, null);
+    }
+
+    /**
+     * Runs diff and returns the results.
+     */
+    public Change diff()
+    {
+        traverseSequences();
+
+        // add the last difference, if pending:
+        if (pending != null) {
+            diffs.add(pending);
+        }
+
+        return Change.fromList(diffs);
+    }
+
+    /**
+     * Traverses the sequences, seeking the longest common subsequences,
+     * invoking the methods finishedA, finishedB,
+     * onANotB, and onBNotA.
+     */
+    protected void traverseSequences()
+    {
+        Integer[] matches = getLongestCommonSubsequences();
+
+        int lastA = a.size() - 1;
+        int lastB = b.size() - 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 finishedA invoked
+     * at the last element in the a array.
+     */
+    protected boolean callFinishedA()
+    {
+        return false;
+    }
+
+    /**
+     * Override and return true in order to have finishedB invoked
+     * at the last element in the b array.
+     */
+    protected boolean callFinishedB()
+    {
+        return false;
+    }
+
+    /**
+     * Invoked at the last element in a, if
+     * callFinishedA returns true.
+     */
+    protected void finishedA(int lastA)
+    {
+    }
+
+    /**
+     * Invoked at the last element in b, if
+     * callFinishedB returns true.
+     */
+    protected void finishedB(int lastB)
+    {
+    }
+
+    /**
+     * Invoked for elements in a and not in b.
+     */
+    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 b and not in a.
+     */
+    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 a and b.
+     */
+    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;
     }
     
-    /* Handle simple cases. */
-    if (xoff == xlim)
-      while (yoff < ylim)
-	filevec[1].changed_flag[1+filevec[1].realindexes[yoff++]] = true;
-    else if (yoff == ylim)
-      while (xoff < xlim)
-	filevec[0].changed_flag[1+filevec[0].realindexes[xoff++]] = true;
-    else
-      {
-	/* Find a point of correspondence in the middle of the files.  */
-
-	int d = diag (xoff, xlim, yoff, ylim);
-	int c = cost;
-	//int f = fdiag[fdiagoff + d];
-	int b = bdiag[bdiagoff + d];
-
-	if (c == 1)
-	  {
-	    /* This should be impossible, because it implies that
-	       one of the two subsequences is empty,
-	       and that case was handled above without calling `diag'.
-	       Let's verify that this is true.  */
-	    throw new IllegalArgumentException("Empty subsequence");
-	  }
-	else
-	  {
-	    /* Use that point to split this problem into two subproblems.  */
-	    compareseq (xoff, b, yoff, b - d);
-	    /* This used to use f instead of b,
-	       but that is incorrect!
-	       It is not necessarily the case that diagonal d
-	       has a snake from b to f.  */
-	    compareseq (b, xlim, b - d, ylim);
-	  }
-      }
-  }
-
-  /** Discard lines from one file that have no matches in the other file.
-   */
-
-  private void discard_confusing_lines() {
-    filevec[0].discard_confusing_lines(filevec[1]);
-    filevec[1].discard_confusing_lines(filevec[0]);
-  }
-
-  private boolean inhibit = false;
-
-  /** Adjust inserts/deletes of blank lines to join changes
-     as much as possible.
-   */
-
-  private void shift_boundaries() {
-    if (inhibit)
-      return;
-    filevec[0].shift_boundaries(filevec[1]);
-    filevec[1].shift_boundaries(filevec[0]);
-  }
-
-  /** Scan the tables of which lines are inserted and deleted,
-     producing an edit script in reverse order.  */
-
-  private change build_reverse_script() {
-    change script = null;
-    final boolean[] changed0 = filevec[0].changed_flag;
-    final boolean[] changed1 = filevec[1].changed_flag;
-    final int len0 = filevec[0].buffered_lines;
-    final int len1 = filevec[1].buffered_lines;
-
-    /* Note that changedN[len0] does exist, and contains 0.  */
-
-    int i0 = 0, i1 = 0;
-
-    while (i0 < len0 || i1 < len1)
-      {
-	if (changed0[1+i0] || changed1[1+i1])
-	  {
-	    int line0 = i0, line1 = i1;
-
-	    /* Find # lines changed here in each file.  */
-	    while (changed0[1+i0]) ++i0;
-	    while (changed1[1+i1]) ++i1;
-
-	    /* Record this change.  */
-	    script = new change(line0, line1, i0 - line0, i1 - line1, script);
-	  }
-
-	/* We have reached lines in the two files that match each other.  */
-	i0++; i1++;
-      }
-
-    return script;
-  }
-
-  /** Scan the tables of which lines are inserted and deleted,
-     producing an edit script in forward order.  */
-
-  private change build_script() {
-    change script = null;
-    final boolean[] changed0 = filevec[0].changed_flag;
-    final boolean[] changed1 = filevec[1].changed_flag;
-    final int len0 = filevec[0].buffered_lines;
-    final int len1 = filevec[1].buffered_lines;
-    int i0 = len0, i1 = len1;
-
-    /* Note that changedN[-1] does exist, and contains 0.  */
-
-    while (i0 >= 0 || i1 >= 0)
-      {
-	if (changed0[i0] || changed1[i1])
-	  {
-	    int line0 = i0, line1 = i1;
-
-	    /* Find # lines changed here in each file.  */
-	    while (changed0[i0]) --i0;
-	    while (changed1[i1]) --i1;
-
-	    /* Record this change.  */
-	    script = new change(i0, i1, line0 - i0, line1 - i1, script);
-	  }
-
-	/* We have reached lines in the two files that match each other.  */
-	i0--; i1--;
-      }
-
-    return script;
-  }
-  
-  /**
-   *  Convenience method to get a diff on the two files.
-   */
-  public change diff() {
-    return diff_2 (false);
-  }
-
-  /* Report the differences of two files.  DEPTH is the current directory
-     depth. */
-  public change diff_2(final boolean reverse) {
-
-    /* Some lines are obviously insertions or deletions
-       because they don't match anything.  Detect them now,
-       and avoid even thinking about them in the main comparison algorithm.  */
-
-    discard_confusing_lines ();
-
-    /* Now do the main comparison algorithm, considering just the
-       undiscarded lines.  */
-
-    xvec = filevec[0].undiscarded;
-    yvec = filevec[1].undiscarded;
-
-    int diags =
-      filevec[0].nondiscarded_lines + filevec[1].nondiscarded_lines + 3;
-    fdiag = new int[diags];
-    fdiagoff = filevec[1].nondiscarded_lines + 1;
-    bdiag = new int[diags];
-    bdiagoff = filevec[1].nondiscarded_lines + 1;
-
-    compareseq (0, filevec[0].nondiscarded_lines,
-		0, filevec[1].nondiscarded_lines);
-    fdiag = null;
-    bdiag = null;
-
-    /* Modify the results slightly to make them prettier
-       in cases where that can validly be done.  */
-
-    shift_boundaries ();
-
-    /* Get the results of comparison in the form of a chain
-       of `struct change's -- an edit script.  */
-
-    if (reverse)
-      return build_reverse_script();
-    else
-      return build_script();
-  }
-
-  /** The result of comparison is an "edit script": a chain of change objects.
-     Each change represents one place where some lines are deleted
-     and some are inserted.
-     
-     LINE0 and LINE1 are the first affected lines in the two files (origin 0).
-     DELETED is the number of lines deleted here from file 0.
-     INSERTED is the number of lines inserted here in file 1.
-
-     If DELETED is 0 then LINE0 is the number of the line before
-     which the insertion was done; vice versa for INSERTED and LINE1.  */
-
-  public static class change {
-    /** Previous or next edit command. */
-    public change link;		
-    /** # lines of file 1 changed here.  */
-    public final int inserted;	
-    /** # lines of file 0 changed here.  */
-    public final int deleted;		
-    /** Line number of 1st deleted line.  */
-    public final int line0;		
-    /** Line number of 1st inserted line.  */
-    public final int line1;		
-
-    /** Cons an additional entry onto the front of an edit script OLD.
-       LINE0 and LINE1 are the first affected lines in the two files (origin 0).
-       DELETED is the number of lines deleted here from file 0.
-       INSERTED is the number of lines inserted here in file 1.
-
-       If DELETED is 0 then LINE0 is the number of the line before
-       which the insertion was done; vice versa for INSERTED and LINE1.  */
-    change(int line0, int line1, int deleted, int inserted, change old) {
-      this.line0 = line0;
-      this.line1 = line1;
-      this.inserted = inserted;
-      this.deleted = deleted;
-      this.link = old;
-      //System.err.println(line0+","+line1+","+inserted+","+deleted);
-    }
-  }
-
-  /** Data on one input file being compared.  
-   */
-
-  class file_data {
-
-    /** Allocate changed array for the results of comparison.  */
-    void clear() {
-      /* Allocate a flag for each line of each file, saying whether that line
-	 is an insertion or deletion.
-	 Allocate an extra element, always zero, at each end of each vector.
-       */
-      changed_flag = new boolean[buffered_lines + 2];
-    }
-
-    /** Return equiv_count[I] as the number of lines in this file
-       that fall in equivalence class I.
-         @return the array of equivalence class counts.
+    /**
+     * Returns an array of the longest common subsequences.
      */
-    int[] equivCount() {
-      int[] equiv_count = new int[equiv_max];
-      for (int i = 0; i < buffered_lines; ++i)
-	++equiv_count[equivs[i]];
-      return equiv_count;
+    public Integer[] getLongestCommonSubsequences()
+    {
+        int aStart = 0;
+        int aEnd = a.size() - 1;
+
+        int bStart = 0;
+        int bEnd = b.size() - 1;
+
+        TreeMap matches = new TreeMap();
+
+        while (aStart <= aEnd && bStart <= bEnd && equals(a.get(aStart), b.get(bStart))) {
+            matches.put(Integer.valueOf(aStart++), Integer.valueOf(bStart++));
+        }
+
+        while (aStart <= aEnd && bStart <= bEnd && equals(a.get(aEnd), b.get(bEnd))) {
+            matches.put(Integer.valueOf(aEnd--), Integer.valueOf(bEnd--));
+        }
+
+        Map bMatches = null;
+        if (comparator == null) {
+            if (a.size() > 0 && a.get(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.get(bi);
+            Object          key       = element;
+            List positions = (List) bMatches.get(key);
+            
+            if (positions == null) {
+                positions = new ArrayList();
+                bMatches.put(key, positions);
+            }
+            
+            positions.add(Integer.valueOf(bi));
+        }
+
+        thresh = new TreeMap();
+        Map links = new HashMap();
+
+        for (int i = aStart; i <= aEnd; ++i) {
+            Object aElement  = a.get(i);
+            List positions = (List) bMatches.get(aElement);
+
+            if (positions != null) {
+                Integer  k   = Integer.valueOf(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(Integer.valueOf(k.intValue() - 1)) : null;
+                        links.put(k, new Object[] { value, Integer.valueOf(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];
+            }
+        }
+
+        int       size = matches.size() == 0 ? 0 : 1 + ((Integer) matches.lastKey()).intValue();
+        Integer[] ary  = new Integer[size];
+        for (Iterator it = matches.keySet().iterator(); it.hasNext();) {
+            Integer idx = (Integer) it.next();
+            Integer val = (Integer) matches.get(idx);
+            ary[idx.intValue()] = val;
+        }
+        return ary;
     }
 
-    /** Discard lines that have no matches in another file.
-
-       A line which is discarded will not be considered by the actual
-       comparison algorithm; it will be as if that line were not in the file.
-       The file's `realindexes' table maps virtual line numbers
-       (which don't count the discarded lines) into real line numbers;
-       this is how the actual comparison algorithm produces results
-       that are comprehensible when the discarded lines are counted.
-
-       When we discard a line, we also mark it as a deletion or insertion
-       so that it will be printed in the output.  
-      @param f the other file   
+    /**
+     * Returns whether the integer is not zero (including if it is not null).
      */
-    void discard_confusing_lines(file_data f) {
-      clear();
-    /* Set up table of which lines are going to be discarded. */
-      final byte[] discarded = discardable(f.equivCount());
-
-    /* Don't really discard the provisional lines except when they occur
-       in a run of discardables, with nonprovisionals at the beginning
-       and end.  */
-      filterDiscards(discarded);
-
-    /* Actually discard the lines. */
-      discard(discarded);
+    protected static boolean isNonzero(Integer i)
+    {
+        return i != null && i.intValue() != 0;
     }
 
-    /** Mark to be discarded each line that matches no line of another file.
-       If a line matches many lines, mark it as provisionally discardable.  
-       @see equivCount()
-       @param counts The count of each equivalence number for the other file.
-       @return 0=nondiscardable, 1=discardable or 2=provisionally discardable
-       	for each line
+    /**
+     * Returns whether the value in the map for the given index is greater than
+     * the given value.
      */
-
-    private byte[] discardable(final int[] counts) {
-      final int end = buffered_lines;
-      final byte[] discards = new byte[end];
-      final int[] equivs = this.equivs;
-      int many = 5;
-      int tem = end / 64;
-
-      /* Multiply MANY by approximate square root of number of lines.
-	 That is the threshold for provisionally discardable lines.  */
-      while ((tem = tem >> 2) > 0)
-	many *= 2;
-
-      for (int i = 0; i < end; i++)
-	{
-	  int nmatch;
-	  if (equivs[i] == 0)
-	    continue;
-	  nmatch = counts[equivs[i]];
-	  if (nmatch == 0)
-	    discards[i] = 1;
-	  else if (nmatch > many)
-	    discards[i] = 2;
-	}
-      return discards;
+    protected boolean isGreaterThan(Integer index, Integer val)
+    {
+        Integer lhs = (Integer) thresh.get(index);
+        return lhs != null && val != null && lhs.compareTo(val) > 0;
     }
 
-    /** Don't really discard the provisional lines except when they occur
-       in a run of discardables, with nonprovisionals at the beginning
-       and end.  */
-
-    private void filterDiscards(final byte[] discards) {
-	final int end = buffered_lines;
-
-	for (int i = 0; i < end; i++)
-	  {
-	    /* Cancel provisional discards not in middle of run of discards.  */
-	    if (discards[i] == 2)
-	      discards[i] = 0;
-	    else if (discards[i] != 0)
-	      {
-		/* We have found a nonprovisional discard.  */
-		int j;
-		int length;
-		int provisional = 0;
-
-		/* Find end of this run of discardable lines.
-		   Count how many are provisionally discardable.  */
-		for (j = i; j < end; j++)
-		  {
-		    if (discards[j] == 0)
-		      break;
-		    if (discards[j] == 2)
-		      ++provisional;
-		  }
-
-		/* Cancel provisional discards at end, and shrink the run.  */
-		while (j > i && discards[j - 1] == 2) {
-		  discards[--j] = 0; --provisional;
-		}
-
-		/* Now we have the length of a run of discardable lines
-		   whose first and last are not provisional.  */
-		length = j - i;
-
-		/* If 1/4 of the lines in the run are provisional,
-		   cancel discarding of all provisional lines in the run.  */
-		if (provisional * 4 > length)
-		  {
-		    while (j > i)
-		      if (discards[--j] == 2)
-			discards[j] = 0;
-		  }
-		else
-		  {
-		    int consec;
-		    int minimum = 1;
-		    int tem = length / 4;
-
-		    /* MINIMUM is approximate square root of LENGTH/4.
-		       A subrun of two or more provisionals can stand
-		       when LENGTH is at least 16.
-		       A subrun of 4 or more can stand when LENGTH >= 64.  */
-		    while ((tem = tem >> 2) > 0)
-		      minimum *= 2;
-		    minimum++;
-
-		    /* Cancel any subrun of MINIMUM or more provisionals
-		       within the larger run.  */
-		    for (j = 0, consec = 0; j < length; j++)
-		      if (discards[i + j] != 2)
-			consec = 0;
-		      else if (minimum == ++consec)
-			/* Back up to start of subrun, to cancel it all.  */
-			j -= consec;
-		      else if (minimum < consec)
-			discards[i + j] = 0;
-
-		    /* Scan from beginning of run
-		       until we find 3 or more nonprovisionals in a row
-		       or until the first nonprovisional at least 8 lines in.
-		       Until that point, cancel any provisionals.  */
-		    for (j = 0, consec = 0; j < length; j++)
-		      {
-			if (j >= 8 && discards[i + j] == 1)
-			  break;
-			if (discards[i + j] == 2) {
-			  consec = 0; discards[i + j] = 0;
-			}
-			else if (discards[i + j] == 0)
-			  consec = 0;
-			else
-			  consec++;
-			if (consec == 3)
-			  break;
-		      }
-
-		    /* I advances to the last line of the run.  */
-		    i += length - 1;
-
-		    /* Same thing, from end.  */
-		    for (j = 0, consec = 0; j < length; j++)
-		      {
-			if (j >= 8 && discards[i - j] == 1)
-			  break;
-			if (discards[i - j] == 2) {
-			  consec = 0; discards[i - j] = 0;
-			}
-			else if (discards[i - j] == 0)
-			  consec = 0;
-			else
-			  consec++;
-			if (consec == 3)
-			  break;
-		      }
-		  }
-	      }
-	  }
-      }
-
-    /** Actually discard the lines.
-      @param discards flags lines to be discarded
+    /**
+     * Returns whether the value in the map for the given index is less than
+     * the given value.
      */
-    private void discard(final byte[] discards) {
-      final int end = buffered_lines;
-      int j = 0;
-      for (int i = 0; i < end; ++i)
-	if (no_discards || discards[i] == 0)
-	  {
-	    undiscarded[j] = equivs[i];
-	    realindexes[j++] = i;
-	  }
-	else
-	  changed_flag[1+i] = true;
-      nondiscarded_lines = j;
+    protected boolean isLessThan(Integer index, Integer val)
+    {
+        Integer lhs = (Integer) thresh.get(index);
+        return lhs != null && (val == null || lhs.compareTo(val) < 0);
     }
 
-    file_data(Object[] data,Hashtable h) {
-      buffered_lines = data.length;
-
-      equivs = new int[buffered_lines]; 
-      undiscarded = new int[buffered_lines];
-      realindexes = new int[buffered_lines];
-
-      for (int i = 0; i < data.length; ++i) {
-        Integer ir = (Integer)h.get(data[i]);
-	if (ir == null)
-	  h.put(data[i],new Integer(equivs[i] = equiv_max++));
-	else
-	  equivs[i] = ir.intValue();
-      }
+    /**
+     * Returns the value for the greatest key in the map.
+     */
+    protected Integer getLastValue()
+    {
+        return (Integer) thresh.get(thresh.lastKey());
     }
 
-    /** Adjust inserts/deletes of blank lines to join changes
-       as much as possible.
-
-       We do something when a run of changed lines include a blank
-       line at one end and have an excluded blank line at the other.
-       We are free to choose which blank line is included.
-       `compareseq' always chooses the one at the beginning,
-       but usually it is cleaner to consider the following blank line
-       to be the "change".  The only exception is if the preceding blank line
-       would join this change to other changes.  
-      @param f the file being compared against
-    */
-
-    void shift_boundaries(file_data f) {
-      final boolean[] changed = changed_flag;
-      final boolean[] other_changed = f.changed_flag;
-      int i = 0;
-      int j = 0;
-      int i_end = buffered_lines;
-      int preceding = -1;
-      int other_preceding = -1;
-
-      for (;;)
-	{
-	  int start, end, other_start;
-
-	  /* Scan forwards to find beginning of another run of changes.
-	     Also keep track of the corresponding point in the other file.  */
-
-	  while (i < i_end && !changed[1+i])
-	    {
-	      while (other_changed[1+j++])
-		/* Non-corresponding lines in the other file
-		   will count as the preceding batch of changes.  */
-		other_preceding = j;
-	      i++;
-	    }
-
-	  if (i == i_end)
-	    break;
-
-	  start = i;
-	  other_start = j;
-
-	  for (;;)
-	    {
-	      /* Now find the end of this run of changes.  */
-
-	      while (i < i_end && changed[1+i]) i++;
-	      end = i;
-
-	      /* If the first changed line matches the following unchanged one,
-		 and this run does not follow right after a previous run,
-		 and there are no lines deleted from the other file here,
-		 then classify the first changed line as unchanged
-		 and the following line as changed in its place.  */
-
-	      /* You might ask, how could this run follow right after another?
-		 Only because the previous run was shifted here.  */
-
-	      if (end != i_end
-		  && equivs[start] == equivs[end]
-		  && !other_changed[1+j]
-		  && end != i_end
-		  && !((preceding >= 0 && start == preceding)
-		       || (other_preceding >= 0
-			   && other_start == other_preceding)))
-		{
-		  changed[1+end++] = true;
-		  changed[1+start++] = false;
-		  ++i;
-		  /* Since one line-that-matches is now before this run
-		     instead of after, we must advance in the other file
-		     to keep in synch.  */
-		  ++j;
-		}
-	      else
-		break;
-	    }
-
-	  preceding = i;
-	  other_preceding = j;
-	}
+    /**
+     * 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 = Integer.valueOf(0);
+        }
+        else {
+            Integer lastKey = (Integer) thresh.lastKey();
+            addIdx = Integer.valueOf(lastKey.intValue() + 1);
+        }
+        thresh.put(addIdx, value);
     }
 
-    /** Number of elements (lines) in this file. */
-    final int buffered_lines;
+    /**
+     * Inserts the given values into the threshold map.
+     */
+    protected Integer insert(Integer j, Integer k)
+    {
+        if (isNonzero(k) && isGreaterThan(k, j) && isLessThan(Integer.valueOf(k.intValue() - 1), j)) {
+            thresh.put(k, j);
+        }
+        else {
+            int high = -1;
+            
+            if (isNonzero(k)) {
+                high = k.intValue();
+            }
+            else if (thresh.size() > 0) {
+                high = ((Integer) thresh.lastKey()).intValue();
+            }
 
-    /** Vector, indexed by line number, containing an equivalence code for
-       each line.  It is this vector that is actually compared with that
-       of another file to generate differences. */
-    private final int[]	    equivs;
+            // off the end?
+            if (high == -1 || j.compareTo(getLastValue()) > 0) {
+                append(j);
+                k = Integer.valueOf(high + 1);
+            }
+            else {
+                // binary search for insertion point:
+                int low = 0;
+        
+                while (low <= high) {
+                    int     index = (high + low) / 2;
+                    Integer val   = (Integer) thresh.get(Integer.valueOf(index));
+                    int     cmp   = j.compareTo(val);
 
-    /** Vector, like the previous one except that
-       the elements for discarded lines have been squeezed out.  */
-    final int[]	   undiscarded;
+                    if (cmp == 0) {
+                        return null;
+                    }
+                    else if (cmp > 0) {
+                        low = index + 1;
+                    }
+                    else {
+                        high = index - 1;
+                    }
+                }
+        
+                thresh.put(Integer.valueOf(low), j);
+                k = Integer.valueOf(low);
+            }
+        }
 
-    /** Vector mapping virtual line numbers (not counting discarded lines)
-       to real ones (counting those lines).  Both are origin-0.  */
-    final int[]	   realindexes;
+        return k;
+    }
 
-    /** Total number of nondiscarded lines. */
-    int		    nondiscarded_lines;
+    /**
+     * This is the orignal class used for representation of a single change.
+     * We replace this with our own {@link Change} class for reasons of compatibility
+     * and easier scripting. 
+     */
+    class Difference
+    {
+        public static final int NONE = -1;
 
-    /** Array, indexed by real origin-1 line number,
-       containing true for a line that is an insertion or a deletion.
-       The results of comparison are stored here.  */
-    boolean[]	    changed_flag;
+        /**
+         * 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
+         * NONE means this is an addition.
+         */
+        public int getDeletedStart()
+        {
+            return delStart;
+        }
+
+        /**
+         * The point at which the deletion ends, if any. A value equal to
+         * NONE means this is an addition.
+         */
+        public int getDeletedEnd()
+        {
+            return delEnd;
+        }
+
+        /**
+         * The point at which the addition starts, if any. A value equal to
+         * NONE means this must be an addition.
+         */
+        public int getAddedStart()
+        {
+            return addStart;
+        }
+
+        /**
+         * The point at which the addition ends, if any. A value equal to
+         * NONE 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();
+        }
+
+    }
+
+
+    /**
+     * A legacy adapter that is compatible to the interface of the old GPL licenced Diff.
+     */
+    public static class Change {
+
+        public final Change link;
+        public final int line0;
+        public final int line1;
+        public final int inserted;
+        public final int deleted;
+
+        public static Change fromList(List diffs) {
+            Iterator iter = diffs.iterator();
+            return iter.hasNext() ? new Change(iter, 0, 0) : null;
+        }
+
+        private Change(Iterator iter, int prev0, int prev1) {
+            Difference diff = (Difference) iter.next();
+            if (diff.getDeletedEnd() == Difference.NONE) {
+                line0 = prev0 + diff.getAddedStart() - prev1;
+                deleted = 0;
+            } else {
+                line0 = diff.getDeletedStart();
+                deleted = diff.getDeletedEnd() - line0 + 1;
+            }
+            if (diff.getAddedEnd() == Difference.NONE) {
+                line1 = prev1 + diff.getDeletedStart() - prev0;
+                inserted = 0;
+            } else {
+                line1 = diff.getAddedStart();
+                inserted = diff.getAddedEnd() - line1 + 1;
+            }
+            this.link = iter.hasNext() ? new Change(iter, line0 + deleted, line1 + inserted) : null;
+        }
+    }
 }
+