001/* 002 * Copyright 2012-2017 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 017package org.springframework.boot.bind; 018 019import java.beans.PropertyDescriptor; 020import java.util.HashSet; 021import java.util.LinkedHashSet; 022import java.util.Locale; 023import java.util.Map; 024import java.util.Properties; 025import java.util.Set; 026 027import org.apache.commons.logging.Log; 028import org.apache.commons.logging.LogFactory; 029 030import org.springframework.beans.BeanUtils; 031import org.springframework.beans.PropertyValues; 032import org.springframework.beans.factory.FactoryBean; 033import org.springframework.beans.factory.InitializingBean; 034import org.springframework.context.MessageSource; 035import org.springframework.context.MessageSourceAware; 036import org.springframework.core.convert.ConversionService; 037import org.springframework.core.env.PropertySources; 038import org.springframework.util.Assert; 039import org.springframework.util.StringUtils; 040import org.springframework.validation.BindException; 041import org.springframework.validation.BindingResult; 042import org.springframework.validation.DataBinder; 043import org.springframework.validation.ObjectError; 044import org.springframework.validation.Validator; 045 046/** 047 * Validate some {@link Properties} (or optionally {@link PropertySources}) by binding 048 * them to an object of a specified type and then optionally running a {@link Validator} 049 * over it. 050 * 051 * @param <T> the target type 052 * @author Dave Syer 053 */ 054public class PropertiesConfigurationFactory<T> 055 implements FactoryBean<T>, MessageSourceAware, InitializingBean { 056 057 private static final char[] EXACT_DELIMITERS = { '_', '.', '[' }; 058 059 private static final char[] TARGET_NAME_DELIMITERS = { '_', '.' }; 060 061 private static final Log logger = LogFactory 062 .getLog(PropertiesConfigurationFactory.class); 063 064 private boolean ignoreUnknownFields = true; 065 066 private boolean ignoreInvalidFields; 067 068 private boolean exceptionIfInvalid = true; 069 070 private PropertySources propertySources; 071 072 private final T target; 073 074 private Validator validator; 075 076 private MessageSource messageSource; 077 078 private boolean hasBeenBound = false; 079 080 private boolean ignoreNestedProperties = false; 081 082 private String targetName; 083 084 private ConversionService conversionService; 085 086 private boolean resolvePlaceholders = true; 087 088 /** 089 * Create a new {@link PropertiesConfigurationFactory} instance. 090 * @param target the target object to bind too 091 * @see #PropertiesConfigurationFactory(Class) 092 */ 093 public PropertiesConfigurationFactory(T target) { 094 Assert.notNull(target, "target must not be null"); 095 this.target = target; 096 } 097 098 /** 099 * Create a new {@link PropertiesConfigurationFactory} instance. 100 * @param type the target type 101 * @see #PropertiesConfigurationFactory(Class) 102 */ 103 @SuppressWarnings("unchecked") 104 public PropertiesConfigurationFactory(Class<?> type) { 105 Assert.notNull(type, "type must not be null"); 106 this.target = (T) BeanUtils.instantiate(type); 107 } 108 109 /** 110 * Flag to disable binding of nested properties (i.e. those with period separators in 111 * their paths). Can be useful to disable this if the name prefix is empty and you 112 * don't want to ignore unknown fields. 113 * @param ignoreNestedProperties the flag to set (default false) 114 */ 115 public void setIgnoreNestedProperties(boolean ignoreNestedProperties) { 116 this.ignoreNestedProperties = ignoreNestedProperties; 117 } 118 119 /** 120 * Set whether to ignore unknown fields, that is, whether to ignore bind parameters 121 * that do not have corresponding fields in the target object. 122 * <p> 123 * Default is "true". Turn this off to enforce that all bind parameters must have a 124 * matching field in the target object. 125 * @param ignoreUnknownFields if unknown fields should be ignored 126 */ 127 public void setIgnoreUnknownFields(boolean ignoreUnknownFields) { 128 this.ignoreUnknownFields = ignoreUnknownFields; 129 } 130 131 /** 132 * Set whether to ignore invalid fields, that is, whether to ignore bind parameters 133 * that have corresponding fields in the target object which are not accessible (for 134 * example because of null values in the nested path). 135 * <p> 136 * Default is "false". Turn this on to ignore bind parameters for nested objects in 137 * non-existing parts of the target object graph. 138 * @param ignoreInvalidFields if invalid fields should be ignored 139 */ 140 public void setIgnoreInvalidFields(boolean ignoreInvalidFields) { 141 this.ignoreInvalidFields = ignoreInvalidFields; 142 } 143 144 /** 145 * Set the target name. 146 * @param targetName the target name 147 */ 148 public void setTargetName(String targetName) { 149 this.targetName = targetName; 150 } 151 152 /** 153 * Set the message source. 154 * @param messageSource the message source 155 */ 156 @Override 157 public void setMessageSource(MessageSource messageSource) { 158 this.messageSource = messageSource; 159 } 160 161 /** 162 * Set the property sources. 163 * @param propertySources the property sources 164 */ 165 public void setPropertySources(PropertySources propertySources) { 166 this.propertySources = propertySources; 167 } 168 169 /** 170 * Set the conversion service. 171 * @param conversionService the conversion service 172 */ 173 public void setConversionService(ConversionService conversionService) { 174 this.conversionService = conversionService; 175 } 176 177 /** 178 * Set the validator. 179 * @param validator the validator 180 */ 181 public void setValidator(Validator validator) { 182 this.validator = validator; 183 } 184 185 /** 186 * Set a flag to indicate that an exception should be raised if a Validator is 187 * available and validation fails. 188 * @param exceptionIfInvalid the flag to set 189 * @deprecated as of 1.5, do not specify a {@link Validator} if validation should not 190 * occur 191 */ 192 @Deprecated 193 public void setExceptionIfInvalid(boolean exceptionIfInvalid) { 194 this.exceptionIfInvalid = exceptionIfInvalid; 195 } 196 197 /** 198 * Flag to indicate that placeholders should be replaced during binding. Default is 199 * true. 200 * @param resolvePlaceholders flag value 201 */ 202 public void setResolvePlaceholders(boolean resolvePlaceholders) { 203 this.resolvePlaceholders = resolvePlaceholders; 204 } 205 206 @Override 207 public void afterPropertiesSet() throws Exception { 208 bindPropertiesToTarget(); 209 } 210 211 @Override 212 public Class<?> getObjectType() { 213 if (this.target == null) { 214 return Object.class; 215 } 216 return this.target.getClass(); 217 } 218 219 @Override 220 public boolean isSingleton() { 221 return true; 222 } 223 224 @Override 225 public T getObject() throws Exception { 226 if (!this.hasBeenBound) { 227 bindPropertiesToTarget(); 228 } 229 return this.target; 230 } 231 232 public void bindPropertiesToTarget() throws BindException { 233 Assert.state(this.propertySources != null, "PropertySources should not be null"); 234 try { 235 if (logger.isTraceEnabled()) { 236 logger.trace("Property Sources: " + this.propertySources); 237 238 } 239 this.hasBeenBound = true; 240 doBindPropertiesToTarget(); 241 } 242 catch (BindException ex) { 243 if (this.exceptionIfInvalid) { 244 throw ex; 245 } 246 PropertiesConfigurationFactory.logger 247 .error("Failed to load Properties validation bean. " 248 + "Your Properties may be invalid.", ex); 249 } 250 } 251 252 private void doBindPropertiesToTarget() throws BindException { 253 RelaxedDataBinder dataBinder = (this.targetName != null 254 ? new RelaxedDataBinder(this.target, this.targetName) 255 : new RelaxedDataBinder(this.target)); 256 if (this.validator != null 257 && this.validator.supports(dataBinder.getTarget().getClass())) { 258 dataBinder.setValidator(this.validator); 259 } 260 if (this.conversionService != null) { 261 dataBinder.setConversionService(this.conversionService); 262 } 263 dataBinder.setAutoGrowCollectionLimit(Integer.MAX_VALUE); 264 dataBinder.setIgnoreNestedProperties(this.ignoreNestedProperties); 265 dataBinder.setIgnoreInvalidFields(this.ignoreInvalidFields); 266 dataBinder.setIgnoreUnknownFields(this.ignoreUnknownFields); 267 customizeBinder(dataBinder); 268 Iterable<String> relaxedTargetNames = getRelaxedTargetNames(); 269 Set<String> names = getNames(relaxedTargetNames); 270 PropertyValues propertyValues = getPropertySourcesPropertyValues(names, 271 relaxedTargetNames); 272 dataBinder.bind(propertyValues); 273 if (this.validator != null) { 274 dataBinder.validate(); 275 } 276 checkForBindingErrors(dataBinder); 277 } 278 279 private Iterable<String> getRelaxedTargetNames() { 280 return (this.target != null && StringUtils.hasLength(this.targetName) 281 ? new RelaxedNames(this.targetName) : null); 282 } 283 284 private Set<String> getNames(Iterable<String> prefixes) { 285 Set<String> names = new LinkedHashSet<String>(); 286 if (this.target != null) { 287 PropertyDescriptor[] descriptors = BeanUtils 288 .getPropertyDescriptors(this.target.getClass()); 289 for (PropertyDescriptor descriptor : descriptors) { 290 String name = descriptor.getName(); 291 if (!name.equals("class")) { 292 RelaxedNames relaxedNames = RelaxedNames.forCamelCase(name); 293 if (prefixes == null) { 294 for (String relaxedName : relaxedNames) { 295 names.add(relaxedName); 296 } 297 } 298 else { 299 for (String prefix : prefixes) { 300 for (String relaxedName : relaxedNames) { 301 names.add(prefix + "." + relaxedName); 302 names.add(prefix + "_" + relaxedName); 303 } 304 } 305 } 306 } 307 } 308 } 309 return names; 310 } 311 312 private PropertyValues getPropertySourcesPropertyValues(Set<String> names, 313 Iterable<String> relaxedTargetNames) { 314 PropertyNamePatternsMatcher includes = getPropertyNamePatternsMatcher(names, 315 relaxedTargetNames); 316 return new PropertySourcesPropertyValues(this.propertySources, names, includes, 317 this.resolvePlaceholders); 318 } 319 320 private PropertyNamePatternsMatcher getPropertyNamePatternsMatcher(Set<String> names, 321 Iterable<String> relaxedTargetNames) { 322 if (this.ignoreUnknownFields && !isMapTarget()) { 323 // Since unknown fields are ignored we can filter them out early to save 324 // unnecessary calls to the PropertySource. 325 return new DefaultPropertyNamePatternsMatcher(EXACT_DELIMITERS, true, names); 326 } 327 if (relaxedTargetNames != null) { 328 // We can filter properties to those starting with the target name, but 329 // we can't do a complete filter since we need to trigger the 330 // unknown fields check 331 Set<String> relaxedNames = new HashSet<String>(); 332 for (String relaxedTargetName : relaxedTargetNames) { 333 relaxedNames.add(relaxedTargetName); 334 } 335 return new DefaultPropertyNamePatternsMatcher(TARGET_NAME_DELIMITERS, true, 336 relaxedNames); 337 } 338 // Not ideal, we basically can't filter anything 339 return PropertyNamePatternsMatcher.ALL; 340 } 341 342 private boolean isMapTarget() { 343 return this.target != null && Map.class.isAssignableFrom(this.target.getClass()); 344 } 345 346 private void checkForBindingErrors(RelaxedDataBinder dataBinder) 347 throws BindException { 348 BindingResult errors = dataBinder.getBindingResult(); 349 if (errors.hasErrors()) { 350 logger.error("Properties configuration failed validation"); 351 for (ObjectError error : errors.getAllErrors()) { 352 logger.error( 353 this.messageSource != null 354 ? this.messageSource.getMessage(error, 355 Locale.getDefault()) + " (" + error + ")" 356 : error); 357 } 358 if (this.exceptionIfInvalid) { 359 throw new BindException(errors); 360 } 361 } 362 } 363 364 /** 365 * Customize the data binder. 366 * @param dataBinder the data binder that will be used to bind and validate 367 */ 368 protected void customizeBinder(DataBinder dataBinder) { 369 } 370 371}