1 /*
2 * GraphBuilder.java
3 * Created on September 18, 2003
4 *
5 * The Blues Framework - A lightweight application framework
6 * Copyright (C) 2003 Lonnie Pryor
7 * http://blues.lonniepryor.com
8 *
9 * This library is free software; you can redistribute it and/or modify it under the
10 * terms of the GNU Lesser General Public License as published by the Free Software
11 * Foundation; either version 2.1 of the License, or (at your option) any later
12 * version.
13 *
14 * This library is distributed in the hope that it will be useful, but WITHOUT ANY
15 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
16 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
17 *
18 * You should have received a copy of the GNU Lesser General Public License along
19 * with this library; if not, write to:
20 *
21 * The Free Software Foundation, Inc.
22 * 59 Temple Place, Suite 330
23 * Boston, MA 02111-1307 USA
24 *
25 */
26 package com.lonniepryor.blues.xml;
27
28 import java.io.IOException;
29
30 import java.net.URL;
31
32 import java.util.HashMap;
33 import java.util.LinkedList;
34 import java.util.Map;
35
36 import javax.xml.parsers.SAXParser;
37
38 import com.lonniepryor.blues.cfg.TextContainer;
39 import com.lonniepryor.blues.cfg.Verifiable;
40 import com.lonniepryor.blues.util.Strings;
41
42 import org.xml.sax.Attributes;
43 import org.xml.sax.Locator;
44 import org.xml.sax.SAXException;
45 import org.xml.sax.SAXParseException;
46 import org.xml.sax.helpers.DefaultHandler;
47
48 /***
49 * Handles transforming a single XML document into a graph of objects. NOTE: this
50 * class is NOT thread-safe and instances may only be used on a single thread at a
51 * time.
52 *
53 * @author Lonnie Pryor
54 * @version $Revision: 1.1 $
55 */
56 public class GraphBuilder {
57 /*** The URI of the Blues namespace. */
58 public static final String bluesNamespaceUri = "http://blues.lonniepryor.com";
59 /*** The class loader to load element classes from. */
60 private final ClassLoader classLoader;
61 /*** Cache of xml bean helpers. */
62 private final Map xmlBeanHelpers = new HashMap();
63 /*** The current relative path base. */
64 private String currentPathBase = null;
65 /*** The current value resolver. */
66 private ValueResolver resolver = null;
67 /*** The stack of element objects. */
68 private LinkedList stack = new LinkedList();
69 /*** The object representing the root element of the document. */
70 private Object result = null;
71
72 /***
73 * Creates a new GraphBuilder object.
74 *
75 * @param classLoader The class loader to load element classes from.
76 */
77 public GraphBuilder (ClassLoader classLoader) {
78 if (classLoader == null)
79 throw new NullPointerException("classLoader");
80 this.classLoader = classLoader;
81 }
82
83 /***
84 * Parses the XML document at the specified URL, transforming it into a graph of
85 * objects.
86 *
87 * @param parser The parser to use.
88 * @param documentPath The location of the document to parse.
89 *
90 * @return The object generated for the root element of the document.
91 */
92 public Object buildGraph (SAXParser parser, String documentPath) {
93 if (parser == null)
94 throw new NullPointerException("parser");
95 if (documentPath == null)
96 throw new NullPointerException("documentPath");
97 URL resource = classLoader.getResource(documentPath);
98 if (resource == null)
99 throw new XmlException("XML document not found at '" + documentPath + "'");
100 Object thisResult = null;
101 try {
102 currentPathBase = documentPath.substring(
103 0, documentPath.lastIndexOf('/') + 1);
104 parser.parse(resource.openStream(), newDocumentHandler());
105 thisResult = result;
106 } catch (SAXParseException spe) {
107 throw new XmlException(
108 "Parse error: " + resource.toExternalForm() + " line "
109 + spe.getLineNumber() + " column " + spe.getColumnNumber() + ": "
110 + spe.getMessage(), spe);
111 } catch (SAXException se) {
112 throw new XmlException(
113 "XML error '" + resource.toExternalForm() + "': " + se.getMessage(), se);
114 } catch (IOException ioe) {
115 throw new XmlException(
116 "IO error '" + resource.toExternalForm() + "': " + ioe.getMessage(), ioe);
117 } finally {
118 currentPathBase = null;
119 resolver = null;
120 stack.clear();
121 result = null;
122 }
123 return thisResult;
124 }
125
126 /***
127 * Signals the beginning of a prefixed element.
128 *
129 * @param attributes The element attributes.
130 */
131 private void startPrefixedElement (Attributes attributes) {
132 if (stack.isEmpty())
133 resolver = new ValueResolver(
134 classLoader, currentPathBase,
135 attributes.getValue(bluesNamespaceUri, "imports"));
136 XmlBeanHelper helper = getHelper(
137 resolver.resolveClass(attributes.getValue(bluesNamespaceUri, "cfg")));
138 stack.addLast(createElementInstance(helper, attributes));
139 }
140
141 /***
142 * Signals the end of a prefixed element.
143 */
144 private void endPrefixedElement () {
145 if (stack.isEmpty())
146 throw new IllegalStateException("Prefixed element to end not found");
147 Object instance = stack.removeLast();
148 if (instance instanceof Verifiable)
149 ((Verifiable)instance).verify();
150 if (stack.isEmpty())
151 result = instance;
152 else
153 getHelper(stack.getLast().getClass()).declarePrefixedElement(
154 stack.getLast(), instance);
155 }
156
157 /***
158 * Signals the beginning of a local element.
159 *
160 * @param name The name of the element.
161 * @param attributes The element attributes.
162 */
163 private void startLocalElement (String name, Attributes attributes) {
164 if (stack.isEmpty())
165 throw new IllegalStateException(
166 "Documents must begin with a prefixed element");
167 Class elementType = getHelper(stack.getLast().getClass())
168 .getLocalDeclarationType(transformName(name));
169 if (elementType == null)
170 throw new IllegalArgumentException(
171 "Local element declaration for '" + name + "' not found on "
172 + stack.getLast().getClass().getName());
173 stack.addLast(createElementInstance(getHelper(elementType), attributes));
174 }
175
176 /***
177 * Signals the end of a local element.
178 *
179 * @param name The name of the element.
180 */
181 private void endLocalElement (String name) {
182 if (stack.isEmpty())
183 throw new IllegalStateException("Local element to end not found");
184 Object instance = stack.removeLast();
185 if (instance instanceof Verifiable)
186 ((Verifiable)instance).verify();
187 if (stack.isEmpty())
188 throw new IllegalStateException("Parent of local element not found");
189 getHelper(stack.getLast().getClass()).declareLocalElement(
190 stack.getLast(), transformName(name), instance);
191 }
192
193 /***
194 * Signals the presence of character data.
195 *
196 * @param ch The character text.
197 * @param start The start of the active region.
198 * @param length The length of the active region.
199 */
200 private void handleCharacters (char[] ch, int start, int length) {
201 Object instance = stack.getLast();
202 if (instance instanceof TextContainer)
203 ((TextContainer)instance).appendText(ch, start, length);
204 }
205
206 /***
207 * Finds the xml bean helper for the specified class.
208 *
209 * @param forClass The class to find a helper for.
210 *
211 * @return The requested xml bean helper.
212 */
213 private XmlBeanHelper getHelper (Class forClass) {
214 XmlBeanHelper helper = (XmlBeanHelper)xmlBeanHelpers.get(forClass);
215 if (helper == null)
216 xmlBeanHelpers.put(forClass, helper = new XmlBeanHelper(forClass));
217 return helper;
218 }
219
220 /***
221 * Creates a new element of the specified type and populates its properties with
222 * the supplied attributes.
223 *
224 * @param helper The type of element to create.
225 * @param attrs The attributes to populate properties from.
226 *
227 * @return The new, populated instance.
228 */
229 private Object createElementInstance (XmlBeanHelper helper, Attributes attrs) {
230 Object instance = helper.newBeanInstance();
231 for (int i = 0; i < attrs.getLength(); ++i) {
232 if (Strings.notEmpty().isSatisfiedBy(attrs.getURI(i)))
233 continue;
234 String propertyName = transformName(attrs.getQName(i));
235 Class propertyType = helper.getPropertyType(propertyName);
236 if (propertyType == null)
237 throw new IllegalArgumentException(
238 "Property for '" + attrs.getQName(i) + "' not found on "
239 + instance.getClass().getName());
240 Object resolved = resolver.resolve(propertyType, attrs.getValue(i));
241 if (resolved != null)
242 helper.setProperty(instance, propertyName, resolved);
243 else if (
244 !helper.setPropertyAsString(instance, propertyName, attrs.getValue(i)))
245 throw new IllegalArgumentException(
246 "Cannot set property for '" + attrs.getQName(i) + "'");
247 }
248 return instance;
249 }
250
251 /***
252 * Transforms an xml name to a java name by removing each instance of '-' and
253 * capitalizing the character immeadiately following it.
254 *
255 * @param xmlName The xml name to transform.
256 *
257 * @return The transformed name.
258 */
259 private String transformName (String xmlName) {
260 if (xmlName == null)
261 throw new NullPointerException("xmlName");
262 StringBuffer javaName = new StringBuffer(xmlName.length());
263 boolean capitalize = false;
264 for (int i = 0; i < xmlName.length(); ++i) {
265 char current = xmlName.charAt(i);
266 switch (current) {
267 case '-':
268 capitalize = true;
269 break;
270 default:
271 javaName.append(capitalize ? Character.toUpperCase(current) : current);
272 capitalize = false;
273 }
274 }
275 return javaName.toString();
276 }
277
278 /***
279 * Creates a new document handler that acts as an exception boundry.
280 *
281 * @return A new document handler that acts as an exception boundry.
282 */
283 private DefaultHandler newDocumentHandler () {
284 return new DefaultHandler() {
285 Locator locator = null;
286
287 public void setDocumentLocator (Locator locator) {
288 this.locator = locator;
289 }
290
291 public void startElement (
292 String uri, String localName, String qName, Attributes attributes)
293 throws SAXException {
294 try {
295 if (!Strings.notEmpty().isSatisfiedBy(uri))
296 startLocalElement(qName, attributes);
297 else
298 startPrefixedElement(attributes);
299 } catch (Exception e) {
300 throw new SAXParseException(e.getMessage(), locator, e);
301 }
302 }
303
304 public void endElement (String uri, String localName, String qName)
305 throws SAXException {
306 try {
307 if (!Strings.notEmpty().isSatisfiedBy(uri))
308 endLocalElement(qName);
309 else
310 endPrefixedElement();
311 } catch (Exception e) {
312 throw new SAXParseException(e.getMessage(), locator, e);
313 }
314 }
315
316 public void characters (char[] ch, int start, int length)
317 throws SAXException {
318 try {
319 handleCharacters(ch, start, length);
320 } catch (Exception e) {
321 throw new SAXParseException(e.getMessage(), locator, e);
322 }
323 }
324
325 public void warning (SAXParseException spe)
326 throws SAXException {
327 throw spe;
328 }
329
330 public void error (SAXParseException spe)
331 throws SAXException {
332 throw spe;
333 }
334
335 public void fatalError (SAXParseException spe)
336 throws SAXException {
337 throw spe;
338 }
339 };
340 }
341 }
This page was automatically generated by Maven