blob: ddf4f9548af25694dac0d7c5752a23a6e98b1155 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jackrabbit.vault.util.diff;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
/**
* The document diff is used to create a <em>diff</em> between 2 documents.
* It provides 2 possibilites to use the differences: either using the
* {@link ChangeListener} which is called for each line of the document, or
* using the {@link Hunk}s that form the differences of this diff.
*/
public class DocumentDiff {
/**
* the left document
*/
private Document left;
/**
* the right document
*/
private Document right;
/**
* the change script
*/
private final Diff.Change change;
/**
* the hunks
*/
private Hunk hunks;
/**
* the number of changes
*/
private int numDelta = 0;
/**
* Create a new diff from the given 2 documents
* @param left the left document
* @param right the right document
*/
public DocumentDiff(Document left, Document right) {
this.left = left;
this.right = right;
Document.Element[] leftElems = left.getElements();
Document.Element[] rightElems = right.getElements();
change = new Diff(leftElems, rightElems).diff_2(false);
init();
}
/**
* Returns the number of changed elements. each insertion and each deletion
* counts as 1 change. if elements were modified the greate change is counted.
* eg: if you change 'foo' to 'bar' this is actually 1 deletion and 1 insertion
* but counts as 1 change. if you change 'foo\nbar\n' to 'hello' this counts
* as 2 changes since this includes 2 deletions.
*
* @return the number of changed elements.
*/
public int getNumDeltaElements() {
return numDelta;
}
/**
* Returns the linked list of hunks
* @return the hunks.
*/
public Hunk getHunks() {
return hunks;
}
/**
* Same as {@link #write(Writer, int)} but to a string buffer.
*
* @param buf the buffer
* @param numContextLines the number of context lines.
*/
public void write(StringBuffer buf, int numContextLines) {
try {
StringWriter out = new StringWriter();
write(new DiffWriter(out), numContextLines);
out.close();
buf.append(out.getBuffer());
} catch (IOException e) {
throw new IllegalStateException(e.toString());
}
}
/**
* Same as {@link #write(DiffWriter, int)} but to a string buffer.
*
* @param buf the buffer
* @param lineSeparator the line separator to use
* @param numContextLines the number of context lines.
*/
public void write(StringBuffer buf, String lineSeparator, int numContextLines) {
try {
StringWriter out = new StringWriter();
write(new DiffWriter(out, lineSeparator), numContextLines);
out.close();
buf.append(out.getBuffer());
} catch (IOException e) {
throw new IllegalStateException(e.toString());
}
}
/**
* Same as {@link #write(DiffWriter, int)} but wraps the given writer
* with a default diff writer.
*
* @param out the writer
* @param numContextLines the number of context lines.
* @throws IOException if an I/O error occurs
*/
public void write(Writer out, int numContextLines) throws IOException {
DiffWriter dw = new DiffWriter(out);
write(dw, numContextLines);
dw.flush();
}
/**
* Writes the differences to the given writer in a unified diff
* format. the context lines specify how many unmodified lines should
* sourround the actual difference.
*
* @param out the writer
* @param numContextLines the number of context lines.
* @throws IOException if an I/O error occurs
*/
public void write(DiffWriter out, int numContextLines) throws IOException {
if (hunks != null) {
if (left.getSource() != null && right.getSource() != null) {
out.write("--- ");
out.write(left.getSource().getLocation());
out.writeNewLine();
out.write("+++ ");
out.write(right.getSource().getLocation());
out.writeNewLine();
} else {
out.write("--- .mine");
out.writeNewLine();
out.write("+++ .theirs");
out.writeNewLine();
}
Hunk hunk = hunks;
while (hunk != null) {
hunk = hunk.write(out, numContextLines);
}
}
}
/**
* init the hunks and the delta counter.
*/
private void init() {
Diff.Change c = change;
int leftPos = 0;
int rightPos = 0;
Hunk first = new Hunk(null, null, 0, null);
Hunk hunk = first;
while (c != null) {
numDelta += Math.max(c.deleted, c.inserted);
if (leftPos < c.line0) {
// add unmodified hunk
int len = c.line0 - leftPos;
hunk = new Hunk(
new Range(left, leftPos, leftPos + len),
new Range(right, rightPos, rightPos + len),
Hunk.UNMODIFIED,
hunk);
leftPos+= len;
rightPos+= len;
}
// add hunk
hunk = new Hunk(
new Range(left, leftPos, leftPos + c.deleted),
new Range(right, rightPos, rightPos + c.inserted),
(c.deleted > 0 ? Hunk.DELETED : 0)|
(c.inserted > 0 ? Hunk.INSERTED : 0),
hunk);
leftPos+=c.deleted;
rightPos+=c.inserted;
c = c.nextChange;
}
// last hunk
if (leftPos < left.getElements().length) {
int len = left.getElements().length - leftPos;
new Hunk(
new Range(left, leftPos, leftPos + len),
new Range(right, rightPos, rightPos + len),
Hunk.UNMODIFIED,
hunk);
}
// and record the first valid hunk
hunks = first.next();
}
/**
* Iterate over all changes and invoke the respective methods in the given
* listener. the context lines specify how many unmodified lines should
* sourround the respective change.
*
* @param listener the change listener
* @param numContextLines the number of context lines
*/
public void showChanges(ChangeListener listener, int numContextLines) {
Diff.Change c = change;
Document.Element[] lines0 = left.getElements();
Document.Element[] lines1 = right.getElements();
listener.onDocumentsStart(left, right);
while (c != null) {
Diff.Change first = c;
int start0 = Math.max(c.line0 - numContextLines, 0);
int end0 = Math.min(c.line0 + c.deleted + numContextLines, lines0.length);
int start1 = Math.max(c.line1 - numContextLines, 0);
int end1 = Math.min(c.line1 + c.inserted + numContextLines, lines1.length);
while (c != null) {
if (c.line0 <= end0) {
end0 = Math.min(c.line0 + c.deleted + numContextLines, lines0.length);
end1 = Math.min(c.line1 + c.inserted + numContextLines, lines1.length);
c = c.nextChange;
} else {
break;
}
}
listener.onChangeStart(start0, end0 - start0 - 1, start1, end1 - start1 - 1);
//dump(fmt, first, c, numContextLines);
while (first != c) {
while (start0 < first.line0) {
listener.onUnmodified(start0, start1, lines0[start0]);
start0++;
start1++;
}
for (int i = 0; i < first.deleted; i++) {
listener.onDeleted(start0, first.line1, lines0[start0]);
start0++;
}
for (int i = 0; i < first.inserted; i++) {
listener.onInserted(first.line0, start1, lines1[start1]);
start1++;
}
first = first.nextChange;
}
for (int i = 0; i < numContextLines && start0 < lines0.length; i++) {
listener.onUnmodified(start0, start1, lines0[start0]);
start0++;
start1++;
}
listener.onChangeEnd();
}
listener.onDocumentsEnd(left, right);
}
/**
* Returns an element factory that provides the elements of the merged
* result of this diff where the left document is dominant.
* @return the merged elements.
*/
public ElementsFactory getMergedLeft() {
return new MergeElementFactory(true);
}
/**
* Returns an element factory that provides the elements of the merged
* result of this diff where the right document is dominant.
* @return the merged elements.
*/
public ElementsFactory getMergedRight() {
return new MergeElementFactory(false);
}
/**
* element factory that provides the merged result.
*/
private class MergeElementFactory implements ElementsFactory {
/**
* if {@code true}, do the merge reverse
*/
boolean reverse;
/**
* Create a new element factory.
* @param reverse if {@code true}, do the merge revese
*/
public MergeElementFactory(boolean reverse) {
this.reverse = reverse;
}
/**
* {@inheritDoc}
*/
public Document.Element[] getElements() {
Diff.Change c = change;
Document.Element[] lines0 = left.getElements();
Document.Element[] lines1 = right.getElements();
ArrayList elems = new ArrayList();
while (c != null) {
Diff.Change first = c;
int start0 = 0;
int end0 = lines0.length;
int start1 = 0;
int end1 = lines1.length;
while (c != null) {
if (c.line0 <= end0) {
end0 = lines0.length;
end1 = lines1.length;
c = c.nextChange;
} else {
break;
}
}
while (first != c) {
while (start0 < first.line0) {
elems.add(lines0[start0]);
start0++;
start1++;
}
for (int i = 0; i < first.deleted; i++) {
if (reverse) {
elems.add(lines0[start0]);
}
start0++;
}
for (int i = 0; i < first.inserted; i++) {
if (!reverse) {
elems.add(lines1[start1]);
}
start1++;
}
first = first.nextChange;
}
while (start0 < lines0.length) {
elems.add(lines0[start0]);
start0++;
start1++;
}
}
return (Document.Element[]) elems.toArray(new Document.Element[elems.size()]);
}
}
}