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

difflib.DiffRowGenerator Maven / Gradle / Ivy

Go to download

A library for computing diffs, applying patches, generation side-by-side view in Java

There is a newer version: 4.12
Show newest version
/*
 * SPDX-License-Identifier: Apache-1.1
 *
 * ====================================================================
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 1999-2003 The Apache Software Foundation.
 * Copyright (c) 2010 Dmitry Naumenko ([email protected])
 * Copyright (c) 2015-2016 Brenden Kromhout and contributors to java-diff-utils
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowledgement:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgement may appear in the software itself,
 *    if and wherever such third-party acknowledgements normally appear.
 *
 * 4. The names "The Jakarta Project", "Commons", and "Apache Software
 *    Foundation" must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written
 *    permission, please contact [email protected].
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * .
 */
package difflib;

import difflib.DiffRow.Tag;
import difflib.myers.Equalizer;
import difflib.myers.MyersDiff;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This class for generating DiffRows for side-by-sidy view. You can customize the way of generating. For example, show
 * inline diffs on not, ignoring white spaces or/and blank lines and so on. All parameters for generating are optional.
 * If you do not specify them, the class will use the default values.
 * 

* These values are: showInlineDiffs = false; ignoreWhiteSpaces = true; ignoreBlankLines = true; ... *

* For instantiating the DiffRowGenerator you should use the its builder. Like in example *

* DiffRowGenerator generator = new DiffRowGenerator.Builder().showInlineDiffs(true). * ignoreWhiteSpaces(true).columnWidth(100).build(); * @author Dmitry Naumenko */ public class DiffRowGenerator { private static final String NEW_LINE = "\n"; private static final Pattern WS_PATTERN = Pattern.compile("\\s+"); private static final String DEFAULT_TAG_DELETE = "del"; private static final String DEFAULT_TAG_INSERT = "ins"; private static final String DEFAULT_TAG_CHANGE = "span"; private static final String DEFAULT_CSSCLASS_DELETE = null; private static final String DEFAULT_CSSCLASS_INSERT = null; private static final String DEFAULT_CSSCLASS_CHANGE = "change"; private static final DiffAlgorithm DEFAULT_DIFFALGORITHM = new MyersDiff(new Equalizer() { public boolean equals(String original, String revised) { return Objects.equals(original, revised); } @Override public boolean skip(String original) { return false; } }); private final boolean showInlineDiffs; private final boolean ignoreWhiteSpaces; private final String inlineOriginDeleteTag; private final String inlineRevisedInsertTag; private final String inlineOriginChangeTag; private final String inlineRevisedChangeTag; private final String inlineOriginDeleteCssClass; private final String inlineRevisedInsertCssClass; private final String inlineOriginChangeCssClass; private final String inlineRevisedChangeCssClass; private final int columnWidth; @Nullable private final String defaultString; private final DiffAlgorithm diffAlgorithm; /** * This class used for building the DiffRowGenerator. * @author dmitry */ public static class Builder { private boolean showInlineDiffs = false; private boolean ignoreWhiteSpaces = false; private String inlineOriginDeleteTag = DEFAULT_TAG_DELETE; private String inlineOriginChangeTag = DEFAULT_TAG_CHANGE; private String inlineRevisedInsertTag = DEFAULT_TAG_INSERT; private String inlineRevisedChangeTag = DEFAULT_TAG_CHANGE; private String inlineOriginDeleteCssClass = DEFAULT_CSSCLASS_DELETE; private String inlineRevisedInsertCssClass = DEFAULT_CSSCLASS_INSERT; private String inlineOriginChangeCssClass = DEFAULT_CSSCLASS_CHANGE; private String inlineRevisedChangeCssClass = DEFAULT_CSSCLASS_CHANGE; private int columnWidth = -1; @Nullable private String defaultString = ""; private DiffAlgorithm diffAlgorithm = DEFAULT_DIFFALGORITHM; /** * Show inline diffs in generating diff rows or not. * @param val the value to set. Default: false. * @return builder with configured showInlineDiff parameter */ public Builder showInlineDiffs(boolean val) { showInlineDiffs = val; return this; } /** * Ignore white spaces in generating diff rows or not. * @param val the value to set. Default: true. * @return builder with configured ignoreWhiteSpaces parameter */ public Builder ignoreWhiteSpaces(boolean val) { ignoreWhiteSpaces = val; return this; } /** * Set the tag used for displaying changes in the original text. * @param tag the tag to set. Without angle brackets. Default: {@value #DEFAULT_TAG_DELETE}. * @return builder with configured inlineOriginDeleteTag parameter * @deprecated Use {@link #inlineOriginDeleteTag(String)} */ @Deprecated public Builder InlineOldTag(String tag) { inlineOriginDeleteTag = tag; return this; } /** * Set the tag used for displaying delete data in the original text. * @param tag the tag to set. Without angle brackets. Default: {@value #DEFAULT_TAG_DELETE}. * @return builder with configured inlineOriginDeleteTag parameter */ public Builder inlineOriginDeleteTag(String tag) { inlineOriginDeleteTag = tag; return this; } /** * Set the tag used for displaying changes in the revised text. * @param tag the tag to set. Without angle brackets. Default: {@value #DEFAULT_TAG_INSERT}. * @return builder with configured inlineRevisedInsertTag parameter * @deprecated Use {@link #inlineRevisedInsertTag(String)} */ public Builder InlineNewTag(String tag) { inlineRevisedInsertTag = tag; return this; } /** * Set the tag used for displaying changes in the revised text. * @param tag the tag to set. Without angle brackets. Default: {@value #DEFAULT_TAG_INSERT}. * @return builder with configured inlineRevisedInsertTag parameter */ public Builder inlineRevisedInsertTag(String tag) { inlineRevisedInsertTag = tag; return this; } /** * Set the css class used for displaying changes in the original text. * @param cssClass the tag to set. Without any quotes, just word. Default: {@value #DEFAULT_CSSCLASS_DELETE}. * @return builder with configured inlineOriginDeleteCssClass parameter * @deprecated Use {@link #inlineOriginDeleteCssClass(String)} */ public Builder InlineOldCssClass(String cssClass) { inlineOriginDeleteCssClass = cssClass; return this; } /** * Set the css class used for displaying delete data in the original text. * @param cssClass the tag to set. Without any quotes, just word. Default: {@value #DEFAULT_CSSCLASS_DELETE}. * @return builder with configured inlineOriginDeleteCssClass parameter */ public Builder inlineOriginDeleteCssClass(String cssClass) { inlineOriginDeleteCssClass = cssClass; return this; } /** * Set the css class used for displaying changes in the revised text. * @param cssClass the tag to set. Without any quotes, just word. Default: {@value #DEFAULT_CSSCLASS_INSERT}. * @return builder with configured inlineRevisedInsertCssClass parameter * @deprecated Use {@link #inlineRevisedInsertCssClass(String)} */ public Builder InlineNewCssClass(String cssClass) { inlineRevisedInsertCssClass = cssClass; return this; } /** * Set the css class used for displaying insert data in the revised text. * @param cssClass the tag to set. Without any quotes, just word. Default: {@value #DEFAULT_CSSCLASS_INSERT}. * @return builder with configured inlineRevisedInsertCssClass parameter */ public Builder inlineRevisedInsertCssClass(String cssClass) { inlineRevisedInsertCssClass = cssClass; return this; } /** * Set the column with of generated lines of original and revised texts. * @param width the width to set. Making it < 0 disable line breaking. * @return builder with configured columnWidth parameter */ public Builder columnWidth(int width) { columnWidth = width; return this; } @Nonnull public Builder defaultString(@Nullable String defaultString) { this.defaultString = defaultString; return this; } /** * Set the custom equalizer to use while comparing the lines of the revisions. * @param stringEqualizer to use (custom one) * @return builder with configured stringEqualizer */ public Builder stringEqualizer(Equalizer stringEqualizer) { this.diffAlgorithm = new MyersDiff<>(stringEqualizer); return this; } /** * Set the custom {@link DiffAlgorithm} to use while comparing the lines of the revisions. * @param diffAlgorithm to use (custom one) * @return builder with configured stringEqualizer */ public Builder diffAlgorithm(DiffAlgorithm diffAlgorithm) { this.diffAlgorithm = diffAlgorithm; return this; } /** * Build the DiffRowGenerator using the default Equalizer for rows. If some parameters are not set, the default * values are used. * @return the customized DiffRowGenerator */ public DiffRowGenerator build() { return new DiffRowGenerator(this); } } private DiffRowGenerator(Builder builder) { showInlineDiffs = builder.showInlineDiffs; ignoreWhiteSpaces = builder.ignoreWhiteSpaces; inlineOriginDeleteTag = builder.inlineOriginDeleteTag; inlineOriginDeleteCssClass = builder.inlineOriginDeleteCssClass; inlineOriginChangeTag = builder.inlineOriginChangeTag; inlineOriginChangeCssClass = builder.inlineOriginChangeCssClass; inlineRevisedInsertTag = builder.inlineRevisedInsertTag; inlineRevisedInsertCssClass = builder.inlineRevisedInsertCssClass; inlineRevisedChangeTag = builder.inlineRevisedChangeTag; inlineRevisedChangeCssClass = builder.inlineRevisedChangeCssClass; columnWidth = builder.columnWidth; defaultString = builder.defaultString; diffAlgorithm = builder.diffAlgorithm; } /** * Get the DiffRows describing the difference between original and revised texts using the given patch. Useful for * displaying side-by-side diff. * @param original the original text * @param revised the revised text * @return the DiffRows between original and revised texts */ public List generateDiffRows(List original, List revised) { if (ignoreWhiteSpaces) { replAllWs(original); replAllWs(revised); } return generateDiffRows(original, revised, DiffUtils.diff(original, revised, diffAlgorithm)); } private void replAllWs(List strList) { for (final ListIterator i = strList.listIterator(); i.hasNext(); ) { final String s = i.next(); if (s != null) i.set(WS_PATTERN.matcher(s.trim()).replaceAll(" ")); } } /** * Generates the DiffRows describing the difference between original and revised texts using the given patch. Useful * for displaying side-by-side diff. * @param original the original text * @param revised the revised text * @param patch the given patch * @return the DiffRows between original and revised texts */ public List generateDiffRows(List original, List revised, Patch patch) { // normalize the lines (expand tabs, escape html entities) original = Utils.normalize(original); revised = Utils.normalize(revised); // wrap to the column width if (columnWidth > 0) { original = Utils.wrapText(original, this.columnWidth); revised = Utils.wrapText(revised, this.columnWidth); } List diffRows = new ArrayList(); int orgEndPos = 0; int revEndPos = 0; final List> deltaList = patch.getDeltas(); Equalizer equalizer = diffAlgorithm.getEqualizer(); for (int i = 0; i < deltaList.size(); i++) { Delta delta = deltaList.get(i); Chunk orig = delta.getOriginal(); Chunk rev = delta.getRevised(); // We should normalize and wrap lines in deltas too. orig.setLines(Utils.normalize(orig.getLines())); rev.setLines(Utils.normalize(rev.getLines())); if (columnWidth > 0) { orig.setLines(Utils.wrapText(orig.getLines(), this.columnWidth)); rev.setLines(Utils.wrapText(rev.getLines(), this.columnWidth)); } // catch the equal prefix for each chunk copyEqualsLines(equalizer, diffRows, original, orgEndPos, orig.getPosition(), revised, revEndPos, rev.getPosition()); // Inserted DiffRow if (delta.getClass() == InsertDelta.class) { orgEndPos = orig.last() + 1; revEndPos = rev.last() + 1; for (String line : rev.getLines()) { if (equalizer.skip(line)) { diffRows.add(new DiffRow(Tag.SKIP, defaultString, line)); } else { diffRows.add(new DiffRow(Tag.INSERT, defaultString, line)); } } continue; } // Deleted DiffRow if (delta.getClass() == DeleteDelta.class) { orgEndPos = orig.last() + 1; revEndPos = rev.last() + 1; for (String line : orig.getLines()) { if (equalizer.skip(line)) { diffRows.add(new DiffRow(Tag.SKIP, line, defaultString)); } else { diffRows.add(new DiffRow(Tag.DELETE, line, defaultString)); } } continue; } if (showInlineDiffs) { addInlineDiffs(delta); } // the changed size is match if (orig.size() == rev.size()) { for (int j = 0; j < orig.size(); j++) { addChangeDiffRow(equalizer, diffRows, orig.getLines().get(j), rev.getLines().get(j), defaultString); } } else if (orig.size() > rev.size()) { for (int j = 0; j < orig.size(); j++) { final String orgLine = orig.getLines().get(j); final String revLine = rev.getLines().size() > j ? rev.getLines().get(j) : defaultString; addChangeDiffRow(equalizer, diffRows, orgLine, revLine, defaultString); } } else { for (int j = 0; j < rev.size(); j++) { final String orgLine = orig.getLines().size() > j ? orig.getLines().get(j) : defaultString; final String revLine = rev.getLines().get(j); addChangeDiffRow(equalizer, diffRows, orgLine, revLine, defaultString); } } orgEndPos = orig.last() + 1; revEndPos = rev.last() + 1; } // Copy the final matching chunk if any. copyEqualsLines(equalizer, diffRows, original, orgEndPos, original.size(), revised, revEndPos, revised.size()); return diffRows; } private static void addChangeDiffRow(Equalizer equalizer, List diffRows, String orgLine, String revLine, String defaultString) { boolean skipOrg = equalizer.skip(orgLine); boolean skipRev = equalizer.skip(revLine); if (skipOrg && skipRev) { diffRows.add(new DiffRow(Tag.SKIP, orgLine, revLine)); } else if (skipOrg) { diffRows.add(new DiffRow(Tag.SKIP, orgLine, defaultString)); diffRows.add(new DiffRow(Tag.CHANGE, defaultString, revLine)); } else if (skipRev) { diffRows.add(new DiffRow(Tag.CHANGE, orgLine, defaultString)); diffRows.add(new DiffRow(Tag.SKIP, defaultString, revLine)); } else { diffRows.add(new DiffRow(Tag.CHANGE, orgLine, revLine)); } } protected void copyEqualsLines(Equalizer equalizer, List diffRows, List original, int originalStartPos, int originalEndPos, List revised, int revisedStartPos, int revisedEndPos) { String[][] lines = new String[originalEndPos - originalStartPos][2]; int idx = 0; for (String line : original.subList(originalStartPos, originalEndPos)) { lines[idx++][0] = line; } idx = 0; for (String line : revised.subList(revisedStartPos, revisedEndPos)) { lines[idx++][1] = line; } for (String[] line : lines) { String orgLine = line[0]; String revLine = line[1]; if (equalizer.skip(orgLine) && equalizer.skip(revLine)) { diffRows.add(new DiffRow(Tag.SKIP, orgLine, revLine)); } else { diffRows.add(new DiffRow(Tag.EQUAL, orgLine, revLine)); } } } /** * Add the inline diffs for given delta * @param delta the given delta */ private void addInlineDiffs(Delta delta) { List orig = delta.getOriginal().getLines(); List rev = delta.getRevised().getLines(); LinkedList origList = charArrayToStringList(Utils.join(orig, NEW_LINE).toCharArray()); LinkedList revList = charArrayToStringList(Utils.join(rev, NEW_LINE).toCharArray()); List> inlineDeltas = DiffUtils.diff(origList, revList).getDeltas(); Collections.reverse(inlineDeltas); for (Delta inlineDelta : inlineDeltas) { Chunk inlineOrig = inlineDelta.getOriginal(); Chunk inlineRev = inlineDelta.getRevised(); if (inlineDelta.getClass().equals(DeleteDelta.class)) { origList = wrapInTag(origList, inlineOrig.getPosition(), inlineOrig.getPosition() + inlineOrig.size() + 1, this.inlineOriginDeleteTag, this.inlineOriginDeleteCssClass); } else if (inlineDelta.getClass().equals(InsertDelta.class)) { revList = wrapInTag(revList, inlineRev.getPosition(), inlineRev.getPosition() + inlineRev.size() + 1, this.inlineRevisedInsertTag, this.inlineRevisedInsertCssClass); } else if (inlineDelta.getClass().equals(ChangeDelta.class)) { origList = wrapInTag(origList, inlineOrig.getPosition(), inlineOrig.getPosition() + inlineOrig.size() + 1, this.inlineOriginChangeTag, this.inlineOriginChangeCssClass); revList = wrapInTag(revList, inlineRev.getPosition(), inlineRev.getPosition() + inlineRev.size() + 1, this.inlineRevisedChangeTag, this.inlineRevisedChangeCssClass); } } delta.getOriginal().setLines(addMissingLines(origList, orig.size())); delta.getRevised().setLines(addMissingLines(revList, rev.size())); } private List addMissingLines(final List lines, final int targetSize) { List tempList = Arrays.asList(Utils.join(lines, "").split(NEW_LINE)); if (tempList.size() < targetSize) { tempList = new ArrayList<>(tempList); while (tempList.size() < targetSize) { tempList.add(""); } } return tempList; } private static LinkedList charArrayToStringList(char[] cs) { LinkedList result = new LinkedList(); for (Character character : cs) { result.add(character.toString()); } return result; } private static final Pattern PATTERN_CRLF = Pattern.compile("([\\n\\r]+)"); /** * Wrap the elements in the sequence with the given tag * @param startPosition the position from which tag should start. The counting start from a zero. * @param endPosition the position before which tag should should be closed. * @param tag the tag name without angle brackets, just a word * @param cssClass the optional css class */ public static LinkedList wrapInTag(LinkedList sequence, int startPosition, int endPosition, String tag, String cssClass) { LinkedList result = (LinkedList) sequence.clone(); StringBuilder tagBuilder = new StringBuilder(); tagBuilder.append("<"); tagBuilder.append(tag); if (cssClass != null) { tagBuilder.append(" class=\""); tagBuilder.append(cssClass); tagBuilder.append("\""); } tagBuilder.append(">"); final String startTag = tagBuilder.toString(); tagBuilder.delete(0, tagBuilder.length()); tagBuilder.append(""); final String endTag = tagBuilder.toString(); result.add(startPosition, startTag); result.add(endPosition, endTag); final String joinTag = new StringBuilder(Matcher.quoteReplacement(endTag)).append("$1") .append(Matcher .quoteReplacement(startTag)) .toString(); for (int i = startPosition + 1; i < endPosition; ++i) { final String val = result.get(i); if (val.contains("\n") || val.contains("\r")) { result.set(i, PATTERN_CRLF.matcher(val).replaceAll(joinTag)); } } return result; } /** * Wrap the given line with the given tag * @param line the given line * @param tag the tag name without angle brackets, just a word * @param cssClass the optional css class * @return the wrapped string */ public static String wrapInTag(String line, String tag, String cssClass) { final StringBuilder tagBuilder = new StringBuilder(); tagBuilder.append("<"); tagBuilder.append(tag); if (cssClass != null) { tagBuilder.append(" class=\""); tagBuilder.append(cssClass); tagBuilder.append("\""); } tagBuilder.append(">"); final String startTag = tagBuilder.toString(); tagBuilder.delete(0, tagBuilder.length()); tagBuilder.append(""); final String endTag = tagBuilder.toString(); final String joinTag = new StringBuilder(Matcher.quoteReplacement(endTag)).append("$1") .append(Matcher .quoteReplacement(startTag)) .toString(); return startTag + PATTERN_CRLF.matcher(line).replaceAll(joinTag) + endTag; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy