/* * 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.commons.jxpath.ri.model; import java.util.HashSet; import java.util.Locale; import org.apache.commons.jxpath.AbstractFactory; import org.apache.commons.jxpath.JXPathContext; import org.apache.commons.jxpath.JXPathException; import org.apache.commons.jxpath.NodeSet; import org.apache.commons.jxpath.Pointer; import org.apache.commons.jxpath.ri.Compiler; import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl; import org.apache.commons.jxpath.ri.NamespaceResolver; import org.apache.commons.jxpath.ri.QName; import org.apache.commons.jxpath.ri.compiler.NodeNameTest; import org.apache.commons.jxpath.ri.compiler.NodeTest; import org.apache.commons.jxpath.ri.compiler.NodeTypeTest; import org.apache.commons.jxpath.ri.model.beans.NullPointer; /** * Common superclass for Pointers of all kinds. A NodePointer maps to * a deterministic XPath that represents the location of a node in an * object graph. This XPath uses only simple axes: child, namespace and * attribute and only simple, context-independent predicates. * * @author Dmitri Plotnikov * @version $Revision: 668329 $ $Date: 2008-06-16 16:59:48 -0500 (Mon, 16 Jun 2008) $ */ public abstract class NodePointer implements Pointer { /** Whole collection index. */ public static final int WHOLE_COLLECTION = Integer.MIN_VALUE; /** Constant to indicate unknown namespace */ public static final String UNKNOWN_NAMESPACE = "<<unknown namespace>>"; /** Index for this NodePointer */ protected int index = WHOLE_COLLECTION; private boolean attribute = false; private NamespaceResolver namespaceResolver; private transient Object rootNode; /** * Allocates an entirely new NodePointer by iterating through all installed * NodePointerFactories until it finds one that can create a pointer. * @param name QName * @param bean Object * @param locale Locale * @return NodePointer */ public static NodePointer newNodePointer( QName name, Object bean, Locale locale) { NodePointer pointer = null; if (bean == null) { pointer = new NullPointer(name, locale); return pointer; } NodePointerFactory[] factories = JXPathContextReferenceImpl.getNodePointerFactories(); for (int i = 0; i < factories.length; i++) { pointer = factories[i].createNodePointer(name, bean, locale); if (pointer != null) { return pointer; } } throw new JXPathException( "Could not allocate a NodePointer for object of " + bean.getClass()); } /** * Allocates an new child NodePointer by iterating through all installed * NodePointerFactories until it finds one that can create a pointer. * @param parent pointer * @param name QName * @param bean Object * @return NodePointer */ public static NodePointer newChildNodePointer( NodePointer parent, QName name, Object bean) { NodePointerFactory[] factories = JXPathContextReferenceImpl.getNodePointerFactories(); for (int i = 0; i < factories.length; i++) { NodePointer pointer = factories[i].createNodePointer(parent, name, bean); if (pointer != null) { return pointer; } } throw new JXPathException( "Could not allocate a NodePointer for object of " + bean.getClass()); } /** Parent pointer */ protected NodePointer parent; /** Locale */ protected Locale locale; /** * Create a new NodePointer. * @param parent Pointer */ protected NodePointer(NodePointer parent) { this.parent = parent; } /** * Create a new NodePointer. * @param parent Pointer * @param locale Locale */ protected NodePointer(NodePointer parent, Locale locale) { this.parent = parent; this.locale = locale; } /** * Get the NamespaceResolver associated with this NodePointer. * @return NamespaceResolver */ public NamespaceResolver getNamespaceResolver() { if (namespaceResolver == null && parent != null) { namespaceResolver = parent.getNamespaceResolver(); } return namespaceResolver; } /** * Set the NamespaceResolver for this NodePointer. * @param namespaceResolver NamespaceResolver */ public void setNamespaceResolver(NamespaceResolver namespaceResolver) { this.namespaceResolver = namespaceResolver; } /** * Get the parent pointer. * @return NodePointer */ public NodePointer getParent() { NodePointer pointer = parent; while (pointer != null && pointer.isContainer()) { pointer = pointer.getImmediateParentPointer(); } return pointer; } /** * Get the immediate parent pointer. * @return NodePointer */ public NodePointer getImmediateParentPointer() { return parent; } /** * Set to true if the pointer represents the "attribute::" axis. * @param attribute boolean */ public void setAttribute(boolean attribute) { this.attribute = attribute; } /** * Returns true if the pointer represents the "attribute::" axis. * @return boolean */ public boolean isAttribute() { return attribute; } /** * Returns true if this Pointer has no parent. * @return boolean */ public boolean isRoot() { return parent == null; } /** * If true, this node does not have children * @return boolean */ public abstract boolean isLeaf(); /** * Learn whether this pointer is considered to be a node. * @return boolean * @deprecated Please use !isContainer() */ public boolean isNode() { return !isContainer(); } /** * If true, this node is auxiliary and can only be used as an intermediate in * the chain of pointers. * @return boolean */ public boolean isContainer() { return false; } /** * If the pointer represents a collection, the index identifies * an element of that collection. The default value of <code>index</code> * is <code>WHOLE_COLLECTION</code>, which just means that the pointer * is not indexed at all. * Note: the index on NodePointer starts with 0, not 1. * @return int */ public int getIndex() { return index; } /** * Set the index of this NodePointer. * @param index int */ public void setIndex(int index) { this.index = index; } /** * Returns <code>true</code> if the value of the pointer is an array or * a Collection. * @return boolean */ public abstract boolean isCollection(); /** * If the pointer represents a collection (or collection element), * returns the length of the collection. * Otherwise returns 1 (even if the value is null). * @return int */ public abstract int getLength(); /** * By default, returns <code>getNode()</code>, can be overridden to * return a "canonical" value, like for instance a DOM element should * return its string value. * @return Object value */ public Object getValue() { NodePointer valuePointer = getValuePointer(); if (valuePointer != this) { return valuePointer.getValue(); } // Default behavior is to return the same as getNode() return getNode(); } /** * If this pointer manages a transparent container, like a variable, * this method returns the pointer to the contents. * Only an auxiliary (non-node) pointer can (and should) return a * value pointer other than itself. * Note that you probably don't want to override * <code>getValuePointer()</code> directly. Override the * <code>getImmediateValuePointer()</code> method instead. The * <code>getValuePointer()</code> method is calls * <code>getImmediateValuePointer()</code> and, if the result is not * <code>this</code>, invokes <code>getValuePointer()</code> recursively. * The idea here is to open all nested containers. Let's say we have a * container within a container within a container. The * <code>getValuePointer()</code> method should then open all those * containers and return the pointer to the ultimate contents. It does so * with the above recursion. * @return NodePointer */ public NodePointer getValuePointer() { NodePointer ivp = getImmediateValuePointer(); return ivp == this ? this : ivp.getValuePointer(); } /** * @see #getValuePointer() * * @return NodePointer is either <code>this</code> or a pointer * for the immediately contained value. */ public NodePointer getImmediateValuePointer() { return this; } /** * An actual pointer points to an existing part of an object graph, even * if it is null. A non-actual pointer represents a part that does not exist * at all. * For instance consider the pointer "/address/street". * If both <em>address</em> and <em>street</em> are not null, * the pointer is actual. * If <em>address</em> is not null, but <em>street</em> is null, * the pointer is still actual. * If <em>address</em> is null, the pointer is not actual. * (In JavaBeans) if <em>address</em> is not a property of the root bean, * a Pointer for this path cannot be obtained at all - actual or otherwise. * @return boolean */ public boolean isActual() { return index == WHOLE_COLLECTION || index >= 0 && index < getLength(); } /** * Returns the name of this node. Can be null. * @return QName */ public abstract QName getName(); /** * Returns the value represented by the pointer before indexing. * So, if the node represents an element of a collection, this * method returns the collection itself. * @return Object value */ public abstract Object getBaseValue(); /** * Returns the object the pointer points to; does not convert it * to a "canonical" type. * @return Object node value * @deprecated 1.1 Please use getNode() */ public Object getNodeValue() { return getNode(); } /** * Returns the object the pointer points to; does not convert it * to a "canonical" type. Opens containers, properties etc and returns * the ultimate contents. * @return Object node */ public Object getNode() { return getValuePointer().getImmediateNode(); } /** * Get the root node. * @return Object value of this pointer's root (top parent). */ public synchronized Object getRootNode() { if (rootNode == null) { rootNode = parent == null ? getImmediateNode() : parent.getRootNode(); } return rootNode; } /** * Returns the object the pointer points to; does not convert it * to a "canonical" type. * @return Object node */ public abstract Object getImmediateNode(); /** * Converts the value to the required type and changes the corresponding * object to that value. * @param value the value to set */ public abstract void setValue(Object value); /** * Compares two child NodePointers and returns a positive number, * zero or a positive number according to the order of the pointers. * @param pointer1 first pointer to be compared * @param pointer2 second pointer to be compared * @return int per Java comparison conventions */ public abstract int compareChildNodePointers( NodePointer pointer1, NodePointer pointer2); /** * Checks if this Pointer matches the supplied NodeTest. * @param test the NodeTest to execute * @return true if a match */ public boolean testNode(NodeTest test) { if (test == null) { return true; } if (test instanceof NodeNameTest) { if (isContainer()) { return false; } NodeNameTest nodeNameTest = (NodeNameTest) test; QName testName = nodeNameTest.getNodeName(); QName nodeName = getName(); if (nodeName == null) { return false; } String testPrefix = testName.getPrefix(); String nodePrefix = nodeName.getPrefix(); if (!equalStrings(testPrefix, nodePrefix)) { String testNS = getNamespaceURI(testPrefix); String nodeNS = getNamespaceURI(nodePrefix); if (!equalStrings(testNS, nodeNS)) { return false; } } if (nodeNameTest.isWildcard()) { return true; } return testName.getName().equals(nodeName.getName()); } return test instanceof NodeTypeTest && ((NodeTypeTest) test).getNodeType() == Compiler.NODE_TYPE_NODE && isNode(); } /** * Compare two strings, either of which may be null, for equality. * @param s1 the first String to compare * @param s2 the second String to compare * @return true if both Strings are null, same or equal */ private static boolean equalStrings(String s1, String s2) { return s1 == s2 || s1 != null && s1.equals(s2); } /** * Called directly by JXPathContext. Must create path and * set value. * @param context the owning JXPathContext * @param value the new value to set * @return created NodePointer */ public NodePointer createPath(JXPathContext context, Object value) { setValue(value); return this; } /** * Remove the node of the object graph this pointer points to. */ public void remove() { // It is a no-op // System.err.println("REMOVING: " + asPath() + " " + getClass()); // printPointerChain(); } /** * Called by a child pointer when it needs to create a parent object. * Must create an object described by this pointer and return * a new pointer that properly describes the new object. * @param context the owning JXPathContext * @return created NodePointer */ public NodePointer createPath(JXPathContext context) { return this; } /** * Called by a child pointer if that child needs to assign the value * supplied in the createPath(context, value) call to a non-existent * node. This method may have to expand the collection in order to assign * the element. * @param context the owning JXPathCOntext * @param name the QName at which a child should be created * @param index child index. * @param value node value to set * @return created NodePointer */ public NodePointer createChild( JXPathContext context, QName name, int index, Object value) { throw new JXPathException("Cannot create an object for path " + asPath() + "/" + name + "[" + (index + 1) + "]" + ", operation is not allowed for this type of node"); } /** * Called by a child pointer when it needs to create a parent object for a * non-existent collection element. It may have to expand the collection, * then create an element object and return a new pointer describing the * newly created element. * @param context the owning JXPathCOntext * @param name the QName at which a child should be created * @param index child index. * @return created NodePointer */ public NodePointer createChild(JXPathContext context, QName name, int index) { throw new JXPathException("Cannot create an object for path " + asPath() + "/" + name + "[" + (index + 1) + "]" + ", operation is not allowed for this type of node"); } /** * Called to create a non-existing attribute * @param context the owning JXPathCOntext * @param name the QName at which an attribute should be created * @return created NodePointer */ public NodePointer createAttribute(JXPathContext context, QName name) { throw new JXPathException("Cannot create an attribute for path " + asPath() + "/@" + name + ", operation is not allowed for this type of node"); } /** * If the Pointer has a parent, returns the parent's locale; otherwise * returns the locale specified when this Pointer was created. * @return Locale for this NodePointer */ public Locale getLocale() { if (locale == null && parent != null) { locale = parent.getLocale(); } return locale; } /** * Check whether our locale matches the specified language. * @param lang String language to check * @return true if the selected locale name starts * with the specified prefix <i>lang</i>, case-insensitive. */ public boolean isLanguage(String lang) { Locale loc = getLocale(); String name = loc.toString().replace('_', '-'); return name.toUpperCase(Locale.ENGLISH).startsWith(lang.toUpperCase(Locale.ENGLISH)); } /** * Returns a NodeIterator that iterates over all children or all children * that match the given NodeTest, starting with the specified one. * @param test NodeTest to filter children * @param reverse specified iteration direction * @param startWith the NodePointer to start with * @return NodeIterator */ public NodeIterator childIterator( NodeTest test, boolean reverse, NodePointer startWith) { NodePointer valuePointer = getValuePointer(); return valuePointer == null || valuePointer == this ? null : valuePointer.childIterator(test, reverse, startWith); } /** * Returns a NodeIterator that iterates over all attributes of the current * node matching the supplied node name (could have a wildcard). * May return null if the object does not support the attributes. * @param qname the attribute name to test * @return NodeIterator */ public NodeIterator attributeIterator(QName qname) { NodePointer valuePointer = getValuePointer(); return valuePointer == null || valuePointer == this ? null : valuePointer.attributeIterator(qname); } /** * Returns a NodeIterator that iterates over all namespaces of the value * currently pointed at. * May return null if the object does not support the namespaces. * @return NodeIterator */ public NodeIterator namespaceIterator() { return null; } /** * Returns a NodePointer for the specified namespace. Will return null * if namespaces are not supported. * Will return UNKNOWN_NAMESPACE if there is no such namespace. * @param namespace incoming namespace * @return NodePointer for <code>namespace</code> */ public NodePointer namespacePointer(String namespace) { return null; } /** * Decodes a namespace prefix to the corresponding URI. * @param prefix prefix to decode * @return String uri */ public String getNamespaceURI(String prefix) { return null; } /** * Returns the namespace URI associated with this Pointer. * @return String uri */ public String getNamespaceURI() { return null; } /** * Returns true if the supplied prefix represents the * default namespace in the context of the current node. * @param prefix the prefix to check * @return <code>true</code> if prefix is default */ protected boolean isDefaultNamespace(String prefix) { if (prefix == null) { return true; } String namespace = getNamespaceURI(prefix); return namespace != null && namespace.equals(getDefaultNamespaceURI()); } /** * Get the default ns uri * @return String uri */ protected String getDefaultNamespaceURI() { return null; } /** * Locates a node by ID. * @param context JXPathContext owning context * @param id String id * @return Pointer found */ public Pointer getPointerByID(JXPathContext context, String id) { return context.getPointerByID(id); } /** * Locates a node by key and value. * @param context owning JXPathContext * @param key key to search for * @param value value to match * @return Pointer found */ public Pointer getPointerByKey( JXPathContext context, String key, String value) { return context.getPointerByKey(key, value); } /** * Find a NodeSet by key/value. * @param context owning JXPathContext * @param key key to search for * @param value value to match * @return NodeSet found */ public NodeSet getNodeSetByKey(JXPathContext context, String key, Object value) { return context.getNodeSetByKey(key, value); } /** * Returns an XPath that maps to this Pointer. * @return String xpath expression */ public String asPath() { // If the parent of this node is a container, it is responsible // for appended this node's part of the path. if (parent != null && parent.isContainer()) { return parent.asPath(); } StringBuffer buffer = new StringBuffer(); if (parent != null) { buffer.append(parent.asPath()); } if (buffer.length() == 0 || buffer.charAt(buffer.length() - 1) != '/') { buffer.append('/'); } if (attribute) { buffer.append('@'); } buffer.append(getName()); if (index != WHOLE_COLLECTION && isCollection()) { buffer.append('[').append(index + 1).append(']'); } return buffer.toString(); } /** * Clone this NodePointer. * @return cloned NodePointer */ public Object clone() { try { NodePointer ptr = (NodePointer) super.clone(); if (parent != null) { ptr.parent = (NodePointer) parent.clone(); } return ptr; } catch (CloneNotSupportedException ex) { // Of course it is supported ex.printStackTrace(); } return null; } public String toString() { return asPath(); } public int compareTo(Object object) { if (object == this) { return 0; } // Let it throw a ClassCastException NodePointer pointer = (NodePointer) object; if (parent == pointer.parent) { return parent == null ? 0 : parent.compareChildNodePointers(this, pointer); } // Task 1: find the common parent int depth1 = 0; NodePointer p1 = this; HashSet parents1 = new HashSet(); while (p1 != null) { depth1++; p1 = p1.parent; if (p1 != null) { parents1.add(p1); } } boolean commonParentFound = false; int depth2 = 0; NodePointer p2 = pointer; while (p2 != null) { depth2++; p2 = p2.parent; if (parents1.contains(p2)) { commonParentFound = true; } } //nodes from different graphs are equal, else continue comparison: return commonParentFound ? compareNodePointers(this, depth1, pointer, depth2) : 0; } /** * Compare node pointers. * @param p1 pointer 1 * @param depth1 depth 1 * @param p2 pointer 2 * @param depth2 depth 2 * @return comparison result: (< 0) -> (p1 lt p2); (0) -> (p1 eq p2); (> 0) -> (p1 gt p2) */ private int compareNodePointers( NodePointer p1, int depth1, NodePointer p2, int depth2) { if (depth1 < depth2) { int r = compareNodePointers(p1, depth1, p2.parent, depth2 - 1); return r == 0 ? -1 : r; } if (depth1 > depth2) { int r = compareNodePointers(p1.parent, depth1 - 1, p2, depth2); return r == 0 ? 1 : r; } //henceforth depth1 == depth2: if (p1 == p2 || p1 != null && p1.equals(p2)) { return 0; } if (depth1 == 1) { throw new JXPathException( "Cannot compare pointers that do not belong to the same tree: '" + p1 + "' and '" + p2 + "'"); } int r = compareNodePointers(p1.parent, depth1 - 1, p2.parent, depth2 - 1); return r == 0 ? p1.parent.compareChildNodePointers(p1, p2) : r; } /** * Print internal structure of a pointer for debugging */ public void printPointerChain() { printDeep(this, ""); } /** * Return a string escaping single and double quotes. * @param string string to treat * @return string with any necessary changes made. */ protected String escape(String string) { final char[] c = new char[] { '\'', '"' }; final String[] esc = new String[] { "'", """ }; StringBuffer sb = null; for (int i = 0; sb == null && i < c.length; i++) { if (string.indexOf(c[i]) >= 0) { sb = new StringBuffer(string); } } if (sb == null) { return string; } for (int i = 0; i < c.length; i++) { if (string.indexOf(c[i]) < 0) { continue; } int pos = 0; while (pos < sb.length()) { if (sb.charAt(pos) == c[i]) { sb.replace(pos, pos + 1, esc[i]); pos += esc[i].length(); } else { pos++; } } } return sb.toString(); } /** * Get the AbstractFactory associated with the specified JXPathContext. * @param context JXPathContext * @return AbstractFactory */ protected AbstractFactory getAbstractFactory(JXPathContext context) { AbstractFactory factory = context.getFactory(); if (factory == null) { throw new JXPathException( "Factory is not set on the JXPathContext - cannot create path: " + asPath()); } return factory; } /** * Print deep * @param pointer to print * @param indent indentation level */ private static void printDeep(NodePointer pointer, String indent) { if (indent.length() == 0) { System.err.println( "POINTER: " + pointer + "(" + pointer.getClass().getName() + ")"); } else { System.err.println( indent + " of " + pointer + "(" + pointer.getClass().getName() + ")"); } if (pointer.getImmediateParentPointer() != null) { printDeep(pointer.getImmediateParentPointer(), indent + " "); } } }