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.GenericArrayType;
21  import java.lang.reflect.Type;
22  
23  import java.util.Collection;
24  import java.util.Map;
25  
26  import com.addthis.codec.annotations.FieldConfig;
27  import com.addthis.codec.codables.Codable;
28  
29  import com.google.common.annotations.Beta;
30  
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  
34  /**
35   * information about a field in a class - expensive to get so runs and gets cached
36   */
37  @Beta
38  public final class CodableFieldInfo {
39  
40      private static final Logger log = LoggerFactory.getLogger(CodableFieldInfo.class);
41  
42      public static final int ARRAY      = 1 << 0;
43      public static final int CODABLE    = 1 << 1;
44      public static final int COLLECTION = 1 << 2;
45      public static final int GENERIC    = 1 << 3;
46      public static final int NATIVE     = 1 << 4;
47      public static final int MAP        = 1 << 5;
48      public static final int NUMBER     = 1 << 6;
49      public static final int REQUIRED   = 1 << 7;
50      public static final int READONLY   = 1 << 8;
51      public static final int WRITEONLY  = 1 << 9;
52      public static final int ENUM       = 1 << 10;
53  
54      @Nonnull private final Field    field;
55      @Nonnull private final Class<?> typeOrComponentType;
56      private final int bits;
57  
58      @Nullable private final FieldConfig fieldConfig;
59      @Nullable private final Type[]      genTypes;
60      @Nullable private final boolean[]   genArray;
61  
62      public CodableFieldInfo(@Nonnull Field field) {
63          this.field = field;
64          field.setAccessible(true);
65          fieldConfig = field.getAnnotation(FieldConfig.class);
66  
67          Class<?> type = field.getType();
68          boolean array = type.isArray();
69          if (array) {
70              typeOrComponentType = type.getComponentType();
71              this.bits = cacheFlags(CodableFieldInfo.ARRAY);
72          } else {
73              typeOrComponentType = type;
74              this.bits = cacheFlags(0);
75          }
76          // extract generics info
77          if (!Fields.isNative(typeOrComponentType)) {
78              genTypes = Fields.collectTypes(typeOrComponentType, field.getGenericType());
79          } else {
80              genTypes = null;
81          }
82          if (genTypes == null) {
83              genArray = null;
84          } else {
85              genArray = new boolean[genTypes.length];
86              mutateGenericTypes(genTypes, genArray);
87          }
88      }
89  
90      private int cacheFlags(int externalBits) {
91          int partialBits = externalBits;
92          if (Codable.class.isAssignableFrom(typeOrComponentType)) {
93              partialBits |= CodableFieldInfo.CODABLE;
94          }
95          if (Collection.class.isAssignableFrom(typeOrComponentType)) {
96              partialBits |= CodableFieldInfo.COLLECTION;
97          }
98          if (Map.class.isAssignableFrom(typeOrComponentType)) {
99              partialBits |= CodableFieldInfo.MAP;
100         }
101         if (typeOrComponentType.isEnum()) {
102             partialBits |= CodableFieldInfo.ENUM;
103         }
104         if (Number.class.isAssignableFrom(typeOrComponentType)) {
105             partialBits |= CodableFieldInfo.NUMBER;
106         }
107         if (Fields.isNative(typeOrComponentType)) {
108             partialBits |= CodableFieldInfo.NATIVE;
109         }
110         if (fieldConfig != null) {
111             if (fieldConfig.readonly()) {
112                 partialBits |= CodableFieldInfo.READONLY;
113             }
114             if (fieldConfig.writeonly()) {
115                 partialBits |= CodableFieldInfo.WRITEONLY;
116             }
117             if (fieldConfig.codable()) {
118                 partialBits |= CodableFieldInfo.CODABLE;
119             }
120             if (fieldConfig.required()) {
121                 partialBits |= CodableFieldInfo.REQUIRED;
122             }
123         }
124         return partialBits;
125     }
126 
127     @Nonnull public Field getField() {
128         return field;
129     }
130 
131     public String getName() {
132         return field.getName();
133     }
134 
135     @Nonnull public Class<?> getTypeOrComponentType() {
136         return typeOrComponentType;
137     }
138 
139     @Nullable public Type[] getGenericTypes() {
140         return genTypes;
141     }
142 
143     // interacts with the ill-defined generic support
144     private void mutateGenericTypes(@Nonnull final Type[] collectedTypes,
145                                     @Nonnull final boolean[] genericFlags) {
146         for (int i = 0; i < collectedTypes.length; i++) {
147             Type currentType = collectedTypes[i];
148             if (currentType instanceof GenericArrayType) {
149                 genericFlags[i] = true;
150                 collectedTypes[i] = ((GenericArrayType) currentType).getGenericComponentType();
151             } else if ((currentType instanceof Class) && ((Class) currentType).isArray()) {
152                 genericFlags[i] = true;
153                 collectedTypes[i] = ((Class) currentType).getComponentType();
154             } else {
155                 genericFlags[i] = false;
156             }
157         }
158     }
159 
160     public Object newInstance() throws Exception {
161         return typeOrComponentType.newInstance();
162     }
163 
164     @Nullable public Class<?> getCollectionClass() {
165         return ((genTypes != null) && (genTypes.length == 1)) ? (Class<?>) genTypes[0] : null;
166     }
167 
168     @Nullable public Class<?> getMapKeyClass() {
169         return ((genTypes != null) && (genTypes.length == 2)) ? (Class<?>) genTypes[0] : null;
170     }
171 
172     @Nullable public Class<?> getMapValueClass() {
173         return ((genTypes != null) && (genTypes.length == 2)) ? (Class<?>) genTypes[1] : null;
174     }
175 
176     public boolean isCollectionArray() {
177         return ((genArray != null) && (genArray.length == 1)) ? genArray[0] : false;
178     }
179 
180     public boolean isMapKeyArray() {
181         return ((genArray != null) && (genArray.length == 2)) ? genArray[0] : false;
182     }
183 
184     public boolean isMapValueArray() {
185         return ((genArray != null) && (genArray.length == 2)) ? genArray[1] : false;
186     }
187 
188     public Object get(Object src) {
189         try {
190             return field.get(src);
191         } catch (IllegalAccessException e) {
192             throw new RuntimeException(e);
193         }
194     }
195 
196     /**
197      * Sets the field value for the destination object if and only if it is non-null and passes the field's
198      * validation method (if any). If the value is null and the field is marked as required, then an exception
199      * is thrown, otherwise the null value is ignored and the current value (if any) is kept.
200      */
201     public void setStrict(@Nonnull Object dst, @Nullable Object value) throws IllegalAccessException {
202         if (value == null) {
203             if (isRequired()) {
204                 throw new RequiredFieldException("missing required field '" +
205                                                  this.getName() + "' for " + dst, getName());
206             }
207             return;
208         }
209         field.set(dst, value);
210     }
211 
212     public void set(@Nonnull Object dst, @Nullable Object value) {
213         if (value == null) {
214             if (isRequired() && (get(dst) == null)) {
215                 throw new RequiredFieldException("missing required field '" +
216                     this.getName() + "' for " + dst, getName());
217             }
218             return;
219         }
220         try {
221             field.set(dst, value);
222         } catch (RuntimeException ex) {
223             throw ex;
224         } catch (Exception ex) {
225             log.warn("error setting ({})({}) on ({}) in {}", value, value.getClass(), dst, toString());
226             throw new RuntimeException(ex);
227         }
228     }
229 
230     public boolean isArray() {
231         return (bits & ARRAY) == ARRAY;
232     }
233 
234     public boolean isCodable() {
235         return (bits & CODABLE) == CODABLE;
236     }
237 
238     public boolean isCollection() {
239         return (bits & COLLECTION) == COLLECTION;
240     }
241 
242     public boolean isGeneric() {
243         return (bits & GENERIC) == GENERIC;
244     }
245 
246     public boolean isMap() {
247         return (bits & MAP) == MAP;
248     }
249 
250     public boolean isEnum() {
251         return (bits & ENUM) == ENUM;
252     }
253 
254     public boolean isNative() {
255         return (bits & NATIVE) == NATIVE;
256     }
257 
258     public boolean isRequired() {
259         return (bits & REQUIRED) == REQUIRED;
260     }
261 
262     public boolean isReadOnly() {
263         return (bits & READONLY) == READONLY;
264     }
265 
266     public boolean isWriteOnly() {
267         return (bits & WRITEONLY) == WRITEONLY;
268     }
269 
270     public String toString() {
271         return "[" + getName() + "," + typeOrComponentType + (isArray() ? "[]," : ",") + Integer.toString(bits, 2) + "]";
272     }
273 }