From 3c9d71162176339d50f941a49ba546ce20b7dfe4 Mon Sep 17 00:00:00 2001 From: Vicne Date: Tue, 21 Feb 2023 03:19:31 +0100 Subject: [PATCH] Attempt to follow suggestion from https://issues.apache.org/jira/browse/JXPATH-194 --- pom.xml | 5 + .../jxpath/ri/JXPathContextReferenceImpl.java | 8 + .../ri/model/jdom2/JDOMAttributeIterator.java | 121 +++ .../ri/model/jdom2/JDOMAttributePointer.java | 136 +++ .../ri/model/jdom2/JDOMNamespaceIterator.java | 109 +++ .../ri/model/jdom2/JDOMNamespacePointer.java | 135 +++ .../ri/model/jdom2/JDOMNodeIterator.java | 179 ++++ .../ri/model/jdom2/JDOMNodePointer.java | 830 ++++++++++++++++++ .../ri/model/jdom2/JDOMPointerFactory.java | 63 ++ .../jxpath/ri/model/jdom2/package-info.java | 21 + 10 files changed, 1607 insertions(+) create mode 100644 src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMAttributeIterator.java create mode 100644 src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMAttributePointer.java create mode 100644 src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMNamespaceIterator.java create mode 100644 src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMNamespacePointer.java create mode 100644 src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMNodeIterator.java create mode 100644 src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMNodePointer.java create mode 100644 src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMPointerFactory.java create mode 100644 src/main/java/org/apache/commons/jxpath/ri/model/jdom2/package-info.java diff --git a/pom.xml b/pom.xml index 1df81f347..763340fc5 100644 --- a/pom.xml +++ b/pom.xml @@ -214,6 +214,11 @@ 1.0 true + + org.jdom + jdom2 + 2.0.6.1 + commons-beanutils commons-beanutils diff --git a/src/main/java/org/apache/commons/jxpath/ri/JXPathContextReferenceImpl.java b/src/main/java/org/apache/commons/jxpath/ri/JXPathContextReferenceImpl.java index 2d7319944..69f5ce572 100644 --- a/src/main/java/org/apache/commons/jxpath/ri/JXPathContextReferenceImpl.java +++ b/src/main/java/org/apache/commons/jxpath/ri/JXPathContextReferenceImpl.java @@ -97,6 +97,14 @@ public class JXPathContextReferenceImpl extends JXPathContext { nodeFactories.add(jdomFactory); } + // JDOM2 factory is only registered if JDOM2 is on the classpath + final Object jdom2Factory = allocateConditionally( + "org.apache.commons.jxpath.ri.model.jdom2.JDOMPointerFactory", + "org.jdom2.Document"); + if (jdom2Factory != null) { + nodeFactories.add(jdom2Factory); + } + // DynaBean factory is only registered if BeanUtils are on the classpath final Object dynaBeanFactory = allocateConditionally( diff --git a/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMAttributeIterator.java b/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMAttributeIterator.java new file mode 100644 index 000000000..5f3ca0429 --- /dev/null +++ b/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMAttributeIterator.java @@ -0,0 +1,121 @@ +/* + * 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.jdom2; + +import org.apache.commons.jxpath.ri.QName; +import org.apache.commons.jxpath.ri.model.NodeIterator; +import org.apache.commons.jxpath.ri.model.NodePointer; +import org.jdom2.Attribute; +import org.jdom2.Element; +import org.jdom2.Namespace; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * An iterator of attributes of a DOM Node. + */ +public class JDOMAttributeIterator implements NodeIterator { + private NodePointer parent; + private List attributes; + private int position = 0; + + /** + * Create a new JDOMAttributeIterator. + * @param parent pointer + * @param name test + */ + public JDOMAttributeIterator(final NodePointer parent, final QName name) { + this.parent = parent; + if (parent.getNode() instanceof Element) { + final Element element = (Element) parent.getNode(); + final String prefix = name.getPrefix(); + Namespace ns = null; + if (prefix != null) { + if (prefix.equals("xml")) { + ns = Namespace.XML_NAMESPACE; + } + else { + final String uri = parent.getNamespaceResolver().getNamespaceURI(prefix); + if (uri != null) { + ns = Namespace.getNamespace(prefix, uri); + } + if (ns == null) { + // TBD: no attributes + attributes = Collections.EMPTY_LIST; + return; + } + } + } + else { + ns = Namespace.NO_NAMESPACE; + } + + final String lname = name.getName(); + if (!lname.equals("*")) { + attributes = new ArrayList(); + final Attribute attr = element.getAttribute(lname, ns); + if (attr != null) { + attributes.add(attr); + } + } + else { + attributes = new ArrayList(); + final List allAttributes = element.getAttributes(); + for (int i = 0; i < allAttributes.size(); i++) { + final Attribute attr = (Attribute) allAttributes.get(i); + if (ns == Namespace.NO_NAMESPACE + || attr.getNamespace().equals(ns)) { + attributes.add(attr); + } + } + } + } + } + + @Override + public NodePointer getNodePointer() { + if (position == 0) { + if (!setPosition(1)) { + return null; + } + position = 0; + } + int index = position - 1; + if (index < 0) { + index = 0; + } + return new JDOMAttributePointer( + parent, + (Attribute) attributes.get(index)); + } + + @Override + public int getPosition() { + return position; + } + + @Override + public boolean setPosition(final int position) { + if (attributes == null) { + return false; + } + this.position = position; + return position >= 1 && position <= attributes.size(); + } +} diff --git a/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMAttributePointer.java b/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMAttributePointer.java new file mode 100644 index 000000000..4b46def61 --- /dev/null +++ b/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMAttributePointer.java @@ -0,0 +1,136 @@ +/* + * 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.jdom2; + +import org.apache.commons.jxpath.ri.QName; +import org.apache.commons.jxpath.ri.model.NodePointer; +import org.apache.commons.jxpath.util.TypeUtils; +import org.jdom2.Attribute; + +/** + * A Pointer that points to a DOM node. + */ +public class JDOMAttributePointer extends NodePointer { + private final Attribute attr; + + private static final long serialVersionUID = 8896050354479644028L; + + /** + * Create a JDOMAttributePointer. + * @param parent NodePointer parent + * @param attr JDOM Attribute + */ + public JDOMAttributePointer(final NodePointer parent, final Attribute attr) { + super(parent); + this.attr = attr; + } + + @Override + public QName getName() { + return new QName( + JDOMNodePointer.getPrefix(attr), + JDOMNodePointer.getLocalName(attr)); + } + + @Override + public String getNamespaceURI() { + String uri = attr.getNamespaceURI(); + if (uri != null && uri.equals("")) { + uri = null; + } + return uri; + } + + @Override + public Object getValue() { + return attr.getValue(); + } + + @Override + public Object getBaseValue() { + return attr; + } + + @Override + public boolean isCollection() { + return false; + } + + @Override + public int getLength() { + return 1; + } + + @Override + public Object getImmediateNode() { + return attr; + } + + @Override + public boolean isActual() { + return true; + } + + @Override + public boolean isLeaf() { + return true; + } + + @Override + public void setValue(final Object value) { + attr.setValue((String) TypeUtils.convert(value, String.class)); + } + + @Override + public void remove() { + attr.getParent().removeAttribute(attr); + } + + @Override + public String asPath() { + final StringBuffer buffer = new StringBuffer(); + if (parent != null) { + buffer.append(parent.asPath()); + if (buffer.length() == 0 + || buffer.charAt(buffer.length() - 1) != '/') { + buffer.append('/'); + } + } + buffer.append('@'); + buffer.append(getName()); + return buffer.toString(); + } + + @Override + public int hashCode() { + return System.identityHashCode(attr); + } + + @Override + public boolean equals(final Object object) { + return object == this || object instanceof JDOMAttributePointer + && ((JDOMAttributePointer) object).attr == attr; + } + + @Override + public int compareChildNodePointers( + final NodePointer pointer1, + final NodePointer pointer2) { + // Won't happen - attributes don't have children + return 0; + } +} diff --git a/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMNamespaceIterator.java b/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMNamespaceIterator.java new file mode 100644 index 000000000..94c12c80a --- /dev/null +++ b/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMNamespaceIterator.java @@ -0,0 +1,109 @@ +/* + * 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.jdom2; + +import org.apache.commons.jxpath.ri.model.NodeIterator; +import org.apache.commons.jxpath.ri.model.NodePointer; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.Namespace; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * An iterator of namespaces of a DOM Node. + */ +public class JDOMNamespaceIterator implements NodeIterator { + private final NodePointer parent; + private List namespaces; + private Set prefixes; + private int position = 0; + + /** + * Create a new JDOMNamespaceIterator. + * @param parent the parent NodePointer. + */ + public JDOMNamespaceIterator(final NodePointer parent) { + this.parent = parent; + Object node = parent.getNode(); + if (node instanceof Document) { + node = ((Document) node).getRootElement(); + } + if (node instanceof Element) { + namespaces = new ArrayList(); + prefixes = new HashSet(); + collectNamespaces((Element) node); + } + } + + /** + * Collect the namespaces from a JDOM Element. + * @param element the source Element + */ + private void collectNamespaces(final Element element) { + Namespace ns = element.getNamespace(); + if (ns != null && !prefixes.contains(ns.getPrefix())) { + namespaces.add(ns); + prefixes.add(ns.getPrefix()); + } + final List others = element.getAdditionalNamespaces(); + for (int i = 0; i < others.size(); i++) { + ns = (Namespace) others.get(i); + if (ns != null && !prefixes.contains(ns.getPrefix())) { + namespaces.add(ns); + prefixes.add(ns.getPrefix()); + } + } + final Object elementParent = element.getParent(); + if (elementParent instanceof Element) { + collectNamespaces((Element) elementParent); + } + } + + @Override + public NodePointer getNodePointer() { + if (position == 0) { + if (!setPosition(1)) { + return null; + } + position = 0; + } + int index = position - 1; + if (index < 0) { + index = 0; + } + final Namespace ns = (Namespace) namespaces.get(index); + return new JDOMNamespacePointer(parent, ns.getPrefix(), ns.getURI()); + } + + @Override + public int getPosition() { + return position; + } + + @Override + public boolean setPosition(final int position) { + if (namespaces == null) { + return false; + } + this.position = position; + return position >= 1 && position <= namespaces.size(); + } +} diff --git a/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMNamespacePointer.java b/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMNamespacePointer.java new file mode 100644 index 000000000..565a6d158 --- /dev/null +++ b/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMNamespacePointer.java @@ -0,0 +1,135 @@ +/* + * 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.jdom2; + +import org.apache.commons.jxpath.ri.QName; +import org.apache.commons.jxpath.ri.model.NodePointer; + +/** + * Represents a namespace node. + */ +public class JDOMNamespacePointer extends NodePointer { + private final String prefix; + private String namespaceURI; + + private static final long serialVersionUID = 7935311686545862379L; + + /** + * Create a new JDOMNamespacePointer. + * @param parent parent pointer + * @param prefix ns prefix + */ + public JDOMNamespacePointer(final NodePointer parent, final String prefix) { + super(parent); + this.prefix = prefix; + } + + /** + * Create a new JDOMNamespacePointer. + * @param parent parent pointer + * @param prefix ns prefix + * @param namespaceURI ns URI + */ + public JDOMNamespacePointer( + final NodePointer parent, + final String prefix, + final String namespaceURI) { + super(parent); + this.prefix = prefix; + this.namespaceURI = namespaceURI; + } + + @Override + public QName getName() { + return new QName(prefix); + } + + @Override + public Object getBaseValue() { + return null; + } + + @Override + public boolean isCollection() { + return false; + } + + @Override + public int getLength() { + return 1; + } + + @Override + public Object getImmediateNode() { + return getNamespaceURI(); + } + + @Override + public String getNamespaceURI() { + if (namespaceURI == null) { + namespaceURI = parent.getNamespaceURI(prefix); + } + return namespaceURI; + } + + @Override + public boolean isLeaf() { + return true; + } + + /** + * Throws UnsupportedOperationException. + * @param value Object value to set + */ + @Override + public void setValue(final Object value) { + throw new UnsupportedOperationException("Cannot modify a namespace"); + } + + @Override + public String asPath() { + final StringBuffer buffer = new StringBuffer(); + if (parent != null) { + buffer.append(parent.asPath()); + if (buffer.length() == 0 + || buffer.charAt(buffer.length() - 1) != '/') { + buffer.append('/'); + } + } + buffer.append("namespace::"); + buffer.append(prefix); + return buffer.toString(); + } + + @Override + public int hashCode() { + return prefix.hashCode(); + } + + @Override + public boolean equals(final Object object) { + return object == this || object instanceof JDOMNamespacePointer && prefix.equals(((JDOMNamespacePointer) object).prefix); + } + + @Override + public int compareChildNodePointers( + final NodePointer pointer1, + final NodePointer pointer2) { + // Won't happen - namespaces don't have children + return 0; + } +} diff --git a/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMNodeIterator.java b/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMNodeIterator.java new file mode 100644 index 000000000..f1966d4cb --- /dev/null +++ b/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMNodeIterator.java @@ -0,0 +1,179 @@ +/* + * 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.jdom2; + +import org.apache.commons.jxpath.ri.compiler.NodeTest; +import org.apache.commons.jxpath.ri.model.NodeIterator; +import org.apache.commons.jxpath.ri.model.NodePointer; +import org.jdom2.Document; +import org.jdom2.Element; + +import java.util.Collections; +import java.util.List; + +/** + * An iterator of children of a JDOM Node. + */ +public class JDOMNodeIterator implements NodeIterator { + private final NodePointer parent; + private final NodeTest nodeTest; + + private final boolean reverse; + private int position = 0; + private int index = 0; + private List children; + private Object child; + + /** + * Create a new JDOMNodeIterator. + * @param parent pointer + * @param nodeTest test + * @param reverse whether to iterate in reverse + * @param startWith starting pointer + */ + public JDOMNodeIterator( + final NodePointer parent, final NodeTest nodeTest, + final boolean reverse, final NodePointer startWith) { + this.parent = parent; + if (startWith != null) { + this.child = startWith.getNode(); + } + // TBD: optimize me for different node tests + final Object node = parent.getNode(); + if (node instanceof Document) { + this.children = ((Document) node).getContent(); + } + else if (node instanceof Element) { + this.children = ((Element) node).getContent(); + } + else { + this.children = Collections.EMPTY_LIST; + } + this.nodeTest = nodeTest; + this.reverse = reverse; + } + + @Override + public NodePointer getNodePointer() { + if (child == null) { + if (!setPosition(1)) { + return null; + } + position = 0; + } + + return new JDOMNodePointer(parent, child); + } + + @Override + public int getPosition() { + return position; + } + + @Override + public boolean setPosition(final int position) { + while (this.position < position) { + if (!next()) { + return false; + } + } + while (this.position > position) { + if (!previous()) { + return false; + } + } + return true; + } + + /** + * This is actually never invoked during the normal evaluation + * of xpaths - an iterator is always going forward, never backwards. + * So, this is implemented only for completeness and perhaps for + * those who use these iterators outside of XPath evaluation. + * @return boolean + */ + private boolean previous() { + position--; + if (!reverse) { + while (--index >= 0) { + child = children.get(index); + if (testChild()) { + return true; + } + } + } + else { + for (; index < children.size(); index++) { + child = children.get(index); + if (testChild()) { + return true; + } + } + } + return false; + } + + /** + * Iterate to next pointer. + * @return whether valid + */ + private boolean next() { + position++; + if (!reverse) { + if (position == 1) { + index = 0; + if (child != null) { + index = children.indexOf(child) + 1; + } + } + else { + index++; + } + for (; index < children.size(); index++) { + child = children.get(index); + if (testChild()) { + return true; + } + } + return false; + } + if (position == 1) { + index = children.size() - 1; + if (child != null) { + index = children.indexOf(child) - 1; + } + } + else { + index--; + } + for (; index >= 0; index--) { + child = children.get(index); + if (testChild()) { + return true; + } + } + return false; + } + + /** + * Test a child node. + * @return whether test passes. + */ + private boolean testChild() { + return JDOMNodePointer.testNode(parent, child, nodeTest); + } +} diff --git a/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMNodePointer.java b/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMNodePointer.java new file mode 100644 index 000000000..2e2115df9 --- /dev/null +++ b/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMNodePointer.java @@ -0,0 +1,830 @@ +/* + * 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.jdom2; + +import org.apache.commons.jxpath.JXPathAbstractFactoryException; +import org.apache.commons.jxpath.JXPathContext; +import org.apache.commons.jxpath.JXPathException; +import org.apache.commons.jxpath.ri.Compiler; +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.compiler.ProcessingInstructionTest; +import org.apache.commons.jxpath.ri.model.NodeIterator; +import org.apache.commons.jxpath.ri.model.NodePointer; +import org.apache.commons.jxpath.util.TypeUtils; +import org.jdom2.*; + +import java.util.List; +import java.util.Locale; + +/** + * A Pointer that points to a DOM node. + */ +public class JDOMNodePointer extends NodePointer { + private static final long serialVersionUID = -6346532297491082651L; + + private final Object node; + private String id; + private NamespaceResolver localNamespaceResolver; + + /** XML ns uri */ + public static final String XML_NAMESPACE_URI = + "http://www.w3.org/XML/1998/namespace"; + + /** XMLNS ns uri */ + public static final String XMLNS_NAMESPACE_URI = + "http://www.w3.org/2000/xmlns/"; + + /** + * Create a new JDOMNodePointer. + * @param node pointed + * @param locale Locale + */ + public JDOMNodePointer(final Object node, final Locale locale) { + super(null, locale); + this.node = node; + } + + /** + * Create a new JDOMNodePointer. + * @param node pointed + * @param locale Locale + * @param id String id + */ + public JDOMNodePointer(final Object node, final Locale locale, final String id) { + super(null, locale); + this.node = node; + this.id = id; + } + + /** + * Create a new JDOMNodePointer. + * @param parent NodePointer + * @param node pointed + */ + public JDOMNodePointer(final NodePointer parent, final Object node) { + super(parent); + this.node = node; + } + + @Override + public NodeIterator childIterator( + final NodeTest test, + final boolean reverse, + final NodePointer startWith) { + return new JDOMNodeIterator(this, test, reverse, startWith); + } + + @Override + public NodeIterator attributeIterator(final QName name) { + return new JDOMAttributeIterator(this, name); + } + + @Override + public NodeIterator namespaceIterator() { + return new JDOMNamespaceIterator(this); + } + + @Override + public NodePointer namespacePointer(final String prefix) { + return new JDOMNamespacePointer(this, prefix); + } + + @Override + public String getNamespaceURI() { + return getNamespaceURI(node); + } + + /** + * Get the ns uri of the specified node. + * @param node Node to check + * @return String + */ + private static String getNamespaceURI(final Object node) { + if (node instanceof Element) { + final Element element = (Element) node; + String ns = element.getNamespaceURI(); + if ("".equals(ns)) { + ns = null; + } + return ns; + } + return null; + } + + @Override + public synchronized NamespaceResolver getNamespaceResolver() { + if (localNamespaceResolver == null) { + localNamespaceResolver = new NamespaceResolver(super.getNamespaceResolver()); + localNamespaceResolver.setNamespaceContextPointer(this); + } + return localNamespaceResolver; + } + + @Override + public String getNamespaceURI(final String prefix) { + if (prefix.equals("xml")) { + return Namespace.XML_NAMESPACE.getURI(); + } + Element element = null; + if (node instanceof Document) { + element = ((Document) node).getRootElement(); + } + if (node instanceof Element) { + element = (Element) node; + } + if (element == null) { + return null; + } + final Namespace ns = element.getNamespace(prefix); + return ns == null ? null : ns.getURI(); + } + + @Override + public int compareChildNodePointers( + final NodePointer pointer1, + final NodePointer pointer2) { + final Object node1 = pointer1.getBaseValue(); + final Object node2 = pointer2.getBaseValue(); + if (node1 == node2) { + return 0; + } + + if (node1 instanceof Attribute && !(node2 instanceof Attribute)) { + return -1; + } + if ( + !(node1 instanceof Attribute) && node2 instanceof Attribute) { + return 1; + } + if ( + node1 instanceof Attribute && node2 instanceof Attribute) { + final List list = ((Element) getNode()).getAttributes(); + final int length = list.size(); + for (int i = 0; i < length; i++) { + final Object n = list.get(i); + if (n == node1) { + return -1; + } + else if (n == node2) { + return 1; + } + } + return 0; // Should not happen + } + + if (!(node instanceof Element)) { + throw new RuntimeException( + "JXPath internal error: " + + "compareChildNodes called for " + + node); + } + + final List children = ((Element) node).getContent(); + final int length = children.size(); + for (int i = 0; i < length; i++) { + final Object n = children.get(i); + if (n == node1) { + return -1; + } + if (n == node2) { + return 1; + } + } + + return 0; + } + + @Override + public Object getBaseValue() { + return node; + } + + @Override + public boolean isCollection() { + return false; + } + + @Override + public int getLength() { + return 1; + } + + @Override + public boolean isLeaf() { + if (node instanceof Element) { + return ((Element) node).getContent().size() == 0; + } + if (node instanceof Document) { + return ((Document) node).getContent().size() == 0; + } + return true; + } + + @Override + public QName getName() { + String ns = null; + String ln = null; + if (node instanceof Element) { + ns = ((Element) node).getNamespacePrefix(); + if (ns != null && ns.equals("")) { + ns = null; + } + ln = ((Element) node).getName(); + } + else if (node instanceof ProcessingInstruction) { + ln = ((ProcessingInstruction) node).getTarget(); + } + return new QName(ns, ln); + } + + @Override + public Object getImmediateNode() { + return node; + } + + @Override + public Object getValue() { + if (node instanceof Element) { + final StringBuffer buf = new StringBuffer(); + for (final NodeIterator children = childIterator(null, false, null); children.setPosition(children.getPosition() + 1);) { + final NodePointer ptr = children.getNodePointer(); + if (ptr.getImmediateNode() instanceof Element || ptr.getImmediateNode() instanceof Text) { + buf.append(ptr.getValue()); + } + } + return buf.toString(); + } + if (node instanceof Comment) { + String text = ((Comment) node).getText(); + if (text != null) { + text = text.trim(); + } + return text; + } + String result = null; + if (node instanceof Text) { + result = ((Text) node).getText(); + } + if (node instanceof ProcessingInstruction) { + result = ((ProcessingInstruction) node).getData(); + } + final boolean trim = !"preserve".equals(findEnclosingAttribute(node, "space", Namespace.XML_NAMESPACE)); + return result != null && trim ? result.trim() : result; + } + + @Override + public void setValue(final Object value) { + if (node instanceof Text) { + final String string = (String) TypeUtils.convert(value, String.class); + if (string != null && !string.equals("")) { + ((Text) node).setText(string); + } + else { + nodeParent(node).removeContent((Text) node); + } + } + else { + final Element element = (Element) node; + element.getContent().clear(); + + if (value instanceof Element) { + final Element valueElement = (Element) value; + addContent(valueElement.getContent()); + } + else if (value instanceof Document) { + final Document valueDocument = (Document) value; + addContent(valueDocument.getContent()); + } + else if (value instanceof Text || value instanceof CDATA) { + final String string = ((Text) value).getText(); + element.addContent(new Text(string)); + } + else if (value instanceof ProcessingInstruction) { + final ProcessingInstruction pi = + (ProcessingInstruction) ((ProcessingInstruction) value) + .clone(); + element.addContent(pi); + } + else if (value instanceof Comment) { + final Comment comment = (Comment) ((Comment) value).clone(); + element.addContent(comment); + } + else { + final String string = (String) TypeUtils.convert(value, String.class); + if (string != null && !string.equals("")) { + element.addContent(new Text(string)); + } + } + } + } + + /** + * Add the specified content to this element. + * @param content List + */ + private void addContent(final List content) { + final Element element = (Element) node; + final int count = content.size(); + + for (int i = 0; i < count; i++) { + Object child = content.get(i); + if (child instanceof Element) { + child = ((Element) child).clone(); + element.addContent((Element) child); + } + else if (child instanceof Text) { + child = ((Text) child).clone(); + element.addContent((Text) child); + } + else if (node instanceof CDATA) { + child = ((CDATA) child).clone(); + element.addContent((CDATA) child); + } + else if (node instanceof ProcessingInstruction) { + child = ((ProcessingInstruction) child).clone(); + element.addContent((ProcessingInstruction) child); + } + else if (node instanceof Comment) { + child = ((Comment) child).clone(); + element.addContent((Comment) child); + } + } + } + + @Override + public boolean testNode(final NodeTest test) { + return testNode(this, node, test); + } + + /** + * Execute test against node on behalf of pointer. + * @param pointer Pointer + * @param node to test + * @param test to execute + * @return true if node passes test + */ + public static boolean testNode( + final NodePointer pointer, + final Object node, + final NodeTest test) { + if (test == null) { + return true; + } + if (test instanceof NodeNameTest) { + if (!(node instanceof Element)) { + return false; + } + + final NodeNameTest nodeNameTest = (NodeNameTest) test; + final QName testName = nodeNameTest.getNodeName(); + final String namespaceURI = nodeNameTest.getNamespaceURI(); + final boolean wildcard = nodeNameTest.isWildcard(); + final String testPrefix = testName.getPrefix(); + if (wildcard && testPrefix == null) { + return true; + } + if (wildcard + || testName.getName() + .equals(JDOMNodePointer.getLocalName(node))) { + final String nodeNS = JDOMNodePointer.getNamespaceURI(node); + return equalStrings(namespaceURI, nodeNS) || nodeNS == null + && equalStrings(testPrefix, getPrefix(node)); + } + return false; + } + if (test instanceof NodeTypeTest) { + switch (((NodeTypeTest) test).getNodeType()) { + case Compiler.NODE_TYPE_NODE : + return true; + case Compiler.NODE_TYPE_TEXT : + return node instanceof Text || node instanceof CDATA; + case Compiler.NODE_TYPE_COMMENT : + return node instanceof Comment; + case Compiler.NODE_TYPE_PI : + return node instanceof ProcessingInstruction; + default: + return false; + } + } + if (test instanceof ProcessingInstructionTest && node instanceof ProcessingInstruction) { + final String testPI = ((ProcessingInstructionTest) test).getTarget(); + final String nodePI = ((ProcessingInstruction) node).getTarget(); + return testPI.equals(nodePI); + } + return false; + } + + /** + * Learn whether two strings are == or .equals() + * @param s1 string 1 + * @param s2 string 2 + * @return true if equal + */ + private static boolean equalStrings(String s1, String s2) { + if (s1 == s2) { + return true; + } + s1 = s1 == null ? "" : s1.trim(); + s2 = s2 == null ? "" : s2.trim(); + return s1.equals(s2); + } + + /** + * Get the prefix from a given node. + * @param node to check + * @return String + */ + public static String getPrefix(final Object node) { + if (node instanceof Element) { + final String prefix = ((Element) node).getNamespacePrefix(); + return prefix == null || prefix.equals("") ? null : prefix; + } + if (node instanceof Attribute) { + final String prefix = ((Attribute) node).getNamespacePrefix(); + return prefix == null || prefix.equals("") ? null : prefix; + } + return null; + } + + /** + * Get the local name of the specified node. + * @param node to check + * @return String local name + */ + public static String getLocalName(final Object node) { + if (node instanceof Element) { + return ((Element) node).getName(); + } + if (node instanceof Attribute) { + return ((Attribute) node).getName(); + } + return null; + } + + /** + * Returns true if the xml:lang attribute for the current node + * or its parent has the specified prefix lang. + * If no node has this prefix, calls super.isLanguage(lang). + * @param lang to compare + * @return true if this element uses the specified language. + */ + @Override + public boolean isLanguage(final String lang) { + final String current = getLanguage(); + return current == null ? super.isLanguage(lang) : current.toUpperCase( + Locale.ENGLISH).startsWith(lang.toUpperCase(Locale.ENGLISH)); + } + + /** + * Get the language of this element. + * @return String language + */ + protected String getLanguage() { + return findEnclosingAttribute(node, "lang", Namespace.XML_NAMESPACE); + } + + /** + * Find the nearest occurrence of the specified attribute + * on the specified and enclosing elements. + * @param n current node + * @param attrName attribute name + * @param ns Namespace + * @return attribute value + */ + protected static String findEnclosingAttribute(Object n, final String attrName, final Namespace ns) { + while (n != null) { + if (n instanceof Element) { + final Element e = (Element) n; + final String attr = e.getAttributeValue(attrName, ns); + if (attr != null && !attr.equals("")) { + return attr; + } + } + n = nodeParent(n); + } + return null; + } + + /** + * Get the parent of the specified node. + * @param node to check + * @return parent Element + */ + private static Element nodeParent(final Object node) { + if (node instanceof Element) { + final Object parent = ((Element) node).getParent(); + return parent instanceof Element ? (Element) parent : null; + } + if (node instanceof Text) { + return (Element) ((Text) node).getParent(); + } + if (node instanceof CDATA) { + return (Element) ((CDATA) node).getParent(); + } + if (node instanceof ProcessingInstruction) { + return (Element) ((ProcessingInstruction) node).getParent(); + } + if (node instanceof Comment) { + return (Element) ((Comment) node).getParent(); + } + return null; + } + + @Override + public NodePointer createChild( + final JXPathContext context, + final QName name, + int index) { + if (index == WHOLE_COLLECTION) { + index = 0; + } + final boolean success = + getAbstractFactory(context).createObject( + context, + this, + node, + name.toString(), + index); + if (success) { + NodeTest nodeTest; + final String prefix = name.getPrefix(); + final String namespaceURI = prefix == null ? null : context + .getNamespaceURI(prefix); + nodeTest = new NodeNameTest(name, namespaceURI); + + final NodeIterator it = + childIterator(nodeTest, false, null); + if (it != null && it.setPosition(index + 1)) { + return it.getNodePointer(); + } + } + throw new JXPathAbstractFactoryException("Factory could not create " + + "a child node for path: " + asPath() + "/" + name + "[" + + (index + 1) + "]"); + } + + @Override + public NodePointer createChild( + final JXPathContext context, final QName name, final int index, final Object value) { + final NodePointer ptr = createChild(context, name, index); + ptr.setValue(value); + return ptr; + } + + @Override + public NodePointer createAttribute(final JXPathContext context, final QName name) { + if (!(node instanceof Element)) { + return super.createAttribute(context, name); + } + + final Element element = (Element) node; + final String prefix = name.getPrefix(); + if (prefix != null) { + final String namespaceUri = getNamespaceResolver().getNamespaceURI(prefix); + if (namespaceUri == null) { + throw new JXPathException( + "Unknown namespace prefix: " + prefix); + } + final Namespace ns = Namespace.getNamespace(prefix, namespaceUri); + final Attribute attr = element.getAttribute(name.getName(), ns); + if (attr == null) { + element.setAttribute(name.getName(), "", ns); + } + } + else { + final Attribute attr = element.getAttribute(name.getName()); + if (attr == null) { + element.setAttribute(name.getName(), ""); + } + } + final NodeIterator it = attributeIterator(name); + it.setPosition(1); + return it.getNodePointer(); + } + + @Override + public void remove() { + final Element parent = nodeParent(node); + if (parent == null) { + throw new JXPathException("Cannot remove root JDOM node"); + } + parent.getContent().remove(node); + } + + @Override + public String asPath() { + if (id != null) { + return "id('" + escape(id) + "')"; + } + + final StringBuffer buffer = new StringBuffer(); + if (parent != null) { + buffer.append(parent.asPath()); + } + if (node instanceof Element) { + // If the parent pointer is not a JDOMNodePointer, it is + // the parent's responsibility to produce the node test part + // of the path + if (parent instanceof JDOMNodePointer) { + if (buffer.length() == 0 + || buffer.charAt(buffer.length() - 1) != '/') { + buffer.append('/'); + } + final String nsURI = getNamespaceURI(); + final String ln = JDOMNodePointer.getLocalName(node); + + if (nsURI == null) { + buffer.append(ln); + buffer.append('['); + buffer.append(getRelativePositionByQName()).append(']'); + } + else { + final String prefix = getNamespaceResolver().getPrefix(nsURI); + if (prefix != null) { + buffer.append(prefix); + buffer.append(':'); + buffer.append(ln); + buffer.append('['); + buffer.append(getRelativePositionByQName()); + buffer.append(']'); + } + else { + buffer.append("node()"); + buffer.append('['); + buffer.append(getRelativePositionOfElement()); + buffer.append(']'); + } + } + + } + } + else if (node instanceof Text || node instanceof CDATA) { + buffer.append("/text()"); + buffer.append('[').append(getRelativePositionOfTextNode()).append( + ']'); + } + else if (node instanceof ProcessingInstruction) { + buffer.append("/processing-instruction(\'").append(((ProcessingInstruction) node).getTarget()).append( + "')"); + buffer.append('[').append(getRelativePositionOfPI()).append( + ']'); + } + return buffer.toString(); + } + + /** + * Get relative position of this among like-named siblings. + * @return 1..n + */ + private int getRelativePositionByQName() { + if (node instanceof Element) { + final Object parent = ((Element) node).getParent(); + if (!(parent instanceof Element)) { + return 1; + } + + final List children = ((Element) parent).getContent(); + int count = 0; + for (final Object child : children) { + if (child instanceof Element && matchesQName((Element) child)) { + count++; + } + if (child == node) { + break; + } + } + return count; + } + return 1; + } + + private boolean matchesQName(final Element element) { + if (getNamespaceURI() != null) { + final String ns = getNamespaceURI(element); + if (ns == null || !ns.equals(getNamespaceURI())) { + return false; + } + } + return element.getName().equals(((Element) node).getName()); + } + + /** + * Get relative position of this among all siblings. + * @return 1..n + */ + private int getRelativePositionOfElement() { + final Object parent = ((Element) node).getParent(); + if (parent == null) { + return 1; + } + List children; + if (parent instanceof Element) { + children = ((Element) parent).getContent(); + } + else { + children = ((Document) parent).getContent(); + } + int count = 0; + for (final Object child : children) { + if (child instanceof Element) { + count++; + } + if (child == node) { + break; + } + } + return count; + } + + /** + * Get the relative position of this among sibling text nodes. + * @return 1..n + */ + private int getRelativePositionOfTextNode() { + Element parent; + if (node instanceof Text) { + parent = (Element) ((Text) node).getParent(); + } + else { + parent = (Element) ((CDATA) node).getParent(); + } + if (parent == null) { + return 1; + } + final List children = parent.getContent(); + int count = 0; + for (final Object child : children) { + if (child instanceof Text || child instanceof CDATA) { + count++; + } + if (child == node) { + break; + } + } + return count; + } + + /** + * Get the relative position of this among same-target processing instruction siblings. + * @return 1..n + */ + private int getRelativePositionOfPI() { + final String target = ((ProcessingInstruction) node).getTarget(); + final Element parent = (Element) ((ProcessingInstruction) node).getParent(); + if (parent == null) { + return 1; + } + final List children = parent.getContent(); + int count = 0; + for (final Object child : children) { + if (child instanceof ProcessingInstruction + && (target == null + || target.equals( + ((ProcessingInstruction) child).getTarget()))) { + count++; + } + if (child == node) { + break; + } + } + return count; + } + + @Override + public int hashCode() { + return node.hashCode(); + } + + @Override + public boolean equals(final Object object) { + if (object == this) { + return true; + } + + if (!(object instanceof JDOMNodePointer)) { + return false; + } + + final JDOMNodePointer other = (JDOMNodePointer) object; + return node == other.node; + } + +} diff --git a/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMPointerFactory.java b/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMPointerFactory.java new file mode 100644 index 000000000..5e3585ff3 --- /dev/null +++ b/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/JDOMPointerFactory.java @@ -0,0 +1,63 @@ +/* + * 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.jdom2; + +import org.apache.commons.jxpath.ri.QName; +import org.apache.commons.jxpath.ri.model.NodePointer; +import org.apache.commons.jxpath.ri.model.NodePointerFactory; +import org.jdom2.Document; +import org.jdom2.Element; + +import java.util.Locale; + +/** + * Implements NodePointerFactory for DOM elements. + */ +public class JDOMPointerFactory implements NodePointerFactory { + + /** factory order constant */ + public static final int JDOM_POINTER_FACTORY_ORDER = 110; + + @Override + public int getOrder() { + return JDOM_POINTER_FACTORY_ORDER; + } + + @Override + public NodePointer createNodePointer( + final QName name, final Object bean, final Locale locale) { + if (bean instanceof Document) { + return new JDOMNodePointer(bean, locale); + } + if (bean instanceof Element) { + return new JDOMNodePointer(bean, locale); + } + return null; + } + + @Override + public NodePointer createNodePointer( + final NodePointer parent, final QName name, final Object bean) { + if (bean instanceof Document) { + return new JDOMNodePointer(parent, bean); + } + if (bean instanceof Element) { + return new JDOMNodePointer(parent, bean); + } + return null; + } +} diff --git a/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/package-info.java b/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/package-info.java new file mode 100644 index 000000000..8743b3d0c --- /dev/null +++ b/src/main/java/org/apache/commons/jxpath/ri/model/jdom2/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Implementation of "model" APIs for JDOM 2.x (see jdom.org). + */ +package org.apache.commons.jxpath.ri.model.jdom2;