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

org.xmlbeam.ProjectionInvocationHandler Maven / Gradle / Ivy

/**
 *  Copyright 2012 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.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import java.io.IOException;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathVariableResolver;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import org.xmlbeam.XBProjector.IOBuilder;
import org.xmlbeam.annotation.XBAuto;
import org.xmlbeam.annotation.XBDelete;
import org.xmlbeam.annotation.XBDocURL;
import org.xmlbeam.annotation.XBOverride;
import org.xmlbeam.annotation.XBRead;
import org.xmlbeam.annotation.XBUpdate;
import org.xmlbeam.annotation.XBValue;
import org.xmlbeam.annotation.XBWrite;
import org.xmlbeam.dom.DOMAccess;
import org.xmlbeam.evaluation.DefaultXPathEvaluator;
import org.xmlbeam.evaluation.InvocationContext;
import org.xmlbeam.exceptions.XBDataNotFoundException;
import org.xmlbeam.exceptions.XBPathException;
import org.xmlbeam.types.XBAutoList;
import org.xmlbeam.types.XBAutoMap;
import org.xmlbeam.types.XBAutoValue;
import org.xmlbeam.util.IOHelper;
import org.xmlbeam.util.intern.DOMHelper;
import org.xmlbeam.util.intern.MethodParamVariableResolver;
import org.xmlbeam.util.intern.Preprocessor;
import org.xmlbeam.util.intern.ReflectionHelper;
import org.xmlbeam.util.intern.duplex.DuplexExpression;
import org.xmlbeam.util.intern.duplex.DuplexXPathParser;
import org.xmlbeam.util.intern.duplex.ExpressionType;
import org.xmlbeam.util.intern.duplex.XBPathParsingException;

/**
 * This class implements the "magic" behind projection methods. Each projection is linked with a
 * ProjectionInvocatonHandler which handles method invocations on the projections. Notice that this
 * class is not part of the public API. You should not get in touch with this class at all. See
 * {@link org.xmlbeam.XBProjector} for API usage.
 *
 * @author Sven Ewald
 */
@SuppressWarnings("serial")
final class ProjectionInvocationHandler implements InvocationHandler, Serializable {

    private Map getDefaultInvokers(final Object defaultInvokerObject) {
        final ReflectionInvoker reflectionInvoker = new ReflectionInvoker(defaultInvokerObject);
        final Map invokers = new HashMap();
        for (Method m : DOMAccess.class.getMethods()) {
            if (m.getAnnotation(XBWrite.class) == null) {
                invokers.put(MethodSignature.forMethod(m), reflectionInvoker);
            }
        }

        invokers.put(MethodSignature.forVoidMethod("toString"), reflectionInvoker);
        invokers.put(MethodSignature.forSingleParam("equals", Object.class), reflectionInvoker);
        invokers.put(MethodSignature.forVoidMethod("hashCode"), reflectionInvoker);
        return invokers;//Collections.unmodifiableMap(invokers);
    }

    private static class ReflectionInvoker implements InvocationHandler, Serializable {
        protected final Object obj;

        ReflectionInvoker(final Object obj) {
            this.obj = obj;
        }

        @Override
        public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
            try {
                return method.invoke(obj, args);
            } catch (InvocationTargetException e) {
                throw e.getCause() == null ? e : e.getCause();
            }
        }
    }

    private static class MixinInvoker extends ReflectionInvoker {
        private final Class projectionInterface;

        MixinInvoker(final Object obj, final Class projectionInterface) {
            super(obj);
            this.projectionInterface = projectionInterface;
        }

        @Override
        public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
            injectMeAttribute((DOMAccess) proxy, obj, projectionInterface);
            try {
                return super.invoke(proxy, method, args);
            } catch (InvocationTargetException e) {
                throw e.getCause() == null ? e : e.getCause();
            }
        }
    }

    private static abstract class ProjectionMethodInvocationHandler implements InvocationHandler, Serializable {

        private static final InvocationContext EMPTY_INVOCATION_CONTEXT = new InvocationContext(null, null, null, null, null, Object.class, null);

        protected final Method method;
        protected final String annotationValue;
        protected final XBProjector projector;
        protected final Node node;
        private final String docAnnotationValue;
        private final boolean isVoidMethod;
        protected InvocationContext lastInvocationContext = EMPTY_INVOCATION_CONTEXT;
        protected final Map methodParameterIndexes;

        ProjectionMethodInvocationHandler(final Node node, final Method method, final String annotationValue, final XBProjector projector) {
            this.method = method;
            this.annotationValue = annotationValue;
            this.projector = projector;
            this.node = node;
            final XBDocURL annotation = method.getAnnotation(XBDocURL.class);
            this.docAnnotationValue = annotation == null ? null : annotation.value();
            this.isVoidMethod = !ReflectionHelper.hasReturnType(method);
            methodParameterIndexes = ReflectionHelper.getMethodParameterIndexes(method);
        }

        protected Node getNodeForMethod(final Method method, final Object[] args) throws SAXException, IOException, ParserConfigurationException {
            if (docAnnotationValue != null) {
                String uri = projector.config().getExternalizer().resolveURL(docAnnotationValue, method, args);
                final Map requestParams = ((IOBuilder) projector.io()).filterRequestParamsFromParams(uri, args);
                uri = Preprocessor.applyParams(uri, methodParameterIndexes, args);
                Class callerClass = null;
                if (IOHelper.isResourceProtocol(uri)) {
                    callerClass = ReflectionHelper.getCallerClass(8);
                }
                return IOHelper.getDocumentFromURL(projector.config().createDocumentBuilder(), uri, requestParams, method.getDeclaringClass(), callerClass);
            }
            return node;
        }

        protected String resolveXPath(final Object[] args) {
            return Preprocessor.applyParams(projector.config().getExternalizer().resolveXPath(annotationValue, method, args), methodParameterIndexes, args);
        }

        /**
         * Determine a methods return value that does not depend on the methods execution. Possible
         * values are void or the proxy itself (would be "this").
         *
         * @param method
         * @return
         */
        protected Object getProxyReturnValueForMethod(final Object proxy, final Method method, final Integer numberOfChanges) {
            if (isVoidMethod) {
                return null;
            }
            if (method.getReturnType().equals(method.getDeclaringClass())) {
                return proxy;
            }
            if ((numberOfChanges != null) && (method.getReturnType().isAssignableFrom(Integer.class) || method.getReturnType().isAssignableFrom(int.class))) {
                return numberOfChanges;
            }
            throw new IllegalArgumentException("Method " + method + " has illegal return type \"" + method.getReturnType() + "\". I don't know what to return. I expected void or " + method.getDeclaringClass().getSimpleName());
        }

        abstract protected Object invokeProjection(final String resolvedXpath, final Object proxy, final Object[] args) throws Throwable;

        @Override
        public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
            final String xPath = resolveXPath(args);
            final String resolvedXpath = Preprocessor.applyParams(xPath, methodParameterIndexes, args);
            try {
                return invokeProjection(resolvedXpath, proxy, args);
            } finally {
                if (!(this instanceof ReadInvocationHandler)) {
                    projector.notifyDOMChangeListeners();
                }
            }
        }

    }

    private static abstract class XPathInvocationHandler extends ProjectionMethodInvocationHandler {

        protected final Class exceptionType;
        protected final boolean isThrowIfAbsent;

        private XPathInvocationHandler(final Node node, final Method method, final String annotationValue, final XBProjector projector) {
            super(node, method, annotationValue, projector);
            Class[] exceptionTypes = method.getExceptionTypes();
            exceptionType = exceptionTypes.length > 0 ? exceptionTypes[0] : null;
            this.isThrowIfAbsent = exceptionType != null;
        }

        @Override
        final protected Object invokeProjection(final String resolvedXpath, final Object proxy, final Object[] args) throws Throwable {
            final XPath xPath = projector.config().createXPath(DOMHelper.getOwnerDocumentFor(node));

            if (!lastInvocationContext.isStillValid(resolvedXpath)) {
                final DuplexExpression duplexExpression = new DuplexXPathParser(projector.config().getUserDefinedNamespaceMapping()).compile(resolvedXpath);
                String strippedXPath = duplexExpression.getExpressionAsStringWithoutFormatPatterns();
                MethodParamVariableResolver resolver = null;
                if (duplexExpression.isUsingVariables()) {
                    XPathVariableResolver peviousResolver = xPath.getXPathVariableResolver();
                    resolver = new MethodParamVariableResolver(method, args, duplexExpression, projector.config().getStringRenderer(), peviousResolver);
                    xPath.setXPathVariableResolver(resolver);

                }
                final XPathExpression xPathExpression = xPath.compile(strippedXPath);
                final Class targetComponentType = findTargetComponentType(method);

                lastInvocationContext = new InvocationContext(resolvedXpath, xPath, xPathExpression, duplexExpression, resolver, targetComponentType, projector);
            }
            lastInvocationContext.updateMethodArgs(args);
            return invokeXpathProjection(lastInvocationContext, proxy, args);
        }

        abstract protected Object invokeXpathProjection(final InvocationContext invocationContext, final Object proxy, final Object[] args) throws Throwable;
    }

    static class ReadInvocationHandler extends XPathInvocationHandler {
        private final boolean absentIsEmpty;
        private final boolean wrappedInOptional;
        private final boolean isEvaluateAsProjected;
        private final Class returnType;
        private final boolean isConvertable;
        private final boolean isReturnAsNode;
        private final boolean isEvaluateAsList;
        private final boolean isEvaluateAsArray;
        private final boolean isEvaluateAsSubProjection;
        private final boolean isEvaluateAsMap;
        private final boolean isReturnAsStream;

        ReadInvocationHandler(final Node node, final Method method, final String annotationValue, final XBProjector projector, final boolean absentIsEmpty) {
            super(node, method, annotationValue, projector);
            final Class methodReturnType = method.getReturnType();
            this.isEvaluateAsList = List.class.equals(methodReturnType) || ReflectionHelper.isStreamClass(methodReturnType) || XBAutoList.class.equals(methodReturnType);
            this.isEvaluateAsMap = XBAutoMap.class.equals(methodReturnType) || Map.class.equals(methodReturnType);
            this.isReturnAsStream = ReflectionHelper.isStreamClass(methodReturnType);
            this.isEvaluateAsArray = methodReturnType.isArray();
            this.wrappedInOptional = ReflectionHelper.isOptional(method.getGenericReturnType());
            this.isEvaluateAsProjected = Map.class.equals(methodReturnType) || XBAutoMap.class.equals(methodReturnType) || XBAutoValue.class.equals(methodReturnType) || (method.getAnnotation(XBAuto.class) != null);
            this.returnType = (wrappedInOptional || isEvaluateAsProjected) ? ReflectionHelper.getParameterType(method.getGenericReturnType()) : methodReturnType;
            this.isConvertable = (!isEvaluateAsList) && (!isEvaluateAsMap) && (!isEvaluateAsArray) && (!isReturnAsStream) && projector.config().getTypeConverter().isConvertable(returnType);
            this.isReturnAsNode = Node.class.isAssignableFrom(returnType);
            if (wrappedInOptional && (isEvaluateAsArray || isEvaluateAsList || isEvaluateAsProjected)) {
                throw new IllegalArgumentException("Method " + method + " must not declare an optional return type of AutoValue, List or Array. Lists, and arrays may be empty but will never be null.");
            }
            this.isEvaluateAsSubProjection = returnType.isInterface();

            // Throwing exception overrides empty default value.
            this.absentIsEmpty = absentIsEmpty && (!isThrowIfAbsent);
        }

        @Override
        public Object invokeXpathProjection(final InvocationContext invocationContext, final Object proxy, final Object[] args) throws Throwable {
            final Object result = invokeReadProjection(invocationContext, proxy, args);
            if ((result == null) && (isThrowIfAbsent)) {
                throwDeclaredException(invocationContext, args, exceptionType);
            }
            return result;
        }

        @SuppressWarnings("rawtypes")
        private Object invokeReadProjection(final InvocationContext invocationContext, final Object proxy, final Object[] args) throws Throwable {
            final Node node = getNodeForMethod(method, args);
            final ExpressionType expressionType = invocationContext.getDuplexExpression().getExpressionType();
            final XPathExpression expression = invocationContext.getxPathExpression();

            if (isEvaluateAsProjected && (!isEvaluateAsList) && (!isEvaluateAsMap)) {
                return new AutoValue(node, invocationContext);
            }

            if (isConvertable) {
                String data;
                Node dataNode = null;
                if (expressionType.isMustEvalAsString()) {
                    if (isEvaluateAsProjected) {
                        throw new XBPathException("XPath expression is not writeable and can not be mapped to a projected Value.", method, invocationContext.getResolvedXPath());
                    }
                    data = (String) expression.evaluate(node, XPathConstants.STRING);
                } else {
                    dataNode = (Node) expression.evaluate(node, XPathConstants.NODE);
                    data = dataNode == null ? null : dataNode.getTextContent();
                }
                if ((data == null) && (absentIsEmpty)) {
                    data = "";
                }

                try {
                    final Object result = projector.config().getTypeConverter().convertTo(returnType, data, invocationContext.getExpressionFormatPattern());

                    return wrappedInOptional ? ReflectionHelper.createOptional(result) : result;
                } catch (NumberFormatException e) {
                    throw new NumberFormatException(e.getMessage() + " XPath was:" + invocationContext.getResolvedXPath());
                }
            }
            if (isReturnAsNode) {
                // Try to evaluate as node
                // if evaluated type does not match return type, ClassCastException will follow
                final Object result = expression.evaluate(node, XPathConstants.NODE);
                return wrappedInOptional ? ReflectionHelper.createOptional(result) : result;
            }
            if (isEvaluateAsMap) {
                return new AutoMap(node, invocationContext, returnType);
            }
            if (isEvaluateAsList) {
                assert !wrappedInOptional : "Projection methods returning list will never return null";
                if (XBAutoList.class.equals(returnType) || (isEvaluateAsProjected)) {
                    return new AutoList(node, invocationContext);
                }

                final List result = DefaultXPathEvaluator.evaluateAsList(expression, node, method, invocationContext);
                return isReturnAsStream ? ReflectionHelper.toStream(result) : result;
            }
            if (isEvaluateAsArray) {
                assert !wrappedInOptional : "Projection methods returning array will never return null";
                final List list = DefaultXPathEvaluator.evaluateAsList(expression, node, method, invocationContext);
                return list.toArray((Object[]) java.lang.reflect.Array.newInstance(returnType.getComponentType(), list.size()));
            }
            if (isEvaluateAsSubProjection) {
                final Node newNode = (Node) expression.evaluate(node, XPathConstants.NODE);
                if (newNode == null) {
                    return wrappedInOptional ? ReflectionHelper.createOptional(null) : null;
                }
                final DOMAccess subprojection = (DOMAccess) projector.projectDOMNode(newNode, returnType);
                return wrappedInOptional ? ReflectionHelper.createOptional(subprojection) : subprojection;
            }
            throw new IllegalArgumentException("Return type " + returnType + " of method " + method + " is not supported. Please change to an projection interface, a List, an Array or one of current type converters types:" + projector.config().getTypeConverter());
        }
    }

    private static class UpdateInvocationHandler extends XPathInvocationHandler {

        private final int findIndexOfValue;

        /**
         * @param node
         * @param m
         * @param value
         * @param projector
         */
        public UpdateInvocationHandler(final Node node, final Method m, final String value, final XBProjector projector) {
            super(node, m, value, projector);
            findIndexOfValue = findIndexOfValue(m);
            if (isMultiValue(m.getParameterTypes()[findIndexOfValue])) {
                throw new IllegalArgumentException("Method " + m + " was declated as updater but with multiple values. Update is possible for single values only. Consider using @XBWrite.");
            }
        }

        @Override
        public Object invokeXpathProjection(final InvocationContext invocationContext, final Object proxy, final Object[] args) throws Throwable {
            assert ReflectionHelper.hasParameters(method);
            final Node node = getNodeForMethod(method, args);
//            final Document document = DOMHelper.getOwnerDocumentFor(node);
//            final XPath xPath = projector.config().createXPath(document);
            final XPathExpression expression = invocationContext.getxPathExpression();

            final Object valueToSet = args[findIndexOfValue];
            //      final Class typeToSet = method.getParameterTypes()[findIndexOfValue];
            //     final boolean isMultiValue = isMultiValue(typeToSet);
            NodeList nodes = (NodeList) expression.evaluate(node, XPathConstants.NODESET);
            final int count = nodes.getLength();
            for (int i = 0; i < count; ++i) {
                final Node n = nodes.item(i);
                if (n == null) {
                    continue;
                }
                if (Node.ATTRIBUTE_NODE == n.getNodeType()) {
                    Element e = ((Attr) n).getOwnerElement();
                    if (e == null) {
                        continue;
                    }
                    DOMHelper.setOrRemoveAttribute(e, n.getNodeName(), valueToSet == null ? null : valueToSet.toString());
                    continue;
                }
                if (valueToSet instanceof Element) {
                    if (!(n instanceof Element)) {
                        throw new IllegalArgumentException("XPath for element update need to select elements only");
                    }                    
                    DOMHelper.replaceElement((Element) n, (Element) ((Element) valueToSet).cloneNode(true));
                    continue;
                }
                n.setTextContent(valueToSet == null ? null : valueToSet.toString());
            }
            if ((count == 0) && (isThrowIfAbsent)) {
                throwDeclaredException(invocationContext, args, exceptionType);
            }
            return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(count));
        }

    }

    private static class DeleteInvocationHandler extends XPathInvocationHandler {

        /**
         * @param node
         * @param m
         * @param value
         * @param projector
         */
        public DeleteInvocationHandler(final Node node, final Method m, final String value, final XBProjector projector) {
            super(node, m, value, projector);
        }

        @Override
        public Object invokeXpathProjection(final InvocationContext invocationContext, final Object proxy, final Object[] args) throws Throwable {

//            try {
//                if (ReflectionHelper.mayProvideParameterNames()) {
//                    xPath.setXPathVariableResolver(new MethodParamVariableResolver(method, args, xPath.getXPathVariableResolver()));
//               }

            final XPathExpression expression = invocationContext.getxPathExpression();
            NodeList nodes = (NodeList) expression.evaluate(node, XPathConstants.NODESET);
            int count = 0;
            for (int i = 0; i < nodes.getLength(); ++i) {
                if (Node.ATTRIBUTE_NODE == nodes.item(i).getNodeType()) {
                    Attr attr = (Attr) nodes.item(i);
                    attr.getOwnerElement().removeAttributeNode(attr);
                    ++count;
                    continue;
                }
                Node parentNode = nodes.item(i).getParentNode();
                if (parentNode == null) {
                    continue;
                }
                parentNode.removeChild(nodes.item(i));
                ++count;
            }
            if ((count == 0) && (isThrowIfAbsent)) {
                throwDeclaredException(invocationContext, args, exceptionType);
            }
            return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(count));
//            } finally {
//                xPath.reset();
//            }
        }

    }

    static class WriteInvocationHandler extends ProjectionMethodInvocationHandler {

        private final int findIndexOfValue;

        /**
         * @param node
         * @param m
         * @param value
         * @param projector
         */
        public WriteInvocationHandler(final Node node, final Method m, final String value, final XBProjector projector) {
            super(node, m, value, projector);
            findIndexOfValue = findIndexOfValue(m);
        }

        private Object handeRootElementReplacement(final Object proxy, final Method method, final Document document, final Object valueToSet) {
            int count = document.getDocumentElement() == null ? 0 : 1;
            if (valueToSet == null) {
                DOMHelper.setDocumentElement(document, null);
                return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(count));
            }
            if (valueToSet instanceof Element) {
                Element clone = (Element) ((Element) valueToSet).cloneNode(true);
                document.adoptNode(clone);
                if (document.getDocumentElement() == null) {
                    document.appendChild(clone);
                    return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(1));
                }
                document.replaceChild(document.getDocumentElement(), clone);
                return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(1));
            }
            if (!(valueToSet instanceof DOMAccess)) {
                throw new IllegalArgumentException("Method " + method + " was invoked as setter changing the document root element. Expected value type was a projection so I can determine a element name. But you provided a " + valueToSet);
            }
            DOMAccess projection = (DOMAccess) valueToSet;
            Element element = projection.getDOMBaseElement();
            assert element != null;
            DOMHelper.setDocumentElement(document, element);
            return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(count));
        }

        /**
         * @param typeToSet
         * @param iterable
         * @param parentElement
         * @param duplexExpression
         * @param elementSelector
         */
        private int applyIterableSetOnElement(final Iterable iterable, final Element parentElement, final DuplexExpression duplexExpression) {
            int changeCount = 0;
            for (Object o : iterable) {
                if (o == null) {
                    continue;
                }
                if (!isStructureChangingValue(o)) {
                    final Node newElement = duplexExpression.createChildWithPredicate(parentElement);
                    final String asString = projector.config().getStringRenderer().render(o.getClass(), o, duplexExpression.getExpressionFormatPattern());
                    newElement.setTextContent(asString);
                    ++changeCount;
                    continue;
                }
                Element elementToAdd;

                if (o instanceof Node) {
                    final Node n = (Node) o;
                    elementToAdd = (Element) (Node.DOCUMENT_NODE != n.getNodeType() ? n : n.getOwnerDocument() == null ? null : n.getOwnerDocument().getDocumentElement());
                } else {
                    final DOMAccess p = (DOMAccess) o;
                    elementToAdd = p.getDOMBaseElement();
                }
                if (elementToAdd == null) {
                    continue;
                }

                Element clone = (Element) elementToAdd.cloneNode(true);
                Element childWithPredicate = (Element) duplexExpression.createChildWithPredicate(parentElement);
                final String elementName = childWithPredicate.getNodeName();
                if (!elementName.equals(clone.getNodeName())) {
                    if (!"*".equals(elementName)) {
                        clone = DOMHelper.renameNode(clone, elementName);
                    }
                }
                DOMHelper.replaceElement(childWithPredicate, clone);
                ++changeCount;
            }
            return changeCount;
        }

        @Override
        public Object invokeProjection(final String resolvedXpath, final Object proxy, final Object[] args) throws Throwable {
            //   final String pathToElement = resolvedXpath.replaceAll("\\[@", "[attribute::").replaceAll("/?@.*", "").replaceAll("\\[attribute::", "[@");
            lastInvocationContext.updateMethodArgs(args);
            final Document document = DOMHelper.getOwnerDocumentFor(node);
            assert document != null;
            final Object valueToSet = args[findIndexOfValue];
            final boolean isMultiValue = isMultiValue(method.getParameterTypes()[findIndexOfValue]);
            // ROOT element update
            if ("/*".equals(resolvedXpath)) { // Setting a new root element.
                if (isMultiValue) {
                    throw new IllegalArgumentException("Method " + method + " was invoked as setter changing the document root element, but tries to set multiple values.");
                }
                return handeRootElementReplacement(proxy, method, document, valueToSet);
            }
            final boolean wildCardTarget = resolvedXpath.endsWith("/*");
            try {
                if (!lastInvocationContext.isStillValid(resolvedXpath)) {
                    final DuplexExpression duplexExpression = wildCardTarget ? new DuplexXPathParser(projector.config().getUserDefinedNamespaceMapping()).compile(resolvedXpath.substring(0, resolvedXpath.length() - 2))
                            : new DuplexXPathParser(projector.config().getUserDefinedNamespaceMapping()).compile(resolvedXpath);
                    MethodParamVariableResolver resolver = null;
                    if (duplexExpression.isUsingVariables()) {
                        resolver = new MethodParamVariableResolver(method, args, duplexExpression, projector.config().getStringRenderer(), null);
                        duplexExpression.setXPathVariableResolver(resolver);
                    }
                    Class targetComponentType = findTargetComponentType(method);
                    lastInvocationContext = new InvocationContext(resolvedXpath, null, null, duplexExpression, resolver, targetComponentType, projector);
                }
                final DuplexExpression duplexExpression = lastInvocationContext.getDuplexExpression();
                if (duplexExpression.getExpressionType().isMustEvalAsString()) {
                    throw new XBPathException("Unwriteable xpath selector used ", method, resolvedXpath);
                }
                // MULTIVALUE
                if (isMultiValue) {
                    if (duplexExpression.getExpressionType().equals(ExpressionType.ATTRIBUTE)) {
                        throw new IllegalArgumentException("Method " + method + " was invoked as setter changing some attribute, but was declared to set multiple values. I can not create multiple attributes for one path.");
                    }
                    final Iterable iterable2Set = valueToSet == null ? Collections.emptyList() : (valueToSet.getClass().isArray()) ? ReflectionHelper.array2ObjectList(valueToSet) : (Iterable) valueToSet;
                    if (wildCardTarget) {
                        // TODO: check support of ParameterizedType e.g. Supplier
                        final Element parentElement = (Element) duplexExpression.ensureExistence(node);
                        DOMHelper.removeAllChildren(parentElement);
                        int count = 0;
                        for (Object o : iterable2Set) {
                            if (o == null) {
                                continue;
                            }
                            ++count;
                            if (o instanceof Node) {
                                DOMHelper.appendClone(parentElement, (Node) o);
                                continue;
                            }
                            if (o instanceof DOMAccess) {
                                DOMHelper.appendClone(parentElement, ((DOMAccess) o).getDOMBaseElement());
                                continue;
                            }
                            throw new XBPathException("When using a wildcard target, the type to set must be a DOM Node or another projection. Otherwise I can not determine the element name.", method, resolvedXpath);
                        }
                        return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(count));
                    }
                    final Element parentElement = duplexExpression.ensureParentExistence(node);
                    duplexExpression.deleteAllMatchingChildren(parentElement);
                    int count = applyIterableSetOnElement(iterable2Set, parentElement, duplexExpression);
                    return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(count));
                }

                // ATTRIBUTES
                if (duplexExpression.getExpressionType().equals(ExpressionType.ATTRIBUTE)) {
                    if (wildCardTarget) {
                        //TODO: This may never happen, right?
                        throw new XBPathException("Wildcards are not allowed when writing to an attribute. I need to know to which Element I should set the attribute", method, resolvedXpath);
                    }
                    Attr attribute = (Attr) duplexExpression.ensureExistence(node);
                    if (valueToSet == null) {
                        attribute.getOwnerElement().removeAttributeNode(attribute);
                        return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(1));
                    }
                    attribute.setTextContent(valueToSet.toString());
                    return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(1));
                }

                if ((valueToSet instanceof Node) || (valueToSet instanceof DOMAccess)) {
                    if (valueToSet instanceof Attr) {
                        if (wildCardTarget) {
                            throw new XBPathException("Wildcards are not allowed when writing an attribute. I need to know to which Element I should set the attribute", method, resolvedXpath);
                        }
                        Element parentNode = duplexExpression.ensureParentExistence(node);
                        if (((Attr) valueToSet).getNamespaceURI() != null) {
                            parentNode.setAttributeNodeNS((Attr) valueToSet);
                            return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(1));
                        }
                        parentNode.setAttributeNode((Attr) valueToSet);
                        return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(1));
                    }
                    final Element newNodeOrigin = valueToSet instanceof DOMAccess ? ((DOMAccess) valueToSet).getDOMBaseElement() : (Element) valueToSet;
                    final Element newNode = (Element) newNodeOrigin.cloneNode(true);
                    DOMHelper.ensureOwnership(document, newNode);
                    if (wildCardTarget) {
                        Element parentElement = (Element) duplexExpression.ensureExistence(node);
                        DOMHelper.removeAllChildren(parentElement);
                        parentElement.appendChild(newNode);
                        return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(1));
                    }
                    Element previousElement = (Element) duplexExpression.ensureExistence(node);

                    DOMHelper.replaceElement(previousElement, newNode);
                    return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(1));
                }

                final Element elementToChange = (Element) duplexExpression.ensureExistence(node);
                final Class valueType = method.getParameterTypes()[findIndexOfValue];
                if ((valueToSet == null) && (ProjectionInvocationHandler.isStructureChangingType(method.getParameterTypes()[findIndexOfValue]))) {
                    DOMHelper.removeAllChildren(elementToChange);
                } else {
                    final String asString = projector.config().getStringRenderer().render(valueType, valueToSet, duplexExpression.getExpressionFormatPattern());
                    //elementToChange.setTextContent(asString);
                    DOMHelper.setDirectTextContent(elementToChange, asString);
                }
                return getProxyReturnValueForMethod(proxy, method, Integer.valueOf(1));
            } catch (XBPathParsingException e) {
                throw new XBPathException(e, method, resolvedXpath);
            }
        }
    }

    private static final InvocationHandler DEFAULT_METHOD_INVOCATION_HANDLER = new InvocationHandler() {

        @Override
        public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
            return ReflectionHelper.invokeDefaultMethod(method, args, proxy);
        }
    };

    private static final class OverrideByDefaultMethodInvocationHandler implements InvocationHandler {
        private final Method defaultMethod;

        OverrideByDefaultMethodInvocationHandler(final Method defaultMethod) {
            this.defaultMethod = defaultMethod;
        }

        @Override
        public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
            return DEFAULT_METHOD_INVOCATION_HANDLER.invoke(proxy, defaultMethod, args);
        }
    }

    private final Map handlers = new HashMap();
    private final Map mixinHandlers = new HashMap();

    ProjectionInvocationHandler(final XBProjector projector, final Node node, final Class projectionInterface, final Map, Object> mixins, final boolean toStringRendersXML, final boolean absentIsEmpty) {
        final Object defaultInvokerObject = DefaultDOMAccessInvoker.create(projectionInterface, node, projector, toStringRendersXML);
        final Map defaultInvocationHandlers = getDefaultInvokers(defaultInvokerObject);

        for (Entry, Object> e : mixins.entrySet()) {
            for (Method m : e.getKey().getMethods()) {
                mixinHandlers.put(MethodSignature.forMethod(m), new MixinInvoker(e.getValue(), projectionInterface));
            }
        }

        handlers.putAll(defaultInvocationHandlers);

        List> allSuperInterfaces = ReflectionHelper.findAllSuperInterfaces(projectionInterface);
        for (Class i7e : allSuperInterfaces) {
            for (Method m : i7e.getDeclaredMethods()) {
                if (Modifier.isPrivate(m.getModifiers())) {
                    // ignore private methods
                    continue;
                }
                final MethodSignature methodSignature = MethodSignature.forMethod(m);
                if (ReflectionHelper.isDefaultMethod(m)) {
                    handlers.put(methodSignature, DEFAULT_METHOD_INVOCATION_HANDLER);
                    final XBOverride xbOverride = m.getAnnotation(XBOverride.class);
                    if (xbOverride != null) {
                        handlers.put(methodSignature.overridenBy(xbOverride.value()), new OverrideByDefaultMethodInvocationHandler(m));
                    }
                    continue;
                }
                if (defaultInvocationHandlers.containsKey(methodSignature)) {
                    continue;
                }
                {
                    final XBRead readAnnotation = m.getAnnotation(XBRead.class);
                    if (readAnnotation != null) {
                        handlers.put(methodSignature, new ReadInvocationHandler(node, m, readAnnotation.value(), projector, absentIsEmpty));
                        continue;
                    }
                }
                {
                    final XBAuto bindAnnotation = m.getAnnotation(XBAuto.class);
                    if (bindAnnotation != null) {
                        handlers.put(methodSignature, new ReadInvocationHandler(node, m, bindAnnotation.value(), projector, absentIsEmpty));
                        continue;
                    }
                }
                {
                    final XBUpdate updateAnnotation = m.getAnnotation(XBUpdate.class);
                    if (updateAnnotation != null) {
                        handlers.put(methodSignature, new UpdateInvocationHandler(node, m, updateAnnotation.value(), projector));
                        continue;
                    }
                }
                {
                    final XBWrite writeAnnotation = m.getAnnotation(XBWrite.class);
                    if (writeAnnotation != null) {
                        handlers.put(methodSignature, new WriteInvocationHandler(node, m, writeAnnotation.value(), projector));
                        continue;
                    }
                }
                {
                    final XBDelete delAnnotation = m.getAnnotation(XBDelete.class);
                    if (delAnnotation != null) {
                        handlers.put(methodSignature, new DeleteInvocationHandler(node, m, delAnnotation.value(), projector));
                        continue;
                    }
                }

                if (mixinHandlers.containsKey(methodSignature)) {
                    continue;
                }

                throw new IllegalArgumentException("I don't known how to handle method " + m + ". Did you forget to add a XB*-annotation or to register a mixin?");
            }
        }

    }

    /**
     * @param o
     * @return
     * @deprecated use isStructureChangingType instead
     */
    @Deprecated
    static boolean isStructureChangingValue(final Object o) {
        return (o instanceof DOMAccess) || (o instanceof Node);
    }

    public static boolean isStructureChangingType(final Class c) {
        return (DOMAccess.class.isAssignableFrom(c)) || (Node.class.isAssignableFrom(c)) || (c.isInterface());
    }

    /**
     * Setter projection methods may have multiple parameters. One of them may be annotated with
     * {@link XBValue} to select it as value to be set.
     *
     * @param method
     * @return index of fist parameter annotated with {@link XBValue} annotation.
     */
    private static int findIndexOfValue(final Method method) {
        int index = 0;
        for (Annotation[] annotations : method.getParameterAnnotations()) {
            for (Annotation a : annotations) {
                if (XBValue.class.equals(a.annotationType())) {
                    return index;
                }
            }
            ++index;
        }
        return 0; // If no attribute is annotated, the first one is taken.
    }

    /**
     * Find the "me" attribute (which is a replacement for "this") and inject the projection proxy
     * instance.
     *
     * @param me
     * @param target
     */
    private static void injectMeAttribute(final DOMAccess me, final Object target, final Class projectionInterface) {
        //final Class projectionInterface = me.getProjectionInterface();
        for (Field field : target.getClass().getDeclaredFields()) {
            if (!isValidMeField(field, projectionInterface)) {
                continue;
            }
            if (!field.isAccessible()) {
                field.setAccessible(true);
            }
            try {
                field.set(target, me);
                return;
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
        throw new IllegalArgumentException("Mixin " + target.getClass().getSimpleName() + " needs an attribute \"private " + projectionInterface.getSimpleName() + " me;\" to be able to access the projection.");
    }

    private static boolean isValidMeField(final Field field, final Class projInterface) {
        if (field == null) {
            return false;
        }
        if (!"me".equalsIgnoreCase(field.getName())) {
            return false;
        }
        if (DOMAccess.class.equals(field.getType())) {
            return true;
        }
        return field.getType().isAssignableFrom(projInterface);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
        unwrapArgs(method.getParameterTypes(), args);
        if (!mixinHandlers.isEmpty()) {
            MethodSignature methodSignature = MethodSignature.forMethod(method);
            if (mixinHandlers.containsKey(methodSignature)) {
                return mixinHandlers.get(methodSignature).invoke(proxy, method, args);
            }
        }

        final InvocationHandler invocationHandler = handlers.get(MethodSignature.forMethod(method));
        if (invocationHandler != null) {
            try {
                return invocationHandler.invoke(proxy, method, args);
            } catch (XPathExpressionException e) {
                throw new XBPathException(e, method, "??");
            }
        }

        throw new IllegalArgumentException("I don't known how to invoke method " + method + ". Did you forget to add a XB*-annotation or to register a mixin?");
    }

    /**
     * If parameter is instance of Callable or Supplier then resolve its value.
     *
     * @param args
     * @param args2
     */
    private static void unwrapArgs(final Class[] types, final Object[] args) {
        if (args == null) {
            return;
        }
        try {
            for (int i = 0; i < args.length; ++i) {
                args[i] = ReflectionHelper.unwrap(types[i], args[i]);
            }
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }

    }

    /**
     * @param typeToSet
     * @return
     */
    private static boolean isMultiValue(final Class type) {
        return type.isArray() || Iterable.class.isAssignableFrom(type);
    }

    /**
     * When reading collections, determine the collection component type.
     *
     * @param method
     * @return
     */
    private static Class findTargetComponentType(final Method method) {
        final Class returnType = method.getReturnType();
        if (returnType.isArray()) {
            return method.getReturnType().getComponentType();
        }

        if (!(List.class.equals(returnType) || (Map.class.equals(returnType)) || XBAutoMap.class.isAssignableFrom(returnType) || XBAutoList.class.equals(returnType) || XBAutoValue.class.equals(returnType) || ReflectionHelper.isStreamClass(returnType))) {
            return null;
        }
        final Type type = method.getGenericReturnType();
        if (!(type instanceof ParameterizedType) || (((ParameterizedType) type).getActualTypeArguments() == null) || (((ParameterizedType) type).getActualTypeArguments().length < 1)) {
            throw new IllegalArgumentException("When using List as return type for method " + method + ", please specify a generic type for the List. Otherwise I do not know which type I should fill the List with.");
        }
        int index = Map.class.equals(returnType) ? 1 : 0;

        assert index < ((ParameterizedType) type).getActualTypeArguments().length;
        Type componentType = ((ParameterizedType) type).getActualTypeArguments()[index];
        if (!(componentType instanceof Class)) {
            throw new IllegalArgumentException("I don't know how to instantiate the generic type for the return type of method " + method);
        }
        return (Class) componentType;
    }

    /**
     * @param invocationContext
     * @param args
     * @throws Throwable
     */
    protected static void throwDeclaredException(final InvocationContext invocationContext, final Object[] args, final Class exceptionType) throws Throwable {
        XBDataNotFoundException dataNotFoundException = new XBDataNotFoundException(invocationContext.getResolvedXPath());
        if (XBDataNotFoundException.class.equals(exceptionType)) {
            throw dataNotFoundException;
        }
        ReflectionHelper.throwThrowable(exceptionType, args, dataNotFoundException);
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy