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