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

org.xmlbeam.evaluation.DefaultXPathEvaluator Maven / Gradle / Ivy

/**
 *    Copyright 2015 Sven Ewald
 *
 *    This file is part of JSONBeam.
 *
 *    JSONBeam is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation, either version 3 of the License, any
 *    later version.
 *
 *    JSONBeam is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with JSONBeam.  If not, see .
 */
package org.xmlbeam.evaluation;

import java.util.Collection;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import java.awt.geom.IllegalPathStateException;
import java.io.IOException;
import java.lang.reflect.Method;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xmlbeam.AutoMap;
import org.xmlbeam.XBProjector;
import org.xmlbeam.XBProjector.Flags;
import org.xmlbeam.exceptions.XBException;
import org.xmlbeam.exceptions.XBIOException;
import org.xmlbeam.exceptions.XBPathException;
import org.xmlbeam.types.TypeConverter;
import org.xmlbeam.types.XBAutoMap;
import org.xmlbeam.util.intern.DOMHelper;
import org.xmlbeam.util.intern.ReflectionHelper;
import org.xmlbeam.util.intern.duplex.DuplexExpression;
import org.xmlbeam.util.intern.duplex.DuplexXPathParser;

/**
 * This class is used to provide an fluid interface for direct evaluation of XPath expressions.
 *
 * @author sven
 */
public final class DefaultXPathEvaluator implements XPathEvaluator {

    private final DocumentResolver documentProvider;
    private final DuplexExpression duplexExpression;
    private final XBProjector projector;

    /**
     * Constructor for DefaultXPathEvaluator.
     *
     * @param projector
     * @param documentProvider
     * @param xpath
     */
    public DefaultXPathEvaluator(final XBProjector projector, final DocumentResolver documentProvider, final String xpath) {
        this.projector = projector;
        this.documentProvider = documentProvider;
        this.duplexExpression = new DuplexXPathParser(projector.config().getUserDefinedNamespaceMapping()).compile(xpath);
    }

    /**
     * Evaluates the XPath as a boolean value. This method is just a shortcut for as(Boolean.TYPE);
     *
     * @return true when the selected value equals (ignoring case) 'true'
     */
    @Override
    public boolean asBoolean() {
        final Class callerClass = ReflectionHelper.getDirectCallerClass();
        return evaluateSingeValue(Boolean.TYPE, callerClass);
    }

    /**
     * Evaluates the XPath as a int value. This method is just a shortcut for as(Integer.TYPE);
     *
     * @return int value of evaluation result.
     */
    @Override
    public int asInt() {
        final Class callerClass = ReflectionHelper.getDirectCallerClass();
        return evaluateSingeValue(Integer.TYPE, callerClass);
    }

    /**
     * Evaluates the XPath as a String value. This method is just a shortcut for as(String.class);
     *
     * @return String value of evaluation result.
     */
    @Override
    public String asString() {
        final Class callerClass = ReflectionHelper.getDirectCallerClass();
        return evaluateSingeValue(String.class, callerClass);
    }

    /**
     * Evaluates the XPath as a Date value. This method is just a shortcut for as(Date.class); You
     * probably want to specify ' using ' followed by some formatting pattern consecutive to the
     * XPAth.
     *
     * @return Date value of evaluation result.
     */
    @Override
    public Date asDate() {
        final Class callerClass = ReflectionHelper.getDirectCallerClass();
        return evaluateSingeValue(Date.class, callerClass);
    }

    /**
     * Evaluate the XPath as a value of the given type.
     *
     * @param returnType
     *            Possible values: primitive types (e.g. Short.Type), Projection interfaces, any
     *            class with a String constructor or a String factory method, and org.w3c.Node
     * @return a value of return type that reflects the evaluation result.
     */
    @Override
    public  T as(final Class returnType) {
        validateEvaluationType(returnType);
        final Class callerClass = ReflectionHelper.getDirectCallerClass();
        return evaluateSingeValue(returnType, callerClass);
    }

    @SuppressWarnings("unchecked")
    private  T evaluateSingeValue(final Class returnType, final Class callerClass) {
        try {
            final Document document = documentProvider.resolve(returnType, callerClass);

            final XPathExpression expression = projector.config().createXPath(document).compile(duplexExpression.getExpressionAsStringWithoutFormatPatterns());

            if (projector.config().getTypeConverter().isConvertable(returnType)) {
                String data;
                if (duplexExpression.getExpressionType().isMustEvalAsString()) {
                    data = (String) expression.evaluate(document, XPathConstants.STRING);
                } else {
                    Node dataNode = (Node) expression.evaluate(document, XPathConstants.NODE);
                    data = dataNode == null ? null : dataNode.getTextContent();
                }
                if ((data == null) && (projector.getFlags().contains(Flags.ABSENT_IS_EMPTY))) {
                    data = "";
                }

                final Object result = projector.config().getTypeConverter().convertTo(returnType, data, duplexExpression.getExpressionFormatPattern());
                return (T) result;
            }

            if (Node.class.isAssignableFrom(returnType)) {
                final Object result = expression.evaluate(document, XPathConstants.NODE);
                return (T) result;
            }

            if (returnType.isInterface()) {
                final Node node = (Node) expression.evaluate(document, XPathConstants.NODE);
                if (node == null) {
                    return null;
                }
                return projector.projectDOMNode(node, returnType);
            }
        } catch (XPathExpressionException e) {
            throw new XBPathException(e,duplexExpression.getExpressionAsStringWithoutFormatPatterns());
        } catch (IOException e) {
            throw new XBIOException(e);
        }
        throw new IllegalPathStateException();
    }

    private  void validateEvaluationType(final Class returnType) {
        if (ReflectionHelper.isOptional(returnType)) {
            throw new IllegalArgumentException("Type Optional is only allowed as a method return type.");
        }
        if (Collection.class.isAssignableFrom(returnType)) {
            throw new IllegalArgumentException("A collection type can not be component type.");
        }
    }

    /**
     * Evaluate the XPath as an array of the given type.
     *
     * @param componentType
     *            Possible values: primitive types (e.g. Short.Type), Projection interfaces, any
     *            class with a String constructor or a String factory method, and org.w3c.Node
     * @return an array of return type that reflects the evaluation result.
     */
    @SuppressWarnings("unchecked")
    @Override
    public  T[] asArrayOf(final Class componentType) {
        Class callerClass = ReflectionHelper.getDirectCallerClass();
        List list = evaluateMultiValues(componentType, callerClass);
        return list.toArray((T[]) java.lang.reflect.Array.newInstance(componentType, list.size()));
    }

    /**
     * Evaluate the XPath as a list of the given type.
     *
     * @param componentType
     *            Possible values: primitive types (e.g. Short.Type), Projection interfaces, any
     *            class with a String constructor or a String factory method, and org.w3c.Node
     * @return List of return type that reflects the evaluation result.
     */
    @Override
    public  List asListOf(final Class componentType) {
        Class callerClass = ReflectionHelper.getDirectCallerClass();
        return evaluateMultiValues(componentType, callerClass);
    }

    @SuppressWarnings("unchecked")
    private  List evaluateMultiValues(final Class componentType, final Class callerClass) {
        validateEvaluationType(componentType);
        try {
            Document document = documentProvider.resolve(componentType, callerClass);
            XPathExpression expression = projector.config().createXPath(document).compile(duplexExpression.getExpressionAsStringWithoutFormatPatterns());
            InvocationContext invocationContext = new InvocationContext(null, null, expression, duplexExpression, null, componentType, projector);
            return (List) evaluateAsList(expression, document, null, invocationContext);
        } catch (XPathExpressionException e) {
            throw new XBPathException(e,duplexExpression.getExpressionAsStringWithoutFormatPatterns());
        } catch (IOException e) {
            throw new XBIOException(e);
        }
    }

    /**
     * Perform an XPath evaluation on an invocation context.
     *
     * @param expression
     * @param node
     * @param method
     * @param invocationContext
     * @return a list of evaluation results
     * @throws XPathExpressionException
     */
    public static List evaluateAsList(final XPathExpression expression, final Node node, final Method method, final InvocationContext invocationContext) throws XPathExpressionException {
        //assert targetComponentType != null;
        final Class targetComponentType = invocationContext.getTargetComponentType();
        final NodeList nodes = (NodeList) expression.evaluate(node, XPathConstants.NODESET);
        final List linkedList = new LinkedList();

        final TypeConverter typeConverter = invocationContext.getProjector().config().getTypeConverter();
        if (typeConverter.isConvertable(targetComponentType)) {
            for (int i = 0; i < nodes.getLength(); ++i) {
                linkedList.add(typeConverter.convertTo(targetComponentType, nodes.item(i).getTextContent(), invocationContext.getExpressionFormatPattern()));
            }
            return linkedList;
        }
        if (Node.class.equals(targetComponentType)) {
            for (int i = 0; i < nodes.getLength(); ++i) {
                linkedList.add(nodes.item(i));
            }
            return linkedList;
        }
        if (targetComponentType.isInterface()) {
            for (int i = 0; i < nodes.getLength(); ++i) {
                Object subprojection = invocationContext.getProjector().projectDOMNode(nodes.item(i), targetComponentType);
                linkedList.add(subprojection);
            }
            return linkedList;
        }
        throw new IllegalArgumentException("Return type " + targetComponentType + " is not valid for list or array component type returning from method " + method + " using the current type converter:" + invocationContext.getProjector().config().getTypeConverter()
                + ". Please change the return type to a sub projection or add a conversion to the type converter.");
    }

    /**
     * @param invocationContext
     *            invocation context
     * @param item
     * @param targetComponentType
     * @return node content as target type
     */
    @SuppressWarnings("unchecked")
    public static  E convertToComponentType(final InvocationContext invocationContext, final Node item, final Class targetComponentType) {
        TypeConverter typeConverter = invocationContext.getProjector().config().getTypeConverter();
        if (typeConverter.isConvertable(invocationContext.getTargetComponentType())) {
            return (E) typeConverter.convertTo(targetComponentType, item != null ? DOMHelper.directTextContent(item) : null, invocationContext.getExpressionFormatPattern());
        }
        if (Node.class.equals(targetComponentType)) {
            return (E) item;
        }
        if (targetComponentType.isInterface()) {
            if (item == null) {
                return null;
            }
            Object subprojection = invocationContext.getProjector().projectDOMNode(item, targetComponentType);
            return (E) subprojection;
        }
        throw new IllegalArgumentException("Return type " + targetComponentType + " is not valid for a ProjectedList using the current type converter:" + invocationContext.getProjector().config().getTypeConverter()
                + ". Please change the return type to a sub projection or add a conversion to the type converter.");
    }

    /**
     * @param componentType
     * @return map bound to the element resolved by XPath.
     * @see org.xmlbeam.evaluation.XPathEvaluator#asMapOf(java.lang.Class)
     */
    @Override
    public  XBAutoMap asMapOf(final Class componentType) {
        try {
            final Class callerClass = ReflectionHelper.getDirectCallerClass();
            Document document = documentProvider.resolve(componentType, callerClass);
            final String resolvedXPath = duplexExpression.getExpressionAsStringWithoutFormatPatterns();
            final XPath xpath = projector.config().createXPath(document);
            XPathExpression expression = xpath.compile(resolvedXPath);
//            final Node baseNode = (Node) expression.evaluate(document, XPathConstants.NODE);
//            if (baseNode.getNodeType() != Node.ELEMENT_NODE) {
//                throw new XBException("XPath expression does not resolve to an element. Maps can only be created for elements.");
//            }
            InvocationContext invocationContext = new InvocationContext(resolvedXPath, xpath, expression, duplexExpression, null, componentType, projector);
            return new AutoMap(document, invocationContext, componentType);
        } catch (IOException e) {
            throw new XBIOException(e);
        } catch (XPathExpressionException e) {
            throw new XBPathException(e, duplexExpression.getExpressionAsStringWithoutFormatPatterns());
        }
    }

}