/** * Copyright (c) 2000-present Liferay, Inc. All rights reserved. * * This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This library 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. */ package com.liferay.portal.diff; import com.liferay.portal.kernel.diff.DiffResult; import com.liferay.portal.kernel.util.FileUtil; import com.liferay.portal.kernel.util.StringBundler; import com.liferay.portal.kernel.util.StringPool; import java.io.Reader; import java.util.ArrayList; import java.util.List; import org.incava.util.diff.Diff; import org.incava.util.diff.Difference; /** * This class can compare two different versions of a text. Source refers to the * earliest version of the text and target refers to a modified version of * source. Changes are considered either as a removal from the source or as an * addition to the target. This class detects changes to an entire line and also * detects changes within lines, such as, removal or addition of characters. * Take a look at <code>DiffTest</code> to see the expected inputs and outputs. * * @author Bruno Farache */ public class DiffImpl implements com.liferay.portal.kernel.diff.Diff { /** * This is a diff method with default values. * * @param source the source text * @param target the modified version of the source text * @return an array containing two lists of <code>DiffResults</code>, the * first element contains DiffResults related to changes in source * and the second element to changes in target */ @Override public List<DiffResult>[] diff(Reader source, Reader target) { int margin = 2; return diff( source, target, OPEN_INS, CLOSE_INS, OPEN_DEL, CLOSE_DEL, margin); } /** * The main entrance of this class. This method will compare the two texts, * highlight the changes by enclosing them with markers and return a list of * <code>DiffResults</code>. * * @param source the source text * @param target the modified version of the source text * @param addedMarkerStart the marker to indicate the start of text added * to the source * @param addedMarkerEnd the marker to indicate the end of text added to * the source * @param deletedMarkerStart the marker to indicate the start of text * deleted from the source * @param deletedMarkerEnd the marker to indicate the end of text deleted * from the source * @param margin the vertical margin to use in displaying differences * between changed line changes * @return an array containing two lists of <code>DiffResults</code>, the * first element contains DiffResults related to changes in source * and the second element to changes in target */ @Override public List<DiffResult>[] diff( Reader source, Reader target, String addedMarkerStart, String addedMarkerEnd, String deletedMarkerStart, String deletedMarkerEnd, int margin) { List<DiffResult> sourceResults = new ArrayList<>(); List<DiffResult> targetResults = new ArrayList<>(); List<DiffResult>[] results = new List[] {sourceResults, targetResults}; // Convert the texts to Lists where each element are lines of the texts. List<String> sourceStringList = FileUtil.toList(source); List<String> targetStringList = FileUtil.toList(target); // Make a a Diff of these lines and iterate over their Differences. Diff diff = new Diff(sourceStringList, targetStringList); List<Difference> differences = diff.diff(); for (Difference difference : differences) { if (difference.getAddedEnd() == Difference.NONE) { // Lines were deleted from source only. _highlightLines( sourceStringList, deletedMarkerStart, deletedMarkerEnd, difference.getDeletedStart(), difference.getDeletedEnd()); margin = _calculateMargin( sourceResults, targetResults, difference.getDeletedStart(), difference.getAddedStart(), margin); List<String> changedLines = _addMargins( sourceStringList, difference.getDeletedStart(), margin); _addResults( sourceResults, sourceStringList, changedLines, difference.getDeletedStart(), difference.getDeletedEnd()); changedLines = _addMargins( targetStringList, difference.getAddedStart(), margin); int deletedLines = difference.getDeletedEnd() + 1 - difference.getDeletedStart(); for (int i = 0; i < deletedLines; i++) { changedLines.add(CONTEXT_LINE); } DiffResult diffResult = new DiffResult( difference.getDeletedStart(), changedLines); targetResults.add(diffResult); } else if (difference.getDeletedEnd() == Difference.NONE) { // Lines were added to target only. _highlightLines( targetStringList, addedMarkerStart, addedMarkerEnd, difference.getAddedStart(), difference.getAddedEnd()); margin = _calculateMargin( sourceResults, targetResults, difference.getDeletedStart(), difference.getAddedStart(), margin); List<String> changedLines = _addMargins( sourceStringList, difference.getDeletedStart(), margin); int addedLines = difference.getAddedEnd() + 1 - difference.getAddedStart(); for (int i = 0; i < addedLines; i++) { changedLines.add(CONTEXT_LINE); } DiffResult diffResult = new DiffResult( difference.getAddedStart(), changedLines); sourceResults.add(diffResult); changedLines = _addMargins( targetStringList, difference.getAddedStart(), margin); _addResults( targetResults, targetStringList, changedLines, difference.getAddedStart(), difference.getAddedEnd()); } else { // Lines were deleted from source and added to target at the // same position. It needs to check for characters differences. _checkCharDiffs( sourceResults, targetResults, sourceStringList, targetStringList, addedMarkerStart, addedMarkerEnd, deletedMarkerStart, deletedMarkerEnd, difference); } } return results; } private static List<String> _addMargins( List<String> stringList, int startPos, int margin) { List<String> changedLines = new ArrayList<>(); if ((margin == 0) || (startPos == 0)) { return changedLines; } int i = startPos - margin; for (; i < 0; i++) { changedLines.add(CONTEXT_LINE); } for (; i < startPos; i++) { if (i < stringList.size()) { changedLines.add(stringList.get(i)); } } return changedLines; } private static void _addResults( List<DiffResult> results, List<String> stringList, List<String> changedLines, int start, int end) { changedLines.addAll(stringList.subList(start, end + 1)); DiffResult diffResult = new DiffResult(start, changedLines); results.add(diffResult); } private static int _calculateMargin( List<DiffResult> sourceResults, List<DiffResult> targetResults, int sourceBeginPos, int targetBeginPos, int margin) { int sourceMargin = _checkOverlapping( sourceResults, sourceBeginPos, margin); int targetMargin = _checkOverlapping( targetResults, targetBeginPos, margin); if (sourceMargin < targetMargin) { return sourceMargin; } return targetMargin; } private static void _checkCharDiffs( List<DiffResult> sourceResults, List<DiffResult> targetResults, List<String> sourceStringList, List<String> targetStringList, String addedMarkerStart, String addedMarkerEnd, String deletedMarkerStart, String deletedMarkerEnd, Difference difference) { boolean aligned = false; int i = difference.getDeletedStart(); int j = difference.getAddedStart(); // A line with changed characters may have its position shifted some // lines above or below. These for loops will try to align these lines. // While these lines are not aligned, highlight them as either additions // or deletions. for (; i <= difference.getDeletedEnd(); i++) { for (; j <= difference.getAddedEnd(); j++) { if (!_isMaxLineLengthExceeded( sourceStringList.get(i), targetStringList.get(j)) && _lineDiff( sourceResults, targetResults, sourceStringList, targetStringList, addedMarkerStart, addedMarkerEnd, deletedMarkerStart, deletedMarkerEnd, i, j, false)) { aligned = true; break; } _highlightLines( targetStringList, addedMarkerStart, addedMarkerEnd, j, j); DiffResult targetResult = new DiffResult( j, targetStringList.subList(j, j + 1)); targetResults.add(targetResult); sourceResults.add(new DiffResult(j, CONTEXT_LINE)); } if (aligned) { break; } _highlightLines( sourceStringList, deletedMarkerStart, deletedMarkerEnd, i, i); DiffResult sourceResult = new DiffResult( i, sourceStringList.subList(i, i + 1)); sourceResults.add(sourceResult); targetResults.add(new DiffResult(i, CONTEXT_LINE)); } i = i + 1; j = j + 1; // Lines are aligned, check for differences of the following lines. for (; i <= difference.getDeletedEnd() && j <= difference.getAddedEnd(); i++, j++) { if (!_isMaxLineLengthExceeded( sourceStringList.get(i), targetStringList.get(j))) { _lineDiff( sourceResults, targetResults, sourceStringList, targetStringList, addedMarkerStart, addedMarkerEnd, deletedMarkerStart, deletedMarkerEnd, i, j, true); } else { _highlightLines( sourceStringList, deletedMarkerStart, deletedMarkerEnd, i, i); DiffResult sourceResult = new DiffResult( i, sourceStringList.subList(i, i + 1)); sourceResults.add(sourceResult); targetResults.add(new DiffResult(i, CONTEXT_LINE)); _highlightLines( targetStringList, addedMarkerStart, addedMarkerEnd, j, j); DiffResult targetResult = new DiffResult( j, targetStringList.subList(j, j + 1)); targetResults.add(targetResult); sourceResults.add(new DiffResult(j, CONTEXT_LINE)); } } // After the for loop above, some lines might remained unchecked. They // are considered as deletions or additions. for (; i <= difference.getDeletedEnd(); i++) { _highlightLines( sourceStringList, deletedMarkerStart, deletedMarkerEnd, i, i); DiffResult sourceResult = new DiffResult( i, sourceStringList.subList(i, i + 1)); sourceResults.add(sourceResult); targetResults.add(new DiffResult(i, CONTEXT_LINE)); } for (; j <= difference.getAddedEnd(); j++) { _highlightLines( targetStringList, addedMarkerStart, addedMarkerEnd, j, j); DiffResult targetResult = new DiffResult( j, targetStringList.subList(j, j + 1)); targetResults.add(targetResult); sourceResults.add(new DiffResult(j, CONTEXT_LINE)); } } private static int _checkOverlapping( List<DiffResult> results, int startPos, int margin) { if (results.isEmpty() || ((startPos - margin) < 0)) { return margin; } DiffResult lastDiff = results.get(results.size() - 1); List<String> changedLines = lastDiff.getChangedLines(); if (changedLines.isEmpty()) { return margin; } int lastChangedLine = (lastDiff.getLineNumber() - 1) + changedLines.size(); int currentChangedLine = startPos - margin; if ((changedLines.size() == 1) && changedLines.get(0).equals(CONTEXT_LINE)) { currentChangedLine = currentChangedLine + 1; } if (currentChangedLine < lastChangedLine) { return margin + currentChangedLine - lastChangedLine; } return margin; } private static void _highlightChars( List<String> stringList, String markerStart, String markerEnd, int startPos, int endPos) { String start = markerStart + stringList.get(startPos); stringList.set(startPos, start); String end = stringList.get(endPos) + markerEnd; stringList.set(endPos, end); } private static void _highlightLines( List<String> stringList, String markerStart, String markerEnd, int startPos, int endPos) { for (int i = startPos; i <= endPos; i++) { stringList.set(i, markerStart + stringList.get(i) + markerEnd); } } private static boolean _isMaxLineLengthExceeded( String sourceString, String targetString) { if ((sourceString.length() > _DIFF_MAX_LINE_LENGTH) || (targetString.length() > _DIFF_MAX_LINE_LENGTH)) { return true; } return false; } private static boolean _lineDiff( List<DiffResult> sourceResults, List<DiffResult> targetResults, List<String> sourceStringList, List<String> targetStringList, String addedMarkerStart, String addedMarkerEnd, String deletedMarkerStart, String deletedMarkerEnd, int sourceChangedLine, int targetChangedLine, boolean aligned) { String source = sourceStringList.get(sourceChangedLine); String target = targetStringList.get(targetChangedLine); // Convert the lines to lists where each element are chars of the lines. List<String> sourceList = _toList(source); List<String> targetList = _toList(target); Diff diff = new Diff(sourceList, targetList); List<Difference> differences = diff.diff(); int deletedChars = 0; int addedChars = 0; // The following while loop will calculate how many characters of the // source line need to be changed to be equals to the target line. if (!aligned) { for (Difference difference : differences) { if (difference.getDeletedEnd() != Difference.NONE) { deletedChars += difference.getDeletedEnd() - difference.getDeletedStart() + 1; } if (difference.getAddedEnd() != Difference.NONE) { addedChars += difference.getAddedEnd() - difference.getAddedStart() + 1; } } } // If a lot of changes were needed (more than half of the source line // length), consider this as not aligned yet. if ((deletedChars > (sourceList.size() / 2)) || (addedChars > (sourceList.size() / 2))) { return false; } boolean sourceChanged = false; boolean targetChanged = false; // Iterate over Differences between chars of these lines. for (Difference difference : differences) { if (difference.getAddedEnd() == Difference.NONE) { // Chars were deleted from source only. _highlightChars( sourceList, deletedMarkerStart, deletedMarkerEnd, difference.getDeletedStart(), difference.getDeletedEnd()); sourceChanged = true; } else if (difference.getDeletedEnd() == Difference.NONE) { // Chars were added to target only. _highlightChars( targetList, addedMarkerStart, addedMarkerEnd, difference.getAddedStart(), difference.getAddedEnd()); targetChanged = true; } else { // Chars were both deleted and added. _highlightChars( sourceList, deletedMarkerStart, deletedMarkerEnd, difference.getDeletedStart(), difference.getDeletedEnd()); sourceChanged = true; _highlightChars( targetList, addedMarkerStart, addedMarkerEnd, difference.getAddedStart(), difference.getAddedEnd()); targetChanged = true; } } if (sourceChanged) { DiffResult sourceResult = new DiffResult( sourceChangedLine, _toString(sourceList)); sourceResults.add(sourceResult); if (!targetChanged) { DiffResult targetResult = new DiffResult( targetChangedLine, target); targetResults.add(targetResult); } } if (targetChanged) { if (!sourceChanged) { DiffResult sourceResult = new DiffResult( sourceChangedLine, source); sourceResults.add(sourceResult); } DiffResult targetResult = new DiffResult( targetChangedLine, _toString(targetList)); targetResults.add(targetResult); } return true; } private static List<String> _toList(String line) { List<String> result = new ArrayList<>(line.length()); for (int i = 0; i < line.length(); i++) { result.add(line.substring(i, i + 1)); } return result; } private static String _toString(List<String> line) { if (line.isEmpty()) { return StringPool.BLANK; } StringBundler sb = new StringBundler(line.size()); for (String linePart : line) { sb.append(linePart); } return sb.toString(); } private static final int _DIFF_MAX_LINE_LENGTH = 5000; }