org.data.support.beans.factory.xml.XmlQueryDefinitionReader.java Source code

Java tutorial

Introduction

Here is the source code for org.data.support.beans.factory.xml.XmlQueryDefinitionReader.java

Source

/*
 * Copyright 2013 the original author or authors.
 *
 * 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.data.support.beans.factory.xml;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Set;

import javax.xml.parsers.ParserConfigurationException;

import org.data.support.beans.factory.QueryDefinitionStoreException;
import org.data.support.beans.factory.XmlQueryDefinitionStoreException;
import org.data.support.beans.factory.parsing.DefaultReaderEventListener;
import org.data.support.beans.factory.parsing.FailFastProblemReporter;
import org.data.support.beans.factory.support.QueryDefinitionRegistry;
import org.data.support.beans.factory.support.QueryNameGenerator;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.parsing.EmptyReaderEventListener;
import org.springframework.beans.factory.parsing.NullSourceExtractor;
import org.springframework.beans.factory.parsing.ProblemReporter;
import org.springframework.beans.factory.parsing.ReaderEventListener;
import org.springframework.beans.factory.parsing.SourceExtractor;
import org.springframework.beans.factory.xml.BeanDefinitionDocumentReader;
import org.springframework.beans.factory.xml.DefaultDocumentLoader;
import org.springframework.beans.factory.xml.DefaultNamespaceHandlerResolver;
import org.springframework.beans.factory.xml.DocumentLoader;
import org.springframework.beans.factory.xml.NamespaceHandlerResolver;
import org.springframework.core.Constants;
import org.springframework.core.NamedThreadLocal;
import org.springframework.core.io.DescriptiveResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.util.Assert;
import org.springframework.util.xml.SimpleSaxErrorHandler;
import org.springframework.util.xml.XmlValidationModeDetector;
import org.w3c.dom.Document;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/**
 * Query definition reader for XML query definitions.
 * Delegates the actual XML document reading to an implementation
 * of the {@link QueryDefinitionDocumentReader} interface.
 *
 * <p>This class loads a DOM document and applies the QueryDefinitionDocumentReader to it.
 * The document reader will register each query definition with the given query factory,
 * talking to the latter's implementation of the
 * {@link QueryDefinitionRegistryy} interface.
 * 
 * @author chen
 * @created Dec 27, 2013
 * @since 2013
 */
public class XmlQueryDefinitionReader extends AbstractQueryDefinitionReader {

    /**
     * Indicates that the validation should be disabled.
     */
    public static final int VALIDATION_NONE = XmlValidationModeDetector.VALIDATION_NONE;

    /**
     * Indicates that the validation mode should be detected automatically.
     */
    public static final int VALIDATION_AUTO = XmlValidationModeDetector.VALIDATION_AUTO;

    /**
     * Indicates that DTD validation should be used.
     */
    public static final int VALIDATION_DTD = XmlValidationModeDetector.VALIDATION_DTD;

    /**
     * Indicates that XSD validation should be used.
     */
    public static final int VALIDATION_XSD = XmlValidationModeDetector.VALIDATION_XSD;

    /** Constants instance for this class */
    private static final Constants constants = new Constants(XmlQueryDefinitionReader.class);

    private int validationMode = VALIDATION_AUTO;

    private boolean namespaceAware = false;

    private Class documentReaderClass = DefaultQueryDefinitionDocumentReader.class;

    private ProblemReporter problemReporter = new FailFastProblemReporter();

    private ReaderEventListener eventListener = new EmptyReaderEventListener();

    private SourceExtractor sourceExtractor = new NullSourceExtractor();

    private NamespaceHandlerResolver namespaceHandlerResolver;

    private DocumentLoader documentLoader = new DefaultDocumentLoader();

    private EntityResolver entityResolver;

    private ErrorHandler errorHandler = new SimpleSaxErrorHandler(logger);

    private final XmlValidationModeDetector validationModeDetector = new XmlValidationModeDetector();

    private final ThreadLocal<Set<EncodedResource>> resourcesCurrentlyBeingLoaded = new NamedThreadLocal<Set<EncodedResource>>(
            "XML query definition resources currently being loaded");

    /**
     * Create new XmlBeanDefinitionReader for the given bean factory.
     * @param registry the BeanFactory to load bean definitions into,
     * in the form of a BeanDefinitionRegistry
     */
    public XmlQueryDefinitionReader(QueryDefinitionRegistry registry) {
        super(registry);
    }

    /**
     * Set whether to use XML validation. Default is <code>true</code>.
     * <p>This method switches namespace awareness on if validation is turned off,
     * in order to still process schema namespaces properly in such a scenario.
     * @see #setValidationMode
     * @see #setNamespaceAware
     */
    public void setValidating(boolean validating) {
        this.validationMode = (validating ? VALIDATION_AUTO : VALIDATION_NONE);
        this.namespaceAware = !validating;
    }

    /**
     * Set the validation mode to use by name. Defaults to {@link #VALIDATION_AUTO}.
     * @see #setValidationMode
     */
    public void setValidationModeName(String validationModeName) {
        setValidationMode(constants.asNumber(validationModeName).intValue());
    }

    /**
     * Set the validation mode to use. Defaults to {@link #VALIDATION_AUTO}.
     * <p>Note that this only activates or deactivates validation itself.
     * If you are switching validation off for schema files, you might need to
     * activate schema namespace support explicitly: see {@link #setNamespaceAware}.
     */
    public void setValidationMode(int validationMode) {
        this.validationMode = validationMode;
    }

    /**
     * Return the validation mode to use.
     */
    public int getValidationMode() {
        return this.validationMode;
    }

    /**
     * Set whether or not the XML parser should be XML namespace aware.
     * Default is "false".
     * <p>This is typically not needed when schema validation is active.
     * However, without validation, this has to be switched to "true"
     * in order to properly process schema namespaces.
     */
    public void setNamespaceAware(boolean namespaceAware) {
        this.namespaceAware = namespaceAware;
    }

    /**
     * Return whether or not the XML parser should be XML namespace aware.
     */
    public boolean isNamespaceAware() {
        return this.namespaceAware;
    }

    /**
     * Specify which {@link org.springframework.beans.factory.parsing.ProblemReporter} to use.
     * <p>The default implementation is {@link org.springframework.beans.factory.parsing.FailFastProblemReporter}
     * which exhibits fail fast behaviour. External tools can provide an alternative implementation
     * that collates errors and warnings for display in the tool UI.
     */
    public void setProblemReporter(ProblemReporter problemReporter) {
        this.problemReporter = (problemReporter != null ? problemReporter : new FailFastProblemReporter());
    }

    /**
     * Specify which {@link ReaderEventListener} to use.
     * <p>The default implementation is EmptyReaderEventListener which discards every event notification.
     * External tools can provide an alternative implementation to monitor the components being
     * registered in the BeanFactory.
     */
    public void setEventListener(ReaderEventListener eventListener) {
        this.eventListener = (eventListener != null ? eventListener : new EmptyReaderEventListener());
    }

    /**
     * Specify the {@link SourceExtractor} to use.
     * <p>The default implementation is {@link NullSourceExtractor} which simply returns <code>null</code>
     * as the source object. This means that - during normal runtime execution -
     * no additional source metadata is attached to the bean configuration metadata.
     */
    public void setSourceExtractor(SourceExtractor sourceExtractor) {
        this.sourceExtractor = (sourceExtractor != null ? sourceExtractor : new NullSourceExtractor());
    }

    /**
     * Specify the {@link NamespaceHandlerResolver} to use.
     * <p>If none is specified, a default instance will be created through
     * {@link #createDefaultNamespaceHandlerResolver()}.
     */
    public void setNamespaceHandlerResolver(NamespaceHandlerResolver namespaceHandlerResolver) {
        this.namespaceHandlerResolver = namespaceHandlerResolver;
    }

    /**
     * Specify the {@link DocumentLoader} to use.
     * <p>The default implementation is {@link DefaultDocumentLoader}
     * which loads {@link Document} instances using JAXP.
     */
    public void setDocumentLoader(DocumentLoader documentLoader) {
        this.documentLoader = (documentLoader != null ? documentLoader : new DefaultDocumentLoader());
    }

    /**
     * Set a SAX entity resolver to be used for parsing.
     * <p>By default, {@link ResourceEntityResolver} will be used. Can be overridden
     * for custom entity resolution, for example relative to some specific base path.
     */
    public void setEntityResolver(EntityResolver entityResolver) {
        this.entityResolver = entityResolver;
    }

    /**
     * Return the EntityResolver to use, building a default resolver
     * if none specified.
     */
    protected EntityResolver getEntityResolver() {
        if (this.entityResolver == null) {
            // Determine default EntityResolver to use.
            ResourceLoader resourceLoader = getResourceLoader();
            if (resourceLoader != null) {
                this.entityResolver = new ResourceEntityResolver(resourceLoader);
            } else {
                this.entityResolver = new DelegateEntityResolver(getQueryClassLoader());
            }
        }
        return this.entityResolver;
    }

    /**
     * Set an implementation of the <code>org.xml.sax.ErrorHandler</code>
     * interface for custom handling of XML parsing errors and warnings.
     * <p>If not set, a default SimpleSaxErrorHandler is used that simply
     * logs warnings using the logger instance of the view class,
     * and rethrows errors to discontinue the XML transformation.
     * @see SimpleSaxErrorHandler
     */
    public void setErrorHandler(ErrorHandler errorHandler) {
        this.errorHandler = errorHandler;
    }

    /**
     * Specify the {@link QueryDefinitionDocumentReader} implementation to use,
     * responsible for the actual reading of the XML bean definition document.
     * <p>The default is {@link DefaultQueryDefinitionDocumentReader}.
     * @param documentReaderClass the desired BeanDefinitionDocumentReader implementation class
     */
    public void setDocumentReaderClass(Class documentReaderClass) {
        if (documentReaderClass == null
                || !QueryDefinitionDocumentReader.class.isAssignableFrom(documentReaderClass)) {
            throw new IllegalArgumentException(
                    "documentReaderClass must be an implementation of the BeanDefinitionDocumentReader interface");
        }
        this.documentReaderClass = documentReaderClass;
    }

    /**
     * Load query definitions from the specified XML file.
     * @param resource the resource descriptor for the XML file
     * @return the number of query definitions found
     * @throws QueryDefinitionStoreException in case of loading or parsing errors
     */
    public int loadQueryDefinitions(Resource resource) throws QueryDefinitionStoreException {
        return loadQueryDefinitions(new EncodedResource(resource));
    }

    /**
     * Load query definitions from the specified XML file.
     * @param encodedResource the resource descriptor for the XML file,
     * allowing to specify an encoding to use for parsing the file
     * @return the number of query definitions found
     * @throws QueryDefinitionStoreException in case of loading or parsing errors
     */
    public int loadQueryDefinitions(EncodedResource encodedResource) throws QueryDefinitionStoreException {
        Assert.notNull(encodedResource, "EncodedResource must not be null");
        if (logger.isInfoEnabled()) {
            logger.info("Loading XML bean definitions from " + encodedResource.getResource());
        }

        Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
        if (currentResources == null) {
            currentResources = new HashSet<EncodedResource>(4);
            this.resourcesCurrentlyBeingLoaded.set(currentResources);
        }
        if (!currentResources.add(encodedResource)) {
            throw new QueryDefinitionStoreException(
                    "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
        }
        try {
            InputStream inputStream = encodedResource.getResource().getInputStream();
            try {
                InputSource inputSource = new InputSource(inputStream);
                if (encodedResource.getEncoding() != null) {
                    inputSource.setEncoding(encodedResource.getEncoding());
                }
                return doLoadQueryDefinitions(inputSource, encodedResource.getResource());
            } finally {
                inputStream.close();
            }
        } catch (IOException ex) {
            throw new QueryDefinitionStoreException(
                    "IOException parsing XML document from " + encodedResource.getResource(), ex);
        } finally {
            currentResources.remove(encodedResource);
            if (currentResources.isEmpty()) {
                this.resourcesCurrentlyBeingLoaded.remove();
            }
        }
    }

    /**
     * Load query definitions from the specified XML file.
     * @param inputSource the SAX InputSource to read from
     * @return the number of query definitions found
     * @throws QueryDefinitionStoreException in case of loading or parsing errors
     */
    public int loadQueryDefinitions(InputSource inputSource) throws QueryDefinitionStoreException {
        return loadQueryDefinitions(inputSource, "resource loaded through SAX InputSource");
    }

    /**
     * Load query definitions from the specified XML file.
     * @param inputSource the SAX InputSource to read from
     * @param resourceDescription a description of the resource
     * (can be <code>null</code> or empty)
     * @return the number of query definitions found
     * @throws QueryDefinitionStoreException in case of loading or parsing errors
     */
    public int loadQueryDefinitions(InputSource inputSource, String resourceDescription)
            throws QueryDefinitionStoreException {

        return doLoadQueryDefinitions(inputSource, new DescriptiveResource(resourceDescription));
    }

    /**
     * Actually load query definitions from the specified XML file.
     * @param inputSource the SAX InputSource to read from
     * @param resource the resource descriptor for the XML file
     * @return the number of query definitions found
     * @throws QueryDefinitionStoreException in case of loading or parsing errors
     */
    protected int doLoadQueryDefinitions(InputSource inputSource, Resource resource)
            throws QueryDefinitionStoreException {
        try {
            int validationMode = getValidationModeForResource(resource);
            Document doc = this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
                    validationMode, isNamespaceAware());
            return registerQueryDefinitions(doc, resource);
        } catch (QueryDefinitionStoreException ex) {
            throw ex;
        } catch (SAXParseException ex) {
            throw new XmlQueryDefinitionStoreException(resource.getDescription(),
                    "Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
        } catch (SAXException ex) {
            throw new XmlQueryDefinitionStoreException(resource.getDescription(),
                    "XML document from " + resource + " is invalid", ex);
        } catch (ParserConfigurationException ex) {
            throw new QueryDefinitionStoreException(resource.getDescription(),
                    "Parser configuration exception parsing XML from " + resource, ex);
        } catch (IOException ex) {
            throw new QueryDefinitionStoreException(resource.getDescription(),
                    "IOException parsing XML document from " + resource, ex);
        } catch (Throwable ex) {
            throw new QueryDefinitionStoreException(resource.getDescription(),
                    "Unexpected exception parsing XML document from " + resource, ex);
        }
    }

    /**
     * Gets the validation mode for the specified {@link Resource}. If no explicit
     * validation mode has been configured then the validation mode is
     * {@link #detectValidationMode detected}.
     * <p>Override this method if you would like full control over the validation
     * mode, even when something other than {@link #VALIDATION_AUTO} was set.
     */
    protected int getValidationModeForResource(Resource resource) {
        int validationModeToUse = getValidationMode();
        if (validationModeToUse != VALIDATION_AUTO) {
            return validationModeToUse;
        }
        int detectedMode = detectValidationMode(resource);
        if (detectedMode != VALIDATION_AUTO) {
            return detectedMode;
        }
        // Hmm, we didn't get a clear indication... Let's assume XSD,
        // since apparently no DTD declaration has been found up until
        // detection stopped (before finding the document's root tag).
        return VALIDATION_XSD;
    }

    /**
     * Detects which kind of validation to perform on the XML file identified
     * by the supplied {@link Resource}. If the file has a <code>DOCTYPE</code>
     * definition then DTD validation is used otherwise XSD validation is assumed.
     * <p>Override this method if you would like to customize resolution
     * of the {@link #VALIDATION_AUTO} mode.
     */
    protected int detectValidationMode(Resource resource) {
        if (resource.isOpen()) {
            throw new QueryDefinitionStoreException(
                    "Passed-in Resource [" + resource + "] contains an open stream: "
                            + "cannot determine validation mode automatically. Either pass in a Resource "
                            + "that is able to create fresh streams, or explicitly specify the validationMode "
                            + "on your XmlBeanDefinitionReader instance.");
        }

        InputStream inputStream;
        try {
            inputStream = resource.getInputStream();
        } catch (IOException ex) {
            throw new QueryDefinitionStoreException(
                    "Unable to determine validation mode for [" + resource + "]: cannot open InputStream. "
                            + "Did you attempt to load directly from a SAX InputSource without specifying the "
                            + "validationMode on your XmlBeanDefinitionReader instance?",
                    ex);
        }

        try {
            return this.validationModeDetector.detectValidationMode(inputStream);
        } catch (IOException ex) {
            throw new QueryDefinitionStoreException("Unable to determine validation mode for [" + resource
                    + "]: an error occurred whilst reading from the InputStream.", ex);
        }
    }

    /**
     * Register the bean definitions contained in the given DOM document.
     * Called by <code>loadBeanDefinitions</code>.
     * <p>Creates a new instance of the parser class and invokes
     * <code>registerBeanDefinitions</code> on it.
     * @param doc the DOM document
     * @param resource the resource descriptor (for context information)
     * @return the number of bean definitions found
     * @throws QueryDefinitionStoreException in case of parsing errors
     * @see #loadBeanDefinitions
     * @see #setDocumentReaderClass
     * @see QueryDefinitionDocumentReader#registerBeanDefinitions
     */
    public int registerQueryDefinitions(Document doc, Resource resource) throws QueryDefinitionStoreException {
        // Read document based on new BeanDefinitionDocumentReader SPI.
        QueryDefinitionDocumentReader documentReader = createQueryDefinitionDocumentReader();
        int countBefore = getRegistry().getQueryDefinitionCount();
        documentReader.registerQueryDefinitions(doc, createReaderContext(resource));
        return getRegistry().getQueryDefinitionCount() - countBefore;
    }

    /**
     * Create the {@link QueryDefinitionDocumentReader} to use for actually
     * reading query definitions from an XML document.
     * <p>The default implementation instantiates the specified "documentReaderClass".
     * @see #setDocumentReaderClass
     */
    @SuppressWarnings("unchecked")
    protected QueryDefinitionDocumentReader createQueryDefinitionDocumentReader() {
        return QueryDefinitionDocumentReader.class.cast(BeanUtils.instantiateClass(this.documentReaderClass));
    }

    /**
     * Create the {@link XmlReaderContext} to pass over to the document reader.
     */
    protected XmlReaderContext createReaderContext(Resource resource) {
        if (this.namespaceHandlerResolver == null) {
            this.namespaceHandlerResolver = createDefaultNamespaceHandlerResolver();
        }
        return new XmlReaderContext(resource, this.problemReporter, this.eventListener, this.sourceExtractor, this,
                this.namespaceHandlerResolver);
    }

    /**
     * Create the default implementation of {@link NamespaceHandlerResolver} used if none is specified.
     * Default implementation returns an instance of {@link DefaultNamespaceHandlerResolver}.
     */
    protected NamespaceHandlerResolver createDefaultNamespaceHandlerResolver() {
        return new DefaultNamespaceHandlerResolver(getResourceLoader().getClassLoader());
    }

}