Open Source Repository

Home /spring/spring-beans-3.0.5 | Repository Home



org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java
/*
 * Copyright 2002-2008 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.beans.factory.support;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;

import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyAccessor;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.CannotLoadBeanClassException;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.util.DefaultPropertiesPersister;
import org.springframework.util.PropertiesPersister;
import org.springframework.util.StringUtils;

/**
 * Bean definition reader for a simple properties format.
 *
 <p>Provides bean definition registration methods for Map/Properties and
 * ResourceBundle. Typically applied to a DefaultListableBeanFactory.
 *
 <p><b>Example:</b>
 *
 <pre class="code">
 * employee.(class)=MyClass       // bean is of class MyClass
 * employee.(abstract)=true       // this bean can't be instantiated directly
 * employee.group=Insurance       // real property
 * employee.usesDialUp=false      // real property (potentially overridden)
 *
 * salesrep.(parent)=employee     // derives from "employee" bean definition
 * salesrep.(lazy-init)=true      // lazily initialize this singleton bean
 * salesrep.manager(ref)=tony     // reference to another bean
 * salesrep.department=Sales      // real property
 *
 * techie.(parent)=employee       // derives from "employee" bean definition
 * techie.(scope)=prototype       // bean is a prototype (not a shared instance)
 * techie.manager(ref)=jeff       // reference to another bean
 * techie.department=Engineering  // real property
 * techie.usesDialUp=true         // real property (overriding parent value)
 *
 * ceo.$0(ref)=secretary          // inject 'secretary' bean as 0th constructor arg
 * ceo.$1=1000000                 // inject value '1000000' at 1st constructor arg
 </pre>
 
 @author Rod Johnson
 @author Juergen Hoeller
 @author Rob Harrop
 @since 26.11.2003
 @see DefaultListableBeanFactory
 */
public class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader {

  /**
   * Value of a T/F attribute that represents true.
   * Anything else represents false. Case seNsItive.
   */
  public static final String TRUE_VALUE = "true";

  /**
   * Separator between bean name and property name.
   * We follow normal Java conventions.
   */
  public static final String SEPARATOR = ".";

  /**
   * Special key to distinguish <code>owner.(class)=com.myapp.MyClass</code>-
   */
  public static final String CLASS_KEY = "(class)";

  /**
   * Special key to distinguish <code>owner.(parent)=parentBeanName</code>.
   */
  public static final String PARENT_KEY = "(parent)";

  /**
   * Special key to distinguish <code>owner.(scope)=prototype</code>.
   * Default is "true".
   */
  public static final String SCOPE_KEY = "(scope)";

  /**
   * Special key to distinguish <code>owner.(singleton)=false</code>.
   * Default is "true".
   */
  public static final String SINGLETON_KEY = "(singleton)";

  /**
   * Special key to distinguish <code>owner.(abstract)=true</code>
   * Default is "false".
   */
  public static final String ABSTRACT_KEY = "(abstract)";

  /**
   * Special key to distinguish <code>owner.(lazy-init)=true</code>
   * Default is "false".
   */
  public static final String LAZY_INIT_KEY = "(lazy-init)";

  /**
   * Property suffix for references to other beans in the current
   * BeanFactory: e.g. <code>owner.dog(ref)=fido</code>.
   * Whether this is a reference to a singleton or a prototype
   * will depend on the definition of the target bean.
   */
  public static final String REF_SUFFIX = "(ref)";

  /**
   * Prefix before values referencing other beans.
   */
  public static final String REF_PREFIX = "*";

  /**
   * Prefix used to denote a constructor argument definition.
   */
  public static final String CONSTRUCTOR_ARG_PREFIX = "$";


  private String defaultParentBean;

  private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister();


  /**
   * Create new PropertiesBeanDefinitionReader for the given bean factory.
   @param registry the BeanFactory to load bean definitions into,
   * in the form of a BeanDefinitionRegistry
   */
  public PropertiesBeanDefinitionReader(BeanDefinitionRegistry registry) {
    super(registry);
  }


  /**
   * Set the default parent bean for this bean factory.
   * If a child bean definition handled by this factory provides neither
   * a parent nor a class attribute, this default value gets used.
   <p>Can be used e.g. for view definition files, to define a parent
   * with a default view class and common attributes for all views.
   * View definitions that define their own parent or carry their own
   * class can still override this.
   <p>Strictly speaking, the rule that a default parent setting does
   * not apply to a bean definition that carries a class is there for
   * backwards compatiblity reasons. It still matches the typical use case.
   */
  public void setDefaultParentBean(String defaultParentBean) {
    this.defaultParentBean = defaultParentBean;
  }

  /**
   * Return the default parent bean for this bean factory.
   */
  public String getDefaultParentBean() {
    return this.defaultParentBean;
  }

  /**
   * Set the PropertiesPersister to use for parsing properties files.
   * The default is DefaultPropertiesPersister.
   @see org.springframework.util.DefaultPropertiesPersister
   */
  public void setPropertiesPersister(PropertiesPersister propertiesPersister) {
    this.propertiesPersister =
        (propertiesPersister != null ? propertiesPersister : new DefaultPropertiesPersister());
  }

  /**
   * Return the PropertiesPersister to use for parsing properties files.
   */
  public PropertiesPersister getPropertiesPersister() {
    return this.propertiesPersister;
  }


  /**
   * Load bean definitions from the specified properties file,
   * using all property keys (i.e. not filtering by prefix).
   @param resource the resource descriptor for the properties file
   @return the number of bean definitions found
   @throws BeanDefinitionStoreException in case of loading or parsing errors
   @see #loadBeanDefinitions(org.springframework.core.io.Resource, String)
   */
  public int loadBeanDefinitions(Resource resourcethrows BeanDefinitionStoreException {
    return loadBeanDefinitions(new EncodedResource(resource)null);
  }

  /**
   * Load bean definitions from the specified properties file.
   @param resource the resource descriptor for the properties file
   @param prefix a filter within the keys in the map: e.g. 'beans.'
   * (can be empty or <code>null</code>)
   @return the number of bean definitions found
   @throws BeanDefinitionStoreException in case of loading or parsing errors
   */
  public int loadBeanDefinitions(Resource resource, String prefixthrows BeanDefinitionStoreException {
    return loadBeanDefinitions(new EncodedResource(resource), prefix);
  }

  /**
   * Load bean definitions from the specified properties file.
   @param encodedResource the resource descriptor for the properties file,
   * allowing to specify an encoding to use for parsing the file
   @return the number of bean definitions found
   @throws BeanDefinitionStoreException in case of loading or parsing errors
   */
  public int loadBeanDefinitions(EncodedResource encodedResourcethrows BeanDefinitionStoreException {
    return loadBeanDefinitions(encodedResource, null);
  }

  /**
   * Load bean definitions from the specified properties file.
   @param encodedResource the resource descriptor for the properties file,
   * allowing to specify an encoding to use for parsing the file
   @param prefix a filter within the keys in the map: e.g. 'beans.'
   * (can be empty or <code>null</code>)
   @return the number of bean definitions found
   @throws BeanDefinitionStoreException in case of loading or parsing errors
   */
  public int loadBeanDefinitions(EncodedResource encodedResource, String prefix)
      throws BeanDefinitionStoreException {

    Properties props = new Properties();
    try {
      InputStream is = encodedResource.getResource().getInputStream();
      try {
        if (encodedResource.getEncoding() != null) {
          getPropertiesPersister().load(props, new InputStreamReader(is, encodedResource.getEncoding()));
        }
        else {
          getPropertiesPersister().load(props, is);
        }
      }
      finally {
        is.close();
      }
      return registerBeanDefinitions(props, prefix, encodedResource.getResource().getDescription());
    }
    catch (IOException ex) {
      throw new BeanDefinitionStoreException("Could not parse properties from " + encodedResource.getResource(), ex);
    }
  }

  /**
   * Register bean definitions contained in a resource bundle,
   * using all property keys (i.e. not filtering by prefix).
   @param rb the ResourceBundle to load from
   @return the number of bean definitions found
   @throws BeanDefinitionStoreException in case of loading or parsing errors
   @see #registerBeanDefinitions(java.util.ResourceBundle, String)
   */
  public int registerBeanDefinitions(ResourceBundle rbthrows BeanDefinitionStoreException {
    return registerBeanDefinitions(rb, null);
  }

  /**
   * Register bean definitions contained in a ResourceBundle.
   <p>Similar syntax as for a Map. This method is useful to enable
   * standard Java internationalization support.
   @param rb the ResourceBundle to load from
   @param prefix a filter within the keys in the map: e.g. 'beans.'
   * (can be empty or <code>null</code>)
   @return the number of bean definitions found
   @throws BeanDefinitionStoreException in case of loading or parsing errors
   */
  public int registerBeanDefinitions(ResourceBundle rb, String prefixthrows BeanDefinitionStoreException {
    // Simply create a map and call overloaded method.
    Map<String, Object> map = new HashMap<String, Object>();
    Enumeration keys = rb.getKeys();
    while (keys.hasMoreElements()) {
      String key = (Stringkeys.nextElement();
      map.put(key, rb.getObject(key));
    }
    return registerBeanDefinitions(map, prefix);
  }


  /**
   * Register bean definitions contained in a Map,
   * using all property keys (i.e. not filtering by prefix).
   @param map Map: name -> property (String or Object). Property values
   * will be strings if coming from a Properties file etc. Property names
   * (keys) <b>must</b> be Strings. Class keys must be Strings.
   @return the number of bean definitions found
   @throws BeansException in case of loading or parsing errors
   @see #registerBeanDefinitions(java.util.Map, String, String)
   */
  public int registerBeanDefinitions(Map mapthrows BeansException {
    return registerBeanDefinitions(map, null);
  }

  /**
   * Register bean definitions contained in a Map.
   * Ignore ineligible properties.
   @param map Map name -> property (String or Object). Property values
   * will be strings if coming from a Properties file etc. Property names
   * (keys) <b>must</b> be Strings. Class keys must be Strings.
   @param prefix a filter within the keys in the map: e.g. 'beans.'
   * (can be empty or <code>null</code>)
   @return the number of bean definitions found
   @throws BeansException in case of loading or parsing errors
   */
  public int registerBeanDefinitions(Map map, String prefixthrows BeansException {
    return registerBeanDefinitions(map, prefix, "Map " + map);
  }

  /**
   * Register bean definitions contained in a Map.
   * Ignore ineligible properties.
   @param map Map name -> property (String or Object). Property values
   * will be strings if coming from a Properties file etc. Property names
   * (keys) <b>must</b> be strings. Class keys must be Strings.
   @param prefix a filter within the keys in the map: e.g. 'beans.'
   * (can be empty or <code>null</code>)
   @param resourceDescription description of the resource that the
   * Map came from (for logging purposes)
   @return the number of bean definitions found
   @throws BeansException in case of loading or parsing errors
   @see #registerBeanDefinitions(Map, String)
   */
  public int registerBeanDefinitions(Map map, String prefix, String resourceDescription)
      throws BeansException {

    if (prefix == null) {
      prefix = "";
    }
    int beanCount = 0;

    for (Object key : map.keySet()) {
      if (!(key instanceof String)) {
        throw new IllegalArgumentException("Illegal key [" + key + "]: only Strings allowed");
      }
      String keyString = (Stringkey;
      if (keyString.startsWith(prefix)) {
        // Key is of form: prefix<name>.property
        String nameAndProperty = keyString.substring(prefix.length());
        // Find dot before property name, ignoring dots in property keys.
        int sepIdx = -1;
        int propKeyIdx = nameAndProperty.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX);
        if (propKeyIdx != -1) {
          sepIdx = nameAndProperty.lastIndexOf(SEPARATOR, propKeyIdx);
        }
        else {
          sepIdx = nameAndProperty.lastIndexOf(SEPARATOR);
        }
        if (sepIdx != -1) {
          String beanName = nameAndProperty.substring(0, sepIdx);
          if (logger.isDebugEnabled()) {
            logger.debug("Found bean name '" + beanName + "'");
          }
          if (!getRegistry().containsBeanDefinition(beanName)) {
            // If we haven't already registered it...
            registerBeanDefinition(beanName, map, prefix + beanName, resourceDescription);
            ++beanCount;
          }
        }
        else {
          // Ignore it: It wasn't a valid bean name and property,
          // although it did start with the required prefix.
          if (logger.isDebugEnabled()) {
            logger.debug("Invalid bean name and property [" + nameAndProperty + "]");
          }
        }
      }
    }

    return beanCount;
  }

  /**
   * Get all property values, given a prefix (which will be stripped)
   * and add the bean they define to the factory with the given name
   @param beanName name of the bean to define
   @param map Map containing string pairs
   @param prefix prefix of each entry, which will be stripped
   @param resourceDescription description of the resource that the
   * Map came from (for logging purposes)
   @throws BeansException if the bean definition could not be parsed or registered
   */
  protected void registerBeanDefinition(String beanName, Map<?, ?> map, String prefix, String resourceDescription)
      throws BeansException {

    String className = null;
    String parent = null;
    String scope = GenericBeanDefinition.SCOPE_SINGLETON;
    boolean isAbstract = false;
    boolean lazyInit = false;

    ConstructorArgumentValues cas = new ConstructorArgumentValues();
    MutablePropertyValues pvs = new MutablePropertyValues();

    for (Map.Entry entry : map.entrySet()) {
      String key = StringUtils.trimWhitespace((Stringentry.getKey());
      if (key.startsWith(prefix + SEPARATOR)) {
        String property = key.substring(prefix.length() + SEPARATOR.length());
        if (CLASS_KEY.equals(property)) {
          className = StringUtils.trimWhitespace((Stringentry.getValue());
        }
        else if (PARENT_KEY.equals(property)) {
          parent = StringUtils.trimWhitespace((Stringentry.getValue());
        }
        else if (ABSTRACT_KEY.equals(property)) {
          String val = StringUtils.trimWhitespace((Stringentry.getValue());
          isAbstract = TRUE_VALUE.equals(val);
        }
        else if (SCOPE_KEY.equals(property)) {
          // Spring 2.0 style
          scope = StringUtils.trimWhitespace((Stringentry.getValue());
        }
        else if (SINGLETON_KEY.equals(property)) {
          // Spring 1.2 style
          String val = StringUtils.trimWhitespace((Stringentry.getValue());
          scope = ((val == null || TRUE_VALUE.equals(val? GenericBeanDefinition.SCOPE_SINGLETON :
              GenericBeanDefinition.SCOPE_PROTOTYPE));
        }
        else if (LAZY_INIT_KEY.equals(property)) {
          String val = StringUtils.trimWhitespace((Stringentry.getValue());
          lazyInit = TRUE_VALUE.equals(val);
        }
        else if (property.startsWith(CONSTRUCTOR_ARG_PREFIX)) {
          if (property.endsWith(REF_SUFFIX)) {
            int index = Integer.parseInt(property.substring(1, property.length() - REF_SUFFIX.length()));
            cas.addIndexedArgumentValue(index, new RuntimeBeanReference(entry.getValue().toString()));
          }
          else {
            int index = Integer.parseInt(property.substring(1));
            cas.addIndexedArgumentValue(index, readValue(entry));
          }
        }
        else if (property.endsWith(REF_SUFFIX)) {
          // This isn't a real property, but a reference to another prototype
          // Extract property name: property is of form dog(ref)
          property = property.substring(0, property.length() - REF_SUFFIX.length());
          String ref = StringUtils.trimWhitespace((Stringentry.getValue());

          // It doesn't matter if the referenced bean hasn't yet been registered:
          // this will ensure that the reference is resolved at runtime.
          Object val = new RuntimeBeanReference(ref);
          pvs.add(property, val);
        }
        else {
          // It's a normal bean property.
          pvs.add(property, readValue(entry));
        }
      }
    }

    if (logger.isDebugEnabled()) {
      logger.debug("Registering bean definition for bean name '" + beanName + "' with " + pvs);
    }

    // Just use default parent if we're not dealing with the parent itself,
    // and if there's no class name specified. The latter has to happen for
    // backwards compatibility reasons.
    if (parent == null && className == null && !beanName.equals(this.defaultParentBean)) {
      parent = this.defaultParentBean;
    }

    try {
      AbstractBeanDefinition bd = BeanDefinitionReaderUtils.createBeanDefinition(
          parent, className, getBeanClassLoader());
      bd.setScope(scope);
      bd.setAbstract(isAbstract);
      bd.setLazyInit(lazyInit);
      bd.setConstructorArgumentValues(cas);
      bd.setPropertyValues(pvs);
      getRegistry().registerBeanDefinition(beanName, bd);
    }
    catch (ClassNotFoundException ex) {
      throw new CannotLoadBeanClassException(resourceDescription, beanName, className, ex);
    }
    catch (LinkageError err) {
      throw new CannotLoadBeanClassException(resourceDescription, beanName, className, err);
    }
  }

  /**
   * Reads the value of the entry. Correctly interprets bean references for
   * values that are prefixed with an asterisk.
   */
  private Object readValue(Map.Entry entry) {
    Object val = entry.getValue();
    if (val instanceof String) {
      String strVal = (Stringval;
      // If it starts with a reference prefix...
      if (strVal.startsWith(REF_PREFIX)) {
        // Expand the reference.
        String targetName = strVal.substring(1);
        if (targetName.startsWith(REF_PREFIX)) {
          // Escaped prefix -> use plain value.
          val = targetName;
        }
        else {
          val = new RuntimeBeanReference(targetName);
        }
      }
    }
    return val;
  }

}