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}