View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   */
14  package com.addthis.codec.reflection;
15  
16  import javax.annotation.Nonnull;
17  import javax.annotation.Nullable;
18  
19  import java.lang.reflect.Field;
20  import java.lang.reflect.Modifier;
21  
22  import java.util.Collection;
23  import java.util.HashMap;
24  import java.util.Map;
25  import java.util.SortedMap;
26  import java.util.TreeMap;
27  
28  import com.addthis.codec.annotations.FieldConfig;
29  import com.addthis.codec.annotations.Pluggable;
30  import com.addthis.codec.plugins.PluginMap;
31  import com.addthis.codec.plugins.PluginRegistry;
32  
33  import com.google.common.annotations.Beta;
34  import com.google.common.collect.ImmutableSortedMap;
35  
36  import com.typesafe.config.Config;
37  import com.typesafe.config.ConfigFactory;
38  import com.typesafe.config.ConfigObject;
39  import com.typesafe.config.ConfigValue;
40  
41  import org.slf4j.Logger;
42  import org.slf4j.LoggerFactory;
43  
44  @Beta
45  public class CodableClassInfo {
46      private static final Logger log = LoggerFactory.getLogger(CodableClassInfo.class);
47  
48      @Nonnull private final Class<?>     baseClass;
49      @Nonnull private final PluginMap    pluginMap;
50      @Nonnull private final Config       fieldDefaults;
51      @Nonnull private final ImmutableSortedMap<String, CodableFieldInfo> classData;
52  
53      public CodableClassInfo(@Nonnull Class<?> clazz) {
54          this(clazz, ConfigFactory.load(), PluginRegistry.defaultRegistry());
55      }
56  
57      public CodableClassInfo(@Nonnull Class<?> clazz,
58                              @Nonnull Config globalDefaults,
59                              @Nonnull PluginRegistry pluginRegistry) {
60  
61          // skip native classes
62          if (Fields.isNative(clazz) || clazz.isArray()) {
63              classData = ImmutableSortedMap.of();
64              baseClass = clazz;
65              pluginMap = PluginMap.EMPTY;
66              fieldDefaults = ConfigFactory.empty();
67              return;
68          }
69  
70          // find any parent class (or itself) with a Pluggable annotation
71          Class<?> findBaseClass = clazz;
72          PluginMap findPluginMap = PluginMap.EMPTY;
73  
74          Class<?> ptr = clazz;
75          while (ptr != null) {
76              if (pluginRegistry.byClass().containsKey(ptr)) {
77                  findPluginMap = pluginRegistry.byClass().get(ptr);
78                  findBaseClass = ptr;
79                  break;
80              }
81              Pluggable pluggable = ptr.getAnnotation(Pluggable.class);
82              if (pluggable != null) {
83                  String category = pluggable.value();
84                  findPluginMap = pluginRegistry.asMap().get(category);
85                  findBaseClass = ptr;
86                  if (findPluginMap == null) {
87                      log.warn("missing plugin map for {}, reached from {}", ptr, clazz);
88                      findPluginMap = PluginMap.EMPTY;
89                  }
90                  break;
91              }
92              ptr = ptr.getSuperclass();
93          }
94          pluginMap = findPluginMap;
95          baseClass = findBaseClass;
96  
97          // find all fields in the class and its parent classes, and aggregate any defaults
98          Map<String, Field> fields = new HashMap<>();
99          // slower than using unwrapped, mutable conversions but this preserves origins
100         ConfigObject buildDefaults = ConfigFactory.empty().root();
101 
102         Class<?> ptrForFields = clazz;
103         while (ptrForFields != null) {
104             String canonicalClassName = ptrForFields.getCanonicalName();
105             ConfigObject classDefaults;
106             if ((canonicalClassName != null) && globalDefaults.hasPath(canonicalClassName)) {
107                 classDefaults = globalDefaults.getObject(canonicalClassName);
108             } else {
109                 classDefaults = ConfigFactory.empty().root();
110             }
111             for (Field field : ptrForFields.getDeclaredFields()) {
112                 if (fields.get(field.getName()) == null) {
113                     fields.put(field.getName(), field);
114                 } else {
115                     classDefaults = classDefaults.withoutKey(field.getName());
116                     log.debug("({}) ignoring field in parent class ({}) with duplicate name ({})",
117                               clazz, ptrForFields, field.getName());
118                 }
119             }
120             for (Map.Entry<String, ConfigValue> pair : classDefaults.entrySet()) {
121                 if (!buildDefaults.containsKey(pair.getKey())) {
122                     buildDefaults = buildDefaults.withValue(pair.getKey(), pair.getValue());
123                 }
124             }
125             ptrForFields = ptrForFields.getSuperclass();
126         }
127         fieldDefaults = buildDefaults.toConfig();
128 
129         // turn all the found fields into CodableFieldInfo objects
130         Map<String, CodableFieldInfo> buildClassData = buildFieldInfoMap(fields.values());
131         classData = ImmutableSortedMap.<String, CodableFieldInfo>naturalOrder()
132                                       .putAll(buildClassData).build();
133     }
134 
135     @Nonnull public PluginMap getPluginMap() {
136         return pluginMap;
137     }
138 
139     @Nonnull public Config getFieldDefaults() {
140         return fieldDefaults;
141     }
142 
143     @Nonnull public Class<?> getBaseClass() {
144         return baseClass;
145     }
146 
147     @Nonnull public String getClassField() {
148         return pluginMap.classField();
149     }
150 
151     @Nullable public Class<?> getArraySugar() {
152         return pluginMap.arraySugar();
153     }
154 
155     @Nullable public Class<?> getDefaultSugar() {
156         return pluginMap.defaultSugar();
157     }
158 
159     @Nullable public String getClassName(Object val) {
160         if (val.getClass() != baseClass) {
161             return pluginMap.getClassName(val.getClass());
162         } else {
163             return null;
164         }
165     }
166 
167     @Nonnull public Class<?> getClass(String name) throws ClassNotFoundException {
168         return pluginMap.getClass(name);
169     }
170 
171     public int size() {
172         return classData.size();
173     }
174 
175     @Nonnull public Collection<CodableFieldInfo> values() {
176         return classData.values();
177     }
178 
179     /** Immutable view of codable fields as a map of field names to {@link CodableFieldInfo}s. */
180     @Nonnull public Map<String, CodableFieldInfo> fields() {
181         return classData;
182     }
183 
184     /**
185      * Decide whether it is okay to read/ write a field. If configured via an annotation on the field, use that.
186      * Otherwise return true only if the field is both public and non-final.
187      */
188     private static boolean isCodable(Field field) {
189         FieldConfig fieldConfigPolicy = field.getAnnotation(FieldConfig.class);
190         if (fieldConfigPolicy != null) {
191             return fieldConfigPolicy.codable();
192         }
193         int modifierBitSet = field.getModifiers();
194         return !Modifier.isFinal(modifierBitSet) && Modifier.isPublic(modifierBitSet);
195     }
196 
197     @Nonnull private static Map<String, CodableFieldInfo> buildFieldInfoMap(Iterable<Field> fields) {
198         SortedMap<String, CodableFieldInfo> buildClassData = new TreeMap<>();
199         for (Field field : fields) {
200             if (isCodable(field)) {
201                 CodableFieldInfo info = new CodableFieldInfo(field);
202                 buildClassData.put(field.getName(), info);
203             }
204         }
205         return buildClassData;
206     }
207 
208 }