001: /*
002: * Copyright 2006 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.dev.util.xml;
017:
018: import com.google.gwt.core.ext.TreeLogger;
019: import com.google.gwt.core.ext.UnableToCompleteException;
020: import com.google.gwt.util.tools.Utility;
021:
022: import org.xml.sax.Attributes;
023: import org.xml.sax.InputSource;
024: import org.xml.sax.Locator;
025: import org.xml.sax.SAXException;
026: import org.xml.sax.XMLReader;
027: import org.xml.sax.helpers.DefaultHandler;
028:
029: import java.io.IOException;
030: import java.io.Reader;
031: import java.util.Stack;
032:
033: import javax.xml.parsers.ParserConfigurationException;
034: import javax.xml.parsers.SAXParser;
035: import javax.xml.parsers.SAXParserFactory;
036:
037: /**
038: * Somewhat general-purpose SAX-style XML parser that uses reflection and calls
039: * into your "schema" classes. For example, the element
040: * <code><server-name></code> maps to the method
041: * <code>server_name</code>. Note that the mapping is one-way, hyphens become
042: * underscores, but then you don't really want to use underscores in XML tag
043: * names anyway, do you? Also, all mixed content text (that is, text inside
044: * elements) is ignored, so think attributes.
045: */
046: public final class ReflectiveParser {
047:
048: private static final class Impl extends DefaultHandler {
049:
050: private Locator locator;
051:
052: private Reader reader;
053:
054: private Stack schemaLevels = new Stack();
055:
056: private Stack argStack = new Stack();
057:
058: private Schema defaultSchema;
059:
060: public void characters(char[] ch, int start, int length)
061: throws SAXException {
062: int lineNumber = locator.getLineNumber();
063:
064: // Get the active schema level.
065: //
066: Schema schemaLevel = getTopSchemaLevel();
067:
068: if (schemaLevel == null) {
069: // It is legitimate to run out of schemaLevels if there is an empty node
070: // in the XML. Otherwise, it indicates that the user has specified
071: // extra stuff in the body of the XML tag that we don't understand.
072: //
073: for (int i = 0; i < length; i++) {
074: if (!Character.isWhitespace(ch[i + start])) {
075: throw new SAXException(
076: "Unexpected XML data found: "
077: + String.valueOf(ch, start,
078: length));
079: }
080: }
081: // This is okay. Nothing special to do.
082: //
083: return;
084: }
085: // Find the precomputed handler class info.
086: //
087: Class slc = schemaLevel.getClass();
088: HandlerClassInfo classInfo = HandlerClassInfo
089: .getClassInfo(slc);
090: assert (classInfo != null); // would've thrown if unregistered
091: HandlerMethod method = classInfo.getTextMethod();
092: if (method == null) {
093: // This is okay. Nothing special to do.
094: //
095: return;
096: }
097:
098: // Call the handler.
099: //
100: try {
101: final String text = String.valueOf(ch, start, length);
102: method.invokeText(lineNumber, text, schemaLevel);
103: } catch (UnableToCompleteException e) {
104: throw new SAXException(e);
105: }
106: }
107:
108: public void endElement(String namespaceURI, String localName,
109: String elem) throws SAXException {
110: int lineNumber = locator.getLineNumber();
111:
112: // Get the active schema level.
113: //
114: Schema schemaLevel = popLevel();
115: if (schemaLevel == null) {
116: // This was an unexpected child, but we already informed the schema
117: // about it during startElement(), so we can just return.
118: //
119: return;
120: }
121:
122: // Find the precomputed handler class info.
123: //
124: Class slc = schemaLevel.getClass();
125: HandlerClassInfo classInfo = HandlerClassInfo
126: .getClassInfo(slc);
127: assert (classInfo != null); // would've thrown if unregistered
128: HandlerMethod method = classInfo.getEndMethod(elem);
129: if (method == null) {
130: // This is okay. Nothing special to do.
131: //
132: return;
133: }
134:
135: Object[] args = getCurrentArgs();
136: if (args != null) {
137: // Call the handler using the same arguments we send to the "begin"
138: // handler.
139: //
140: try {
141: method.invokeEnd(lineNumber, elem, schemaLevel,
142: args);
143: } catch (UnableToCompleteException e) {
144: throw new SAXException(e);
145: }
146: }
147: }
148:
149: public void setDocumentLocator(Locator locator) {
150: this .locator = locator;
151: }
152:
153: public void startElement(String namespaceURI, String localName,
154: String elemName, Attributes atts) throws SAXException {
155: int lineNumber = locator.getLineNumber();
156:
157: // Get the active schema level.
158: //
159: Schema schemaLevel = getTopSchemaLevel();
160: if (schemaLevel == null) {
161: // This means that children should not appear at this level.
162: //
163: Schema nextToTop = getNextToTopSchemaLevel();
164:
165: // Push another null since this child shouldn't have children either.
166: //
167: setArgsAndPushLevel(null, null);
168:
169: // Inform the next-to-top schema level about this.
170: //
171: try {
172: nextToTop.onUnexpectedChild(lineNumber, elemName);
173: } catch (UnableToCompleteException e) {
174: throw new SAXException(e);
175: }
176:
177: return;
178: }
179:
180: // Find the precomputed handler class info.
181: //
182: Class slc = schemaLevel.getClass();
183: HandlerClassInfo classInfo = HandlerClassInfo
184: .getClassInfo(slc);
185: HandlerMethod method = classInfo.getStartMethod(elemName);
186:
187: if (method == null) {
188: // This is not okay. The schema has to at least have a stub
189: // to indicate that a particular tag is allowed.
190: //
191: try {
192: schemaLevel.onUnexpectedElement(lineNumber,
193: elemName);
194: } catch (UnableToCompleteException e) {
195: throw new SAXException(e);
196: }
197:
198: // Since we don't know about this element, assume it should not have
199: // children either.
200: //
201: setArgsAndPushLevel(null, null);
202:
203: return;
204: }
205:
206: HandlerArgs args = method.createArgs(schemaLevel,
207: lineNumber, elemName);
208:
209: // For each attribute found, try to match it up to a parameter.
210: //
211: for (int i = 0, n = atts.getLength(); i < n; ++i) {
212: String attrName = atts.getQName(i);
213: String attrValue = atts.getValue(i);
214:
215: if (!args.setArg(attrName, attrValue)) {
216: // Inform the handler that the attribute was unknown.
217: //
218: try {
219: schemaLevel.onUnexpectedAttribute(lineNumber,
220: elemName, attrName, attrValue);
221: } catch (UnableToCompleteException e) {
222: throw new SAXException(e);
223: }
224: }
225: }
226:
227: // Check for unset parameters.
228: //
229: int missingCount = 0;
230: for (int i = 0, n = args.getArgCount(); i < n; ++i) {
231: if (!args.isArgSet(i)) {
232: // Inform the handler that the required attribute was not set.
233: // It might throw, but it also might not.
234: //
235: try {
236: schemaLevel.onMissingAttribute(lineNumber,
237: elemName, args.getArgName(i));
238: } catch (UnableToCompleteException e) {
239: throw new SAXException(e);
240: }
241:
242: ++missingCount;
243: }
244: }
245:
246: if (missingCount > 0) {
247: // Do not invoke the handler.
248: //
249:
250: // Assume that children shouldn't be recognized either since the
251: // handler wasn't invoked.
252: //
253: setArgsAndPushLevel(null, null);
254:
255: return;
256: }
257:
258: // Invoke the handler method, which will internally
259: // convert all the args to their respective parameter types
260: // (or warn if there is a problem doing so).
261: //
262: Object[] invokeArgs = new Object[method.getParamCount()];
263: Schema childSchemaLevel;
264: try {
265: childSchemaLevel = method.invokeBegin(lineNumber,
266: elemName, schemaLevel, args, invokeArgs);
267: } catch (UnableToCompleteException e) {
268: throw new SAXException(e);
269: }
270:
271: // childSchemaLevel can be null and that's okay -- it means no children
272: // are expected. Same for invokeArgs[0] -- it means that the "begin"
273: // handler was not called, so neither will we call the "end" handler.
274: //
275: setArgsAndPushLevel(invokeArgs, childSchemaLevel);
276: }
277:
278: private Object[] getCurrentArgs() {
279: return (Object[]) argStack.peek();
280: }
281:
282: private Schema getNextToTopSchemaLevel() {
283: return (Schema) schemaLevels.get(schemaLevels.size() - 2);
284: }
285:
286: private Schema getTopSchemaLevel() {
287: return (Schema) schemaLevels.peek();
288: }
289:
290: private void parse(TreeLogger logger, Schema topSchema,
291: Reader reader) throws UnableToCompleteException {
292: // Set up the parentmost schema which is used to find default converters
293: // and handlers (but isn't actually on the schema stack.)
294: //
295: defaultSchema = new DefaultSchema(logger);
296:
297: // Tell this schema level about the default schema, which is initialized
298: // with
299: // converters for basic types.
300: //
301: topSchema.setParent(defaultSchema);
302:
303: // Make a slot for the document element's args.
304: //
305: argStack.push(null);
306:
307: // Push the first schema.
308: //
309: setArgsAndPushLevel(null, topSchema);
310:
311: Throwable caught = null;
312: try {
313: this .reader = reader;
314: SAXParser parser = SAXParserFactory.newInstance()
315: .newSAXParser();
316: InputSource inputSource = new InputSource(this .reader);
317: XMLReader xmlReader = parser.getXMLReader();
318: xmlReader.setContentHandler(this );
319: xmlReader.parse(inputSource);
320: } catch (SAXException e) {
321: // If it's an exception wrapped in a SAXException, rip off the outer SAX
322: // exception.
323: //
324: caught = e;
325:
326: Exception inner = e.getException();
327: if (inner instanceof RuntimeException) {
328: throw (RuntimeException) inner;
329: } else if (inner != null) {
330: caught = inner;
331: }
332:
333: } catch (ParserConfigurationException e) {
334: caught = e;
335: } catch (IOException e) {
336: caught = e;
337: } finally {
338: Utility.close(reader);
339: }
340:
341: if (caught != null) {
342: Messages.XML_PARSE_FAILED.log(logger, caught);
343: throw new UnableToCompleteException();
344: }
345: }
346:
347: private Schema popLevel() {
348: argStack.pop();
349: schemaLevels.pop();
350: return getTopSchemaLevel();
351: }
352:
353: private void setArgsAndPushLevel(Object[] handlerArgs,
354: Schema schemaLevel) {
355: // Set the args on the current schema level.
356: argStack.set(argStack.size() - 1, handlerArgs);
357: // A slot for the args at the childrens' depth.
358: argStack.push(null);
359: if (!schemaLevels.isEmpty()) {
360: // Tell this schema level about its parent.
361: //
362: Schema maybeParent = null;
363: for (int i = schemaLevels.size() - 1; i >= 0; --i) {
364: maybeParent = (Schema) schemaLevels.get(i);
365: if (maybeParent != null) {
366: break;
367: }
368: }
369: if (maybeParent == null) {
370: throw new IllegalStateException(
371: "Cannot find any parent schema");
372: }
373: if (schemaLevel != null) {
374: schemaLevel.setParent(maybeParent);
375: }
376: }
377: // The schema for children.
378: schemaLevels.push(schemaLevel);
379: }
380: }
381:
382: public static void parse(TreeLogger logger, Schema schema,
383: Reader reader) throws UnableToCompleteException {
384:
385: // Register the schema level.
386: //
387: registerSchemaLevel(schema.getClass());
388:
389: // Do the parse.
390: //
391: Impl impl = new Impl();
392: impl.parse(logger, schema, reader);
393: }
394:
395: /**
396: * Can safely register the same class recursively.
397: */
398: public static void registerSchemaLevel(Class schemaLevelClass) {
399: HandlerClassInfo.registerClass(schemaLevelClass);
400:
401: // Try to register nested classes.
402: //
403: Class[] nested = schemaLevelClass.getDeclaredClasses();
404: for (int i = 0, n = nested.length; i < n; ++i) {
405: Class nestedClass = nested[i];
406: if (Schema.class.isAssignableFrom(nestedClass)) {
407: registerSchemaLevel(nestedClass);
408: }
409: }
410: }
411: }
|