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