View Javadoc

1   package com.atlassian.marketplace.client.impl;
2   
3   import java.lang.reflect.Field;
4   import java.net.URI;
5   import java.util.HashMap;
6   import java.util.Map;
7   import java.util.concurrent.ConcurrentHashMap;
8   
9   import com.atlassian.fugue.Option;
10  import com.atlassian.marketplace.client.encoding.MissingRequiredField;
11  import com.atlassian.marketplace.client.encoding.SchemaViolation;
12  import com.atlassian.marketplace.client.model.Links;
13  import com.atlassian.marketplace.client.model.RequiredLink;
14  
15  import com.google.common.collect.ImmutableList;
16  import com.google.gson.JsonParseException;
17  
18  import static com.atlassian.fugue.Option.none;
19  import static com.google.common.collect.Iterables.concat;
20  import static com.google.common.collect.Iterables.isEmpty;
21  
22  /**
23   * Provides post-processing of generic model instances after they have gone through the standard
24   * JSON deserialization.  This consists of the following:
25   * <ul>
26   * <li> For any field with an {@code Option} type, if the field's value is null (meaning that the
27   * field was omitted in the JSON document) we set it to {@code none()}.  This is necessary because
28   * there's no concept of default values in Gson.
29   * <li> For any field with a {@code @RequiredLink} annotation, we look for the corresponding link
30   * within the {@code _links} property and set it to that value, or signal an error if the link is
31   * missing.  This ensures, for instance, that an object can promise that it always has a {@code self}
32   * link and we will never return an instance that does not.
33   * <li> For any other field, if its value is null we signal an error.  Thus, no model properties are
34   * nullable; optional fields must always use {@code Option}.
35   * </ul>  
36   * @since 2.0.0
37   */
38  public abstract class EntityValidator
39  {
40      private static ConcurrentHashMap<Class<?>, Map<String, Field>> classFields =
41          new ConcurrentHashMap<Class<?>, Map<String,Field>>();
42      
43      public static <T> T validateInstance(T instance) throws SchemaViolationException
44      {
45          Iterable<SchemaViolation> violations = ImmutableList.of();
46          for (Field f: getClassFields(instance.getClass()).values())
47          {
48              violations = concat(violations, postProcessField(f, instance));
49          }
50          if (!isEmpty(violations))
51          {
52              throw new SchemaViolationException(violations);
53          }
54          return instance;
55      }
56      
57      private static Iterable<SchemaViolation> postProcessField(Field f, Object o)
58      {
59          f.setAccessible(true);
60          try
61          {
62              if (f.get(o) == null)
63              {
64                  // The field did not have a value in the source document (or had an explicit null value)
65                  if (Option.class.isAssignableFrom(f.getType()))
66                  {
67                      // That's OK, it's an optional field, just replace the null with none()
68                      f.set(o, none());
69                  }
70                  else
71                  {
72                      RequiredLink reqLinkAnno = f.getAnnotation(RequiredLink.class);
73                      if (reqLinkAnno != null)
74                      {
75                          // It's a calculated field derived from a link
76                          return setRequiredLinkField(reqLinkAnno, f, o);
77                      }
78                      else
79                      {
80                          // It's not optional and it's not a calculated field, so this is an error
81                          return Option.<SchemaViolation>some(new MissingRequiredField(o.getClass(), f.getName()));
82                      }
83                  }
84              }
85              // No schema violations detected
86              return none();
87          }
88          catch (IllegalAccessException e)
89          {
90              // This should never happen
91              throw new JsonParseException(e);
92          }
93      }
94      
95      private static Iterable<SchemaViolation> setRequiredLinkField(RequiredLink anno, Field f, Object o) throws IllegalAccessException
96      {
97          Field linksField = getClassFields(o.getClass()).get("_links");
98          if (linksField == null || linksField.getType() != Links.class)
99          {
100             throw new IllegalStateException("@RequiredLink annotation was found in a class without a 'Links _links' field");
101         }
102         Links links = (Links) linksField.get(o);
103         for (URI u: links.getUri(anno.rel()))
104         {
105             f.set(o, u);
106             return none();
107         }
108         return Option.<SchemaViolation>some(new MissingRequiredField(o.getClass(), "_links." + anno.rel()));
109     }
110     
111     public static Map<String, Field> getClassFields(Class<?> c)
112     {
113         // Unfortunately this logic is necessary because Class.getFields() won't give you private fields,
114         // but Class.getDeclaredFields() won't give you any superclass fields.
115         Map<String, Field> m = classFields.get(c);
116         if (m == null)
117         {
118             m = new HashMap<String, Field>();
119             Class<?> c1 = c;
120             while (c1 != null)
121             {
122                 for (Field f: c1.getDeclaredFields())
123                 {
124                     f.setAccessible(true);
125                     m.put(f.getName(), f);
126                 }
127                 c1 = c1.getSuperclass();
128             }
129             classFields.put(c, m);
130         }
131         return m;
132     }
133 }