blob: 613f8dfeb51c6f6eb3e5b82705e964ef31b7cdb9 [file] [log] [blame]
/*
* 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 groovy.util;
import groovy.lang.Closure;
import groovy.lang.DelegatingMetaClass;
import groovy.lang.GroovySystem;
import groovy.lang.MetaClass;
import groovy.lang.Tuple2;
import groovy.namespace.QName;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.StringGroovyMethods;
import org.codehaus.groovy.util.ListHashMap;
import java.io.PrintWriter;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;
/**
* Represents an arbitrary tree node which can be used for structured metadata or any arbitrary XML-like tree.
* A node can have a name, a value and an optional Map of attributes.
* Typically the name is a String and a value is either a String or a List of other Nodes,
* though the types are extensible to provide a flexible structure, e.g. you could use a
* QName as the name which includes a namespace URI and a local name. Or a JMX ObjectName etc.
* So this class can represent metadata like <code>{foo a=1 b="abc"}</code> or nested
* metadata like <code>{foo a=1 b="123" { bar x=12 text="hello" }}</code>
*/
public class Node implements Serializable, Cloneable {
static {
// wrap the standard MetaClass with the delegate
setMetaClass(GroovySystem.getMetaClassRegistry().getMetaClass(Node.class), Node.class);
}
private static final long serialVersionUID = 4121134753270542643L;
private Node parent;
private final Object name;
private final Map attributes;
private Object value;
/**
* Creates a new Node with the same name, no parent, shallow cloned attributes
* and if the value is a NodeList, a (deep) clone of those nodes.
*
* @return the clone
*/
@Override
public Object clone() {
Object newValue = value;
if (value instanceof NodeList) {
NodeList nodes = (NodeList) value;
newValue = nodes.clone();
}
return new Node(null, name, new HashMap(attributes), newValue);
}
/**
* Creates a new Node named <code>name</code> and if a parent is supplied, adds
* the newly created node as a child of the parent.
*
* @param parent the parent node or null if no parent
* @param name the name of the node
*/
public Node(Node parent, Object name) {
this(parent, name, new NodeList());
}
/**
* Creates a new Node named <code>name</code> with value <code>value</code> and
* if a parent is supplied, adds the newly created node as a child of the parent.
*
* @param parent the parent node or null if no parent
* @param name the name of the node
* @param value the Node value, e.g. some text but in general any Object
*/
public Node(Node parent, Object name, Object value) {
this(parent, name, new HashMap(), value);
}
/**
* Creates a new Node named <code>name</code> with
* attributes specified in the <code>attributes</code> Map. If a parent is supplied,
* the newly created node is added as a child of the parent.
*
* @param parent the parent node or null if no parent
* @param name the name of the node
* @param attributes a Map of name-value pairs
*/
public Node(Node parent, Object name, Map attributes) {
this(parent, name, attributes, new NodeList());
}
/**
* Creates a new Node named <code>name</code> with value <code>value</code> and
* with attributes specified in the <code>attributes</code> Map. If a parent is supplied,
* the newly created node is added as a child of the parent.
*
* @param parent the parent node or null if no parent
* @param name the name of the node
* @param attributes a Map of name-value pairs
* @param value the Node value, e.g. some text but in general any Object
*/
public Node(Node parent, Object name, Map attributes, Object value) {
this.parent = parent;
this.name = name;
this.attributes = attributes;
this.value = value;
if (parent != null) {
getParentList(parent).add(this);
}
}
private static List getParentList(Node parent) {
Object parentValue = parent.value();
List parentList;
if (parentValue instanceof List) {
parentList = (List) parentValue;
} else {
parentList = new NodeList();
parentList.add(parentValue);
parent.setValue(parentList);
}
return parentList;
}
/**
* Appends a child to the current node.
*
* @param child the child to append
* @return <code>true</code>
*/
public boolean append(Node child) {
child.setParent(this);
return getParentList(this).add(child);
}
/**
* Removes a child of the current node.
*
* @param child the child to remove
* @return <code>true</code> if the param was a child of the current node
*/
public boolean remove(Node child) {
child.setParent(null);
return getParentList(this).remove(child);
}
/**
* Creates a new node as a child of the current node.
*
* @param name the name of the new node
* @param attributes the attributes of the new node
* @return the newly created <code>Node</code>
*/
public Node appendNode(Object name, Map attributes) {
return new Node(this, name, attributes);
}
/**
* Creates a new node as a child of the current node.
*
* @param name the name of the new node
* @return the newly created <code>Node</code>
*/
public Node appendNode(Object name) {
return new Node(this, name);
}
/**
* Creates a new node as a child of the current node.
*
* @param name the name of the new node
* @param value the value of the new node
* @return the newly created <code>Node</code>
*/
public Node appendNode(Object name, Object value) {
return new Node(this, name, value);
}
/**
* Creates a new node as a child of the current node.
*
* @param name the name of the new node
* @param attributes the attributes of the new node
* @param value the value of the new node
* @return the newly created <code>Node</code>
*/
public Node appendNode(Object name, Map attributes, Object value) {
return new Node(this, name, attributes, value);
}
/**
* Replaces the current node with nodes defined using builder-style notation via a Closure.
*
* @param c A Closure defining the new nodes using builder-style notation.
* @return the original now replaced node
*/
public Node replaceNode(Closure c) {
if (parent() == null) {
throw new UnsupportedOperationException("Replacing the root node is not supported");
}
appendNodes(c);
getParentList(parent()).remove(this);
this.setParent(null);
return this;
}
/**
* Replaces the current node with the supplied node.
*
* @param n the new Node
* @return the original now replaced node
*/
public Node replaceNode(Node n) {
if (parent() == null) {
throw new UnsupportedOperationException("Replacing the root node is not supported");
}
List tail = getTail();
parent().appendNode(n.name(), n.attributes(), n.value());
parent().children().addAll(tail);
getParentList(parent()).remove(this);
this.setParent(null);
return this;
}
private List getTail() {
List list = parent().children();
int afterIndex = list.indexOf(this);
List tail = new ArrayList(list.subList(afterIndex + 1, list.size()));
list.subList(afterIndex + 1, list.size()).clear();
return tail;
}
/**
* Adds sibling nodes (defined using builder-style notation via a Closure) after the current node.
*
* @param c A Closure defining the new sibling nodes to add using builder-style notation.
*/
public void plus(Closure c) {
if (parent() == null) {
throw new UnsupportedOperationException("Adding sibling nodes to the root node is not supported");
}
appendNodes(c);
}
private void appendNodes(Closure c) {
List tail = getTail();
for (Node child : buildChildrenFromClosure(c)) {
parent().appendNode(child.name(), child.attributes(), child.value());
}
parent().children().addAll(tail);
}
private static List<Node> buildChildrenFromClosure(Closure c) {
NodeBuilder b = new NodeBuilder();
Node newNode = (Node) b.invokeMethod("dummyNode", c);
return newNode.children();
}
/**
* Extension point for subclasses to override the metaclass. The default
* one supports the property and @ attribute notations.
*
* @param metaClass the original metaclass
* @param nodeClass the class whose metaclass we wish to override (this class or a subclass)
*/
protected static void setMetaClass(final MetaClass metaClass, final Class nodeClass) {
// TODO Is protected static a bit of a smell?
// TODO perhaps set nodeClass to be Class<? extends Node>
GroovySystem.getMetaClassRegistry().setMetaClass(nodeClass, new DelegatingMetaClass(metaClass) {
@Override
public Object getAttribute(final Object object, final String attribute) {
return ((Node) object).get("@" + attribute);
}
@Override
public Object getAttribute(final Class sender, final Object object, final String attribute, final boolean isSuper) {
return getAttribute(object, attribute);
}
@Override
public void setAttribute(final Object object, final String attribute, final Object newValue) {
((Node) object).attributes().put(attribute, newValue);
}
@Override
public void setAttribute(final Class sender, final Object object, final String attribute, final Object newValue, final boolean isSuper, final boolean isInner) {
setAttribute(object, attribute, newValue);
}
@Override
public Object getProperty(final Object object, final String property) {
if (object instanceof Node) {
return ((Node) object).get(property);
}
return super.getProperty(object, property);
}
@Override
public Object getProperty(final Class sender, final Object object, final String property, final boolean isSuper, final boolean isInner) {
if (object instanceof Node) {
return ((Node) object).get(property);
}
return super.getProperty(sender, object, property, isSuper, isInner);
}
@Override
public void setProperty(final Object object, final String property, final Object newValue) {
if (property.startsWith("@")) {
setAttribute(object, property.substring(1), newValue);
} else {
super.setProperty(object, property, newValue);
}
}
@Override
public void setProperty(final Class sender, final Object object, final String property, final Object newValue, final boolean isSuper, final boolean isInner) {
if (property.startsWith("@")) {
setAttribute(object, property.substring(1), newValue);
} else {
super.setProperty(sender, object, property, newValue, isSuper, isInner);
}
}
});
}
/**
* Returns the textual representation of the current node and all its child nodes.
*
* @return the text value of the node including child text
*/
public String text() {
if (value instanceof String) {
return (String) value;
}
if (value instanceof NodeList) {
return ((NodeList) value).text();
}
if (value instanceof Collection) {
Collection coll = (Collection) value;
String previousText = null;
StringBuilder sb = null;
for (Object child : coll) {
String childText = null;
if (child instanceof String) {
childText = (String) child;
} else if (child instanceof Node) {
childText = ((Node) child).text();
}
if (childText != null) {
if (previousText == null) {
previousText = childText;
} else {
if (sb == null) {
sb = new StringBuilder();
sb.append(previousText);
}
sb.append(childText);
}
}
}
if (sb != null) {
return sb.toString();
} else {
if (previousText != null) {
return previousText;
}
return "";
}
}
return "" + value;
}
/**
* Returns an <code>Iterator</code> of the children of the node.
*
* @return the iterator of the nodes children
*/
public Iterator iterator() {
return children().iterator();
}
/**
* Returns a <code>List</code> of the nodes children.
*
* @return the nodes children
*/
public List children() {
if (value == null) {
return new NodeList();
}
if (value instanceof List) {
return (List) value;
}
// we're probably just a String
List result = new NodeList();
result.add(value);
return result;
}
/**
* Returns a <code>Map</code> of the attributes of the node or an empty <code>Map</code>
* if the node does not have any attributes.
*
* @return the attributes of the node
*/
public Map attributes() {
return attributes;
}
/**
* Provides lookup of attributes by key.
*
* @param key the key of interest
* @return the attribute matching the key or <code>null</code> if no match exists
*/
public Object attribute(Object key) {
return (attributes != null) ? attributes.get(key) : null;
}
/**
* Returns an <code>Object</code> representing the name of the node.
*
* @return the name or <code>null</code> if name is empty
*/
public Object name() {
return name;
}
/**
* Returns an <code>Object</code> representing the value of the node.
*
* @return the value or <code>null</code> if value is empty
*/
public Object value() {
return value;
}
/**
* Adds or replaces the value of the node.
*
* @param value the new value of the node
*/
public void setValue(Object value) {
this.value = value;
}
/**
* Returns the parent of the node.
*
* @return the parent or <code>null</code> for the root node
*/
public Node parent() {
return parent;
}
/**
* Adds or replaces the parent of the node.
*
* @param parent the new parent of the node
*/
protected void setParent(Node parent) {
this.parent = parent;
}
/**
* Provides lookup of elements by non-namespaced name
*
* @param key the name (or shortcut key) of the node(s) of interest
* @return the nodes which match key
*/
public Object get(String key) {
if (key != null && key.charAt(0) == '@') {
String attributeName = key.substring(1);
return attributes().get(attributeName);
}
if ("..".equals(key)) {
return parent();
}
if ("*".equals(key)) {
return children();
}
if ("**".equals(key)) {
return depthFirst();
}
return getByName(key);
}
/**
* Provides lookup of elements by QName.
*
* @param name the QName of interest
* @return the nodes matching name
*/
public NodeList getAt(QName name) {
NodeList answer = new NodeList();
for (Object child : children()) {
if (child instanceof Node) {
Node childNode = (Node) child;
Object childNodeName = childNode.name();
if (name.matches(childNodeName)) {
answer.add(childNode);
}
}
}
return answer;
}
/**
* Provides lookup of elements by name.
*
* @param name the name of interest
* @return the nodes matching name
*/
private NodeList getByName(String name) {
NodeList answer = new NodeList();
for (Object child : children()) {
if (child instanceof Node) {
Node childNode = (Node) child;
Object childNodeName = childNode.name();
if (childNodeName instanceof QName) {
QName qn = (QName) childNodeName;
if (qn.matches(name)) {
answer.add(childNode);
}
} else if (name.equals(childNodeName)) {
answer.add(childNode);
}
}
}
return answer;
}
/**
* Provides a collection of all the nodes in the tree
* using a depth-first preorder traversal.
*
* @return the list of (depth-first) ordered nodes
*/
public List depthFirst() {
return depthFirst(true);
}
/**
* Provides a collection of all the nodes in the tree
* using a depth-first traversal.
*
* @param preorder if false, a postorder depth-first traversal will be performed
* @return the list of (depth-first) ordered nodes
* @since 2.5.0
*/
public List depthFirst(boolean preorder) {
List answer = new NodeList();
if (preorder) answer.add(this);
answer.addAll(depthFirstRest(preorder));
if (!preorder) answer.add(this);
return answer;
}
private List depthFirstRest(boolean preorder) {
List answer = new NodeList();
for (Iterator iter = InvokerHelper.asIterator(value); iter.hasNext(); ) {
Object child = iter.next();
if (child instanceof Node) {
Node childNode = (Node) child;
List children = childNode.depthFirstRest(preorder);
if (preorder) answer.add(childNode);
if (children.size() > 1 || (children.size() == 1 && !(children.get(0) instanceof String))) answer.addAll(children);
if (!preorder) answer.add(childNode);
} else if (child instanceof String) {
answer.add(child);
}
}
return answer;
}
/**
* Provides a collection of all the nodes in the tree
* using a depth-first preorder traversal.
*
* @param c the closure to run for each node (a one or two parameter can be used; if one parameter is given the
* closure will be passed the node, for a two param closure the second parameter will be the level).
* @since 2.5.0
*/
public void depthFirst(Closure c) {
Map<String, Object> options = new ListHashMap<String, Object>();
options.put("preorder", true);
depthFirst(options, c);
}
/**
* Provides a collection of all the nodes in the tree
* using a depth-first traversal.
* A boolean 'preorder' options is supported.
*
* @param options map containing options
* @param c the closure to run for each node (a one or two parameter can be used; if one parameter is given the
* closure will be passed the node, for a two param closure the second parameter will be the level).
* @since 2.5.0
*/
public void depthFirst(Map<String, Object> options, Closure c) {
boolean preorder = Boolean.parseBoolean(options.get("preorder").toString());
if (preorder) callClosureForNode(c, this, 1);
depthFirstRest(preorder, 2, c);
if (!preorder) callClosureForNode(c, this, 1);
}
private static <T> T callClosureForNode(Closure<T> closure, Object node, int level) {
if (closure.getMaximumNumberOfParameters() == 2) {
return closure.call(node, level);
}
return closure.call(node);
}
private void depthFirstRest(boolean preorder, int level, Closure c) {
for (Iterator iter = InvokerHelper.asIterator(value); iter.hasNext(); ) {
Object child = iter.next();
if (child instanceof Node) {
Node childNode = (Node) child;
if (preorder) callClosureForNode(c, childNode, level);
childNode.depthFirstRest(preorder, level + 1, c);
if (!preorder) callClosureForNode(c, childNode, level);
}
}
}
/**
* Provides a collection of all the nodes in the tree
* using a breadth-first preorder traversal.
*
* @return the list of (breadth-first) ordered nodes
*/
public List breadthFirst() {
return breadthFirst(true);
}
/**
* Provides a collection of all the nodes in the tree
* using a breadth-first traversal.
*
* @param preorder if false, a postorder breadth-first traversal will be performed
* @return the list of (breadth-first) ordered nodes
* @since 2.5.0
*/
public List breadthFirst(boolean preorder) {
List answer = new NodeList();
if (preorder) answer.add(this);
answer.addAll(breadthFirstRest(preorder));
if (!preorder) answer.add(this);
return answer;
}
private List breadthFirstRest(boolean preorder) {
List answer = new NodeList();
Stack stack = new Stack();
List nextLevelChildren = preorder ? getDirectChildren() : DefaultGroovyMethods.reverse(getDirectChildren());
while (!nextLevelChildren.isEmpty()) {
List working = new NodeList(nextLevelChildren);
nextLevelChildren = new NodeList();
for (Object child : working) {
if (preorder) {
answer.add(child);
} else {
stack.push(child);
}
if (child instanceof Node) {
Node childNode = (Node) child;
List children = childNode.getDirectChildren();
if (children.size() > 1 || (children.size() == 1 && !(children.get(0) instanceof String))) nextLevelChildren.addAll(preorder ? children : DefaultGroovyMethods.reverse(children));
}
}
}
while (!stack.isEmpty()) {
answer.add(stack.pop());
}
return answer;
}
/**
* Calls the provided closure for all the nodes in the tree
* using a breadth-first preorder traversal.
*
* @param c the closure to run for each node (a one or two parameter can be used; if one parameter is given the
* closure will be passed the node, for a two param closure the second parameter will be the level).
* @since 2.5.0
*/
public void breadthFirst(Closure c) {
Map<String, Object> options = new ListHashMap<String, Object>();
options.put("preorder", true);
breadthFirst(options, c);
}
/**
* Calls the provided closure for all the nodes in the tree
* using a breadth-first traversal.
* A boolean 'preorder' options is supported.
*
* @param options map containing options
* @param c the closure to run for each node (a one or two parameter can be used; if one parameter is given the
* closure will be passed the node, for a two param closure the second parameter will be the level).
* @since 2.5.0
*/
public void breadthFirst(Map<String, Object> options, Closure c) {
boolean preorder = Boolean.parseBoolean(options.get("preorder").toString());
if (preorder) callClosureForNode(c, this, 1);
breadthFirstRest(preorder, 2, c);
if (!preorder) callClosureForNode(c, this, 1);
}
private void breadthFirstRest(boolean preorder, int level, Closure c) {
Stack<Tuple2<Object, Integer>> stack = new Stack<Tuple2<Object, Integer>>();
List nextLevelChildren = preorder ? getDirectChildren() : DefaultGroovyMethods.reverse(getDirectChildren());
while (!nextLevelChildren.isEmpty()) {
List working = new NodeList(nextLevelChildren);
nextLevelChildren = new NodeList();
for (Object child : working) {
if (preorder) {
callClosureForNode(c, child, level);
} else {
stack.push(new Tuple2<Object, Integer>(child, level));
}
if (child instanceof Node) {
Node childNode = (Node) child;
List children = childNode.getDirectChildren();
if (children.size() > 1 || (children.size() == 1 && !(children.get(0) instanceof String))) nextLevelChildren.addAll(preorder ? children : DefaultGroovyMethods.reverse(children));
}
}
level++;
}
while (!stack.isEmpty()) {
Tuple2<Object, Integer> next = stack.pop();
callClosureForNode(c, next.getV1(), next.getV2());
}
}
/**
* Returns the list of any direct String nodes of this node.
*
* @return the list of String values from this node
* @since 2.3.0
*/
public List<String> localText() {
List<String> answer = new ArrayList<String>();
for (Iterator iter = InvokerHelper.asIterator(value); iter.hasNext(); ) {
Object child = iter.next();
if (!(child instanceof Node)) {
answer.add(child.toString());
}
}
return answer;
}
private List getDirectChildren() {
List answer = new NodeList();
for (Iterator iter = InvokerHelper.asIterator(value); iter.hasNext(); ) {
Object child = iter.next();
if (child instanceof Node) {
Node childNode = (Node) child;
answer.add(childNode);
} else if (child instanceof String) {
answer.add(child);
}
}
return answer;
}
@Override
public String toString() {
return name + "[attributes=" + attributes + "; value=" + value + "]";
}
/**
* Writes the node to the specified <code>PrintWriter</code>.
*
* @param out the writer receiving the output
*/
public void print(PrintWriter out) {
new NodePrinter(out).print(this);
}
/**
* Converts the text of this GPathResult to an Integer object.
*
* @return the GPathResult, converted to a <code>Integer</code>
*/
public Integer toInteger() {
if(textIsEmptyOrNull()){
return null;
}
return StringGroovyMethods.toInteger((CharSequence)text());
}
/**
* Converts the text of this GPathResult to a Long object.
*
* @return the GPathResult, converted to a <code>Long</code>
*/
public Long toLong() {
if(textIsEmptyOrNull()){
return null;
}
return StringGroovyMethods.toLong((CharSequence)text());
}
/**
* Converts the text of this GPathResult to a Float object.
*
* @return the GPathResult, converted to a <code>Float</code>
*/
public Float toFloat() {
if(textIsEmptyOrNull()){
return null;
}
return StringGroovyMethods.toFloat((CharSequence)text());
}
/**
* Converts the text of this GPathResult to a Double object.
*
* @return the GPathResult, converted to a <code>Double</code>
*/
public Double toDouble() {
if(textIsEmptyOrNull()){
return null;
}
return StringGroovyMethods.toDouble((CharSequence)text());
}
/**
* Converts the text of this GPathResult to a BigDecimal object.
*
* @return the GPathResult, converted to a <code>BigDecimal</code>
*/
public BigDecimal toBigDecimal() {
if(textIsEmptyOrNull()){
return null;
}
return StringGroovyMethods.toBigDecimal((CharSequence)text());
}
/**
* Converts the text of this GPathResult to a BigInteger object.
*
* @return the GPathResult, converted to a <code>BigInteger</code>
*/
public BigInteger toBigInteger() {
if(textIsEmptyOrNull()){
return null;
}
return StringGroovyMethods.toBigInteger((CharSequence)text());
}
private boolean textIsEmptyOrNull() {
String t = text();
return null == t || 0 == t.length();
}
}