org.jamwiki.utils.DiffUtil.java Source code

Java tutorial

Introduction

Here is the source code for org.jamwiki.utils.DiffUtil.java

Source

/**
 * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE, version 2.1, dated February 1999.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the latest version of the GNU Lesser General
 * Public License as published by the Free Software Foundation;
 *
 * 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program (LICENSE.txt); if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package org.jamwiki.utils;

import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.incava.util.diff.Diff;
import org.incava.util.diff.Difference;
import org.jamwiki.model.WikiDiff;

/**
 * Utility class for processing the difference between two topics and returing a
 * list of WikiDiff objects that can be used to display the diff.
 */
public class DiffUtil {

    private static final WikiLogger logger = WikiLogger.getLogger(DiffUtil.class.getName());
    /**
     * The number of lines of unchanged text to display before and after each
     * diff.
     */
    // FIXME - make this a property value
    private static final int DIFF_UNCHANGED_LINE_DISPLAY = 2;
    /** Cache name for the cache of diff information. */
    private static final String CACHE_DIFF_INFORMATION = "org.jamwiki.utils.DiffUtil.CACHE_DIFF_INFORMATION";

    /**
      *
      */
    private DiffUtil() {
    }

    /**
      *
      */
    // private static void addToCache(String newVersion, String oldVersion,
    // List<WikiDiff> results) {
    // String key = generateCacheKey(newVersion, oldVersion);
    // WikiCache.addToCache(CACHE_DIFF_INFORMATION, key, results);
    // }

    /**
     * Utility method for determining whether or not to append lines of context
     * around a diff.
     */
    private static boolean canPostBuffer(Difference nextDiff, int current, String[] replacementArray,
            boolean adding) {
        if (current < 0 || current >= replacementArray.length) {
            // if out of a valid range, don't buffer
            return false;
        }
        if (nextDiff == null) {
            // if in a valid range and no next diff, buffer away
            return true;
        }
        int nextStart = (adding) ? nextDiff.getAddedStart() : nextDiff.getDeletedStart();
        // if in a valid range and the next diff starts several lines away, buffer
        // away. otherwise
        // the default is not to diff.
        return (nextStart > current);
    }

    /**
     * Utility method for determining whether or not to prepend lines of context
     * around a diff.
     */
    private static boolean canPreBuffer(Difference previousDiff, int current, int currentStart,
            String[] replacementArray, int bufferAmount, boolean adding) {
        if (current < 0 || current >= replacementArray.length) {
            // current position is out of range for buffering
            return false;
        }
        if (previousDiff == null) {
            // if no previous diff, buffer away
            return true;
        }
        if (bufferAmount == -1) {
            // if everything is being buffered and there was a previous diff do not
            // pre-buffer
            return false;
        }
        int previousEnd = (adding) ? previousDiff.getAddedEnd() : previousDiff.getDeletedEnd();
        if (previousEnd != -1) {
            // if there was a previous diff but it was several lines previous, buffer
            // away.
            // if there was a previous diff, and it overlaps with the current diff,
            // don't buffer.
            return (current > (previousEnd + bufferAmount));
        }
        int previousStart = (adding) ? previousDiff.getAddedStart() : previousDiff.getDeletedStart();
        if (current <= (previousStart + bufferAmount)) {
            // the previous diff did not specify an end, and the current diff would
            // overlap with
            // buffering from its start, don't buffer
            return false;
        }
        // the previous diff did not specify an end, and the current diff will not
        // overlap
        // with buffering from its start, buffer away. otherwise the default is not
        // to buffer.
        return (currentStart > current);
    }

    /**
     * Return a list of WikiDiff objects that can be used to create a display of
     * the diff content.
     * 
     * @param newVersion
     *          The String that is to be compared to, ie the later version of a
     *          topic.
     * @param oldVersion
     *          The String that is to be considered as having changed, ie the
     *          earlier version of a topic.
     * @return Returns a list of WikiDiff objects that correspond to the changed
     *         text.
     */
    public static List<WikiDiff> diff(String newVersion, String oldVersion) {
        List<WikiDiff> result = null;
        // List<WikiDiff> result = DiffUtil.retrieveFromCache(newVersion,
        // oldVersion);
        // if (result != null) {
        // return result;
        // }
        String version1 = newVersion;
        String version2 = oldVersion;
        if (version2 == null) {
            version2 = "";
        }
        if (version1 == null) {
            version1 = "";
        }
        // remove line-feeds to avoid unnecessary noise in the diff due to
        // cut & paste or other issues
        version2 = StringUtils.remove(version2, '\r');
        version1 = StringUtils.remove(version1, '\r');
        result = DiffUtil.process(version1, version2);
        // DiffUtil.addToCache(newVersion, oldVersion, result);
        return result;
    }

    /**
     * Generate a mostly-unique key to use for the cache. This key uses the first
     * ten characters of the string and a hash of the full string, which is not
     * guaranteed to be unique but should be unique enough.
     */
    private static String generateCacheKey(String newVersion, String oldVersion) {
        StringBuilder result = new StringBuilder();
        if (newVersion == null) {
            result.append(-1);
        } else if (newVersion.length() <= 10) {
            result.append(newVersion);
        } else {
            result.append(newVersion.substring(0, 10)).append(newVersion.hashCode());
        }
        result.append('-');
        if (oldVersion == null) {
            result.append(-1);
        } else if (oldVersion.length() <= 10) {
            result.append(oldVersion);
        } else {
            result.append(oldVersion.substring(0, 10)).append(oldVersion.hashCode());
        }
        return result.toString();
    }

    /**
     * Format the list of Difference objects into a list of WikiDiff objects,
     * which will include information about what values are different and also
     * include some unchanged values surrounded the changed values, thus giving
     * some context.
     */
    private static List<WikiDiff> generateWikiDiffs(List<Difference> diffs, String[] oldArray, String[] newArray) {
        List<WikiDiff> wikiDiffs = new ArrayList<WikiDiff>();
        Difference previousDiff = null;
        Difference nextDiff = null;
        List<WikiDiff> changedLineWikiDiffs = null;
        String[] oldLineArray = null;
        String[] newLineArray = null;
        List<Difference> changedLineDiffs = null;
        List<WikiDiff> wikiSubDiffs = null;
        Difference nextLineDiff = null;
        int i = 0;
        for (Difference currentDiff : diffs) {
            i++;
            wikiDiffs.addAll(DiffUtil.preBufferDifference(currentDiff, previousDiff, oldArray, newArray,
                    DIFF_UNCHANGED_LINE_DISPLAY));
            changedLineWikiDiffs = DiffUtil.processDifference(currentDiff, oldArray, newArray);
            // loop through the difference and diff the individual lines so that it is
            // possible to highlight the exact
            // text that was changed
            for (WikiDiff changedLineWikiDiff : changedLineWikiDiffs) {
                oldLineArray = DiffUtil.stringToArray(changedLineWikiDiff.getOldText());
                newLineArray = DiffUtil.stringToArray(changedLineWikiDiff.getNewText());
                changedLineDiffs = new Diff<String>(oldLineArray, newLineArray).diff();
                wikiSubDiffs = new ArrayList<WikiDiff>();
                int j = 0;
                for (Difference changedLineDiff : changedLineDiffs) {
                    // build sub-diff list, which is the difference for the individual
                    // line item
                    j++;
                    if (j == 1) {
                        // pre-buffering is only necessary for the first element as
                        // post-buffering
                        // will handle all further buffering when bufferAmount is -1.
                        wikiSubDiffs.addAll(DiffUtil.preBufferDifference(changedLineDiff, null, oldLineArray,
                                newLineArray, -1));
                    }
                    wikiSubDiffs.addAll(DiffUtil.processDifference(changedLineDiff, oldLineArray, newLineArray));
                    nextLineDiff = (j < changedLineDiffs.size()) ? changedLineDiffs.get(j) : null;
                    wikiSubDiffs.addAll(DiffUtil.postBufferDifference(changedLineDiff, nextLineDiff, oldLineArray,
                            newLineArray, -1));
                }
                changedLineWikiDiff.setSubDiffs(wikiSubDiffs);
            }
            wikiDiffs.addAll(changedLineWikiDiffs);
            nextDiff = (i < diffs.size()) ? diffs.get(i) : null;
            wikiDiffs.addAll(DiffUtil.postBufferDifference(currentDiff, nextDiff, oldArray, newArray,
                    DIFF_UNCHANGED_LINE_DISPLAY));
            previousDiff = currentDiff;
        }
        return wikiDiffs;
    }

    /**
      *
      */
    private static boolean hasMoreDiffInfo(int addedCurrent, int deletedCurrent, Difference currentDiff) {
        if (addedCurrent == -1) {
            addedCurrent = 0;
        }
        if (deletedCurrent == -1) {
            deletedCurrent = 0;
        }
        return (addedCurrent <= currentDiff.getAddedEnd() || deletedCurrent <= currentDiff.getDeletedEnd());
    }

    /**
     * If possible, append a few lines of unchanged text that appears after to the
     * changed line in order to add context to the current list of WikiDiff
     * objects.
     * 
     * @param currentDiff
     *          The current diff object.
     * @param nextDiff
     *          The diff object that immediately follows this object (if any).
     * @param oldArray
     *          The original array of string objects that was compared from in
     *          order to generate the diff.
     * @param newArray
     *          The original array of string objects that was compared to in order
     *          to generate the diff.
     * @param bufferAmount
     *          The number of unchanged elements to display after the diff, or -1
     *          if all unchanged lines should be displayed.
     */
    private static List<WikiDiff> postBufferDifference(Difference currentDiff, Difference nextDiff,
            String[] oldArray, String[] newArray, int bufferAmount) {
        List<WikiDiff> wikiDiffs = new ArrayList<WikiDiff>();
        if (bufferAmount == 0) {
            // do not buffer
            return wikiDiffs;
        }
        int deletedCurrent = (currentDiff.getDeletedEnd() == -1) ? currentDiff.getDeletedStart()
                : (currentDiff.getDeletedEnd() + 1);
        int addedCurrent = (currentDiff.getAddedEnd() == -1) ? currentDiff.getAddedStart()
                : (currentDiff.getAddedEnd() + 1);
        int numIterations = bufferAmount;
        if (bufferAmount == -1) {
            // buffer everything
            numIterations = (nextDiff != null)
                    ? Math.max(nextDiff.getAddedStart() - addedCurrent, nextDiff.getDeletedStart() - deletedCurrent)
                    : Math.max(oldArray.length - deletedCurrent, newArray.length - addedCurrent);
        }
        String oldText = null;
        String newText = null;
        for (int i = 0; i < numIterations; i++) {
            int position = (deletedCurrent < 0) ? 0 : deletedCurrent;
            oldText = null;
            newText = null;
            if (canPostBuffer(nextDiff, deletedCurrent, oldArray, false)) {
                oldText = oldArray[deletedCurrent];
                deletedCurrent++;
            }
            if (canPostBuffer(nextDiff, addedCurrent, newArray, true)) {
                newText = newArray[addedCurrent];
                addedCurrent++;
            }
            if (oldText == null && newText == null) {
                logger.fine("Possible DIFF bug: no elements post-buffered.  position: " + position
                        + " / deletedCurrent: " + deletedCurrent + " / addedCurrent " + addedCurrent
                        + " / numIterations: " + numIterations);
                break;
            }
            wikiDiffs.add(new WikiDiff(oldText, newText, position));
        }
        return wikiDiffs;
    }

    /**
     * If possible, prepend a few lines of unchanged text that before after to the
     * changed line in order to add context to the current list of WikiDiff
     * objects.
     * 
     * @param currentDiff
     *          The current diff object.
     * @param previousDiff
     *          The diff object that immediately preceded this object (if any).
     * @param oldArray
     *          The original array of string objects that was compared from in
     *          order to generate the diff.
     * @param newArray
     *          The original array of string objects that was compared to in order
     *          to generate the diff.
     * @param bufferAmount
     *          The number of unchanged elements to display after the diff, or -1
     *          if all unchanged lines should be displayed.
     */
    private static List<WikiDiff> preBufferDifference(Difference currentDiff, Difference previousDiff,
            String[] oldArray, String[] newArray, int bufferAmount) {
        List<WikiDiff> wikiDiffs = new ArrayList<WikiDiff>();
        if (bufferAmount == 0) {
            return wikiDiffs;
        }
        if (bufferAmount == -1 && previousDiff != null) {
            // when buffering everything, only pre-buffer for the first element as the
            // post-buffer code
            // will handle everything else.
            return wikiDiffs;
        }
        // deletedCurrent is the current position in oldArray to start buffering
        // from
        int deletedCurrent = (bufferAmount == -1 || bufferAmount > currentDiff.getDeletedStart()) ? 0
                : (currentDiff.getDeletedStart() - bufferAmount);
        // addedCurrent is the current position in newArray to start buffering from
        int addedCurrent = (bufferAmount == -1 || bufferAmount > currentDiff.getAddedStart()) ? 0
                : (currentDiff.getAddedStart() - bufferAmount);
        if (previousDiff != null) {
            // if there was a previous diff make sure that it is not being overlapped
            deletedCurrent = Math.max(previousDiff.getDeletedEnd() + 1, deletedCurrent);
            addedCurrent = Math.max(previousDiff.getAddedEnd() + 1, addedCurrent);
        }
        // number of iterations is number of loops required to fully buffer the
        // added and deleted diff
        int numIterations = Math.max(currentDiff.getDeletedStart() - deletedCurrent,
                currentDiff.getAddedStart() - addedCurrent);
        String oldText = null;
        String newText = null;
        for (int i = 0; i < numIterations; i++) {
            int position = (deletedCurrent < 0) ? 0 : deletedCurrent;
            oldText = null;
            newText = null;
            // if diffs are close together, do not allow buffers to overlap
            if (canPreBuffer(previousDiff, deletedCurrent, currentDiff.getDeletedStart(), oldArray, bufferAmount,
                    false)) {
                oldText = oldArray[deletedCurrent];
                deletedCurrent++;
            }
            if (canPreBuffer(previousDiff, addedCurrent, currentDiff.getAddedStart(), newArray, bufferAmount,
                    true)) {
                newText = newArray[addedCurrent];
                addedCurrent++;
            }
            if (oldText == null && newText == null) {
                logger.fine("Possible DIFF bug: no elements pre-buffered.  position: " + position
                        + " / deletedCurrent: " + deletedCurrent + " / addedCurrent " + addedCurrent
                        + " / numIterations: " + numIterations);
                break;
            }
            wikiDiffs.add(new WikiDiff(oldText, newText, position));
        }
        return wikiDiffs;
    }

    /**
     * @param newVersion
     *          The String that is being compared to.
     * @param oldVersion
     *          The String that is being compared against.
     */
    private static List<WikiDiff> process(String newVersion, String oldVersion) {
        logger.finer("Diffing: " + oldVersion + " against: " + newVersion);
        if (newVersion.equals(oldVersion)) {
            return new ArrayList<WikiDiff>();
        }
        String[] oldArray = DiffUtil.split(oldVersion);
        String[] newArray = DiffUtil.split(newVersion);
        Diff<String> diffObject = new Diff<String>(oldArray, newArray);
        List<Difference> diffs = diffObject.diff();
        return DiffUtil.generateWikiDiffs(diffs, oldArray, newArray);
    }

    /**
     * Process the diff object and add it to the output. Text will either have
     * been deleted or added (it cannot have remained the same, since a diff
     * object represents a change). This method steps through the diff result and
     * converts it into an array of objects that can be used to easily represent
     * the diff.
     */
    private static List<WikiDiff> processDifference(Difference currentDiff, String[] oldArray, String[] newArray) {
        List<WikiDiff> wikiDiffs = new ArrayList<WikiDiff>();
        // if text was deleted then deletedCurrent represents the starting position
        // of the deleted text.
        int deletedCurrent = currentDiff.getDeletedStart();
        // if text was added then addedCurrent represents the starting position of
        // the added text.
        int addedCurrent = currentDiff.getAddedStart();
        // count is simply used to ensure that the loop is not infinite, which
        // should never happen
        int count = 0;
        // the text of the element that changed
        String oldText = null;
        // the text of what the element was changed to
        String newText = null;
        while (hasMoreDiffInfo(addedCurrent, deletedCurrent, currentDiff)) {
            // the position within the diff array (line number, character, etc) at
            // which the change
            // started (starting at 0)
            int position = ((deletedCurrent < 0) ? 0 : deletedCurrent);
            oldText = null;
            newText = null;
            if (currentDiff.getDeletedEnd() >= 0 && currentDiff.getDeletedEnd() >= deletedCurrent) {
                oldText = oldArray[deletedCurrent];
                deletedCurrent++;
            }
            if (currentDiff.getAddedEnd() >= 0 && currentDiff.getAddedEnd() >= addedCurrent) {
                newText = newArray[addedCurrent];
                addedCurrent++;
            }
            wikiDiffs.add(new WikiDiff(oldText, newText, position));
            // FIXME - this shouldn't be necessary
            count++;
            if (count > 5000) {
                logger.warning("Infinite loop in DiffUtils.processDifference");
                break;
            }
        }
        return wikiDiffs;
    }

    /**
     * Determine if diff information is available in the cache. If so return it,
     * otherwise return <code>null</code>.
     */
    // private static List<WikiDiff> retrieveFromCache(String newVersion, String
    // oldVersion) {
    // String key = generateCacheKey(newVersion, oldVersion);
    // Element cachedDiffInformation =
    // WikiCache.retrieveFromCache(CACHE_DIFF_INFORMATION, key);
    // return (cachedDiffInformation != null) ?
    // (List<WikiDiff>)cachedDiffInformation.getObjectValue() : null;
    // }

    /**
     * Split up a String into an array of values using the specified string
     * pattern.
     * 
     * @param original
     *          The value that is being split.
     */
    private static String[] split(String original) {
        if (original == null) {
            return new String[0];
        }
        return original.split("\n");
    }

    /**
     * Convert a string to a string array of characters.
     * 
     * @param original
     *          The value that is being split.
     */
    private static String[] stringToArray(String original) {
        if (original == null) {
            return new String[0];
        }
        String[] result = new String[original.length()];
        for (int i = 0; i < result.length; i++) {
            result[i] = String.valueOf(original.charAt(i));
        }
        return result;
    }
}