/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.cxf.aegis.type; import java.beans.PropertyDescriptor; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.XMLConstants; import javax.xml.namespace.QName; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import javax.xml.xpath.XPathConstants; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.ErrorHandler; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.apache.cxf.aegis.DatabindingException; import org.apache.cxf.aegis.type.basic.BeanType; import org.apache.cxf.aegis.type.basic.XMLBeanTypeInfo; import org.apache.cxf.aegis.type.java5.Java5TypeCreator; import org.apache.cxf.aegis.util.NamespaceHelper; import org.apache.cxf.common.classloader.ClassLoaderUtils; import org.apache.cxf.common.logging.LogUtils; import org.apache.cxf.helpers.DOMUtils; import org.apache.cxf.helpers.XPathUtils; /** * Deduce mapping information from an xml file. The xml file should be in the * same packages as the class, with the name <code>className.aegis.xml</code>. * For example, given the following service interface: <p/> * * <pre> * public Collection getResultsForValues(String id, Collection values); //method 1 * * public Collection getResultsForValues(int id, Collection values); //method 2 * * public String getResultForValue(String value); //method 3 * </pre> * * An example of the type xml is: * * <pre> * <mappings> * <mapping> * <method name="getResultsForValues"> * <return-type componentType="com.acme.ResultBean" /> * <!-- no need to specify index 0, since it's a String --> * <parameter index="1" componentType="java.lang.String" /> * </method> * </mapping> * </mappings> * </pre> * * <p/> Note that for values which can be easily deduced (such as the String * parameter, or the second service method) no mapping need be specified in the * xml descriptor, which is why no mapping is specified for method 3. <p/> * However, if you have overloaded methods with different semantics, then you * will need to specify enough parameters to disambiguate the method and * uniquely identify it. So in the example above, the mapping specifies will * apply to both method 1 and method 2, since the parameter at index 0 is not * specified. */ public class XMLTypeCreator extends AbstractTypeCreator { private static final Logger LOG = LogUtils.getL7dLogger(XMLTypeCreator.class); private static List<Class<?>> stopClasses = new ArrayList<Class<?>>(); static { stopClasses.add(Object.class); stopClasses.add(Exception.class); stopClasses.add(RuntimeException.class); stopClasses.add(Throwable.class); } private static final DocumentBuilderFactory AEGIS_DOCUMENT_BUILDER_FACTORY; // cache of classes to documents private Map<String, Document> documents = new HashMap<>(); static { AEGIS_DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); AEGIS_DOCUMENT_BUILDER_FACTORY.setNamespaceAware(true); String path = "/META-INF/cxf/aegis.xsd"; InputStream is = XMLTypeCreator.class.getResourceAsStream(path); if (is != null) { try { SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); Schema aegisSchema = schemaFactory.newSchema(new StreamSource(is)); AEGIS_DOCUMENT_BUILDER_FACTORY.setSchema(aegisSchema); } catch (Throwable e) { String msg = "Could not set aegis schema. Not validating."; if (LOG.isLoggable(Level.FINE)) { LOG.log(Level.INFO, msg, e); } else { LOG.log(Level.INFO, msg); } } finally { try { is.close(); } catch (IOException ex) { //ignore } } } } private volatile XPathUtils xpathUtils; private synchronized XPathUtils getXPathUtils() { if (xpathUtils == null) { xpathUtils = new XPathUtils(); } return xpathUtils; } private Document readAegisFile(InputStream is, final String path) throws IOException { DocumentBuilder documentBuilder; try { documentBuilder = AEGIS_DOCUMENT_BUILDER_FACTORY.newDocumentBuilder(); } catch (ParserConfigurationException e) { LOG.log(Level.SEVERE, "Unable to create a document builder, e"); throw new RuntimeException("Unable to create a document builder, e"); } org.w3c.dom.Document doc; documentBuilder.setErrorHandler(new ErrorHandler() { private String errorMessage(SAXParseException exception) { return MessageFormat.format("{0} at {1} line {2} column {3}.", new Object[] {exception.getMessage(), path, Integer.valueOf(exception.getLineNumber()), Integer.valueOf(exception.getColumnNumber())}); } private void throwDatabindingException(String message) { //DatabindingException is quirky. This dance is required to get the full message //to where it belongs. DatabindingException e = new DatabindingException(message); e.setMessage(message); throw e; } public void error(SAXParseException exception) throws SAXException { String message = errorMessage(exception); LOG.log(Level.SEVERE, message, exception); throwDatabindingException(message); } public void fatalError(SAXParseException exception) throws SAXException { String message = errorMessage(exception); LOG.log(Level.SEVERE, message, exception); throwDatabindingException(message); } public void warning(SAXParseException exception) throws SAXException { LOG.log(Level.INFO, errorMessage(exception), exception); } }); try { doc = documentBuilder.parse(is); } catch (SAXException e) { LOG.log(Level.SEVERE, "Error parsing Aegis file.", e); // can't happen due to // above. return null; } return doc; } protected Document getDocument(Class<?> clazz) { if (clazz == null) { return null; } if (documents.containsKey(clazz.getName())) { return documents.get(clazz.getName()); } String path = '/' + clazz.getName().replace('.', '/') + ".aegis.xml"; InputStream is = clazz.getResourceAsStream(path); if (is == null) { documents.put(clazz.getName(), null); LOG.finest("Mapping file : " + path + " not found."); return null; } LOG.finest("Found mapping file : " + path); try { Document doc = readAegisFile(is, path); documents.put(clazz.getName(), doc); return doc; } catch (IOException e) { LOG.log(Level.SEVERE, "Error loading file " + path, e); documents.put(clazz.getName(), null); return null; } } @Override protected boolean isEnum(Class<?> javaType) { Element mapping = findMapping(javaType); if (mapping != null) { return super.isEnum(javaType); } else { return nextCreator.isEnum(javaType); } } @Override public AegisType createEnumType(TypeClassInfo info) { Element mapping = findMapping(info.getType()); if (mapping != null) { return super.createEnumType(info); } else { return nextCreator.createEnumType(info); } } @Override public AegisType createCollectionType(TypeClassInfo info) { /* If it is a parameterized type, then we already know * the parameter(s) and we don't need to fish them out of the XML. */ if (info.getType() instanceof Class) { return createCollectionTypeFromGeneric(info); } return nextCreator.createCollectionType(info); } @Override public TypeClassInfo createClassInfo(PropertyDescriptor pd) { Element mapping = findMapping(pd.getReadMethod().getDeclaringClass()); if (mapping == null) { return nextCreator.createClassInfo(pd); } Element propertyEl = getMatch(mapping, "./property[@name='" + pd.getName() + "']"); if (propertyEl == null) { return nextCreator.createClassInfo(pd); } TypeClassInfo info = new TypeClassInfo(); Type returnType = pd.getReadMethod().getGenericReturnType(); info.setType(returnType); info.setDescription("property " + pd.getDisplayName()); readMetadata(info, mapping, propertyEl); return info; } protected Element findMapping(Type type) { // We are not prepared to find .aegis.xml files for Parameterized types. Class<?> clazz = TypeUtil.getTypeClass(type, false); if (clazz == null) { return null; } Document doc = getDocument(clazz); if (doc == null) { return null; } Element mapping = getMatch(doc, "/mappings/mapping[@uri='" + getTypeMapping().getMappingIdentifierURI() + "']"); if (mapping == null) { mapping = getMatch(doc, "/mappings/mapping[not(@uri)]"); } return mapping; } protected List<Element> findMappings(Type type) { Class<?> clazz = TypeUtil.getTypeClass(type, false); List<Element> mappings = new ArrayList<>(); if (clazz == null) { return mappings; } Element top = findMapping(clazz); if (top != null) { mappings.add(top); } Class<?> parent = clazz; while (true) { // Read mappings for interfaces as well Class<?>[] interfaces = parent.getInterfaces(); for (int i = 0; i < interfaces.length; i++) { Class<?> interfaze = interfaces[i]; List<Element> interfaceMappings = findMappings(interfaze); mappings.addAll(interfaceMappings); } Class<?> sup = parent.getSuperclass(); if (sup == null || stopClasses.contains(sup)) { break; } Element mapping = findMapping(sup); if (mapping != null) { mappings.add(mapping); } parent = sup; } return mappings; } @Override public AegisType createDefaultType(TypeClassInfo info) { Element mapping = findMapping(info.getType()); List<Element> mappings = findMappings(info.getType()); Class<?> relatedClass = TypeUtil.getTypeRelatedClass(info.getType()); if (mapping != null || !mappings.isEmpty()) { String typeNameAtt = null; if (mapping != null) { typeNameAtt = DOMUtils.getAttributeValueEmptyNull(mapping, "name"); } String extensibleElements = null; if (mapping != null) { extensibleElements = mapping.getAttribute("extensibleElements"); } String extensibleAttributes = null; if (mapping != null) { extensibleAttributes = mapping.getAttribute("extensibleAttributes"); } String defaultNS = NamespaceHelper.makeNamespaceFromClassName(relatedClass.getName(), "http"); QName name = null; if (typeNameAtt != null) { name = NamespaceHelper.createQName(mapping, typeNameAtt, defaultNS); defaultNS = name.getNamespaceURI(); } // We do not deal with Generic beans at this point. XMLBeanTypeInfo btinfo = new XMLBeanTypeInfo(relatedClass, mappings, defaultNS); btinfo.setTypeMapping(getTypeMapping()); btinfo.setDefaultMinOccurs(getConfiguration().getDefaultMinOccurs()); btinfo.setDefaultNillable(getConfiguration().isDefaultNillable()); if (extensibleElements != null) { btinfo.setExtensibleElements(Boolean.valueOf(extensibleElements).booleanValue()); } else { btinfo.setExtensibleElements(getConfiguration().isDefaultExtensibleElements()); } if (extensibleAttributes != null) { btinfo.setExtensibleAttributes(Boolean.valueOf(extensibleAttributes).booleanValue()); } else { btinfo.setExtensibleAttributes(getConfiguration().isDefaultExtensibleAttributes()); } btinfo.setQualifyAttributes(this.getConfiguration().isQualifyAttributes()); btinfo.setQualifyElements(this.getConfiguration().isQualifyElements()); BeanType type = new BeanType(btinfo); if (name == null) { name = createQName(relatedClass); } type.setSchemaType(name); type.setTypeClass(info.getType()); type.setTypeMapping(getTypeMapping()); return type; } else { return nextCreator.createDefaultType(info); } } @Override public TypeClassInfo createClassInfo(Method m, int index) { Element mapping = findMapping(m.getDeclaringClass()); if (mapping == null) { return nextCreator.createClassInfo(m, index); } // find the elements that apply to the specified method TypeClassInfo info = nextCreator.createClassInfo(m, index); // start // with the // java5 // (or whatever) version. if (info == null) { info = new TypeClassInfo(); } info.setDescription("method " + m.getName() + " parameter " + index); if (index >= 0) { if (index >= m.getParameterTypes().length) { throw new DatabindingException("Method " + m + " does not have a parameter at index " + index); } // we don't want nodes for which the specified index is not // specified List<Element> nodes = getMatches(mapping, "./method[@name='" + m.getName() + "']/parameter[@index='" + index + "']/parent::*"); if (nodes.size() == 0) { // no mapping for this method return info; } // pick the best matching node Element bestMatch = getBestMatch(mapping, m, nodes); if (bestMatch == null) { // no mapping for this method return info; } info.setType(m.getGenericParameterTypes()[index]); // info.setAnnotations(m.getParameterAnnotations()[index]); Element parameter = getMatch(bestMatch, "parameter[@index='" + index + "']"); readMetadata(info, mapping, parameter); } else { List<Element> nodes = getMatches(mapping, "./method[@name='" + m.getName() + "']/return-type/parent::*"); if (nodes.size() == 0) { return info; } Element bestMatch = getBestMatch(mapping, m, nodes); if (bestMatch == null) { // no mapping for this method return info; } info.setType(m.getGenericReturnType()); // info.setAnnotations(m.getAnnotations()); Element rtElement = DOMUtils.getFirstChildWithName(bestMatch, "", "return-type"); readMetadata(info, mapping, rtElement); } return info; } protected void readMetadata(TypeClassInfo info, Element mapping, Element parameter) { info.setTypeName(createQName(parameter, DOMUtils.getAttributeValueEmptyNull(parameter, "typeName"))); info.setMappedName(createQName(parameter, DOMUtils.getAttributeValueEmptyNull(parameter, "mappedName"))); Class<?> relatedClass = TypeUtil.getTypeRelatedClass(info.getType()); // we only mess with the generic issues for list and map if (Collection.class.isAssignableFrom(relatedClass)) { Type componentType = getComponentType(mapping, parameter); if (componentType != null) { // there is actually XML config. Type fullType = ParameterizedTypeFactory.createParameterizedType(relatedClass, new Type[] {componentType}); info.setType(fullType); } } else if (Map.class.isAssignableFrom(relatedClass)) { Type keyType = getKeyType(mapping, parameter); if (keyType != null) { info.setKeyType(keyType); } Type valueType = getValueType(mapping, parameter); if (valueType != null) { info.setValueType(valueType); } // if the XML only specifies one, we expect the other to come from a full // parameterized type. if (keyType != null || valueType != null) { if (keyType == null || valueType == null) { if (keyType == null) { keyType = TypeUtil.getSingleTypeParameter(info.getType(), 0); } if (keyType == null) { keyType = Object.class; } if (valueType == null) { valueType = TypeUtil.getSingleTypeParameter(info.getType(), 1); } if (valueType == null) { valueType = Object.class; } } Type fullType = ParameterizedTypeFactory.createParameterizedType(relatedClass, new Type[] {keyType, valueType}); info.setType(fullType); } } setType(info, parameter); String min = DOMUtils.getAttributeValueEmptyNull(parameter, "minOccurs"); if (min != null) { info.setMinOccurs(Long.parseLong(min)); } String max = DOMUtils.getAttributeValueEmptyNull(parameter, "maxOccurs"); if (max != null) { info.setMaxOccurs(Long.parseLong(max)); } String flat = DOMUtils.getAttributeValueEmptyNull(parameter, "flat"); if (flat != null) { info.setFlat(Boolean.valueOf(flat.toLowerCase()).booleanValue()); } String nillable = DOMUtils.getAttributeValueEmptyNull(parameter, "nillable"); if (nillable != null) { info.setNillable(Boolean.valueOf(nillable.toLowerCase()).booleanValue()); } } @Override protected AegisType getOrCreateGenericType(TypeClassInfo info) { AegisType type = null; if (info.getType() instanceof ParameterizedType) { type = createTypeFromGeneric(info.getType()); } if (type == null) { type = super.getOrCreateGenericType(info); } return type; } private AegisType createTypeFromGeneric(Object cType) { if (cType instanceof TypeClassInfo) { return createTypeForClass((TypeClassInfo)cType); } else if (cType instanceof Class) { return createType((Class<?>)cType); } else { return null; } } @Override protected AegisType getOrCreateMapKeyType(TypeClassInfo info) { AegisType type = null; if (info.getKeyType() != null) { type = createTypeFromGeneric(info.getKeyType()); } if (type == null) { type = super.getOrCreateMapKeyType(info); } return type; } @Override protected AegisType getOrCreateMapValueType(TypeClassInfo info) { AegisType type = null; if (info.getType() instanceof ParameterizedType) { // well, let's hope that someone has filled in the value type. type = createTypeFromGeneric(info.getValueType()); } if (type == null) { type = super.getOrCreateMapValueType(info); } return type; } private Type getComponentType(Element mapping, Element parameter) { String componentSpec = DOMUtils.getAttributeValueEmptyNull(parameter, "componentType"); if (componentSpec == null) { return null; } return getGenericParameterFromSpec(mapping, componentSpec); } private Type getKeyType(Element mapping, Element parameter) { String spec = DOMUtils.getAttributeValueEmptyNull(parameter, "keyType"); if (spec == null) { return null; } return getGenericParameterFromSpec(mapping, spec); } private Type getValueType(Element mapping, Element parameter) { String spec = DOMUtils.getAttributeValueEmptyNull(parameter, "valueType"); if (spec == null) { return null; } return getGenericParameterFromSpec(mapping, spec); } // This cannot do List<List<x>>. private Type getGenericParameterFromSpec(Element mapping, String componentType) { if (componentType.startsWith("#")) { String name = componentType.substring(1); Element propertyEl = getMatch(mapping, "./component[@name='" + name + "']"); if (propertyEl == null) { throw new DatabindingException("Could not find <component> element in mapping named '" + name + "'"); } String className = DOMUtils.getAttributeValueEmptyNull(propertyEl, "class"); if (className == null) { throw new DatabindingException("A 'class' attribute must be specified for <component> " + name); } return loadComponentClass(className); } else { return loadComponentClass(componentType); } } private Class<?> loadComponentClass(String componentType) { try { return ClassLoaderUtils.loadClass(componentType, getClass()); } catch (ClassNotFoundException e) { throw new DatabindingException("Unable to load component type class " + componentType, e); } } protected void setType(TypeClassInfo info, Element parameter) { String type = DOMUtils.getAttributeValueEmptyNull(parameter, "type"); if (type != null) { try { Class<?> aegisTypeClass = ClassLoaderUtils.loadClass(type, getClass()); info.setAegisTypeClass(Java5TypeCreator.castToAegisTypeClass(aegisTypeClass)); } catch (ClassNotFoundException e) { throw new DatabindingException("Unable to load type class " + type, e); } } } private Element getBestMatch(Element mapping, Method method, List<Element> availableNodes) { // first find all the matching method names List<Element> nodes = getMatches(mapping, "./method[@name='" + method.getName() + "']"); // remove the ones that aren't in our acceptable set, if one is // specified if (availableNodes != null) { nodes.retainAll(availableNodes); } // no name found, so no matches if (nodes.size() == 0) { return null; } // if the method has no params, then more than one mapping is pointless Class<?>[] parameterTypes = method.getParameterTypes(); if (parameterTypes.length == 0) { return nodes.get(0); } // here's the fun part. // we go through the method parameters, ruling out matches for (int i = 0; i < parameterTypes.length; i++) { Class<?> parameterType = parameterTypes[i]; for (Iterator<Element> iterator = nodes.iterator(); iterator.hasNext();) { Element element = iterator.next(); // first we check if the parameter index is specified Element match = getMatch(element, "parameter[@index='" + i + "']"); if (match != null // we check if the type is specified and matches && DOMUtils.getAttributeValueEmptyNull(match, "class") != null // if it doesn't match, then we can definitely rule out // this result && !DOMUtils.getAttributeValueEmptyNull(match, "class").equals(parameterType.getName())) { iterator.remove(); } } } // if we have just one node left, then it has to be the best match if (nodes.size() == 1) { return nodes.get(0); } // all remaining definitions could apply, so we need to now pick the // best one // the best one is the one with the most parameters specified Element bestCandidate = null; int highestSpecified = 0; for (Iterator<Element> iterator = nodes.iterator(); iterator.hasNext();) { Element element = iterator.next(); List<Element> params = DOMUtils.getChildrenWithName(element, "", "parameter"); int availableParameters = params.size(); if (availableParameters > highestSpecified) { bestCandidate = element; highestSpecified = availableParameters; } } return bestCandidate; } private Element getMatch(Node doc, String xpath) { return (Element)getXPathUtils().getValue(xpath, doc, XPathConstants.NODE); } private List<Element> getMatches(Node doc, String xpath) { NodeList nl = (NodeList)getXPathUtils().getValue(xpath, doc, XPathConstants.NODESET); List<Element> r = new ArrayList<>(); for (int x = 0; x < nl.getLength(); x++) { r.add((Element)nl.item(x)); } return r; } /** * Creates a QName from a string, such as "ns:Element". */ protected QName createQName(Element e, String value) { if (value == null || value.length() == 0) { return null; } int index = value.indexOf(":"); if (index == -1) { return new QName(getTypeMapping().getMappingIdentifierURI(), value); } String prefix = value.substring(0, index); String localName = value.substring(index + 1); String ns = DOMUtils.getNamespace(e, prefix); if (ns == null || localName == null) { throw new DatabindingException("Invalid QName in mapping: " + value); } return new QName(ns, localName, prefix); } }