/* * ============================================================================ * GNU Lesser General Public License * ============================================================================ * * Beanlet - JSE Application Container. * Copyright (C) 2006 Leon van Zantvoort * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. * * Leon van Zantvoort * 243 Acalanes Drive #11 * Sunnyvale, CA 94086 * USA * * zantvoort@users.sourceforge.net * http://beanlet.org */ package org.beanlet.common; import org.beanlet.plugin.ElementAnnotationContext; import org.beanlet.plugin.XMLElementAnnotation; import org.beanlet.plugin.ElementAnnotationFactory; import org.beanlet.BeanletValidationException; import static org.beanlet.common.BeanletConstants.*; import org.beanlet.annotation.AnnotationValueResolver; import org.beanlet.annotation.AnnotationProxy; import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.StringTokenizer; import javax.xml.namespace.QName; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.beanlet.annotation.ConstructorElement; import org.beanlet.annotation.ConstructorParameterElement; import org.beanlet.annotation.Element; import org.beanlet.annotation.FieldElement; import org.beanlet.annotation.MethodElement; import org.beanlet.annotation.MethodParameterElement; import org.beanlet.annotation.PackageElement; import org.beanlet.annotation.TypeElement; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * * @author Leon van Zantvoort */ public abstract class AbstractElementAnnotationFactory<T extends Annotation> implements ElementAnnotationFactory<T> { private static final XPath xpath; private static final XPathExpression ALL_EXPRESSION; private static final XPathExpression PACKAGE_ATTRIBUTE_EXPRESSION; private static final XPathExpression FIELD_ATTRIBUTE_EXPRESSION; private static final XPathExpression FIELD_ELEMENT_EXPRESSION; private static final XPathExpression METHOD_ATTRIBUTE_EXPRESSION; private static final XPathExpression METHOD_INDEX_EXPRESSION; private static final XPathExpression METHOD_ELEMENT_EXPRESSION; private static final XPathExpression METHOD_PARAMETER_ELEMENT_EXPRESSION; private static final XPathExpression METHOD_PARAMETER_INDEX_EXPRESSION; private static final XPathExpression METHOD_PARAMETERS_EXPRESSION; private static final XPathExpression CONSTRUCTOR_ATTRIBUTE_EXPRESSION; private static final XPathExpression CONSTRUCTOR_INDEX_EXPRESSION; private static final XPathExpression CONSTRUCTOR_ELEMENT_EXPRESSION; private static final XPathExpression CONSTRUCTOR_PARAMETER_ELEMENT_EXPRESSION; private static final XPathExpression CONSTRUCTOR_PARAMETER_INDEX_EXPRESSION; private static final XPathExpression CONSTRUCTOR_PARAMETERS_EXPRESSION; private static final XPathExpression PARAMETER_TYPE_EXPRESSION; static { xpath = XPathFactory.newInstance().newXPath(); xpath.setNamespaceContext(BEANLET_NAMESPACE_CONTEXT); try { ALL_EXPRESSION = xpath.compile("./*"); PACKAGE_ATTRIBUTE_EXPRESSION = xpath.compile("../@package"); FIELD_ATTRIBUTE_EXPRESSION = xpath.compile("@field"); FIELD_ELEMENT_EXPRESSION = xpath.compile("./:field/@name"); METHOD_ATTRIBUTE_EXPRESSION = xpath.compile("@method"); METHOD_INDEX_EXPRESSION = xpath.compile("../@index"); METHOD_ELEMENT_EXPRESSION = xpath.compile("./:method/@name"); METHOD_PARAMETER_ELEMENT_EXPRESSION = xpath.compile("./:method-parameter/@name"); METHOD_PARAMETER_INDEX_EXPRESSION = xpath.compile("../@index"); CONSTRUCTOR_ATTRIBUTE_EXPRESSION = xpath.compile("@constructor"); CONSTRUCTOR_INDEX_EXPRESSION = xpath.compile("../@index"); CONSTRUCTOR_ELEMENT_EXPRESSION = xpath.compile("./:constructor"); CONSTRUCTOR_PARAMETER_ELEMENT_EXPRESSION = xpath.compile("./:constructor-parameter"); CONSTRUCTOR_PARAMETER_INDEX_EXPRESSION = xpath.compile("@index"); CONSTRUCTOR_PARAMETERS_EXPRESSION = xpath.compile("./:parameters"); METHOD_PARAMETERS_EXPRESSION = xpath.compile("../:parameters"); PARAMETER_TYPE_EXPRESSION = xpath.compile("./:parameter/@type"); } catch (XPathExpressionException e) { throw new AssertionError(e); } } /** * Returns the annotation type supported by this factory. */ public abstract Class<T> annotationType(); /** * Returns the name of the xml element supported by this factory. */ public abstract String getNodeName(); /** * Returns the URI of the namespace supported by this factory, or * {@code null} if this factory is not namespace aware. */ public abstract String getNamespaceURI(); /** * This method is called if multiple elements are found for a given node. * Default implementation returns {@code true} */ public boolean isMatch(Element element, T annotation) { return true; } public PackageElement getPackageElement(Node node, Package pkg) { try { Node value = (Node) PACKAGE_ATTRIBUTE_EXPRESSION.evaluate(node, XPathConstants.NODE); if (value == null || pkg.getName().equals(value.getNodeValue())) { return PackageElement.instance(pkg); } } catch (XPathExpressionException e) { assert false : e; } return null; } public List<? extends Element> getElements(String beanletName, Node node, Class<?> cls) { QName q = XPathConstants.NODE; try { // Duplicate element restriction should be enforced by schema. Node fieldNode = (Node) FIELD_ATTRIBUTE_EXPRESSION.evaluate(node, q); if (fieldNode == null) { fieldNode = (Node) FIELD_ELEMENT_EXPRESSION.evaluate(node, q); } if (fieldNode != null) { return getFieldElements(beanletName, fieldNode, cls); } Node methodNode = (Node) METHOD_ATTRIBUTE_EXPRESSION.evaluate(node, q); if (methodNode == null) { methodNode = (Node) METHOD_ELEMENT_EXPRESSION.evaluate(node, q); } if (methodNode != null) { return getMethodElements(beanletName, methodNode, cls); } Node methodParameterNode = (Node) METHOD_PARAMETER_ELEMENT_EXPRESSION.evaluate(node, q); if (methodParameterNode != null) { return getMethodParameterElements(beanletName, methodParameterNode, cls); } Node constructorNode = (Node) CONSTRUCTOR_ATTRIBUTE_EXPRESSION.evaluate(node, q); if (constructorNode != null && !Boolean.valueOf(constructorNode.getNodeValue())) { constructorNode = null; } if (constructorNode == null) { constructorNode = (Node) CONSTRUCTOR_ELEMENT_EXPRESSION.evaluate(node, q); } if (constructorNode != null) { return getConstructorElements(beanletName, constructorNode, cls); } Node constructorParameterNode = (Node) CONSTRUCTOR_PARAMETER_ELEMENT_EXPRESSION.evaluate(node, q); if (constructorParameterNode != null) { return getConstructorParameterElements(beanletName, constructorParameterNode, cls); } return Arrays.asList(TypeElement.instance(cls)); } catch (XPathExpressionException e) { assert false : e; } return Collections.emptyList(); } /** * @param elementName name of the element as specified by the xml * element. Upper case characters are replaced by a dash followed by the * lower case character. */ public String getMappedName(String elementName) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < elementName.length(); i++) { char c = elementName.charAt(i); if (Character.isUpperCase(c)) { builder.append("-"); builder.append(Character.toLowerCase(c)); } else { builder.append(c); } } return builder.toString(); } /** * Extracts the annotation member value from xml. Return {@code null} to * use default method value. * * @param node xml annotation node. * @param elementName mapped xml element name. * @param type type of the annotation member. * @param parentValue value specified by parent annotation. * @param ctx * @return value from xml, to use parent or default method value. */ public Object getValueFromNode(Node node, String elementName, Class type, Object parentValue, ElementAnnotationContext ctx) throws Throwable { Node n = (Node) xpath.evaluate("@" + elementName, node, XPathConstants.NODE); Object v = null; if (n != null) { v = valueOf(n.getNodeValue(), type, ctx.getClassLoader()); } if (v == null) { v = parentValue; } return v; } public XMLElementAnnotation<PackageElement, T> getElementAnnotation( final Node node, Package pkg, ElementAnnotationContext ctx) { final PackageElement e = getPackageElement(node, pkg); if (e != null) { if (getNamespaceAwareNodeName(node).equals(getNodeName()) && (getNamespaceURI() == null || getNamespaceURI().equals(node.getNamespaceURI()))) { T p = null; if (ctx.getAnnotationDomain() != null) { p = ctx.getAnnotationDomain(). getDeclaration(annotationType()).getAnnotation(e); } final T t = AnnotationProxy.newProxyInstance(annotationType(), ctx.getClassLoader(), new AnnotationValueResolverImpl(node, p, ctx), true); return new XMLElementAnnotation<PackageElement, T>() { public Node getNode() { return node; } public PackageElement getElement() { return e; } public T getAnnotation() { return t; } public String toString() { return "{element=" + getElement() + ", annotation=" + getAnnotation() + "}"; } }; } } return null; } public XMLElementAnnotation<Element, T> getElementAnnotation( final Node node, Class<?> cls, ElementAnnotationContext ctx) { if (getNamespaceAwareNodeName(node).equals(getNodeName()) && (getNamespaceURI() == null || getNamespaceURI().equals(node.getNamespaceURI()))) { List<XMLElementAnnotation<Element, T>> matched = new ArrayList<XMLElementAnnotation<Element, T>>(); List<? extends Element> elements = getElements(ctx.getBeanletName(), node, cls); for (final Element e : elements) { T p = null; if (ctx.getAnnotationDomain() != null) { p = ctx.getAnnotationDomain(). getDeclaration(annotationType()).getAnnotation(e); } final T t = AnnotationProxy.newProxyInstance(annotationType(), ctx.getClassLoader(), new AnnotationValueResolverImpl(node, p, ctx), true); if (elements.size() > 1 && !isMatch(e, t)) { // Only verify if more than one elements are found. continue; } matched.add(new XMLElementAnnotation<Element, T>() { public Node getNode() { return node; } public Element getElement() { return e; } public T getAnnotation() { return t; } public String toString() { return "{element=" + getElement() + ", annotation=" + getAnnotation() + "}"; } }); } if (matched.isEmpty()) { if (elements.isEmpty()) { throw new BeanletValidationException(ctx.getBeanletName(), "No element found for '" + toString(node, 2) + "' at " + cls + "."); } else { throw new BeanletValidationException(ctx.getBeanletName(), "Elements found for '" + toString(node, 2) + "', " + "but none match annotation: " + elements + "."); } } else if (matched.size() > 1) { List<Element> list = new ArrayList<Element>(); for (XMLElementAnnotation e : matched) { list.add(e.getElement()); } throw new BeanletValidationException(ctx.getBeanletName(), "Ambiguous elements specified for '" + toString(node, 2) + "': " + list + "."); } else { return matched.get(0); } } return null; } private List<? extends Element> getFieldElements(final String beanletName, final Node node, final Class<?> cls) { return AccessController.doPrivileged(new PrivilegedAction<List<? extends Element>>() { public List<? extends Element> run() { List<FieldElement> elements = new ArrayList<FieldElement>(); Class<?> tmp = cls; do { // PERMISSION: java.lang.RuntimePermission accessDeclaredMembers for (Field f : tmp.getDeclaredFields()) { if (f.getName().equals(node.getNodeValue())) { FieldElement element = FieldElement.instance(f); assert !element.isHidden(cls); elements.add(element); break; } } } while (elements.isEmpty() && (tmp = tmp.getSuperclass()) != null); assert elements.size() <= 1; return elements; } }); } private List<? extends Element> getMethodElements( String beanletName, Node node, final Class<?> cls) { final List<Element> elements = new ArrayList<Element>(); try { final String methodName = node.getNodeValue(); final String[] parameters; Node parametersNode = (Node) METHOD_PARAMETERS_EXPRESSION.evaluate(node, XPathConstants.NODE); if (parametersNode != null) { NodeList list = (NodeList) PARAMETER_TYPE_EXPRESSION.evaluate( parametersNode, XPathConstants.NODESET); parameters = new String[list.getLength()]; for (int i = 0; i < parameters.length; i++) { parameters[i] = list.item(i).getNodeValue(); } } else { parameters = null; } final Integer index; Node indexNode = (Node) METHOD_INDEX_EXPRESSION.evaluate(node, XPathConstants.NODE); if (indexNode != null) { try { index = new Integer(indexNode.getNodeValue()); } catch (NumberFormatException e) { throw new BeanletValidationException(beanletName, "Failed to parse index: '" + methodName + "'.", e); } } else { index = null; } AccessController.doPrivileged(new PrivilegedAction<Object>() { public Object run() { Class<?> tmp = cls; do { // PERMISSION: java.lang.RuntimePermission accessDeclaredMembers for (Method m : tmp.getDeclaredMethods()) { if (m.getName().equals(methodName)) { final Element element; if (index == null) { element = MethodElement.instance(m); } else { if (index < m.getParameterTypes().length) { element = MethodParameterElement.instance(m, index); } else { continue; } } if (!element.isHidden(cls)) { if (parameters != null) { if (isExactMatch(m.getParameterTypes(), parameters)) { assert !element.isOverridden(cls); elements.add(element); } } else { if (!element.isOverridden(cls)) { elements.add(element); } } } } } } while ((parameters == null || elements.isEmpty()) && (tmp = tmp.getSuperclass()) != null); return null; } }); } catch (XPathExpressionException e) { assert false : e; } return elements; } private List<? extends Element> getMethodParameterElements( String beanletName, Node node, final Class<?> cls) { final List<MethodParameterElement> elements = new ArrayList<MethodParameterElement>(); try { final String methodName = node.getNodeValue(); final String[] parameters; Node parametersNode = (Node) METHOD_PARAMETERS_EXPRESSION.evaluate(node, XPathConstants.NODE); if (parametersNode != null) { NodeList list = (NodeList) PARAMETER_TYPE_EXPRESSION.evaluate( parametersNode, XPathConstants.NODESET); parameters = new String[list.getLength()]; for (int i = 0; i < parameters.length; i++) { parameters[i] = list.item(i).getNodeValue(); } } else { parameters = null; } final int index; Node indexNode = (Node) METHOD_PARAMETER_INDEX_EXPRESSION. evaluate(node, XPathConstants.NODE); assert indexNode != null; try { index = Integer.parseInt(indexNode.getNodeValue()); } catch (NumberFormatException e) { throw new BeanletValidationException(beanletName, "Failed to parse index: '" + methodName + "'.", e); } AccessController.doPrivileged(new PrivilegedAction<Object>() { public Object run() { Class<?> tmp = cls; do { // PERMISSION: java.lang.RuntimePermission accessDeclaredMembers for (Method m : tmp.getDeclaredMethods()) { if (m.getName().equals(methodName)) { MethodElement element = MethodElement. instance(m); if (!element.isHidden(cls)) { if (parameters != null) { if (isExactMatch(m.getParameterTypes(), parameters)) { assert !element.isOverridden(cls); elements.add(MethodParameterElement. instance(m, index)); } } else { if (!element.isOverridden(cls)) { elements.add(MethodParameterElement. instance(m, index)); } } } } } } while ((parameters == null || elements.isEmpty()) && (tmp = tmp.getSuperclass()) != null); return null; } }); } catch (XPathExpressionException e) { assert false : e; } return elements; } private List<? extends Element> getConstructorElements(String beanletName, Node node, final Class<?> cls) { final List<Element> elements = new ArrayList<Element>(); try { final String[] parameters; Node parametersNode = (Node) CONSTRUCTOR_PARAMETERS_EXPRESSION. evaluate(node, XPathConstants.NODE); if (parametersNode != null) { NodeList list = (NodeList) PARAMETER_TYPE_EXPRESSION.evaluate( parametersNode, XPathConstants.NODESET); parameters = new String[list.getLength()]; for (int i = 0; i < parameters.length; i++) { parameters[i] = list.item(i).getNodeValue(); } } else { parameters = null; } final Integer index; Node indexNode = (Node) CONSTRUCTOR_INDEX_EXPRESSION.evaluate(node, XPathConstants.NODE); if (indexNode != null) { try { index = new Integer(indexNode.getNodeValue()); } catch (NumberFormatException e) { throw new BeanletValidationException(beanletName, "Failed to parse constructor index: '" + indexNode.getNodeValue() + "'.", e); } } else { index = null; } AccessController.doPrivileged(new PrivilegedAction<Object>() { public Object run() { // PERMISSION: java.lang.RuntimePermission accessDeclaredMembers for (Constructor c : cls.getDeclaredConstructors()) { final Element element; if (index == null) { element = ConstructorElement.instance(c); } else { if (index < c.getParameterTypes().length) { element = ConstructorParameterElement.instance(c, index); } else { continue; } } if (!element.isHidden(cls)) { if (parameters != null) { if (isExactMatch(c.getParameterTypes(), parameters)) { elements.add(element); } } else { elements.add(element); } } } return null; } }); } catch (XPathExpressionException e) { assert false : e; } return elements; } private List<? extends Element> getConstructorParameterElements( String beanletName, Node node, final Class<?> cls) { final List<ConstructorParameterElement> elements = new ArrayList<ConstructorParameterElement>(); try { final String[] parameters; Node parametersNode = (Node) CONSTRUCTOR_PARAMETERS_EXPRESSION. evaluate(node, XPathConstants.NODE); if (parametersNode != null) { NodeList list = (NodeList) PARAMETER_TYPE_EXPRESSION.evaluate( parametersNode, XPathConstants.NODESET); parameters = new String[list.getLength()]; for (int i = 0; i < parameters.length; i++) { parameters[i] = list.item(i).getNodeValue(); } } else { parameters = null; } final int index; Node indexNode = (Node) CONSTRUCTOR_PARAMETER_INDEX_EXPRESSION. evaluate(node, XPathConstants.NODE); assert indexNode != null; try { index = Integer.parseInt(indexNode.getNodeValue()); } catch (NumberFormatException e) { throw new BeanletValidationException(beanletName, "Failed to parse index: '" + indexNode.getNodeValue() + "'.", e); } AccessController.doPrivileged(new PrivilegedAction<Object>() { public Object run() { // PERMISSION: java.lang.RuntimePermission accessDeclaredMembers for (Constructor c : cls.getDeclaredConstructors()) { ConstructorElement element = ConstructorElement. instance(c); assert !element.isHidden(cls); if (parameters != null) { if (isExactMatch(c.getParameterTypes(), parameters)) { elements.add(ConstructorParameterElement. instance(c, index)); } } else { elements.add(ConstructorParameterElement. instance(c, index)); } } return null; } }); } catch (XPathExpressionException e) { assert false : e; } return elements; } private boolean isExactMatch(Class<?>[] memberParameterTypes, String[] xmlParameterTypes) { if (memberParameterTypes.length != xmlParameterTypes.length) { return false; } boolean match = true; for (int i = 0; i < memberParameterTypes.length; i++) { if (!memberParameterTypes[i].getCanonicalName(). equals(xmlParameterTypes[i])) { match = false; break; } } return match; } private static String getNamespaceAwareNodeName(Node node) { // PENDING: find a better way to remove namespace prefix. String nodeName = node.getNodeName(); int ix = nodeName.indexOf(":"); if (ix != -1) { nodeName = nodeName.substring(ix + 1); } return nodeName; } private class AnnotationValueResolverImpl implements AnnotationValueResolver { private final Node node; private final T parent; private final ElementAnnotationContext ctx; public AnnotationValueResolverImpl(Node node, T parent, ElementAnnotationContext ctx) { this.node = node; this.parent = parent; this.ctx = ctx; } public Object getValue(Method method, ClassLoader loader) throws Throwable { String mappedName = getMappedName(method.getName()); try { Object parentValue = null; if (parent != null) { parentValue = method.invoke(parent); } Object v = getValueFromNode(node, mappedName, method.getReturnType(), parentValue, ctx); if (v == null) { v = method.getDefaultValue(); } return v; } catch (InvocationTargetException e) { throw e.getTargetException(); } } } public static String toString(Node node, int level) { if (level == 0) { return "<!-- ... -->"; } try { StringBuilder builder = new StringBuilder(); builder.append("<" + node.getNodeName()); NamedNodeMap map = node.getAttributes(); for (int i = 0; i < map.getLength(); i++) { Node n = map.item(i); builder.append(" "); builder.append(n.getNodeName()); builder.append("=\""); builder.append(n.getNodeValue()); builder.append("\""); } NodeList list = (NodeList) ALL_EXPRESSION.evaluate(node, XPathConstants.NODESET); if (list.getLength() > 0) { builder.append(">"); if (level > 1) { for (int i = 0; i < list.getLength(); i++) { builder.append(toString(list.item(i), level - 1)); } } else { builder.append("<!-- ... -->"); } builder.append("</" + node.getNodeName() + ">"); } else { builder.append("/>"); } return builder.toString(); } catch (XPathExpressionException ex) { return "<!-- Cannot parse " + node.getNodeName() + ". -->"; } } /** * The default implementation of this method is restricted to * {@code String}s, primitives, arrays of primitives, * {@code Class}es, arrays of {@code Class}es, * {@code Enum}s and arrays of {@code Enum}s */ public static Object valueOf(String str, Class type, ClassLoader loader) throws Throwable { final Object o; if (type.isPrimitive()) { final Class<?> objectType; if (Boolean.TYPE.equals(type)) { objectType = Boolean.class; } else if (Byte.TYPE.equals(type)) { objectType = Byte.class; } else if (Short.TYPE.equals(type)) { objectType = Short.class; } else if (Integer.TYPE.equals(type)) { objectType = Integer.class; } else if (Long.TYPE.equals(type)) { objectType = Long.class; } else if (Float.TYPE.equals(type)) { objectType = Float.class; } else if (Double.TYPE.equals(type)) { objectType = Double.class; } else { throw new AssertionError(type); } try { o = str.length() == 0 ? null : objectType.getMethod( "valueOf", String.class).invoke(null, str); } catch (InvocationTargetException e) { throw e.getTargetException(); } } else if (type.isEnum()) { if (str.length() != 0) { Object tmp = null; try { tmp = Enum.class.getMethod("valueOf", Class.class, String.class).invoke(null, type, str); } catch (InvocationTargetException e) { try { throw e.getTargetException(); } catch (RuntimeException e2) { throw e2; } catch (Exception e2) { assert false : e; } } o = tmp; } else { o = null; } } else if (String.class.equals(type)) { o = str; } else if (Class.class.equals(type)) { o = loader.loadClass(str); } else if (type.isAnnotation()) { o = null; } else if (type.isArray()) { String[] list = str.split("\"(?<!\\\\), "); Class<?> componentType = type.getComponentType(); if (componentType.isPrimitive() || componentType.isEnum() || componentType.equals(Class.class) || componentType.equals(String.class)) { int len = list.length; o = Array.newInstance(componentType, len); for (int i = 0; i < len; i++) { ((Object[]) o)[i] = valueOf(list[i], componentType, loader); } } else { o = null; } } else { o = null; } return o; } }