View Javadoc

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   * Gson-based implementation of {@link EntityEncoding}.
34   * @since 2.0.0
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();  // we don't want it to transform '<' to an HTML entity within strings
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      * Creates a JSON document in the JSON Patch format to describe differences between two JSON documents. 
128      * @param jOrig  the original JSON document
129      * @param jUpdated  a document that is based on {@code jOrig} but may have had some properties added/changed/removed
130      * @param basePath  a JSON Pointer string such as "/_links" describing the path to the enclosing object if any,
131      * or an empty string if {@code jOrig} is the root document
132      * @return  a list of valid JSON Patch elements - empty if there were no changes
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     // We need a TypeAdapterFactory, in addition to the TypeAdapters we previously registered,
206     // because we need to be able to override the default reflective serialization for generic
207     // objects with our ObjectTypeAdapter, and we also have special handling for enums.
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                 // Returning null here just means Gson will use one of the TypeAdapters we previously
236                 // registered if appropriate, or else its default serialization, rather than
237                 // EnumTypeAdapter or ObjectTypeAdapter.
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 }