1 /*
2 * Copyright 2002-2008 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package org.springframework.beans.factory.xml;
18
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.util.HashSet;
22 import java.util.Set;
23
24 import javax.xml.parsers.ParserConfigurationException;
25
26 import org.w3c.dom.Document;
27 import org.xml.sax.EntityResolver;
28 import org.xml.sax.ErrorHandler;
29 import org.xml.sax.InputSource;
30 import org.xml.sax.SAXException;
31 import org.xml.sax.SAXParseException;
32
33 import org.springframework.beans.BeanUtils;
34 import org.springframework.beans.factory.BeanDefinitionStoreException;
35 import org.springframework.beans.factory.parsing.EmptyReaderEventListener;
36 import org.springframework.beans.factory.parsing.FailFastProblemReporter;
37 import org.springframework.beans.factory.parsing.NullSourceExtractor;
38 import org.springframework.beans.factory.parsing.ProblemReporter;
39 import org.springframework.beans.factory.parsing.ReaderEventListener;
40 import org.springframework.beans.factory.parsing.SourceExtractor;
41 import org.springframework.beans.factory.support.AbstractBeanDefinitionReader;
42 import org.springframework.beans.factory.support.BeanDefinitionRegistry;
43 import org.springframework.core.Constants;
44 import org.springframework.core.NamedThreadLocal;
45 import org.springframework.core.io.DescriptiveResource;
46 import org.springframework.core.io.Resource;
47 import org.springframework.core.io.ResourceLoader;
48 import org.springframework.core.io.support.EncodedResource;
49 import org.springframework.util.Assert;
50 import org.springframework.util.xml.SimpleSaxErrorHandler;
51 import org.springframework.util.xml.XmlValidationModeDetector;
52
53 /**
54 * Bean definition reader for XML bean definitions.
55 * Delegates the actual XML document reading to an implementation
56 * of the {@link BeanDefinitionDocumentReader} interface.
57 *
58 * <p>Typically applied to a
59 * {@link org.springframework.beans.factory.support.DefaultListableBeanFactory}
60 * or a {@link org.springframework.context.support.GenericApplicationContext}.
61 *
62 * <p>This class loads a DOM document and applies the BeanDefinitionDocumentReader to it.
63 * The document reader will register each bean definition with the given bean factory,
64 * talking to the latter's implementation of the
65 * {@link org.springframework.beans.factory.support.BeanDefinitionRegistry} interface.
66 *
67 * @author Juergen Hoeller
68 * @author Rob Harrop
69 * @since 26.11.2003
70 * @see #setDocumentReaderClass
71 * @see BeanDefinitionDocumentReader
72 * @see DefaultBeanDefinitionDocumentReader
73 * @see BeanDefinitionRegistry
74 * @see org.springframework.beans.factory.support.DefaultListableBeanFactory
75 * @see org.springframework.context.support.GenericApplicationContext
76 */
77 public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
78
79 /**
80 * Indicates that the validation should be disabled.
81 */
82 public static final int VALIDATION_NONE = XmlValidationModeDetector.VALIDATION_NONE;
83
84 /**
85 * Indicates that the validation mode should be detected automatically.
86 */
87 public static final int VALIDATION_AUTO = XmlValidationModeDetector.VALIDATION_AUTO;
88
89 /**
90 * Indicates that DTD validation should be used.
91 */
92 public static final int VALIDATION_DTD = XmlValidationModeDetector.VALIDATION_DTD;
93
94 /**
95 * Indicates that XSD validation should be used.
96 */
97 public static final int VALIDATION_XSD = XmlValidationModeDetector.VALIDATION_XSD;
98
99
100 /** Constants instance for this class */
101 private static final Constants constants = new Constants(XmlBeanDefinitionReader.class);
102
103 private boolean namespaceAware;
104
105 private int validationMode = VALIDATION_AUTO;
106
107 private Class parserClass;
108
109 private Class documentReaderClass = DefaultBeanDefinitionDocumentReader.class;
110
111 private ProblemReporter problemReporter = new FailFastProblemReporter();
112
113 private ReaderEventListener eventListener = new EmptyReaderEventListener();
114
115 private SourceExtractor sourceExtractor = new NullSourceExtractor();
116
117 private NamespaceHandlerResolver namespaceHandlerResolver;
118
119 private DocumentLoader documentLoader = new DefaultDocumentLoader();
120
121 private EntityResolver entityResolver;
122
123 private ErrorHandler errorHandler = new SimpleSaxErrorHandler(logger);
124
125 private final XmlValidationModeDetector validationModeDetector = new XmlValidationModeDetector();
126
127 private final ThreadLocal resourcesCurrentlyBeingLoaded =
128 new NamedThreadLocal("XML bean definition resources currently being loaded");
129
130
131 /**
132 * Create new XmlBeanDefinitionReader for the given bean factory.
133 * @param registry the BeanFactory to load bean definitions into,
134 * in the form of a BeanDefinitionRegistry
135 */
136 public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
137 super(registry);
138 }
139
140
141 /**
142 * Set whether or not the XML parser should be XML namespace aware.
143 * Default is "false".
144 */
145 public void setNamespaceAware(boolean namespaceAware) {
146 this.namespaceAware = namespaceAware;
147 }
148
149 /**
150 * Return whether or not the XML parser should be XML namespace aware.
151 */
152 public boolean isNamespaceAware() {
153 return this.namespaceAware;
154 }
155
156 /**
157 * Set if the XML parser should validate the document and thus enforce a DTD.
158 * @deprecated as of Spring 2.0: superseded by "validationMode"
159 * @see #setValidationMode
160 */
161 public void setValidating(boolean validating) {
162 this.validationMode = (validating ? VALIDATION_AUTO : VALIDATION_NONE);
163 }
164
165 /**
166 * Set the validation mode to use by name. Defaults to {@link #VALIDATION_AUTO}.
167 */
168 public void setValidationModeName(String validationModeName) {
169 setValidationMode(constants.asNumber(validationModeName).intValue());
170 }
171
172 /**
173 * Set the validation mode to use. Defaults to {@link #VALIDATION_AUTO}.
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 * Specify which {@link org.springframework.beans.factory.parsing.ProblemReporter} to use. Default implementation is
188 * {@link org.springframework.beans.factory.parsing.FailFastProblemReporter} which exhibits fail fast behaviour. External tools
189 * can provide an alternative implementation that collates errors and warnings for
190 * display in the tool UI.
191 */
192 public void setProblemReporter(ProblemReporter problemReporter) {
193 this.problemReporter = (problemReporter != null ? problemReporter : new FailFastProblemReporter());
194 }
195
196 /**
197 * Specify which {@link ReaderEventListener} to use. Default implementation is
198 * EmptyReaderEventListener which discards every event notification. External tools
199 * can provide an alternative implementation to monitor the components being registered
200 * in the BeanFactory.
201 */
202 public void setEventListener(ReaderEventListener eventListener) {
203 this.eventListener = (eventListener != null ? eventListener : new EmptyReaderEventListener());
204 }
205
206 /**
207 * Specify the {@link SourceExtractor} to use. The default implementation is
208 * {@link NullSourceExtractor} which simply returns <code>null</code> as the source object.
209 * This means that during normal runtime execution no additional source metadata is attached
210 * to the bean configuration metadata.
211 */
212 public void setSourceExtractor(SourceExtractor sourceExtractor) {
213 this.sourceExtractor = (sourceExtractor != null ? sourceExtractor : new NullSourceExtractor());
214 }
215
216 /**
217 * Specify the {@link NamespaceHandlerResolver} to use. If none is specified a default
218 * instance will be created by {@link #createDefaultNamespaceHandlerResolver()}.
219 */
220 public void setNamespaceHandlerResolver(NamespaceHandlerResolver namespaceHandlerResolver) {
221 this.namespaceHandlerResolver = namespaceHandlerResolver;
222 }
223
224 /**
225 * Specify the {@link DocumentLoader} to use. The default implementation is
226 * {@link DefaultDocumentLoader} which loads {@link Document} instances using JAXP.
227 */
228 public void setDocumentLoader(DocumentLoader documentLoader) {
229 this.documentLoader = (documentLoader != null ? documentLoader : new DefaultDocumentLoader());
230 }
231
232 /**
233 * Set a SAX entity resolver to be used for parsing. By default,
234 * BeansDtdResolver will be used. Can be overridden for custom entity
235 * resolution, for example relative to some specific base path.
236 * @see BeansDtdResolver
237 */
238 public void setEntityResolver(EntityResolver entityResolver) {
239 this.entityResolver = entityResolver;
240 }
241
242 /**
243 * Return the EntityResolver to use, building a default resolver
244 * if none specified.
245 */
246 protected EntityResolver getEntityResolver() {
247 if (this.entityResolver == null) {
248 // Determine default EntityResolver to use.
249 ResourceLoader resourceLoader = getResourceLoader();
250 if (resourceLoader != null) {
251 this.entityResolver = new ResourceEntityResolver(resourceLoader);
252 }
253 else {
254 this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader());
255 }
256 }
257 return this.entityResolver;
258 }
259
260 /**
261 * Set an implementation of the <code>org.xml.sax.ErrorHandler</code>
262 * interface for custom handling of XML parsing errors and warnings.
263 * <p>If not set, a default SimpleSaxErrorHandler is used that simply
264 * logs warnings using the logger instance of the view class,
265 * and rethrows errors to discontinue the XML transformation.
266 * @see SimpleSaxErrorHandler
267 */
268 public void setErrorHandler(ErrorHandler errorHandler) {
269 this.errorHandler = errorHandler;
270 }
271
272 /**
273 * Set the XmlBeanDefinitionParser implementation to use,
274 * responsible for the actual parsing of XML bean definitions.
275 * @deprecated as of Spring 2.0: superseded by "documentReaderClass"
276 * @see #setDocumentReaderClass
277 * @see XmlBeanDefinitionParser
278 */
279 public void setParserClass(Class parserClass) {
280 if (this.parserClass == null || !XmlBeanDefinitionParser.class.isAssignableFrom(parserClass)) {
281 throw new IllegalArgumentException("'parserClass' must be an XmlBeanDefinitionParser");
282 }
283 this.parserClass = parserClass;
284 }
285
286 /**
287 * Specify the BeanDefinitionDocumentReader implementation to use,
288 * responsible for the actual reading of the XML bean definition document.
289 * <p>Default is DefaultBeanDefinitionDocumentReader.
290 * @param documentReaderClass the desired BeanDefinitionDocumentReader implementation class
291 * @see BeanDefinitionDocumentReader
292 * @see DefaultBeanDefinitionDocumentReader
293 */
294 public void setDocumentReaderClass(Class documentReaderClass) {
295 if (documentReaderClass == null || !BeanDefinitionDocumentReader.class.isAssignableFrom(documentReaderClass)) {
296 throw new IllegalArgumentException(
297 "documentReaderClass must be an implementation of the BeanDefinitionDocumentReader interface");
298 }
299 this.documentReaderClass = documentReaderClass;
300 }
301
302
303 /**
304 * Load bean definitions from the specified XML file.
305 * @param resource the resource descriptor for the XML file
306 * @return the number of bean definitions found
307 * @throws BeanDefinitionStoreException in case of loading or parsing errors
308 */
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.isInfoEnabled()) {
323 logger.info("Loading XML bean definitions from " + encodedResource.getResource());
324 }
325
326 Set currentResources = (Set) this.resourcesCurrentlyBeingLoaded.get();
327 if (currentResources == null) {
328 currentResources = new HashSet(4);
329 this.resourcesCurrentlyBeingLoaded.set(currentResources);
330 }
331 if (!currentResources.add(encodedResource)) {
332 throw new BeanDefinitionStoreException(
333 "Detected recursive loading of " + encodedResource + " - check your import definitions!");
334 }
335 try {
336 InputStream inputStream = encodedResource.getResource().getInputStream();
337 try {
338 InputSource inputSource = new InputSource(inputStream);
339 if (encodedResource.getEncoding() != null) {
340 inputSource.setEncoding(encodedResource.getEncoding());
341 }
342 return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
343 }
344 finally {
345 inputStream.close();
346 }
347 }
348 catch (IOException ex) {
349 throw new BeanDefinitionStoreException(
350 "IOException parsing XML document from " + encodedResource.getResource(), ex);
351 }
352 finally {
353 currentResources.remove(encodedResource);
354 if (currentResources.isEmpty()) {
355 this.resourcesCurrentlyBeingLoaded.set(null);
356 }
357 }
358 }
359
360 /**
361 * Load bean definitions from the specified XML file.
362 * @param inputSource the SAX InputSource to read from
363 * @return the number of bean definitions found
364 * @throws BeanDefinitionStoreException in case of loading or parsing errors
365 */
366 public int loadBeanDefinitions(InputSource inputSource) throws BeanDefinitionStoreException {
367 return loadBeanDefinitions(inputSource, "resource loaded through SAX InputSource");
368 }
369
370 /**
371 * Load bean definitions from the specified XML file.
372 * @param inputSource the SAX InputSource to read from
373 * @param resourceDescription a description of the resource
374 * (can be <code>null</code> or empty)
375 * @return the number of bean definitions found
376 * @throws BeanDefinitionStoreException in case of loading or parsing errors
377 */
378 public int loadBeanDefinitions(InputSource inputSource, String resourceDescription)
379 throws BeanDefinitionStoreException {
380
381 return doLoadBeanDefinitions(inputSource, new DescriptiveResource(resourceDescription));
382 }
383
384
385 /**
386 * Actually load bean definitions from the specified XML file.
387 * @param inputSource the SAX InputSource to read from
388 * @param resource the resource descriptor for the XML file
389 * @return the number of bean definitions found
390 * @throws BeanDefinitionStoreException in case of loading or parsing errors
391 */
392 protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
393 throws BeanDefinitionStoreException {
394 try {
395 int validationMode = getValidationModeForResource(resource);
396 Document doc = this.documentLoader.loadDocument(
397 inputSource, getEntityResolver(), this.errorHandler, validationMode, isNamespaceAware());
398 return registerBeanDefinitions(doc, resource);
399 }
400 catch (BeanDefinitionStoreException ex) {
401 throw ex;
402 }
403 catch (SAXParseException ex) {
404 throw new XmlBeanDefinitionStoreException(resource.getDescription(),
405 "Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
406 }
407 catch (SAXException ex) {
408 throw new XmlBeanDefinitionStoreException(resource.getDescription(),
409 "XML document from " + resource + " is invalid", ex);
410 }
411 catch (ParserConfigurationException ex) {
412 throw new BeanDefinitionStoreException(resource.getDescription(),
413 "Parser configuration exception parsing XML from " + resource, ex);
414 }
415 catch (IOException ex) {
416 throw new BeanDefinitionStoreException(resource.getDescription(),
417 "IOException parsing XML document from " + resource, ex);
418 }
419 catch (Throwable ex) {
420 throw new BeanDefinitionStoreException(resource.getDescription(),
421 "Unexpected exception parsing XML document from " + resource, ex);
422 }
423 }
424
425
426 /**
427 * Gets the validation mode for the specified {@link Resource}. If no explicit
428 * validation mode has been configured then the validation mode is
429 * {@link #detectValidationMode detected}.
430 * <p>Override this method if you would like full control over the validation
431 * mode, even when something other than {@link #VALIDATION_AUTO} was set.
432 */
433 protected int getValidationModeForResource(Resource resource) {
434 int validationModeToUse = getValidationMode();
435 if (validationModeToUse != VALIDATION_AUTO) {
436 return validationModeToUse;
437 }
438 int detectedMode = detectValidationMode(resource);
439 if (detectedMode != VALIDATION_AUTO) {
440 return detectedMode;
441 }
442 // Hmm, we didn't get a clear indication... Let's assume XSD,
443 // since apparently no DTD declaration has been found up until
444 // detection stopped (before finding the document's root tag).
445 return VALIDATION_XSD;
446 }
447
448 /**
449 * Detects which kind of validation to perform on the XML file identified
450 * by the supplied {@link Resource}. If the file has a <code>DOCTYPE</code>
451 * definition then DTD validation is used otherwise XSD validation is assumed.
452 * <p>Override this method if you would like to customize resolution
453 * of the {@link #VALIDATION_AUTO} mode.
454 */
455 protected int detectValidationMode(Resource resource) {
456 if (resource.isOpen()) {
457 throw new BeanDefinitionStoreException(
458 "Passed-in Resource [" + resource + "] contains an open stream: " +
459 "cannot determine validation mode automatically. Either pass in a Resource " +
460 "that is able to create fresh streams, or explicitly specify the validationMode " +
461 "on your XmlBeanDefinitionReader instance.");
462 }
463
464 InputStream inputStream;
465 try {
466 inputStream = resource.getInputStream();
467 }
468 catch (IOException ex) {
469 throw new BeanDefinitionStoreException(
470 "Unable to determine validation mode for [" + resource + "]: cannot open InputStream. " +
471 "Did you attempt to load directly from a SAX InputSource without specifying the " +
472 "validationMode on your XmlBeanDefinitionReader instance?", ex);
473 }
474
475 try {
476 return this.validationModeDetector.detectValidationMode(inputStream);
477 }
478 catch (IOException ex) {
479 throw new BeanDefinitionStoreException("Unable to determine validation mode for [" +
480 resource + "]: an error occurred whilst reading from the InputStream.", ex);
481 }
482 }
483
484 /**
485 * Register the bean definitions contained in the given DOM document.
486 * Called by <code>loadBeanDefinitions</code>.
487 * <p>Creates a new instance of the parser class and invokes
488 * <code>registerBeanDefinitions</code> on it.
489 * @param doc the DOM document
490 * @param resource the resource descriptor (for context information)
491 * @return the number of bean definitions found
492 * @throws BeanDefinitionStoreException in case of parsing errors
493 * @see #loadBeanDefinitions
494 * @see #setDocumentReaderClass
495 * @see BeanDefinitionDocumentReader#registerBeanDefinitions
496 */
497 public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
498 // Support old XmlBeanDefinitionParser SPI for backwards-compatibility.
499 if (this.parserClass != null) {
500 XmlBeanDefinitionParser parser =
501 (XmlBeanDefinitionParser) BeanUtils.instantiateClass(this.parserClass);
502 return parser.registerBeanDefinitions(this, doc, resource);
503 }
504 // Read document based on new BeanDefinitionDocumentReader SPI.
505 BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
506 int countBefore = getRegistry().getBeanDefinitionCount();
507 documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
508 return getRegistry().getBeanDefinitionCount() - countBefore;
509 }
510
511 /**
512 * Create the {@link BeanDefinitionDocumentReader} to use for actually
513 * reading bean definitions from an XML document.
514 * <p>Default implementation instantiates the specified "documentReaderClass".
515 * @see #setDocumentReaderClass
516 */
517 protected BeanDefinitionDocumentReader createBeanDefinitionDocumentReader() {
518 return (BeanDefinitionDocumentReader) BeanUtils.instantiateClass(this.documentReaderClass);
519 }
520
521 /**
522 * Create the {@link XmlReaderContext} to pass over to the document reader.
523 */
524 protected XmlReaderContext createReaderContext(Resource resource) {
525 if (this.namespaceHandlerResolver == null) {
526 this.namespaceHandlerResolver = createDefaultNamespaceHandlerResolver();
527 }
528 return new XmlReaderContext(resource, this.problemReporter, this.eventListener,
529 this.sourceExtractor, this, this.namespaceHandlerResolver);
530 }
531
532 /**
533 * Create the default implementation of {@link NamespaceHandlerResolver} used if none is specified.
534 * Default implementation returns an instance of {@link DefaultNamespaceHandlerResolver}.
535 */
536 protected NamespaceHandlerResolver createDefaultNamespaceHandlerResolver() {
537 return new DefaultNamespaceHandlerResolver(getResourceLoader().getClassLoader());
538 }
539
540 }