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

org.xwiki.diff.display.internal.DefaultUnifiedDiffDisplayer Maven / Gradle / Ivy

There is a newer version: 16.10.0-rc-1
Show newest version
/*
 * 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.display.internal;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.xwiki.component.annotation.Component;
import org.xwiki.diff.Chunk;
import org.xwiki.diff.Conflict;
import org.xwiki.diff.Delta;
import org.xwiki.diff.DiffException;
import org.xwiki.diff.DiffManager;
import org.xwiki.diff.DiffResult;
import org.xwiki.diff.display.InlineDiffChunk;
import org.xwiki.diff.display.InlineDiffDisplayer;
import org.xwiki.diff.display.UnifiedDiffBlock;
import org.xwiki.diff.display.UnifiedDiffConfiguration;
import org.xwiki.diff.display.UnifiedDiffDisplayer;
import org.xwiki.diff.display.UnifiedDiffElement;
import org.xwiki.diff.display.UnifiedDiffElement.Type;
import org.xwiki.diff.internal.DefaultChunk;
import org.xwiki.diff.internal.DeltaFactory;

/**
 * Displays a {@link DiffResult} as a unified diff. The
 * unified diff consists in a sequence of blocks, each having elements marked as either added or removed, padded with
 * unmodified elements that put changes in context.
 * 

* NOTE: This class was greatly inspired by the {@code UnifiedPrint} class written by * Ludovic Claude for the * CVSGrab project under the Apache Software License version 1.1. * * @version $Id: 6b8bd2829501d3ad0fb3ac4bdaff2522b7c6c3e4 $ * @since 4.1RC1 */ @Component @Singleton public class DefaultUnifiedDiffDisplayer implements UnifiedDiffDisplayer { /** * The state of the displayer. * * @param the type of composite elements that are compared to produce the first level diff * @param the type of sub-elements that are compared to produce the second-level diff */ private static class State { /** * The collection of unified diff blocks build so far. */ private final Stack> blocks = new Stack<>(); /** * The previous version, used to take the unmodified elements from. */ private final List previous; /** * The last change processed by the displayer. */ private Delta lastDelta; private Conflict lastConflict; /** * Creates a new instance. * * @param previous the previous version used to take the unmodified elements from */ State(List previous) { this.previous = previous; } /** * @return the last processed change */ public Delta getLastDelta() { return this.lastDelta; } /** * Sets the last processed change. * * @param lastDelta the last processed change */ public void setLastDelta(Delta lastDelta) { this.lastDelta = lastDelta; } /** * Sets the last detected conflict. * * @param lastConflict the conflict detected. * @since 11.8RC1 */ public void setLastConflict(Conflict lastConflict) { this.lastConflict = lastConflict; } /** * @return the last detected conflict. * @since 11.8RC1 */ public Conflict getLastConflict() { return this.lastConflict; } /** * @return the collection of unified diff blocks build so far */ public Stack> getBlocks() { return this.blocks; } /** * @return the previous version, used to take the unmodified elements from */ public List getPrevious() { return this.previous; } } /** * The component used to determine the second level of changes, inside a modified element. */ @Inject private DiffManager diffManager; /** * The component used to display the second level of changes, between modified elements. */ @Inject private InlineDiffDisplayer inlineDisplayer; @Override public UnifiedDiffConfiguration getDefaultConfiguration() { return new UnifiedDiffConfiguration<>(); } @Override public List> display(DiffResult diffResult) { return display(diffResult, this.getDefaultConfiguration()); } @Override public List> display(DiffResult diffResult, List> conflicts) { return display(diffResult, conflicts, this.getDefaultConfiguration()); } @Override public List> display(DiffResult diffResult, UnifiedDiffConfiguration config) { return display(diffResult, null, config); } /** * Find the first conflict matching the delta, or null if there is no matching conflict. * * @param delta the delta for which we want to find a matching conflict. * @param conflicts the list of possible conflicts. * @param the type of elements we manipulate. * @return an instance of conflict from the given list, or null if no conflict has been found. */ private Conflict findConflict(Delta delta, List> conflicts) { if (conflicts != null) { for (Conflict conflict : conflicts) { if (conflict.concerns(delta)) { return conflict; } } } return null; } /** * Isolate part of a chunk concerns by a conflict, and create the right pre and post chunks. * * @param originalChunk the chunk to split. * @param conflictIndex index where a conflict occurred, this is the started index where the chunk needs to be * isolated. * @param conflictSize the size of a conflict, this is the size of the part of the chunk which needs to be isolated. * @param the type of elements we manipulate. * @return a list of chunks, which can contain from 1 (if the whole original chunk is concerned by the conflict) to * 3 chunks maximal (if there is a prefix chunk and a postfix chunk). */ private List> splitChunk(Chunk originalChunk, int conflictIndex, int conflictSize) { List> result = new ArrayList<>(); List elements = originalChunk.getElements(); int index = originalChunk.getIndex(); int listIndex = 0; // prefix chunk. int prefixEnd = conflictIndex - index; if (prefixEnd > 0 && elements.size() > prefixEnd) { result.add(new DefaultChunk<>(index, elements.subList(0, prefixEnd))); index = conflictIndex; listIndex = prefixEnd; } // chunk concerned by the conflict int listLastIndex = Math.min(elements.size(), listIndex + conflictSize); result.add(new DefaultChunk<>(index, elements.subList(listIndex, listLastIndex))); index += conflictSize; listIndex = listLastIndex; // postfix chunk if (listIndex < elements.size()) { result.add(new DefaultChunk<>(index, elements.subList(listIndex, elements.size()))); } return result; } /** * Split a delta element to isolate the delta that is related to a conflict. * * @param delta the delta to split. * @param conflict the conflict which is related to this delta. * @param the type of element we manipulate. * @return a list of 2 or 3 deltas, depending if the conflict is on the middle of the delta, or on top/bottom side. * @throws IllegalArgumentException if the delta concerns another type than {@link Delta.Type#CHANGE}. */ private List> splitDelta(Delta delta, Conflict conflict) { List> result = new ArrayList<>(); if (delta.getType() != Delta.Type.CHANGE) { throw new IllegalArgumentException( String.format("Only delta concerning change can be splitted: [%s]", delta)); } List> previousChunks = splitChunk(delta.getPrevious(), conflict.getIndex(), conflict.getMaxSize()); List> nextChunks = splitChunk(delta.getNext(), conflict.getIndex(), conflict.getMaxSize()); for (int i = 0; i < Math.max(previousChunks.size(), nextChunks.size()); i++) { Chunk previousChunk; Chunk nextChunk; Delta.Type deltaType; if (i < previousChunks.size()) { previousChunk = previousChunks.get(i); } else { previousChunk = null; } if (i < nextChunks.size()) { nextChunk = nextChunks.get(i); } else { nextChunk = new DefaultChunk<>(previousChunk.getIndex(), Collections.emptyList()); } if (previousChunk == null) { previousChunk = new DefaultChunk<>(nextChunk.getIndex(), Collections.emptyList()); } if (previousChunk.getElements().isEmpty()) { deltaType = Delta.Type.INSERT; } else if (nextChunk.getElements().isEmpty()) { deltaType = Delta.Type.DELETE; } else { deltaType = Delta.Type.CHANGE; } result.add(DeltaFactory.createDelta(previousChunk, nextChunk, deltaType)); } return result; } /** * Build a map of delta and their associated conflicts. * If only a subset of a delta is concerned by a conflict, then the delta is splitted to allow fixing the * conflict only, we use a recursive algorithm to do so. * If the conflicts list is null or empty, then return the original list of delta from the patch * with a null value associated. * * @param deltaList the list of delta to check * @param conflicts the list of conflicts to resolve, this list is consumed during the built of the map. * @param the type of elements that are compared. * @return a map containing the delta to display and their associated conflicts. The map is ordered by delta * insertion, which is related to their index. */ private Map, Conflict> buildDeltaConflictMap(List> deltaList, List> conflicts) { // We need to use a LinkedHashMap here since we want to keep track of the order of insertion of the elements. Map, Conflict> result = new LinkedHashMap<>(); for (Delta delta : deltaList) { Conflict conflict = findConflict(delta, conflicts); // If we found a conflict, but it only concerns a subpart of the delta, then we need to split this // delta, so we can associate the conflict with only the part of the delta concerned by // the conflict. if (canDeltaBeSplit(conflict, delta)) { result.putAll(buildDeltaConflictMap(splitDelta(delta, conflict), conflicts)); // Else the conflict concerns the whole delta, so we can add it to the map and associate it // with the delta. We remove the conflict from our list, since it's already associated // with a delta. } else { result.put(delta, conflict); if (conflicts != null && conflict != null) { conflicts.remove(conflict); } } } return result; } private boolean isDeltaChange(Delta delta) { return delta.getType() == Delta.Type.CHANGE; } private boolean canDeltaBeSplit(Conflict conflict, Delta delta) { if (conflict != null) { // a delta can be split only if one of its chunk is > 1 // a delta can be split only if it concerns a change boolean isChange = isDeltaChange(delta) && isDeltaChange(conflict.getDeltaCurrent()) && isDeltaChange(conflict.getDeltaNext()); boolean deltaCanBeSplitted = (delta.getPrevious().size() > 1 || delta.getNext().size() > 1) && conflict.getMaxSize() < delta.getMaxChunkSize(); boolean conflictIsSubpartOfDelta = (conflict.getMaxSize() != delta.getNext().size() || conflict.getMaxSize() != delta.getPrevious().size()); return isChange && deltaCanBeSplitted && conflictIsSubpartOfDelta; } return false; } @Override public List> display(DiffResult diffResult, List> conflicts, UnifiedDiffConfiguration config) { State state = new State<>(diffResult.getPrevious()); // We use a new array list of conflicts here, since the conflicts will be consumed during the creation of the // map later. List> conflictList = null; if (conflicts != null) { conflictList = new ArrayList<>(conflicts); } // The current diffResult contains a list of delta, but we want two things: // 1. To associate each delta with a conflict if there is any // 2. To split up the delta, to only get the area concerned by the conflicts. Map, Conflict> deltaAndConflicts = buildDeltaConflictMap(diffResult.getPatch(), conflictList); for (Delta delta : deltaAndConflicts.keySet()) { Conflict conflict = deltaAndConflicts.get((delta)); // Add unmodified elements before the current delta. Start a new block if the distance between the current // delta and the last one is greater than or equal to 2 * context size. // In case of conflict, we actually create a dedicated block for the context, before the conflict area. maybeStartBlock(delta, state, config.getContextSize(), conflict); // Add changed elements. switch (delta.getType()) { case CHANGE: state.getBlocks().peek().addAll(this.getModifiedElements(delta, config)); break; case DELETE: state.getBlocks().peek().addAll(this.getElements(delta.getPrevious(), Type.DELETED)); break; case INSERT: state.getBlocks().peek().addAll(this.getElements(delta.getNext(), Type.ADDED)); break; default: break; } state.setLastDelta(delta); } // Add unmodified elements after the last delta. maybeEndBlock(state, config.getContextSize(), true); return state.getBlocks(); } /** * Starts a new {@link UnifiedDiffBlock} if the provided change is in a different context, or if it belongs to a * conflict. The distance between two changes inside the same block is less than 2 * context size. * * @param delta the change * @param state the state of the displayer * @param contextSize the number of unmodified elements to display before and after each change * @param conflict the conflict the change is related to * @param the type of composite elements that are compared to produce the first level diff * @param the type of sub-elements that are compared to produce the second-level diff when a composite element * is modified */ private void maybeStartBlock(Delta delta, State state, int contextSize, Conflict conflict) { if (state.getLastConflict() != conflict || state.getLastDelta() == null || state.getLastDelta().getPrevious().getLastIndex() < delta.getPrevious().getIndex() - contextSize * 2) { maybeEndBlock(state, contextSize, false); state.getBlocks().push(new UnifiedDiffBlock<>()); } // Add the unmodified elements before the given delta. int count = state.getBlocks().peek().isEmpty() ? contextSize : contextSize * 2; int lastChangeIndex = state.getLastDelta() == null ? -1 : state.getLastDelta().getPrevious().getLastIndex(); int end = delta.getPrevious().getIndex(); int start = Math.max(end - count, lastChangeIndex + 1); state.getBlocks().peek().addAll(this.getUnmodifiedElements(state.getPrevious(), start, end)); if (conflict != null && !state.getBlocks().peek().isEmpty()) { state.getBlocks().push(new UnifiedDiffBlock<>()); } state.getBlocks().peek().setConflict(conflict); state.setLastConflict(conflict); } /** * Processes a change. In a unified diff the modified elements are either added or removed so we model a change by * listing the removed elements (from the previous version) followed by the added elements (from the next version). * If a splitter is provided through the given configuration object then we use it to split the changed elements (if * the number of removed elements equals the number of added elements) in sub-elements and produce an in-line diff * for the changes inside the modified elements. * * @param delta the change * @param config the configuration used to access the splitter * @param the type of composite elements that are compared to produce the first level diff * @param the type of sub-elements that are compared to produce the second-level diff when a composite element * is modified * @return the list of unified diff elements corresponding to the elements modified in the given delta */ private List> getModifiedElements(Delta delta, UnifiedDiffConfiguration config) { List> elements = new ArrayList<>(); elements.addAll(this.getElements(delta.getPrevious(), Type.DELETED)); elements.addAll(this.getElements(delta.getNext(), Type.ADDED)); // Compute the in-line diff if the number of removed elements equals the number of added elements. if (config.getSplitter() != null && delta.getPrevious().size() == delta.getNext().size()) { int changeSize = delta.getPrevious().size(); for (int i = 0; i < changeSize; i++) { displayInlineDiff(elements.get(i), elements.get(changeSize + i), config); } } return elements; } /** * @param chunk the modified elements (both added and deleted) * @param changeType the change type * @param the type of composite elements that are compared to produce the first level diff * @param the type of sub-elements that are compared to produce the second-level diff when a composite element * is modified * @return the list of corresponding unified diff elements, matching the change type */ private List> getElements(Chunk chunk, Type changeType) { int index = chunk.getIndex(); List> elements = new ArrayList<>(); for (E element : chunk.getElements()) { elements.add(new UnifiedDiffElement<>(index++, changeType, element)); } return elements; } /** * @param previous the previous version * @param start the index of the first unmodified element to return * @param end the index to stop at * @param the type of composite elements that are compared to produce the first level diff * @param the type of sub-elements that are compared to produce the second-level diff when a composite element * is modified * @return the list of unmodified elements between the given start and end index */ private List> getUnmodifiedElements(List previous, int start, int end) { List> unmodifiedElements = new ArrayList<>(); if (start < previous.size()) { for (int i = start; i < Math.min(end, previous.size()); i++) { unmodifiedElements.add(new UnifiedDiffElement<>(i, Type.CONTEXT, previous.get(i))); } } return unmodifiedElements; } /** * Ends the last {@link UnifiedDiffBlock} by adding a number of unmodified elements. * * @param state the state of the displayer * @param contextSize the number of unmodified elements to display at the end of a block * @param lastBlock if true, it's actually the last call after all delta, so we might want to display unmodified * elements in a new block if previous one was a conflict block. * @param the type of composite elements that are compared to produce the first level diff * @param the type of sub-elements that are compared to produce the second-level diff when a composite element * is modified */ private void maybeEndBlock(State state, int contextSize, boolean lastBlock) { if (!state.getBlocks().isEmpty() && (state.getLastConflict() == null || lastBlock)) { int start = state.getLastDelta().getPrevious().getLastIndex() + 1; int end = Math.min(start + contextSize, state.getPrevious().size()); List> unmodifiedElements = this.getUnmodifiedElements(state.getPrevious(), start, end); if (!unmodifiedElements.isEmpty() && state.getLastConflict() != null) { state.getBlocks().push(new UnifiedDiffBlock<>()); } state.getBlocks().peek().addAll(unmodifiedElements); } } /** * Computes the changes between two versions of an element by splitting the element into sub-elements and displays * the result using the in-line format. * * @param previous the previous version * @param next the next version version * @param config the configuration for the in-line diff * @param the type of composite elements that are compared to produce the first level diff * @param the type of sub-elements that are compared to produce the second-level diff when a composite element * is modified */ private void displayInlineDiff(UnifiedDiffElement previous, UnifiedDiffElement next, UnifiedDiffConfiguration config) { try { List previousSubElements = config.getSplitter().split(previous.getValue()); List nextSubElements = config.getSplitter().split(next.getValue()); DiffResult diffResult = this.diffManager.diff(previousSubElements, nextSubElements, config); List> chunks = this.inlineDisplayer.display(diffResult); previous.setChunks(new ArrayList<>()); next.setChunks(new ArrayList<>()); for (InlineDiffChunk chunk : chunks) { if (!chunk.isAdded()) { previous.getChunks().add(chunk); } if (!chunk.isDeleted()) { next.getChunks().add(chunk); } } } catch (DiffException e) { // Do nothing. } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy