/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2001 - 2013 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved. */ package org.pentaho.reporting.libraries.xmlns.parser; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.reporting.libraries.base.config.Configuration; import org.pentaho.reporting.libraries.base.config.DefaultConfiguration; import org.pentaho.reporting.libraries.base.util.ObjectUtilities; import org.pentaho.reporting.libraries.resourceloader.CompoundResource; import org.pentaho.reporting.libraries.resourceloader.FactoryParameterKey; import org.pentaho.reporting.libraries.resourceloader.Resource; import org.pentaho.reporting.libraries.resourceloader.ResourceCreationException; import org.pentaho.reporting.libraries.resourceloader.ResourceData; import org.pentaho.reporting.libraries.resourceloader.ResourceFactory; import org.pentaho.reporting.libraries.resourceloader.ResourceKey; import org.pentaho.reporting.libraries.resourceloader.ResourceKeyCreationException; import org.pentaho.reporting.libraries.resourceloader.ResourceLoadingException; import org.pentaho.reporting.libraries.resourceloader.ResourceManager; import org.pentaho.reporting.libraries.resourceloader.loader.raw.RawResourceData; import org.xml.sax.ErrorHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Iterator; import java.util.Map; /** * A base-class for resource-factories that load their resources from XML files. This class provides a multiplexing * option. For this, the parser looks at the root-element of the document to be parsed and selects the most suitable * XmlFactoryModule implementation registered. * * @author Thomas Morgner * @noinspection HardCodedStringLiteral */ public abstract class AbstractXmlResourceFactory implements ResourceFactory { private static final Log logger = LogFactory.getLog( AbstractXmlResourceFactory.class ); /** * A key for the content base. */ public static final String CONTENTBASE_KEY = "content-base"; private static final byte[] EMPTY_DATA = new byte[ 0 ]; private ArrayList<XmlFactoryModule> modules; private ArrayList<XmlFactoryModule> modulesFromConfiguration; private SAXParserFactory factory; /** * Default-Constructor. */ protected AbstractXmlResourceFactory() { modules = new ArrayList<XmlFactoryModule>(); modulesFromConfiguration = new ArrayList<XmlFactoryModule>(); } /** * Returns a SAX parser. * * @return a SAXParser. * @throws ParserConfigurationException if there is a problem configuring the parser. * @throws SAXException if there is a problem with the parser initialisation */ protected SAXParser getParser() throws ParserConfigurationException, SAXException { if ( this.factory == null ) { this.factory = SAXParserFactory.newInstance(); } return this.factory.newSAXParser(); } /** * Configures the xml reader. Use this to set features or properties before the documents get parsed. * * @param handler the parser implementation that will handle the SAX-Callbacks. * @param reader the xml reader that should be configured. */ protected void configureReader( final XMLReader reader, final RootXmlReadHandler handler ) { try { reader.setProperty( "http://xml.org/sax/properties/lexical-handler", handler.getCommentHandler() ); } catch ( final SAXException se ) { // ignore .. logger.debug( "Comments are not supported by this SAX implementation." ); } try { reader.setFeature( "http://xml.org/sax/features/xmlns-uris", true ); } catch ( final SAXException e ) { // ignore handler.setXmlnsUrisNotAvailable( true ); } try { // disable validation, as our parsers should handle that already. And we do not want to read // external DTDs that may not exist at all. reader.setFeature( "http://xml.org/sax/features/validation", false ); reader.setFeature( "http://xml.org/sax/features/external-parameter-entities", false ); reader.setFeature( "http://xml.org/sax/features/external-general-entities", false ); } catch ( final SAXException e ) { // ignore if ( logger.isDebugEnabled() ) { logger.debug( "Disabling external validation failed. Parsing may or may not fail with a parse error later." ); } } try { reader.setFeature( "http://xml.org/sax/features/namespaces", true ); reader.setFeature( "http://xml.org/sax/features/namespace-prefixes", true ); } catch ( final SAXException e ) { if ( logger.isDebugEnabled() ) { logger.warn( "No Namespace features will be available. (Yes, this is serious)", e ); } else if ( logger.isWarnEnabled() ) { logger.warn( "No Namespace features will be available. (Yes, this is serious)" ); } } } /** * Creates a resource by interpreting the data given in the resource-data object. If additional datastreams need to be * parsed, the provided resource manager should be used. This method parses the given resource-data as XML stream. * * @param manager the resource manager used for all resource loading. * @param data the resource-data from where the binary data is read. * @param context the resource context used to resolve relative resource paths. * @return the parsed result, never null. * @throws ResourceCreationException if the resource could not be parsed due to syntaxctial or logical errors in the * data. * @throws ResourceLoadingException if the resource could not be accessed from the physical storage. */ public Resource create( final ResourceManager manager, final ResourceData data, final ResourceKey context ) throws ResourceCreationException, ResourceLoadingException { try { final SAXParser parser = getParser(); final XMLReader reader = parser.getXMLReader(); final XmlFactoryModule[] rootHandlers = getModules(); if ( rootHandlers.length == 0 ) { throw new ResourceCreationException( "There are no root-handlers registered for the factory for type " + getFactoryType() ); } final ResourceDataInputSource input = new ResourceDataInputSource( data, manager ); final ResourceKey contextKey; final long version; final ResourceKey targetKey = data.getKey(); if ( context == null ) { contextKey = targetKey; version = data.getVersion( manager ); } else { contextKey = context; version = -1; } final RootXmlReadHandler handler = createRootHandler( manager, targetKey, rootHandlers, contextKey, version ); final DefaultConfiguration parserConfiguration = handler.getParserConfiguration(); final URL value = manager.toURL( contextKey ); if ( value != null ) { parserConfiguration.setConfigProperty( CONTENTBASE_KEY, value.toExternalForm() ); } configureReader( reader, handler ); reader.setContentHandler( handler ); reader.setDTDHandler( handler ); reader.setEntityResolver( handler.getEntityResolver() ); reader.setErrorHandler( getErrorHandler() ); final Map parameters = targetKey.getFactoryParameters(); final Iterator it = parameters.keySet().iterator(); while ( it.hasNext() ) { final Object o = it.next(); if ( o instanceof FactoryParameterKey ) { final FactoryParameterKey fpk = (FactoryParameterKey) o; handler.setHelperObject( fpk.getName(), parameters.get( fpk ) ); } } reader.parse( input ); final Object createdProduct = finishResult ( handler.getResult(), manager, data, contextKey ); handler.getDependencyCollector().add( targetKey, data.getVersion( manager ) ); return createResource( targetKey, handler, createdProduct, getFactoryType() ); } catch ( ParserConfigurationException e ) { throw new ResourceCreationException( "Unable to initialize the XML-Parser", e ); } catch ( SAXException e ) { throw new ResourceCreationException( "Unable to parse the document: " + data.getKey(), e ); } catch ( IOException e ) { throw new ResourceLoadingException( "Unable to read the stream from document: " + data.getKey(), e ); } } protected RootXmlReadHandler createRootHandler( final ResourceManager manager, final ResourceKey targetKey, final XmlFactoryModule[] rootHandlers, final ResourceKey contextKey, final long version ) { return new MultiplexRootElementHandler( manager, targetKey, contextKey, version, rootHandlers ); } /** * A method to allow to invoke the parsing without accessing the LibLoader layer. The data to be parsed is held in the * given InputSource object. * * @param manager the resource manager used for all resource loading. * @param input the raw-data given as SAX-InputSource. * @param context the resource context used to resolve relative resource paths. * @param parameters the parse parameters. * @return the parsed result, never null. * @throws ResourceCreationException if the resource could not be parsed due to syntaxctial or logical errors in * the data. * @throws ResourceLoadingException if the resource could not be accessed from the physical storage. * @throws ResourceKeyCreationException if creating the context key failed. */ public Object parseDirectly( final ResourceManager manager, final InputSource input, final ResourceKey context, final Map parameters ) throws ResourceKeyCreationException, ResourceCreationException, ResourceLoadingException { try { final SAXParser parser = getParser(); final XMLReader reader = parser.getXMLReader(); final ResourceKey targetKey = manager.createKey( EMPTY_DATA ); final ResourceKey contextKey; if ( context == null ) { contextKey = targetKey; } else { contextKey = context; } final XmlFactoryModule[] rootHandlers = getModules(); final RootXmlReadHandler handler = createRootHandler( manager, targetKey, rootHandlers, contextKey, -1 ); final DefaultConfiguration parserConfiguration = handler.getParserConfiguration(); final URL value = manager.toURL( contextKey ); if ( value != null ) { parserConfiguration.setConfigProperty( CONTENTBASE_KEY, value.toExternalForm() ); } configureReader( reader, handler ); reader.setContentHandler( handler ); reader.setDTDHandler( handler ); reader.setEntityResolver( handler.getEntityResolver() ); reader.setErrorHandler( getErrorHandler() ); final Iterator it = parameters.keySet().iterator(); while ( it.hasNext() ) { final Object o = it.next(); if ( o instanceof FactoryParameterKey ) { final FactoryParameterKey fpk = (FactoryParameterKey) o; handler.setHelperObject( fpk.getName(), parameters.get( fpk ) ); } } reader.parse( input ); return finishResult( handler.getResult(), manager, new RawResourceData( targetKey ), contextKey ); } catch ( ParserConfigurationException e ) { throw new ResourceCreationException ( "Unable to initialize the XML-Parser", e ); } catch ( SAXException e ) { throw new ResourceCreationException( "Unable to parse the document", e ); } catch ( IOException e ) { throw new ResourceLoadingException( "Unable to read the stream", e ); } } /** * Returns the registered XmlFactoryModules as array. We assume that the modules are evaluated in the given order. The * modules from the configuration are listed first (highest priority, as they may be supplied by user-overrides), then * the modules that have been registered manually, where the oldest modules are returned as lowest priority elements. * * @return the modules as array. */ protected final XmlFactoryModule[] getModules() { final ArrayList<XmlFactoryModule> realModules = new ArrayList<XmlFactoryModule>(); realModules.addAll( modulesFromConfiguration ); for ( int i = modules.size() - 1; i >= 0; i -= 1 ) { final XmlFactoryModule xmlFactoryModule = modules.get( i ); realModules.add( xmlFactoryModule ); } return realModules.toArray( new XmlFactoryModule[ realModules.size() ] ); } /** * Creates a Resource object for the given product. By default this returns a compound-resource that holds all the key * that identify the resources used during the content production. * * @param targetKey the target key. * @param handler the root handler used for the parsing. * @param createdProduct the created product. * @param createdType the type information for the object that has been parsed. * @return the product wrapped into a resource object. */ protected Resource createResource( final ResourceKey targetKey, final RootXmlReadHandler handler, final Object createdProduct, final Class createdType ) { return new CompoundResource( targetKey, handler.getDependencyCollector(), createdProduct, createdType ); } /** * Finishes up the result. This can be used for general clean up and post-parse initializaion of the result. The * default implementation does nothing and just returns the object itself. * * @param res the parsed resource. * @param manager the resource manager that was used to load the resource. * @param data the data object from where the resource is loaded. * @param context the context that resolves relative resource paths. * @return the parsed resource. * @throws ResourceCreationException if the post initialization fails. * @throws ResourceLoadingException if loading external resources failed with an IO error. */ protected Object finishResult( final Object res, final ResourceManager manager, final ResourceData data, final ResourceKey context ) throws ResourceCreationException, ResourceLoadingException { return res; } /** * Returns the configuration that should be used to initialize this factory. * * @return the configuration for initializing the factory. */ protected abstract Configuration getConfiguration(); /** * Loads all XmlFactoryModule-implementations from the given configuration. * * @see #getConfiguration() */ public void initializeDefaults() { final String type = getFactoryType().getName(); final String prefix = ResourceFactory.CONFIG_PREFIX + type; final Configuration config = getConfiguration(); final Iterator itType = config.findPropertyKeys( prefix ); while ( itType.hasNext() ) { final String key = (String) itType.next(); final String modClass = config.getConfigProperty( key ); final XmlFactoryModule maybeFactory = ObjectUtilities.loadAndInstantiate ( modClass, AbstractXmlResourceFactory.class, XmlFactoryModule.class ); if ( maybeFactory == null ) { continue; } modulesFromConfiguration.add( maybeFactory ); } } /** * Registers a factory module for being used during the parsing. If the factory module does not return a result that * matches the factory's type, the parsing will always fail. * * @param factoryModule the factory module. * @throws NullPointerException if the module given is null. */ public void registerModule( final XmlFactoryModule factoryModule ) { if ( factoryModule == null ) { throw new NullPointerException(); } modules.add( factoryModule ); } /** * Returns the XML-Error handler that should be registered with the XML parser. By default, this returns a logger. * * @return the error handler. */ protected ErrorHandler getErrorHandler() { return new LoggingErrorHandler(); } }