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.support;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.InputStreamReader;
022import java.util.Enumeration;
023import java.util.HashMap;
024import java.util.Map;
025import java.util.Properties;
026import java.util.ResourceBundle;
027
028import org.springframework.beans.BeansException;
029import org.springframework.beans.MutablePropertyValues;
030import org.springframework.beans.PropertyAccessor;
031import org.springframework.beans.factory.BeanDefinitionStoreException;
032import org.springframework.beans.factory.CannotLoadBeanClassException;
033import org.springframework.beans.factory.config.BeanDefinition;
034import org.springframework.beans.factory.config.ConstructorArgumentValues;
035import org.springframework.beans.factory.config.RuntimeBeanReference;
036import org.springframework.core.io.Resource;
037import org.springframework.core.io.support.EncodedResource;
038import org.springframework.lang.Nullable;
039import org.springframework.util.DefaultPropertiesPersister;
040import org.springframework.util.PropertiesPersister;
041import org.springframework.util.StringUtils;
042
043/**
044 * Bean definition reader for a simple properties format.
045 *
046 * <p>Provides bean definition registration methods for Map/Properties and
047 * ResourceBundle. Typically applied to a DefaultListableBeanFactory.
048 *
049 * <p><b>Example:</b>
050 *
051 * <pre class="code">
052 * employee.(class)=MyClass       // bean is of class MyClass
053 * employee.(abstract)=true       // this bean can't be instantiated directly
054 * employee.group=Insurance       // real property
055 * employee.usesDialUp=false      // real property (potentially overridden)
056 *
057 * salesrep.(parent)=employee     // derives from "employee" bean definition
058 * salesrep.(lazy-init)=true      // lazily initialize this singleton bean
059 * salesrep.manager(ref)=tony     // reference to another bean
060 * salesrep.department=Sales      // real property
061 *
062 * techie.(parent)=employee       // derives from "employee" bean definition
063 * techie.(scope)=prototype       // bean is a prototype (not a shared instance)
064 * techie.manager(ref)=jeff       // reference to another bean
065 * techie.department=Engineering  // real property
066 * techie.usesDialUp=true         // real property (overriding parent value)
067 *
068 * ceo.$0(ref)=secretary          // inject 'secretary' bean as 0th constructor arg
069 * ceo.$1=1000000                 // inject value '1000000' at 1st constructor arg
070 * </pre>
071 *
072 * @author Rod Johnson
073 * @author Juergen Hoeller
074 * @author Rob Harrop
075 * @since 26.11.2003
076 * @see DefaultListableBeanFactory
077 */
078public class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader {
079
080        /**
081         * Value of a T/F attribute that represents true.
082         * Anything else represents false. Case seNsItive.
083         */
084        public static final String TRUE_VALUE = "true";
085
086        /**
087         * Separator between bean name and property name.
088         * We follow normal Java conventions.
089         */
090        public static final String SEPARATOR = ".";
091
092        /**
093         * Special key to distinguish {@code owner.(class)=com.myapp.MyClass}.
094         */
095        public static final String CLASS_KEY = "(class)";
096
097        /**
098         * Special key to distinguish {@code owner.(parent)=parentBeanName}.
099         */
100        public static final String PARENT_KEY = "(parent)";
101
102        /**
103         * Special key to distinguish {@code owner.(scope)=prototype}.
104         * Default is "true".
105         */
106        public static final String SCOPE_KEY = "(scope)";
107
108        /**
109         * Special key to distinguish {@code owner.(singleton)=false}.
110         * Default is "true".
111         */
112        public static final String SINGLETON_KEY = "(singleton)";
113
114        /**
115         * Special key to distinguish {@code owner.(abstract)=true}
116         * Default is "false".
117         */
118        public static final String ABSTRACT_KEY = "(abstract)";
119
120        /**
121         * Special key to distinguish {@code owner.(lazy-init)=true}
122         * Default is "false".
123         */
124        public static final String LAZY_INIT_KEY = "(lazy-init)";
125
126        /**
127         * Property suffix for references to other beans in the current
128         * BeanFactory: e.g. {@code owner.dog(ref)=fido}.
129         * Whether this is a reference to a singleton or a prototype
130         * will depend on the definition of the target bean.
131         */
132        public static final String REF_SUFFIX = "(ref)";
133
134        /**
135         * Prefix before values referencing other beans.
136         */
137        public static final String REF_PREFIX = "*";
138
139        /**
140         * Prefix used to denote a constructor argument definition.
141         */
142        public static final String CONSTRUCTOR_ARG_PREFIX = "$";
143
144
145        @Nullable
146        private String defaultParentBean;
147
148        private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister();
149
150
151        /**
152         * Create new PropertiesBeanDefinitionReader for the given bean factory.
153         * @param registry the BeanFactory to load bean definitions into,
154         * in the form of a BeanDefinitionRegistry
155         */
156        public PropertiesBeanDefinitionReader(BeanDefinitionRegistry registry) {
157                super(registry);
158        }
159
160
161        /**
162         * Set the default parent bean for this bean factory.
163         * If a child bean definition handled by this factory provides neither
164         * a parent nor a class attribute, this default value gets used.
165         * <p>Can be used e.g. for view definition files, to define a parent
166         * with a default view class and common attributes for all views.
167         * View definitions that define their own parent or carry their own
168         * class can still override this.
169         * <p>Strictly speaking, the rule that a default parent setting does
170         * not apply to a bean definition that carries a class is there for
171         * backwards compatibility reasons. It still matches the typical use case.
172         */
173        public void setDefaultParentBean(@Nullable String defaultParentBean) {
174                this.defaultParentBean = defaultParentBean;
175        }
176
177        /**
178         * Return the default parent bean for this bean factory.
179         */
180        @Nullable
181        public String getDefaultParentBean() {
182                return this.defaultParentBean;
183        }
184
185        /**
186         * Set the PropertiesPersister to use for parsing properties files.
187         * The default is DefaultPropertiesPersister.
188         * @see org.springframework.util.DefaultPropertiesPersister
189         */
190        public void setPropertiesPersister(@Nullable PropertiesPersister propertiesPersister) {
191                this.propertiesPersister =
192                                (propertiesPersister != null ? propertiesPersister : new DefaultPropertiesPersister());
193        }
194
195        /**
196         * Return the PropertiesPersister to use for parsing properties files.
197         */
198        public PropertiesPersister getPropertiesPersister() {
199                return this.propertiesPersister;
200        }
201
202
203        /**
204         * Load bean definitions from the specified properties file,
205         * using all property keys (i.e. not filtering by prefix).
206         * @param resource the resource descriptor for the properties file
207         * @return the number of bean definitions found
208         * @throws BeanDefinitionStoreException in case of loading or parsing errors
209         * @see #loadBeanDefinitions(org.springframework.core.io.Resource, String)
210         */
211        @Override
212        public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
213                return loadBeanDefinitions(new EncodedResource(resource), null);
214        }
215
216        /**
217         * Load bean definitions from the specified properties file.
218         * @param resource the resource descriptor for the properties file
219         * @param prefix a filter within the keys in the map: e.g. 'beans.'
220         * (can be empty or {@code null})
221         * @return the number of bean definitions found
222         * @throws BeanDefinitionStoreException in case of loading or parsing errors
223         */
224        public int loadBeanDefinitions(Resource resource, @Nullable String prefix) throws BeanDefinitionStoreException {
225                return loadBeanDefinitions(new EncodedResource(resource), prefix);
226        }
227
228        /**
229         * Load bean definitions from the specified properties file.
230         * @param encodedResource the resource descriptor for the properties file,
231         * allowing to specify an encoding to use for parsing the file
232         * @return the number of bean definitions found
233         * @throws BeanDefinitionStoreException in case of loading or parsing errors
234         */
235        public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
236                return loadBeanDefinitions(encodedResource, null);
237        }
238
239        /**
240         * Load bean definitions from the specified properties file.
241         * @param encodedResource the resource descriptor for the properties file,
242         * allowing to specify an encoding to use for parsing the file
243         * @param prefix a filter within the keys in the map: e.g. 'beans.'
244         * (can be empty or {@code null})
245         * @return the number of bean definitions found
246         * @throws BeanDefinitionStoreException in case of loading or parsing errors
247         */
248        public int loadBeanDefinitions(EncodedResource encodedResource, @Nullable String prefix)
249                        throws BeanDefinitionStoreException {
250
251                if (logger.isTraceEnabled()) {
252                        logger.trace("Loading properties bean definitions from " + encodedResource);
253                }
254
255                Properties props = new Properties();
256                try {
257                        try (InputStream is = encodedResource.getResource().getInputStream()) {
258                                if (encodedResource.getEncoding() != null) {
259                                        getPropertiesPersister().load(props, new InputStreamReader(is, encodedResource.getEncoding()));
260                                }
261                                else {
262                                        getPropertiesPersister().load(props, is);
263                                }
264                        }
265
266                        int count = registerBeanDefinitions(props, prefix, encodedResource.getResource().getDescription());
267                        if (logger.isDebugEnabled()) {
268                                logger.debug("Loaded " + count + " bean definitions from " + encodedResource);
269                        }
270                        return count;
271                }
272                catch (IOException ex) {
273                        throw new BeanDefinitionStoreException("Could not parse properties from " + encodedResource.getResource(), ex);
274                }
275        }
276
277        /**
278         * Register bean definitions contained in a resource bundle,
279         * using all property keys (i.e. not filtering by prefix).
280         * @param rb the ResourceBundle to load from
281         * @return the number of bean definitions found
282         * @throws BeanDefinitionStoreException in case of loading or parsing errors
283         * @see #registerBeanDefinitions(java.util.ResourceBundle, String)
284         */
285        public int registerBeanDefinitions(ResourceBundle rb) throws BeanDefinitionStoreException {
286                return registerBeanDefinitions(rb, null);
287        }
288
289        /**
290         * Register bean definitions contained in a ResourceBundle.
291         * <p>Similar syntax as for a Map. This method is useful to enable
292         * standard Java internationalization support.
293         * @param rb the ResourceBundle to load from
294         * @param prefix a filter within the keys in the map: e.g. 'beans.'
295         * (can be empty or {@code null})
296         * @return the number of bean definitions found
297         * @throws BeanDefinitionStoreException in case of loading or parsing errors
298         */
299        public int registerBeanDefinitions(ResourceBundle rb, @Nullable String prefix) throws BeanDefinitionStoreException {
300                // Simply create a map and call overloaded method.
301                Map<String, Object> map = new HashMap<>();
302                Enumeration<String> keys = rb.getKeys();
303                while (keys.hasMoreElements()) {
304                        String key = keys.nextElement();
305                        map.put(key, rb.getObject(key));
306                }
307                return registerBeanDefinitions(map, prefix);
308        }
309
310
311        /**
312         * Register bean definitions contained in a Map, using all property keys (i.e. not
313         * filtering by prefix).
314         * @param map a map of {@code name} to {@code property} (String or Object). Property
315         * values will be strings if coming from a Properties file etc. Property names
316         * (keys) <b>must</b> be Strings. Class keys must be Strings.
317         * @return the number of bean definitions found
318         * @throws BeansException in case of loading or parsing errors
319         * @see #registerBeanDefinitions(java.util.Map, String, String)
320         */
321        public int registerBeanDefinitions(Map<?, ?> map) throws BeansException {
322                return registerBeanDefinitions(map, null);
323        }
324
325        /**
326         * Register bean definitions contained in a Map.
327         * Ignore ineligible properties.
328         * @param map a map of {@code name} to {@code property} (String or Object). Property
329         * values will be strings if coming from a Properties file etc. Property names
330         * (keys) <b>must</b> be Strings. Class keys must be Strings.
331         * @param prefix a filter within the keys in the map: e.g. 'beans.'
332         * (can be empty or {@code null})
333         * @return the number of bean definitions found
334         * @throws BeansException in case of loading or parsing errors
335         */
336        public int registerBeanDefinitions(Map<?, ?> map, @Nullable String prefix) throws BeansException {
337                return registerBeanDefinitions(map, prefix, "Map " + map);
338        }
339
340        /**
341         * Register bean definitions contained in a Map.
342         * Ignore ineligible properties.
343         * @param map a map of {@code name} to {@code property} (String or Object). Property
344         * values will be strings if coming from a Properties file etc. Property names
345         * (keys) <b>must</b> be Strings. Class keys must be Strings.
346         * @param prefix a filter within the keys in the map: e.g. 'beans.'
347         * (can be empty or {@code null})
348         * @param resourceDescription description of the resource that the
349         * Map came from (for logging purposes)
350         * @return the number of bean definitions found
351         * @throws BeansException in case of loading or parsing errors
352         * @see #registerBeanDefinitions(Map, String)
353         */
354        public int registerBeanDefinitions(Map<?, ?> map, @Nullable String prefix, String resourceDescription)
355                        throws BeansException {
356
357                if (prefix == null) {
358                        prefix = "";
359                }
360                int beanCount = 0;
361
362                for (Object key : map.keySet()) {
363                        if (!(key instanceof String)) {
364                                throw new IllegalArgumentException("Illegal key [" + key + "]: only Strings allowed");
365                        }
366                        String keyString = (String) key;
367                        if (keyString.startsWith(prefix)) {
368                                // Key is of form: prefix<name>.property
369                                String nameAndProperty = keyString.substring(prefix.length());
370                                // Find dot before property name, ignoring dots in property keys.
371                                int sepIdx ;
372                                int propKeyIdx = nameAndProperty.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX);
373                                if (propKeyIdx != -1) {
374                                        sepIdx = nameAndProperty.lastIndexOf(SEPARATOR, propKeyIdx);
375                                }
376                                else {
377                                        sepIdx = nameAndProperty.lastIndexOf(SEPARATOR);
378                                }
379                                if (sepIdx != -1) {
380                                        String beanName = nameAndProperty.substring(0, sepIdx);
381                                        if (logger.isTraceEnabled()) {
382                                                logger.trace("Found bean name '" + beanName + "'");
383                                        }
384                                        if (!getRegistry().containsBeanDefinition(beanName)) {
385                                                // If we haven't already registered it...
386                                                registerBeanDefinition(beanName, map, prefix + beanName, resourceDescription);
387                                                ++beanCount;
388                                        }
389                                }
390                                else {
391                                        // Ignore it: It wasn't a valid bean name and property,
392                                        // although it did start with the required prefix.
393                                        if (logger.isDebugEnabled()) {
394                                                logger.debug("Invalid bean name and property [" + nameAndProperty + "]");
395                                        }
396                                }
397                        }
398                }
399
400                return beanCount;
401        }
402
403        /**
404         * Get all property values, given a prefix (which will be stripped)
405         * and add the bean they define to the factory with the given name.
406         * @param beanName name of the bean to define
407         * @param map a Map containing string pairs
408         * @param prefix prefix of each entry, which will be stripped
409         * @param resourceDescription description of the resource that the
410         * Map came from (for logging purposes)
411         * @throws BeansException if the bean definition could not be parsed or registered
412         */
413        protected void registerBeanDefinition(String beanName, Map<?, ?> map, String prefix, String resourceDescription)
414                        throws BeansException {
415
416                String className = null;
417                String parent = null;
418                String scope = BeanDefinition.SCOPE_SINGLETON;
419                boolean isAbstract = false;
420                boolean lazyInit = false;
421
422                ConstructorArgumentValues cas = new ConstructorArgumentValues();
423                MutablePropertyValues pvs = new MutablePropertyValues();
424
425                String prefixWithSep = prefix + SEPARATOR;
426                int beginIndex = prefixWithSep.length();
427
428                for (Map.Entry<?, ?> entry : map.entrySet()) {
429                        String key = StringUtils.trimWhitespace((String) entry.getKey());
430                        if (key.startsWith(prefixWithSep)) {
431                                String property = key.substring(beginIndex);
432                                if (CLASS_KEY.equals(property)) {
433                                        className = StringUtils.trimWhitespace((String) entry.getValue());
434                                }
435                                else if (PARENT_KEY.equals(property)) {
436                                        parent = StringUtils.trimWhitespace((String) entry.getValue());
437                                }
438                                else if (ABSTRACT_KEY.equals(property)) {
439                                        String val = StringUtils.trimWhitespace((String) entry.getValue());
440                                        isAbstract = TRUE_VALUE.equals(val);
441                                }
442                                else if (SCOPE_KEY.equals(property)) {
443                                        // Spring 2.0 style
444                                        scope = StringUtils.trimWhitespace((String) entry.getValue());
445                                }
446                                else if (SINGLETON_KEY.equals(property)) {
447                                        // Spring 1.2 style
448                                        String val = StringUtils.trimWhitespace((String) entry.getValue());
449                                        scope = (!StringUtils.hasLength(val) || TRUE_VALUE.equals(val) ?
450                                                        BeanDefinition.SCOPE_SINGLETON : BeanDefinition.SCOPE_PROTOTYPE);
451                                }
452                                else if (LAZY_INIT_KEY.equals(property)) {
453                                        String val = StringUtils.trimWhitespace((String) entry.getValue());
454                                        lazyInit = TRUE_VALUE.equals(val);
455                                }
456                                else if (property.startsWith(CONSTRUCTOR_ARG_PREFIX)) {
457                                        if (property.endsWith(REF_SUFFIX)) {
458                                                int index = Integer.parseInt(property.substring(1, property.length() - REF_SUFFIX.length()));
459                                                cas.addIndexedArgumentValue(index, new RuntimeBeanReference(entry.getValue().toString()));
460                                        }
461                                        else {
462                                                int index = Integer.parseInt(property.substring(1));
463                                                cas.addIndexedArgumentValue(index, readValue(entry));
464                                        }
465                                }
466                                else if (property.endsWith(REF_SUFFIX)) {
467                                        // This isn't a real property, but a reference to another prototype
468                                        // Extract property name: property is of form dog(ref)
469                                        property = property.substring(0, property.length() - REF_SUFFIX.length());
470                                        String ref = StringUtils.trimWhitespace((String) entry.getValue());
471
472                                        // It doesn't matter if the referenced bean hasn't yet been registered:
473                                        // this will ensure that the reference is resolved at runtime.
474                                        Object val = new RuntimeBeanReference(ref);
475                                        pvs.add(property, val);
476                                }
477                                else {
478                                        // It's a normal bean property.
479                                        pvs.add(property, readValue(entry));
480                                }
481                        }
482                }
483
484                if (logger.isTraceEnabled()) {
485                        logger.trace("Registering bean definition for bean name '" + beanName + "' with " + pvs);
486                }
487
488                // Just use default parent if we're not dealing with the parent itself,
489                // and if there's no class name specified. The latter has to happen for
490                // backwards compatibility reasons.
491                if (parent == null && className == null && !beanName.equals(this.defaultParentBean)) {
492                        parent = this.defaultParentBean;
493                }
494
495                try {
496                        AbstractBeanDefinition bd = BeanDefinitionReaderUtils.createBeanDefinition(
497                                        parent, className, getBeanClassLoader());
498                        bd.setScope(scope);
499                        bd.setAbstract(isAbstract);
500                        bd.setLazyInit(lazyInit);
501                        bd.setConstructorArgumentValues(cas);
502                        bd.setPropertyValues(pvs);
503                        getRegistry().registerBeanDefinition(beanName, bd);
504                }
505                catch (ClassNotFoundException ex) {
506                        throw new CannotLoadBeanClassException(resourceDescription, beanName, className, ex);
507                }
508                catch (LinkageError err) {
509                        throw new CannotLoadBeanClassException(resourceDescription, beanName, className, err);
510                }
511        }
512
513        /**
514         * Reads the value of the entry. Correctly interprets bean references for
515         * values that are prefixed with an asterisk.
516         */
517        private Object readValue(Map.Entry<?, ?> entry) {
518                Object val = entry.getValue();
519                if (val instanceof String) {
520                        String strVal = (String) val;
521                        // If it starts with a reference prefix...
522                        if (strVal.startsWith(REF_PREFIX)) {
523                                // Expand the reference.
524                                String targetName = strVal.substring(1);
525                                if (targetName.startsWith(REF_PREFIX)) {
526                                        // Escaped prefix -> use plain value.
527                                        val = targetName;
528                                }
529                                else {
530                                        val = new RuntimeBeanReference(targetName);
531                                }
532                        }
533                }
534                return val;
535        }
536
537}