/*
* Copyright 2012-2017 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.boot.context.properties.bind;
import java.util.Collection;
import java.util.Map;
import java.util.Properties;
import org.springframework.boot.context.properties.bind.convert.BinderConversionService;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.IterableConfigurationPropertySource;
import org.springframework.core.CollectionFactory;
import org.springframework.core.ResolvableType;
/**
* {@link AggregateBinder} for Maps.
*
* @author Phillip Webb
* @author Madhura Bhave
*/
class MapBinder extends AggregateBinder<Map<Object, Object>> {
private static final Bindable<Map<String, String>> STRING_STRING_MAP = Bindable
.mapOf(String.class, String.class);
MapBinder(BindContext context) {
super(context);
}
@Override
protected Object bind(ConfigurationPropertyName name, Bindable<?> target,
AggregateElementBinder elementBinder, Class<?> type) {
Map<Object, Object> map = CollectionFactory.createMap(type, 0);
Bindable<?> resolvedTarget = resolveTarget(target);
for (ConfigurationPropertySource source : getContext().getSources()) {
if (!ConfigurationPropertyName.EMPTY.equals(name)) {
source = source.filter(name::isAncestorOf);
}
new EntryBinder(name, resolvedTarget, elementBinder).bindEntries(source, map);
}
return (map.isEmpty() ? null : map);
}
private Bindable<?> resolveTarget(Bindable<?> target) {
Class<?> type = target.getType().resolve();
if (Properties.class.isAssignableFrom(type)) {
return STRING_STRING_MAP;
}
return target;
}
@Override
protected Map<Object, Object> merge(Map<Object, Object> existing,
Map<Object, Object> additional) {
existing.putAll(additional);
return existing;
}
private class EntryBinder {
private final ConfigurationPropertyName root;
private final AggregateElementBinder elementBinder;
private final ResolvableType mapType;
private final ResolvableType keyType;
private final ResolvableType valueType;
EntryBinder(ConfigurationPropertyName root, Bindable<?> target,
AggregateElementBinder elementBinder) {
this.root = root;
this.elementBinder = elementBinder;
this.mapType = target.getType().asMap();
this.keyType = this.mapType.getGeneric(0);
this.valueType = this.mapType.getGeneric(1);
}
public void bindEntries(ConfigurationPropertySource source,
Map<Object, Object> map) {
if (source instanceof IterableConfigurationPropertySource) {
for (ConfigurationPropertyName name : (IterableConfigurationPropertySource) source) {
Bindable<?> valueBindable = getValueBindable(name);
ConfigurationPropertyName entryName = getEntryName(source, name);
Object key = getContext().getConversionService()
.convert(getKeyName(entryName), this.keyType);
Object value = this.elementBinder.bind(entryName, valueBindable);
map.putIfAbsent(key, value);
}
}
}
private Bindable<?> getValueBindable(ConfigurationPropertyName name) {
if (!this.root.isParentOf(name) && isValueTreatedAsNestedMap()) {
return Bindable.of(this.mapType);
}
return Bindable.of(this.valueType);
}
private ConfigurationPropertyName getEntryName(ConfigurationPropertySource source,
ConfigurationPropertyName name) {
if (!this.root.isParentOf(name)
&& (isValueTreatedAsNestedMap() || !isScalarValue(source, name))) {
return name.chop(this.root.getNumberOfElements() + 1);
}
return name;
}
private boolean isValueTreatedAsNestedMap() {
return Object.class.equals(this.valueType.resolve(Object.class));
}
private boolean isScalarValue(ConfigurationPropertySource source,
ConfigurationPropertyName name) {
if (Map.class.isAssignableFrom(this.valueType.resolve())
|| Collection.class.isAssignableFrom(this.valueType.resolve())
|| this.valueType.isArray()) {
return false;
}
ConfigurationProperty property = source.getConfigurationProperty(name);
if (property == null) {
return false;
}
Object value = property.getValue();
value = getContext().getPlaceholdersResolver().resolvePlaceholders(value);
BinderConversionService conversionService = getContext()
.getConversionService();
return conversionService.canConvert(value, this.valueType);
}
private String getKeyName(ConfigurationPropertyName name) {
StringBuilder result = new StringBuilder();
for (int i = this.root.getNumberOfElements(); i < name
.getNumberOfElements(); i++) {
result.append(result.length() == 0 ? "" : ".");
result.append(name.getElement(i, Form.ORIGINAL));
}
return result.toString();
}
}
}