001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.beans.factory.xml;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.util.HashSet;
022import java.util.Set;
023
024import javax.xml.parsers.ParserConfigurationException;
025
026import org.w3c.dom.Document;
027import org.xml.sax.EntityResolver;
028import org.xml.sax.ErrorHandler;
029import org.xml.sax.InputSource;
030import org.xml.sax.SAXException;
031import org.xml.sax.SAXParseException;
032
033import org.springframework.beans.BeanUtils;
034import org.springframework.beans.factory.BeanDefinitionStoreException;
035import org.springframework.beans.factory.parsing.EmptyReaderEventListener;
036import org.springframework.beans.factory.parsing.FailFastProblemReporter;
037import org.springframework.beans.factory.parsing.NullSourceExtractor;
038import org.springframework.beans.factory.parsing.ProblemReporter;
039import org.springframework.beans.factory.parsing.ReaderEventListener;
040import org.springframework.beans.factory.parsing.SourceExtractor;
041import org.springframework.beans.factory.support.AbstractBeanDefinitionReader;
042import org.springframework.beans.factory.support.BeanDefinitionRegistry;
043import org.springframework.core.Constants;
044import org.springframework.core.NamedThreadLocal;
045import org.springframework.core.io.DescriptiveResource;
046import org.springframework.core.io.Resource;
047import org.springframework.core.io.ResourceLoader;
048import org.springframework.core.io.support.EncodedResource;
049import org.springframework.lang.Nullable;
050import org.springframework.util.Assert;
051import org.springframework.util.xml.SimpleSaxErrorHandler;
052import org.springframework.util.xml.XmlValidationModeDetector;
053
054/**
055 * Bean definition reader for XML bean definitions.
056 * Delegates the actual XML document reading to an implementation
057 * of the {@link BeanDefinitionDocumentReader} interface.
058 *
059 * <p>Typically applied to a
060 * {@link org.springframework.beans.factory.support.DefaultListableBeanFactory}
061 * or a {@link org.springframework.context.support.GenericApplicationContext}.
062 *
063 * <p>This class loads a DOM document and applies the BeanDefinitionDocumentReader to it.
064 * The document reader will register each bean definition with the given bean factory,
065 * talking to the latter's implementation of the
066 * {@link org.springframework.beans.factory.support.BeanDefinitionRegistry} interface.
067 *
068 * @author Juergen Hoeller
069 * @author Rob Harrop
070 * @author Chris Beams
071 * @since 26.11.2003
072 * @see #setDocumentReaderClass
073 * @see BeanDefinitionDocumentReader
074 * @see DefaultBeanDefinitionDocumentReader
075 * @see BeanDefinitionRegistry
076 * @see org.springframework.beans.factory.support.DefaultListableBeanFactory
077 * @see org.springframework.context.support.GenericApplicationContext
078 */
079public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
080
081        /**
082         * Indicates that the validation should be disabled.
083         */
084        public static final int VALIDATION_NONE = XmlValidationModeDetector.VALIDATION_NONE;
085
086        /**
087         * Indicates that the validation mode should be detected automatically.
088         */
089        public static final int VALIDATION_AUTO = XmlValidationModeDetector.VALIDATION_AUTO;
090
091        /**
092         * Indicates that DTD validation should be used.
093         */
094        public static final int VALIDATION_DTD = XmlValidationModeDetector.VALIDATION_DTD;
095
096        /**
097         * Indicates that XSD validation should be used.
098         */
099        public static final int VALIDATION_XSD = XmlValidationModeDetector.VALIDATION_XSD;
100
101
102        /** Constants instance for this class. */
103        private static final Constants constants = new Constants(XmlBeanDefinitionReader.class);
104
105        private int validationMode = VALIDATION_AUTO;
106
107        private boolean namespaceAware = false;
108
109        private Class<? extends BeanDefinitionDocumentReader> documentReaderClass =
110                        DefaultBeanDefinitionDocumentReader.class;
111
112        private ProblemReporter problemReporter = new FailFastProblemReporter();
113
114        private ReaderEventListener eventListener = new EmptyReaderEventListener();
115
116        private SourceExtractor sourceExtractor = new NullSourceExtractor();
117
118        @Nullable
119        private NamespaceHandlerResolver namespaceHandlerResolver;
120
121        private DocumentLoader documentLoader = new DefaultDocumentLoader();
122
123        @Nullable
124        private EntityResolver entityResolver;
125
126        private ErrorHandler errorHandler = new SimpleSaxErrorHandler(logger);
127
128        private final XmlValidationModeDetector validationModeDetector = new XmlValidationModeDetector();
129
130        private final ThreadLocal<Set<EncodedResource>> resourcesCurrentlyBeingLoaded =
131                        new NamedThreadLocal<Set<EncodedResource>>("XML bean definition resources currently being loaded"){
132                                @Override
133                                protected Set<EncodedResource> initialValue() {
134                                        return new HashSet<>(4);
135                                }
136                        };
137
138
139        /**
140         * Create new XmlBeanDefinitionReader for the given bean factory.
141         * @param registry the BeanFactory to load bean definitions into,
142         * in the form of a BeanDefinitionRegistry
143         */
144        public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
145                super(registry);
146        }
147
148
149        /**
150         * Set whether to use XML validation. Default is {@code true}.
151         * <p>This method switches namespace awareness on if validation is turned off,
152         * in order to still process schema namespaces properly in such a scenario.
153         * @see #setValidationMode
154         * @see #setNamespaceAware
155         */
156        public void setValidating(boolean validating) {
157                this.validationMode = (validating ? VALIDATION_AUTO : VALIDATION_NONE);
158                this.namespaceAware = !validating;
159        }
160
161        /**
162         * Set the validation mode to use by name. Defaults to {@link #VALIDATION_AUTO}.
163         * @see #setValidationMode
164         */
165        public void setValidationModeName(String validationModeName) {
166                setValidationMode(constants.asNumber(validationModeName).intValue());
167        }
168
169        /**
170         * Set the validation mode to use. Defaults to {@link #VALIDATION_AUTO}.
171         * <p>Note that this only activates or deactivates validation itself.
172         * If you are switching validation off for schema files, you might need to
173         * activate schema namespace support explicitly: see {@link #setNamespaceAware}.
174         */
175        public void setValidationMode(int validationMode) {
176                this.validationMode = validationMode;
177        }
178
179        /**
180         * Return the validation mode to use.
181         */
182        public int getValidationMode() {
183                return this.validationMode;
184        }
185
186        /**
187         * Set whether or not the XML parser should be XML namespace aware.
188         * Default is "false".
189         * <p>This is typically not needed when schema validation is active.
190         * However, without validation, this has to be switched to "true"
191         * in order to properly process schema namespaces.
192         */
193        public void setNamespaceAware(boolean namespaceAware) {
194                this.namespaceAware = namespaceAware;
195        }
196
197        /**
198         * Return whether or not the XML parser should be XML namespace aware.
199         */
200        public boolean isNamespaceAware() {
201                return this.namespaceAware;
202        }
203
204        /**
205         * Specify which {@link org.springframework.beans.factory.parsing.ProblemReporter} to use.
206         * <p>The default implementation is {@link org.springframework.beans.factory.parsing.FailFastProblemReporter}
207         * which exhibits fail fast behaviour. External tools can provide an alternative implementation
208         * that collates errors and warnings for display in the tool UI.
209         */
210        public void setProblemReporter(@Nullable ProblemReporter problemReporter) {
211                this.problemReporter = (problemReporter != null ? problemReporter : new FailFastProblemReporter());
212        }
213
214        /**
215         * Specify which {@link ReaderEventListener} to use.
216         * <p>The default implementation is EmptyReaderEventListener which discards every event notification.
217         * External tools can provide an alternative implementation to monitor the components being
218         * registered in the BeanFactory.
219         */
220        public void setEventListener(@Nullable ReaderEventListener eventListener) {
221                this.eventListener = (eventListener != null ? eventListener : new EmptyReaderEventListener());
222        }
223
224        /**
225         * Specify the {@link SourceExtractor} to use.
226         * <p>The default implementation is {@link NullSourceExtractor} which simply returns {@code null}
227         * as the source object. This means that - during normal runtime execution -
228         * no additional source metadata is attached to the bean configuration metadata.
229         */
230        public void setSourceExtractor(@Nullable SourceExtractor sourceExtractor) {
231                this.sourceExtractor = (sourceExtractor != null ? sourceExtractor : new NullSourceExtractor());
232        }
233
234        /**
235         * Specify the {@link NamespaceHandlerResolver} to use.
236         * <p>If none is specified, a default instance will be created through
237         * {@link #createDefaultNamespaceHandlerResolver()}.
238         */
239        public void setNamespaceHandlerResolver(@Nullable NamespaceHandlerResolver namespaceHandlerResolver) {
240                this.namespaceHandlerResolver = namespaceHandlerResolver;
241        }
242
243        /**
244         * Specify the {@link DocumentLoader} to use.
245         * <p>The default implementation is {@link DefaultDocumentLoader}
246         * which loads {@link Document} instances using JAXP.
247         */
248        public void setDocumentLoader(@Nullable DocumentLoader documentLoader) {
249                this.documentLoader = (documentLoader != null ? documentLoader : new DefaultDocumentLoader());
250        }
251
252        /**
253         * Set a SAX entity resolver to be used for parsing.
254         * <p>By default, {@link ResourceEntityResolver} will be used. Can be overridden
255         * for custom entity resolution, for example relative to some specific base path.
256         */
257        public void setEntityResolver(@Nullable EntityResolver entityResolver) {
258                this.entityResolver = entityResolver;
259        }
260
261        /**
262         * Return the EntityResolver to use, building a default resolver
263         * if none specified.
264         */
265        protected EntityResolver getEntityResolver() {
266                if (this.entityResolver == null) {
267                        // Determine default EntityResolver to use.
268                        ResourceLoader resourceLoader = getResourceLoader();
269                        if (resourceLoader != null) {
270                                this.entityResolver = new ResourceEntityResolver(resourceLoader);
271                        }
272                        else {
273                                this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader());
274                        }
275                }
276                return this.entityResolver;
277        }
278
279        /**
280         * Set an implementation of the {@code org.xml.sax.ErrorHandler}
281         * interface for custom handling of XML parsing errors and warnings.
282         * <p>If not set, a default SimpleSaxErrorHandler is used that simply
283         * logs warnings using the logger instance of the view class,
284         * and rethrows errors to discontinue the XML transformation.
285         * @see SimpleSaxErrorHandler
286         */
287        public void setErrorHandler(ErrorHandler errorHandler) {
288                this.errorHandler = errorHandler;
289        }
290
291        /**
292         * Specify the {@link BeanDefinitionDocumentReader} implementation to use,
293         * responsible for the actual reading of the XML bean definition document.
294         * <p>The default is {@link DefaultBeanDefinitionDocumentReader}.
295         * @param documentReaderClass the desired BeanDefinitionDocumentReader implementation class
296         */
297        public void setDocumentReaderClass(Class<? extends BeanDefinitionDocumentReader> documentReaderClass) {
298                this.documentReaderClass = documentReaderClass;
299        }
300
301
302        /**
303         * Load bean definitions from the specified XML file.
304         * @param resource the resource descriptor for the XML file
305         * @return the number of bean definitions found
306         * @throws BeanDefinitionStoreException in case of loading or parsing errors
307         */
308        @Override
309        public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
310                return loadBeanDefinitions(new EncodedResource(resource));
311        }
312
313        /**
314         * Load bean definitions from the specified XML file.
315         * @param encodedResource the resource descriptor for the XML file,
316         * allowing to specify an encoding to use for parsing the file
317         * @return the number of bean definitions found
318         * @throws BeanDefinitionStoreException in case of loading or parsing errors
319         */
320        public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
321                Assert.notNull(encodedResource, "EncodedResource must not be null");
322                if (logger.isTraceEnabled()) {
323                        logger.trace("Loading XML bean definitions from " + encodedResource);
324                }
325
326                Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
327
328                if (!currentResources.add(encodedResource)) {
329                        throw new BeanDefinitionStoreException(
330                                        "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
331                }
332
333                try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
334                        InputSource inputSource = new InputSource(inputStream);
335                        if (encodedResource.getEncoding() != null) {
336                                inputSource.setEncoding(encodedResource.getEncoding());
337                        }
338                        return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
339                }
340                catch (IOException ex) {
341                        throw new BeanDefinitionStoreException(
342                                        "IOException parsing XML document from " + encodedResource.getResource(), ex);
343                }
344                finally {
345                        currentResources.remove(encodedResource);
346                        if (currentResources.isEmpty()) {
347                                this.resourcesCurrentlyBeingLoaded.remove();
348                        }
349                }
350        }
351
352        /**
353         * Load bean definitions from the specified XML file.
354         * @param inputSource the SAX InputSource to read from
355         * @return the number of bean definitions found
356         * @throws BeanDefinitionStoreException in case of loading or parsing errors
357         */
358        public int loadBeanDefinitions(InputSource inputSource) throws BeanDefinitionStoreException {
359                return loadBeanDefinitions(inputSource, "resource loaded through SAX InputSource");
360        }
361
362        /**
363         * Load bean definitions from the specified XML file.
364         * @param inputSource the SAX InputSource to read from
365         * @param resourceDescription a description of the resource
366         * (can be {@code null} or empty)
367         * @return the number of bean definitions found
368         * @throws BeanDefinitionStoreException in case of loading or parsing errors
369         */
370        public int loadBeanDefinitions(InputSource inputSource, @Nullable String resourceDescription)
371                        throws BeanDefinitionStoreException {
372
373                return doLoadBeanDefinitions(inputSource, new DescriptiveResource(resourceDescription));
374        }
375
376
377        /**
378         * Actually load bean definitions from the specified XML file.
379         * @param inputSource the SAX InputSource to read from
380         * @param resource the resource descriptor for the XML file
381         * @return the number of bean definitions found
382         * @throws BeanDefinitionStoreException in case of loading or parsing errors
383         * @see #doLoadDocument
384         * @see #registerBeanDefinitions
385         */
386        protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
387                        throws BeanDefinitionStoreException {
388
389                try {
390                        Document doc = doLoadDocument(inputSource, resource);
391                        int count = registerBeanDefinitions(doc, resource);
392                        if (logger.isDebugEnabled()) {
393                                logger.debug("Loaded " + count + " bean definitions from " + resource);
394                        }
395                        return count;
396                }
397                catch (BeanDefinitionStoreException ex) {
398                        throw ex;
399                }
400                catch (SAXParseException ex) {
401                        throw new XmlBeanDefinitionStoreException(resource.getDescription(),
402                                        "Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
403                }
404                catch (SAXException ex) {
405                        throw new XmlBeanDefinitionStoreException(resource.getDescription(),
406                                        "XML document from " + resource + " is invalid", ex);
407                }
408                catch (ParserConfigurationException ex) {
409                        throw new BeanDefinitionStoreException(resource.getDescription(),
410                                        "Parser configuration exception parsing XML from " + resource, ex);
411                }
412                catch (IOException ex) {
413                        throw new BeanDefinitionStoreException(resource.getDescription(),
414                                        "IOException parsing XML document from " + resource, ex);
415                }
416                catch (Throwable ex) {
417                        throw new BeanDefinitionStoreException(resource.getDescription(),
418                                        "Unexpected exception parsing XML document from " + resource, ex);
419                }
420        }
421
422        /**
423         * Actually load the specified document using the configured DocumentLoader.
424         * @param inputSource the SAX InputSource to read from
425         * @param resource the resource descriptor for the XML file
426         * @return the DOM Document
427         * @throws Exception when thrown from the DocumentLoader
428         * @see #setDocumentLoader
429         * @see DocumentLoader#loadDocument
430         */
431        protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
432                return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
433                                getValidationModeForResource(resource), isNamespaceAware());
434        }
435
436        /**
437         * Determine the validation mode for the specified {@link Resource}.
438         * If no explicit validation mode has been configured, then the validation
439         * mode gets {@link #detectValidationMode detected} from the given resource.
440         * <p>Override this method if you would like full control over the validation
441         * mode, even when something other than {@link #VALIDATION_AUTO} was set.
442         * @see #detectValidationMode
443         */
444        protected int getValidationModeForResource(Resource resource) {
445                int validationModeToUse = getValidationMode();
446                if (validationModeToUse != VALIDATION_AUTO) {
447                        return validationModeToUse;
448                }
449                int detectedMode = detectValidationMode(resource);
450                if (detectedMode != VALIDATION_AUTO) {
451                        return detectedMode;
452                }
453                // Hmm, we didn't get a clear indication... Let's assume XSD,
454                // since apparently no DTD declaration has been found up until
455                // detection stopped (before finding the document's root tag).
456                return VALIDATION_XSD;
457        }
458
459        /**
460         * Detect which kind of validation to perform on the XML file identified
461         * by the supplied {@link Resource}. If the file has a {@code DOCTYPE}
462         * definition then DTD validation is used otherwise XSD validation is assumed.
463         * <p>Override this method if you would like to customize resolution
464         * of the {@link #VALIDATION_AUTO} mode.
465         */
466        protected int detectValidationMode(Resource resource) {
467                if (resource.isOpen()) {
468                        throw new BeanDefinitionStoreException(
469                                        "Passed-in Resource [" + resource + "] contains an open stream: " +
470                                        "cannot determine validation mode automatically. Either pass in a Resource " +
471                                        "that is able to create fresh streams, or explicitly specify the validationMode " +
472                                        "on your XmlBeanDefinitionReader instance.");
473                }
474
475                InputStream inputStream;
476                try {
477                        inputStream = resource.getInputStream();
478                }
479                catch (IOException ex) {
480                        throw new BeanDefinitionStoreException(
481                                        "Unable to determine validation mode for [" + resource + "]: cannot open InputStream. " +
482                                        "Did you attempt to load directly from a SAX InputSource without specifying the " +
483                                        "validationMode on your XmlBeanDefinitionReader instance?", ex);
484                }
485
486                try {
487                        return this.validationModeDetector.detectValidationMode(inputStream);
488                }
489                catch (IOException ex) {
490                        throw new BeanDefinitionStoreException("Unable to determine validation mode for [" +
491                                        resource + "]: an error occurred whilst reading from the InputStream.", ex);
492                }
493        }
494
495        /**
496         * Register the bean definitions contained in the given DOM document.
497         * Called by {@code loadBeanDefinitions}.
498         * <p>Creates a new instance of the parser class and invokes
499         * {@code registerBeanDefinitions} on it.
500         * @param doc the DOM document
501         * @param resource the resource descriptor (for context information)
502         * @return the number of bean definitions found
503         * @throws BeanDefinitionStoreException in case of parsing errors
504         * @see #loadBeanDefinitions
505         * @see #setDocumentReaderClass
506         * @see BeanDefinitionDocumentReader#registerBeanDefinitions
507         */
508        public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
509                BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
510                int countBefore = getRegistry().getBeanDefinitionCount();
511                documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
512                return getRegistry().getBeanDefinitionCount() - countBefore;
513        }
514
515        /**
516         * Create the {@link BeanDefinitionDocumentReader} to use for actually
517         * reading bean definitions from an XML document.
518         * <p>The default implementation instantiates the specified "documentReaderClass".
519         * @see #setDocumentReaderClass
520         */
521        protected BeanDefinitionDocumentReader createBeanDefinitionDocumentReader() {
522                return BeanUtils.instantiateClass(this.documentReaderClass);
523        }
524
525        /**
526         * Create the {@link XmlReaderContext} to pass over to the document reader.
527         */
528        public XmlReaderContext createReaderContext(Resource resource) {
529                return new XmlReaderContext(resource, this.problemReporter, this.eventListener,
530                                this.sourceExtractor, this, getNamespaceHandlerResolver());
531        }
532
533        /**
534         * Lazily create a default NamespaceHandlerResolver, if not set before.
535         * @see #createDefaultNamespaceHandlerResolver()
536         */
537        public NamespaceHandlerResolver getNamespaceHandlerResolver() {
538                if (this.namespaceHandlerResolver == null) {
539                        this.namespaceHandlerResolver = createDefaultNamespaceHandlerResolver();
540                }
541                return this.namespaceHandlerResolver;
542        }
543
544        /**
545         * Create the default implementation of {@link NamespaceHandlerResolver} used if none is specified.
546         * <p>The default implementation returns an instance of {@link DefaultNamespaceHandlerResolver}.
547         * @see DefaultNamespaceHandlerResolver#DefaultNamespaceHandlerResolver(ClassLoader)
548         */
549        protected NamespaceHandlerResolver createDefaultNamespaceHandlerResolver() {
550                ClassLoader cl = (getResourceLoader() != null ? getResourceLoader().getClassLoader() : getBeanClassLoader());
551                return new DefaultNamespaceHandlerResolver(cl);
552        }
553
554}