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.net.URISyntaxException;
021import java.util.LinkedHashSet;
022import java.util.Set;
023
024import org.apache.commons.logging.Log;
025import org.apache.commons.logging.LogFactory;
026import org.w3c.dom.Document;
027import org.w3c.dom.Element;
028import org.w3c.dom.Node;
029import org.w3c.dom.NodeList;
030
031import org.springframework.beans.factory.BeanDefinitionStoreException;
032import org.springframework.beans.factory.config.BeanDefinitionHolder;
033import org.springframework.beans.factory.parsing.BeanComponentDefinition;
034import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
035import org.springframework.core.io.Resource;
036import org.springframework.core.io.support.ResourcePatternUtils;
037import org.springframework.lang.Nullable;
038import org.springframework.util.Assert;
039import org.springframework.util.ResourceUtils;
040import org.springframework.util.StringUtils;
041
042/**
043 * Default implementation of the {@link BeanDefinitionDocumentReader} interface that
044 * reads bean definitions according to the "spring-beans" DTD and XSD format
045 * (Spring's default XML bean definition format).
046 *
047 * <p>The structure, elements, and attribute names of the required XML document
048 * are hard-coded in this class. (Of course a transform could be run if necessary
049 * to produce this format). {@code <beans>} does not need to be the root
050 * element of the XML document: this class will parse all bean definition elements
051 * in the XML file, regardless of the actual root element.
052 *
053 * @author Rod Johnson
054 * @author Juergen Hoeller
055 * @author Rob Harrop
056 * @author Erik Wiersma
057 * @since 18.12.2003
058 */
059public class DefaultBeanDefinitionDocumentReader implements BeanDefinitionDocumentReader {
060
061        public static final String BEAN_ELEMENT = BeanDefinitionParserDelegate.BEAN_ELEMENT;
062
063        public static final String NESTED_BEANS_ELEMENT = "beans";
064
065        public static final String ALIAS_ELEMENT = "alias";
066
067        public static final String NAME_ATTRIBUTE = "name";
068
069        public static final String ALIAS_ATTRIBUTE = "alias";
070
071        public static final String IMPORT_ELEMENT = "import";
072
073        public static final String RESOURCE_ATTRIBUTE = "resource";
074
075        public static final String PROFILE_ATTRIBUTE = "profile";
076
077
078        protected final Log logger = LogFactory.getLog(getClass());
079
080        @Nullable
081        private XmlReaderContext readerContext;
082
083        @Nullable
084        private BeanDefinitionParserDelegate delegate;
085
086
087        /**
088         * This implementation parses bean definitions according to the "spring-beans" XSD
089         * (or DTD, historically).
090         * <p>Opens a DOM Document; then initializes the default settings
091         * specified at the {@code <beans/>} level; then parses the contained bean definitions.
092         */
093        @Override
094        public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
095                this.readerContext = readerContext;
096                doRegisterBeanDefinitions(doc.getDocumentElement());
097        }
098
099        /**
100         * Return the descriptor for the XML resource that this parser works on.
101         */
102        protected final XmlReaderContext getReaderContext() {
103                Assert.state(this.readerContext != null, "No XmlReaderContext available");
104                return this.readerContext;
105        }
106
107        /**
108         * Invoke the {@link org.springframework.beans.factory.parsing.SourceExtractor}
109         * to pull the source metadata from the supplied {@link Element}.
110         */
111        @Nullable
112        protected Object extractSource(Element ele) {
113                return getReaderContext().extractSource(ele);
114        }
115
116
117        /**
118         * Register each bean definition within the given root {@code <beans/>} element.
119         */
120        @SuppressWarnings("deprecation")  // for Environment.acceptsProfiles(String...)
121        protected void doRegisterBeanDefinitions(Element root) {
122                // Any nested <beans> elements will cause recursion in this method. In
123                // order to propagate and preserve <beans> default-* attributes correctly,
124                // keep track of the current (parent) delegate, which may be null. Create
125                // the new (child) delegate with a reference to the parent for fallback purposes,
126                // then ultimately reset this.delegate back to its original (parent) reference.
127                // this behavior emulates a stack of delegates without actually necessitating one.
128                BeanDefinitionParserDelegate parent = this.delegate;
129                this.delegate = createDelegate(getReaderContext(), root, parent);
130
131                if (this.delegate.isDefaultNamespace(root)) {
132                        String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
133                        if (StringUtils.hasText(profileSpec)) {
134                                String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
135                                                profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
136                                // We cannot use Profiles.of(...) since profile expressions are not supported
137                                // in XML config. See SPR-12458 for details.
138                                if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
139                                        if (logger.isDebugEnabled()) {
140                                                logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
141                                                                "] not matching: " + getReaderContext().getResource());
142                                        }
143                                        return;
144                                }
145                        }
146                }
147
148                preProcessXml(root);
149                parseBeanDefinitions(root, this.delegate);
150                postProcessXml(root);
151
152                this.delegate = parent;
153        }
154
155        protected BeanDefinitionParserDelegate createDelegate(
156                        XmlReaderContext readerContext, Element root, @Nullable BeanDefinitionParserDelegate parentDelegate) {
157
158                BeanDefinitionParserDelegate delegate = new BeanDefinitionParserDelegate(readerContext);
159                delegate.initDefaults(root, parentDelegate);
160                return delegate;
161        }
162
163        /**
164         * Parse the elements at the root level in the document:
165         * "import", "alias", "bean".
166         * @param root the DOM root element of the document
167         */
168        protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
169                if (delegate.isDefaultNamespace(root)) {
170                        NodeList nl = root.getChildNodes();
171                        for (int i = 0; i < nl.getLength(); i++) {
172                                Node node = nl.item(i);
173                                if (node instanceof Element) {
174                                        Element ele = (Element) node;
175                                        if (delegate.isDefaultNamespace(ele)) {
176                                                parseDefaultElement(ele, delegate);
177                                        }
178                                        else {
179                                                delegate.parseCustomElement(ele);
180                                        }
181                                }
182                        }
183                }
184                else {
185                        delegate.parseCustomElement(root);
186                }
187        }
188
189        private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
190                if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
191                        importBeanDefinitionResource(ele);
192                }
193                else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
194                        processAliasRegistration(ele);
195                }
196                else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
197                        processBeanDefinition(ele, delegate);
198                }
199                else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
200                        // recurse
201                        doRegisterBeanDefinitions(ele);
202                }
203        }
204
205        /**
206         * Parse an "import" element and load the bean definitions
207         * from the given resource into the bean factory.
208         */
209        protected void importBeanDefinitionResource(Element ele) {
210                String location = ele.getAttribute(RESOURCE_ATTRIBUTE);
211                if (!StringUtils.hasText(location)) {
212                        getReaderContext().error("Resource location must not be empty", ele);
213                        return;
214                }
215
216                // Resolve system properties: e.g. "${user.dir}"
217                location = getReaderContext().getEnvironment().resolveRequiredPlaceholders(location);
218
219                Set<Resource> actualResources = new LinkedHashSet<>(4);
220
221                // Discover whether the location is an absolute or relative URI
222                boolean absoluteLocation = false;
223                try {
224                        absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute();
225                }
226                catch (URISyntaxException ex) {
227                        // cannot convert to an URI, considering the location relative
228                        // unless it is the well-known Spring prefix "classpath*:"
229                }
230
231                // Absolute or relative?
232                if (absoluteLocation) {
233                        try {
234                                int importCount = getReaderContext().getReader().loadBeanDefinitions(location, actualResources);
235                                if (logger.isTraceEnabled()) {
236                                        logger.trace("Imported " + importCount + " bean definitions from URL location [" + location + "]");
237                                }
238                        }
239                        catch (BeanDefinitionStoreException ex) {
240                                getReaderContext().error(
241                                                "Failed to import bean definitions from URL location [" + location + "]", ele, ex);
242                        }
243                }
244                else {
245                        // No URL -> considering resource location as relative to the current file.
246                        try {
247                                int importCount;
248                                Resource relativeResource = getReaderContext().getResource().createRelative(location);
249                                if (relativeResource.exists()) {
250                                        importCount = getReaderContext().getReader().loadBeanDefinitions(relativeResource);
251                                        actualResources.add(relativeResource);
252                                }
253                                else {
254                                        String baseLocation = getReaderContext().getResource().getURL().toString();
255                                        importCount = getReaderContext().getReader().loadBeanDefinitions(
256                                                        StringUtils.applyRelativePath(baseLocation, location), actualResources);
257                                }
258                                if (logger.isTraceEnabled()) {
259                                        logger.trace("Imported " + importCount + " bean definitions from relative location [" + location + "]");
260                                }
261                        }
262                        catch (IOException ex) {
263                                getReaderContext().error("Failed to resolve current resource location", ele, ex);
264                        }
265                        catch (BeanDefinitionStoreException ex) {
266                                getReaderContext().error(
267                                                "Failed to import bean definitions from relative location [" + location + "]", ele, ex);
268                        }
269                }
270                Resource[] actResArray = actualResources.toArray(new Resource[0]);
271                getReaderContext().fireImportProcessed(location, actResArray, extractSource(ele));
272        }
273
274        /**
275         * Process the given alias element, registering the alias with the registry.
276         */
277        protected void processAliasRegistration(Element ele) {
278                String name = ele.getAttribute(NAME_ATTRIBUTE);
279                String alias = ele.getAttribute(ALIAS_ATTRIBUTE);
280                boolean valid = true;
281                if (!StringUtils.hasText(name)) {
282                        getReaderContext().error("Name must not be empty", ele);
283                        valid = false;
284                }
285                if (!StringUtils.hasText(alias)) {
286                        getReaderContext().error("Alias must not be empty", ele);
287                        valid = false;
288                }
289                if (valid) {
290                        try {
291                                getReaderContext().getRegistry().registerAlias(name, alias);
292                        }
293                        catch (Exception ex) {
294                                getReaderContext().error("Failed to register alias '" + alias +
295                                                "' for bean with name '" + name + "'", ele, ex);
296                        }
297                        getReaderContext().fireAliasRegistered(name, alias, extractSource(ele));
298                }
299        }
300
301        /**
302         * Process the given bean element, parsing the bean definition
303         * and registering it with the registry.
304         */
305        protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
306                BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
307                if (bdHolder != null) {
308                        bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
309                        try {
310                                // Register the final decorated instance.
311                                BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
312                        }
313                        catch (BeanDefinitionStoreException ex) {
314                                getReaderContext().error("Failed to register bean definition with name '" +
315                                                bdHolder.getBeanName() + "'", ele, ex);
316                        }
317                        // Send registration event.
318                        getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
319                }
320        }
321
322
323        /**
324         * Allow the XML to be extensible by processing any custom element types first,
325         * before we start to process the bean definitions. This method is a natural
326         * extension point for any other custom pre-processing of the XML.
327         * <p>The default implementation is empty. Subclasses can override this method to
328         * convert custom elements into standard Spring bean definitions, for example.
329         * Implementors have access to the parser's bean definition reader and the
330         * underlying XML resource, through the corresponding accessors.
331         * @see #getReaderContext()
332         */
333        protected void preProcessXml(Element root) {
334        }
335
336        /**
337         * Allow the XML to be extensible by processing any custom element types last,
338         * after we finished processing the bean definitions. This method is a natural
339         * extension point for any other custom post-processing of the XML.
340         * <p>The default implementation is empty. Subclasses can override this method to
341         * convert custom elements into standard Spring bean definitions, for example.
342         * Implementors have access to the parser's bean definition reader and the
343         * underlying XML resource, through the corresponding accessors.
344         * @see #getReaderContext()
345         */
346        protected void postProcessXml(Element root) {
347        }
348
349}