/*
* 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.source;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.IntStream;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form;
import org.springframework.core.env.PropertySource;
import org.springframework.util.StringUtils;
/**
* {@link PropertyMapper} for system environment variables. Names are mapped by removing
* invalid characters, converting to lower case and replacing "{@code _}" with
* "{@code .}". For example, "{@code SERVER_PORT}" is mapped to "{@code server.port}". In
* addition, numeric elements are mapped to indexes (e.g. "{@code HOST_0}" is mapped to
* "{@code host[0]}").
* <p>
* List shortcuts (names that end with double underscore) are also supported by this
* mapper. For example, "{@code MY_LIST__=a,b,c}" is mapped to "{@code my.list[0]=a}",
* "{@code my.list[1]=b}" ,"{@code my.list[2]=c}".
*
* @author Phillip Webb
* @author Madhura Bhave
* @see PropertyMapper
* @see SpringConfigurationPropertySource
*/
final class SystemEnvironmentPropertyMapper implements PropertyMapper {
public static final PropertyMapper INSTANCE = new SystemEnvironmentPropertyMapper();
private SystemEnvironmentPropertyMapper() {
}
@Override
public List<PropertyMapping> map(PropertySource<?> propertySource,
String propertySourceName) {
ConfigurationPropertyName name = convertName(propertySourceName);
if (name == null || name.isEmpty()) {
return Collections.emptyList();
}
if (propertySourceName.endsWith("__")) {
return expandListShortcut(propertySourceName, name,
propertySource.getProperty(propertySourceName));
}
return Collections.singletonList(new PropertyMapping(propertySourceName, name));
}
private ConfigurationPropertyName convertName(String propertySourceName) {
try {
return ConfigurationPropertyName.adapt(propertySourceName, '_',
this::processElementValue);
}
catch (Exception ex) {
return null;
}
}
private List<PropertyMapping> expandListShortcut(String propertySourceName,
ConfigurationPropertyName rootName, Object value) {
if (value == null) {
return Collections.emptyList();
}
List<PropertyMapping> mappings = new ArrayList<>();
String[] elements = StringUtils
.commaDelimitedListToStringArray(String.valueOf(value));
for (int i = 0; i < elements.length; i++) {
ConfigurationPropertyName name = rootName.append("[" + i + "]");
mappings.add(new PropertyMapping(propertySourceName, name,
new ElementExtractor(i)));
}
return mappings;
}
@Override
public List<PropertyMapping> map(PropertySource<?> propertySource,
ConfigurationPropertyName configurationPropertyName) {
String name = convertName(configurationPropertyName);
List<PropertyMapping> result = Collections
.singletonList(new PropertyMapping(name, configurationPropertyName));
if (isListShortcutPossible(configurationPropertyName)) {
result = new ArrayList<>(result);
result.addAll(mapListShortcut(propertySource, configurationPropertyName));
}
return result;
}
private String convertName(ConfigurationPropertyName name) {
return convertName(name, name.getNumberOfElements());
}
private String convertName(ConfigurationPropertyName name, int numberOfElements) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < numberOfElements; i++) {
result.append(result.length() == 0 ? "" : "_");
result.append(name.getElement(i, Form.UNIFORM).toString().toUpperCase());
}
return result.toString();
}
private boolean isListShortcutPossible(ConfigurationPropertyName name) {
return (name.isLastElementIndexed() && isNumber(name.getLastElement(Form.UNIFORM))
&& name.getNumberOfElements() >= 1);
}
private List<PropertyMapping> mapListShortcut(PropertySource<?> propertySource,
ConfigurationPropertyName name) {
String result = convertName(name, name.getNumberOfElements() - 1) + "__";
if (propertySource.containsProperty(result)) {
int index = Integer.parseInt(name.getLastElement(Form.UNIFORM));
return Collections.singletonList(
new PropertyMapping(result, name, new ElementExtractor(index)));
}
return Collections.emptyList();
}
private CharSequence processElementValue(CharSequence value) {
String result = value.toString().toLowerCase();
return (isNumber(result) ? "[" + result + "]" : result);
}
private static boolean isNumber(String string) {
IntStream nonDigits = string.chars().filter((c) -> !Character.isDigit(c));
boolean hasNonDigit = nonDigits.findFirst().isPresent();
return !hasNonDigit;
}
/**
* Function used to extract an element from a comma list.
*/
private static class ElementExtractor implements Function<Object, Object> {
private final int index;
ElementExtractor(int index) {
this.index = index;
}
@Override
public Object apply(Object value) {
if (value == null) {
return null;
}
return StringUtils
.commaDelimitedListToStringArray(value.toString())[this.index];
}
}
}