/*
* Copyright 2007 Guy Van den Broeck
*
* Licensed 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.outerj.daisy.diff.html;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import org.eclipse.compare.rangedifferencer.IRangeComparator;
import org.outerj.daisy.diff.html.ancestor.AncestorComparator;
import org.outerj.daisy.diff.html.ancestor.AncestorComparatorResult;
import org.outerj.daisy.diff.html.dom.BodyNode;
import org.outerj.daisy.diff.html.dom.DomTree;
import org.outerj.daisy.diff.html.dom.Node;
import org.outerj.daisy.diff.html.dom.TagNode;
import org.outerj.daisy.diff.html.dom.TextNode;
import org.outerj.daisy.diff.html.dom.helper.LastCommonParentResult;
import org.outerj.daisy.diff.html.modification.Modification;
import org.outerj.daisy.diff.html.modification.ModificationType;
/**
* A comparator that generates a DOM tree of sorts from handling SAX events.
* Then it can be used to compute the difference between DOM trees and mark
* elements accordingly.
*/
public class TextNodeComparator implements IRangeComparator, Iterable<TextNode> {
private List<TextNode> textNodes = new ArrayList<TextNode>(50);
private List<Modification> lastModified = new ArrayList<Modification>();
private BodyNode bodyNode;
private Locale locale;
public TextNodeComparator(DomTree tree, Locale locale) {
super();
this.locale = locale;
textNodes = tree.getTextNodes();
bodyNode = tree.getBodyNode();
}
public BodyNode getBodyNode() {
return bodyNode;
}
public int getRangeCount() {
return textNodes.size();
}
public TextNode getTextNode(int i) {
return textNodes.get(i);
}
private long newID = 0;
/**
* Marks the given range as new. In the output, the range will be formatted as
* specified by the anOutputFormat parameter.
* @param start
* @param end
* @param outputFormat specifies how this range shall be formatted in the output
*/
public void markAsNew(int start, int end, ModificationType outputFormat) {
if (end <= start)
return;
if (whiteAfterLastChangedPart)
getTextNode(start).setWhiteBefore(false);
List<Modification> nextLastModified = new ArrayList<Modification>();
for (int i = start; i < end; i++) {
Modification mod = new Modification(ModificationType.ADDED, outputFormat);
mod.setID(newID);
if (lastModified.size() > 0) {
mod.setPrevious(lastModified.get(0));
if (lastModified.get(0).getNext() == null) {
for (Modification lastMod : lastModified) {
lastMod.setNext(mod);
}
}
}
nextLastModified.add(mod);
getTextNode(i).setModification(mod);
}
getTextNode(start).getModification().setFirstOfID(true);
newID++;
lastModified = nextLastModified;
}
/**
* Marks the given range as new. In the output, the range will be formatted
* as "added".
* @param start
* @param end
*/
public void markAsNew(int start, int end) {
markAsNew(start, end, ModificationType.ADDED);
}
public boolean rangesEqual(int i1, IRangeComparator rangeComp, int i2) {
TextNodeComparator comp;
try {
comp = (TextNodeComparator) rangeComp;
} catch (RuntimeException e) {
return false;
}
return getTextNode(i1).isSameText(comp.getTextNode(i2));
}
public boolean skipRangeComparison(int arg0, int arg1, IRangeComparator arg2) {
return false;
}
private long changedID = 0;
private boolean changedIDUsed = false;
public void handlePossibleChangedPart(int leftstart, int leftend,
int rightstart, int rightend, TextNodeComparator leftComparator) {
int i = rightstart;
int j = leftstart;
if (changedIDUsed) {
changedID++;
changedIDUsed = false;
}
List<Modification> nextLastModified = new ArrayList<Modification>();
String changes = null;
while (i < rightend) {
AncestorComparator acthis = new AncestorComparator(getTextNode(i)
.getParentTree());
AncestorComparator acother = new AncestorComparator(leftComparator
.getTextNode(j).getParentTree());
AncestorComparatorResult result = acthis.getResult(acother, locale);
if (result.isChanged()) {
Modification mod = new Modification(ModificationType.CHANGED, ModificationType.CHANGED);
if (!changedIDUsed) {
mod.setFirstOfID(true);
if (nextLastModified.size() > 0) {
lastModified = nextLastModified;
nextLastModified = new ArrayList<Modification>();
}
} else if (result.getChanges() != null
&& !result.getChanges().equals(changes)) {
changedID++;
mod.setFirstOfID(true);
if (nextLastModified.size() > 0) {
lastModified = nextLastModified;
nextLastModified = new ArrayList<Modification>();
}
}
if (lastModified.size() > 0) {
mod.setPrevious(lastModified.get(0));
if (lastModified.get(0).getNext() == null) {
for (Modification lastMod : lastModified) {
lastMod.setNext(mod);
}
}
}
nextLastModified.add(mod);
mod.setChanges(result.getChanges());
mod.setHtmlLayoutChanges(result.getHtmlLayoutChanges());
mod.setID(changedID);
getTextNode(i).setModification(mod);
changes = result.getChanges();
changedIDUsed = true;
} else if (changedIDUsed) {
changedID++;
changedIDUsed = false;
}
i++;
j++;
}
if (nextLastModified.size() > 0)
lastModified = nextLastModified;
}
// used to remove the whitespace between a red and green block
private boolean whiteAfterLastChangedPart = false;
private long deletedID = 0;
/**
* Marks the given range as deleted. In the output, the range will be
* formatted as specified by the parameter anOutputFormat.
* @param start
* @param end
* @param oldComp
* @param before
* @param anOutputFormat specifies how this range shall be formatted in the output
*/
public void markAsDeleted(int start, int end, TextNodeComparator oldComp,
int before, int after, ModificationType outputFormat) {
if (end <= start)
return;
if (before > 0 && getTextNode(before - 1).isWhiteAfter()) {
whiteAfterLastChangedPart = true;
} else {
whiteAfterLastChangedPart = false;
}
List<Modification> nextLastModified = new ArrayList<Modification>();
for (int i = start; i < end; i++) {
Modification mod = new Modification(ModificationType.REMOVED, outputFormat);
mod.setID(deletedID);
if (lastModified.size() > 0) {
mod.setPrevious(lastModified.get(0));
if (lastModified.get(0).getNext() == null) {
for (Modification lastMod : lastModified) {
lastMod.setNext(mod);
}
}
}
nextLastModified.add(mod);
// oldComp is used here because we're going to move its deleted
// elements
// to this tree!
oldComp.getTextNode(i).setModification(mod);
}
oldComp.getTextNode(start).getModification().setFirstOfID(true);
List<Node> deletedNodes = oldComp.getBodyNode().getMinimalDeletedSet(
deletedID);
// Set prevLeaf to the leaf after which the old HTML needs to be
// inserted
Node prevLeaf = null;
if (before > 0)
prevLeaf = getTextNode(before - 1);
// Set nextLeaf to the leaf before which the old HTML needs to be
// inserted
Node nextLeaf = null;
boolean useAfter = false;
if (after < getRangeCount()) {
LastCommonParentResult orderResult = getTextNode(before).getLastCommonParent(getTextNode(after));
List<TagNode> check = getTextNode(before).getParentTree();
Collections.reverse(check);
for(TagNode curr : check) {
if(curr == orderResult.getLastCommonParent()) {
break;
} else if (curr.isBlockLevel()) {
useAfter = true;
break;
}
}
if(!useAfter) {
check = getTextNode(after).getParentTree();
Collections.reverse(check);
for(TagNode curr : check) {
if(curr == orderResult.getLastCommonParent()) {
break;
} else if (curr.isBlockLevel()) {
useAfter = true;
break;
}
}
}
} else {
useAfter = false;
}
if(useAfter)
nextLeaf = getTextNode(after);
else if (before < getRangeCount())
nextLeaf = getTextNode(before);
while (deletedNodes.size() > 0) {
LastCommonParentResult prevResult, nextResult;
if (prevLeaf != null) {
prevResult = prevLeaf.getLastCommonParent(deletedNodes
.get(0));
} else {
prevResult = new LastCommonParentResult();
prevResult.setLastCommonParent(getBodyNode());
prevResult.setIndexInLastCommonParent(-1);
}
if (nextLeaf != null) {
nextResult = nextLeaf.getLastCommonParent(deletedNodes
.get(deletedNodes.size() - 1));
} else {
nextResult = new LastCommonParentResult();
nextResult.setLastCommonParent(getBodyNode());
nextResult.setIndexInLastCommonParent(getBodyNode()
.getNbChildren());
}
if (prevResult.getLastCommonParentDepth() == nextResult
.getLastCommonParentDepth()) {
// We need some metric to choose which way to add...
if (deletedNodes.get(0).getParent() == deletedNodes.get(
deletedNodes.size() - 1).getParent()
&& prevResult.getLastCommonParent() == nextResult
.getLastCommonParent()) {
// The difference is not in the parent
prevResult.setLastCommonParentDepth(prevResult
.getLastCommonParentDepth() + 1);
} else {
// The difference is in the parent, so compare them
// now THIS is tricky
double distancePrev = deletedNodes
.get(0)
.getParent()
.getMatchRatio(prevResult.getLastCommonParent());
double distanceNext = deletedNodes
.get(deletedNodes.size() - 1)
.getParent()
.getMatchRatio(nextResult.getLastCommonParent());
if (distancePrev <= distanceNext) {
// insert after the previous node
prevResult.setLastCommonParentDepth(prevResult
.getLastCommonParentDepth() + 1);
} else {
// insert before the next node
nextResult.setLastCommonParentDepth(nextResult
.getLastCommonParentDepth() + 1);
}
}
}
if (prevResult.getLastCommonParentDepth() > nextResult
.getLastCommonParentDepth()) {
// Inserting at the front
if (prevResult.isSplittingNeeded()) {
prevLeaf.getParent().splitUntill(
prevResult.getLastCommonParent(), prevLeaf,
true);
}
prevLeaf = deletedNodes.remove(0).copyTree();
prevLeaf.setParent(prevResult.getLastCommonParent());
prevResult.getLastCommonParent().addChild(
prevResult.getIndexInLastCommonParent() + 1,
prevLeaf);
} else if (prevResult.getLastCommonParentDepth() < nextResult
.getLastCommonParentDepth()) {
// Inserting at the back
if (nextResult.isSplittingNeeded()) {
boolean splitOccured = nextLeaf.getParent()
.splitUntill(nextResult.getLastCommonParent(),
nextLeaf, false);
if (splitOccured) {
// The place where to insert is shifted one place to the
// right
nextResult.setIndexInLastCommonParent(nextResult
.getIndexInLastCommonParent() + 1);
}
}
nextLeaf = deletedNodes.remove(deletedNodes.size() - 1)
.copyTree();
nextLeaf.setParent(nextResult.getLastCommonParent());
nextResult.getLastCommonParent().addChild(
nextResult.getIndexInLastCommonParent(), nextLeaf);
} else
throw new IllegalStateException();
}
lastModified = nextLastModified;
deletedID++;
}
/**
* Marks the given range as deleted. In the output, the range will be
* formatted as "removed".
* @param start
* @param end
* @param oldComp
* @param before
*/
public void markAsDeleted(int start, int end, TextNodeComparator oldComp,
int before, int after) {
markAsDeleted(start, end, oldComp, before, after, ModificationType.REMOVED);
}
public void expandWhiteSpace() {
getBodyNode().expandWhiteSpace();
}
public Iterator<TextNode> iterator() {
return textNodes.iterator();
}
/**
* Used for combining multiple comparators in order to create a single
* output document. The IDs must be successive along the different
* comparators.
* @param aDeletedID
*/
public void setStartDeletedID(long aDeletedID) {
deletedID = aDeletedID;
}
/**
* Used for combining multiple comparators in order to create a single
* output document. The IDs must be successive along the different
* comparators.
* @param aDeletedID
*/
public void setStartChangedID(long aChangedID) {
changedID = aChangedID;
}
/**
* Used for combining multiple comparators in order to create a single
* output document. The IDs must be successive along the different
* comparators.
* @param aDeletedID
*/
public void setStartNewID(long aNewID) {
newID = aNewID;
}
public long getChangedID() {
return changedID;
}
public long getDeletedID() {
return deletedID;
}
public long getNewID() {
return newID;
}
public List<Modification> getLastModified() {
return lastModified;
}
public void setLastModified(List<Modification> aLastModified) {
lastModified = new ArrayList<Modification>(aLastModified);
}
}