001: /**
002: * Licensed to the Apache Software Foundation (ASF) under one
003: * or more contributor license agreements. See the NOTICE file
004: * distributed with this work for additional information
005: * regarding copyright ownership. The ASF licenses this file
006: * to you under the Apache License, Version 2.0 (the
007: * "License"); you may not use this file except in compliance
008: * with the License. You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing,
013: * software distributed under the License is distributed on an
014: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015: * KIND, either express or implied. See the License for the
016: * specific language governing permissions and limitations
017: * under the License.
018: */package org.apache.cxf.aegis.type;
019:
020: import java.beans.PropertyDescriptor;
021: import java.io.InputStream;
022: import java.lang.reflect.Method;
023: import java.util.ArrayList;
024: import java.util.HashMap;
025: import java.util.Iterator;
026: import java.util.List;
027: import java.util.Map;
028:
029: import javax.xml.namespace.QName;
030: import javax.xml.stream.XMLStreamException;
031:
032: import org.apache.commons.logging.Log;
033: import org.apache.commons.logging.LogFactory;
034: import org.apache.cxf.aegis.DatabindingException;
035: import org.apache.cxf.aegis.type.basic.BeanType;
036: import org.apache.cxf.aegis.type.basic.XMLBeanTypeInfo;
037: import org.apache.cxf.aegis.util.NamespaceHelper;
038: import org.apache.cxf.aegis.util.jdom.StaxBuilder;
039: import org.apache.cxf.common.classloader.ClassLoaderUtils;
040: import org.jdom.Document;
041: import org.jdom.Element;
042: import org.jdom.JDOMException;
043: import org.jdom.Namespace;
044: import org.jdom.xpath.XPath;
045:
046: /**
047: * Deduce mapping information from an xml file. The xml file should be in the
048: * same packages as the class, with the name <code>className.aegis.xml</code>.
049: * For example, given the following service interface: <p/>
050: *
051: * <pre>
052: * public Collection getResultsForValues(String id, Collection values); //method 1
053: *
054: * public Collection getResultsForValues(int id, Collection values); //method 2
055: *
056: * public String getResultForValue(String value); //method 3
057: * </pre>
058: *
059: * An example of the type xml is:
060: *
061: * <pre>
062: * <mappings>
063: * <mapping>
064: * <method name="getResultsForValues">
065: * <return-type componentType="com.acme.ResultBean" />
066: * <!-- no need to specify index 0, since it's a String -->
067: * <parameter index="1" componentType="java.lang.String" />
068: * </method>
069: * </mapping>
070: * </mappings>
071: * </pre>
072: *
073: * <p/> Note that for values which can be easily deduced (such as the String
074: * parameter, or the second service method) no mapping need be specified in the
075: * xml descriptor, which is why no mapping is specified for method 3. <p/>
076: * However, if you have overloaded methods with different semantics, then you
077: * will need to specify enough parameters to disambiguate the method and
078: * uniquely identify it. So in the example above, the mapping specifies will
079: * apply to both method 1 and method 2, since the parameter at index 0 is not
080: * specified.
081: *
082: */
083: public class XMLTypeCreator extends AbstractTypeCreator {
084: private static final Log LOG = LogFactory
085: .getLog(XMLTypeCreator.class);
086: private static List<Class> stopClasses = new ArrayList<Class>();
087: static {
088: stopClasses.add(Object.class);
089: stopClasses.add(Exception.class);
090: stopClasses.add(RuntimeException.class);
091: stopClasses.add(Throwable.class);
092: }
093:
094: // cache of classes to documents
095: private Map<String, Document> documents = new HashMap<String, Document>();
096:
097: protected Document getDocument(Class clazz) {
098: if (clazz == null) {
099: return null;
100: }
101: Document doc = documents.get(clazz.getName());
102: if (doc != null) {
103: return doc;
104: }
105: String path = '/' + clazz.getName().replace('.', '/')
106: + ".aegis.xml";
107: InputStream is = clazz.getResourceAsStream(path);
108: if (is == null) {
109: LOG.debug("Mapping file : " + path + " not found.");
110: return null;
111: }
112: LOG.debug("Found mapping file : " + path);
113: try {
114: doc = new StaxBuilder().build(is);
115: documents.put(clazz.getName(), doc);
116: return doc;
117: } catch (XMLStreamException e) {
118: LOG.error("Error loading file " + path, e);
119: }
120: return null;
121: }
122:
123: @Override
124: protected boolean isEnum(Class javaType) {
125: Element mapping = findMapping(javaType);
126: if (mapping != null) {
127: return super .isEnum(javaType);
128: } else {
129: return nextCreator.isEnum(javaType);
130: }
131: }
132:
133: @Override
134: public Type createEnumType(TypeClassInfo info) {
135: Element mapping = findMapping(info.getTypeClass());
136: if (mapping != null) {
137: return super .createEnumType(info);
138: } else {
139: return nextCreator.createEnumType(info);
140: }
141: }
142:
143: @Override
144: public Type createCollectionType(TypeClassInfo info) {
145: if (info.getGenericType() instanceof Class
146: || info.getGenericType() instanceof TypeClassInfo) {
147: return createCollectionTypeFromGeneric(info);
148: }
149:
150: return nextCreator.createCollectionType(info);
151: }
152:
153: @Override
154: public TypeClassInfo createClassInfo(PropertyDescriptor pd) {
155: Element mapping = findMapping(pd.getReadMethod()
156: .getDeclaringClass());
157: if (mapping == null) {
158: return nextCreator.createClassInfo(pd);
159: }
160:
161: Element propertyEl = getMatch(mapping, "./property[@name='"
162: + pd.getName() + "']");
163: if (propertyEl == null) {
164: return nextCreator.createClassInfo(pd);
165: }
166:
167: TypeClassInfo info = new TypeClassInfo();
168: info.setTypeClass(pd.getReadMethod().getReturnType());
169: readMetadata(info, mapping, propertyEl);
170:
171: return info;
172: }
173:
174: protected Element findMapping(Class clazz) {
175: Document doc = getDocument(clazz);
176: if (doc == null) {
177: return null;
178: }
179:
180: Element mapping = getMatch(doc, "/mappings/mapping[@uri='"
181: + getTypeMapping().getEncodingStyleURI() + "']");
182: if (mapping == null) {
183: mapping = getMatch(doc, "/mappings/mapping[not(@uri)]");
184: }
185:
186: return mapping;
187: }
188:
189: protected List<Element> findMappings(Class clazz) {
190: List<Element> mappings = new ArrayList<Element>();
191:
192: Element top = findMapping(clazz);
193: if (top != null) {
194: mappings.add(top);
195: }
196:
197: Class parent = clazz;
198: while (true) {
199:
200: // Read mappings for interfaces as well
201: Class[] interfaces = parent.getInterfaces();
202: for (int i = 0; i < interfaces.length; i++) {
203: Class interfaze = interfaces[i];
204: List<Element> interfaceMappings = findMappings(interfaze);
205: mappings.addAll(interfaceMappings);
206: }
207:
208: Class sup = parent.getSuperclass();
209:
210: if (sup == null || stopClasses.contains(sup)) {
211: break;
212: }
213:
214: Element mapping = findMapping(sup);
215: if (mapping != null) {
216: mappings.add(mapping);
217: }
218:
219: parent = sup;
220: }
221:
222: return mappings;
223: }
224:
225: @Override
226: public Type createDefaultType(TypeClassInfo info) {
227: Element mapping = findMapping(info.getTypeClass());
228: List mappings = findMappings(info.getTypeClass());
229:
230: if (mapping != null || mappings.size() > 0) {
231: String typeNameAtt = null;
232: if (mapping != null) {
233: typeNameAtt = mapping.getAttributeValue("name");
234: }
235:
236: String extensibleElements = null;
237: if (mapping != null) {
238: extensibleElements = mapping
239: .getAttributeValue("extensibleElements");
240: }
241:
242: String extensibleAttributes = null;
243: if (mapping != null) {
244: extensibleAttributes = mapping
245: .getAttributeValue("extensibleAttributes");
246: }
247:
248: String defaultNS = NamespaceHelper
249: .makeNamespaceFromClassName(info.getTypeClass()
250: .getName(), "http");
251: QName name = null;
252: if (typeNameAtt != null) {
253: name = NamespaceHelper.createQName(mapping,
254: typeNameAtt, defaultNS);
255:
256: defaultNS = name.getNamespaceURI();
257: }
258:
259: XMLBeanTypeInfo btinfo = new XMLBeanTypeInfo(info
260: .getTypeClass(), mappings, defaultNS);
261: btinfo.setTypeMapping(getTypeMapping());
262: btinfo.setDefaultMinOccurs(getConfiguration()
263: .getDefaultMinOccurs());
264: btinfo.setDefaultNillable(getConfiguration()
265: .isDefaultNillable());
266:
267: if (extensibleElements != null) {
268: btinfo.setExtensibleElements(Boolean.valueOf(
269: extensibleElements).booleanValue());
270: } else {
271: btinfo.setExtensibleElements(getConfiguration()
272: .isDefaultExtensibleElements());
273: }
274:
275: if (extensibleAttributes != null) {
276: btinfo.setExtensibleAttributes(Boolean.valueOf(
277: extensibleAttributes).booleanValue());
278: } else {
279: btinfo.setExtensibleAttributes(getConfiguration()
280: .isDefaultExtensibleAttributes());
281: }
282:
283: BeanType type = new BeanType(btinfo);
284:
285: if (name == null) {
286: name = createQName(info.getTypeClass());
287: }
288:
289: type.setSchemaType(name);
290:
291: type.setTypeClass(info.getTypeClass());
292: type.setTypeMapping(getTypeMapping());
293:
294: return type;
295: } else {
296: return nextCreator.createDefaultType(info);
297: }
298: }
299:
300: @Override
301: public TypeClassInfo createClassInfo(Method m, int index) {
302: Element mapping = findMapping(m.getDeclaringClass());
303:
304: if (mapping == null) {
305: return nextCreator.createClassInfo(m, index);
306: }
307:
308: // find the elements that apply to the specified method
309: TypeClassInfo info = new TypeClassInfo();
310: if (index >= 0) {
311: if (index >= m.getParameterTypes().length) {
312: throw new DatabindingException("Method " + m
313: + " does not have a parameter at index "
314: + index);
315: }
316: // we don't want nodes for which the specified index is not
317: // specified
318: List<Element> nodes = getMatches(mapping,
319: "./method[@name='" + m.getName()
320: + "']/parameter[@index='" + index
321: + "']/parent::*");
322: if (nodes.size() == 0) {
323: // no mapping for this method
324: return nextCreator.createClassInfo(m, index);
325: }
326: // pick the best matching node
327: Element bestMatch = getBestMatch(mapping, m, nodes);
328:
329: if (bestMatch == null) {
330: // no mapping for this method
331: return nextCreator.createClassInfo(m, index);
332: }
333: info.setTypeClass(m.getParameterTypes()[index]);
334: // info.setAnnotations(m.getParameterAnnotations()[index]);
335: Element parameter = getMatch(bestMatch,
336: "parameter[@index='" + index + "']");
337: readMetadata(info, mapping, parameter);
338: } else {
339: List<Element> nodes = getMatches(mapping,
340: "./method[@name='" + m.getName()
341: + "']/return-type/parent::*");
342: if (nodes.size() == 0) {
343: return nextCreator.createClassInfo(m, index);
344: }
345: Element bestMatch = getBestMatch(mapping, m, nodes);
346: if (bestMatch == null) {
347: // no mapping for this method
348: return nextCreator.createClassInfo(m, index);
349: }
350: info.setTypeClass(m.getReturnType());
351: // info.setAnnotations(m.getAnnotations());
352: Element rtElement = bestMatch.getChild("return-type");
353: readMetadata(info, mapping, rtElement);
354: }
355:
356: return info;
357: }
358:
359: protected void readMetadata(TypeClassInfo info, Element mapping,
360: Element parameter) {
361: info.setTypeName(createQName(parameter, parameter
362: .getAttributeValue("typeName")));
363: info.setMappedName(createQName(parameter, parameter
364: .getAttributeValue("mappedName")));
365: setComponentType(info, mapping, parameter);
366: setKeyType(info, mapping, parameter);
367: setType(info, parameter);
368:
369: String min = parameter.getAttributeValue("minOccurs");
370: if (min != null) {
371: info.setMinOccurs(Long.parseLong(min));
372: }
373:
374: String max = parameter.getAttributeValue("maxOccurs");
375: if (max != null) {
376: info.setMaxOccurs(Long.parseLong(max));
377: }
378:
379: String flat = parameter.getAttributeValue("flat");
380: if (flat != null) {
381: info.setFlat(Boolean.valueOf(flat.toLowerCase())
382: .booleanValue());
383: }
384: }
385:
386: @Override
387: protected Type getOrCreateGenericType(TypeClassInfo info) {
388: Type type = null;
389: if (info.getGenericType() != null) {
390: type = createTypeFromGeneric(info.getGenericType());
391: }
392:
393: if (type == null) {
394: type = super .getOrCreateGenericType(info);
395: }
396:
397: return type;
398: }
399:
400: private Type createTypeFromGeneric(Object cType) {
401: if (cType instanceof TypeClassInfo) {
402: return createTypeForClass((TypeClassInfo) cType);
403: } else if (cType instanceof Class) {
404: return createType((Class) cType);
405: } else {
406: return null;
407: }
408: }
409:
410: @Override
411: protected Type getOrCreateMapKeyType(TypeClassInfo info) {
412: Type type = null;
413: if (info.getKeyType() != null) {
414: type = createTypeFromGeneric(info.getKeyType());
415: }
416:
417: if (type == null) {
418: type = super .getOrCreateMapKeyType(info);
419: }
420:
421: return type;
422: }
423:
424: @Override
425: protected Type getOrCreateMapValueType(TypeClassInfo info) {
426: Type type = null;
427: if (info.getGenericType() != null) {
428: type = createTypeFromGeneric(info.getGenericType());
429: }
430:
431: if (type == null) {
432: type = super .getOrCreateMapValueType(info);
433: }
434:
435: return type;
436: }
437:
438: protected void setComponentType(TypeClassInfo info,
439: Element mapping, Element parameter) {
440: String componentType = parameter
441: .getAttributeValue("componentType");
442: if (componentType != null) {
443: info.setGenericType(loadGeneric(info, mapping,
444: componentType));
445: }
446: }
447:
448: private Object loadGeneric(TypeClassInfo info, Element mapping,
449: String componentType) {
450: if (componentType.startsWith("#")) {
451: String name = componentType.substring(1);
452: Element propertyEl = getMatch(mapping,
453: "./component[@name='" + name + "']");
454: if (propertyEl == null) {
455: throw new DatabindingException(
456: "Could not find <component> element in mapping named '"
457: + name + "'");
458: }
459:
460: TypeClassInfo componentInfo = new TypeClassInfo();
461: readMetadata(componentInfo, mapping, propertyEl);
462: String className = propertyEl.getAttributeValue("class");
463: if (className == null) {
464: throw new DatabindingException(
465: "A 'class' attribute must be specified for <component> "
466: + name);
467: }
468:
469: componentInfo.setTypeClass(loadComponentClass(className));
470:
471: return componentInfo;
472: } else {
473: return loadComponentClass(componentType);
474: }
475: }
476:
477: private Class loadComponentClass(String componentType) {
478: try {
479: return ClassLoaderUtils
480: .loadClass(componentType, getClass());
481: } catch (ClassNotFoundException e) {
482: throw new DatabindingException(
483: "Unable to load component type class "
484: + componentType, e);
485: }
486: }
487:
488: protected void setType(TypeClassInfo info, Element parameter) {
489: String type = parameter.getAttributeValue("type");
490: if (type != null) {
491: try {
492: info.setType(ClassLoaderUtils.loadClass(type,
493: getClass()));
494: } catch (ClassNotFoundException e) {
495: throw new DatabindingException(
496: "Unable to load type class " + type, e);
497: }
498: }
499: }
500:
501: protected void setKeyType(TypeClassInfo info, Element mapping,
502: Element parameter) {
503: String componentType = parameter.getAttributeValue("keyType");
504: if (componentType != null) {
505: info.setKeyType(loadGeneric(info, mapping, componentType));
506: }
507: }
508:
509: private Element getBestMatch(Element mapping, Method method,
510: List<Element> availableNodes) {
511: // first find all the matching method names
512: List<Element> nodes = getMatches(mapping, "./method[@name='"
513: + method.getName() + "']");
514: // remove the ones that aren't in our acceptable set, if one is
515: // specified
516: if (availableNodes != null) {
517: nodes.retainAll(availableNodes);
518: }
519: // no name found, so no matches
520: if (nodes.size() == 0) {
521: return null;
522: }
523: // if the method has no params, then more than one mapping is pointless
524: Class[] parameterTypes = method.getParameterTypes();
525: if (parameterTypes.length == 0) {
526: return nodes.get(0);
527: }
528: // here's the fun part.
529: // we go through the method parameters, ruling out matches
530: for (int i = 0; i < parameterTypes.length; i++) {
531: Class parameterType = parameterTypes[i];
532: for (Iterator iterator = nodes.iterator(); iterator
533: .hasNext();) {
534: Element element = (Element) iterator.next();
535: // first we check if the parameter index is specified
536: Element match = getMatch(element, "parameter[@index='"
537: + i + "']");
538: if (match != null
539: // we check if the type is specified and matches
540: && match.getAttributeValue("class") != null
541: // if it doesn't match, then we can definitely rule out
542: // this result
543: && !match.getAttributeValue("class").equals(
544: parameterType.getName())) {
545:
546: iterator.remove();
547: }
548: }
549: }
550: // if we have just one node left, then it has to be the best match
551: if (nodes.size() == 1) {
552: return nodes.get(0);
553: }
554: // all remaining definitions could apply, so we need to now pick the
555: // best one
556: // the best one is the one with the most parameters specified
557: Element bestCandidate = null;
558: int highestSpecified = 0;
559: for (Iterator iterator = nodes.iterator(); iterator.hasNext();) {
560: Element element = (Element) iterator.next();
561: int availableParameters = element.getChildren("parameter")
562: .size();
563: if (availableParameters > highestSpecified) {
564: bestCandidate = element;
565: highestSpecified = availableParameters;
566: }
567: }
568: return bestCandidate;
569: }
570:
571: private Element getMatch(Object doc, String xpath) {
572: try {
573: XPath path = XPath.newInstance(xpath);
574: return (Element) path.selectSingleNode(doc);
575: } catch (JDOMException e) {
576: throw new DatabindingException("Error evaluating xpath "
577: + xpath, e);
578: }
579: }
580:
581: @SuppressWarnings("unchecked")
582: private List<Element> getMatches(Object doc, String xpath) {
583: try {
584: XPath path = XPath.newInstance(xpath);
585: return path.selectNodes(doc);
586: } catch (JDOMException e) {
587: throw new DatabindingException("Error evaluating xpath "
588: + xpath, e);
589: }
590: }
591:
592: /**
593: * Creates a QName from a string, such as "ns:Element".
594: */
595: protected QName createQName(Element e, String value) {
596: if (value == null || value.length() == 0) {
597: return null;
598: }
599:
600: int index = value.indexOf(":");
601:
602: if (index == -1) {
603: return new QName(getTypeMapping().getEncodingStyleURI(),
604: value);
605: }
606:
607: String prefix = value.substring(0, index);
608: String localName = value.substring(index + 1);
609: Namespace ns = e.getNamespace(prefix);
610:
611: if (ns == null || localName == null) {
612: throw new DatabindingException("Invalid QName in mapping: "
613: + value);
614: }
615:
616: return new QName(ns.getURI(), localName, prefix);
617: }
618: }
|