001: /*
002: #IFNDEF ALT_LICENSE
003: ThinWire(R) RIA Ajax Framework
004: Copyright (C) 2003-2007 Custom Credit Systems
005:
006: This library is free software; you can redistribute it and/or modify it under
007: the terms of the GNU Lesser General Public License as published by the Free
008: Software Foundation; either version 2.1 of the License, or (at your option) any
009: later version.
010:
011: This library is distributed in the hope that it will be useful, but WITHOUT ANY
012: WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
013: PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
014:
015: You should have received a copy of the GNU Lesser General Public License along
016: with this library; if not, write to the Free Software Foundation, Inc., 59
017: Temple Place, Suite 330, Boston, MA 02111-1307 USA
018:
019: Users who would rather have a commercial license, warranty or support should
020: contact the following company who invented, built and supports the technology:
021:
022: Custom Credit Systems, Richardson, TX 75081, USA.
023: email: info@thinwire.com ph: +1 (888) 644-6405
024: http://www.thinwire.com
025: #ENDIF
026: [ v1.2_RC2 ]
027: */
028: package thinwire.util;
029:
030: import java.io.File;
031: import java.io.InputStream;
032: import java.lang.reflect.Field;
033: import java.lang.reflect.InvocationTargetException;
034: import java.lang.reflect.Method;
035: import java.lang.reflect.Modifier;
036: import java.util.ArrayList;
037: import java.util.Collection;
038: import java.util.Collections;
039: import java.util.HashMap;
040: import java.util.List;
041: import java.util.Map;
042: import java.util.logging.Level;
043: import java.util.logging.Logger;
044: import java.util.regex.Matcher;
045: import java.util.regex.Pattern;
046:
047: import javax.xml.parsers.DocumentBuilder;
048: import javax.xml.parsers.DocumentBuilderFactory;
049:
050: import org.w3c.dom.DOMException;
051: import org.w3c.dom.Document;
052: import org.w3c.dom.NamedNodeMap;
053: import org.w3c.dom.Node;
054: import org.w3c.dom.NodeList;
055:
056: import thinwire.ui.Application;
057:
058: /**
059: * Xml Object Document (XOD) generates objects from the XML file, and associates them with a Map.
060: * XODs can be used to create instances of plain old java objects (POJO). For example, a Dialog can be
061: * created as follows:<p>
062: * <h3>Sample File</h3>
063: * <img src="doc-files/XOD-1.png"> <p>
064: * <h3>Sample File XML</h3>
065: * <pre>
066: * <?xml version="1.0"?>
067: * <xod>
068: * <include file="classes.xml"/>
069: * <Dialog id="dialog">
070: * <title>XOD Demo File</title>
071: * <width>300</width>
072: * <height>200</height>
073: * <children>
074: * <TextField id="name">
075: * <x>90</x>
076: * <y>5</y>
077: * <width>80</width>
078: * <height>25</height>
079: * </TextField>
080: * <Label>
081: * <text>Name:</text>
082: * <labelFor>name</labelFor>
083: * <alignX>RIGHT</alignX>
084: * <x>5</x>
085: * <y>5</y>
086: * <width>80</width>
087: * <height>25</height>
088: * </Label>
089: * </children>
090: * </Dialog>
091: * </xod>
092: * </pre>
093: * <h3>Sample Java Code Using XOD</h3>
094: * <pre>
095: * XOD xod = new XOD();
096: * xod.execute("dialog.xml");
097: * Dialog dialog = (Dialog)xod.getObjectMap().get("dialog");
098: * dialog.setVisible(true);
099: * </pre>
100: * <h3>Sample Alias XML File</h3>
101: * The <alias> tag associates a short name with a class name. This content doesn't
102: * have to be separate. Instead of using the <include> tag in the demo file displayed
103: * above, the demo file could have included it directly.<p>
104: * <pre>
105: * <xod>
106: * <alias name="Button" class="thinwire.ui.Button"/>
107: * <alias name="CheckBox" class="thinwire.ui.CheckBox"/>
108: * <alias name="Dialog" class="thinwire.ui.Dialog"/>
109: * <alias name="Divider" class="thinwire.ui.Divider"/>
110: * <alias name="DropDownGridBox" class="thinwire.ui.DropDownGridBox"/>
111: * <alias name="Frame" class="thinwire.ui.Frame"/>
112: * <alias name="GridBox" class="thinwire.ui.GridBox"/>
113: * <alias name="Image" class="thinwire.ui.Image"/>
114: * <alias name="Label" class="thinwire.ui.Label"/>
115: * <alias name="Menu" class="thinwire.ui.Menu"/>
116: * <alias name="Panel" class="thinwire.ui.Panel"/>
117: * <alias name="RadioButton" class="thinwire.ui.RadioButton"/>
118: * <alias name="RadioButton.Group" class="thinwire.ui.RadioButton$Group"/>
119: * <alias name="TabFolder" class="thinwire.ui.TabFolder"/>
120: * <alias name="TabSheet" class="thinwire.ui.TabSheet"/>
121: * <alias name="TextArea" class="thinwire.ui.TextArea"/>
122: * <alias name="TextField" class="thinwire.ui.TextField"/>
123: * </xod>
124: *</pre>
125: *<h3>Notes on the XOD Tags</h3>
126: *<h4><xod></h4>
127: *The <xod> tag is the top level tag. It occurs once and encloses all other
128: *tags.
129: *<h4><include></h4>
130: *The <include> tag allows you to include other XOD XML files within an XOD file.
131: *Use it to reduce duplication. E.g. You can use it to include an alias file like the
132: *one shown above.
133: *<h4><alias></h4>
134: *Many of the tags in the demo listed above can be thought of as
135: *instruction to construct plain old java objects. E.g. The <Button> tag can be thought of as
136: *instruction to build a Button class instance.
137: *<p>
138: *In general, an XOD file can contain tags of the form
139: *<pre>
140: * <com.mypackage.XXXX>
141: *</pre>
142: *where XXXX is a class, and such a tag can be thought of as an
143: *instruction to create an instance of the XXXX class.<p>
144: *But the full class names are long. To allow for shorter tags in your xod files, you can define an alias: <br>
145: *<pre>
146: * <alias name="Button" class="thinwire.ui.Button"/>
147: *</pre>
148: *Once you've included this alias in your file, you can construct
149: *an object using it:<br>
150: *<pre>
151: * <Button id="button_ok">
152: * <text>OK</text>
153: * <x>73</x>
154: * <y>301</y>
155: * <width>84</width>
156: * <height>30</height>
157: * </Button>
158: * </pre>
159: *<h4><ref></h4>
160: *Some objects need to be associated with other objects.
161: *E.g. We need a means to indicate that the RadioButtons described in a
162: *container are members of the RadioButton.Group.
163: *We use the <ref> tag to accomplish this.
164: *<p>
165: *<h4>Object Class Tags</h4>
166: *If there's a class with the name "com.mypackage.XXXX", you can include an
167: *<com.mypackage.XXXX> element in your XOD definition. You can also
168: *create an alias for that class and use it in place of the class name.
169: *<p>
170: *<h4>Component Property Tags - e.g. <width>, <text>, <x></h4>
171: *To specify properties - e.g. location, size, and text - for the components you wish
172: *to build, add property tag elements. In general, if a UI component class has a "setXxxx" method,
173: *you can include an <xxxx> property tag.<p>
174: *E.g. Since the thinwire.ui.Button class has a setText method,
175: *you can specify the text for your button with a <text> element.
176: *
177: *<pre>
178: * <Button id="button_ok">
179: * ....
180: * <text>OK</text>
181: * ....
182: * </Button>
183: * </pre>
184: *
185: * <h4>Property Tag Values</h4>
186: * The properties of objects have types. E.g. The text property of the
187: * Button class is of type String, and the x property of the Button class is of type
188: * int. Other standard types are Integer, double, Double, char, Character, long, Long,
189: * boolean, Boolean, float, Float, byte, and Byte.
190: * <p>
191: * In the case of these standard types, the XOD class will make the appropriate conversion
192: * while processing a xod file. E.g. When it comes across
193: * <pre>
194: * <Button id="Search_btn">
195: * .....
196: * <x>73</x>
197: * .....
198: * </Button>
199: * </pre>
200: * the XOD engine will convert "73" to an int and assign the x property of the
201: * new Button that int value.
202: * <p>
203: * In the case of non-literal types, XOD has three special strategies.
204: * First, it will check to see if the class type of the property you are assigning
205: * to has a 'valueOf' method that returns the appropriate type. If it does, it
206: * will call that method and assign the returned value to the property.
207: * E.g. The Label class has an alignX property of type thinwire.ui.AlignX.
208: * This class has a 'valueOf' method. When XOD processes the above demo file and comes across
209: * <pre>
210: * <Label>
211: * .....
212: * <alignX>RIGHT</alignX>
213: * .....
214: * </Label>
215: * </pre>
216: * it calls the static 'valueOf' on AlignX, which returns the appropriate constant AlignX.RIGHT.
217: * Second, it will check to see if the class type has a static final field (i.e. constant) that
218: * is named the same as the value specified in the property tag. If it finds a constant, it will
219: * be assed to the property.
220: * Third, it looks for an object defined elsewhere in the XOD, an object
221: * whose id in the XOD matches the property value. If it finds one, it assigns
222: * the object as a value for the property. E.g. When it comes across
223: * <pre>
224: * <Label>
225: * .....
226: * <labelFor>name</labelFor>
227: * .....
228: * </Label>
229: * </pre>
230: * then XOD engine discovers that there is a 'name' object defined previously, and it assigns
231: * this object to the Label's labelFor property.<p>
232: * <p>
233: * <h4>Collection Properties</h4>
234: * A component may have a property with a Collection type. E.g. Dialogs have
235: * a getChildren method, and a children property. The children of a Dialog form
236: * a collection.<p>
237: * When an XML element represents a property with a Collection type, the content of
238: * the element represents the members of the collection. E.g. When XOD comes
239: * across
240: * <pre>
241: * <children>
242: * <Label>
243: * ....
244: * </Label>
245: * <Label>
246: * ....
247: * </Label>
248: * <Label>
249: * ....
250: * </Label>
251: * <Divider>
252: * ....
253: * </Divider>
254: * <Divider>
255: * ....
256: * </Divider>
257: * <TextField id="name">
258: * ....
259: * </TextField>
260: * ....
261: * </children>
262: * </pre>
263: * it constructs Label, Divider, and TextField components and makes these
264: * components children of the Dialog.
265: *
266: * @author Joshua J. Gertzen
267: */
268: public final class XOD {
269: private static final Logger log = Logger.getLogger(XOD.class
270: .getName());
271: private static final Level LEVEL = Level.FINER;
272:
273: private static File getRelativeFile(String uri) {
274: Application app = Application.current();
275: return app == null ? new File(uri) : app.getRelativeFile(uri);
276: }
277:
278: private static File getRelativeFile(String parent, String child) {
279: Application app = Application.current();
280: return app == null ? new File(parent, child) : app
281: .getRelativeFile(parent, child);
282: }
283:
284: private Map<String, Object> objectMap;
285: private Map<Object, String> idMap;
286: private List<Object> rootObjects;
287: private List<Object> objects;
288: private Map<String, Class> aliases;
289: private Map<String, String> properties;
290: private Map<Class, Map<String, Object[]>> propertyAliases;
291: private List<String> uriStack;
292: private boolean processingInclude;
293:
294: /**
295: * Create a new XOD.
296: */
297: public XOD() {
298: this (null);
299: }
300:
301: /**
302: * Create a new XOD.
303: * @param uri the name of the XML Object Document file to execute.
304: */
305: public XOD(String uri) {
306: rootObjects = new ArrayList<Object>();
307: objectMap = new HashMap<String, Object>();
308: objects = new ArrayList<Object>();
309: aliases = new HashMap<String, Class>();
310: properties = new HashMap<String, String>();
311: uriStack = new ArrayList<String>();
312: if (uri != null)
313: execute(uri);
314: }
315:
316: public Map<String, Class> getAliasMap() {
317: return aliases;
318: }
319:
320: public Map<String, String> getPropertyMap() {
321: return properties;
322: }
323:
324: /**
325: * The object map contains references to previously defined objects
326: * identified by id. Typically, it will map an ID found in the
327: * XOD to an object.
328: * @return the object Map
329: */
330: public Map<String, Object> getObjectMap() {
331: return objectMap;
332: }
333:
334: public List<Object> getObjects() {
335: return Collections.unmodifiableList(objects);
336: }
337:
338: public List<Object> getRootObjects() {
339: return Collections.unmodifiableList(rootObjects);
340: }
341:
342: /**
343: * Gets the id that is associated to the specified object.
344: * For this to succeed, the object must be in the object map.
345: * @param o the object to retrieve the id for.
346: * @return the id associated to the object, or null if the object is not in the object map.
347: */
348: public String getObjectId(Object o) {
349: return getIdMap().get(o);
350: }
351:
352: private Map<Object, String> getIdMap() {
353: if (idMap == null) {
354: idMap = new HashMap<Object, String>();
355:
356: for (Map.Entry<String, Object> entry : objectMap.entrySet()) {
357: idMap.put(entry.getValue(), entry.getKey());
358: }
359: }
360:
361: return idMap;
362: }
363:
364: /**
365: * Executes a XOD file, adding the created objects to the map and list.
366: * @param uri the name of the XML Object Document file.
367: */
368: public void execute(String uri) {
369: processFile(null, uri, 0);
370: }
371:
372: private Object processFile(Object parent, String uri, int level) {
373: Object ret = null;
374:
375: try {
376: DocumentBuilder builder = DocumentBuilderFactory
377: .newInstance().newDocumentBuilder();
378: uri = uri.replace('\\', '/');
379: InputStream is = Application.getResourceAsStream(uri);
380: if (is == null)
381: throw new IllegalArgumentException(
382: "Content for URI was not found:" + uri);
383: uriStack.add(uri);
384: int index = uri.lastIndexOf('/');
385: if (index == -1)
386: index = 0;
387: properties.put("xod.file", uri.substring(index));
388: properties.put("xod.path", index == 0 ? "" : uri.substring(
389: 0, index));
390:
391: Document doc = builder.parse(is);
392: is.close();
393: ret = processBranch(parent, doc.getChildNodes(), level);
394: } catch (Exception e) {
395: throw new RuntimeException("processing file '" + uri + "'",
396: e);
397: } finally {
398: uriStack.remove(uriStack.size() - 1);
399:
400: if (uriStack.size() > 0) {
401: uri = uriStack.get(uriStack.size() - 1);
402: int index = uri.lastIndexOf('/');
403: if (index == -1)
404: index = 0;
405: properties.put("xod.file", uri.substring(index));
406: properties.put("xod.path", index == 0 ? "" : uri
407: .substring(0, index));
408: }
409: }
410:
411: return ret;
412: }
413:
414: private void appendAttributes(Node n) {
415: NamedNodeMap nnm = n.getAttributes();
416: Document doc = n.getOwnerDocument();
417:
418: for (int i = 0, cnt = nnm.getLength(); i < cnt; i++) {
419: Node attribute = nnm.item(i);
420: Node tag = doc.createElement(attribute.getNodeName());
421: tag.appendChild(doc
422: .createTextNode(attribute.getNodeValue()));
423: n.appendChild(tag);
424: }
425: }
426:
427: private Object processBranch(Object parent, NodeList nl, int level) {
428: for (int i = 0, cnt = nl.getLength(); i < cnt; i++) {
429: Node n = nl.item(i);
430:
431: switch (n.getNodeType()) {
432: case Node.COMMENT_NODE:
433: //Valid, but just ignored
434: break;
435:
436: case Node.TEXT_NODE:
437: if (n.getNodeValue().trim().length() != 0)
438: throw new DOMException(
439: DOMException.NO_DATA_ALLOWED_ERR,
440: "a value for the <" + n.getNodeName()
441: + "> tag is not allowed");
442: break;
443:
444: case Node.ELEMENT_NODE:
445: evaluateNode(parent, n, level);
446: break;
447:
448: default:
449: throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
450: "node type " + n.getNodeType()
451: + " is not supported");
452: }
453: }
454:
455: return null;
456: }
457:
458: private Object evaluateNode(Object parent, Node n, int level) {
459: Object ret = null;
460: String name = n.getNodeName();
461:
462: if (name.equals("xod")) {
463: if (n.getChildNodes().getLength() == 0)
464: throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
465: name + ":n.getChildNodes().getLength() == 0");
466: if (level != 0 && !processingInclude)
467: throw new DOMException(DOMException.INVALID_STATE_ERR,
468: "level != 0");
469: if (log.isLoggable(LEVEL))
470: log.log(LEVEL, "xod");
471: processBranch(parent, n.getChildNodes(), level + 1);
472: } else if (name.equals("property") && parent == null) {
473: if (n.getAttributes().getLength() != 2)
474: throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
475: name + ":n.getAttributes().getLength() != 2");
476: String key = (String) n.getAttributes()
477: .getNamedItem("name").getNodeValue();
478: String value = (String) n.getAttributes().getNamedItem(
479: "value").getNodeValue();
480: value = this .replaceProperties(value);
481: properties.put(key, value);
482: if (log.isLoggable(LEVEL))
483: log.log(LEVEL, "property[name:" + key + ",value:"
484: + value + "]");
485: } else if (name.equals("alias")) {
486: if (n.getAttributes().getLength() != 2)
487: throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
488: name + ":n.getAttributes().getLength() != 2");
489: //if (n.getChildNodes().getLength() != 0) throw new DOMException(DOMException.NOT_SUPPORTED_ERR, name + ":n.getChildNodes().getLength() != 0");
490: String aliasName = (String) n.getAttributes().getNamedItem(
491: "name").getNodeValue();
492: String className = this .replaceProperties((String) n
493: .getAttributes().getNamedItem("class")
494: .getNodeValue());
495: Class clazz = getClassForName(aliasName, className);
496: if (n.hasChildNodes())
497: processBranch(clazz, n.getChildNodes(), level + 1);
498: if (log.isLoggable(LEVEL))
499: log.log(LEVEL, "alias[name:" + aliasName + ",class:"
500: + className + "]");
501: } else if (name.equals("property") && parent instanceof Class) {
502: if (n.getAttributes().getLength() != 2
503: && n.getAttributes().getLength() != 3)
504: throw new DOMException(
505: DOMException.NOT_SUPPORTED_ERR,
506: name
507: + ":n.getAttributes().getLength() != 2 && n.getAttributes().getLength() != 3");
508: String propName = this .replaceProperties((String) n
509: .getAttributes().getNamedItem("name")
510: .getNodeValue());
511: String aliasName = this .replaceProperties((String) n
512: .getAttributes().getNamedItem("alias")
513: .getNodeValue());
514: String className = this .replaceProperties((String) n
515: .getAttributes().getNamedItem("class")
516: .getNodeValue());
517: setPropertyAlias((Class) parent, propName, aliasName,
518: getClassForName(null, className));
519: } else if (name.equals("include")) {
520: if (n.getAttributes().getLength() != 1)
521: throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
522: name + ":n.getAttributes().getLength() != 1");
523: if (n.getChildNodes().getLength() != 0)
524: throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
525: name + ":n.getChildNodes().getLength() != 0");
526: String fileUri = this .replaceProperties((String) n
527: .getAttributes().getNamedItem("file")
528: .getNodeValue());
529:
530: if (!getRelativeFile(fileUri).exists()) {
531: String curFileUri = uriStack.get(0);
532: String newUri = fileUri;
533:
534: do {
535: File parentFolder = getRelativeFile(curFileUri)
536: .getParentFile();
537:
538: if (parentFolder == null) {
539: newUri = null;
540: break;
541: }
542:
543: curFileUri = parentFolder.getAbsolutePath();
544: newUri = getRelativeFile(curFileUri, fileUri)
545: .getAbsolutePath();
546: } while (!getRelativeFile(newUri).exists());
547:
548: if (newUri != null)
549: fileUri = newUri;
550: }
551:
552: processingInclude = true;
553: processFile(parent, fileUri, level + 1);
554: processingInclude = false;
555: if (log.isLoggable(LEVEL))
556: log.log(LEVEL, "include[file:" + fileUri + "]");
557: } else if (name.equals("ref")) {
558: if (n.getAttributes().getLength() != 1)
559: throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
560: name + ":n.getAttributes().getLength() != 1");
561: if (n.getChildNodes().getLength() != 0)
562: throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
563: name + ":n.getChildNodes().getLength() != 0");
564: String id = (String) n.getAttributes().getNamedItem("id")
565: .getNodeValue();
566: ret = objectMap.get(id);
567: if (log.isLoggable(LEVEL))
568: log.log(LEVEL, "ref[id:" + id + "]");
569:
570: if (parent != null && parent instanceof Collection) {
571: if (log.isLoggable(LEVEL))
572: log.log(LEVEL, "adding ref child to collection");
573: ((Collection) parent).add(ret);
574: }
575: } else {
576: boolean property = false;
577:
578: //If there is a parent and the tag name does not contain a period and the first character is lowerCase, then this might be a property
579: if (parent != null && name.indexOf('.') == -1
580: && Character.isLowerCase(name.charAt(0))) {
581: //if (n.getAttributes().getLength() != 0) throw new DOMException(DOMException.NOT_SUPPORTED_ERR, name + ":n.getAttributes().getLength() != 0");
582:
583: //Check to see if this property has an Alias.
584: Object[] propertyAlias = getPropertyAlias(parent
585: .getClass(), name);
586: if (propertyAlias != null)
587: name = (String) propertyAlias[0];
588:
589: //Search for a set method for the property
590: String setter = Character.toUpperCase(name.charAt(0))
591: + name.substring(1);
592: String getter = "get" + setter;
593: String istter = "is" + setter;
594: setter = "set" + setter;
595:
596: Method setMethod = null;
597: Method getMethod = null;
598:
599: for (Method m : parent.getClass().getMethods()) {
600: if (setMethod != null && getMethod != null)
601: break;
602: String methodName = m.getName();
603:
604: if (methodName.equals(setter)) {
605: setMethod = m;
606: } else if (methodName.equals(getter)
607: || methodName.equals(istter)) {
608: getMethod = m;
609: }
610: }
611:
612: if (getMethod != null) {
613: if (setMethod == null) {
614: Object subObject = invoke(parent, getMethod,
615: (Object[]) null);
616: appendAttributes(n);
617: processBranch(subObject, n.getChildNodes(),
618: level + 1);
619: property = true;
620: } else {
621: Class[] params = setMethod.getParameterTypes();
622:
623: if (params.length == 1) {
624: NodeList propNodes = n.getChildNodes();
625: Node node = null;
626:
627: if (propNodes.getLength() == 1)
628: node = propNodes.item(0);
629: else {
630: for (int i = propNodes.getLength() - 1; i >= 0; i--) {
631: Node item = propNodes.item(i);
632:
633: if (item.getNodeType() == Node.ELEMENT_NODE) {
634: node = item;
635: break;
636: }
637: }
638: }
639:
640: if (node != null) {
641: short nodeType = node.getNodeType();
642: Class paramClass = propertyAlias != null ? (Class) propertyAlias[1]
643: : params[0];
644: Object value;
645:
646: //If there is only one property then this is a simple value assignment
647: //Else if there is three then there is a tag being set as the value
648: if (nodeType == Node.TEXT_NODE) {
649: value = getObjectForTypeFromString(
650: paramClass, (String) node
651: .getNodeValue(),
652: true);
653: } else if (nodeType == Node.ELEMENT_NODE) {
654: value = evaluateNode(null, node,
655: level + 1);
656: Class valueClass = value.getClass();
657:
658: if (!paramClass
659: .isAssignableFrom(valueClass)) {
660: value = getObjectForTypeFromString(
661: paramClass, value
662: .toString(),
663: false);
664: if (value == null)
665: throw new DOMException(
666: DOMException.NOT_SUPPORTED_ERR,
667: "no conversion from "
668: + valueClass
669: + " to "
670: + paramClass
671: + " is known");
672: }
673: } else {
674: throw new DOMException(
675: DOMException.NOT_SUPPORTED_ERR,
676: "a property tag can only contain a value or a single tag");
677: }
678:
679: if (value == null)
680: throw new DOMException(
681: DOMException.NOT_SUPPORTED_ERR,
682: "the property tag's value resolved to null which is not valid");
683:
684: invoke(parent, setMethod, value);
685: if (log.isLoggable(LEVEL))
686: log.log(LEVEL, "set property "
687: + name + " = " + value);
688: }
689:
690: property = true;
691: }
692: }
693: }
694: }
695:
696: //If this was determined to not be a property, this must be a class instantiation
697: if (!property) {
698: Class c = aliases.get(name);
699: if (c == null)
700: c = getClassForName(name, name);
701:
702: NodeList children = n.getChildNodes();
703:
704: try {
705: String id = null;
706: NamedNodeMap attrs = n.getAttributes();
707: List<Object[]> nonStatic = null;
708:
709: for (int cnt = attrs.getLength(); --cnt >= 0;) {
710: Node attr = attrs.item(cnt);
711: String attrName = attr.getNodeName();
712: String attrValue = attr.getNodeValue();
713:
714: if (attrName.equals("id")) {
715: id = attrValue;
716: } else {
717: String setter = "set"
718: + Character.toUpperCase(attrName
719: .charAt(0))
720: + attrName.substring(1);
721: boolean found = false;
722:
723: for (Method m : c.getMethods()) {
724: String methodName = m.getName();
725:
726: if (methodName.equals(attrName)
727: || methodName.equals(setter)) {
728: Class[] args = m
729: .getParameterTypes();
730:
731: if (args.length == 1) {
732: found = true;
733: Object arg = getObjectForTypeFromString(
734: args[0], attrValue,
735: true);
736:
737: if (Modifier.isStatic(m
738: .getModifiers())) {
739: ret = invoke(null, m, arg);
740:
741: if (!c.isInstance(ret)) {
742: if (log
743: .isLoggable(LEVEL))
744: log
745: .log(
746: LEVEL,
747: "invoked static id="
748: + id
749: + ":"
750: + c
751: .getName()
752: + "."
753: + methodName
754: + "('"
755: + attrValue
756: + "')");
757: ret = null;
758: } else {
759: if (log
760: .isLoggable(LEVEL))
761: log
762: .log(
763: LEVEL,
764: "static factory new id="
765: + id
766: + ":"
767: + c
768: .getName()
769: + "."
770: + methodName
771: + "('"
772: + attrValue
773: + "')");
774: }
775: } else {
776: if (nonStatic == null)
777: nonStatic = new ArrayList<Object[]>(
778: 3);
779:
780: if (methodName
781: .equals(attrName)) {
782: nonStatic
783: .add(
784: 0,
785: new Object[] {
786: m,
787: arg });
788: } else {
789: nonStatic
790: .add(new Object[] {
791: m, arg });
792: }
793: }
794: }
795: }
796: }
797:
798: if (!found)
799: throw new DOMException(
800: DOMException.NOT_FOUND_ERR,
801: "no target method found id="
802: + id + ":"
803: + c.getName() + "."
804: + attrName + "('"
805: + attrValue + "')");
806: }
807: }
808:
809: if (ret == null) {
810: ret = c.newInstance();
811: if (log.isLoggable(LEVEL))
812: log.log(LEVEL, "new " + c.getName());
813: }
814:
815: if (nonStatic != null) {
816: for (Object[] callMeth : nonStatic) {
817: invoke(ret, (Method) callMeth[0],
818: callMeth[1]);
819: if (log.isLoggable(LEVEL))
820: log.log(LEVEL, "invoked id="
821: + id
822: + ":"
823: + c.getName()
824: + "."
825: + ((Method) callMeth[0])
826: .getName() + "("
827: + callMeth[1] + ")");
828: }
829: }
830:
831: if (id != null)
832: objectMap.put(id, ret);
833: objects.add(ret);
834: if (level == 1)
835: rootObjects.add(ret);
836:
837: if (parent != null && parent instanceof Collection) {
838: if (log.isLoggable(LEVEL))
839: log
840: .log(LEVEL,
841: "adding child to collection");
842: ((Collection) parent).add(ret);
843: }
844:
845: processBranch(ret, children, level + 1);
846: } catch (IllegalAccessException e) {
847: throw new RuntimeException(e);
848: } catch (InstantiationException e) {
849: throw new RuntimeException(e);
850: }
851: }
852: }
853:
854: return ret;
855: }
856:
857: private static final Pattern REGEX_PROPERTY = Pattern.compile(
858: "(.*?)[$][{]([\\w.]+)[}](.*?)", Pattern.DOTALL);
859:
860: private String replaceProperties(String value) {
861: if (log.isLoggable(LEVEL))
862: log.log(LEVEL, "before replaceProperties:" + value);
863: if (value != null && value.indexOf("${") >= 0) {
864: StringBuffer sb = new StringBuffer();
865: Matcher m = REGEX_PROPERTY.matcher(value);
866:
867: while (m.find()) {
868: String prop = m.group(2);
869: String val = properties.get(prop);
870: if (val == null)
871: val = "${" + prop + "}";
872: m.appendReplacement(sb, m.group(1) + val + m.group(3));
873: }
874:
875: m.appendTail(sb);
876: value = sb.toString();
877: }
878:
879: if (log.isLoggable(LEVEL))
880: log.log(LEVEL, "after replaceProperties:" + value);
881: return value;
882: }
883:
884: private Object getObjectForTypeFromString(Class type, String str,
885: boolean performObjectFieldLookup) {
886: Object value;
887: str = replaceProperties(str);
888:
889: if (type == String.class) {
890: value = str;
891: } else if (type == boolean.class || type == Boolean.class) {
892: value = Boolean.valueOf(str);
893: } else if (type == int.class || type == Integer.class) {
894: value = new Integer(Double.valueOf(str).intValue());
895: } else if (type == long.class || type == Long.class) {
896: value = new Long(Double.valueOf(str).longValue());
897: } else if (type == short.class || type == Short.class) {
898: value = new Short(Double.valueOf(str).shortValue());
899: } else if (type == byte.class || type == Byte.class) {
900: value = new Byte(Double.valueOf(str).byteValue());
901: } else if (type == float.class || type == Float.class) {
902: value = new Float(Double.valueOf(str).floatValue());
903: } else if (type == double.class || type == Double.class) {
904: value = Double.valueOf(str);
905: } else if (type == char.class || type == Character.class) {
906: value = new Character(str.charAt(0));
907: } else if (performObjectFieldLookup) {
908: //See if there is a constant with the name specified
909: try {
910: Method method = type.getMethod("valueOf", String.class);
911: value = method.invoke(null, str);
912: } catch (Exception e) {
913: String upperStr = str.toUpperCase();
914: value = null;
915:
916: for (Field f : type.getFields()) {
917: if (f.getName().toUpperCase().equals(upperStr)) {
918: try {
919: value = f.get(null);
920: } catch (IllegalAccessException e2) {
921: value = null;
922: }
923:
924: break;
925: }
926: }
927:
928: if (value == null)
929: value = objectMap.get(str);
930: }
931: } else
932: value = null;
933:
934: return value == null ? str : value;
935: }
936:
937: private Object invoke(Object object, Method method,
938: Object... params) {
939: try {
940: if (!method.isAccessible())
941: method.setAccessible(true);
942: return method.invoke(object, params);
943: } catch (InvocationTargetException e) {
944: throw new RuntimeException("["
945: + object.getClass().getName() + "."
946: + method.getName() + "]", e);
947: } catch (IllegalAccessException e) {
948: throw new RuntimeException("["
949: + object.getClass().getName() + "."
950: + method.getName() + "]", e);
951: }
952: }
953:
954: private void setPropertyAlias(Class clazz, String propertyName,
955: String aliasName, Class aliasClass) {
956: if (propertyAliases == null)
957: propertyAliases = new HashMap<Class, Map<String, Object[]>>(
958: 3);
959: Map<String, Object[]> map = propertyAliases.get(clazz);
960: if (map == null)
961: propertyAliases.put(clazz,
962: map = new HashMap<String, Object[]>(1));
963: map.put(aliasName, new Object[] { propertyName, aliasClass });
964: }
965:
966: private Object[] getPropertyAlias(Class clazz, String propertyName) {
967: if (propertyAliases != null) {
968: Map<String, Object[]> map = propertyAliases.get(clazz);
969: if (map != null)
970: return map.get(propertyName);
971: }
972:
973: return null;
974: }
975:
976: private Class getClassForName(String name, String className) {
977: try {
978: Class c = Class.forName(className);
979: if (name != null)
980: aliases.put(name, c);
981: return c;
982: } catch (ClassNotFoundException e) {
983: throw new RuntimeException(e);
984: }
985: }
986: }
|