All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.xmlbeam.AutoMap Maven / Gradle / Ivy

/**
 *  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;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy