View Javadoc

1   /*
2    * Copyright 2005 by Mark Vollmann Hamburg, Germany. All rights reserved.
3    * 
4    * mail to mark.vollmann@web.de
5    * 
6    * Created on 05.07.2005
7    */
8   package de.keepondreaming.xml;
9   
10  import java.io.IOException;
11  import java.io.InputStream;
12  import java.lang.reflect.Method;
13  import java.util.ArrayList;
14  import java.util.Collection;
15  import java.util.Date;
16  import java.util.HashMap;
17  import java.util.List;
18  import java.util.Map;
19  import java.util.Stack;
20  
21  import javax.xml.parsers.ParserConfigurationException;
22  import javax.xml.parsers.SAXParser;
23  import javax.xml.parsers.SAXParserFactory;
24  
25  import org.xml.sax.Attributes;
26  import org.xml.sax.InputSource;
27  import org.xml.sax.SAXException;
28  import org.xml.sax.helpers.DefaultHandler;
29  
30  import de.keepondreaming.xml.util.Util;
31  
32  /***
33   * Reads an xml file and sets the data using the passed strategy patterns
34   * 
35   * $Author: wintermond $ $Date: 2005/07/10 18:37:00 $ $Log: XmlConverter.java,v $
36   * $Author: wintermond $ $Date: 2005/07/10 18:37:00 $ Revision 1.1  2005/07/10 18:37:00  wintermond
37   * $Author: wintermond $ $Date: 2005/07/10 18:37:00 $ Renamed Parser to XMLConverter
38   * $Author: wintermond $ $Date: 2005/07/10 18:37:00 $ Major performance improvements
39   * $Author: wintermond $ $Date: 2005/07/10 18:37:00 $ Support for java.util.Date, java.sql.Date and java.sql.TimeStamp as return types
40   * $Author: wintermond $ $Date: 2005/07/10 18:37:00 $ Support for attributes modelled as sub tags
41   * $Author: wintermond $ $Date: 2005/07/10 18:37:00 $ Support for method getters/setter with names different from the xml names
42   * $Author: wintermond $ $Date: 2005/07/10 18:37:00 $
43   * Revision 1.4 2005/07/09 14:11:54 wintermond Introduced ability to handle
44   * subtags that contain simple values
45   * 
46   * Revision 1.3 2005/07/09 09:57:58 wintermond Javadoc, moved some methods to
47   * Util class
48   * 
49   */
50  public class XmlConverter extends DefaultHandler
51  {
52  	private static final Object[] NO_PARAMETERS = new Object[0];
53  
54  	/***
55  	 * Default parser
56  	 */
57  	private SAXParser parserM = null;
58  
59  	/***
60  	 * Holds the object tree
61  	 */
62  	private Stack<Object> stackM = new Stack<Object>();
63  
64  	/***
65  	 * strategy used to obtain runtime information not resolvable via reflection
66  	 */
67  	private AnnotationStrategy annotationStrategyM = null;
68  
69  	/***
70  	 * Strategy used to create objects and set attributes
71  	 */
72  	private ObjectStrategy objectStrategyM = null;
73  
74  	/***
75  	 * Method cached to handle attributes that appear as subtag in xml
76  	 */
77  	private Method stackedMethodM;
78  
79  	/***
80  	 * Element name cached to handle attributes that appear as subtag in xml
81  	 */
82  	private String stackedElementM;
83  
84  	/***
85  	 * Converts read date strings into date objects
86  	 */
87  	private DateHandler dateHandlerM;
88  
89  	/***
90  	 * Caches the found getter methods
91  	 */
92  	private Map<String, Method> methodCacheM = new HashMap<String, Method>();
93  
94  	/***
95  	 * Caches for ever class the valid methods for which set operations apply
96  	 */
97  	private Map<Class, List<Method>> validMethodsCacheM = new HashMap<Class, List<Method>>(101);
98  
99  	/***
100 	 * Default constructor
101 	 * 
102 	 * @throws ParserConfigurationException
103 	 * @throws SAXException
104 	 * @throws NullPointerException If any parameter is null
105 	 */
106 	public XmlConverter(AnnotationStrategy parserStrategy, ObjectStrategy objectStrategy) throws ParserConfigurationException, SAXException
107 	{
108 		if (parserStrategy == null)
109 		{
110 			throw new IllegalArgumentException("parserStrategy is null");
111 		}
112 		if (objectStrategy == null)
113 		{
114 			throw new IllegalArgumentException("objectStrategy is null");
115 		}
116 
117 		SAXParserFactory factory = SAXParserFactory.newInstance();
118 		parserM = factory.newSAXParser();
119 		annotationStrategyM = parserStrategy;
120 		objectStrategyM = objectStrategy;
121 	}
122 
123 	/***
124 	 * Reads the xml from the stream and sets the data structures accordingly
125 	 * using reflection. The type of <rootElement> is expected to be the root
126 	 * node in the xml structure
127 	 * 
128 	 * @param in
129 	 * @param rootElement Must be an interface
130 	 * 
131 	 * @return An object containing the root data read from the 
132 	 * 		inpustream. This object is assignable to the interface 
133 	 * 		passed by <code>rootElement</code>
134 	 * 
135 	 * @throws IOException
136 	 *             Error while reading the data from the stream
137 	 * @throws SAXException
138 	 * @throws IllegalArgumentException <code>rootElement</code> is not an interface
139 	 */
140 	@SuppressWarnings( { "unchecked" })
141 	public Object convert(InputStream in, Class rootElement) throws IOException, SAXException
142 	{
143 		return parse(new InputSource(in), rootElement);
144 	}
145 
146 	/***
147 	 * Reads the xml from the stream and sets the data structures accordingly
148 	 * using reflection. The type of <rootElement> is expected to be the root
149 	 * node in the xml structure
150 	 * 
151 	 * @param in
152 	 * @param rootElement
153 	 * 
154 	 * @return An object containing the root data read from the 
155 	 * 		inpustream. This object is assignable to the interface 
156 	 * 		passed by <code>rootElement</code>
157 	 * 
158 	 * @throws IOException
159 	 *             Error while reading the data from the stream
160 	 * @throws SAXException
161 	 * @throws IllegalArgumentException <code>rootElement</code> is not an interface
162 	 */
163 	@SuppressWarnings( { "unchecked" })
164 	public Object parse(InputSource in, Class rootElement) throws IOException, SAXException
165 	{
166 		if(!rootElement.isInterface())
167 		{
168 			throw new IllegalArgumentException("rootElement is not an interface");
169 		}
170 		Object resultM = init(rootElement);
171 
172 		parserM.parse(in, this);
173 		return resultM;
174 	}
175 
176 	
177 	/***
178 	 * Initializes the parser
179 	 * 
180 	 * @param rootElement
181 	 * 
182 	 * @return The root element of the parsed data
183 	 */
184 	private Object init(Class rootElement)
185 	{
186 		objectStrategyM.init();
187 		annotationStrategyM.init();
188 		methodCacheM.clear();
189 		validMethodsCacheM.clear();
190 		if (dateHandlerM == null)
191 		{
192 			dateHandlerM = new DateHandler(annotationStrategyM);
193 		}
194 		stackedElementM = null;
195 		stackedMethodM = null;
196 
197 		Object result = objectStrategyM.createInstance(rootElement);
198 		stackM.push(result);
199 		return result;
200 	}
201 
202 	/*
203 	 * (non-Javadoc)
204 	 * 
205 	 * @see org.xml.sax.ContentHandler#characters(char[], int, int)
206 	 */
207 	@Override
208 	@SuppressWarnings( { "unchecked" })
209 	public void characters(char[] chars, int start, int len) throws SAXException
210 	{
211 		String content = new String(chars, start, len).trim();
212 
213 		if (content.length() > 0)
214 		{
215 			//
216 			// It is assumed that the object has a method with the
217 			// signature getContent() if no content attribute is supplied
218 			Object current = stackM.peek();
219 			if (stackedElementM != null)
220 			{
221 				Object value = content;
222 				Class returnType = stackedMethodM.getReturnType();
223 				if (returnType.isPrimitive())
224 				{
225 					value = Util.getPrimitiveObject(returnType, content);
226 				}
227 				else if (!String.class.isAssignableFrom(returnType))
228 				{
229 					value = Util.createObject(returnType, content);
230 				}
231 				objectStrategyM.setAttribute(current, stackedElementM, value);
232 
233 				// we cannot set stackedElement and stackedMethod to null, since
234 				// endElement will pop the stack. Since in this case no element
235 				// got
236 				// pushed, the wrong element will get popped
237 			}
238 			else
239 			{
240 				String contentAttribute = annotationStrategyM.getContentAttribute(objectStrategyM.resolveInterface(current), current);
241 
242 				String setterAttribute = "Content";
243 				if (contentAttribute != null)
244 				{
245 					setterAttribute = contentAttribute;
246 				}
247 				objectStrategyM.setAttribute(current, setterAttribute, content);
248 			}
249 		}
250 	}
251 
252 	/*
253 	 * (non-Javadoc)
254 	 * 
255 	 * @see org.xml.sax.ContentHandler#endElement(java.lang.String,
256 	 *      java.lang.String, java.lang.String)
257 	 */
258 	@Override
259 	public void endElement(String arg0, String arg1, String elementName) throws SAXException
260 	{
261 		elementName = Util.capitalize(elementName);
262 		if (elementName.equals(stackedElementM))
263 		{
264 			stackedElementM = null;
265 			stackedMethodM = null;
266 		}
267 		else
268 		{
269 			stackM.pop();
270 		}
271 	}
272 
273 	/*
274 	 * (non-Javadoc)
275 	 * 
276 	 * @see org.xml.sax.ContentHandler#startElement(java.lang.String,
277 	 *      java.lang.String, java.lang.String, org.xml.sax.Attributes)
278 	 */
279 	@Override
280 	@SuppressWarnings("unchecked")
281 	public void startElement(String arg0, String arg1, String elementName, Attributes attributes) throws SAXException
282 	{
283 		// get the current object
284 		Object current = stackM.peek();
285 		
286 		// ensure first letter is uppercase
287 		elementName = Util.capitalize(elementName);
288 //		elementName = objectStrategyM.resolveElement(current, elementName);
289 
290 
291 		Class currentType = objectStrategyM.resolveInterface(current);
292 
293 		//
294 		// If the names do not match this is a sub element of the current.
295 		// If the sub element is part of a Collection or Map it gets added
296 		//
297 		if (!currentType.getSimpleName().equals(elementName))
298 		{
299 			Method method = getGetterMethod(elementName, currentType);
300 			if (method != null)
301 			{
302 				if (Collection.class.isAssignableFrom(method.getReturnType()))
303 				{
304 					try
305 					{
306 						//
307 						// Obtain class type of elements within the collection
308 						//
309 						currentType = annotationStrategyM.getGenericReturnType(method);
310 						if (currentType == null)
311 						{
312 							throw new IllegalArgumentException(
313 									"Method "
314 											+ method.getName()
315 											+ " is not annotated and returns an object derived from java.util.Collection. Please annotate method with ReturnTypeAnnotation!");
316 						}
317 
318 						//
319 						// Create instance and store it in collection
320 						//
321 						Collection collection = (Collection) method.invoke(current, NO_PARAMETERS);
322 						current = objectStrategyM.createInstance(currentType);
323 
324 						collection.add(current);
325 						stackM.push(current);
326 					}
327 					catch (Throwable e)
328 					{
329 						throw new IllegalStateException("Error while adding to collection", e);
330 					}
331 
332 				}
333 				else if (Map.class.isAssignableFrom(method.getReturnType()))
334 				{
335 					try
336 					{
337 						//
338 						// Obtain class type of elements within the collection
339 						//                    	
340 						currentType = annotationStrategyM.getGenericReturnType(method);
341 						if (currentType == null)
342 						{
343 							throw new IllegalArgumentException(
344 									"Method "
345 											+ method.getName()
346 											+ " is not annotated and returns an object derived from java.util.Collection. Please annotate method with ReturnTypeAnnotation!");
347 						}
348 
349 						//
350 						// Create instance and store it in map
351 						// To store it, we need to obtain the mapping key
352 						//                        
353 						Map map = (Map) method.invoke(current, NO_PARAMETERS);
354 						current = objectStrategyM.createInstance(currentType);
355 						String attribute = annotationStrategyM.getKeyAttribute(method);
356 
357 						// TODO: enable key types other than String
358 						map.put(attributes.getValue(attribute), current);
359 						stackM.push(current);
360 					}
361 					catch (Throwable e)
362 					{
363 						throw new IllegalStateException("Error while adding to map", e);
364 					}
365 				}
366 				else if (method.getReturnType().isInterface())
367 				{
368 					Object target = current;
369 					currentType = method.getReturnType();
370 					current = objectStrategyM.createInstance(currentType);
371 					objectStrategyM.setAttribute(target, elementName, current);
372 					stackM.push(current);
373 				}
374 				else
375 				{
376 					//
377 					// seems that a non tagged value is not represented as
378 					// attribute but
379 					// as a subtag. i.e.
380 					// instead of <currentObject myInt="42">...
381 					// the stream reads
382 					// <currentObject ....>
383 					// <myInt>42</myInt>
384 					//
385 					// The interface seems to have the method int
386 					// <currentObject>.getMyInt()
387 					// So now we stack method and element for the
388 					// characters.method
389 					//
390 					stackedMethodM = method;
391 					stackedElementM = elementName;
392 				}
393 			}
394 		}
395 		setAttributesOfCurrentObject(attributes, current, currentType);
396 	}
397 
398 	/***
399 	 * Looks up the getter method for the current paramter type
400 	 * 
401 	 * @param elementName
402 	 * @param currentType
403 	 * 
404 	 * @return The getter method for the current paramter type
405 	 */
406 	private Method getGetterMethod(String elementName, Class currentType)
407 	{
408 		String key = currentType.getName() + "." + elementName;
409 		// System.out.println(key);
410 		Method method = methodCacheM.get(key);
411 		if (method == null)
412 		{
413 			String methodName = objectStrategyM.getMethodName(currentType, elementName, false);
414 			if (methodName == null)
415 			{
416 				methodName = "get" + elementName;
417 			}
418 			try
419 			{
420 				method = currentType.getMethod(methodName, new Class[0]);
421 			}
422 			catch (Throwable e)
423 			{
424 				try
425 				{
426 					method = currentType.getMethod("get" + elementName + "s", new Class[0]);
427 				}
428 				catch (Throwable e1)
429 				{
430 					System.err.println("Could not find getter for attribute <" + elementName + ">");
431 				}
432 			}
433 
434 			if (method != null)
435 			{
436 				methodCacheM.put(key, method);
437 			}
438 		}
439 		return method;
440 	}
441 
442 	/***
443 	 * Sets the attributes of the current object
444 	 * 
445 	 * @param attributes
446 	 * @param current
447 	 * @param currentType
448 	 */
449 	private void setAttributesOfCurrentObject(Attributes attributes, Object current, Class currentType)
450 	{
451 		try
452 		{
453 			List<Method> validMethods = validMethodsCacheM.get(currentType);
454 			if (validMethods == null)
455 			{
456 				//
457 				//	This caching halves the used time!
458 				//
459 				String methodName = null;
460 				validMethods = new ArrayList<Method>();
461 				validMethodsCacheM.put(currentType, validMethods);
462 				
463 				for (Method method : currentType.getMethods())
464 				{
465 					methodName = method.getName();
466 					Class<?> returnType = method.getReturnType();
467 					if (method.getParameterTypes().length == 0 && methodName.startsWith("get")
468 							&& (returnType.isPrimitive() || returnType.getPackage().getName().equals("java.lang") // String, Integer					
469 							|| Date.class.isAssignableFrom(returnType)))
470 					{
471 						validMethods.add(method);
472 					}
473 				}
474 				
475 			}
476 			for (Method method : validMethods)
477 			{
478 				String methodName = method.getName();
479 				Class<?> returnType = method.getReturnType();
480 				String attributeName = methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
481 				String value = attributes.getValue(attributeName);
482 				if (value != null)
483 				{
484 					Object typedValue = null;
485 					if (returnType.isPrimitive())
486 					{
487 						typedValue = Util.getPrimitiveObject(returnType, value);
488 					}
489 					else if (Date.class.isAssignableFrom(returnType))
490 					{
491 						typedValue = dateHandlerM.getDateString(method, value);
492 					}
493 					else
494 					{
495 						typedValue = Util.createObject(returnType, value);
496 					}
497 
498 					objectStrategyM.setAttribute(current, methodName.substring(3), typedValue);
499 				}
500 			}
501 		}
502 		catch (Throwable e)
503 		{
504 			e.printStackTrace();
505 			throw new IllegalStateException("Error while applying attributes", e);
506 		}
507 
508 	}
509 
510 }