001/*
002 * Copyright 2012-2018 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.test.autoconfigure.properties;
018
019import java.lang.annotation.Annotation;
020import java.lang.reflect.Method;
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.HashSet;
024import java.util.LinkedHashMap;
025import java.util.List;
026import java.util.Locale;
027import java.util.Map;
028import java.util.Set;
029import java.util.regex.Matcher;
030import java.util.regex.Pattern;
031
032import org.springframework.core.annotation.AnnotatedElementUtils;
033import org.springframework.core.annotation.AnnotationUtils;
034import org.springframework.core.env.EnumerablePropertySource;
035import org.springframework.util.ObjectUtils;
036import org.springframework.util.ReflectionUtils;
037import org.springframework.util.StringUtils;
038
039/**
040 * {@link EnumerablePropertySource} to adapt annotations marked with
041 * {@link PropertyMapping @PropertyMapping}.
042 *
043 * @author Phillip Webb
044 * @author Andy Wilkinson
045 * @since 1.4.0
046 */
047public class AnnotationsPropertySource extends EnumerablePropertySource<Class<?>> {
048
049        private static final Pattern CAMEL_CASE_PATTERN = Pattern.compile("([^A-Z-])([A-Z])");
050
051        private final Map<String, Object> properties;
052
053        public AnnotationsPropertySource(Class<?> source) {
054                this("Annotations", source);
055        }
056
057        public AnnotationsPropertySource(String name, Class<?> source) {
058                super(name, source);
059                this.properties = getProperties(source);
060        }
061
062        private Map<String, Object> getProperties(Class<?> source) {
063                Map<String, Object> properties = new LinkedHashMap<>();
064                collectProperties(source, source, properties, new HashSet<>());
065                return Collections.unmodifiableMap(properties);
066        }
067
068        private void collectProperties(Class<?> root, Class<?> source,
069                        Map<String, Object> properties, Set<Class<?>> seen) {
070                if (source != null && seen.add(source)) {
071                        for (Annotation annotation : getMergedAnnotations(root, source)) {
072                                if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) {
073                                        PropertyMapping typeMapping = annotation.annotationType()
074                                                        .getAnnotation(PropertyMapping.class);
075                                        for (Method attribute : annotation.annotationType()
076                                                        .getDeclaredMethods()) {
077                                                collectProperties(annotation, attribute, typeMapping, properties);
078                                        }
079                                        collectProperties(root, annotation.annotationType(), properties,
080                                                        seen);
081                                }
082                        }
083                        collectProperties(root, source.getSuperclass(), properties, seen);
084                }
085        }
086
087        private List<Annotation> getMergedAnnotations(Class<?> root, Class<?> source) {
088                List<Annotation> mergedAnnotations = new ArrayList<>();
089                Annotation[] annotations = AnnotationUtils.getAnnotations(source);
090                if (annotations != null) {
091                        for (Annotation annotation : annotations) {
092                                if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) {
093                                        Annotation mergedAnnotation = findMergedAnnotation(root,
094                                                        annotation.annotationType());
095                                        if (mergedAnnotation != null) {
096                                                mergedAnnotations.add(mergedAnnotation);
097                                        }
098                                }
099                        }
100                }
101                return mergedAnnotations;
102        }
103
104        private Annotation findMergedAnnotation(Class<?> source,
105                        Class<? extends Annotation> annotationType) {
106                if (source == null) {
107                        return null;
108                }
109                Annotation mergedAnnotation = AnnotatedElementUtils.getMergedAnnotation(source,
110                                annotationType);
111                return (mergedAnnotation != null) ? mergedAnnotation
112                                : findMergedAnnotation(source.getSuperclass(), annotationType);
113        }
114
115        private void collectProperties(Annotation annotation, Method attribute,
116                        PropertyMapping typeMapping, Map<String, Object> properties) {
117                PropertyMapping attributeMapping = AnnotationUtils.getAnnotation(attribute,
118                                PropertyMapping.class);
119                SkipPropertyMapping skip = getMappingType(typeMapping, attributeMapping);
120                if (skip == SkipPropertyMapping.YES) {
121                        return;
122                }
123                String name = getName(typeMapping, attributeMapping, attribute);
124                ReflectionUtils.makeAccessible(attribute);
125                Object value = ReflectionUtils.invokeMethod(attribute, annotation);
126                if (skip == SkipPropertyMapping.ON_DEFAULT_VALUE) {
127                        Object defaultValue = AnnotationUtils.getDefaultValue(annotation,
128                                        attribute.getName());
129                        if (ObjectUtils.nullSafeEquals(value, defaultValue)) {
130                                return;
131                        }
132                }
133                putProperties(name, value, properties);
134        }
135
136        private SkipPropertyMapping getMappingType(PropertyMapping typeMapping,
137                        PropertyMapping attributeMapping) {
138                if (attributeMapping != null) {
139                        return attributeMapping.skip();
140                }
141                if (typeMapping != null) {
142                        return typeMapping.skip();
143                }
144                return SkipPropertyMapping.YES;
145        }
146
147        private String getName(PropertyMapping typeMapping, PropertyMapping attributeMapping,
148                        Method attribute) {
149                String prefix = (typeMapping != null) ? typeMapping.value() : "";
150                String name = (attributeMapping != null) ? attributeMapping.value() : "";
151                if (!StringUtils.hasText(name)) {
152                        name = toKebabCase(attribute.getName());
153                }
154                return dotAppend(prefix, name);
155        }
156
157        private String toKebabCase(String name) {
158                Matcher matcher = CAMEL_CASE_PATTERN.matcher(name);
159                StringBuffer result = new StringBuffer();
160                while (matcher.find()) {
161                        matcher.appendReplacement(result,
162                                        matcher.group(1) + '-' + StringUtils.uncapitalize(matcher.group(2)));
163                }
164                matcher.appendTail(result);
165                return result.toString().toLowerCase(Locale.ENGLISH);
166        }
167
168        private String dotAppend(String prefix, String postfix) {
169                if (StringUtils.hasText(prefix)) {
170                        return prefix.endsWith(".") ? prefix + postfix : prefix + "." + postfix;
171                }
172                return postfix;
173        }
174
175        private void putProperties(String name, Object value,
176                        Map<String, Object> properties) {
177                if (ObjectUtils.isArray(value)) {
178                        Object[] array = ObjectUtils.toObjectArray(value);
179                        for (int i = 0; i < array.length; i++) {
180                                properties.put(name + "[" + i + "]", array[i]);
181                        }
182                }
183                else {
184                        properties.put(name, value);
185                }
186        }
187
188        @Override
189        public boolean containsProperty(String name) {
190                return this.properties.containsKey(name);
191        }
192
193        @Override
194        public Object getProperty(String name) {
195                return this.properties.get(name);
196        }
197
198        @Override
199        public String[] getPropertyNames() {
200                return StringUtils.toStringArray(this.properties.keySet());
201        }
202
203        public boolean isEmpty() {
204                return this.properties.isEmpty();
205        }
206
207        @Override
208        public boolean equals(Object obj) {
209                if (obj == this) {
210                        return true;
211                }
212                if (obj == null || getClass() != obj.getClass()) {
213                        return false;
214                }
215                return this.properties.equals(((AnnotationsPropertySource) obj).properties);
216        }
217
218        @Override
219        public int hashCode() {
220                return this.properties.hashCode();
221        }
222
223}