[jboss-cvs] jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/util ...

Christian Bauer christian at hibernate.org
Sun Sep 2 05:39:44 EDT 2007


  User: cbauer  
  Date: 07/09/02 05:39:44

  Modified:    examples/wiki/src/main/org/jboss/seam/wiki/util  Diff.java
  Log:
  New diff and improvements to history function
  
  Revision  Changes    Path
  1.5       +572 -79   jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/util/Diff.java
  
  (In the diff below, changes in quantity of whitespace are not shown.)
  
  Index: Diff.java
  ===================================================================
  RCS file: /cvsroot/jboss/jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/util/Diff.java,v
  retrieving revision 1.4
  retrieving revision 1.5
  diff -u -b -r1.4 -r1.5
  --- Diff.java	17 Aug 2007 13:00:31 -0000	1.4
  +++ Diff.java	2 Sep 2007 09:39:44 -0000	1.5
  @@ -1,105 +1,598 @@
  -/*
  - * JBoss, Home of Professional Open Source
  - *
  - * Distributable under LGPL license.
  - * See terms of license at gnu.org.
  - */
   package org.jboss.seam.wiki.util;
   
  -import java.util.Arrays;
  -import java.util.List;
  +import java.util.*;
  +
   
   /**
  - * String comparison and diff algorithms with customizable result rendering.
  - * <p>
  - * TODO: Support new methods for word and character diff
  - * <p>
  - * @author Christian Bauer
  + * 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 abstract class Diff {
  +public class Diff
  +{
  +
  +    public static final int NONE = -1;
   
       /**
  -     * Compares two strings, left and right side, renders a new string with custom boundary markers
  -     * for deleted and added lines.
  -     *
  -     * @param x The "left" side of the comparison.
  -     * @param y The "right" side of the comparison.
  -     * @param ignoreStrings These strings are ignored and not marked as different.
  -     * @return String A result with all deletions and modifications highlighted with custom boundary markers.
  -     */
  -    public String[] createDiff(String[] x, String[] y, String... ignoreStrings) {
  -        List<String> ignoreList = Arrays.asList(ignoreStrings);
  -
  -        int M = x.length;
  -        int N = y.length;
  -
  -        String[] result = new String[M + N]; // N + M? Not nice but safe
  -        int k = 0;
  -
  -        int[][] opt = new int[M + 1][N + 1];
  -
  -        for (int i = M - 1; i >= 0; i--) {
  -            for (int j = N - 1; j >= 0; j--) {
  -                if (x[i].equals(y[j]))
  -                    opt[i][j] = opt[i + 1][j + 1] + 1;
  -                else
  -                    opt[i][j] = Math.max(opt[i + 1][j], opt[i][j + 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;
           }
   
  -        int i = 0, j = 0;
  -        while (i < M && j < N) {
  -            if (x[i].equals(y[j])) {
  -                result[k++] = (x[i]);
  -                i++;
  -                j++;
  -            } else if (opt[i + 1][j] >= opt[i][j + 1]) {
  -                if (ignoreList.contains(x[i]) || "".equals(x[i]) ) {
  -                    result[k++] = x[i++];
  -                } else {
  -                    result[k++] = getDeletionStartMarker() + x[i++] + getDeletionEndMarker();
  +        /**
  +         * 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;
                   }
  -            } else {
  -                if (ignoreList.contains(y[j]) || "".equals(y[j]) ) {
  -                    result[k++] = y[j++];
  -                 } else {
  -                     result[k++] = getAdditionStartMarker() + y[j++] + getAdditionEndMarker();
  +
  +        /**
  +         * 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);
           }
   
  -        while (i < M || j < N) {
  -            if (j == N)  {
  -                if (ignoreList.contains(x[i]) || "".equals(x[i]) ) {
  -                    result[k++] = x[i++];
  -                } else {
  -                    result[k++] = getDeletionStartMarker() + x[i++] + getDeletionEndMarker();
  +        /**
  +         * 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);
                   }
  -            } else if (i == M) {
  -                if (ignoreList.contains(y[j]) || "".equals(y[j]) ) {
  -                    result[k++] = y[j++];
  +
  +        /**
  +         * 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 {
  -                     result[k++] = getAdditionStartMarker() + y[j++] + getAdditionEndMarker();
  +                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);
               }
           }
  -        return result;
       }
   
  -    public static String renderWithDelimiter(String[] strings, String delimiter) {
  -        StringBuilder diff = new StringBuilder();
  -        for (String s: strings) {
  -            if (s != null) {
  -                diff.append(s);
  -                diff.append(delimiter);
  +            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);
           }
  -        return diff.toString();
  +
  +    /**
  +     * Returns the value for the greatest key in the map.
  +     */
  +    protected Integer getLastValue()
  +    {
  +        return (Integer)thresh.get(thresh.lastKey());
       }
   
  -    protected abstract String getDeletionStartMarker();
  -    protected abstract String getDeletionEndMarker();
  -    protected abstract String getAdditionStartMarker();
  -    protected abstract String getAdditionEndMarker();
  +    /**
  +     * 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;
  +    }
   
   }
  
  
  



More information about the jboss-cvs-commits mailing list