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
60
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
289
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
390
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
418
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 }