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}