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         setComponentType(info, propertyEl);
112         setKeyType(info, propertyEl);
113         info.setName(createQName(propertyEl, propertyEl.getAttributeValue("mappedName")));
114 
115         return info;
116     }
117     
118     protected Element findMapping(Class clazz)
119     {
120         Document doc = getDocument(clazz);
121         if(doc == null) return null;
122         
123         Element mapping = getMatch(doc, "/mappings/mapping[@uri='" + getTypeMapping().getEncodingStyleURI() + "']");
124         if (mapping == null)
125         {
126             mapping = getMatch(doc, "/mappings/mapping");
127         }
128         
129         return mapping;
130     }
131 
132     public Type createDefaultType(TypeClassInfo info)
133     {
134         Element mapping = findMapping(info.getTypeClass());
135         
136         if (mapping != null)
137         {
138             XMLBeanTypeInfo btinfo = new XMLBeanTypeInfo(getTypeMapping(), 
139                                                  info.getTypeClass(),
140                                                  mapping);
141             btinfo.setTypeMapping(getTypeMapping());
142             
143             BeanType type = new BeanType(btinfo);
144             
145             QName name = btinfo.getSchemaType();
146             if (name == null) name = createQName(info.getTypeClass());
147             
148             type.setSchemaType(name);
149             
150             type.setTypeClass(info.getTypeClass());
151             type.setTypeMapping(getTypeMapping());
152 
153             return type;
154         }
155         else
156         {
157             return nextCreator.createDefaultType(info);
158         }
159     }
160 
161     public TypeClassInfo createClassInfo(Method m, int index)
162     {
163         Element mapping = findMapping(m.getDeclaringClass());
164         
165         if(mapping == null) return nextCreator.createClassInfo(m, index);
166         
167         //find the elements that apply to the specified method
168         TypeClassInfo info = new TypeClassInfo();
169         if(index >= 0)
170         {
171             if(index >= m.getParameterTypes().length)
172             {
173                 throw new XFireRuntimeException("Method " + m + " does not have a parameter at index " + index);
174             }
175             //we don't want nodes for which the specified index is not specified
176             List nodes = getMatches(mapping, "./method[@name='" + m.getName() + "']/parameter[@index='" + index + "']/parent::*");
177             if(nodes.size() == 0)
178             {
179                 //no mapping for this method
180                 return nextCreator.createClassInfo(m, index);
181             }
182             //pick the best matching node
183             Element bestMatch = getBestMatch(mapping, m, nodes);
184             if(bestMatch == null)
185             {
186                 //no mapping for this method
187                 return nextCreator.createClassInfo(m, index);
188             }
189             info.setTypeClass(m.getParameterTypes()[index]);
190             //info.setAnnotations(m.getParameterAnnotations()[index]);
191             Element parameter = getMatch(bestMatch, "parameter[@index='" + index + "']");
192             
193             setComponentType(info, parameter);
194             setKeyType(info, parameter);
195             
196             info.setName(createQName(parameter, parameter.getAttributeValue("mappedName")));
197         }
198         else
199         {
200             List nodes = getMatches(mapping, "./method[@name='" + m.getName() + "']/return-type/parent::*");
201             if(nodes.size() == 0) return nextCreator.createClassInfo(m, index);
202             Element bestMatch = getBestMatch(mapping, m, nodes);
203             if(bestMatch == null)
204             {
205                 //no mapping for this method
206                 return nextCreator.createClassInfo(m, index);
207             }
208             info.setTypeClass(m.getReturnType());
209             //info.setAnnotations(m.getAnnotations());
210             Element rtElement = bestMatch.getFirstChildElement("return-type");
211             String componentType = rtElement.getAttributeValue("componentType");
212             if(componentType != null)
213             {
214                 try
215                 {
216                     info.setGenericType(ClassLoaderUtils.loadClass(componentType, getClass()));
217                 }
218                 catch(ClassNotFoundException e)
219                 {
220                     log.error("Unable to load mapping class " + componentType);
221                 }
222             }
223             
224             info.setName(createQName(rtElement, rtElement.getAttributeValue("mappedName")));
225         }
226 
227         return info;
228     }
229 
230     protected void setComponentType(TypeClassInfo info, Element parameter)
231     {
232         String componentType = parameter.getAttributeValue("componentType");
233         if(componentType != null)
234         {
235             try
236             {
237                 info.setGenericType(ClassLoaderUtils.loadClass(componentType, getClass()));
238             }
239             catch(ClassNotFoundException e)
240             {
241                 log.error("Unable to load mapping class " + componentType);
242             }
243         }
244     }
245 
246     protected void setKeyType(TypeClassInfo info, Element parameter)
247     {
248         String componentType = parameter.getAttributeValue("keyType");
249         if(componentType != null)
250         {
251             try
252             {
253                 info.setKeyType(ClassLoaderUtils.loadClass(componentType, getClass()));
254             }
255             catch(ClassNotFoundException e)
256             {
257                 log.error("Unable to load mapping class " + componentType);
258             }
259         }
260     }
261     
262     private Element getBestMatch(Element mapping, Method method, List availableNodes)
263     {
264         //first find all the matching method names
265         List nodes = getMatches(mapping, "./method[@name='" + method.getName() + "']");
266         //remove the ones that aren't in our acceptable set, if one is specified
267         if(availableNodes != null)
268         {
269             nodes.retainAll(availableNodes);
270         }
271         //no name found, so no matches
272         if(nodes.size() == 0) return null;
273         //if the method has no params, then more than one mapping is pointless
274         Class[] parameterTypes = method.getParameterTypes();
275         if(parameterTypes.length == 0) return (Element)nodes.get(0);
276         //here's the fun part.
277         //we go through the method parameters, ruling out matches
278         for(int i = 0; i < parameterTypes.length; i++)
279         {
280             Class parameterType = parameterTypes[i];
281             for(Iterator iterator = nodes.iterator(); iterator.hasNext();)
282             {
283                 Element element = (Element)iterator.next();
284                 //first we check if the parameter index is specified
285                 Element match = getMatch(element, "parameter[@index='" + i + "']");
286                 if(match != null)
287                 {
288                     //we check if the type is specified and matches
289                     if(match.getAttributeValue("type") != null)
290                     {
291                         //if it doesn't match, then we can definitely rule out this result
292                         if(!match.getAttributeValue("type").equals(parameterType.getName()))
293                         {
294                             iterator.remove();
295                         }
296                     }
297                 }
298             }
299         }
300         //if we have just one node left, then it has to be the best match
301         if(nodes.size() == 1) return (Element)nodes.get(0);
302         //all remaining definitions could apply, so we need to now pick the best one
303         //the best one is the one with the most parameters specified
304         Element bestCandidate = null;
305         int highestSpecified = 0;
306         for(Iterator iterator = nodes.iterator(); iterator.hasNext();)
307         {
308             Element element = (Element)iterator.next();
309             int availableParameters = element.getChildElements("parameter").size();
310             if(availableParameters > highestSpecified)
311             {
312                 bestCandidate = element;
313                 highestSpecified = availableParameters;
314             }
315         }
316         return bestCandidate;
317     }
318 
319     private Element getMatch(Object doc, String xpath)
320     {
321         try
322         {
323             XPath path = new YOMXPath(xpath);
324             return (Element)path.selectSingleNode(doc);
325         }
326         catch(JaxenException e)
327         {
328             throw new XFireRuntimeException("Error evaluating xpath " + xpath, e);
329         }
330     }
331 
332     private List getMatches(Object doc, String xpath)
333     {
334         try
335         {
336             XPath path = new YOMXPath(xpath);
337             List result = path.selectNodes(doc);
338             return result;
339         }
340         catch(JaxenException e)
341         {
342             throw new XFireRuntimeException("Error evaluating xpath " + xpath, e);
343         }
344     }
345 
346     /***
347      * Creates a QName from a string, such as "ns:Element".
348      */
349     protected QName createQName(Element e, String value)
350     {
351         if (value == null || value.length() == 0) return null;
352         
353         int index = value.indexOf(":");
354         
355         if (index == -1)
356         {
357             return new QName(getTypeMapping().getEncodingStyleURI(), value);
358         }
359         
360         String prefix = value.substring(0, index);
361         String localName = value.substring(index+1);
362         String ns = e.getNamespaceURI(prefix);
363         
364         if (ns == null || localName == null)
365             throw new XFireRuntimeException("Invalid QName in mapping: " + value);
366         
367         return new QName(ns, localName, prefix);
368     }
369 }