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