
org.xwiki.diff.internal.DefaultDiffManager Maven / Gradle / Ivy
/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This 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 software 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 software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.diff.internal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.diff.Chunk;
import org.xwiki.diff.ConflictDecision;
import org.xwiki.diff.Delta;
import org.xwiki.diff.Delta.Type;
import org.xwiki.diff.DiffConfiguration;
import org.xwiki.diff.DiffException;
import org.xwiki.diff.DiffManager;
import org.xwiki.diff.DiffResult;
import org.xwiki.diff.MergeConfiguration;
import org.xwiki.diff.MergeConfiguration.Version;
import org.xwiki.diff.MergeException;
import org.xwiki.diff.MergeResult;
import org.xwiki.diff.Patch;
import com.github.difflib.DiffUtils;
/**
* Default implementation of {@link DiffManager}.
*
* @version $Id: 356a2eafa680be69a6af00731e4e3c2d54fd3cb4 $
*/
@Component
@Singleton
public class DefaultDiffManager implements DiffManager
{
@Inject
private Logger logger;
@Override
public DiffResult diff(List previous, List next, DiffConfiguration diff) throws DiffException
{
DefaultDiffResult result = new DefaultDiffResult<>(previous, next);
// DiffUtils#diff does not support null
Patch patch;
if (previous == null || previous.isEmpty()) {
patch = new DefaultPatch<>();
if (next != null && !next.isEmpty()) {
patch.add(new InsertDelta<>(new DefaultChunk<>(0, Collections.emptyList()),
new DefaultChunk<>(0, next)));
}
} else if (next == null || next.isEmpty()) {
patch = new DefaultPatch<>();
patch.add(new DeleteDelta<>(new DefaultChunk<>(0, previous),
new DefaultChunk<>(0, Collections.emptyList())));
} else {
patch = new DefaultPatch<>(DiffUtils.diff(previous, next));
}
result.setPatch(patch);
return result;
}
@Override
public MergeResult merge(List commonAncestor, List next, List current,
MergeConfiguration configuration) throws MergeException
{
DefaultMergeResult mergeResult = new DefaultMergeResult<>(commonAncestor, next, current);
// Get diff between common ancestor and next version
DiffResult diffNextResult;
try {
diffNextResult = diff(commonAncestor, next, null);
} catch (DiffException e) {
throw new MergeException("Fail to diff between common ancestor and next version", e);
}
mergeResult.getLog().addAll(diffNextResult.getLog());
Patch patchNext = diffNextResult.getPatch();
// If there is no modification stop there
if (patchNext.isEmpty()) {
// No change so nothing to do
return mergeResult;
}
// Check current version
if (current.isEmpty() && (commonAncestor.isEmpty() || next.isEmpty())) {
// Empty current version
if (commonAncestor.isEmpty()) {
mergeResult.setMerged(next);
} else if (next.isEmpty()) {
// The new modification was already applied
mergeResult.getLog().warn("The modification was already applied");
}
} else {
// Get diff between common ancestor and current version
DiffResult diffCurrentResult;
try {
diffCurrentResult = diff(commonAncestor, current, null);
} catch (DiffException e) {
throw new MergeException("Fail to diff between common ancestor and current version", e);
}
mergeResult.getLog().addAll(diffCurrentResult.getLog());
Patch patchCurrent = diffCurrentResult.getPatch();
mergeResult.setMerged(new ArrayList<>());
if (patchCurrent.isEmpty()) {
mergeResult.setMerged(next);
} else if (!current.equals(next) && (isFullyModified(commonAncestor, patchCurrent)
|| isFullyModified(commonAncestor, patchNext))) {
// If current or next is completely modified compared to the common ancestor we assume
// any change in next or current is a conflict
// ... except if the current content is identical to the next one!
Delta deltaNext = nextElement(patchNext);
Delta deltaCurrent = nextElement(patchCurrent);
List> conflictDecisionList =
(configuration != null) ? configuration.getConflictDecisionList() : Collections.emptyList();
int newIndex = applyDecision(conflictDecisionList, mergeResult.getMerged(), 0);
if (newIndex == Integer.MIN_VALUE) {
logConflict(mergeResult, deltaCurrent, deltaNext, commonAncestor, next, current, 0);
mergeResult.setMerged(fallback(commonAncestor, next, current, configuration));
}
} else {
merge(mergeResult, commonAncestor, next, current, patchNext, patchCurrent, configuration);
}
}
return mergeResult;
}
private ConflictDecision findDecision(List> decisions, int currentIndex)
{
ConflictDecision result = null;
if (decisions != null && !decisions.isEmpty()) {
for (ConflictDecision decision : decisions) {
if (decision.getConflict().getIndex() == currentIndex) {
result = decision;
break;
}
}
}
return result;
}
private int applyDecision(List> decisions, List merged, int currentIndex)
{
ConflictDecision conflictDecision = findDecision(decisions, currentIndex);
if (conflictDecision == null || conflictDecision.getType() == ConflictDecision.DecisionType.UNDECIDED) {
return Integer.MIN_VALUE;
} else {
merged.addAll(conflictDecision.getChunk().getElements());
return merged.size();
}
}
private int applyDecision(List> decisions, List commonAncestor, Delta deltaNext,
Delta deltaCurrent, List merged, int currentIndex)
{
ConflictDecision conflictDecision = findDecision(decisions, currentIndex);
if (conflictDecision == null || conflictDecision.getType() == ConflictDecision.DecisionType.UNDECIDED) {
return Integer.MIN_VALUE;
} else {
switch (conflictDecision.getType()) {
case PREVIOUS:
return fallback(commonAncestor, deltaNext, deltaCurrent, merged, currentIndex, Version.PREVIOUS);
case NEXT:
return fallback(commonAncestor, deltaNext, deltaCurrent, merged, currentIndex, Version.NEXT);
case CURRENT:
return fallback(commonAncestor, deltaNext, deltaCurrent, merged, currentIndex, Version.CURRENT);
case CUSTOM:
merged.addAll(conflictDecision.getChunk().getElements());
return Math.max(deltaCurrent.getPrevious().getLastIndex(), deltaNext.getPrevious().getLastIndex());
default:
// should never occur
throw new IllegalArgumentException();
}
}
}
private List fallback(List commonAncestor, List next, List current,
MergeConfiguration configuration)
{
Version fallbackVersion;
if (configuration != null) {
fallbackVersion = configuration.getFallbackOnConflict();
} else {
fallbackVersion = Version.CURRENT;
}
switch (fallbackVersion) {
case NEXT:
return next;
case PREVIOUS:
return commonAncestor;
default:
return current;
}
}
private int fallback(List commonAncestor, Delta deltaNext, Delta deltaCurrent, List merged,
int currentIndex, MergeConfiguration configuration)
{
Version fallbackVersion;
if (configuration != null) {
fallbackVersion = configuration.getFallbackOnConflict();
} else {
fallbackVersion = Version.CURRENT;
}
return fallback(commonAncestor, deltaNext, deltaCurrent, merged, currentIndex, fallbackVersion);
}
private int fallback(List commonAncestor, Delta deltaNext, Delta deltaCurrent, List merged,
int currentIndex, Version fallbackVersion)
{
int newIndex = currentIndex;
switch (fallbackVersion) {
case NEXT:
for (; newIndex < deltaNext.getPrevious().getIndex(); ++newIndex) {
merged.add(commonAncestor.get(newIndex));
}
newIndex = apply(deltaNext, merged, currentIndex);
break;
case PREVIOUS:
int stopIndex =
Math.max(deltaCurrent.getPrevious().getLastIndex(), deltaNext.getPrevious().getLastIndex()) + 1;
for (; newIndex < stopIndex; ++newIndex) {
merged.add(commonAncestor.get(newIndex));
}
if (currentIndex != newIndex) {
// each time this fallback is called, the loop increment back the index
// so we have to decrement it to be sure we are at the right position.
newIndex--;
}
break;
default:
// CURRENT is the default
for (; newIndex < deltaCurrent.getPrevious().getIndex(); ++newIndex) {
merged.add(commonAncestor.get(newIndex));
}
newIndex = apply(deltaCurrent, merged, currentIndex);
break;
}
return newIndex;
}
/**
* @param the type of compared elements
* @param mergeResult the result of the merge
* @param commonAncestor the common ancestor of the two versions of the content to compare
* @param patchNext the diff between common ancestor and next version
* @param patchCurrent the diff between common ancestor and current version
* @param configuration the configuration of the merge behavior
* @throws MergeException failed to merge
*/
private void merge(DefaultMergeResult mergeResult, List commonAncestor, List next, List current,
Patch patchNext, Patch patchCurrent, MergeConfiguration configuration)
throws MergeException
{
List> conflictDecisions =
(configuration != null) ? configuration.getConflictDecisionList() : Collections.emptyList();
// Merge the two diffs
List merged = new ArrayList<>();
mergeResult.setMerged(merged);
Delta deltaNext = nextElement(patchNext);
Delta deltaCurrent = nextElement(patchCurrent);
// Before common ancestor
if (deltaCurrent.getType() == Type.INSERT && deltaCurrent.getPrevious().getIndex() == 0
&& deltaNext.getType() == Type.INSERT && deltaNext.getPrevious().getIndex() == 0) {
merged.addAll(or(deltaCurrent.getNext().getElements(), deltaNext.getNext().getElements()));
deltaCurrent = nextElement(patchCurrent);
deltaNext = nextElement(patchNext);
} else {
if (deltaCurrent.getType() == Type.INSERT && deltaCurrent.getPrevious().getIndex() == 0) {
merged.addAll(deltaCurrent.getNext().getElements());
deltaCurrent = nextElement(patchCurrent);
}
if (deltaNext.getType() == Type.INSERT && deltaNext.getPrevious().getIndex() == 0) {
merged.addAll(deltaNext.getNext().getElements());
deltaNext = nextElement(patchNext);
}
}
// In common ancestor
int index = 0;
for (; index < commonAncestor.size(); ++index) {
if (isPreviousIndex(deltaCurrent, index)) {
// Modification in current
if (isPreviousIndex(deltaNext, index)) {
// Modifications in both current and next at the same index
if (deltaNext.equals(deltaCurrent)) {
// Choose current
index = apply(deltaCurrent, merged, index);
if (deltaCurrent.getType() == Type.INSERT) {
merged.add(commonAncestor.get(index));
}
} else if (deltaCurrent.getType() == Type.INSERT) {
if (deltaNext.getType() == Type.INSERT) {
int newIndex = applyDecision(conflictDecisions, commonAncestor, deltaNext, deltaCurrent,
merged, index);
if (newIndex == Integer.MIN_VALUE) {
// Conflict
logConflict(mergeResult, deltaCurrent, deltaNext, commonAncestor, next, current, index);
index = fallback(commonAncestor, deltaNext, deltaCurrent, merged, index, configuration);
} else {
index = newIndex;
}
merged.add(commonAncestor.get(index));
} else {
index = apply(deltaCurrent, merged, index);
index = apply(deltaNext, merged, index);
}
} else if (deltaNext.getType() == Type.INSERT) {
index = apply(deltaNext, merged, index);
index = apply(deltaCurrent, merged, index);
} else {
int newIndex = applyDecision(conflictDecisions, commonAncestor, deltaNext, deltaCurrent,
merged, index);
if (newIndex == Integer.MIN_VALUE) {
// Conflict
logConflict(mergeResult, deltaCurrent, deltaNext, commonAncestor, next, current, index);
index = fallback(commonAncestor, deltaNext, deltaCurrent, merged, index, configuration);
} else {
index = newIndex;
}
}
deltaNext = nextElement(patchNext, index);
} else {
if (deltaNext != null
&& deltaCurrent.getPrevious().isOverlappingWith(deltaNext.getPrevious())) {
int newIndex = applyDecision(conflictDecisions, commonAncestor, deltaNext, deltaCurrent,
merged, index);
if (newIndex == Integer.MIN_VALUE) {
// Conflict
logConflict(mergeResult, deltaCurrent, deltaNext, commonAncestor, next, current, index);
index = fallback(commonAncestor, deltaNext, deltaCurrent, merged, index, configuration);
} else {
index = newIndex;
}
deltaNext = nextElement(patchNext, index);
} else {
index = apply(deltaCurrent, merged, index);
if (deltaCurrent.getType() == Type.INSERT) {
merged.add(commonAncestor.get(index));
}
}
}
deltaCurrent = nextElement(patchCurrent, index);
} else if (isPreviousIndex(deltaNext, index)) {
// Modification in next
if (deltaCurrent != null
&& deltaCurrent.getPrevious().isOverlappingWith(deltaNext.getPrevious())) {
int newIndex = applyDecision(conflictDecisions, commonAncestor, deltaNext, deltaCurrent,
merged, index);
if (newIndex == Integer.MIN_VALUE) {
// Conflict
logConflict(mergeResult, deltaCurrent, deltaNext, commonAncestor, next, current, index);
index = fallback(commonAncestor, deltaNext, deltaCurrent, merged, index, configuration);
} else {
index = newIndex;
}
deltaCurrent = nextElement(patchCurrent, index);
} else {
index = apply(deltaNext, merged, index);
if (deltaNext.getType() == Type.INSERT) {
merged.add(commonAncestor.get(index));
}
}
deltaNext = nextElement(patchNext, index);
} else {
merged.add(commonAncestor.get(index));
}
}
// After common ancestor
if (deltaCurrent != null) {
if (deltaCurrent.getType() == Type.INSERT) {
merged.addAll(deltaCurrent.getNext().getElements());
}
if (deltaNext != null && !deltaCurrent.equals(deltaNext)) {
merged.addAll(deltaNext.getNext().getElements());
}
} else if (deltaNext != null) {
merged.addAll(deltaNext.getNext().getElements());
}
}
private List or(List previous, List next) throws MergeException
{
DiffResult diffCurrentResult;
try {
diffCurrentResult = diff(previous, next, null);
} catch (DiffException e) {
throw new MergeException("Faile to diff between two versions", e);
}
List result = new ArrayList<>(previous.size() + next.size());
int index = 0;
for (Delta delta : diffCurrentResult.getPatch()) {
if (delta.getPrevious().getIndex() > index) {
result.addAll(previous.subList(index, delta.getPrevious().getIndex()));
}
if (delta.getType() != Type.INSERT) {
result.addAll(delta.getPrevious().getElements());
}
if (delta.getType() != Type.DELETE) {
result.addAll(delta.getNext().getElements());
}
index = delta.getPrevious().getLastIndex() + 1;
}
if (previous.size() > index) {
result.addAll(previous.subList(index, previous.size()));
}
return result;
}
private List extractConflictPart(Delta delta, List previous, List next, int chunkSize, int index)
{
int previousChangeSize;
int remainingChunkSize;
List result = new ArrayList<>();
switch (delta.getType()) {
case DELETE:
previousChangeSize = delta.getPrevious().size();
remainingChunkSize = chunkSize - previousChangeSize;
// We only perform the extract if there's actually something to extract.
if (remainingChunkSize > previousChangeSize) {
int offsetEnd = Math.min(previous.size(), index + remainingChunkSize);
result = extractFromList(previous, index + previousChangeSize, offsetEnd);
}
break;
case CHANGE:
int endOffset = index + Math.min(chunkSize, next.size());
if (endOffset > next.size()) {
endOffset = next.size();
}
result = extractFromList(next, index, endOffset);
break;
case INSERT:
int newIndex = Math.min(delta.getNext().getIndex(), index);
int indexOffset = delta.getNext().getIndex() - newIndex;
int listSize = Math.min(chunkSize, delta.getNext().size());
result = extractFromList(next, newIndex, newIndex + indexOffset + Math.min(listSize, next.size()));
break;
default:
throw new IllegalArgumentException(
String.format("Cannot extract conflict part for unknown type [%s]", delta.getType()));
}
return result;
}
private List extractFromList(List list, int startOffset, int endOffset)
{
List result = new ArrayList<>();
if (startOffset >= 0 && startOffset <= endOffset && endOffset <= list.size()) {
result = new ArrayList<>(list.subList(startOffset, endOffset));
} else {
logger.info("Trying to extract data from a list [{}] with wrong offsets [{}, {}].",
list, startOffset, endOffset);
}
return result;
}
private void logConflict(DefaultMergeResult mergeResult, Delta deltaCurrent, Delta deltaNext,
List previous, List next, List current, int index)
{
Delta conflictDeltaCurrent;
Delta conflictDeltaNext;
int chunkSize;
List subsetPrevious;
chunkSize = Math.max(deltaCurrent.getMaxChunkSize(), deltaNext.getMaxChunkSize());
int prevMinIndex = Math.min(deltaCurrent.getPrevious().getIndex(), deltaNext.getPrevious().getIndex());
int nextMinIndex = Math.min(deltaCurrent.getNext().getIndex(), deltaNext.getNext().getIndex());
int newIndex;
if (deltaCurrent.getType() == Type.INSERT && deltaNext.getType() == Type.INSERT) {
subsetPrevious = Collections.emptyList();
} else {
newIndex = prevMinIndex;
int newOffsetEnd = Math.min(chunkSize, previous.size()) + newIndex;
if (newOffsetEnd > previous.size()) {
newOffsetEnd = previous.size();
}
subsetPrevious = extractFromList(previous, newIndex, newOffsetEnd);
}
// We need to always use the minimum index to extract the conflict.
newIndex = Math.min(prevMinIndex, nextMinIndex);
List subsetNext = extractConflictPart(deltaNext, previous, next, chunkSize, newIndex);
List subsetCurrent = extractConflictPart(deltaCurrent, previous, current, chunkSize,
newIndex);
// We might have found a conflict such as [a, b], [b, c], [d, c].
// In that case we only want to record the conflict between b and d VS a.
// We don't care about c since it's validated in both current and next versions.
if (subsetPrevious.size() == subsetNext.size() && subsetPrevious.size() == subsetCurrent.size()) {
for (int i = subsetNext.size() - 1; i >= 0; i--) {
if (subsetCurrent.get(i).equals(subsetNext.get(i))) {
subsetCurrent.remove(i);
subsetNext.remove(i);
subsetPrevious.remove(i);
}
}
}
Chunk previousChunk = new DefaultChunk<>(index, subsetPrevious);
Chunk nextChunk = new DefaultChunk<>(index, subsetNext);
Chunk currentChunk = new DefaultChunk<>(index, subsetCurrent);
conflictDeltaCurrent = DeltaFactory.createDelta(
previousChunk,
currentChunk,
getDeltaType(previousChunk, currentChunk));
conflictDeltaNext = DeltaFactory.createDelta(
previousChunk,
nextChunk, getDeltaType(previousChunk, nextChunk));
mergeResult.getLog().error("Conflict between [{}] and [{}]", conflictDeltaCurrent, conflictDeltaNext);
mergeResult.addConflict(new DefaultConflict<>(index, conflictDeltaCurrent, conflictDeltaNext));
}
private Type getDeltaType(Chunk previousChunk, Chunk nextChunk)
{
if (previousChunk.size() == 0) {
return Type.INSERT;
} else if (nextChunk.size() == 0) {
return Type.DELETE;
} else {
return Type.CHANGE;
}
}
private int apply(Delta delta, List merged, int currentIndex)
{
int index = currentIndex;
switch (delta.getType()) {
case DELETE:
index = delta.getPrevious().getLastIndex();
break;
case INSERT:
merged.addAll(delta.getNext().getElements());
break;
case CHANGE:
merged.addAll(delta.getNext().getElements());
index = delta.getPrevious().getLastIndex();
break;
default:
break;
}
return index;
}
private E nextElement(List list)
{
return list != null && !list.isEmpty() ? list.remove(0) : null;
}
/**
* Get the next element in the list and removed it from the list.
* If the element last index is before the current index, then it can be ignored:
* we already skipped this index, because of a fallback for example.
* @param list the list of delta elements
* @param index the current index
* @param the elements to be merged
* @return a new delta that has been removed from the list or null if the list is now empty.
*/
private Delta nextElement(List> list, int index)
{
Delta result;
do {
result = nextElement(list);
} while (result != null && result.getPrevious().getLastIndex() <= index);
return result;
}
private boolean isPreviousIndex(Delta delta, int index)
{
return delta != null && delta.getPrevious().getIndex() == index;
}
/**
* Check if the content is completely different between the ancestor and the current version
*
* @param the type of compared elements
* @param commonAncestor previous version
* @param patchCurrent patch to the current version
* @return either or not the user has changed everything
*/
private boolean isFullyModified(List commonAncestor, Patch patchCurrent)
{
return patchCurrent.size() == 1 && commonAncestor.size() == patchCurrent.get(0).getPrevious().size();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy