001: package com.wutka.jox;
002:
003: import java.beans.*;
004: import java.io.*;
005: import java.lang.reflect.*;
006: import java.text.*;
007: import java.util.*;
008:
009: import javax.xml.parsers.*;
010: import org.w3c.dom.*;
011: import org.xml.sax.*;
012:
013: /** Performs the actual reading of an XML document and copies the
014: * values into a bean. This class uses the DocumentBuilder portion
015: * of the Java XML API. It is not as efficient as SAX, but much easier
016: * to deal with when copying values into beans.
017: *
018: * @author Mark Wutka
019: * @version 1.0 05/08/2000
020: * @version 1.1 05/09/2000
021: * @version 1.6 07/30/2000
022: */
023:
024: class JOXBeanInput {
025: /** The document builder factory used to instantiate new document
026: * builders.
027: */
028: protected static DocumentBuilderFactory factory = DocumentBuilderFactory
029: .newInstance();
030:
031: protected static Hashtable beanCache = new Hashtable();
032:
033: /** Reads an XML document from an input source and copies its
034: * values into the specified object
035: * @param ob The object to receive the values
036: * @param source The location of the XML document
037: * @throws IOException If there is an error reading the document
038: */
039: public void readObject(Object ob, InputSource source)
040: throws IOException {
041: try {
042: // Create a document builder to read the document
043: DocumentBuilder builder = factory.newDocumentBuilder();
044:
045: // Read the document
046: Document doc = builder.parse(source);
047:
048: // Get the root element
049: Element element = doc.getDocumentElement();
050:
051: // Copy the root element into the bean
052: readObject(ob, element);
053: } catch (SAXException exc) {
054: throw new IOException("Error parsing XML document: "
055: + exc.toString());
056: } catch (ParserConfigurationException exc) {
057: throw new IOException("Error parsing XML document: "
058: + exc.toString());
059: }
060: }
061:
062: /** Reads the children of an XML element and matches them to properties
063: * of a bean.
064: * @param ob The bean to receive the values
065: * @param element The element the corresponds to the bean
066: * @throws IOException If there is an error reading the document
067: */
068: public void readObject(Object ob, Element element)
069: throws IOException {
070: // If the object is null, skip the element
071: if (ob == null) {
072: return;
073: }
074:
075: try {
076: BeanInfo info = (BeanInfo) beanCache.get(ob.getClass());
077:
078: if (info == null) {
079: // Get the bean info for the object
080: info = Introspector.getBeanInfo(ob.getClass(),
081: Object.class);
082:
083: beanCache.put(ob.getClass(), info);
084: }
085:
086: // Get the object's properties
087: PropertyDescriptor[] props = info.getPropertyDescriptors();
088:
089: // Get the attributes of the node
090: NamedNodeMap attrs = element.getAttributes();
091:
092: // Get the children of the XML element
093: NodeList nodes = element.getChildNodes();
094:
095: int numNodes = nodes.getLength();
096:
097: for (int i = 0; i < props.length; i++) {
098: // Treat indexed properties a little differently
099: if (props[i] instanceof IndexedPropertyDescriptor) {
100: readIndexedProperty(ob,
101: (IndexedPropertyDescriptor) props[i],
102: nodes, attrs);
103: } else {
104: readProperty(ob, props[i], nodes, attrs);
105: }
106: }
107: } catch (IntrospectionException exc) {
108: throw new IOException("Error getting bean info for "
109: + ob.getClass().getName() + ": " + exc.toString());
110: }
111: }
112:
113: /** Reads an XML element into a bean property by first locating the
114: * XML element corresponding to this property.
115: * @param ob The bean whose property is being set
116: * @param desc The property that will be set
117: * @param nodes The list of XML items that may contain the property
118: * @throws IOException If there is an error reading the document
119: */
120: public void readProperty(Object ob, PropertyDescriptor desc,
121: NodeList nodes, NamedNodeMap attrs) throws IOException {
122: int numAttrs = attrs.getLength();
123:
124: for (int i = 0; i < numAttrs; i++) {
125: // See if the attribute name matches the property name
126: if (namesMatch(desc.getName(), attrs.item(i).getNodeName())) {
127: // Get the method used to set this property
128: Method setter = desc.getWriteMethod();
129:
130: // If this object has no setter, don't bother writing it
131: if (setter == null)
132: continue;
133:
134: // Get the value of the property
135: Object obValue = getObjectValue(desc, attrs.item(i)
136: .getNodeValue());
137: if (obValue != null) {
138: try {
139: // Set the property value
140: setter.invoke(ob, new Object[] { obValue });
141: } catch (InvocationTargetException exc) {
142: throw new IOException("Error setting property "
143: + desc.getName() + ": "
144: + exc.toString());
145: } catch (IllegalAccessException exc) {
146: throw new IOException("Error setting property "
147: + desc.getName() + ": "
148: + exc.toString());
149: }
150: }
151:
152: return;
153: }
154: }
155:
156: int numNodes = nodes.getLength();
157:
158: Vector arrayBuild = null;
159:
160: for (int i = 0; i < numNodes; i++) {
161: Node node = nodes.item(i);
162:
163: // If this node isn't an element, skip it
164: if (!(node instanceof Element))
165: continue;
166:
167: Element element = (Element) node;
168:
169: // See if the tag name matches the property name
170: if (namesMatch(desc.getName(), element.getTagName())) {
171: // Get the method used to set this property
172: Method setter = desc.getWriteMethod();
173:
174: // If this object has no setter, don't bother writing it
175: if (setter == null)
176: continue;
177:
178: // Get the value of the property
179: Object obValue = getObjectValue(desc, element);
180:
181: // 070201 MAW: Modified from change submitted by Steve Poulson
182: if (setter.getParameterTypes()[0].isArray()) {
183: if (arrayBuild == null) {
184: arrayBuild = new Vector();
185: }
186: arrayBuild.addElement(obValue);
187:
188: // 070201 MAW: Go ahead and read through the rest of the nodes in case
189: // another one matches the array. This has the effect of skipping
190: // over the "return" statement down below
191: continue;
192: }
193:
194: if (obValue != null) {
195: try {
196: // Set the property value
197: setter.invoke(ob, new Object[] { obValue });
198: } catch (InvocationTargetException exc) {
199: throw new IOException("Error setting property "
200: + desc.getName() + ": "
201: + exc.toString());
202: } catch (IllegalAccessException exc) {
203: throw new IOException("Error setting property "
204: + desc.getName() + ": "
205: + exc.toString());
206: }
207: }
208: return;
209: }
210: }
211:
212: // If we build a vector of array members, convert the vector into
213: // an array and save it in the property
214: if (arrayBuild != null) {
215: // Get the method used to set this property
216: Method setter = desc.getWriteMethod();
217:
218: if (setter == null)
219: return;
220:
221: Object[] obValues = (Object[]) Array.newInstance(desc
222: .getPropertyType(), arrayBuild.size());
223:
224: arrayBuild.copyInto(obValues);
225:
226: try {
227: setter.invoke(ob, new Object[] { obValues });
228: } catch (InvocationTargetException exc) {
229: throw new IOException("Error setting property "
230: + desc.getName() + ": " + exc.toString());
231: } catch (IllegalAccessException exc) {
232: throw new IOException("Error setting property "
233: + desc.getName() + ": " + exc.toString());
234: }
235:
236: return;
237: }
238: }
239:
240: /** Reads XML element(s) into an indexed bean property by first locating
241: * the XML element(s) corresponding to this property.
242: * @param ob The bean whose property is being set
243: * @param desc The property that will be set
244: * @param nodes The list of XML items that may contain the property
245: * @throws IOException If there is an error reading the document
246: */
247: public void readIndexedProperty(Object ob,
248: IndexedPropertyDescriptor desc, NodeList nodes,
249: NamedNodeMap attrs) throws IOException {
250: // Create a vector to hold the property values
251: Vector v = new Vector();
252:
253: int numAttrs = attrs.getLength();
254:
255: for (int i = 0; i < numAttrs; i++) {
256: // See if this attribute matches the property name
257: if (namesMatch(desc.getName(), attrs.item(i).getNodeName())) {
258: // Get the property value
259: Object obValue = getObjectValue(desc, attrs.item(i)
260: .getNodeValue());
261:
262: if (obValue != null) {
263: // Add the value to the list of values to be set
264: v.addElement(obValue);
265: }
266: }
267: }
268:
269: int numNodes = nodes.getLength();
270:
271: for (int i = 0; i < numNodes; i++) {
272: Node node = nodes.item(i);
273:
274: // Skip non-element nodes
275: if (!(node instanceof Element))
276: continue;
277:
278: Element element = (Element) node;
279:
280: // See if this element tag matches the property name
281: if (namesMatch(desc.getName(), element.getTagName())) {
282: // Get the property value
283: Object obValue = getObjectValue(desc, element);
284:
285: if (obValue != null) {
286: // Add the value to the list of values to be set
287: v.addElement(obValue);
288: }
289: }
290: }
291:
292: // Get the method used to set the property value
293: Method setter = desc.getWriteMethod();
294:
295: // If this property has no setter, don't write it
296: if (setter == null)
297: return;
298:
299: // Create a new array of property values
300: Object propArray = Array.newInstance(desc.getPropertyType()
301: .getComponentType(), v.size());
302:
303: // Copy the vector into the array
304: v.copyInto((Object[]) propArray);
305:
306: try {
307: // Store the array of property values
308: setter.invoke(ob, new Object[] { propArray });
309: } catch (InvocationTargetException exc) {
310: throw new IOException("Error setting property "
311: + desc.getName() + ": " + exc.toString());
312: } catch (IllegalAccessException exc) {
313: throw new IOException("Error setting property "
314: + desc.getName() + ": " + exc.toString());
315: }
316: }
317:
318: /** Examines a property's type to see which method should be used
319: * to parse the property's value.
320: * @param desc The description of the property
321: * @param element The XML element containing the property value
322: * @return The value stored in the element
323: * @throws IOException If there is an error reading the document
324: */
325: public Object getObjectValue(PropertyDescriptor desc,
326: Element element) throws IOException {
327: // Find out what kind of property it is
328: Class type = desc.getPropertyType();
329:
330: // If it's an array, get the base type
331: if (type.isArray()) {
332: type = type.getComponentType();
333: }
334:
335: // For native types, object wrappers for natives, and strings, use the
336: // basic parse routine
337: if (type.equals(Integer.TYPE) || type.equals(Long.TYPE)
338: || type.equals(Short.TYPE) || type.equals(Byte.TYPE)
339: || type.equals(Boolean.TYPE) || type.equals(Float.TYPE)
340: || type.equals(Double.TYPE)
341: || Integer.class.isAssignableFrom(type)
342: || Long.class.isAssignableFrom(type)
343: || Short.class.isAssignableFrom(type)
344: || Byte.class.isAssignableFrom(type)
345: || Boolean.class.isAssignableFrom(type)
346: || Float.class.isAssignableFrom(type)
347: || Double.class.isAssignableFrom(type)
348: || String.class.isAssignableFrom(type)) {
349: return readBasicType(type, element);
350: } else if (java.util.Date.class.isAssignableFrom(type)) {
351: // If it's a date, use the date parser
352: return readDate(element);
353: } else {
354: try {
355: // If it's an object, create a new instance of the object (it should
356: // be a bean, or there will be trouble)
357: Object newOb = type.newInstance();
358:
359: // Copy the XML element into the bean
360: readObject(newOb, element);
361:
362: return newOb;
363: } catch (InstantiationException exc) {
364: throw new IOException("Error creating object for "
365: + desc.getName() + ": " + exc.toString());
366: } catch (IllegalAccessException exc) {
367: throw new IOException("Error creating object for "
368: + desc.getName() + ": " + exc.toString());
369: }
370: }
371: }
372:
373: /** Examines a property's type to see which method should be used
374: * to parse the property's value.
375: * @param desc The description of the property
376: * @param value The value of the XML attribute containing the prop value
377: * @return The value stored in the element
378: * @throws IOException If there is an error reading the document
379: */
380: public Object getObjectValue(PropertyDescriptor desc, String value)
381: throws IOException {
382: // Find out what kind of property it is
383: Class type = desc.getPropertyType();
384:
385: // If it's an array, get the base type
386: if (type.isArray()) {
387: type = type.getComponentType();
388: }
389:
390: // For native types, object wrappers for natives, and strings, use the
391: // basic parse routine
392: if (type.equals(Integer.TYPE) || type.equals(Long.TYPE)
393: || type.equals(Short.TYPE) || type.equals(Byte.TYPE)
394: || type.equals(Boolean.TYPE) || type.equals(Float.TYPE)
395: || type.equals(Double.TYPE)
396: || Integer.class.isAssignableFrom(type)
397: || Long.class.isAssignableFrom(type)
398: || Short.class.isAssignableFrom(type)
399: || Byte.class.isAssignableFrom(type)
400: || Boolean.class.isAssignableFrom(type)
401: || Float.class.isAssignableFrom(type)
402: || Double.class.isAssignableFrom(type)) {
403: return parseBasicType(type, value);
404: } else if (String.class.isAssignableFrom(type)) {
405: return value;
406: } else if (java.util.Date.class.isAssignableFrom(type)) {
407: // If it's a date, use the date parser
408: return parseDate(value, JOXDateHandler
409: .determineDateFormat());
410: } else {
411: return null;
412: }
413: }
414:
415: /** Reads an XML text element into a basic type
416: * @param type The type of the element to read
417: * @param element The element containing the value
418: * @return The parsed value of the element
419: */
420: public static Object readBasicType(Class type, Element element) {
421: // Get the text corresponding to this element
422: String str = getElementString(element);
423:
424: // If there isn't any, don't parse
425: if (str == null)
426: return null;
427:
428: // If it's a string, don't need to do anything else
429: if (String.class.isAssignableFrom(type))
430: return str;
431:
432: return parseBasicType(type, str);
433: }
434:
435: /** Reads an string into a basic type
436: * @param type The type of the string to read
437: * @param str The string containing the value
438: * @return The parsed value of the string
439: */
440: public static Object parseBasicType(Class type, String str) {
441: // Parse the text based on the property type
442:
443: if (type.equals(Integer.TYPE)
444: || Integer.class.isAssignableFrom(type)) {
445: return new Integer(str);
446: } else if (type.equals(Long.TYPE)
447: || Long.class.isAssignableFrom(type)) {
448: return new Long(str);
449: } else if (type.equals(Short.TYPE)
450: || Short.class.isAssignableFrom(type)) {
451: return new Short(str);
452: } else if (type.equals(Byte.TYPE)
453: || Byte.class.isAssignableFrom(type)) {
454: return new Byte(str);
455: } else if (type.equals(Boolean.TYPE)
456: || Boolean.class.isAssignableFrom(type)) {
457: return new Boolean(str);
458: } else if (type.equals(Float.TYPE)
459: || Float.class.isAssignableFrom(type)) {
460: return new Float(str);
461: } else if (type.equals(Double.TYPE)
462: || Double.class.isAssignableFrom(type)) {
463: return new Double(str);
464: }
465:
466: return null;
467: }
468:
469: /** Parses an XML element as a date
470: * @param element The element containing the date
471: * @return The date value parsed from the element
472: * @throws IOException If there's an error parsing the date
473: */
474: public Object readDate(Element element) throws IOException {
475: // Get the text corresponding to this element
476: String str = getElementString(element);
477: String fmt = element.getAttribute("format");
478: if ("".equals(fmt)) {
479: return parseDate(str, JOXDateHandler.determineDateFormat());
480: } else {
481: return parseDate(str, new SimpleDateFormat(fmt));
482: }
483: }
484:
485: /** Parses a string as a date
486: * @param str The string containing the date
487: * @return The date value parsed from the string
488: * @throws IOException If there's an error parsing the date
489: */
490: public Object parseDate(String str, DateFormat dateFormat)
491: throws IOException {
492: // If there isn't any, don't parse
493: if (str == null)
494: return null;
495:
496: try {
497: return dateFormat.parse(str);
498: } catch (ParseException exc) {
499: throw new IOException("Error parsing date " + str);
500: }
501: }
502:
503: /** Searches the children of an element looking for a Text node. If
504: * it finds one, it returns it.
505: * @param element The element whose children will be searched
506: * @return The text for the element, or null if there is none
507: */
508: public static String getElementString(Element element) {
509: NodeList nodes = element.getChildNodes();
510:
511: int numNodes = nodes.getLength();
512:
513: for (int i = 0; i < numNodes; i++) {
514: Node node = nodes.item(i);
515:
516: if (node instanceof Text) {
517: return ((Text) node).getData();
518: }
519: }
520:
521: return null;
522: }
523:
524: /** Returns true if two names match without regard to case or the
525: * presence of '-' or '_' characters.
526: * @param beanName The name of the bean property to compare
527: * @param elementName The name of the element to compare
528: * @return True if the names match
529: */
530: public static boolean namesMatch(String beanName, String elementName) {
531: int beanNameLen = beanName.length();
532: int elementNameLen = elementName.length();
533:
534: int elementPos = 0;
535: int beanPos = 0;
536:
537: // Keep looping until you hit the end of either of the strings
538: while ((elementPos < elementNameLen) && (beanPos < beanNameLen)) {
539: // If the next character in the bean name is a '-' or a '/', skip it
540: char beanCh = Character.toLowerCase(beanName
541: .charAt(beanPos));
542: if ((beanCh == '-') || (beanCh == '_')) {
543: beanPos++;
544: continue;
545: }
546:
547: // If the next character in the element name is a '-' or a '/', skip it
548: char elementCh = Character.toLowerCase(elementName
549: .charAt(elementPos));
550: if ((elementCh == '-') || (elementCh == '_')) {
551: elementPos++;
552: continue;
553: }
554:
555: // If the characters don't match, the names don't match
556: if (elementCh != beanCh)
557: return false;
558: elementPos++;
559: beanPos++;
560: }
561:
562: // You hit the end of both names at the same time, the names match
563: if ((elementPos == elementNameLen) && (beanPos == beanNameLen)) {
564: return true;
565: }
566:
567: return false;
568: }
569: }
|