/** * Copyright 2008 Google Inc. * * 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.waveprotocol.wave.model.document.util; import org.waveprotocol.wave.model.document.ReadableDocument; import org.waveprotocol.wave.model.util.Preconditions; /** * Represents a point in a DOM. A point is the empty space between nodes or characters. * Immutable. * * A point can be described either by * 1. A parent node + child offset (node offset for element parent, char offset for text parent) * 2. A parent node + node after (the node after the point) * * "Final" - private constructors only. * * TODO(danilatos): Consider boxing up the doc in the point? * Disadvantages: can't have different views for the same point object, extra field required * Advantages: Simplify method signatures, safer equals method based on isSameNode * * @author danilatos@google.com (Daniel Danilatos) * @param <N> The Node type used in the DOM. */ public abstract class Point<N> { /** * Useful argument checking method for validating a point with respect * to a document * * @param doc * @param point * @param msgPrefix string to prepend to exception, if thrown */ public static <N, E extends N, T extends N> void checkPoint( ReadableDocument<N, E, T> doc, Point<N> point, String msgPrefix) { if (point.isInTextNode()) { checkOffset(doc, doc.asText(point.getContainer()), point.getTextOffset(), msgPrefix); } else { checkRelationship(doc, doc.asElement(point.getContainer()), point.getNodeAfter(), msgPrefix); } } /** * Useful argument checking method for validating that an offset lies * within a text node * * @param doc * @param textNode * @param offset * @param msgPrefix string to prepend to exception, if thrown */ public static <N, T extends N> void checkOffset( ReadableDocument<N, ?, T> doc, T textNode, int offset, String msgPrefix) { Preconditions.checkNotNull(textNode, "Container must not be null"); if (offset < 0 || offset > doc.getLength(textNode)) { throw new IllegalArgumentException( msgPrefix + ": offset '" + offset + "' is not inside text node, " + "length " + doc.getLength(textNode) + ", text: '" + doc.getData(textNode) + "'"); } } /** * Useful argument checking method for validating that a nodeAfter is * null or the child of the given parent * * @param doc * @param parent * @param nodeAfter * @param msgPrefix string to prepend to exception, if thrown */ public static <N, E extends N> void checkRelationship( ReadableDocument<N, E, ?> doc, E parent, N nodeAfter, String msgPrefix) { Preconditions.checkNotNull(parent, "Container must not be null"); if (nodeAfter != null && doc.getParentElement(nodeAfter) != parent) { throw new IllegalArgumentException( msgPrefix + ": nodeAfter must be null or a child of parent"); } } /** * An Element-Node style point * Use this for type safety where possible */ public final static class El<X> extends Point<X> { El(El<X> other) { super(other); } El(X container, X nodeAfter) { super(container, nodeAfter); } /** {@inheritDoc} */ @Override public Point.El<X> asElementPoint() { return this; } /** {@inheritDoc} */ @Override public Point.Tx<X> asTextPoint() { return null; } } /** * A Text-Offset style point * Use this for type safety where possible */ public final static class Tx<X> extends Point<X> { Tx(Tx<X> other) { super(other); } Tx(X container, int offset) { super(container, offset); } /** {@inheritDoc} */ @Override public Point.El<X> asElementPoint() { return null; } /** {@inheritDoc} */ @Override public Point.Tx<X> asTextPoint() { return this; } } private final N container; private final N nodeAfter; private final int offset; /** * Same as {@link #enclosingElement(ReadableDocument, Point)}, except * does it for a node. So if it is an element, return it, if it is * a text node, return its parent. */ public static <N, E extends N, T extends N> E enclosingElement( ReadableDocument<N,E,T> doc, N node) { E el = doc.asElement(node); return el == null ? doc.getParentElement(node) : el; } /** * Get the nearest enclosing element (not text node). For an e-n point, that * is simply for the container. For a t-o point, that is the container's parent. * @param doc * @param point * @return The nearest enclosing Element */ public static <N, E extends N, T extends N> E enclosingElement( ReadableDocument<N,E,T> doc, Point<N> point) { return enclosingElement(doc, point.getContainer()); } /** * @param doc * @param point * @return Node before the given point */ public static <N, E extends N, T extends N> E elementBefore( ReadableDocument<N,E,T> doc, Point<N> point) { if (point.isInTextNode()) { if (point.getTextOffset() > 0) { return null; } else { return doc.asElement(doc.getPreviousSibling(point.getContainer())); } } else { return doc.asElement(nodeBefore(doc, point.asElementPoint())); } } /** * @param doc * @param point * @return Node before the given point */ @SuppressWarnings("unchecked") public static <N, E extends N, T extends N> E elementAfter( ReadableDocument<N,E,T> doc, Point<N> point) { if (point.isInTextNode()) { if (point.getTextOffset() < doc.getLength((T) point.getContainer())) { return null; } else { return doc.asElement(doc.getNextSibling(point.getContainer())); } } else { return doc.asElement(point.getNodeAfter()); } } /** * If the given point is equivalent to the end of the inside of an element, * return that element, otherwise null. */ @SuppressWarnings("unchecked") public static <N, E extends N, T extends N> E elementEndingAt( ReadableDocument<N,E,T> doc, Point<N> point) { if (point.isInTextNode()) { if (point.getTextOffset() < doc.getLength((T) point.getContainer())) { return null; } else if (doc.getNextSibling(point.getContainer()) == null) { return doc.getParentElement(point.getContainer()); } else { return null; } } else { return point.getNodeAfter() == null ? (E) point.getContainer() : null; } } /** * @param doc * @param point * @return Node before the given point */ public static <N, E extends N, T extends N> N nodeBefore( ReadableDocument<N,E,T> doc, Point.El<N> point) { N node = point.getNodeAfter(); return node == null ? doc.getLastChild(point.getContainer()) : doc.getPreviousSibling(node); } /** * Construct a point before the given node */ public static <N, E extends N, T extends N> El<N> before( ReadableDocument<N,E,T> doc, N node) { return new El<N>(doc.getParentElement(node), node); } /** * Construct a point after the given node */ public static <N, E extends N, T extends N> El<N> after( ReadableDocument<N,E,T> doc, N node) { return new El<N>(doc.getParentElement(node), doc.getNextSibling(node)); } /** * Construct a point inside the start of the given element (just after the * start tag). */ public static <N, E extends N> El<N> start( ReadableDocument<N,E,?> doc, E element) { return new El<N>(element, doc.getFirstChild(element)); } /** * Construct a point at the start of either an element or text node. * @param doc Document containing the node * @param node The node to get the start of * @return The Point.El start if node is an element, otherwise the position at text offset 0. */ public static <N, E extends N, T extends N> Point<N> textOrElementStart( ReadableDocument<N,E,T> doc, N node) { E elt = doc.asElement(node); return elt == null ? inText(node, 0) : start(doc, elt); // change based on type of node } /** * Construct a point at the end of either an element or text node. * @param doc Document containing the node * @param node The node to get the start of * @return The Point.El end if node is an element, otherwise the position at the last text offset. */ public static <N, E extends N, T extends N> Point<N> textOrElementEnd( ReadableDocument<N,E,T> doc, N node) { E elt = doc.asElement(node); return elt == null ? inText(node, doc.getData(doc.asText(node)).length()) : end(node); } /** * Construct a point inside the end of the given element (just before the * end tag). */ public static <N> El<N> end(N element) { return new El<N>(element, null); } /** * Construct an e-n point in element, just before nodeAfter (if nodeAfter is * null, then inside the end of element). * * @param element * @param nodeAfter */ public static <N> El<N> inElement(N element, N nodeAfter) { return new El<N>(element, nodeAfter); } /** * Similar to {@link #inElement(Object, Object)} * * @param element Parent * @param nodeBefore The node BEFORE the point, or null for the START of parent * @return point */ public static <N, E extends N, T extends N> El<N> inElementReverse( ReadableDocument<N,E,T> doc, E element, N nodeBefore) { return new El<N>(element, nodeBefore == null ? doc.getFirstChild(element) : doc.getNextSibling(nodeBefore)); } /** * Construct a t-o point inside textNode * @param textNode * @param charOffset */ public static <N> Tx<N> inText(N textNode, int charOffset) { return new Tx<N>(textNode, charOffset); } /** * Construct a copy of a point */ public static <N> Point<N> dup(Point<N> other) { return other instanceof El ? new El<N>((El<N>) other) : new Tx<N>((Tx<N>) other); } Point(N container, N nodeAfter) { Preconditions.checkNotNull(container, "Container must not be null"); this.container = container; this.nodeAfter = nodeAfter; this.offset = -1; } Point(N container, int offset) { Preconditions.checkNotNull(container, "Container must not be null"); this.container = container; this.nodeAfter = null; this.offset = offset; } private Point(Point<N> other) { this.container = other.container; this.nodeAfter = other.nodeAfter; this.offset = other.offset; assert container != null; } /** * @return Containing element */ public N getContainer() { return container; } /** * @return The node after. Only valid for e-n points. */ // TODO(danilatos): Make this method only on Point.El public N getNodeAfter() { if (isInTextNode()) { throw new IllegalStateException( "getNodeAfter() can only be called on points within elements"); } return nodeAfter; } /** * @return The text offset. Only valid for t-o points. */ //TODO(danilatos): Make this method only on Point.Tx public int getTextOffset() { if (!isInTextNode()) { throw new IllegalStateException( "getOffset() can only be called on points within text nodes"); } return offset; } /** * Maybe cast to t-o point. * @return As a text point if it is one, or null otherwise */ public abstract Point.Tx<N> asTextPoint(); /** * Maybe cast to e-n point * @return As an element point if it is one, or null otherwise */ public abstract Point.El<N> asElementPoint(); /** * @return true if is a t-o point */ public boolean isInTextNode() { return offset >= 0; } /** * @return true if the point is the inside of the end of an element * (conceptually before the closing tag). Note, of course, that a * point at the end of a text node before a closing tag will not * return true for this. */ public boolean isInElementEnd() { return !isInTextNode() && getNodeAfter() == null; } /** * @return true if the point immediately precedes a node. (conceptually before * the closing tag). Note, of course, that a point at the end of a * text node before a node will not return true for this. */ public boolean isBeforeNode() { return !isInTextNode() && getNodeAfter() != null; } /** * The "canonical" node is * - for text points, the text node * - nodeAfter, when it is not null * - the container, if nodeAfter is null * * It would be desirable to change Point to become a 3 way disjunctive type * as opposed to the current two options (corresponding to the 3 options * above). This is because if nodeAfter is not null, the point class implies * that container is the parent of nodeAfter, which might be true only in * some filtered views of a document. * * @return the canonical node of this point */ public N getCanonicalNode() { return isInTextNode() || nodeAfter == null ? container : nodeAfter; } /** {@inheritDoc} */ @Override public String toString() { return "{" + container + "~" + (isInTextNode() ? getTextOffset() + "" : getNodeAfter() + "") + "}"; } /** * eclipse generated hashCode & equals * Caveat: Assumption is that .equals() is valid for nodes */ @Override public final int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((container == null) ? 0 : container.hashCode()); result = prime * result + ((nodeAfter == null) ? 0 : nodeAfter.hashCode()); result = prime * result + offset; return result; } /** * eclipse generated hashCode & equals * Caveat: Assumption is that .equals() is valid for nodes */ @SuppressWarnings("unchecked") @Override public final boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final Point<N> other = (Point<N>) obj; if (container == null) { if (other.container != null) { return false; } } else if (!container.equals(other.container)) { return false; } if (nodeAfter == null) { if (other.nodeAfter != null) { return false; } } else if (!nodeAfter.equals(other.nodeAfter)) { return false; } if (offset != other.offset) { return false; } return true; } }