groovy.xml.slurpersupport.GPathResult Maven / Gradle / Ivy
/*
* 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.xml.slurpersupport;
import groovy.lang.Buildable;
import groovy.lang.Closure;
import groovy.lang.DelegatingMetaClass;
import groovy.lang.GString;
import groovy.lang.GroovyObject;
import groovy.lang.GroovyObjectSupport;
import groovy.lang.IntRange;
import groovy.lang.MetaClass;
import groovy.lang.Writable;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.ResourceGroovyMethods;
import org.codehaus.groovy.runtime.StringGroovyMethods;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Stack;
/**
* Base class for representing lazy evaluated GPath expressions.
*/
public abstract class GPathResult extends GroovyObjectSupport implements Writable, Buildable, Iterable {
protected final GPathResult parent;
protected final String name;
protected final String namespacePrefix;
protected final Map namespaceMap = new HashMap();
protected final Map namespaceTagHints;
/**
* Creates a new GPathResult named name
with the parent parent
,
* the namespacePrefix namespacePrefix
and the namespaceTagHints specified in
* the namespaceTagHints
Map.
*
* @param parent the GPathResult prior to the application of the expression creating this GPathResult
* @param name if the GPathResult corresponds to something with a name, e.g. a node
* @param namespacePrefix the namespace prefix if any
* @param namespaceTagHints the known tag to namespace mappings
*/
public GPathResult(final GPathResult parent, final String name, final String namespacePrefix, final Map namespaceTagHints) {
if (parent == null) {
// we are the top of the tree
this.parent = this;
this.namespaceMap.put("xml", "http://www.w3.org/XML/1998/namespace"); // The XML namespace is always defined
} else {
this.parent = parent;
this.namespaceMap.putAll(parent.namespaceMap);
}
this.name = name;
this.namespacePrefix = namespacePrefix;
this.namespaceTagHints = namespaceTagHints;
setMetaClass(getMetaClass()); // wrap the standard MetaClass with the delegate
}
/**
* Replaces the MetaClass of this GPathResult.
*
* @param metaClass the new MetaClass
*/
@Override
public void setMetaClass(final MetaClass metaClass) {
final MetaClass newMetaClass = new DelegatingMetaClass(metaClass) {
@Override
public Object getAttribute(final Object object, final String attribute) {
return GPathResult.this.getProperty("@" + attribute);
}
@Override
public void setAttribute(final Object object, final String attribute, final Object newValue) {
GPathResult.this.setProperty("@" + attribute, newValue);
}
};
super.setMetaClass(newMetaClass);
}
/**
* Returns the specified Property of this GPathResult.
*
* Realizes the follow shortcuts:
*
* '..'
for parent()
* '*'
for children()
* '**'
for depthFirst()
* '@'
for attribute access
*
* @param property the Property to fetch
*/
@Override
public Object getProperty(final String property) {
if ("..".equals(property)) {
return parent();
} else if ("*".equals(property)) {
return children();
} else if ("**".equals(property)) {
return depthFirst();
} else if (property.startsWith("@")) {
if (property.contains(":") && !this.namespaceTagHints.isEmpty()) {
final int i = property.indexOf(':');
return new Attributes(this, "@" + property.substring(i + 1), property.substring(1, i), this.namespaceTagHints);
} else {
return new Attributes(this, property, this.namespaceTagHints);
}
} else {
if (property.contains(":") && !this.namespaceTagHints.isEmpty()) {
final int i = property.indexOf(':');
return new NodeChildren(this, property.substring(i + 1), property.substring(0, i), this.namespaceTagHints);
} else {
return new NodeChildren(this, property, this.namespaceTagHints);
}
}
}
/**
* Replaces the specified property of this GPathResult with a new value.
*
* @param property the property of this GPathResult to replace
* @param newValue the new value of the property
*/
@Override
public void setProperty(final String property, final Object newValue) {
if (property.startsWith("@")) {
if (newValue instanceof String || newValue instanceof GString) {
for (Object o : this) {
final NodeChild child = (NodeChild) o;
child.attributes().put(property.substring(1), newValue);
}
}
} else {
final GPathResult result = new NodeChildren(this, property, this.namespaceTagHints);
if (newValue instanceof Map) {
for (Object o : ((Map) newValue).entrySet()) {
final Map.Entry entry = (Map.Entry) o;
result.setProperty("@" + entry.getKey(), entry.getValue());
}
} else {
if (newValue instanceof Closure) {
result.replaceNode((Closure) newValue);
} else {
result.replaceBody(newValue);
}
}
}
}
/**
* Overloads the left shift operator to provide an easy way to
* lazily append Objects to this GPathResult.
*
* @param newValue the Object to append
* @return this
*/
public Object leftShift(final Object newValue) {
appendNode(newValue);
return this;
}
/**
* Lazily adds the specified Object to this GPathResult.
*
* @param newValue the Object to add
* @return this
*/
public Object plus(final Object newValue) {
this.replaceNode(new Closure(this) {
public void doCall(Object[] args) {
final GroovyObject delegate = (GroovyObject) getDelegate();
delegate.getProperty("mkp");
delegate.invokeMethod("yield", args);
delegate.getProperty("mkp");
delegate.invokeMethod("yield", new Object[]{newValue});
}
});
return this;
}
// GROOVY-9852: so that XmlSlurper plus (shorthand for replaceNode)
// is preferred ahead of the DGM plus(Iterable, Iterable) method
// since GPathResult is Iterable
public Object plus(final Closure newValue) {
return plus((Object) newValue);
}
protected abstract void replaceNode(Closure newValue);
protected abstract void replaceBody(Object newValue);
protected abstract void appendNode(Object newValue);
/**
* Returns the name of this GPathResult.
*
* @return the name of this GPathResult
*/
public String name() {
return this.name;
}
/**
* Returns the parent of this GPathResult. If this GPathResult has no parent the GPathResult itself is returned.
* This is no navigation in the XML tree. It is backtracking on the GPath expression chain.
* It is the behavior of parent() prior to 2.2.0.
* Backtracking on '..' actually goes down one level in the tree again.
* find() and findAll() are popped along with the level they have been applied to.
*
* @return the parent or this
*/
public GPathResult pop() {
return this.parent;
}
/**
* Returns as GPathResult with the parent nodes of the current GPathResult
*
* @return the parents GPathResult or this
for the root
*/
public GPathResult parent() {
return new NodeParents(this, this.namespaceTagHints);
}
/**
* Returns the children of this GPathResult as a GPathResult object.
*
* @return the children of this GPathResult
*/
public GPathResult children() {
return new NodeChildren(this, this.namespaceTagHints);
}
/**
* Returns the namespace mapped to the specified prefix.
*
* @param prefix the prefix lookup
* @return the namespace of the prefix
*/
public String lookupNamespace(final String prefix) {
Object namespace = namespaceMap.get(prefix);
if (namespace != null) {
return namespace.toString();
}
return this.namespaceTagHints.isEmpty() ? prefix : this.namespaceTagHints.get(prefix);
}
/**
* Returns the text of this GPathResult.
*
* @return the GPathResult, converted to a String
*/
@Override
public String toString() {
return text();
}
/**
* Converts the text of this GPathResult to an Integer object.
*
* @return the GPathResult, converted to a Integer
*/
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 Long
*/
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 Float
*/
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 Double
*/
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 BigDecimal
*/
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 BigInteger
*/
public BigInteger toBigInteger() {
if(textIsEmptyOrNull()){
return null;
}
return StringGroovyMethods.toBigInteger((CharSequence)text());
}
private boolean textIsEmptyOrNull() {
String t = text();
return null == t || 0 == t.length();
}
/**
* Converts the text of this GPathResult to a URL object.
*
* @return the GPathResult, converted to a URL
*/
public URL toURL() throws MalformedURLException {
return ResourceGroovyMethods.toURL((CharSequence)text());
}
/**
* Converts the text of this GPathResult to a URI object.
*
* @return the GPathResult, converted to a URI
*/
public URI toURI() throws URISyntaxException {
return ResourceGroovyMethods.toURI((CharSequence)text());
}
/**
* Converts the text of this GPathResult to a Boolean object.
*
* @return the GPathResult, converted to a Boolean
*/
public Boolean toBoolean() {
return StringGroovyMethods.toBoolean(text());
}
/**
* Adds the specified map of prefix to namespace mappings to this GPathResult.
* Already existing prefixes are overwritten.
*
* @param newNamespaceMapping the mappings to add
* @return this
*/
public GPathResult declareNamespace(final Map newNamespaceMapping) {
this.namespaceMap.putAll(newNamespaceMapping);
return this;
}
@Override
public int hashCode() {
return text().hashCode();
}
@Override
public boolean equals(Object obj) {
if (null == obj) {
return false;
}
return text().equals(obj.toString());
}
/**
* Supports the subscript operator for a GPathResult.
*
* import groovy.xml.slurpersupport.*
* import groovy.xml.XmlSlurper
* def text = """
* <characterList>
* <character/>
* <character>
* <name>Gromit</name>
* </character>
* </characterList>"""
*
* GPathResult characterList = new XmlSlurper().parseText(text)
*
* assert characterList.character[1].name == 'Gromit'
*
* @param index an index
* @return the value at the given index
*/
public Object getAt(final int index) {
if (index < 0) {
// calculate whole list in this case
// recommend avoiding -ve's as this is obviously not as efficient
List list = list();
int adjustedIndex = index + list.size();
if (adjustedIndex >= 0 && adjustedIndex < list.size())
return list.get(adjustedIndex);
} else {
final Iterator iter = iterator();
int count = 0;
while (iter.hasNext()) {
if (count++ == index) {
return iter.next();
} else {
iter.next();
}
}
}
return new NoChildren(this, this.name, this.namespaceTagHints);
}
/**
* Supports the range subscript operator for a GPathResult.
*
* import groovy.xml.slurpersupport.*
* import groovy.xml.XmlSlurper
* def text = """
* <characterList>
* <character>Wallace</character>
* <character>Gromit</character>
* <character>Shaun</character>
* </characterList>"""
*
* GPathResult characterList = new XmlSlurper().parseText(text)
*
* assert characterList.character[1..2].join(',') == 'Gromit,Shaun'
*
* @param range a Range indicating the items to get
* @return a new list based on range borders
*/
public Object getAt(final IntRange range) {
return DefaultGroovyMethods.getAt(list(), range);
}
/**
* A helper method to allow GPathResults to work with subscript operators
* @param index an index
* @param newValue the value to put at the given index
*/
public void putAt(final int index, final Object newValue) {
final GPathResult result = (GPathResult)getAt(index);
if (newValue instanceof Closure) {
result.replaceNode((Closure)newValue);
} else {
result.replaceBody(newValue);
}
}
/**
* Provides an Iterator over all the nodes of this GPathResult using a depth-first traversal.
*
* @return the Iterator
of (depth-first) ordered GPathResults
*/
public Iterator depthFirst() {
return new Iterator() {
private final List list = new LinkedList();
private final Stack stack = new Stack();
private Iterator iter = iterator();
private GPathResult next = getNextByDepth();
@Override
public boolean hasNext() {
return this.next != null;
}
@Override
public Object next() {
try {
return this.next;
} finally {
this.next = getNextByDepth();
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
private GPathResult getNextByDepth() {
while (this.iter.hasNext()) {
final GPathResult node = (GPathResult) this.iter.next();
this.list.add(node);
this.stack.push(this.iter);
this.iter = node.children().iterator();
}
if (this.list.isEmpty()) {
return null;
} else {
GPathResult result = (GPathResult) this.list.get(0);
this.list.remove(0);
this.iter = (Iterator) this.stack.pop();
return result;
}
}
};
}
/**
* Provides an Iterator over all the nodes of this GPathResult using a breadth-first traversal.
*
* @return the Iterator
of (breadth-first) ordered GPathResults
*/
public Iterator breadthFirst() {
return new Iterator() {
private final List list = new LinkedList();
private Iterator iter = iterator();
private GPathResult next = getNextByBreadth();
@Override
public boolean hasNext() {
return this.next != null;
}
@Override
public Object next() {
try {
return this.next;
} finally {
this.next = getNextByBreadth();
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
private GPathResult getNextByBreadth() {
List children = new ArrayList();
while (this.iter.hasNext() || !children.isEmpty()) {
if (this.iter.hasNext()) {
final GPathResult node = (GPathResult) this.iter.next();
this.list.add(node);
this.list.add(this.iter);
children.add(node.children());
} else {
List nextLevel = new ArrayList();
for (Object child : children) {
GPathResult next = (GPathResult) child;
for (Object o : next) {
nextLevel.add(o);
}
}
this.iter = nextLevel.iterator();
children = new ArrayList();
}
}
if (this.list.isEmpty()) {
return null;
} else {
GPathResult result = (GPathResult) this.list.get(0);
this.list.remove(0);
this.iter = (Iterator) this.list.get(0);
this.list.remove(0);
return result;
}
}
};
}
/**
* Creates a list of objects representing this GPathResult.
*
* @return a list representing of this GPathResult
*/
public List list() {
final Iterator iter = nodeIterator();
final List result = new LinkedList();
while (iter.hasNext()) {
result.add(new NodeChild((Node) iter.next(), this.parent, this.namespacePrefix, this.namespaceTagHints));
}
return result;
}
/**
* Returns true if the GPathResult is empty, i.e. if, and only if, size()
is 0.
*
* @return true if the GPathResult is empty
*/
public boolean isEmpty() {
return size() == 0;
}
/**
* Creates a Closure representing the body of this GPathResult.
*
* @return the body of this GPathResult, converted to a Closure
*/
public Closure getBody() {
return new Closure(this.parent,this) {
public void doCall(Object[] args) {
final GroovyObject delegate = (GroovyObject)getDelegate();
final GPathResult thisObject = (GPathResult)getThisObject();
Node node = (Node)thisObject.getAt(0);
List children = node.children();
for (Object child : children) {
delegate.getProperty("mkp");
if (child instanceof Node) {
delegate.invokeMethod("yield", new Object[]{new NodeChild((Node) child, thisObject, "*", null)});
} else {
delegate.invokeMethod("yield", new Object[]{child});
}
}
}
};
}
/**
* Returns the size of this GPathResult.
* @return the size of this GPathResult
*/
public abstract int size();
/**
* Returns the text of this GPathResult as a String
.
* @return the text of this GPathResult
*/
public abstract String text();
/**
* Returns the parents of this GPathResult as a GPathResult
.
* Warning: The subclasses of this package do not implement this method yet.
* @return the parents of this GPathResult
*/
public abstract GPathResult parents();
/**
* Returns an iterator over the child nodes of this GPathResult.
* @return an iterator over the child nodes of this GPathResult
*/
public abstract Iterator childNodes();
@Override
public abstract Iterator iterator();
/**
* Returns the first child of this GPathResult matching the condition(s)
* specified in the passed closure.
* @param closure a closure to filters the children of this GPathResult
* @return the first child matching the closure
*/
public abstract GPathResult find(Closure closure);
/**
* Returns the children of this GPathResult matching the condition(s)
* specified in the passed closure.
* @param closure a closure to filters the children of this GPathResult
* @return the children matching the closure
*/
public abstract GPathResult findAll(Closure closure);
public abstract Iterator nodeIterator();
protected Iterator createIterator(final Object obj) {
return new Iterator() {
private boolean hasNext = true;
@Override
public boolean hasNext() {
return this.hasNext;
}
@Override
public Object next() {
try {
return (this.hasNext) ? obj : null;
} finally {
this.hasNext = false;
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
}