/** * Copyright 2010 JBoss 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 org.drools.xml; /* * Copyright 2005 JBoss 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. */ import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.net.URL; import java.text.MessageFormat; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.ListIterator; import java.util.Map; import java.util.Set; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.FactoryConfigurationError; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.w3c.dom.Document; import org.w3c.dom.DocumentFragment; import org.w3c.dom.Element; import org.xml.sax.Attributes; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.SAXNotRecognizedException; import org.xml.sax.SAXParseException; import org.xml.sax.helpers.DefaultHandler; /** * <code>RuleSet</code> loader. * * Note you can override the default entity resolver by setting the System property of: * <code>org.drools.io.EntityResolve</code> to your own custom entity resolver. * This can be done using -Dorg.drools.io.EntityResolver=YourClassHere on the command line, for instance. * * @author <a href="mailto:bob@werken.com">bob mcwhirter </a> */ public class ExtensibleXmlParser extends DefaultHandler { // ---------------------------------------------------------------------- // Constants // ---------------------------------------------------------------------- public static final String ENTITY_RESOLVER_PROPERTY_NAME = "org.drools.io.EntityResolver"; /** Namespace URI for the general tags. */ public static final String RULES_NAMESPACE_URI = "http://drools.org/rules"; private static final String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage"; private static final String W3C_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema"; // ---------------------------------------------------------------------- // Instance members // ---------------------------------------------------------------------- /** SAX parser. */ private SAXParser parser; /** isValidating */ private boolean isValidating = true; /** Locator for errors. */ private Locator locator; // private Map repo; /** Stack of configurations. */ private LinkedList configurationStack; /** Current configuration text. */ private StringBuilder characters; private SemanticModules modules; private boolean lastWasEndElement; private LinkedList parents; private Object peer; private Object current; private Object data; private final MessageFormat message = new MessageFormat( "({0}: {1}, {2}): {3}" ); private final Map namespaces = new HashMap(); private EntityResolver entityResolver; private Document document; private DocumentFragment docFragment; private ClassLoader classLoader; private Map metaData = new HashMap(); // ---------------------------------------------------------------------- // Constructors // ---------------------------------------------------------------------- /** * Construct. * * <p> * Uses the default JAXP SAX parser and the default classpath-based * <code>DefaultSemanticModule</code>. * </p> */ public ExtensibleXmlParser() { // init this.configurationStack = new LinkedList(); this.parents = new LinkedList(); initEntityResolver(); } public void setSemanticModules(SemanticModules modules) { this.modules = modules; } /** * Construct. * * <p> * Uses the default classpath-based <code>DefaultSemanticModule</code>. * </p> * * @param parser * The SAX parser. */ public ExtensibleXmlParser(final SAXParser parser) { this(); this.parser = parser; } // ---------------------------------------------------------------------- // Instance methods // ---------------------------------------------------------------------- /** * Read a <code>RuleSet</code> from a <code>Reader</code>. * * @param reader * The reader containing the rule-set. * * @return The rule-set. * @throws ParserConfigurationException */ public Object read(final Reader reader) throws SAXException, IOException { return read( new InputSource( reader ) ); } /** * Read a <code>RuleSet</code> from an <code>InputStream</code>. * * @param inputStream * The input-stream containing the rule-set. * * @return The rule-set. * @throws ParserConfigurationException */ public Object read(final InputStream inputStream) throws SAXException, IOException { return read( new InputSource( inputStream ) ); } /** * Read a <code>RuleSet</code> from an <code>InputSource</code>. * * @param in * The rule-set input-source. * * @return The rule-set. * @throws ParserConfigurationException */ public Object read(final InputSource in) throws SAXException, IOException { if ( this.docFragment == null ) { DocumentBuilderFactory f; try { f = DocumentBuilderFactory.newInstance(); } catch ( FactoryConfigurationError e ) { // obscure JDK1.5 bug where FactoryFinder in the JRE returns a null ClassLoader, so fall back to hard coded xerces. // https://stg.network.org/bugzilla/show_bug.cgi?id=47169 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4633368 try { f = (DocumentBuilderFactory) Class.forName( "org.apache.xerces.jaxp.DocumentBuilderFactoryImpl" ).newInstance(); } catch ( Exception e1 ) { throw new RuntimeException( "Unable to create new DOM Document", e1 ); } } catch ( Exception e ) { throw new RuntimeException( "Unable to create new DOM Document", e ); } try { this.document = f.newDocumentBuilder().newDocument(); } catch ( Exception e ) { throw new RuntimeException( "Unable to create new DOM Document", e ); } this.docFragment = this.document.createDocumentFragment(); } SAXParser localParser = null; if ( this.parser == null ) { SAXParserFactory factory = null; try { factory = SAXParserFactory.newInstance(); } catch ( FactoryConfigurationError e) { // obscure JDK1.5 bug where FactoryFinder in the JRE returns a null ClassLoader, so fall back to hard coded xerces. // https://stg.network.org/bugzilla/show_bug.cgi?id=47169 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4633368 try { factory = (SAXParserFactory) Class.forName( "org.apache.xerces.jaxp.SAXParserFactoryImpl" ).newInstance(); } catch ( Exception e1 ) { throw new RuntimeException( "Unable to create new DOM Document", e1 ); } } catch ( Exception e ) { throw new RuntimeException( "Unable to create new DOM Document", e ); } factory.setNamespaceAware( true ); final String isValidatingString = System.getProperty( "drools.schema.validating" ); if ( System.getProperty( "drools.schema.validating" ) != null ) { this.isValidating = Boolean.getBoolean( "drools.schema.validating" ); } if ( this.isValidating == true ) { factory.setValidating( true ); try { localParser = factory.newSAXParser(); } catch ( final ParserConfigurationException e ) { throw new RuntimeException( e.getMessage() ); } try { localParser.setProperty( ExtensibleXmlParser.JAXP_SCHEMA_LANGUAGE, ExtensibleXmlParser.W3C_XML_SCHEMA ); } catch ( final SAXNotRecognizedException e ) { boolean hideWarnings = Boolean.getBoolean( "drools.schema.hidewarnings" ); if ( !hideWarnings ) { System.err.println( "Your SAX parser is not JAXP 1.2 compliant - turning off validation." ); } localParser = null; } } if ( localParser == null ) { // not jaxp1.2 compliant so turn off validation try { this.isValidating = false; factory.setValidating( this.isValidating ); localParser = factory.newSAXParser(); } catch ( final ParserConfigurationException e ) { throw new RuntimeException( e.getMessage() ); } } } else { localParser = this.parser; } if ( !localParser.isNamespaceAware() ) { throw new RuntimeException( "parser must be namespace-aware" ); } localParser.parse( in, this ); return this.data; } public void setData(final Object data) { this.data = data; } public Object getData() { return this.data; } public ClassLoader getClassLoader() { return classLoader; } public void setClassLoader(ClassLoader classLoader) { this.classLoader = classLoader; } public Map getMetaData() { return this.metaData; } /** * @see org.xml.sax.ContentHandler */ public void setDocumentLocator(final Locator locator) { this.locator = locator; } /** * Get the <code>Locator</code>. * * @return The locator. */ public Locator getLocator() { return this.locator; } public void startDocument() { this.isValidating = true; this.current = null; this.peer = null; this.lastWasEndElement = false; this.parents.clear(); this.characters = null; this.configurationStack.clear(); this.namespaces.clear(); } private int direction = 0; /** * @param uri * @param localName * @param qname * @param attrs * @throws SAXException * @see org.xml.sax.ContentHandler * * @todo: better way to manage unhandled elements */ public void startElement(final String uri, final String localName, final String qname, final Attributes attrs) throws SAXException { if ( direction == 1 ) { // going down again, so clear this.peer = null; } else { direction = 1; } final Handler handler = getHandler( uri, localName ); if ( handler == null ) { startElementBuilder( localName, attrs ); return; } validate( uri, localName, handler ); final Object node = handler.start( uri, localName, attrs, this ); if ( node == null ) { this.parents.add( Null.instance ); } else { this.parents.add( node ); } } /** * @param uri * @param localName * @param qname * @throws SAXException * @see org.xml.sax.ContentHandler */ public void endElement(final String uri, final String localName, final String qname) throws SAXException { direction = -1; final Handler handler = getHandler( uri, localName ); if ( handler == null ) { if ( this.configurationStack.size() >= 1 ) { endElementBuilder(); } return; } this.current = removeParent(); this.peer = handler.end( uri, localName, this ); } public static class Null { public static final Null instance = new Null(); } private void validate(final String uri, final String localName, final Handler handler) throws SAXParseException { boolean validParent = false; boolean validPeer = false; boolean invalidNesting = false; final Set validParents = handler.getValidParents(); final Set validPeers = handler.getValidPeers(); boolean allowNesting = handler.allowNesting(); // get parent Object parent; if ( this.parents.size() != 0 ) { parent = this.parents.getLast(); } else { parent = null; } // check valid parents // null parent means localname is rule-set // dont process if elements are the same // instead check for allowed nesting final Class nodeClass = getHandler( uri, localName ).generateNodeFor(); if ( nodeClass != null && !nodeClass.isInstance( parent ) ) { Object allowedParent; final Iterator it = validParents.iterator(); while ( !validParent && it.hasNext() ) { allowedParent = it.next(); if ( parent == null && allowedParent == null ) { validParent = true; } else if ( allowedParent != null && ((Class) allowedParent).isInstance( parent ) ) { validParent = true; } } if ( !validParent ) { throw new SAXParseException( "<" + localName + "> has an invalid parent element [" + parent + "]", getLocator() ); } } // check valid peers // null peer means localname is rule-set final Object peer = this.peer; Object allowedPeer; Iterator it = validPeers.iterator(); while ( !validPeer && it.hasNext() ) { allowedPeer = it.next(); if ( peer == null && allowedPeer == null ) { validPeer = true; } else if ( allowedPeer != null && ((Class) allowedPeer).isInstance( peer ) ) { validPeer = true; } } if ( !validPeer ) { throw new SAXParseException( "<" + localName + "> is after an invalid element: " + Handler.class.getName(), getLocator() ); } if ( nodeClass != null && !allowNesting ) { it = this.parents.iterator(); while ( !invalidNesting && it.hasNext() ) { if ( nodeClass.isInstance( it.next() ) ) { invalidNesting = true; } } } if ( invalidNesting ) { throw new SAXParseException( "<" + localName + "> may not be nested", getLocator() ); } } /** * Start a configuration node. * * @param name * Tag name. * @param attrs * Tag attributes. */ public void startElementBuilder(final String tagName, final Attributes attrs) { this.characters = new StringBuilder(); final Element element = this.document.createElement( tagName ); //final DefaultConfiguration config = new DefaultConfiguration( tagName ); final int numAttrs = attrs.getLength(); for ( int i = 0; i < numAttrs; ++i ) { element.setAttribute( attrs.getLocalName( i ), attrs.getValue( i ) ); } // // lets add the namespaces as attributes // for ( final Iterator iter = this.namespaces.entrySet().iterator(); iter.hasNext(); ) { // final Map.Entry entry = (Map.Entry) iter.next(); // String ns = (String) entry.getKey(); // final String value = (String) entry.getValue(); // if ( ns == null || ns.length() == 0 ) { // ns = "xmlns"; // } else { // ns = "xmlns:" + ns; // } // config.setAttribute( ns, // value ); // } if ( this.configurationStack.isEmpty() ) { this.configurationStack.addLast( element ); } else { ((Element) this.configurationStack.getLast()).appendChild( element ); this.configurationStack.addLast( element ); } } Handler getHandler(final String uri, final String localName) { SemanticModule module = this.modules.getSemanticModule( uri ); if ( module != null ) { return module.getHandler( localName ); } else { return null; } } /** * @param chars * @param start * @param len * @see org.xml.sax.ContentHandler */ public void characters(final char[] chars, final int start, final int len) { if ( this.characters != null ) { this.characters.append( chars, start, len ); } } /** * End a configuration node. * * @return The configuration. */ public Element endElementBuilder() { final Element element = (Element) this.configurationStack.removeLast(); if ( this.characters != null ) { element.appendChild( this.document.createTextNode( this.characters.toString() ) ); } this.characters = null; return element; } public Object getParent() { return this.parents.getLast(); } public Object getParent(int index) { ListIterator it = this.parents.listIterator( this.parents.size() ); int x = 0; Object parent = null; while ( x++ <= index ) { parent = it.previous(); } return parent; } public Object removeParent() { Object parent = this.parents.removeLast(); while ( parent == null && !this.parents.isEmpty() ) { parent = this.parents.removeLast(); } return parent; } public LinkedList getParents() { return this.parents; } public Object getParent(final Class parent) { final ListIterator it = this.parents.listIterator( this.parents.size() ); Object node = null; while ( it.hasPrevious() ) { node = it.previous(); if ( parent.isInstance( node ) ) { break; } } return node; } public Object getPeer() { return this.peer; } public Object getCurrent() { return this.current; } public InputSource resolveEntity(final String publicId, final String systemId) throws SAXException { try { final InputSource inputSource = resolveSchema( publicId, systemId ); if ( inputSource != null ) { return inputSource; } if ( this.entityResolver != null ) { return this.entityResolver.resolveEntity( publicId, systemId ); } } catch ( final IOException ioe ) { } return null; } public void startPrefixMapping(final String prefix, final String uri) throws SAXException { super.startPrefixMapping( prefix, uri ); this.namespaces.put( prefix, uri ); } public void endPrefixMapping(final String prefix) throws SAXException { super.endPrefixMapping( prefix ); this.namespaces.remove( prefix ); } private void print(final SAXParseException x) { final String msg = this.message.format( new Object[]{x.getSystemId(), new Integer( x.getLineNumber() ), new Integer( x.getColumnNumber() ), x.getMessage()} ); System.out.println( msg ); } public void warning(final SAXParseException x) { print( x ); } public void error(final SAXParseException x) { print( x ); } public void fatalError(final SAXParseException x) throws SAXParseException { print( x ); throw x; } private InputSource resolveSchema(final String publicId, final String systemId) throws SAXException, IOException { // Schema files must end with xsd if ( !systemId.toLowerCase().endsWith( "xsd" ) ) { return null; } // try the actual location given by systemId try { final URL url = new URL( systemId ); return new InputSource( url.openStream() ); } catch ( final Exception e ) { } // Try and get the index for the filename, else return null String xsd; int index = systemId.lastIndexOf( "/" ); if ( index == -1 ) { index = systemId.lastIndexOf( "\\" ); } if ( index != -1 ) { xsd = systemId.substring( index + 1 ); } else { xsd = systemId; } ClassLoader cl = Thread.currentThread().getContextClassLoader(); if ( cl == null ) { cl = ExtensibleXmlParser.class.getClassLoader(); } // Try looking in META-INF { final InputStream is = cl.getResourceAsStream( "META-INF/" + xsd ); if ( is != null ) { return new InputSource( is ); } } // Try looking in /META-INF { final InputStream is = cl.getResourceAsStream( "/META-INF/" + xsd ); if ( is != null ) { return new InputSource( is ); } } // Try looking at root of classpath { final InputStream is = cl.getResourceAsStream( "/" + xsd ); if ( is != null ) { return new InputSource( is ); } } // Try current working directory { final File file = new File( xsd ); if ( file.exists() ) { return new InputSource( new BufferedInputStream( new FileInputStream( file ) ) ); } } cl = ClassLoader.getSystemClassLoader(); // Try looking in META-INF { final InputStream is = cl.getResourceAsStream( "META-INF/" + xsd ); if ( is != null ) { return new InputSource( is ); } } // Try looking in /META-INF { final InputStream is = cl.getResourceAsStream( "/META-INF/" + xsd ); if ( is != null ) { return new InputSource( is ); } } // Try looking at root of classpath { final InputStream is = cl.getResourceAsStream( "/" + xsd ); if ( is != null ) { return new InputSource( is ); } } return null; } /** * Intializes EntityResolver that is configured via system property ENTITY_RESOLVER_PROPERTY_NAME. */ private void initEntityResolver() { final String entityResolveClazzName = System.getProperty( ExtensibleXmlParser.ENTITY_RESOLVER_PROPERTY_NAME ); if ( entityResolveClazzName != null && entityResolveClazzName.length() > 0 ) { try { final Class entityResolverClazz = Thread.currentThread().getContextClassLoader().loadClass( entityResolveClazzName ); this.entityResolver = (EntityResolver) entityResolverClazz.newInstance(); } catch ( final Exception ignoreIt ) { } } } }