![JAR search and dependency download from the Maven repository](/logo.png)
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