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 * <mappings>
40 * <mapping>
41 * <method name="getResultsForValues">
42 * <return-type componentType="com.acme.ResultBean" />
43 * <!-- no need to specify index 0, since it's a String -->
44 * <parameter index="1" componentType="java.lang.String" />
45 * </method>
46 * </mapping>
47 * </mappings>
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
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
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
172 List nodes = getMatches(mapping, "./method[@name='" + m.getName() + "']/parameter[@index='" + index + "']/parent::*");
173 if(nodes.size() == 0)
174 {
175
176 return nextCreator.createClassInfo(m, index);
177 }
178
179 Element bestMatch = getBestMatch(mapping, m, nodes);
180 if(bestMatch == null)
181 {
182
183 return nextCreator.createClassInfo(m, index);
184 }
185 info.setTypeClass(m.getParameterTypes()[index]);
186
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
198 return nextCreator.createClassInfo(m, index);
199 }
200 info.setTypeClass(m.getReturnType());
201
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
268 List nodes = getMatches(mapping, "./method[@name='" + method.getName() + "']");
269
270 if(availableNodes != null)
271 {
272 nodes.retainAll(availableNodes);
273 }
274
275 if(nodes.size() == 0) return null;
276
277 Class[] parameterTypes = method.getParameterTypes();
278 if(parameterTypes.length == 0) return (Element)nodes.get(0);
279
280
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
288 Element match = getMatch(element, "parameter[@index='" + i + "']");
289 if(match != null)
290 {
291
292 if(match.getAttributeValue("type") != null)
293 {
294
295 if(!match.getAttributeValue("type").equals(parameterType.getName()))
296 {
297 iterator.remove();
298 }
299 }
300 }
301 }
302 }
303
304 if(nodes.size() == 1) return (Element)nodes.get(0);
305
306
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 }