View Javadoc

1   package org.codehaus.xfire.aegis.type;
2   
3   import java.beans.PropertyDescriptor;
4   import java.io.InputStream;
5   import java.lang.reflect.Method;
6   import java.util.HashMap;
7   import java.util.Iterator;
8   import java.util.List;
9   import java.util.Map;
10  
11  import javax.xml.namespace.QName;
12  import javax.xml.stream.XMLStreamException;
13  
14  import org.apache.commons.logging.Log;
15  import org.apache.commons.logging.LogFactory;
16  import org.codehaus.xfire.XFireRuntimeException;
17  import org.codehaus.xfire.aegis.type.basic.BeanType;
18  import org.codehaus.xfire.aegis.type.basic.XMLBeanTypeInfo;
19  import org.codehaus.xfire.util.ClassLoaderUtils;
20  import org.codehaus.yom.Document;
21  import org.codehaus.yom.Element;
22  import org.codehaus.yom.stax.StaxBuilder;
23  import org.codehaus.yom.xpath.YOMXPath;
24  import org.jaxen.JaxenException;
25  import org.jaxen.XPath;
26  
27  /***
28   * Deduce mapping information from an xml file.
29   * The xml file should be in the same packages as the class, with the name <code>className.aegis.xml</code>.
30   * For example, given the following service interface:
31   * <p/>
32   * <pre>
33   * public Collection getResultsForValues(String id, Collection values); //method 1
34   * public Collection getResultsForValues(int id, Collection values); //method 2
35   * public String getResultForValue(String value); //method 3
36   * </pre>
37   * An example of the type xml is:
38   * <pre>
39   * &lt;mappings&gt;
40   *  &lt;mapping&gt;
41   *    &lt;method name="getResultsForValues"&gt;
42   *      &lt;return-type componentType="com.acme.ResultBean" /&gt;
43   *      &lt;!-- no need to specify index 0, since it's a String --&gt;
44   *      &lt;parameter index="1" componentType="java.lang.String" /&gt;
45   *    &lt;/method&gt;
46   *  &lt;/mapping&gt;
47   * &lt;/mappings&gt;
48   * </pre>
49   * <p/>
50   * Note that for values which can be easily deduced (such as the String parameter, or the second service method)
51   * no mapping need be specified in the xml descriptor, which is why no mapping is specified for method 3.
52   * <p/>
53   * However, if you have overloaded methods with different semantics, then you will need to specify enough
54   * parameters to disambiguate the method and uniquely identify it. So in the example above, the mapping
55   * specifies will apply to both method 1 and method 2, since the parameter at index 0 is not specified.
56   *
57   * @author Hani Suleiman
58   *         Date: Jun 14, 2005
59   *         Time: 7:47:56 PM
60   */
61  public class XMLTypeCreator extends AbstractTypeCreator
62  {
63      private static final Log log = LogFactory.getLog(XMLTypeCreator.class);
64      //cache of classes to documents
65      private Map documents = new HashMap();
66  
67      protected Document getDocument(Class clazz)
68      {
69          Document doc = (Document)documents.get(clazz.getName());
70          if(doc != null)
71          {
72              return doc;
73          }
74          String path = '/' + clazz.getName().replace('.', '/') + ".aegis.xml";
75          InputStream is = clazz.getResourceAsStream(path);
76          if(is == null) return null;
77          try
78          {
79              doc = new StaxBuilder().build(is);
80              documents.put(clazz.getName(), doc);
81              return doc;
82          }
83          catch(XMLStreamException e)
84          {
85              log.error("Error loading file " + path, e);
86          }
87          return null;
88      }
89  
90      public Type createCollectionType(TypeClassInfo info)
91      {
92          return super.createCollectionType(info, (Class)info.getGenericType());
93      }
94  
95      public TypeClassInfo createClassInfo(PropertyDescriptor pd)
96      {
97          Element mapping = findMapping(pd.getReadMethod().getDeclaringClass());
98          if(mapping == null)
99          {
100             return nextCreator.createClassInfo(pd);
101         }
102         
103         Element propertyEl = getMatch(mapping, "./property[@name='" + pd.getName() + "']");
104         if(propertyEl == null) 
105         {
106             return nextCreator.createClassInfo(pd);
107         }
108 
109         TypeClassInfo info = new TypeClassInfo();
110         info.setTypeClass(pd.getReadMethod().getReturnType());
111         readMetadata(info, propertyEl);
112         
113         return info;
114     }
115     
116     protected Element findMapping(Class clazz)
117     {
118         Document doc = getDocument(clazz);
119         if(doc == null) return null;
120         
121         Element mapping = getMatch(doc, "/mappings/mapping[@uri='" + getTypeMapping().getEncodingStyleURI() + "']");
122         if (mapping == null)
123         {
124             mapping = getMatch(doc, "/mappings/mapping");
125         }
126         
127         return mapping;
128     }
129 
130     public Type createDefaultType(TypeClassInfo info)
131     {
132         Element mapping = findMapping(info.getTypeClass());
133         
134         if (mapping != null)
135         {
136             XMLBeanTypeInfo btinfo = new XMLBeanTypeInfo(info.getTypeClass(), mapping);
137             btinfo.setTypeMapping(getTypeMapping());
138             
139             BeanType type = new BeanType(btinfo);
140             
141             QName name = btinfo.getSchemaType();
142             if (name == null) name = createQName(info.getTypeClass());
143             
144             type.setSchemaType(name);
145             
146             type.setTypeClass(info.getTypeClass());
147             type.setTypeMapping(getTypeMapping());
148 
149             return type;
150         }
151         else
152         {
153             return nextCreator.createDefaultType(info);
154         }
155     }
156 
157     public TypeClassInfo createClassInfo(Method m, int index)
158     {
159         Element mapping = findMapping(m.getDeclaringClass());
160         
161         if(mapping == null) return nextCreator.createClassInfo(m, index);
162         
163         //find the elements that apply to the specified method
164         TypeClassInfo info = new TypeClassInfo();
165         if(index >= 0)
166         {
167             if(index >= m.getParameterTypes().length)
168             {
169                 throw new XFireRuntimeException("Method " + m + " does not have a parameter at index " + index);
170             }
171             //we don't want nodes for which the specified index is not specified
172             List nodes = getMatches(mapping, "./method[@name='" + m.getName() + "']/parameter[@index='" + index + "']/parent::*");
173             if(nodes.size() == 0)
174             {
175                 //no mapping for this method
176                 return nextCreator.createClassInfo(m, index);
177             }
178             //pick the best matching node
179             Element bestMatch = getBestMatch(mapping, m, nodes);
180             if(bestMatch == null)
181             {
182                 //no mapping for this method
183                 return nextCreator.createClassInfo(m, index);
184             }
185             info.setTypeClass(m.getParameterTypes()[index]);
186             //info.setAnnotations(m.getParameterAnnotations()[index]);
187             Element parameter = getMatch(bestMatch, "parameter[@index='" + index + "']");
188             readMetadata(info, parameter);
189         }
190         else
191         {
192             List nodes = getMatches(mapping, "./method[@name='" + m.getName() + "']/return-type/parent::*");
193             if(nodes.size() == 0) return nextCreator.createClassInfo(m, index);
194             Element bestMatch = getBestMatch(mapping, m, nodes);
195             if(bestMatch == null)
196             {
197                 //no mapping for this method
198                 return nextCreator.createClassInfo(m, index);
199             }
200             info.setTypeClass(m.getReturnType());
201             //info.setAnnotations(m.getAnnotations());
202             Element rtElement = bestMatch.getFirstChildElement("return-type");
203             readMetadata(info, rtElement);
204         }
205 
206         return info;
207     }
208 
209     protected void readMetadata(TypeClassInfo info, Element parameter)
210     {        
211         info.setTypeName(createQName(parameter, parameter.getAttributeValue("typeName")));
212         setComponentType(info, parameter);
213         setKeyType(info, parameter);
214         setType(info, parameter);
215     }
216     
217     protected void setComponentType(TypeClassInfo info, Element parameter)
218     {
219         String componentType = parameter.getAttributeValue("componentType");
220         if(componentType != null)
221         {
222             try
223             {
224                 info.setGenericType(ClassLoaderUtils.loadClass(componentType, getClass()));
225             }
226             catch(ClassNotFoundException e)
227             {
228                 throw new XFireRuntimeException("Unable to load component type class " + componentType, e);
229             }
230         }
231     }
232 
233     protected void setType(TypeClassInfo info, Element parameter)
234     {
235         String type = parameter.getAttributeValue("type");
236         if(type != null)
237         {
238             try
239             {
240                 info.setType(ClassLoaderUtils.loadClass(type, getClass()));
241             }
242             catch(ClassNotFoundException e)
243             {
244                 throw new XFireRuntimeException("Unable to load type class " + type, e);
245             }
246         }
247     }
248 
249     protected void setKeyType(TypeClassInfo info, Element parameter)
250     {
251         String componentType = parameter.getAttributeValue("keyType");
252         if(componentType != null)
253         {
254             try
255             {
256                 info.setKeyType(ClassLoaderUtils.loadClass(componentType, getClass()));
257             }
258             catch(ClassNotFoundException e)
259             {
260                 log.error("Unable to load mapping class " + componentType);
261             }
262         }
263     }
264     
265     private Element getBestMatch(Element mapping, Method method, List availableNodes)
266     {
267         //first find all the matching method names
268         List nodes = getMatches(mapping, "./method[@name='" + method.getName() + "']");
269         //remove the ones that aren't in our acceptable set, if one is specified
270         if(availableNodes != null)
271         {
272             nodes.retainAll(availableNodes);
273         }
274         //no name found, so no matches
275         if(nodes.size() == 0) return null;
276         //if the method has no params, then more than one mapping is pointless
277         Class[] parameterTypes = method.getParameterTypes();
278         if(parameterTypes.length == 0) return (Element)nodes.get(0);
279         //here's the fun part.
280         //we go through the method parameters, ruling out matches
281         for(int i = 0; i < parameterTypes.length; i++)
282         {
283             Class parameterType = parameterTypes[i];
284             for(Iterator iterator = nodes.iterator(); iterator.hasNext();)
285             {
286                 Element element = (Element)iterator.next();
287                 //first we check if the parameter index is specified
288                 Element match = getMatch(element, "parameter[@index='" + i + "']");
289                 if(match != null)
290                 {
291                     //we check if the type is specified and matches
292                     if(match.getAttributeValue("type") != null)
293                     {
294                         //if it doesn't match, then we can definitely rule out this result
295                         if(!match.getAttributeValue("type").equals(parameterType.getName()))
296                         {
297                             iterator.remove();
298                         }
299                     }
300                 }
301             }
302         }
303         //if we have just one node left, then it has to be the best match
304         if(nodes.size() == 1) return (Element)nodes.get(0);
305         //all remaining definitions could apply, so we need to now pick the best one
306         //the best one is the one with the most parameters specified
307         Element bestCandidate = null;
308         int highestSpecified = 0;
309         for(Iterator iterator = nodes.iterator(); iterator.hasNext();)
310         {
311             Element element = (Element)iterator.next();
312             int availableParameters = element.getChildElements("parameter").size();
313             if(availableParameters > highestSpecified)
314             {
315                 bestCandidate = element;
316                 highestSpecified = availableParameters;
317             }
318         }
319         return bestCandidate;
320     }
321 
322     private Element getMatch(Object doc, String xpath)
323     {
324         try
325         {
326             XPath path = new YOMXPath(xpath);
327             return (Element)path.selectSingleNode(doc);
328         }
329         catch(JaxenException e)
330         {
331             throw new XFireRuntimeException("Error evaluating xpath " + xpath, e);
332         }
333     }
334 
335     private List getMatches(Object doc, String xpath)
336     {
337         try
338         {
339             XPath path = new YOMXPath(xpath);
340             List result = path.selectNodes(doc);
341             return result;
342         }
343         catch(JaxenException e)
344         {
345             throw new XFireRuntimeException("Error evaluating xpath " + xpath, e);
346         }
347     }
348 
349     /***
350      * Creates a QName from a string, such as "ns:Element".
351      */
352     protected QName createQName(Element e, String value)
353     {
354         if (value == null || value.length() == 0) return null;
355         
356         int index = value.indexOf(":");
357         
358         if (index == -1)
359         {
360             return new QName(getTypeMapping().getEncodingStyleURI(), value);
361         }
362         
363         String prefix = value.substring(0, index);
364         String localName = value.substring(index+1);
365         String ns = e.getNamespaceURI(prefix);
366         
367         if (ns == null || localName == null)
368             throw new XFireRuntimeException("Invalid QName in mapping: " + value);
369         
370         return new QName(ns, localName, prefix);
371     }
372 }