1 package com.atlassian.marketplace.client.impl;
2
3 import java.io.InputStream;
4 import java.io.InputStreamReader;
5 import java.io.OutputStream;
6 import java.io.OutputStreamWriter;
7 import java.util.Collection;
8 import java.util.Map;
9 import java.util.Set;
10
11 import com.atlassian.marketplace.client.MpacException;
12
13 import com.google.common.base.Function;
14 import com.google.common.collect.ImmutableList;
15 import com.google.common.collect.ImmutableSet;
16 import com.google.gson.Gson;
17 import com.google.gson.GsonBuilder;
18 import com.google.gson.JsonArray;
19 import com.google.gson.JsonElement;
20 import com.google.gson.JsonIOException;
21 import com.google.gson.JsonObject;
22 import com.google.gson.JsonParseException;
23 import com.google.gson.TypeAdapter;
24 import com.google.gson.TypeAdapterFactory;
25 import com.google.gson.reflect.TypeToken;
26
27 import org.apache.commons.io.IOUtils;
28
29 import static com.atlassian.fugue.Iterables.flatMap;
30 import static com.google.common.collect.Iterables.concat;
31
32
33
34
35
36 public class JsonEntityEncoding implements EntityEncoding
37 {
38 private final Gson gsonWithReadOnlyFields = makeGson(true);
39 private final Gson gsonWithoutReadOnlyFields = makeGson(false);
40
41 private Gson makeGson(boolean includeReadOnlyFields)
42 {
43 GsonBuilder builder = new GsonBuilder()
44 .disableHtmlEscaping();
45 for (Map.Entry<Class<?>, Object> e: TypeAdapters.all().entrySet())
46 {
47 builder.registerTypeAdapter(e.getKey(), e.getValue());
48 }
49 return builder.registerTypeAdapterFactory(new BaseTypeAdapterFactory(includeReadOnlyFields)).create();
50 }
51
52 @Override
53 public <T> T decode(InputStream stream, Class<T> type) throws MpacException
54 {
55 try
56 {
57 return gsonWithReadOnlyFields.fromJson(new InputStreamReader(stream), type);
58 }
59 catch (JsonParseException e)
60 {
61 throw toMpacException(e);
62 }
63 }
64
65 private static MpacException toMpacException(Throwable e)
66 {
67 while (e.getCause() != null)
68 {
69 e = e.getCause();
70 }
71 if (e instanceof MpacException)
72 {
73 return (MpacException) e;
74 }
75 if (e instanceof SchemaViolationException)
76 {
77 return new MpacException.InvalidResponseError(((SchemaViolationException) e).getSchemaViolations());
78 }
79 return new MpacException.InvalidResponseError(e.getMessage(), e);
80 }
81
82 @Override
83 public <T> void encode(OutputStream stream, T entity, boolean includeReadOnlyFields) throws MpacException
84 {
85 Gson gson = includeReadOnlyFields ? gsonWithReadOnlyFields : gsonWithoutReadOnlyFields;
86 OutputStreamWriter w = new OutputStreamWriter(stream);
87 try
88 {
89 gson.toJson(entity, w);
90 }
91 catch (JsonIOException e)
92 {
93 throw new MpacException(e);
94 }
95 finally
96 {
97 IOUtils.closeQuietly(w);
98 }
99 }
100
101 @Override
102 public <T> void encodeChanges(OutputStream stream, T original, T updated) throws MpacException
103 {
104 JsonObject jOrig = gsonWithoutReadOnlyFields.toJsonTree(original).getAsJsonObject();
105 JsonObject jUpdated = gsonWithoutReadOnlyFields.toJsonTree(updated).getAsJsonObject();
106 JsonArray jResult = new JsonArray();
107 for (JsonElement n: makeJsonPatch(jOrig, jUpdated, ""))
108 {
109 jResult.add(n);
110 }
111 OutputStreamWriter w = new OutputStreamWriter(stream);
112 try
113 {
114 gsonWithoutReadOnlyFields.toJson(jResult, w);
115 }
116 catch (JsonIOException e)
117 {
118 throw new MpacException(e);
119 }
120 finally
121 {
122 IOUtils.closeQuietly(w);
123 }
124 }
125
126
127
128
129
130
131
132
133
134 private Iterable<JsonElement> makeJsonPatch(final JsonObject jOrig, final JsonObject jUpdated, final String basePath)
135 {
136 Iterable<JsonElement> addsAndReplaces = flatMap(jUpdated.entrySet(),
137 new Function<Map.Entry<String, JsonElement>, Iterable<JsonElement>>()
138 {
139 public Iterable<JsonElement> apply(Map.Entry<String, JsonElement> e)
140 {
141 String name = e.getKey();
142 JsonElement n1 = e.getValue();
143 if (!n1.isJsonNull())
144 {
145 String subPath = basePath + "/" + name;
146 JsonElement n0 = jOrig.get(name);
147 if (n0 != null && !n0.isJsonNull())
148 {
149 if (n0.isJsonObject() && n1.isJsonObject())
150 {
151 return makeJsonPatch(n0.getAsJsonObject(), n1.getAsJsonObject(), subPath);
152 }
153 else if (!n0.equals(n1))
154 {
155 JsonObject op = new JsonObject();
156 op.addProperty("op", "replace");
157 op.addProperty("path", subPath);
158 op.add("value", n1);
159 return ImmutableList.<JsonElement>of(op);
160 }
161 else
162 {
163 return ImmutableList.of();
164 }
165 }
166 else
167 {
168 JsonObject op = new JsonObject();
169 op.addProperty("op", "add");
170 op.addProperty("path", subPath);
171 op.add("value", n1);
172 return ImmutableList.<JsonElement>of(op);
173 }
174 }
175 else
176 {
177 return ImmutableList.of();
178 }
179 }
180 });
181 Iterable<JsonElement> removes = flatMap(ImmutableList.copyOf(jOrig.entrySet()),
182 new Function<Map.Entry<String, JsonElement>, Iterable<JsonElement>>()
183 {
184 public Iterable<JsonElement> apply(Map.Entry<String, JsonElement> e)
185 {
186 String name = e.getKey();
187 JsonElement n0 = e.getValue();
188 if (!n0.isJsonNull())
189 {
190 JsonElement n1 = jUpdated.get(name);
191 if (n1 == null || n1.isJsonNull())
192 {
193 JsonObject op = new JsonObject();
194 op.addProperty("op", "remove");
195 op.addProperty("path", basePath + "/" + name);
196 return ImmutableList.<JsonElement>of(op);
197 }
198 }
199 return ImmutableList.of();
200 }
201 });
202 return concat(addsAndReplaces, removes);
203 }
204
205
206
207
208 static class BaseTypeAdapterFactory implements TypeAdapterFactory
209 {
210 private static final Set<Class<?>> TYPES_WITH_DEFAULT_SERIALIZATION = ImmutableSet.<Class<?>>of(
211 String.class,
212 Boolean.class,
213 Integer.class,
214 Long.class,
215 Float.class,
216 Double.class
217 );
218
219 private final boolean includeReadOnlyFields;
220
221 BaseTypeAdapterFactory(boolean includeReadOnlyFields)
222 {
223 this.includeReadOnlyFields = includeReadOnlyFields;
224 }
225
226 @SuppressWarnings({ "unchecked" })
227 @Override
228 public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken)
229 {
230 Class<?> rawType = typeToken.getRawType();
231 if (rawType.isPrimitive() || TYPES_WITH_DEFAULT_SERIALIZATION.contains(rawType) ||
232 Map.class.isAssignableFrom(rawType) || Collection.class.isAssignableFrom(rawType) ||
233 TypeAdapters.all().keySet().contains(rawType))
234 {
235
236
237
238 return null;
239 }
240 if (rawType.isEnum())
241 {
242 return (TypeAdapter<T>) TypeAdapters.enumTypeAdapter(rawType);
243 }
244 return TypeAdapters.objectTypeAdapter(gson, typeToken, includeReadOnlyFields);
245 }
246 }
247 }