| /* |
| * Copyright 2006 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy of |
| * the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package com.google.gwt.dev.util.xml; |
| |
| import com.google.gwt.core.ext.TreeLogger; |
| import com.google.gwt.core.ext.UnableToCompleteException; |
| import com.google.gwt.util.tools.Utility; |
| |
| import org.xml.sax.Attributes; |
| import org.xml.sax.InputSource; |
| import org.xml.sax.Locator; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.XMLReader; |
| import org.xml.sax.helpers.DefaultHandler; |
| |
| import java.io.IOException; |
| import java.io.Reader; |
| import java.util.Stack; |
| |
| import javax.xml.parsers.ParserConfigurationException; |
| import javax.xml.parsers.SAXParser; |
| import javax.xml.parsers.SAXParserFactory; |
| |
| /** |
| * Somewhat general-purpose SAX-style XML parser that uses reflection and calls |
| * into your "schema" classes. For example, the element |
| * <code><server-name></code> maps to the method |
| * <code>server_name</code>. Note that the mapping is one-way, hyphens become |
| * underscores, but then you don't really want to use underscores in XML tag |
| * names anyway, do you? Also, all mixed content text (that is, text inside |
| * elements) is ignored, so think attributes. |
| */ |
| public final class ReflectiveParser { |
| |
| private static SAXParserFactory saxParserFactory; |
| |
| private static synchronized SAXParser createNewSaxParser() throws ParserConfigurationException, |
| SAXException { |
| if (saxParserFactory == null) { |
| Thread currentThread = Thread.currentThread(); |
| ClassLoader oldClassLoader = currentThread.getContextClassLoader(); |
| try { |
| // use system ClassLoader to avoid using expensive GWT ClassLoader |
| currentThread.setContextClassLoader(ClassLoader.getSystemClassLoader()); |
| saxParserFactory = SAXParserFactory.newInstance(); |
| saxParserFactory.setFeature( |
| "http://apache.org/xml/features/nonvalidating/load-external-dtd", false); |
| } finally { |
| currentThread.setContextClassLoader(oldClassLoader); |
| } |
| } |
| return saxParserFactory.newSAXParser(); |
| } |
| |
| private static final class Impl extends DefaultHandler { |
| |
| private Locator locator; |
| |
| private Reader reader; |
| |
| private Stack<Schema> schemaLevels = new Stack<Schema>(); |
| |
| private Stack<Object[]> argStack = new Stack<Object[]>(); |
| |
| private Schema defaultSchema; |
| |
| @Override |
| public void characters(char[] ch, int start, int length) |
| throws SAXException { |
| int lineNumber = locator.getLineNumber(); |
| |
| // Get the active schema level. |
| // |
| Schema schemaLevel = getTopSchemaLevel(); |
| |
| if (schemaLevel == null) { |
| // It is legitimate to run out of schemaLevels if there is an empty node |
| // in the XML. Otherwise, it indicates that the user has specified |
| // extra stuff in the body of the XML tag that we don't understand. |
| // |
| for (int i = 0; i < length; i++) { |
| if (!Character.isWhitespace(ch[i + start])) { |
| throw new SAXException("Unexpected XML data found: " |
| + String.valueOf(ch, start, length)); |
| } |
| } |
| // This is okay. Nothing special to do. |
| // |
| return; |
| } |
| // Find the precomputed handler class info. |
| // |
| Class<? extends Schema> slc = schemaLevel.getClass(); |
| HandlerClassInfo classInfo = HandlerClassInfo.getClassInfo(slc); |
| assert (classInfo != null); // would've thrown if unregistered |
| HandlerMethod method = classInfo.getTextMethod(); |
| if (method == null) { |
| // This is okay. Nothing special to do. |
| // |
| return; |
| } |
| |
| // Call the handler. |
| // |
| try { |
| // The line number is at the end of the text; subtract however many |
| // newlines the text contains. |
| for (int i = start, e = start + length, l = e - 1; i < e; ++i) { |
| switch (ch[i]) { |
| case '\r': |
| if (i < l && ch[i + 1] == '\n') { |
| continue; |
| } |
| // Intentional fall-through |
| case '\n': |
| --lineNumber; |
| } |
| } |
| final String text = String.valueOf(ch, start, length); |
| method.invokeText(lineNumber, text, schemaLevel); |
| } catch (UnableToCompleteException e) { |
| throw new SAXException(e); |
| } |
| } |
| |
| @Override |
| public void endElement(String namespaceURI, String localName, String elem) |
| throws SAXException { |
| int lineNumber = locator.getLineNumber(); |
| |
| // Get the active schema level. |
| // |
| Schema schemaLevel = popLevel(); |
| if (schemaLevel == null) { |
| // This was an unexpected child, but we already informed the schema |
| // about it during startElement(), so we can just return. |
| // |
| return; |
| } |
| |
| // Find the precomputed handler class info. |
| // |
| Class<? extends Schema> slc = schemaLevel.getClass(); |
| HandlerClassInfo classInfo = HandlerClassInfo.getClassInfo(slc); |
| assert (classInfo != null); // would've thrown if unregistered |
| HandlerMethod method = classInfo.getEndMethod(elem); |
| if (method == null) { |
| // This is okay. Nothing special to do. |
| // |
| return; |
| } |
| |
| Object[] args = getCurrentArgs(); |
| if (args != null) { |
| // Call the handler using the same arguments we send to the "begin" |
| // handler. |
| // |
| try { |
| method.invokeEnd(lineNumber, elem, schemaLevel, args); |
| } catch (UnableToCompleteException e) { |
| throw new SAXException(e); |
| } |
| } |
| } |
| |
| @Override |
| public void setDocumentLocator(Locator locator) { |
| this.locator = locator; |
| } |
| |
| @Override |
| public void startElement(String namespaceURI, String localName, |
| String elemName, Attributes atts) throws SAXException { |
| int lineNumber = locator.getLineNumber(); |
| |
| // Get the active schema level. |
| // |
| Schema schemaLevel = getTopSchemaLevel(); |
| if (schemaLevel == null) { |
| // This means that children should not appear at this level. |
| // |
| Schema nextToTop = getNextToTopSchemaLevel(); |
| |
| // Push another null since this child shouldn't have children either. |
| // |
| setArgsAndPushLevel(null, null); |
| |
| // Inform the next-to-top schema level about this. |
| // |
| try { |
| nextToTop.onUnexpectedChild(lineNumber, elemName); |
| } catch (UnableToCompleteException e) { |
| throw new SAXException(e); |
| } |
| |
| return; |
| } |
| |
| // Find the precomputed handler class info. |
| // |
| Class<? extends Schema> slc = schemaLevel.getClass(); |
| HandlerClassInfo classInfo = HandlerClassInfo.getClassInfo(slc); |
| HandlerMethod method = classInfo.getStartMethod(elemName); |
| |
| if (method == null) { |
| // This is not okay. The schema has to at least have a stub |
| // to indicate that a particular tag is allowed. |
| // |
| try { |
| schemaLevel.onUnexpectedElement(lineNumber, elemName); |
| } catch (UnableToCompleteException e) { |
| throw new SAXException(e); |
| } |
| |
| // Since we don't know about this element, assume it should not have |
| // children either. |
| // |
| setArgsAndPushLevel(null, null); |
| |
| return; |
| } |
| |
| HandlerArgs args = method.createArgs(schemaLevel, lineNumber, elemName); |
| |
| // For each attribute found, try to match it up to a parameter. |
| // |
| for (int i = 0, n = atts.getLength(); i < n; ++i) { |
| String attrName = atts.getQName(i); |
| String attrValue = atts.getValue(i); |
| |
| if (!args.setArg(attrName, attrValue)) { |
| // Inform the handler that the attribute was unknown. |
| // |
| try { |
| schemaLevel.onUnexpectedAttribute(lineNumber, elemName, attrName, |
| attrValue); |
| } catch (UnableToCompleteException e) { |
| throw new SAXException(e); |
| } |
| } |
| } |
| |
| // Check for unset parameters. |
| // |
| int missingCount = 0; |
| for (int i = 0, n = args.getArgCount(); i < n; ++i) { |
| if (!args.isArgSet(i)) { |
| // Inform the handler that the required attribute was not set. |
| // It might throw, but it also might not. |
| // |
| try { |
| schemaLevel.onMissingAttribute(lineNumber, elemName, |
| args.getArgName(i)); |
| } catch (UnableToCompleteException e) { |
| throw new SAXException(e); |
| } |
| |
| ++missingCount; |
| } |
| } |
| |
| if (missingCount > 0) { |
| // Do not invoke the handler. |
| // |
| |
| // Assume that children shouldn't be recognized either since the |
| // handler wasn't invoked. |
| // |
| setArgsAndPushLevel(null, null); |
| |
| return; |
| } |
| |
| // Invoke the handler method, which will internally |
| // convert all the args to their respective parameter types |
| // (or warn if there is a problem doing so). |
| // |
| Object[] invokeArgs = new Object[method.getParamCount()]; |
| Schema childSchemaLevel; |
| try { |
| childSchemaLevel = method.invokeBegin(lineNumber, elemName, |
| schemaLevel, args, invokeArgs); |
| } catch (UnableToCompleteException e) { |
| throw new SAXException(e); |
| } |
| |
| // childSchemaLevel can be null and that's okay -- it means no children |
| // are expected. Same for invokeArgs[0] -- it means that the "begin" |
| // handler was not called, so neither will we call the "end" handler. |
| // |
| setArgsAndPushLevel(invokeArgs, childSchemaLevel); |
| } |
| |
| private Object[] getCurrentArgs() { |
| return argStack.peek(); |
| } |
| |
| private Schema getNextToTopSchemaLevel() { |
| return schemaLevels.get(schemaLevels.size() - 2); |
| } |
| |
| private Schema getTopSchemaLevel() { |
| return schemaLevels.peek(); |
| } |
| |
| private void parse(TreeLogger logger, Schema topSchema, Reader reader) |
| throws UnableToCompleteException { |
| // Set up the parentmost schema which is used to find default converters |
| // and handlers (but isn't actually on the schema stack.) |
| // |
| defaultSchema = new DefaultSchema(logger); |
| |
| // Tell this schema level about the default schema, which is initialized |
| // with |
| // converters for basic types. |
| // |
| topSchema.setParent(defaultSchema); |
| |
| // Make a slot for the document element's args. |
| // |
| argStack.push(null); |
| |
| // Push the first schema. |
| // |
| setArgsAndPushLevel(null, topSchema); |
| |
| Throwable caught = null; |
| try { |
| this.reader = reader; |
| SAXParser parser = createNewSaxParser(); |
| InputSource inputSource = new InputSource(this.reader); |
| XMLReader xmlReader = parser.getXMLReader(); |
| xmlReader.setContentHandler(this); |
| xmlReader.parse(inputSource); |
| } catch (SAXException e) { |
| // If it's an exception wrapped in a SAXException, rip off the outer SAX |
| // exception. |
| // |
| caught = e; |
| |
| Exception inner = e.getException(); |
| if (inner instanceof RuntimeException) { |
| throw (RuntimeException) inner; |
| } else if (inner != null) { |
| caught = inner; |
| } |
| |
| } catch (ParserConfigurationException e) { |
| caught = e; |
| } catch (IOException e) { |
| caught = e; |
| } finally { |
| Utility.close(reader); |
| } |
| |
| if (caught != null) { |
| Messages.XML_PARSE_FAILED.log(logger, caught); |
| throw new UnableToCompleteException(); |
| } |
| } |
| |
| private Schema popLevel() { |
| argStack.pop(); |
| schemaLevels.pop(); |
| return getTopSchemaLevel(); |
| } |
| |
| private void setArgsAndPushLevel(Object[] handlerArgs, Schema schemaLevel) { |
| // Set the args on the current schema level. |
| argStack.set(argStack.size() - 1, handlerArgs); |
| // A slot for the args at the childrens' depth. |
| argStack.push(null); |
| if (!schemaLevels.isEmpty()) { |
| // Tell this schema level about its parent. |
| // |
| Schema maybeParent = null; |
| for (int i = schemaLevels.size() - 1; i >= 0; --i) { |
| maybeParent = schemaLevels.get(i); |
| if (maybeParent != null) { |
| break; |
| } |
| } |
| if (maybeParent == null) { |
| throw new IllegalStateException("Cannot find any parent schema"); |
| } |
| if (schemaLevel != null) { |
| schemaLevel.setParent(maybeParent); |
| } |
| } |
| // The schema for children. |
| schemaLevels.push(schemaLevel); |
| } |
| } |
| |
| public static void parse(TreeLogger logger, Schema schema, Reader reader) |
| throws UnableToCompleteException { |
| |
| // Register the schema level. |
| // |
| registerSchemaLevel(schema.getClass()); |
| |
| // Do the parse. |
| // |
| Impl impl = new Impl(); |
| impl.parse(logger, schema, reader); |
| } |
| |
| /** |
| * Can safely register the same class recursively. |
| */ |
| public static void registerSchemaLevel(Class<? extends Schema> schemaLevelClass) { |
| HandlerClassInfo.registerClass(schemaLevelClass); |
| |
| // Try to register nested classes. |
| // |
| Class<?>[] nested = schemaLevelClass.getDeclaredClasses(); |
| for (int i = 0, n = nested.length; i < n; ++i) { |
| Class<?> nestedClass = nested[i]; |
| if (Schema.class.isAssignableFrom(nestedClass)) { |
| registerSchemaLevel(nestedClass.asSubclass(Schema.class)); |
| } |
| } |
| } |
| } |