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