org.xmlbeam.AutoMap Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of xmlprojector Show documentation
Show all versions of xmlprojector Show documentation
The coolest XML library for Java around. Define typesafe views (projections) to xml. Use XPath to read and write XML. Bind XML to Java collections. Requires at least Java6, supports Java8 features and has no further runtime dependencies.
/**
* Copyright 2017 Sven Ewald
*
* Licensed 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.xmlbeam;
import java.util.AbstractMap;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xmlbeam.dom.DOMAccess;
import org.xmlbeam.evaluation.DefaultXPathEvaluator;
import org.xmlbeam.evaluation.InvocationContext;
import org.xmlbeam.exceptions.XBPathException;
import org.xmlbeam.types.TypeConverter;
import org.xmlbeam.types.XBAutoList;
import org.xmlbeam.types.XBAutoMap;
import org.xmlbeam.util.intern.DOMHelper;
import org.xmlbeam.util.intern.duplex.DuplexExpression;
import org.xmlbeam.util.intern.duplex.DuplexXPathParser;
import org.xmlbeam.util.intern.duplex.ExpressionType;
/**
* @author sven
*/
public class AutoMap extends AbstractMap implements XBAutoMap, DOMAccess {
private static final Comparator> ENTRY_COMPARATOR = new Comparator>() {
@Override
public int compare(final Entry o1, final Entry o2) {
return o1.getKey().compareTo(o2.getKey());
}
};
private final InvocationContext invocationContext;
private final Node baseNode;
private Node boundNode;
private final TypeConverter typeConverter;
private final DomChangeTracker domChangeTracker = new DomChangeTracker() {
@Override
void refresh(final boolean forWrite) throws XPathExpressionException {
if (invocationContext.getxPathExpression() == null) {
boundNode = baseNode;
return;//xPath expression is optional for maps
}
final NodeList nodes = (NodeList) invocationContext.getxPathExpression().evaluate(baseNode, XPathConstants.NODESET);
if ((nodes.getLength() == 0) && forWrite) {
boundNode = invocationContext.getDuplexExpression().ensureExistence(baseNode);
} else {
boundNode = nodes.getLength() == 0 ? null : (Element) nodes.item(0);
}
}
};
private final Class> valueType;
/**
* @param baseNode
* @param invocationContext
* @param valueType
* map value type
*/
public AutoMap(final Node baseNode, final InvocationContext invocationContext, final Class> valueType) {
this.invocationContext = invocationContext;
this.baseNode = baseNode;
this.invocationContext.getProjector().addDOMChangeListener(domChangeTracker);
this.typeConverter = invocationContext.getProjector().config().getTypeConverter();
this.valueType = valueType;
}
/**
* @see java.util.AbstractMap#clear()
*/
@Override
public void clear() {
domChangeTracker.refreshForReadIfNeeded();
if (boundNode != null) {
DOMHelper.removeAllChildren(boundNode);
}
}
/**
* @deprecated use get(CharSequence) instead. Key needs to be of CharSequence/String. No sense
* in using object in this case.
* @param path
* @return value at relative path
* @see java.util.AbstractMap#get(java.lang.Object)
*/
@Override
@Deprecated
public T get(final Object path) {
if (!(path instanceof CharSequence)) {
throw new IllegalArgumentException("parameter path must be a CharSequence containing a relative XPath expression.");
}
return get(CharSequence.class.cast(path));
}
/**
* Use given relative xpath to resolve the value.
*
* @param path
* relative xpath
* @return value in DOM tree. null if no value is present.
*/
@SuppressWarnings("unchecked")
@Override
public T get(final CharSequence path) {
return (T) get(path, invocationContext.getTargetComponentType());
}
@Override
public E get(final CharSequence path, final Class asType) {
if ((path == null) || (path.length() == 0)) {
throw new IllegalArgumentException("Parameter path must not be empty or null");
}
domChangeTracker.refreshForReadIfNeeded();
if (boundNode == null) {
// we need not to evaluate, because our context node does not even exist.
return null;
}
final Document document = DOMHelper.getOwnerDocumentFor(baseNode);
final DuplexExpression duplexExpression = new DuplexXPathParser(invocationContext.getProjector().config().getUserDefinedNamespaceMapping()).compile(path);
try {
final XPathExpression expression = invocationContext.getProjector().config().createXPath(document).compile(duplexExpression.getExpressionAsStringWithoutFormatPatterns());
Node prevNode = (Node) expression.evaluate(boundNode, XPathConstants.NODE);
InvocationContext tempContext = new InvocationContext(invocationContext.getResolvedXPath(), invocationContext.getxPath(), expression, duplexExpression, null, asType, invocationContext.getProjector());
final E value = DefaultXPathEvaluator.convertToComponentType(tempContext, prevNode, asType);
return value;
} catch (XPathExpressionException e) {
throw new XBPathException(e, path);
}
}
/**
* Use given relative xpath to resolve the value.
*
* @param path
* relative xpath
* @return value in DOM tree. null if no value is present.
*/
@SuppressWarnings("unchecked")
@Override
public XBAutoList getList(final CharSequence path) {
return (XBAutoList) getList(path, invocationContext.getTargetComponentType());
}
@Override
public XBAutoList getList(final CharSequence path, final Class oType) {
if ((path == null) || (path.length() == 0)) {
throw new IllegalArgumentException("Parameter path must not be empty or null");
}
domChangeTracker.refreshForReadIfNeeded();
if (boundNode == null) {
// Get is a readonly operation, thus
// we can not create the context node here.
// We can not even return a writeable instance.
return AutoList.emptyList();
}
final Document document = DOMHelper.getOwnerDocumentFor(baseNode);
final DuplexExpression duplexExpression = new DuplexXPathParser(invocationContext.getProjector().config().getUserDefinedNamespaceMapping()).compile(path);
try {
final XPathExpression expression = invocationContext.getProjector().config().createXPath(document).compile(duplexExpression.getExpressionAsStringWithoutFormatPatterns());
final InvocationContext tempContext = new InvocationContext(invocationContext.getResolvedXPath(), invocationContext.getxPath(), expression, duplexExpression, null, oType, invocationContext.getProjector());
return new AutoList(boundNode, tempContext);
} catch (XPathExpressionException e) {
throw new XBPathException(e, path);
}
}
/**
* @deprecated
* @see java.util.AbstractMap#containsKey(java.lang.Object)
*/
@Deprecated
@Override
public boolean containsKey(final Object path) {
if (!(path instanceof CharSequence)) {
throw new IllegalArgumentException("parameter path must be a CharSequence containing a relative XPath expression.");
}
return containsKey(CharSequence.class.cast(path));
}
/**
* Checks existence of value at given xpath.
*
* @param path
* @return true if nonnull value exists at given path
*/
@Override
public boolean containsKey(final CharSequence path) {
return get(path) != null;
}
/**
* Like map.containsValue, but this map can not store null values.
*
* @param value
* @return true if value is found in any element or attribute
* @see java.util.AbstractMap#containsValue(java.lang.Object)
*/
@Override
public boolean containsValue(final Object value) {
if (value == null) {
return false;
}
return super.containsValue(value);
}
/**
* {@inheritDoc}
*/
@Override
public T put(final String path, final T value) {
if (path == null) {
throw new IllegalArgumentException("Parameter path must not be null");
}
if (value == null) {
return remove(path);
}
if (boundNode == null) {
// If there is no context node yet, ignore if
// we had read the dom before. We need to create it now.
domChangeTracker.domChanged();
}
domChangeTracker.refreshForWriteIfNeeded();
assert boundNode != null : "Bound node does not exist. No evaluation possible";
final Document document = DOMHelper.getOwnerDocumentFor(baseNode);
final DuplexExpression duplexExpression = new DuplexXPathParser(invocationContext.getProjector().config().getUserDefinedNamespaceMapping()).compile(path);
if ((ExpressionType.ATTRIBUTE == duplexExpression.getExpressionType()) && ProjectionInvocationHandler.isStructureChangingType(valueType)) {
throw new IllegalArgumentException("Value of type " + valueType + "can not be written to XML attributes. Choose a different xpath expression or use a different map component type");
}
try {
final XPathExpression expression = invocationContext.getProjector().config().createXPath(document).compile(duplexExpression.getExpressionAsStringWithoutFormatPatterns());
Node prevNode = (Node) expression.evaluate(boundNode, XPathConstants.NODE);
final T previousValue = DefaultXPathEvaluator.convertToComponentType(invocationContext, prevNode, invocationContext.getTargetComponentType());
if (ProjectionInvocationHandler.isStructureChangingValue(value)) {
final Element parent = duplexExpression.ensureParentExistence(boundNode);
if (Node.class.isAssignableFrom(invocationContext.getTargetComponentType())) {
if (!(value instanceof Node)) {
throw new IllegalArgumentException("Parameter value is not a DOM node.");
}
parent.appendChild((Node) value);
return previousValue;
}
if (invocationContext.getTargetComponentType().isInterface()) {
if (!(value instanceof DOMAccess)) {
throw new IllegalArgumentException("Parameter value is not a subprojection.");
}
// Dont't add the value, add a copy.
//parent.appendChild(DOMAccess.class.cast(value).getDOMNode());
DOMHelper.appendClone(parent, DOMAccess.class.cast(value).getDOMNode());
}
return previousValue;
}
assert boundNode != null : "Without bound node, there is no context where something could be created in.";
Node node = duplexExpression.ensureExistence(boundNode);
node.setTextContent(value.toString());
return previousValue;
} catch (XPathExpressionException e) {
throw new XBPathException(e, path);
}
}
/**
* @deprecated use remove(CharSequence) instead.
* @param path
* @return previous value
* @see java.util.AbstractMap#remove(java.lang.Object)
*/
@Deprecated
@Override
public T remove(final Object path) {
if (!(path instanceof CharSequence)) {
throw new IllegalArgumentException("parameter path must be a CharSequence or String containing a relative XPath expression.");
}
return remove(CharSequence.class.cast(path));
}
/**
* Remove element at relative location.
*
* @param xpath
* @return previous value.
*/
public T remove(final CharSequence xpath) {
if ((xpath == null) || (xpath.length() == 0)) {
throw new IllegalArgumentException("Parameter path must not be empty or null");
}
domChangeTracker.refreshForReadIfNeeded();
final Document document = DOMHelper.getOwnerDocumentFor(baseNode);
final DuplexExpression duplexExpression = new DuplexXPathParser(invocationContext.getProjector().config().getUserDefinedNamespaceMapping()).compile(xpath);
try {
final XPathExpression expression = invocationContext.getProjector().config().createXPath(document).compile(duplexExpression.getExpressionAsStringWithoutFormatPatterns());
Node prevNode = (Node) expression.evaluate(boundNode, XPathConstants.NODE);
if (prevNode == null) {
return null;
}
final T value = DefaultXPathEvaluator.convertToComponentType(invocationContext, prevNode, invocationContext.getTargetComponentType());
duplexExpression.deleteAllMatchingChildren(prevNode.getParentNode());
return value;
} catch (XPathExpressionException e) {
throw new XBPathException(e, xpath);
}
}
/**
* {@inheritDoc}
*/
@Override
public Set> entrySet() {
domChangeTracker.refreshForReadIfNeeded();
if (boundNode == null) {
return Collections.emptySet();
}
final Set> set = new TreeSet>(ENTRY_COMPARATOR) {
};
if ((invocationContext.getTargetComponentType().isInterface()) || (Node.class.isAssignableFrom(invocationContext.getTargetComponentType()))) {
collectChildren(set, boundNode, ".");
} else {
collectChildrenValues(set, boundNode, ".");
}
return set;
}
private void collectChildren(final Set> set, final Node n, final String path) {
if (n.getNodeType() == Node.ATTRIBUTE_NODE) {
return;
}
// NodeList childNodes = (n.getNodeType()==Node.DOCUMENT_NODE) ? ((Document)n).getd n.getChildNodes();
NodeList childNodes = n.getChildNodes();
if (childNodes == null) {
return;
}
for (int i = 0; i < childNodes.getLength(); ++i) {
Node child = childNodes.item(i);
if (child.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
String childPath = path + "/" + child.getNodeName();
E value = DefaultXPathEvaluator.convertToComponentType(invocationContext, child, invocationContext.getTargetComponentType());
if (value != null) {
set.add(new SimpleEntry(childPath, value));
}
collectChildren(set, child, childPath);
}
}
private void collectChildrenValues(final Set> set, final Node n, final String path) {
if (n.getNodeType() == Node.TEXT_NODE) {
return;
}
// Iterate attributes only when target type is String
if (String.class.isAssignableFrom(invocationContext.getTargetComponentType())) {
NamedNodeMap attributes = n.getAttributes();
if (attributes != null) {
for (int i = 0; i < attributes.getLength(); ++i) {
// map.put(path + "/@" + attributes.item(i).getNodeName(), attributes.item(i));
set.add(new SimpleEntry(path + "/@" + attributes.item(i).getNodeName(), (E) (attributes.item(i).getNodeValue())));
}
}
}
NodeList childNodes = n.getChildNodes();
if (childNodes == null) {
return;
}
for (int i = 0; i < childNodes.getLength(); ++i) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.TEXT_NODE) {
continue;
}
final String childPath = path + "/" + child.getNodeName();
if (typeConverter.isConvertable(invocationContext.getTargetComponentType())) {
final String stringContent = DOMHelper.directTextContent(child);
if ((stringContent != null) && (!stringContent.isEmpty())) {
final E value = (E) typeConverter.convertTo(invocationContext.getTargetComponentType(), stringContent, invocationContext.getExpressionFormatPattern());
//T value = DefaultXPathEvaluator.convertToComponentType(invocationContext, child, invocationContext.getTargetComponentType());
if (value != null) {
set.add(new SimpleEntry(childPath, value));
}
}
}
collectChildrenValues(set, child, childPath);
}
}
/**
* @return bound node for this automap
*/
public Node getNode() {
domChangeTracker.refreshForReadIfNeeded();
return boundNode;
}
/**
* @return XBAutoMap.class
* @see org.xmlbeam.dom.DOMAccess#getProjectionInterface()
*/
@Override
public Class> getProjectionInterface() {
return XBAutoMap.class;
}
/**
* @return element that this map is bound to.
* @see org.xmlbeam.dom.DOMAccess#getDOMNode()
*/
@Override
public Node getDOMNode() {
return boundNode;
}
/**
* @return document for this map
* @see org.xmlbeam.dom.DOMAccess#getDOMOwnerDocument()
*/
@Override
public Document getDOMOwnerDocument() {
return DOMHelper.getOwnerDocumentFor(boundNode);
}
/**
* @return base element that was used when this map was created.
* @see org.xmlbeam.dom.DOMAccess#getDOMBaseElement()
*/
@Override
public Element getDOMBaseElement() {
if (baseNode.getNodeType() == Node.DOCUMENT_NODE) {
return ((Document) baseNode).getDocumentElement();
}
return (Element) baseNode;
}
/**
* @return XML String representation for this map.
* @see org.xmlbeam.dom.DOMAccess#asString()
*/
@Override
public String asString() {
return this.invocationContext.getProjector().asString(this);
}
/**
* @param path
* @param value
* @return this
* @see org.xmlbeam.dom.DOMAccess#create(java.lang.String, java.lang.Object)
*/
@Override
public DOMAccess create(final String path, final Object value) {
if (!valueType.isInstance(value)) {
throw new IllegalArgumentException("value must be the component type");
}
put(path, (T) value);
return this;
}
}