/*
* 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.test.autoconfigure.properties;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* {@link EnumerablePropertySource} to adapt annotations marked with
* {@link PropertyMapping @PropertyMapping}.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.4.0
*/
public class AnnotationsPropertySource extends EnumerablePropertySource<Class<?>> {
private static final Pattern CAMEL_CASE_PATTERN = Pattern.compile("([^A-Z-])([A-Z])");
private final Map<String, Object> properties;
public AnnotationsPropertySource(Class<?> source) {
this("Annotations", source);
}
public AnnotationsPropertySource(String name, Class<?> source) {
super(name, source);
this.properties = getProperties(source);
}
private Map<String, Object> getProperties(Class<?> source) {
Map<String, Object> properties = new LinkedHashMap<>();
collectProperties(source, source, properties, new HashSet<Class<?>>());
return Collections.unmodifiableMap(properties);
}
private void collectProperties(Class<?> root, Class<?> source,
Map<String, Object> properties, Set<Class<?>> seen) {
if (source != null && seen.add(source)) {
for (Annotation annotation : getMergedAnnotations(root, source)) {
if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) {
PropertyMapping typeMapping = annotation.annotationType()
.getAnnotation(PropertyMapping.class);
for (Method attribute : annotation.annotationType()
.getDeclaredMethods()) {
collectProperties(annotation, attribute, typeMapping, properties);
}
collectProperties(root, annotation.annotationType(), properties,
seen);
}
}
collectProperties(root, source.getSuperclass(), properties, seen);
}
}
private List<Annotation> getMergedAnnotations(Class<?> root, Class<?> source) {
List<Annotation> mergedAnnotations = new ArrayList<>();
for (Annotation annotation : AnnotationUtils.getAnnotations(source)) {
if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) {
mergedAnnotations
.add(findMergedAnnotation(root, annotation.annotationType()));
}
}
return mergedAnnotations;
}
private Annotation findMergedAnnotation(Class<?> source,
Class<? extends Annotation> annotationType) {
if (source == null) {
return null;
}
Annotation mergedAnnotation = AnnotatedElementUtils.getMergedAnnotation(source,
annotationType);
return mergedAnnotation != null ? mergedAnnotation
: findMergedAnnotation(source.getSuperclass(), annotationType);
}
private void collectProperties(Annotation annotation, Method attribute,
PropertyMapping typeMapping, Map<String, Object> properties) {
PropertyMapping attributeMapping = AnnotationUtils.getAnnotation(attribute,
PropertyMapping.class);
SkipPropertyMapping skip = getMappingType(typeMapping, attributeMapping);
if (skip == SkipPropertyMapping.YES) {
return;
}
String name = getName(typeMapping, attributeMapping, attribute);
ReflectionUtils.makeAccessible(attribute);
Object value = ReflectionUtils.invokeMethod(attribute, annotation);
if (skip == SkipPropertyMapping.ON_DEFAULT_VALUE) {
Object defaultValue = AnnotationUtils.getDefaultValue(annotation,
attribute.getName());
if (ObjectUtils.nullSafeEquals(value, defaultValue)) {
return;
}
}
putProperties(name, value, properties);
}
private SkipPropertyMapping getMappingType(PropertyMapping typeMapping,
PropertyMapping attributeMapping) {
if (attributeMapping != null) {
return attributeMapping.skip();
}
if (typeMapping != null) {
return typeMapping.skip();
}
return SkipPropertyMapping.YES;
}
private String getName(PropertyMapping typeMapping, PropertyMapping attributeMapping,
Method attribute) {
String prefix = (typeMapping == null ? "" : typeMapping.value());
String name = (attributeMapping == null ? "" : attributeMapping.value());
if (!StringUtils.hasText(name)) {
name = toKebabCase(attribute.getName());
}
return dotAppend(prefix, name);
}
private String toKebabCase(String name) {
Matcher matcher = CAMEL_CASE_PATTERN.matcher(name);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(result,
matcher.group(1) + '-' + StringUtils.uncapitalize(matcher.group(2)));
}
matcher.appendTail(result);
return result.toString().toLowerCase();
}
private String dotAppend(String prefix, String postfix) {
if (StringUtils.hasText(prefix)) {
return (prefix.endsWith(".") ? prefix + postfix : prefix + "." + postfix);
}
return postfix;
}
private void putProperties(String name, Object value,
Map<String, Object> properties) {
if (ObjectUtils.isArray(value)) {
Object[] array = ObjectUtils.toObjectArray(value);
for (int i = 0; i < array.length; i++) {
properties.put(name + "[" + i + "]", array[i]);
}
}
else {
properties.put(name, value);
}
}
@Override
public boolean containsProperty(String name) {
return this.properties.containsKey(name);
}
@Override
public Object getProperty(String name) {
return this.properties.get(name);
}
@Override
public String[] getPropertyNames() {
return StringUtils.toStringArray(this.properties.keySet());
}
public boolean isEmpty() {
return this.properties.isEmpty();
}
@Override
public int hashCode() {
return this.properties.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
return this.properties.equals(((AnnotationsPropertySource) obj).properties);
}
}