/**
* 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;
}
}