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}