View Javadoc

1   package com.atlassian.marketplace.client.impl;
2   
3   import java.io.IOException;
4   import java.lang.reflect.ParameterizedType;
5   import java.lang.reflect.Type;
6   import java.net.URI;
7   import java.util.List;
8   import java.util.Map;
9   
10  import com.atlassian.fugue.Option;
11  import com.atlassian.marketplace.client.api.ApplicationKey;
12  import com.atlassian.marketplace.client.api.EnumWithKey;
13  import com.atlassian.marketplace.client.api.UriTemplate;
14  import com.atlassian.marketplace.client.encoding.InvalidFieldValue;
15  import com.atlassian.marketplace.client.model.HtmlString;
16  import com.atlassian.marketplace.client.model.Link;
17  import com.atlassian.marketplace.client.model.Links;
18  import com.atlassian.marketplace.client.model.ReadOnly;
19  import com.atlassian.marketplace.client.model.RequiredLink;
20  
21  import com.google.common.base.Function;
22  import com.google.common.collect.ImmutableList;
23  import com.google.common.collect.ImmutableMap;
24  import com.google.gson.ExclusionStrategy;
25  import com.google.gson.FieldAttributes;
26  import com.google.gson.FieldNamingPolicy;
27  import com.google.gson.Gson;
28  import com.google.gson.InstanceCreator;
29  import com.google.gson.JsonArray;
30  import com.google.gson.JsonDeserializationContext;
31  import com.google.gson.JsonDeserializer;
32  import com.google.gson.JsonElement;
33  import com.google.gson.JsonNull;
34  import com.google.gson.JsonObject;
35  import com.google.gson.JsonParseException;
36  import com.google.gson.JsonPrimitive;
37  import com.google.gson.JsonSerializationContext;
38  import com.google.gson.JsonSerializer;
39  import com.google.gson.TypeAdapter;
40  import com.google.gson.TypeAdapterFactory;
41  import com.google.gson.internal.ConstructorConstructor;
42  import com.google.gson.internal.Excluder;
43  import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory;
44  import com.google.gson.reflect.TypeToken;
45  import com.google.gson.stream.JsonReader;
46  import com.google.gson.stream.JsonWriter;
47  
48  import org.joda.time.DateTime;
49  import org.joda.time.LocalDate;
50  
51  import static com.atlassian.fugue.Option.none;
52  import static com.atlassian.fugue.Option.some;
53  import static com.atlassian.marketplace.client.encoding.DateFormats.DATE_FORMAT;
54  import static com.atlassian.marketplace.client.encoding.DateFormats.DATE_TIME_FORMAT;
55  import static com.atlassian.marketplace.client.impl.EntityValidator.validateInstance;
56  import static com.google.gson.internal.$Gson$Types.newParameterizedTypeWithOwner;
57  
58  /**
59   * Manages all types for which we have custom JSON (de)serialization.
60   * @since 2.0.0
61   */
62  abstract class TypeAdapters
63  {
64      private static TypeAdapterFactory factoryWithReadOnlyFields = makeTypeAdapterFactory(true);
65      private static TypeAdapterFactory factoryWithoutReadOnlyFields = makeTypeAdapterFactory(false);
66      
67      private TypeAdapters()
68      {
69      }
70      
71      public static Map<Class<?>, Object> all()
72      {
73          return ADAPTERS;
74      }
75      
76      @SuppressWarnings("unchecked")
77      static <A extends EnumWithKey> TypeAdapter<A> enumTypeAdapter(final Class<?> enumClass)
78      {
79          final EnumWithKey.Parser<A> parser = EnumWithKey.Parser.forType((Class<A>) enumClass);
80  
81          return new TypeAdapter<A>()
82          {
83              @Override
84              public void write(JsonWriter out, A value) throws IOException
85              {
86                  out.value(value.getKey());
87              }
88          
89              @Override
90              public A read(JsonReader in) throws IOException
91              {
92                  String s = in.nextString();
93                  for (A v: parser.valueForKey(s))
94                  {
95                      return v;
96                  }
97                  throw new SchemaViolationException(new InvalidFieldValue(s, enumClass));
98              }
99          };
100     }
101     
102     static <A> TypeAdapter<A> objectTypeAdapter(Gson gson, com.google.gson.reflect.TypeToken<A> typeToken,
103         boolean includeReadOnlyFields)
104     {
105         TypeAdapterFactory factory = includeReadOnlyFields ? factoryWithReadOnlyFields : factoryWithoutReadOnlyFields;
106         final TypeAdapter<A> baseAdapter = factory.create(gson, typeToken);
107         
108         return new TypeAdapter<A>()
109         {
110             @Override
111             public void write(JsonWriter out, A value) throws IOException
112             {
113                 baseAdapter.write(out, value);
114             }
115         
116             @Override
117             public A read(JsonReader in) throws IOException
118             {
119                 try
120                 {
121                     return validateInstance(baseAdapter.read(in));
122                 }
123                 catch (SchemaViolationException e)
124                 {
125                     throw new JsonParseException(e);
126                 }
127             }
128         };
129     }
130     
131     private interface JsonSerDeser<A> extends JsonSerializer<A>, JsonDeserializer<A>
132     {
133     }
134     
135     private static <A> JsonSerDeser<A> stringLikeTypeAdapter(final Function<A, String> writer, final Function<String, A> reader)
136     {
137         return new JsonSerDeser<A>()
138         {
139             @Override
140             public JsonElement serialize(A value, Type type, JsonSerializationContext context)
141             {
142                 return new JsonPrimitive(writer.apply(value));
143             }
144 
145             @Override
146             public A deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException
147             {
148                 try
149                 {
150                     return reader.apply(json.getAsString());
151                 }
152                 catch (IllegalArgumentException e)
153                 {
154                     String s = json.isJsonPrimitive() ? json.getAsString() : json.toString();
155                     throw new SchemaViolationException(new InvalidFieldValue(s, (Class<?>) type));
156                 }
157             }
158         }; 
159     }
160 
161     private static final Function<ApplicationKey, String> appKeyToString = new Function<ApplicationKey, String>()
162     {
163         public String apply(ApplicationKey value)
164         {
165             return value.getKey();
166         }
167     };
168 
169     private static final Function<String, ApplicationKey> stringToAppKey = new Function<String, ApplicationKey>()
170     {
171         public ApplicationKey apply(String value)
172         {
173             return ApplicationKey.valueOf(value);
174         }
175     };
176 
177     private static final Function<DateTime, String> dateTimeToString = new Function<DateTime, String>()
178     {
179         public String apply(DateTime value)
180         {
181             return DATE_TIME_FORMAT.print(value);
182         }
183     };
184 
185     private static final Function<String, DateTime> stringToDateTime = new Function<String, DateTime>()
186     {
187         public DateTime apply(String value)
188         {
189             return DATE_TIME_FORMAT.parseDateTime(value);
190         }
191     };
192 
193     private static final Function<HtmlString, String> htmlStringToString = new Function<HtmlString, String>()
194     {
195         public String apply(HtmlString value)
196         {
197             return value.getHtml();
198         }
199     };
200 
201     private static final Function<String, HtmlString> stringToHtmlString = new Function<String, HtmlString>()
202     {
203         public HtmlString apply(String value)
204         {
205             return HtmlString.html(value);
206         }
207     };
208 
209     private static final Function<LocalDate, String> localDateToString = new Function<LocalDate, String>()
210     {
211         public String apply(LocalDate value)
212         {
213             return DATE_FORMAT.print(value);
214         }
215     };
216 
217     private static final Function<String, LocalDate> stringToLocalDate = new Function<String, LocalDate>()
218     {
219         public LocalDate apply(String value)
220         {
221             return DATE_FORMAT.parseDateTime(value).toLocalDate();
222         }
223     };
224 
225     private static final Function<URI, String> uriToString = new Function<URI, String>()
226     {
227         public String apply(URI value)
228         {
229             return value.toASCIIString();
230         }
231     };
232 
233     private static final Function<String, URI> stringToUri = new Function<String, URI>()
234     {
235         public URI apply(String value)
236         {
237             return URI.create(value);
238         }
239     };
240 
241     private static final Map<Class<?>, Object> ADAPTERS = ImmutableMap.<Class<?>, Object>builder()
242         .put(ApplicationKey.class, stringLikeTypeAdapter(appKeyToString, stringToAppKey))
243         .put(DateTime.class, stringLikeTypeAdapter(dateTimeToString, stringToDateTime))
244         .put(HtmlString.class, stringLikeTypeAdapter(htmlStringToString, stringToHtmlString))
245         .put(LocalDate.class, stringLikeTypeAdapter(localDateToString, stringToLocalDate))
246         .put(URI.class, stringLikeTypeAdapter(uriToString, stringToUri))
247         .put(ImmutableList.class, new ListTypeAdapter())
248         .put(ImmutableMap.class, new MapTypeAdapter())
249         .put(Option.class, new OptionTypeAdapter())
250         .put(Link.class, new LinkTypeAdapter())
251         .put(Links.class, new LinksTypeAdapter())
252         .build();
253 
254     private static TypeAdapterFactory makeTypeAdapterFactory(boolean includeReadOnlyFields)
255     {
256         return new ReflectiveTypeAdapterFactory(
257             new ConstructorConstructor(ImmutableMap.<Type, InstanceCreator<?>>of()),
258             FieldNamingPolicy.IDENTITY,
259             new Excluder().withExclusionStrategy(new CustomExclusionStrategy(includeReadOnlyFields), true, true)
260         );
261     }
262     
263     private static class CustomExclusionStrategy implements ExclusionStrategy
264     {
265         private final boolean includeReadOnlyFields;
266         
267         CustomExclusionStrategy(boolean includeReadOnlyFields)
268         {
269             this.includeReadOnlyFields = includeReadOnlyFields;
270         }
271         
272         @Override
273         public boolean shouldSkipField(FieldAttributes f)
274         {
275             return (f.getAnnotation(RequiredLink.class) != null) ||
276                 (!includeReadOnlyFields && (f.getAnnotation(ReadOnly.class) != null));
277         }
278 
279         @Override
280         public boolean shouldSkipClass(Class<?> clazz)
281         {
282             return false;
283         }
284     }
285     
286     private static class LinkTypeAdapter implements JsonDeserializer<Link>, JsonSerializer<Link>
287     {
288         // The HAL-JSON schema supports two kinds of links, a regular URI or a URI template, depending on
289         // the "templated" property.
290         
291         @Override
292         public JsonElement serialize(Link link, Type type, JsonSerializationContext context)
293         {
294             LinkInternal li = new LinkInternal();
295             li.href = link.getTemplateOrUri().fold(
296                 new Function<UriTemplate, String>()
297                 {
298                     public String apply(UriTemplate u)
299                     {
300                         return u.getValue();
301                     }
302                 },
303                 new Function<URI, String>()
304                 {
305                     public String apply(URI u)
306                     {
307                         return u.toASCIIString();
308                     }
309                 }
310             );
311             li.type = link.getType();
312             li.templated = link.getUriTemplate().isDefined() ? some(true) : none(Boolean.class);
313             return context.serialize(li);
314         }
315 
316         @Override
317         public Link deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException
318         {
319             LinkInternal li = (LinkInternal) context.deserialize(json, LinkInternal.class);
320             if (li.templated.getOrElse(false))
321             {
322                 return Link.fromUriTemplate(UriTemplate.create(li.href), li.type);
323             }
324             return Link.fromUri(URI.create(li.href), li.type);
325         }
326             
327         private static class LinkInternal
328         {
329             String href;
330             Option<String> type;
331             Option<Boolean> templated;
332         }
333     }
334     
335     private static class LinksTypeAdapter implements JsonDeserializer<Links>, JsonSerializer<Links>
336     {
337         private static final Type linkListType = (new TypeToken<List<Link>>() { }).getType();
338         
339         @Override
340         public JsonElement serialize(Links links, Type type, JsonSerializationContext context)
341         {
342             JsonObject out = new JsonObject();
343             for (Map.Entry<String, ImmutableList<Link>> e: links.getItems().entrySet())
344             {
345                 JsonElement value;
346                 if (e.getValue().size() == 1)
347                 {
348                     value = context.serialize(e.getValue().get(0));
349                 }
350                 else
351                 {
352                     JsonArray a = new JsonArray();
353                     for (Link l: e.getValue())
354                     {
355                         a.add(context.serialize(l));
356                     }
357                     value = a;
358                 }
359                 out.add(e.getKey(), value);
360             }
361             return out;
362         }
363 
364         @SuppressWarnings("unchecked")
365         @Override
366         public Links deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException
367         {
368             ImmutableMap.Builder<String, ImmutableList<Link>> links = ImmutableMap.builder();
369             JsonObject o = json.getAsJsonObject();
370             for (Map.Entry<String, JsonElement> e: o.entrySet())
371             {
372                 ImmutableList<Link> value;
373                 if (e.getValue().isJsonArray())
374                 {
375                     value = ImmutableList.copyOf((List<Link>) context.deserialize(e.getValue(), linkListType));
376                 }
377                 else
378                 {
379                     value = ImmutableList.of((Link) context.deserialize(e.getValue(), Link.class));
380                 }
381                 links.put(e.getKey(), value);
382             }
383             return new Links(links.build());
384         }
385     }
386     
387     private static class ListTypeAdapter implements JsonDeserializer<ImmutableList<?>>, JsonSerializer<ImmutableList<?>>
388     {
389         // Gson can already serialize a list, but we want to make sure that they're always deserialized as
390         // immutable Guava lists.
391 
392         @Override
393         public ImmutableList<?> deserialize(JsonElement json, Type type, JsonDeserializationContext context)
394             throws JsonParseException
395         {
396             Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments();
397             Type listType = makeListType(typeArguments[0]);
398             return ImmutableList.copyOf((List<?>) context.deserialize(json, listType));
399         }
400     
401         @Override
402         public JsonElement serialize(ImmutableList<?> list, Type type, JsonSerializationContext context)
403         {
404             Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments();
405             Type listType = makeListType(typeArguments[0]);
406             return context.serialize(list, listType);
407         }
408     
409         private <T> Type makeListType(Type typeParam)
410         {
411             return newParameterizedTypeWithOwner(null, List.class, typeParam);
412         }
413     }
414 
415     private static class MapTypeAdapter implements JsonDeserializer<ImmutableMap<?, ?>>, JsonSerializer<ImmutableMap<?, ?>>
416     {
417         // Gson can already serialize a map, but we want to make sure that they're always deserialized as
418         // immutable Guava maps.
419         
420         @Override
421         public ImmutableMap<?, ?> deserialize(JsonElement json, Type type, JsonDeserializationContext context)
422             throws JsonParseException
423         {
424             Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments();
425             Type mapType = makeMapType(typeArguments[0], typeArguments[1]);
426             return ImmutableMap.copyOf((Map<?, ?>) context.deserialize(json, mapType));
427         }
428     
429         @Override
430         public JsonElement serialize(ImmutableMap<?, ?> map, Type type, JsonSerializationContext context)
431         {
432             Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments();
433             Type mapType = makeMapType(typeArguments[0], typeArguments[1]);
434             return context.serialize(map, mapType);
435         }
436     
437         private <A, B> Type makeMapType(Type keyTypeParam, Type valueTypeParam)
438         {
439             return newParameterizedTypeWithOwner(null, Map.class, keyTypeParam, valueTypeParam);
440         }
441     }
442 
443     private static class OptionTypeAdapter implements JsonDeserializer<Option<?>>, JsonSerializer<Option<?>>
444     {
445         @Override
446         public JsonElement serialize(Option<?> o, Type type, JsonSerializationContext context)
447         {
448             Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments();
449             for (Object value: o)
450             {
451                 return context.serialize(value, typeArguments[0]);
452             }
453             return JsonNull.INSTANCE;
454         }
455 
456         @Override
457         public Option<?> deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException
458         {
459             if (json.isJsonNull())
460             {
461                 return none();
462             }
463             Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments();
464             return some(context.deserialize(json, typeArguments[0]));
465         }
466     }
467 }