package com.canoo.webtest.extension;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import com.gargoylesoftware.htmlunit.WebResponse;
/**
* Produces a unified diff like output (should be like unified diff, but not sure).
* @author Marc Guillemot
*/
public class VerifyContentTextDiff implements VerifyContentDiff
{
private final static int CONTEXT_SIZE = 3;
final List deltas = new ArrayList();
private List hashValuesA;
private List hashValuesB;
protected String[] tabReference;
protected String[] tabActual;
/**
* {@inheritDoc}
*/
public String compare(final WebResponse reference, final WebResponse actual,
final String referenceLabel, final String actualLabel)
{
final String referenceText = reference.getContentAsString();
final String actualText = actual.getContentAsString();
return produceTextDiffMessage(referenceText, actualText, referenceLabel, actualLabel);
}
/**
* Resets the state to allow new computation
*/
protected void reset() {
deltas.clear();
hashValuesA = null;
hashValuesB = null;
tabReference = null;
tabActual = null;
}
String produceTextDiffMessage(final String referenceText, final String actualText,
final String referenceLabel, final String actualLabel)
{
reset();
tabReference = referenceText.split("[\\n\\r]+");
tabActual = actualText.split("[\\n\\r]+");
preProcess();
compare(0, tabReference.length, 0, tabActual.length);
if (deltas.isEmpty())
return null;
final StringBuffer sb = new StringBuffer();
sb.append("--- " + referenceLabel + "\n");
sb.append("+++ " + actualLabel + "\n");
printDiff(sb);
return sb.toString();
}
/**
* Called before comparision starts
*/
protected void preProcess() {
hashValuesA = computeHash(tabReference);
hashValuesB = computeHash(tabActual);
}
private void printDiff(final StringBuffer sb)
{
for (final Iterator iter = getContexts(deltas).iterator(); iter.hasNext();)
{
final Context context = (Context) iter.next();
context.print(sb);
}
}
private List getContexts(final List _deltas)
{
if (_deltas.isEmpty())
return Collections.EMPTY_LIST;
final List contexts = new ArrayList();
Context context = new Context();
contexts.add(context);
for (final Iterator iter = _deltas.iterator(); iter.hasNext();)
{
final Delta delta = (Delta) iter.next();
if (!context.accept(delta))
{
context = new Context();
contexts.add(context);
}
context.addDelta(delta);
}
return contexts;
}
private void compare(int startA, int endA, int startB, int endB)
{
// handle identical content at the start
while (startA < endA && startB < endB && sameContent(startA, startB))
{
startB++;
startA++;
}
// handle identical content at the end
while (endA > startA && endB > startB && sameContent(endA - 1, endB - 1))
{
endB--;
endA--;
}
if (startA == endA)
{
for (int i=startB; i < endB; ++i)
deltas.add(new Delta(startA, i, 'B'));
}
else if (startB == endB)
{
for (int i=startA; i < endA; ++i)
deltas.add(new Delta(i, startB, 'A'));
}
else if (startA == endA - 1 && startB == endB - 1)
{
if (!sameContent(startA, startB))
{
deltas.add(new Delta(startA, startB, 'A'));
deltas.add(new Delta(startA, startB, 'B'));
}
}
else
{
final MidPoint splitPoints = retrieveMidPoint(startA, endA, startB, endB);
compare(startA, splitPoints.lineA, startB, splitPoints.lineB);
compare(splitPoints.lineA, endA, splitPoints.lineB, endB);
}
}
/**
* Indicates if 2 content lines are identical
* @param posA the line in the first content
* @param posB the line in the 2nd content
* @return <code>true</code> if lines are identical
*/
protected boolean sameContent(final int posA, final int posB) {
return hashValuesA.get(posA).equals(hashValuesB.get(posB));
}
static class MidPoint
{
final int lineA;
final int lineB;
public MidPoint(final int _first, final int _second)
{
lineA = _first;
lineB = _second;
}
}
private MidPoint retrieveMidPoint(final int startA, final int endA, final int startB, final int endB)
{
final int midA = (endA + startA) / 2;
final int midB = (endB + startB) / 2;
int index1 = midA;
int incrementA = -1;
while (index1 >= startA && index1 < endA)
{
int indexB = midB;
int incrementB = -1;
while (indexB >= startB && indexB < endB)
{
if (sameContent(index1, indexB))
{
return new MidPoint(index1, indexB);
}
indexB += incrementB;
incrementB = nextIncrement(incrementB);
}
index1 += incrementA;
incrementA = nextIncrement(incrementA);
}
return new MidPoint(midA, midB);
}
static int nextIncrement(final int increment)
{
if (increment < 0)
return -increment + 1;
else
return -increment - 1;
}
/**
* Holds information about differences
*/
static private class Delta
{
final int lineA;
final int lineB;
final char type;
public Delta(final int _lineA, final int _lineB, final char _type)
{
lineA = _lineA;
lineB = _lineB;
type = _type;
}
}
/**
* Contains a group of deltas
*/
private class Context
{
final List deltas = new ArrayList();
int firstLineA = -1;
int lastLineA = -1;
private int firstA = -1;
private int lastA = -1;
private Delta firstDelta;
public boolean accept(final Delta _delta)
{
if (_delta.type == 'B')
return true;
else if (firstLineA == -1)
return true;
else if (_delta.lineA <= lastLineA)
return true;
return false;
}
public void addDelta(final Delta _delta)
{
if (firstDelta == null)
firstDelta = _delta;
deltas.add(_delta);
if (_delta.type == 'A')
{
if (firstLineA == -1)
{
firstA = _delta.lineA;
firstLineA = Math.max(1, firstA - CONTEXT_SIZE);
}
lastA = _delta.lineA;
lastLineA = Math.min(tabReference.length, lastA + CONTEXT_SIZE);
}
}
int getFirstContextLineB()
{
return Math.max(1, firstDelta.lineB - CONTEXT_SIZE);
}
int getLastContextLineB()
{
final Delta lastDelta = (Delta) deltas.get(deltas.size() - 1);
return Math.min(tabActual.length, lastDelta.lineB + CONTEXT_SIZE);
}
void print(final StringBuffer sb)
{
// the chunk range information
sb.append("@@ -");
sb.append(firstLineA);
sb.append(",");
sb.append(lastLineA - firstLineA + 1);
sb.append(" +");
sb.append(getFirstContextLineB());
sb.append(",");
sb.append(getLastContextLineB() - getFirstContextLineB() + 1);
sb.append(" @@");
sb.append("\n");
for (int i=firstLineA; i<firstA; ++i)
{
sb.append(" ").append(tabReference[i]).append("\n");
}
for (final Iterator iter = deltas.iterator(); iter.hasNext();)
{
final Delta delta = (Delta) iter.next();
if (delta.type == 'B')
sb.append("+").append(tabActual[delta.lineB]).append("\n");
else
sb.append("-").append(tabReference[delta.lineA]).append("\n");
}
for (int i=lastA+1; i<lastLineA; ++i)
{
sb.append(" ").append(tabReference[i]).append("\n");
}
}
}
/**
* Gets for each element its hashCode()
* @param tab
*/
private List computeHash(final String[] tab) {
final List hashValues = new ArrayList(tab.length);
for (int i = 0; i < tab.length; i++)
{
hashValues.add(new Integer(tab[i].hashCode()));
}
return hashValues;
}
}