
infra.beans.factory.xml.XmlBeanDefinitionReader Maven / Gradle / Ivy
/*
* Copyright 2017 - 2024 the original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see [https://www.gnu.org/licenses/]
*/
package infra.beans.factory.xml;
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;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.xml.parsers.ParserConfigurationException;
import infra.beans.BeanUtils;
import infra.beans.factory.BeanDefinitionStoreException;
import infra.beans.factory.parsing.EmptyReaderEventListener;
import infra.beans.factory.parsing.FailFastProblemReporter;
import infra.beans.factory.parsing.NullSourceExtractor;
import infra.beans.factory.parsing.ProblemReporter;
import infra.beans.factory.parsing.ReaderEventListener;
import infra.beans.factory.parsing.SourceExtractor;
import infra.beans.factory.support.AbstractBeanDefinitionReader;
import infra.beans.factory.support.BeanDefinitionRegistry;
import infra.beans.factory.support.StandardBeanFactory;
import infra.core.NamedThreadLocal;
import infra.core.io.DescriptiveResource;
import infra.core.io.EncodedResource;
import infra.core.io.Resource;
import infra.core.io.ResourceLoader;
import infra.lang.Assert;
import infra.lang.Nullable;
import infra.util.xml.SimpleSaxErrorHandler;
import infra.util.xml.XmlValidationModeDetector;
/**
* Bean definition reader for XML bean definitions.
* Delegates the actual XML document reading to an implementation
* of the {@link BeanDefinitionDocumentReader} interface.
*
* Typically applied to a
* {@link StandardBeanFactory}
* or a {@link infra.context.support.GenericApplicationContext}.
*
*
This class loads a DOM document and applies the BeanDefinitionDocumentReader to it.
* The document reader will register each bean definition with the given bean factory,
* talking to the latter's implementation of the
* {@link BeanDefinitionRegistry} interface.
*
* @author Juergen Hoeller
* @author Rob Harrop
* @author Chris Beams
* @author Harry Yang
* @see #setDocumentReaderClass
* @see BeanDefinitionDocumentReader
* @see DefaultBeanDefinitionDocumentReader
* @see BeanDefinitionRegistry
* @see StandardBeanFactory
* @see infra.context.support.GenericApplicationContext
* @since 4.0 2022/3/6 22:04
*/
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
/**
* 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;
/**
* Map of constant names to constant values for the validation constants defined
* in this class.
*/
private static final Map constants = Map.of(
"VALIDATION_NONE", VALIDATION_NONE,
"VALIDATION_AUTO", VALIDATION_AUTO,
"VALIDATION_DTD", VALIDATION_DTD,
"VALIDATION_XSD", VALIDATION_XSD
);
private int validationMode = VALIDATION_AUTO;
private boolean namespaceAware = false;
private Class extends BeanDefinitionDocumentReader> documentReaderClass =
DefaultBeanDefinitionDocumentReader.class;
private ProblemReporter problemReporter = new FailFastProblemReporter();
private ReaderEventListener eventListener = new EmptyReaderEventListener();
private SourceExtractor sourceExtractor = new NullSourceExtractor();
@Nullable
private NamespaceHandlerResolver namespaceHandlerResolver;
private DocumentLoader documentLoader = new DefaultDocumentLoader();
@Nullable
private EntityResolver entityResolver;
private ErrorHandler errorHandler = new SimpleSaxErrorHandler(logger);
private final XmlValidationModeDetector validationModeDetector = new XmlValidationModeDetector();
private final ThreadLocal> resourcesCurrentlyBeingLoaded =
NamedThreadLocal.withInitial("XML bean definition resources currently being loaded", () -> new HashSet<>(4));
/**
* Create new XmlBeanDefinitionReader for the given bean factory.
*
* @param registry the BeanFactory to load bean definitions into,
* in the form of a BeanDefinitionRegistry
*/
public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
super(registry);
}
/**
* Set whether to use XML validation. Default is {@code true}.
* 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) {
Assert.hasText(validationModeName, "'validationModeName' must not be null or blank");
Integer validationMode = constants.get(validationModeName);
Assert.notNull(validationMode, "Only validation mode constants allowed");
this.validationMode = validationMode;
}
/**
* Set the validation mode to use. Defaults to {@link #VALIDATION_AUTO}.
*
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) {
Assert.isTrue(constants.containsValue(validationMode),
"Only values of validation mode constants allowed");
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".
*
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 ProblemReporter} to use.
*
The default implementation is {@link 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(@Nullable ProblemReporter problemReporter) {
this.problemReporter = (problemReporter != null ? problemReporter : new FailFastProblemReporter());
}
/**
* Specify which {@link ReaderEventListener} to use.
*
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(@Nullable ReaderEventListener eventListener) {
this.eventListener = (eventListener != null ? eventListener : new EmptyReaderEventListener());
}
/**
* Specify the {@link SourceExtractor} to use.
*
The default implementation is {@link NullSourceExtractor} which simply returns {@code null}
* 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(@Nullable SourceExtractor sourceExtractor) {
this.sourceExtractor = (sourceExtractor != null ? sourceExtractor : new NullSourceExtractor());
}
/**
* Specify the {@link NamespaceHandlerResolver} to use.
*
If none is specified, a default instance will be created through
* {@link #createDefaultNamespaceHandlerResolver()}.
*/
public void setNamespaceHandlerResolver(@Nullable NamespaceHandlerResolver namespaceHandlerResolver) {
this.namespaceHandlerResolver = namespaceHandlerResolver;
}
/**
* Specify the {@link DocumentLoader} to use.
*
The default implementation is {@link DefaultDocumentLoader}
* which loads {@link Document} instances using JAXP.
*/
public void setDocumentLoader(@Nullable DocumentLoader documentLoader) {
this.documentLoader = (documentLoader != null ? documentLoader : new DefaultDocumentLoader());
}
/**
* Set a SAX entity resolver to be used for parsing.
*
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(@Nullable 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 DelegatingEntityResolver(getBeanClassLoader());
}
}
return this.entityResolver;
}
/**
* Set an implementation of the {@code org.xml.sax.ErrorHandler}
* interface for custom handling of XML parsing errors and warnings.
*
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 BeanDefinitionDocumentReader} implementation to use,
* responsible for the actual reading of the XML bean definition document.
*
The default is {@link DefaultBeanDefinitionDocumentReader}.
*
* @param documentReaderClass the desired BeanDefinitionDocumentReader implementation class
*/
public void setDocumentReaderClass(Class extends BeanDefinitionDocumentReader> documentReaderClass) {
this.documentReaderClass = documentReaderClass;
}
/**
* Load bean definitions from the specified XML file.
*
* @param resource the resource descriptor for the XML file
* @return the number of bean definitions found
* @throws BeanDefinitionStoreException in case of loading or parsing errors
*/
@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
return loadBeanDefinitions(new EncodedResource(resource));
}
/**
* Load bean 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 bean definitions found
* @throws BeanDefinitionStoreException in case of loading or parsing errors
*/
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Assert.notNull(encodedResource, "EncodedResource is required");
if (logger.isTraceEnabled()) {
logger.trace("Loading XML bean definitions from {}", encodedResource);
}
Set currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " + encodedResource.getResource(), ex);
}
finally {
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}
/**
* Load bean definitions from the specified XML file.
*
* @param inputSource the SAX InputSource to read from
* @return the number of bean definitions found
* @throws BeanDefinitionStoreException in case of loading or parsing errors
*/
public int loadBeanDefinitions(InputSource inputSource) throws BeanDefinitionStoreException {
return loadBeanDefinitions(inputSource, "resource loaded through SAX InputSource");
}
/**
* Load bean 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} or empty)
* @return the number of bean definitions found
* @throws BeanDefinitionStoreException in case of loading or parsing errors
*/
public int loadBeanDefinitions(InputSource inputSource, @Nullable String resourceDescription)
throws BeanDefinitionStoreException {
return doLoadBeanDefinitions(inputSource, new DescriptiveResource(resourceDescription));
}
/**
* Actually load bean 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 bean definitions found
* @throws BeanDefinitionStoreException in case of loading or parsing errors
* @see #doLoadDocument
* @see #registerBeanDefinitions
*/
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
Document doc = doLoadDocument(inputSource, resource);
int count = registerBeanDefinitions(doc, resource);
if (logger.isDebugEnabled()) {
logger.debug("Loaded {} bean definitions from {}", count, resource);
}
return count;
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (SAXParseException ex) {
throw new XmlBeanDefinitionStoreException(resource.toString(),
"Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
}
catch (SAXException ex) {
throw new XmlBeanDefinitionStoreException(resource.toString(),
"XML document from " + resource + " is invalid", ex);
}
catch (ParserConfigurationException ex) {
throw new BeanDefinitionStoreException(resource.toString(),
"Parser configuration exception parsing XML from " + resource, ex);
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(resource.toString(),
"IOException parsing XML document from " + resource, ex);
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(resource.toString(),
"Unexpected exception parsing XML document from " + resource, ex);
}
}
/**
* Actually load the specified document using the configured DocumentLoader.
*
* @param inputSource the SAX InputSource to read from
* @param resource the resource descriptor for the XML file
* @return the DOM Document
* @throws Exception when thrown from the DocumentLoader
* @see #setDocumentLoader
* @see DocumentLoader#loadDocument
*/
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
/**
* Determine the validation mode for the specified {@link Resource}.
* If no explicit validation mode has been configured, then the validation
* mode gets {@link #detectValidationMode detected} from the given resource.
* Override this method if you would like full control over the validation
* mode, even when something other than {@link #VALIDATION_AUTO} was set.
*
* @see #detectValidationMode
*/
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;
}
/**
* Detect which kind of validation to perform on the XML file identified
* by the supplied {@link Resource}. If the file has a {@code DOCTYPE}
* definition then DTD validation is used otherwise XSD validation is assumed.
*
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 BeanDefinitionStoreException(
"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 BeanDefinitionStoreException(
"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 BeanDefinitionStoreException("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}.
*
Creates a new instance of the parser class and invokes
* {@code registerBeanDefinitions} on it.
*
* @param doc the DOM document
* @param resource the resource descriptor (for context information)
* @return the number of bean definitions found
* @throws BeanDefinitionStoreException in case of parsing errors
* @see #loadBeanDefinitions
* @see #setDocumentReaderClass
* @see BeanDefinitionDocumentReader#registerBeanDefinitions
*/
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
int countBefore = getRegistry().getBeanDefinitionCount();
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
return getRegistry().getBeanDefinitionCount() - countBefore;
}
/**
* Create the {@link BeanDefinitionDocumentReader} to use for actually
* reading bean definitions from an XML document.
*
The default implementation instantiates the specified "documentReaderClass".
*
* @see #setDocumentReaderClass
*/
protected BeanDefinitionDocumentReader createBeanDefinitionDocumentReader() {
if (documentReaderClass == DefaultBeanDefinitionDocumentReader.class) {
return new DefaultBeanDefinitionDocumentReader();
}
return BeanUtils.newInstance(this.documentReaderClass);
}
/**
* Create the {@link XmlReaderContext} to pass over to the document reader.
*/
public XmlReaderContext createReaderContext(Resource resource) {
return new XmlReaderContext(resource, this.problemReporter, this.eventListener,
this.sourceExtractor, this, getNamespaceHandlerResolver());
}
/**
* Lazily create a default NamespaceHandlerResolver, if not set before.
*
* @see #createDefaultNamespaceHandlerResolver()
*/
public NamespaceHandlerResolver getNamespaceHandlerResolver() {
if (this.namespaceHandlerResolver == null) {
this.namespaceHandlerResolver = createDefaultNamespaceHandlerResolver();
}
return this.namespaceHandlerResolver;
}
/**
* Create the default implementation of {@link NamespaceHandlerResolver} used if none is specified.
*
The default implementation returns an instance of {@link DefaultNamespaceHandlerResolver}.
*
* @see DefaultNamespaceHandlerResolver#DefaultNamespaceHandlerResolver(ClassLoader)
*/
protected NamespaceHandlerResolver createDefaultNamespaceHandlerResolver() {
ClassLoader cl = (getResourceLoader() != null ? getResourceLoader().getClassLoader() : getBeanClassLoader());
return new DefaultNamespaceHandlerResolver(cl);
}
}