package org.xpect.text; import java.util.List; import org.xpect.util.DifferencerImpl; import org.xpect.util.IDifferencer.Match; import org.xpect.util.IDifferencer.MatchKind; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; public class TextDifferencer implements ITextDifferencer { protected static class LineDiff implements ILineDiff { private final MatchKind kind; private final List<ISegmentDiff> segmentDiffs; public LineDiff(List<? extends ISegmentDiff> segmentDiffs) { super(); this.segmentDiffs = ImmutableList.<ISegmentDiff> copyOf(segmentDiffs); this.kind = computeKind(this.segmentDiffs); } protected MatchKind computeKind(List<ISegmentDiff> lineMatches) { boolean hasLeft = false, hasRight = false; for (ISegmentDiff match : lineMatches) { switch (match.getKind()) { case SIMILAR: return MatchKind.SIMILAR; case EQUAL: if (hasLeft != hasRight) return MatchKind.SIMILAR; hasLeft = true; hasRight = true; break; case LEFT_ONLY: hasLeft = true; if (hasRight) return MatchKind.SIMILAR; break; case RIGHT_ONLY: hasRight = true; if (hasLeft) return MatchKind.SIMILAR; break; } } if (hasLeft && hasRight) return MatchKind.EQUAL; if (hasLeft) return MatchKind.LEFT_ONLY; if (hasRight) return MatchKind.RIGHT_ONLY; return MatchKind.EQUAL; } public MatchKind getKind() { return kind; } public List<ISegmentDiff> getSegmentDiffs() { return segmentDiffs; } @Override public String toString() { return new TextDiffToString().toPrefixedLine(this); } } protected static class Segment implements ISegment { private final boolean hidden; private Segment next = null; private Segment previous = null; private final String string; private final int tokenIndex; public Segment(int tokenIndex, String string, boolean hidden) { super(); Preconditions.checkArgument(string.length() > 0); this.tokenIndex = tokenIndex; this.string = string; this.hidden = hidden; } public int getTokenIndex() { return tokenIndex; } public boolean isHidden() { return hidden; } public boolean needsWrap() { if (previous != null) return previous.needsWrapHidden(); return false; } protected boolean needsWrapHidden() { if (!hidden) return false; if (isWrap()) return true; if (previous != null) return previous.needsWrapHidden(); return false; } @Override public String toString() { return string; } public ISegment getNext() { return next; } public ISegment getPrevious() { return previous; } public boolean isWrap() { return string.endsWith("\n"); } } protected static class SegmentDiff implements ISegmentDiff { private final MatchKind kind; private final Segment left; private final Segment right; private final float similarity; public SegmentDiff(MatchKind kind, Segment left, Segment right, float similarity) { super(); this.left = left; this.right = right; this.kind = kind; this.similarity = similarity; } public MatchKind getKind() { return kind; } public Segment getLeft() { return left; } public Segment getRight() { return right; } public float getSimilarity() { return similarity; } @Override public String toString() { switch (kind) { case EQUAL: return left.toString(); case LEFT_ONLY: return "[" + left.toString() + "|]"; case RIGHT_ONLY: return "[|" + right.toString() + "]"; case SIMILAR: return "[" + left.toString() + "|" + right.toString() + "]"; } return super.toString(); } } protected static class TextDiff implements ITextDiff { private final List<ILineDiff> lines; public TextDiff(List<? extends ILineDiff> lines) { super(); this.lines = ImmutableList.copyOf(lines); } public List<ILineDiff> getLines() { return lines; } @Override public String toString() { return new TextDiffToString().apply(this); } } protected DifferencerImpl createDifferencer() { return new DifferencerImpl(); } protected <T> List<Segment> createSegments(List<T> tokens, ITextDiffConfig<T> config) { List<Segment> segments = Lists.newArrayList(); for (int tokenIndex = 0; tokenIndex < tokens.size(); tokenIndex++) { T token = tokens.get(tokenIndex); for (String string : config.toSegments(token)) { boolean hidden = config.isHidden(token, string); int index, lastIndex = 0; while ((index = string.indexOf('\n', lastIndex)) >= 0) { int end = index > 0 && string.charAt(index - 1) == '\r' ? index - 1 : index; if (lastIndex < end) segments.add(new Segment(tokenIndex, string.substring(lastIndex, end), hidden)); segments.add(new Segment(tokenIndex, string.substring(end, index + 1), true)); lastIndex = index + 1; } int end = string.length(); if (lastIndex < end) segments.add(new Segment(tokenIndex, string.substring(lastIndex, end), hidden)); } } if (!segments.isEmpty()) { Segment previous = segments.get(0); for (int i = 1; i < segments.size(); i++) { Segment seg = segments.get(i); seg.previous = previous; previous.next = seg; previous = seg; } } List<Segment> result = Lists.newArrayList(); for (Segment s : segments) if (!s.isHidden()) result.add(s); return result; } public <T> ITextDiff diff(List<T> leftTokens, List<T> rightTokens, ITextDiffConfig<T> config) { List<Segment> leftSegments = createSegments(leftTokens, config); List<Segment> rightSegments = createSegments(rightTokens, config); List<Match> diff = createDifferencer().diff(leftSegments, rightSegments, config); List<SegmentDiff> currentLine = Lists.newArrayList(); List<LineDiff> result = Lists.newArrayList(); for (Match match : diff) { Segment left = match.getLeft() != Match.NO_INDEX ? leftSegments.get(match.getLeft()) : null; Segment right = match.getRight() != Match.NO_INDEX ? rightSegments.get(match.getRight()) : null; if ((left != null && left.needsWrap()) || (right != null && right.needsWrap())) { result.add(new LineDiff(currentLine)); currentLine.clear(); // System.out.println("<flush>"); } // System.out.println(left + "<>" + right); currentLine.add(new SegmentDiff(match.getKind(), left, right, match.getSimilarity())); } if (!currentLine.isEmpty()) { result.add(new LineDiff(currentLine)); currentLine.clear(); } TextDiff textDiff = new TextDiff(result); return textDiff; } }