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 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
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
176 List nodes = getMatches(mapping, "./method[@name='" + m.getName() + "']/parameter[@index='" + index + "']/parent::*");
177 if(nodes.size() == 0)
178 {
179
180 return nextCreator.createClassInfo(m, index);
181 }
182
183 Element bestMatch = getBestMatch(mapping, m, nodes);
184 if(bestMatch == null)
185 {
186
187 return nextCreator.createClassInfo(m, index);
188 }
189 info.setTypeClass(m.getParameterTypes()[index]);
190
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
206 return nextCreator.createClassInfo(m, index);
207 }
208 info.setTypeClass(m.getReturnType());
209
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
265 List nodes = getMatches(mapping, "./method[@name='" + method.getName() + "']");
266
267 if(availableNodes != null)
268 {
269 nodes.retainAll(availableNodes);
270 }
271
272 if(nodes.size() == 0) return null;
273
274 Class[] parameterTypes = method.getParameterTypes();
275 if(parameterTypes.length == 0) return (Element)nodes.get(0);
276
277
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
285 Element match = getMatch(element, "parameter[@index='" + i + "']");
286 if(match != null)
287 {
288
289 if(match.getAttributeValue("type") != null)
290 {
291
292 if(!match.getAttributeValue("type").equals(parameterType.getName()))
293 {
294 iterator.remove();
295 }
296 }
297 }
298 }
299 }
300
301 if(nodes.size() == 1) return (Element)nodes.get(0);
302
303
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 }