All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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