package dk.brics.xact;
import dk.brics.misc.Origin;
import dk.brics.relaxng.converter.ParseException;
import dk.brics.xact.operations.*;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* XML template content node.
* <p/>
* This class contains the main functionality of the XACT runtime system.
* See also the general documentation for the package {@link dk.brics.xact}.
*/
abstract public class XML extends Node implements ToXMLable {
private static final ConcurrentHashMap<String, String> global_namespaces;
private static final ThreadLocal<ConcurrentHashMap<String, String>> thread_namespaces;
private static ThreadLocal<XHTMLMode> threadXHTMLMode = new ThreadLocal<XHTMLMode>();
private static XHTMLMode globalXHTMLMode = XHTMLMode.XHTML1_Transitional;
public enum XHTMLMode {
XHTML1_Transitional("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" " +
"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\r\n"),
XHTML1_Strict("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" " +
"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\r\n"),
XHTML1_FrameSet("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Frameset//EN\" " +
"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd\">\r\n"),
XHTML1_1("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" " +
"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\r\n"),
XMTML5("<!DOCTYPE html>\r\n");
private String doctype;
private String namespace = "http://www.w3.org/1999/xhtml";
private XHTMLMode(String doctype){
this.doctype = doctype;
}
public String getDoctype() {
return doctype;
}
public String getNamespace() {
return namespace;
}
}
public static XHTMLMode getXHTMLDoctype() {
XHTMLMode xhtmlMode = threadXHTMLMode.get();
if (xhtmlMode != null) {
return xhtmlMode;
}
return globalXHTMLMode;
}
public static void setThreadXHTMLMode(XHTMLMode threadXHTMLMode) {
XML.threadXHTMLMode.set(threadXHTMLMode);
}
public static void setGlobalXHTMLMode(XHTMLMode globalXHTMLMode) {
XML.globalXHTMLMode = globalXHTMLMode;
}
static {
global_namespaces = new ConcurrentHashMap<String, String>();
thread_namespaces = new ThreadLocal<ConcurrentHashMap<String, String>>();
}
/**
* Constructs a new template node.
*/
protected XML(Origin origin) {
super(origin);
}
/**
* Normalizes the given node if it is an operation node.
* Also merges adjacent text nodes and non-last empty text nodes.
* Do never inspect the type of a node before invoking this method.
*/
static TempNode normalize(XML x) {
XML res = x;
if (x != null) {
synchronized (x) {
res = follow(x);
boolean operation_sibling = false;
for (XML z = res; z != null; z = follow(z.getRealNextSibling())) {
if (z instanceof OperationNode) {
operation_sibling = true;
break;
} else if (!(z instanceof Text)) {
break;
}
}
if (operation_sibling) {
res = Normalizer.normalize(res); // make sure we normalize the subtree only once
x.forward(res);
}
StringBuilder b = new StringBuilder();
XML y;
int c;
for (c = 0, y = res; y != null && y.isText(); y = y.getRealNextSibling(), c++) {
b.append(y.asText().getString());
}
if (c > 1) {
if (b.length() > 0 || y == null) {
res = new Text(b.toString(), y, res.getOrigin());
} else {
res = y;
}
x.forward(res);
}
}
}
return (TempNode) res;
}
@Override
public final void visitBy(NodeVisitor v) {
normalize(this).visitNormalizedBy(v);
}
@Override
public final <R, A> R visitBy(NodeVisitorParameterized<R, A> v, A arg) {
return normalize(this).visitNormalizedBy(v, arg);
}
/**
* Returns the global namespace map (from prefix to URI).
* The map is modifiable. The empty prefix string represents the default namespace.
*
* @see #getThreadNamespaceMap()
*/
public static Map<String, String> getNamespaceMap() {
return global_namespaces;
}
/**
* Returns the thread local namespace map (from prefix to URI).
* The map is modifiable. The empty prefix string represents the default namespace.
* Overrides the global namespace map for this thread.
*
* @see #getNamespaceMap()
*/
public static Map<String, String> getThreadNamespaceMap() {
ConcurrentHashMap<String, String> map = thread_namespaces.get();
if (map == null) {
map = new ConcurrentHashMap<String, String>();
thread_namespaces.set(map);
}
return map;
}
/**
* Loads an XML schema.
* Supported schema languages: DTD, XML Schema, and Restricted RELAX NG.
*/
public synchronized static void loadXMLSchema(URL url) {
try {
XMLValidator.loadXMLSchema(url);
} catch (ParseException e) {
throw new XMLException(e);
}
}
/**
* Loads an XML schema.
* Supported schema languages: DTD, XML Schema, and Restricted RELAX NG.
*/
public synchronized static void loadXMLSchema(String url) {
try {
loadXMLSchema(new URL(url));
} catch (MalformedURLException e) {
throw new XMLException(e);
}
}
/**
* Returns the next sibling node, or null if none.
*/
public XML getNextSibling() {
XML x = follow(this);
if (x != null) {
x = x.getNextSibling();
}
return x;
}
/**
* Returns the next sibling, without normalizing.
* Overridden in subclasses that can have a next sibling.
*/
XML getRealNextSibling() {
return null;
}
/**
* Returns this object.
*/
public XML toXML() {
return this;
}
/**
* Converts the given value to an XML template using {@link ToXMLable#toXML()} or {@link #toString()}.
*/
public static XML toXML(Object v) {
XML x;
if (v instanceof ToXMLable) {
x = ((ToXMLable) v).toXML();
} else {
x = new Text(v.toString());
}
return x;
}
/**
* Evaluates the given value using {@link ToXMLable#toXML()} or {@link #toString()}
* and returns an object of type <code>XML</code> or a <code>String</code>.
*
* @throws NullPointerException if v is null
*/
private static Object extract(Object v) {
if (v == null) {
throw new NullPointerException("attempt to plug null value");
}
Object r;
if (v instanceof XML || v instanceof String) {
r = v;
} else if (v instanceof ToXMLable) {
r = ((ToXMLable) v).toXML();
} else {
r = v.toString();
}
return r;
}
/**
* Constructs an XML template from its string representation.
* The program analysis requires the string to be constant.
* The global and thread local namespace declarations are used, and gaps are recognized.
* Results are cached in memory.
*
* @throws XMLTemplateException if the string is not a syntactically wellformed XML template
* @see #parseDocument(String)
* @see #parseTemplate(String, Origin)
*/
public static XML parseTemplate(String template) {
return parseTemplate(template, null);
}
/**
* Constructs an XML template from its string representation.
* The program analysis requires the string to be constant.
* The global and thread local namespace declarations are used, and gaps are recognized.
* Results are cached in memory (using the template string as key).
*
* @throws XMLTemplateException if the string is not a syntactically wellformed XML template
* @see #parseDocument(String)
* @see #parseTemplate(InputStream, Origin)
*/
public static XML parseTemplate(String template, Origin origin) {
XML x = TemplateCache.get(template);
if (x == null) {
try {
x = XMLParser.parse(new ByteArrayInputStream(template.getBytes("UTF-8")), "UTF-8", true, null, origin);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new XMLException(e);
}
TemplateCache.put(template, x);
}
return x;
}
/**
* Constructs an XML template from its string representation from an input stream.
* Assumes UTF-8 encoding.
* The program analysis requires the string to be constant.
* The global and thread local namespace declarations are used, and gaps are recognized.
*
* @throws XMLTemplateException if the string is not a syntactically wellformed XML template
* @see #parseDocument(String)
*/
public static XML parseTemplate(InputStream in, Origin origin) {
try {
return XMLParser.parse(in, "UTF-8", true, null, origin);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new XMLException(e);
}
}
/**
* Constructs an XML template from its string representation from an input stream.
* Assumes UTF-8 encoding.
* The program analysis requires the string to be constant.
* The global and thread local namespace declarations are used, and gaps are recognized.
*
* @throws XMLTemplateException if the string is not a syntactically wellformed XML template
* @see #parseDocument(String)
*/
public static XML parseTemplate(InputStream in) {
return parseTemplate(in, null);
}
/**
* Constructs an XML template from its string representation located in a resource.
* Assumes UTF-8 encoding.
* The program analysis requires the string to be constant.
* The global and thread local namespace declarations are used, and gaps are recognized.
* Uses <code>XML.class.getResourceAsStream</code> to load the string.
* Results are cached in memory.
*
* @throws XMLTemplateException if the string is not a syntactically wellformed XML template
* or the resource is not found
* @see #parseDocument(String)
*/
public static XML parseTemplateResource(String name) {
return parseTemplateResource(XML.class, name);
}
/**
* Constructs an XML template from its string representation located in a resource.
* Assumes UTF-8 encoding.
* The program analysis requires the string to be constant.
* The global and thread local namespace declarations are used, and gaps are recognized.
* Uses <code>base.getResourceAsStream</code> to load the string.
* Results are cached in memory.
*
* @throws XMLTemplateException if the string is not a syntactically wellformed XML template
* or the resource is not found
* @see #parseDocument(String)
*/
public static XML parseTemplateResource(Class<?> base, String name) {
String key = base.getName() + "#" + name;
XML x = TemplateCache.get(key);
if (x == null) {
try {
InputStream in = base.getResourceAsStream(name);
if (in == null) {
throw new XMLTemplateException("Resource not found: " + name + " from " + base.toString());
}
x = XMLParser.parse(in, "UTF-8", true, null, new Origin(name, 0, 0));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new XMLException(e);
}
TemplateCache.put(key, x);
}
return x;
}
/**
* Constructs an XML document from its string representation.
* Assumes UTF-8 encoding.
* (The global and thread local namespace declarations are <i>not</i> considered, and gaps are <i>not</i> recognized.)
*
* @throws XMLTemplateException if the string is not a syntactically wellformed XML document
* @see #parseDocument(InputStream)
*/
public static XML parseDocument(String doc) {
try {
return parseDocument(new ByteArrayInputStream(doc.getBytes("UTF-8")));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* Constructs an XML document from its string representation from a URL.
* Assumes UTF-8 encoding.
* (The global and thread local namespace declarations are <i>not</i> considered, and gaps are <i>not</i> recognized.)
*
* @throws XMLTemplateException if the string is not a syntactically wellformed XML document
* @see #parseDocument(InputStream, String, Origin)
* @see #parseDocument(String)
* @see #toDocument(OutputStream, String)
*/
public static XML parseDocument(URL url) {
try {
return parseDocument(url.openStream());
} catch (IOException e) {
throw new XMLException(e);
}
}
/**
* Constructs an XML document from its string representation from an input stream.
* Assumes UTF-8 encoding.
* (The global and thread local namespace declarations are <i>not</i> considered, and gaps are <i>not</i> recognized.)
*
* @throws XMLTemplateException if the string is not a syntactically wellformed XML document
* @see #parseDocument(InputStream, String, Origin)
* @see #parseDocument(String)
* @see #toDocument(OutputStream, String)
*/
public static XML parseDocument(InputStream in) {
return parseDocument(in, "UTF-8", null);
}
/**
* Constructs an XML document from its string representation from an input stream using the given encoding.
* (The global and thread local namespace declarations are <i>not</i> considered, and gaps are <i>not</i> recognized.)
*
* @throws XMLTemplateException if the string is not a syntactically wellformed XML document
* @see #parseDocument(InputStream)
* @see #parseDocument(String)
* @see #toDocument(OutputStream, String)
*/
public static XML parseDocument(InputStream in, String encoding, Origin origin) {
try {
return XMLParser.parse(in, encoding, false, null, origin);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new XMLException(e);
}
}
/**
* Converts this XML template to its string representation with all gaps removed.
* Uses UTF-8 encoding.
* The output contains an XML declaration.
*
* @see #toTemplate()
* @see #toDocument(String)
*/
public String toDocument() {
try {
return toDocument("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* Converts this XML template to its string representation with all gaps removed, using the given encoding.
* The output contains an XML declaration.
*
* @throws XMLWellformednessException if the result is not a wellformed XML document
* @throws UnsupportedEncodingException if the encoding is not supported
* @see #toDocument()
* @see #toTemplate(String)
*/
public String toDocument(String encoding) throws UnsupportedEncodingException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
toDocument(out, encoding);
return out.toString(encoding);
}
/**
* Converts this XML template to its string representation with all gaps removed, using the given encoding.
* The output contains an XML declaration.
*
* @throws XMLWellformednessException if the result is not a wellformed XML document
* @see #toDocument()
* @see #toTemplate(XMLIndentation)
*/
public String toDocument(XMLIndentation indentation) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
XMLPrinter.print(follow(this), out, "UTF-8", false, true, indentation);
return out.toString("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* Converts this template to its string representation.
* Uses UTF-8 encoding.
* Unlike {@link #toString()}, gaps are preserved in the output.
*
* @see #toString()
* @see #toTemplate(String)
*/
public String toTemplate() {
try {
return toTemplate("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* Converts this template to its string representation, using the specified
* indentation strategy.
* Uses UTF-8 encoding.
* Unlike {@link #toDocument()}, gaps are preserved in the output.
*
* @see #toString()
* @see #toTemplate(String)
*/
public String toTemplate(XMLIndentation indentation) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
XMLPrinter.print(follow(this), out, "UTF-8", true, false, indentation);
return out.toString("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* Converts this template to its string representation, using the given encoding.
* Unlike {@link #toDocument()}, gaps are preserved in the output.
*
* @see #toDocument(String)
* @see #toTemplate()
*/
public String toTemplate(String encoding) throws UnsupportedEncodingException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
XMLPrinter.print(follow(this), out, encoding, true, false);
return out.toString(encoding);
}
/**
* Converts this XML template to its string representation with all gaps removed
* and prints the result to an output stream.
* The output contains an XML declaration.
*
* @throws XMLWellformednessException if the result is not a wellformed XML document
* @throws UnsupportedEncodingException if the encoding is not supported
* @see #parseDocument(InputStream)
*/
public void toDocument(OutputStream out, String encoding) throws UnsupportedEncodingException {
XMLPrinter.print(follow(this), out, encoding, false, true);
}
/**
* Computes the number of bytes written if invoking {@link #toDocument(OutputStream, String)}.
*
* @throws XMLWellformednessException if the result is not a wellformed XML document
* @throws UnsupportedEncodingException if the encoding is not supported
*/
public int byteLength(String encoding) throws UnsupportedEncodingException {
LengthOutputStream s = new LengthOutputStream();
toDocument(s, encoding);
return s.getLength();
}
/**
* Constructs a new XML template by plugging the given value into each template gap of the
* given name in this template.
*
* @throws NullPointerException if the value is null
* @see #plugList(String, Iterable)
* @see #plugWrap(String, Iterable)
* @see #close
*/
public final XML plug(String gap, Object v) {
return new PlugSingleNode(follow(this), gap, extract(v));
}
/**
* Constructs a new XML template by plugging the given list of values into the list of template gaps of the
* given name in this template. If too many values are provided, the remaining ones are ignored.
* If too few are provided, the remaining gaps are removed.
*
* @throws NullPointerException if one of the values is null
* @see #plug(String, Object)
* @see #plugWrap(String, Iterable)
* @see #close
*/
public final XML plugList(String gap, Iterable<?> vs) {
ArrayList<Object> values = new ArrayList<Object>();
for (Object v : vs) {
values.add(extract(v));
}
return new PlugListNode(follow(this), gap, values);
}
/**
* Constructs a template by wrapping each value from the given list into the gaps of the
* given name in this template and concatenating the results.
*
* @throws NullPointerException if one of the values is null
* @see #plug(String, Object)
* @see #plugList(String, Iterable)
* @see #close
*/
public final XML plugWrap(String gap, Iterable<?> vs) {
ArrayList<Object> values = new ArrayList<Object>();
for (Object v : vs) {
values.add(plug(gap, v));
}
return concat(values);
}
/**
* Removes all gaps in this template.
*
* @see #plug(String, Object)
* @see #plugList(String, Iterable)
* @see #plugWrap(String, Iterable)
*/
public final XML close() {
return new CloseNode(follow(this));
}
/**
* Returns the nodes specified by the given XPath expression.
* The initial context node is this node. The document root node
* is an implicit node that has this node and its siblings as content.
* The nodes are returned in document order.
*
* @see #getElements(String)
* @see #getElement(String)
* @see #getString(String)
* @see #has(String)
*/
public final NodeList<Node> get(String xpath) {
return new NodeList<Node>(XMLNavigator.selectNodes(follow(this), xpath, true).getNodes());
}
/**
* Returns the elements specified by the given XPath expression.
* The elements are returned in document order.
*
* @see #get(String)
* @see #getElement(String)
* @see #getStrings(String)
* @see #getString(String)
* @see #has(String)
*/
public final NodeList<Element> getElements(String xpath) {
return new NodeList<Element>(XMLNavigator.selectElements(follow(this), xpath, true).getElements());
}
/**
* Returns the first element specified by the given XPath expression.
*
* @see #get(String)
* @see #getFirstElement()
*/
public final Element getElement(String xpath) {
Node n = XMLNavigator.selectSingleNode(follow(this), xpath, true).getNode();
if (!(n instanceof Element)) {
throw new XMLXPathException("node is not an element");
}
return (Element) n;
}
/**
* Returns the element with the given ID.
*
* @see #getElement(String)
*/
public final Element getElementByID(String id) {
return XMLNavigator.selectElementByID(follow(this), id, true);
}
/**
* Returns the value of the attribute of the given name in the first element in this template, or null if not present.
*
* @see #get(String)
* @see #getAttribute(String, String)
*/
public String getAttribute(String localname) {
return getAttribute(null, localname);
}
/**
* Returns the value of the attribute of the given name in the first element in this template, or null if not present.
*
* @see #get(String)
* @see #getAttribute(String)
*/
public String getAttribute(String namespace, String localname) {
return getFirstElement().getAttribute(namespace, localname);
}
/**
* Returns the text or attribute values specified by the given XPath expression.
* Each string value is determined per the <code>string(..)</code> core function as defined in the XPath 1.0 specification.
* The strings are returned in document order. The returned list is unmodifiable.
*
* @see #get(String)
* @see #getElements(String)
* @see #getString(String)
*/
public List<String> getStrings(String xpath) {
return XMLNavigator.selectStrings(follow(this), xpath);
}
/**
* Returns the first element in this template.
*
* @see #getElement(String)
*/
public Element getFirstElement() {
Element res = null;
for (XML x = normalize(this); x != null; x = x.getNextSibling()) {
if (x instanceof Element) {
res = (Element) x;
break;
}
}
if (res == null) {
throw new XMLXPathException("no element");
}
return res.copy((XML) null);
}
/**
* Returns the text specified by the given XPath expression.
* If multiple nodes are selected, only the first one is considered.
* The value is determined per the <code>string(..)</code> core function as defined in the XPath 1.0 specification.
*
* @see #getString()
* @see #get(String)
* @see #getElement(String)
*/
public String getString(String xpath) {
return XMLNavigator.stringValueOf(follow(this), xpath);
}
/**
* Returns the string value of the first element or text node in this template.
*
* @see #getString(String)
*/
public String getString() {
Node n = getFirstElementOrText();
if (n.isElement()) {
return XMLPrinter.getElementStringValue(n.asElement());
} else {
return n.asText().getString();
}
}
/**
* Returns the boolean specified by the given XPath expression.
* If multiple nodes are selected, only the first one is considered.
* The value is determined per the <code>boolean(..)</code> core function as defined in the XPath 1.0 specification.
*
* @see #get(String)
*/
public boolean has(String xpath) {
return XMLNavigator.booleanValueOf(follow(this), xpath);
}
/**
* Returns the number specified by the given XPath expression.
* If multiple nodes are selected, only the first one is considered.
* The value is determined per the <code>number(..)</code> core function as defined in the XPath 1.0 specification.
*
* @see #get(String)
* @see #getNumber()
*/
public Number getNumber(String xpath) {
return XMLNavigator.numberValueOf(follow(this), xpath);
}
/**
* Returns the number value of the first element or text node in this template.
*
* @see #get(String)
* @see #getNumber(String)
* @see #getString()
*/
public Number getNumber() {
try {
return Double.parseDouble(getString().trim());
} catch (NumberFormatException e) {
return Double.NaN;
}
}
/**
* Checks that this template is valid according to the given schema type.
*
* @return this XML template
* @throws XMLValidationException if the template is invalid
* @see #analyze(String)
*/
public XML validate(String type) throws XMLValidationException {
XMLValidator.validate(follow(this), type, null);
return this;
}
/**
* Instruction for the program analyzer; no operation at runtime.
* At analysis-time, the static analyzer will check validity of
* this template relative to the given schema type.
*
* @return this XML template
* @see #validate(String)
*/
public XML analyze(String type) {
return this;
}
/**
* Converts all nodes selected by the given XPath expression into gaps of the given name.
*
* @see #gapify(String, String, String)
*/
public XML gapify(String xpath, String gap) {
return gapify(xpath, gap, null);
}
/**
* Converts all nodes selected by the given XPath expression into gaps of the given name and schema type.
*
* @see #gapify(String, String)
*/
public XML gapify(String xpath, String gap, String type) {
XML t = follow(this);
XMLNavigator.NodeListResult res = XMLNavigator.selectNodes(t, xpath, false);
for (Node n : res.getNodes()) {
if (n.isTemplate()) {
TempNode x = n.asTemplate();
n.forward(new TemplateGap(gap, type, x.getRealNextSibling(), n.getOrigin()));
} else if (n.isAttribute()) {
Attribute a = n.asAttribute();
n.forward(new AttributeGap(a.getNamespace(), a.getLocalName(),
gap, type, a.getNextAttr(), a.getOrigin()));
}
}
return follow(res.getInitial());
}
/**
* Removes the nodes selected by the given XPath expression.
*
* @see #gapify(String, String)
*/
public XML remove(String xpath) {
XML t = follow(this);
XMLNavigator.NodeListResult res = XMLNavigator.selectNodes(t, xpath, false);
for (Node n : res.getNodes()) {
if (n.isTemplate()) {
TempNode x = n.asTemplate();
if (x.getRealNextSibling() != null) {
n.forward(x.getRealNextSibling());
} else {
n.forward(Text.EMPTY);
}
} else if (n.isAttribute()) {
Attribute a = n.asAttribute();
n.forward(a.getNextAttr());
}
}
return follow(res.getInitial());
}
/**
* Sets an attribute at each selected element node.
*
* @see #set(AttrNode)
*/
public XML set(String xpath, AttrNode a) {
return set(XMLNavigator.selectElements(follow(this), xpath, false), a);
}
/**
* Sets an attribute at this element.
*
* @throws ClassCastException if this is not an element node
* @see #set(String, AttrNode)
*/
public XML set(AttrNode a) {
return set(getElementCopied(), a);
}
/**
* Sets a namespace declaration at this element.
*
* @throws ClassCastException if this is not an element node
*/
public XML set(NamespaceDecl d) {
return set(getElementCopied(), d);
}
/**
* Concatenates the given list of XML templates or strings into one template.
*
* @see #concat(Object...)
*/
public static XML concat(Iterable<?> vs) {
XML res;
Iterator<?> i = vs.iterator();
if (!i.hasNext()) {
res = Text.EMPTY;
} else {
res = toXML(i.next());
while (i.hasNext()) {
res = new ConcatNode(res, toXML(i.next()), null);
}
}
return res;
}
/**
* Concatenates the given list of XML templates or strings.
*
* @see #concat(Iterable)
*/
public static XML concat(Object... xs) {
XML res;
if (xs.length == 0) {
res = Text.EMPTY;
} else {
res = toXML(xs[0]);
for (int i = 1; i < xs.length; i++) {
res = new ConcatNode(res, toXML(xs[i]), null);
}
}
return res;
}
/**
* Concatenates the given XML templates.
*/
static XML concatXML(XML x1, XML x2) {
if (x1 == null) {
return x2;
} else if (x2 == null) {
return x1;
} else {
return new ConcatNode(x1, x2, null);
}
}
/**
* Inserts the given value in front of this template.
*
* @see #insertBefore(String, Object)
* @see #append(Object)
*/
public XML prepend(Object v) {
return new ConcatNode(toXML(v), follow(this), null);
}
/**
* Inserts the given value after this template.
*
* @see #insertAfter(String, Object)
* @see #prepend(Object)
*/
public XML append(Object v) {
return new ConcatNode(follow(this), toXML(v), null);
}
/**
* Inserts the given value before each selected content node.
*
* @see #insertAfter(String, Object)
* @see #prepend(Object)
* @see #prependContent(String, Object)
*/
public XML insertBefore(String xpath, Object v) {
XML t = follow(this);
XMLNavigator.NodeListResult res = XMLNavigator.selectNodes(t, xpath, false);
for (Node n : res.getNodes()) {
if (n.isTemplate()) {
TempNode x = n.asTemplate();
n.forward(concatXML(toXML(v), x.copy(x.getRealNextSibling())));
}
}
return follow(res.getInitial());
}
/**
* Inserts the given value after each selected content node.
*
* @see #insertBefore(String, Object)
* @see #append(Object)
* @see #appendContent(String, Object)
*/
public XML insertAfter(String xpath, Object v) {
XML t = follow(this);
XMLNavigator.NodeListResult res = XMLNavigator.selectNodes(t, xpath, false);
for (Node n : res.getNodes()) {
if (n.isTemplate()) {
TempNode x = n.asTemplate();
n.forward(x.copy(concatXML(toXML(v), x.getRealNextSibling())));
}
}
return follow(res.getInitial());
}
/**
* Inserts the given value before the content of each selected element node.
*
* @see #prependContent(Object)
* @see #appendContent(String, Object)
*/
public XML prependContent(String xpath, Object v) {
return prependContent(XMLNavigator.selectElements(follow(this), xpath, false), v);
}
/**
* Inserts the given value before the content of this element.
*
* @throws ClassCastException if this is not an element node
* @see #prependContent(String, Object)
* @see #appendContent(Object)
*/
public XML prependContent(Object v) {
return prependContent(getElementCopied(), v);
}
/**
* Inserts the given value after the content of each selected element node.
*
* @see #appendContent(Object)
* @see #prependContent(String, Object)
*/
public XML appendContent(String xpath, Object v) {
return appendContent(XMLNavigator.selectElements(follow(this), xpath, false), v);
}
/**
* Inserts the given value after the content of this element.
*
* @throws ClassCastException if this is not an element node
* @see #appendContent(String, Object)
* @see #prependContent(Object)
*/
public XML appendContent(Object v) {
return appendContent(getElementCopied(), v);
}
/**
* Replaces each selected content node by the given value.
*
* @see #appendContent(Object)
* @see #prependContent(String, Object)
* @see #setContent(String, Object)
* @see #set(String, AttrNode)
*/
public XML set(String xpath, Object v) {
return set(XMLNavigator.selectNodes(follow(this), xpath, false), v);
}
// TODO: setElementByID
/**
* Sets the content of each selected element node to the given value.
*
* @see #appendContent(Object)
* @see #prependContent(String, Object)
* @see #set(String, Object)
*/
public XML setContent(String xpath, Object v) {
return setContent(XMLNavigator.selectElements(follow(this), xpath, false), v);
}
// TODO: setContentOfElementByID
/**
* Sets the content of this element to the given value.
*
* @throws ClassCastException if this is not an element node
* @see #appendContent(String, Object)
* @see #prependContent(Object)
*/
public XML setContent(Object v) {
return setContent(getElementCopied(), v);
}
/**
* Returns the current element node as a navigator result.
*
* @throws ClassCastException if this is not an element node
*/
private XMLNavigator.ElementListResult getElementCopied() {
Element e = asElement();
e = e.copy();
List<Element> selected = new ArrayList<Element>();
selected.add(e);
return new XMLNavigator.ElementListResult(e, selected);
}
private Node getFirstElementOrText() {
for (XML x = this; x != null; x = x.getNextSibling()) {
if (x.isElement() || x.isText()) {
return x;
}
}
throw new XMLXPathException("no element or text");
}
private XML set(XMLNavigator.ElementListResult res, AttrNode a) {
for (Element e : res.getElements()) {
e.forward(e.copy(a.copy(removeAttrIfFound(a, e.getFirstAttr()))));
}
return follow(res.getInitial());
}
private AttrNode removeAttrIfFound(AttrNode a, AttrNode first) {
boolean found = false;
for (AttrNode f = first; f != null; f = f.getNextAttr()) {
if (sameAttrName(a, f)) {
found = true;
break;
}
}
if (!found) {
return first;
} else {
return removeAttr(a, first);
}
}
private AttrNode removeAttr(AttrNode a, AttrNode first) {
if (sameAttrName(a, first)) {
return first.getNextAttr();
} else {
return first.copy(removeAttr(a, first.getNextAttr()));
}
}
private boolean sameAttrName(AttrNode a, AttrNode b) {
return ((a.getNamespace() == null && b.getNamespace() == null)
|| (a.getNamespace() != null && b.getNamespace() != null && a.getNamespace().equals(b.getNamespace())))
&& (a.getLocalName().equals(b.getLocalName()));
}
private XML set(XMLNavigator.ElementListResult res, NamespaceDecl d) {
for (Element e : res.getElements()) {
e.forward(e.copy(d.copy(e.getFirstNamespaceDecl())));
}
return follow(res.getInitial());
}
private XML prependContent(XMLNavigator.ElementListResult res, Object v) {
for (Element e : res.getElements()) {
e.forward(e.copy(concatXML(toXML(v), e.getRealFirstChild()), e.getRealNextSibling()));
}
return follow(res.getInitial());
}
private XML appendContent(XMLNavigator.ElementListResult res, Object v) {
for (Element e : res.getElements()) {
e.forward(e.copy(concatXML(e.getRealFirstChild(), toXML(v)), e.getRealNextSibling()));
}
return follow(res.getInitial());
}
private XML set(XMLNavigator.NodeListResult res, Object v) {
for (Node n : res.getNodes()) {
if (n.isTemplate()) {
TempNode t = n.asTemplate();
n.forward(concatXML(toXML(v), t.getRealNextSibling()));
}
}
return follow(res.getInitial());
}
private XML setContent(XMLNavigator.ElementListResult res, Object v) {
for (Element e : res.getElements()) {
e.forward(e.copy(toXML(v), e.getRealNextSibling()));
}
return follow(res.getInitial());
}
// TODO: add 'group' operation (like XACT 2.0)
// TODO: getID, setID, setContentOfID ? (desugar to id('...')...)
}