/**
* 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.waveprotocol.wave.model.document.util;
import org.waveprotocol.wave.model.document.MutableDocument;
import org.waveprotocol.wave.model.document.ReadableDocument;
import org.waveprotocol.wave.model.document.ReadableWDocument;
import org.waveprotocol.wave.model.document.indexed.LocationMapper;
import org.waveprotocol.wave.model.document.operation.Attributes;
import org.waveprotocol.wave.model.document.raw.TextNodeOrganiser;
import org.waveprotocol.wave.model.document.raw.impl.Element;
import org.waveprotocol.wave.model.document.raw.impl.Node;
import org.waveprotocol.wave.model.document.raw.impl.Text;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.IdentityMap;
import org.waveprotocol.wave.model.util.Preconditions;
/**
* Miscellaneous document helper functions
*
* @author danilatos@google.com (Daniel Danilatos)
*/
//
// DO NOT JUST PUT ANY ARBITRARY MISCELLANEOUS STUFF IN HERE
//
// (Please think of the big picture - there is too much overlap
// of partially useful utility methods)
//
// If in doubt, send CL to dan
//
// ALL NEW METHODS MUST BE 100% THOROUGHLY UNIT TESTED
//
public class DocHelper {
/**
* Expectations for top-level element existence, used by
* {@link #getOrCreateFirstTopLevelElement(MutableDocument, String, Expectation)}
* . Since the code that uses this does its own interpretation, it is a
* requirement that the semantic intersection of any two values is empty.
*/
private enum Expectation {
NONE,
ABSENT,
PRESENT
}
/**
* "Call" this method so the compiler can help us find code that will break
* when we make the root an implicit object, and location zero refers to its
* first child.
*
* A lot of test cases will need 1 subtracted from their use of hard coded
* integer location values
*/
public static void noteCodeThatWillBreakWithMultipleRoots() {
}
private static class NodeOffset<N> {
/**
* If is an element, then means "node after", and offset is meaningless
* Otherwise, the NodeOffset is the same as an inTextNode point
*/
N node;
int offset;
}
/**
* Action that can be applied to a node.
*
* @param <N> Node
*/
public interface NodeAction<N> {
void apply(N node);
}
private DocHelper() { }
/**
* Checks whether a location has some text immediately to its left.
*
* @return true if text data precedes the given location
*/
public static <N, E extends N, T extends N> boolean textPrecedes(
ReadableDocument<N, E, T> doc, LocationMapper<N> mapper, int location) {
Point<N> point = mapper.locate(location);
if (point.isInTextNode()) {
return point.getTextOffset() > 0
|| doc.asText(doc.getPreviousSibling(point.getContainer())) != null;
} else {
return doc.asText(Point.nodeBefore(doc, point.asElementPoint())) != null;
}
}
/**
* Checks whether a location has some text immediately to its right.
*
* @return true if text data follows the given location
*/
public static <N, E extends N, T extends N> boolean textFollows(
LocationMapper<N> mapper, int location) {
// Locating points always biases to the right, so this case is easy
return mapper.locate(location).asTextPoint() != null;
}
/**
* Returns the first element in the doc with the given tag name. The root
* element will never match.
*
* @param doc document to look in
* @param tagName tag name to find
* @return the first element in the doc with tagName, or null if none exist
*/
public static <N, E extends N> E getElementWithTagName(
ReadableDocument<N, E, ?> doc, String tagName) {
return getElementWithTagName(doc, tagName, doc.getDocumentElement());
}
/**
* Returns the first element in a subtree with the given tag name. The subtree
* root will never match.
*
* @param doc document to look in
* @param tagName tag name to find
* @param subtreeRoot of the subtree to search (exclusive)
* @return the first element in the subtree with tagName, or null if none
* exist
*/
public static <N, E extends N> E getElementWithTagName(ReadableDocument<N, E, ?> doc,
String tagName, E subtreeRoot) {
N node = DocHelper.getNextNodeDepthFirst(doc, subtreeRoot, subtreeRoot, true);
while (node != null) {
E element = doc.asElement(node);
if (element != null) {
if (doc.getTagName(element).equals(tagName)) {
return element;
}
}
node = DocHelper.getNextNodeDepthFirst(doc, node, subtreeRoot, true);
}
return null;
}
/**
* Returns the last element in the doc with the given tag name. The subtree root
* element will never match.
*
* @param doc document to look in
* @param tagName tag name to find
* @return the last element in the doc with tagName, or null if none exist
*/
public static <N, E extends N> E getLastElementWithTagName(
ReadableDocument<N, E, ?> doc, String tagName) {
return getLastElementWithTagName(doc, tagName, doc.getDocumentElement());
}
/**
* Returns the last element in a subtree with the given tag name. The subtree
* root will never match.
*
* @param doc document to look in
* @param tagName tag name to find
* @return the last element in the subtree with tagName, or null if none exist
*/
public static <N, E extends N> E getLastElementWithTagName(ReadableDocument<N, E, ?> doc,
String tagName, E subtreeRoot) {
N node = DocHelper.getPrevNodeDepthFirst(doc, subtreeRoot, subtreeRoot, true);
while (node != null) {
E element = doc.asElement(node);
if (element != null) {
if (doc.getTagName(element).equals(tagName)) {
return element;
}
}
node = DocHelper.getPrevNodeDepthFirst(doc, node, subtreeRoot, true);
}
return null;
}
/**
* Get the text within the given element.
*/
public static <N, E extends N, T extends N> String getText(ReadableWDocument<N, E, T> doc,
E element) {
return getText(doc, doc, element);
}
/**
* Get the text within the given element.
*/
public static <N, E extends N, T extends N> String getText(ReadableDocument<N, E, T> doc,
LocationMapper<N> mapper, E element) {
int start = mapper.getLocation(Point.start(doc, element));
int end = mapper.getLocation(Point.<N>end(element));
return DocHelper.getText(doc, mapper, start, end);
}
/**
* Shortcut to get the text for an element with a specific tag name.
* @see DocHelper#getElementWithTagName(ReadableDocument, String)
* @see DocHelper#getText(ReadableDocument, LocationMapper, Object)
*/
public static <N> String getTextForElement(
ReadableWDocument<N, ?, ?> doc, String tagName) {
return getTextForElement(doc, doc, tagName);
}
/**
* Shortcut to get the text for an element with a specific tag name.
* @see DocHelper#getElementWithTagName(ReadableDocument, String)
* @see DocHelper#getText(ReadableDocument, LocationMapper, Object)
*/
public static <N, E extends N, T extends N> String getTextForElement(
ReadableDocument<N, E, T> doc, LocationMapper<N> mapper, String tagName) {
E element = getElementWithTagName(doc, tagName);
if (element != null) {
return getText(doc, mapper, element);
}
return null;
}
/**
* Variant that accepts an indexed document instead
* @see #getText(ReadableDocument, LocationMapper, int, int)
*/
public static <N> String getText(ReadableWDocument<N, ?, ?> doc, int start, int end) {
return getText(doc, doc, start, end);
}
/**
* Gets text between two locations, using a mapper to convert to points.
* @see #getText(ReadableDocument, Point, Point)
*/
public static <N, E extends N, T extends N> String getText(
ReadableDocument<N, E, T> doc, LocationMapper<N> mapper,
int start, int end) {
Preconditions.checkPositionIndexes(start, end, mapper.size());
Point<N> startPoint = mapper.locate(start);
Point<N> endPoint = mapper.locate(end);
return getText(doc, startPoint, endPoint);
}
/** Get the text between a given range */
public static <N, E extends N, T extends N> String getText(
ReadableDocument<N, E, T> doc, Point<N> startPoint, Point<N> endPoint) {
NodeOffset<N> output = new NodeOffset<N>();
getNodeAfterOutwards(doc, startPoint, output);
N startNode = output.node;
int startOffset = output.offset;
getNodeAfterOutwards(doc, endPoint, output);
N endNode = output.node;
int endOffset = output.offset;
if (startNode == null) {
return "";
}
T text = doc.asText(startNode);
if (doc.isSameNode(startNode, endNode)) {
return text == null ? "" : doc.getData(text).substring(startOffset, endOffset);
}
StringBuilder str = new StringBuilder();
if (text != null) {
str.append(doc.getData(text).substring(startOffset));
}
N node = getNextNodeDepthFirst(doc, startNode, null, true);
while (node != endNode) {
text = doc.asText(node);
if (text != null) {
str.append(doc.getData(text));
}
node = getNextNodeDepthFirst(doc, node, null, true);
}
text = doc.asText(node);
if (text != null) {
str.append(doc.getData(text).substring(0, endOffset));
}
return str.toString();
}
/**
* Step out of end tags, so we get something that is either in a text node,
* or the node after our point in a pre-order traversal
*/
private static <N, E extends N, T extends N> void getNodeAfterOutwards(
ReadableDocument<N, E, T> doc, Point<N> point, NodeOffset<N> output) {
N node;
int startOffset;
if (point.isInTextNode()) {
node = point.getContainer();
startOffset = point.getTextOffset();
} else {
node = point.getNodeAfter();
if (node == null) {
N parent = point.getContainer();
while (parent != null) {
node = doc.getNextSibling(parent);
if (node != null) {
break;
}
parent = doc.getParentElement(parent);
}
}
startOffset = 0;
}
output.node = node;
output.offset = startOffset;
}
/**
* Get the next node in a depth first traversal.
*
* TODO(danilatos): Move this somewhere common (and use for better filtered
* traversals).
*
* @param doc The view to use
* @param start The node to start from
* @param stopAt If we reach this node, return null. If already in the node,
* will only stop while exiting having traversed all its children. If
* we start outside it, it will not be entered.
* @param enter Enter the start node if it is an element (false to skip its
* children - only applies to the start node)
*/
public static <N> N getNextNodeDepthFirst(
ReadableDocument<N, ?, ?> doc, N start, N stopAt, boolean enter) {
return getNextOrPrevNodeDepthFirst(doc, start, stopAt, enter, true);
}
/**
* Same as {@link #getNextNodeDepthFirst(ReadableDocument, Object, Object, boolean)},
* but goes in the other direction
*/
public static <N> N getPrevNodeDepthFirst(
ReadableDocument<N, ?, ?> doc, N start, N stopAt, boolean enter) {
return getNextOrPrevNodeDepthFirst(doc, start, stopAt, enter, false);
}
/**
* Same as {@link #getNextNodeDepthFirst(ReadableDocument, Object, Object, boolean)}
* and {@link #getPrevNodeDepthFirst(ReadableDocument, Object, Object, boolean)}
* except direction is parametrised.
* @param rightwards If true, then go rightwards, otherwise leftwards.
*/
public static <N, E extends N, T extends N> N getNextOrPrevNodeDepthFirst(
ReadableDocument<N, E, T> doc, N start, N stopAt, boolean enter, boolean rightwards) {
// Default stopping place is the very top
if (stopAt == null) {
stopAt = doc.getDocumentElement();
}
// Maybe enter into an element
N next;
if (enter) {
E element = doc.asElement(start);
if (element != null) {
next = rightwards ? doc.getFirstChild(element) : doc.getLastChild(element);
if (next != null) {
return next;
}
}
}
// Go upwards from exiting an element
while (start != null && !doc.isSameNode(start, stopAt)) {
next = rightwards ? doc.getNextSibling(start) : doc.getPreviousSibling(start);
if (doc.isSameNode(next, stopAt)) {
return null;
}
if (next != null) {
return next;
}
start = doc.getParentElement(start);
}
return null;
}
/**
* Same as {@link #getFilteredPoint(ReadableDocumentView, Point)}, but
* returns an integer location
*/
public static <N, E extends N, T extends N> int getFilteredLocation(
LocationMapper<N> locationMapper, ReadableDocumentView<N, E, T> filteredView,
Point<N> point) {
return locationMapper.getLocation(getFilteredPoint(filteredView, point));
}
/**
* Gets the location of a given point in the DOM.
*
* @param filteredView
* @param point
* @return the location of the given point.
*/
public static <N, E extends N, T extends N> Point<N> getFilteredPoint(
ReadableDocumentView<N, E, T> filteredView, Point<N> point) {
filteredView.onBeforeFilter(point);
if (point.isInTextNode()) {
N visible;
visible = filteredView.getVisibleNode(point.getContainer());
if (visible == point.getContainer()) {
return point;
} else {
N next = getNextNodeDepthFirst(filteredView, point.getContainer(), visible, false);
if (next == null) {
return Point.inElement(visible, null);
} else {
return Point.before(filteredView, next);
}
}
} else if (point.getNodeAfter() == null) {
return getLocationOfNodeEnd(filteredView, point.getContainer());
} else {
return getLocationOfBeforeNode(filteredView, point.getNodeAfter());
}
}
/**
* Get location of the end of the inside of the given node
*/
private static <N, E extends N, T extends N> Point<N> getLocationOfNodeEnd(
ReadableDocumentView<N, E, T> doc, N node) {
assert node != null : "Node is null";
N parent = doc.getVisibleNode(node);
assert parent != null : "Parent is null";
if (parent == node) {
return Point.end(node);
}
N next = DocHelper.getNextNodeDepthFirst(doc, node, parent, false);
if (next == null) {
return Point.end(parent);
} else {
return Point.before(doc, next);
}
}
/**
* Get location of the outside of the start of the given node
*/
private static <N, E extends N, T extends N> Point<N> getLocationOfBeforeNode(
ReadableDocumentView<N, E, T> doc, N node) {
assert node != doc.getDocumentElement() : "Cannot get location outside of root element";
N parent = doc.getVisibleNode(node);
if (parent == node) {
return Point.before(doc, node);
}
assert parent != null;
N next = DocHelper.getNextNodeDepthFirst(doc, node, parent, true);
if (next == null) {
return Point.end(parent);
} else {
return Point.before(doc, next);
}
}
public static <N, T extends N> int getItemSize(ReadableWDocument<N, ?, T> doc, N node) {
// Short circuit if it's a text node, implementation is simpler
T textNode = doc.asText(node);
if (textNode != null) {
return doc.getLength(textNode);
}
// Otherwise, calculate two locations and subtract
N parent = doc.getParentElement(node);
if (parent == null) {
// Requesting size of the document root.
// TODO(danilatos/anorth) This would change if we have multiple roots.
noteCodeThatWillBreakWithMultipleRoots();
return doc.size();
}
N next = doc.getNextSibling(node);
int locationAfter = next != null ? doc.getLocation(next)
: doc.getLocation(Point.end(parent));
return locationAfter - doc.getLocation(node);
}
/**
* Normalizes a point so that it is biased towards text nodes, and node ends
* rather than node start.
*
* @param <N>
* @param <E>
* @param <T>
* @param point
* @param doc
*/
public static <N, E extends N, T extends N> Point<N> normalizePoint(Point<N> point,
ReadableDocument<N, E, T> doc) {
N previous = null;
if (!point.isInTextNode()) {
previous = Point.nodeBefore(doc, point.asElementPoint());
T nodeAfterAsText = doc.asText(point.getNodeAfter());
if (nodeAfterAsText != null) {
point = Point.<N>inText(nodeAfterAsText, 0);
}
} else if (point.getTextOffset() == 0) {
previous = doc.getPreviousSibling(point.getContainer());
}
T previousAsText = doc.asText(previous);
if (previous != null && previousAsText != null) {
point = Point.inText(previous, doc.getLength(previousAsText));
}
return point;
}
/**
* Left-aligns a position in a document, given a view over that document of places to align to.
* Achieved by traversing the point backwards through the full document until a position in the
* view is found, then returning a point at that position.
*
* @param current The point in the fullDoc to align
* @param fullDoc Complete document
* @param important view over the complete document
* @return The aligned point in the full document (may use nodes not in the view)
*/
public static <N, E extends N, T extends N> Point<N> leftAlign(Point<N> current,
ReadableDocument<N, E, T> fullDoc, ReadableDocumentView<N, E, T> important) {
if (current == null || current.isInTextNode()) {
return current; // assume text nodes are already aligned
}
N parent = current.getContainer();
N at = current.getNodeAfter();
// calculate the node before the point
N lastBefore = null;
if (at == null) {
lastBefore = fullDoc.getLastChild(parent);
} else {
lastBefore = fullDoc.getPreviousSibling(at);
}
// nothing before the at node, so move up one level
N visibleParent = important.getVisibleNode(parent);
if (lastBefore == null) {
if (parent == visibleParent) {
return Point.textOrElementStart(fullDoc, parent);
}
lastBefore = parent;
}
// and move backwards (starting from right-most child) until we have an important node
N nodeLast = important.getVisibleNodeLast(lastBefore);
N lcaVis = nodeLast == null ? lastBefore : nearestCommonAncestor(fullDoc, nodeLast, lastBefore);
// special case when last visible is a parent - so use visibleParent iff it is a child of lcaVis
if (isAncestor(fullDoc, lcaVis, visibleParent, false)) {
return Point.textOrElementStart(fullDoc, visibleParent);
} else {
lastBefore = nodeLast;
}
// get the child after the node before the new point, then correct the parent in full document.
at = lastBefore == null ? null : important.getNextSibling(lastBefore);
if (at != null) {
parent = fullDoc.getParentElement(at);
} else if (lastBefore != null) {
parent = fullDoc.getParentElement(lastBefore);
}
return at == null ? Point.end(parent) : Point.before(fullDoc, at);
}
/**
* Gets the first child element of an element, if there is one.
*
* @param doc document accessor
* @param element parent element
* @return the first child element of {@code element} if there is one,
* otherwise {@code null}.
*/
public static <N, E extends N> E getFirstChildElement(ReadableDocument<N, E, ?> doc, E element) {
return getNextElementInclusive(doc, doc.getFirstChild(element), true);
}
/**
* Gets the last child element of an element, if there is one.
*
* @param doc document accessor
* @param element parent element
* @return the last child element of {@code element} if there is one,
* otherwise {@code null}.
*/
public static <N, E extends N> E getLastChildElement(ReadableDocument<N, E, ?> doc, E element) {
return getNextElementInclusive(doc, doc.getLastChild(element), false);
}
/**
* Gets the next sibling of an element that is also an element itself.
*
* @param doc document accessor
* @param element an element
* @return the next element sibling of {@code element} if there is one,
* otherwise {@code null}.
*/
public static <N, E extends N> E getNextSiblingElement(ReadableDocument<N, E, ?> doc, E element) {
return getNextElementInclusive(doc, doc.getNextSibling(element), true);
}
/**
* @param doc document accessor.
* @param element a document element.
* @return The previous element sibling of {@code element} if there is one,
* otherwise {@code null}.
*/
public static <N, E extends N> E getPreviousSiblingElement(
ReadableDocument<N, E, ?> doc, E element) {
Preconditions.checkNotNull(element, "Previous element for null element is undefined");
Preconditions.checkNotNull(doc, "Previous element for null document is undefined");
return getNextElementInclusive(doc, doc.getPreviousSibling(element), false);
}
/**
* Returns a node as an element if it is one; otherwise, finds the next
* sibling of that node that is an element.
*
* @param doc document accessor
* @param node reference node
* @return the next element in the inclusive sibling chain from {@code node}.
*/
public static <N, E extends N> E getNextElementInclusive(ReadableDocument<N, E, ?> doc, N node,
boolean forward) {
E asElement = doc.asElement(node);
while (node != null && asElement == null) {
node = forward ? doc.getNextSibling(node) : doc.getPreviousSibling(node);
asElement = doc.asElement(node);
}
return asElement;
}
/**
* Apply action to a node and its descendants.
*
* @param doc view for traversing
* @param node reference node
* @param nodeAction action to apply to node and its descendants
*/
public static <N, E extends N, T extends N> void traverse(ReadableDocument<N, E, T> doc, N node,
NodeAction<N> nodeAction) {
for (; node != null; node = doc.getNextSibling(node)) {
nodeAction.apply(node);
traverse(doc, doc.getFirstChild(node), nodeAction);
}
}
/**
* Ensures the given point is at a node boundary, possibly splitting a text
* node in order to do so, in which case a new point is returned.
*
* @param point
* @return a point at the same place as the input point, guaranteed to be at
* a node boundary.
*/
public static <N, T extends N> Point.El<N> ensureNodeBoundary(Point<N> point,
ReadableDocument<N, ?, T> doc, TextNodeOrganiser<T> textNodeOrganiser) {
Point.Tx<N> textPoint = point.asTextPoint();
if (textPoint != null) {
T textNode = doc.asText(textPoint.getContainer());
N maybeSecond = textNodeOrganiser.splitText(textNode,
textPoint.getTextOffset());
if (maybeSecond != null) {
return Point.inElement(doc.getParentElement(maybeSecond), maybeSecond);
} else {
return Point.inElement(doc.getParentElement(textNode), doc.getNextSibling(textNode));
}
} else {
return point.asElementPoint();
}
}
/**
* Ensures the given point precedes a node, possibly splitting a text
* node in order to do so, and possibly traversing until a node is found.
*
* @param point
* @return a node at the same place as the input point, guaranteed to be at
* a node boundary. If there is no node, the next available node.
*/
public static <N, T extends N> N ensureNodeBoundaryReturnNextNode(Point<N> point,
ReadableDocument<N, ?, T> doc, TextNodeOrganiser<T> textNodeOrganiser) {
Point.Tx<N> textStartPoint = point.asTextPoint();
if (textStartPoint != null) {
T textNode = doc.asText(textStartPoint.getContainer());
N maybeSecond = textNodeOrganiser.splitText(textNode,
textStartPoint.getTextOffset());
if (maybeSecond != null) {
return maybeSecond;
} else {
return getNextNodeDepthFirst(doc, textNode, null, false);
}
} else if (point.getNodeAfter() != null) {
return point.getNodeAfter();
} else {
return getNextNodeDepthFirst(doc, point.getContainer(), null, false);
}
}
/**
* Generalisation of {@link WritableLocalDocument#transparentSlice(Object)},
* allowing a slice at a point, returning a point.
*
* Avoids slicing where possible, including where the splitAt point would map
* to a location in the persistent view corresponding to a point that is also
* valid in the full view.
*/
public static <N, E extends N, T extends N> Point<N> transparentSlice(Point<N> splitAt,
DocumentContext<N, E, T> cxt) {
// Convert to a point in the persistent view
// TODO(danilatos) More efficiently? This is simple but brutish.
int location = getFilteredLocation(cxt.locationMapper(), cxt.persistentView(), splitAt);
Point<N> pPoint = cxt.locationMapper().locate(location);
if (pPoint.isInTextNode()) {
T text = cxt.document().asText(pPoint.getContainer());
E pParent = cxt.document().getParentElement(text);
if (pParent == cxt.annotatableContent().getParentElement(text)) {
return pPoint;
} else {
pPoint = ensureNodeBoundary(pPoint, cxt.document(), cxt.textNodeOrganiser());
}
}
if (pPoint.getNodeAfter() != null) {
N nodeAfter = pPoint.getNodeAfter();
if (cxt.annotatableContent().getParentElement(nodeAfter) != pPoint.getContainer()) {
return Point.inElement(pPoint.getContainer(),
cxt.annotatableContent().transparentSlice(nodeAfter));
} else {
return pPoint;
}
} else {
return pPoint;
}
}
/**
* Counts how many children a particular element in a document has.
*
* @param doc The doc that the element is in.
* @param elem An element.
* @return Number of children the specified element has.
*/
public static <N, E extends N, T extends N> int countChildren(
ReadableDocument<Node, Element, Text> doc, Element elem) {
int children = 0;
Node currentChild = doc.getFirstChild(elem);
while (currentChild != null) {
children++;
currentChild = doc.getNextSibling(currentChild);
}
return children;
}
/**
* Does a linear search from the startNode for an element with the given id
*
* @param doc
* @param subtreeRoot the element to start looking from. Only startNode or it's
* child elements will be found.
* @param id id attribute's value
* @return first matching element, or null if none found
*/
public static <N, E extends N, T extends N> E findElementById(
ReadableDocument<N, E, T> doc, E subtreeRoot, String id) {
return findElementByAttr(doc, subtreeRoot, "id", id);
}
/**
* Does a linear search for an element with the given id
* @param doc
* @param id id attribute's value
* @return first matching element, or null if none found
*/
public static <N, E extends N, T extends N> E findElementById(
ReadableDocument<N, E, T> doc, String id) {
return findElementByAttr(doc, "id", id);
}
/**
* Iterates through startNode and its child elements and returns the first
* with the matching name value pair amongst its attributes.
*/
public static <N, E extends N, T extends N> E findElementByAttr(
ReadableDocument<N, E, T> doc, E subtreeRoot, String name, String value) {
Preconditions.checkNotNull(name, "name must not be null");
Preconditions.checkNotNull(value, "value must not be null");
for (E el : DocIterate.deepElements(doc, subtreeRoot, subtreeRoot)) {
if (value.equals(doc.getAttribute(el, name))) {
return el;
}
}
return null;
}
/**
* Iterates through elements in the document and returns the first with the
* matching name value pair amongst its attributes.
*/
public static <N, E extends N, T extends N> E findElementByAttr(
ReadableDocument<N, E, T> doc, String name, String value) {
return findElementByAttr(doc, doc.getDocumentElement(), name, value);
}
/**
* Does a linear search for an element with the given id and
* returns its location
*
* @param doc
* @param id id attribute's value
* @return first matching element's location, or -1 if none found
*/
public static <N, E extends N, T extends N> int findLocationById(
ReadableWDocument<N, E, T> doc, String id) {
return findLocationByAttr(doc, "id", id);
}
/**
* Returns the location of the first matching element
*
* @see #findElementByAttr(ReadableDocument, String, String)
*
* @return the location of the first matching element or -1 if none found
*/
public static <N, E extends N, T extends N> int findLocationByAttr(
ReadableWDocument<N, E, T> doc, String name, String value) {
E el = findElementByAttr(doc, name, value);
return el != null ? doc.getLocation(el) : -1;
}
/**
* A predicate that matches the document's root element
*/
public static final DocPredicate ROOT_PREDICATE = new DocPredicate() {
@Override
public <N, E extends N, T extends N> boolean apply(ReadableDocument<N, E, T> doc, N node) {
return node == doc.getDocumentElement();
}
};
/**
* @return true if the node is an element with the given tag name
*/
public static <N, E extends N> boolean isMatchingElement(
final ReadableDocument<N, E, ?> doc, N node, String tagName) {
E el = doc.asElement(node);
return el != null && doc.getTagName(el).equals(tagName);
}
/**
* Maneuvers the given point upwards such that its containing element matches
* the given predicate. Where this requires an element point, the nodeAfter
* will be forced rightwards as necessary. If the location is in a text node
* whose parent matches the predicate, the location already satisfies.
*
* Will return the same point by identity where possible.
*
* @return the point within an element matching the predicate, or null if
* there were none.
*/
@SuppressWarnings("unchecked") // safe
public static <N, E extends N, T extends N> Point<N> jumpOut(
ReadableDocument<N, E, T> doc, Point<N> location, DocPredicate predicate) {
E el;
N nodeAfter;
if (location.isInTextNode()) {
el = doc.getParentElement(location.getContainer());
nodeAfter = doc.getNextSibling(location.getContainer());
if (predicate.apply(doc, el)) {
return location;
}
} else {
assert doc.asElement(location.getContainer()) != null;
el = (E) location.getContainer();
nodeAfter = location.getNodeAfter();
}
while (el != null && !predicate.apply(doc, el)) {
nodeAfter = doc.getNextSibling(el);
el = doc.getParentElement(el);
}
if (el == null) {
return null;
}
// nodeAfter is of type N, el is of type (E extends N), so inElement(el, nodeAfter)
// should return a Point<N>. But Sun's java compiler doesn't figure that out,
// so we need to hint: Point.<N>inElement(...)
return el == location.getContainer() ? location : Point.<N>inElement(el, nodeAfter);
}
/**
* Gets the first top-level element in a document.
*
* This is a transition method. It has a different contact for old ops vs new
* ops. After moving to new ops, this method should be deleted and calls to it
* replaced with the direct version.
*
* In old ops, this returns:
* <code>
* doc.getDocumentElement();
* </code>
* and so is never null.
*
* In new ops, this returns:
* <code>
* DocHelper.getFirstChildElement(doc, doc.getDocumentElement());
* </code>
* and may be null.
*
* @param doc document
* @return first top-level element in a document. May be null.
*/
private static <N, E extends N> E getOrCreateFirstTopLevelElement(MutableDocument<N, E, ?> doc,
String tag, Expectation expectation) {
N firstNode = doc.locate(0).getNodeAfter();
if (expectation == Expectation.PRESENT && firstNode == null) {
throw new IllegalArgumentException("Document has no top-level element");
} else if (expectation == Expectation.ABSENT && firstNode != null) {
throw new IllegalArgumentException("Document already has top-level node: " + firstNode);
}
if (firstNode == null) {
return doc.createChildElement(doc.getDocumentElement(), tag, Attributes.EMPTY_MAP);
} else {
E firstElement = doc.asElement(firstNode);
if (firstElement == null) {
throw new IllegalArgumentException("First node is not an element: " + firstNode);
}
// Check that this element matches what is expected.
String actualTag = doc.getTagName(firstElement);
if (!tag.equals(actualTag)) {
throw new RuntimeException("Document already has non-matching top-level element: "
+ firstElement);
} else {
return firstElement;
}
}
}
/**
* Gets the first top-level element, creating it if it does not exist. If
* there is an existing top-level element, but it does not match the expected
* tag, this method fails.
*
* In order to avoid race conditions from multiple clients creating multiple
* top-level elements, please consider using
* {@link #expectAndGetFirstTopLevelElement(MutableDocument, String)} or
* {@link #createFirstTopLevelElement(MutableDocument, String)} instead.
*
* @param doc document
* @param tag tag name for the top-level element
* @return first top-level element, created if necessary. Never null.
*/
public static <E> E getOrCreateFirstTopLevelElement(MutableDocument<? super E, E, ?> doc,
String tag) {
return getOrCreateFirstTopLevelElement(doc, tag, Expectation.NONE);
}
/**
* Gets the first top-level element if it is present.
*
* @param doc document
* @param tag tag name for the top-level element
* @throws RuntimeException if there is no such element, or it does not match
* the specific tag.
* @return the first top-level element. Never null.
*/
public static <E> E expectAndGetFirstTopLevelElement(MutableDocument<? super E, E, ?> doc,
String tag) {
return getOrCreateFirstTopLevelElement(doc, tag, Expectation.PRESENT);
}
/**
* Creates the first top-level element. If a top-level element already exists,
* this method fails.
*
* @param doc document
* @param tag tag name for the top-level element
* @throws RuntimeException if a top-level element already exists.
* @return the newly created top-level element. Never null.
*/
public static <E> E createFirstTopLevelElement(MutableDocument<? super E, E, ?> doc, String tag) {
return getOrCreateFirstTopLevelElement(doc, tag, Expectation.ABSENT);
}
/**
* Find the nearest common ancestor of two nodes
*
* @return The nearest common ancestor of node1 and node2
*/
public static <N, E extends N, T extends N> N nearestCommonAncestor(
ReadableDocument<N, E, T> doc, N node1, N node2) {
IdentityMap<N, N> ancestors = CollectionUtils.createIdentityMap();
if (node1 == node2) {
return node1;
}
N commonAncestor = null;
while (node1 != null || node2 != null) {
if (node1 != null) {
if (ancestors.has(node1)) {
commonAncestor = node1;
break;
}
ancestors.put(node1, node1);
node1 = doc.getParentElement(node1);
}
if (node2 != null) {
if (ancestors.has(node2)) {
commonAncestor = node2;
break;
}
ancestors.put(node2, node2);
node2 = doc.getParentElement(node2);
}
}
if (commonAncestor == null) {
throw new IllegalArgumentException("nearestCommonAncestor: " +
"Given nodes are not in the same document");
}
return commonAncestor;
}
/**
* Checks whether a given node is an ancestory of another (either inclusive or exclusive).
* @param doc Document for tree traversal
* @param ancestor A (non-null) node to check to check if the next param is a descendant of
* @param child The node whose ancestory is being checked
* @param canEqual The result if the two nodes are equal
*/
public static <N, E extends N, T extends N>
boolean isAncestor(ReadableDocument<N, E, T> doc, N ancestor, N child, boolean canEqual) {
Preconditions.checkNotNull(ancestor, "Shouldn't check ancestry of a null node");
// keep going up the tree until we break out the parent (complexity = depth of child)
while (child != null) {
if (ancestor == child) {
return canEqual;
}
canEqual = true; // now equality represents absolute descendancy
child = doc.getParentElement(child);
}
return false; // no match
}
}