1 /*
2  * Copyright (C) 2014 Square, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

16 package com.squareup.moshi;
17
18 import com.squareup.moshi.internal.Util;
19 import java.io.IOException;
20 import java.lang.annotation.Annotation;
21 import java.lang.reflect.Type;
22 import java.util.Arrays;
23 import java.util.Collection;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Set;
27 import javax.annotation.Nullable;
28
29 import static com.squareup.moshi.internal.Util.generatedAdapter;
30
31 final class StandardJsonAdapters {
32   private StandardJsonAdapters() {
33   }
34
35   public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
36     @Override public JsonAdapter<?> create(
37         Type type, Set<? extends Annotation> annotations, Moshi moshi) {
38       if (!annotations.isEmpty()) return null;
39       if (type == boolean.classreturn BOOLEAN_JSON_ADAPTER;
40       if (type == byte.classreturn BYTE_JSON_ADAPTER;
41       if (type == char.classreturn CHARACTER_JSON_ADAPTER;
42       if (type == double.classreturn DOUBLE_JSON_ADAPTER;
43       if (type == float.classreturn FLOAT_JSON_ADAPTER;
44       if (type == int.classreturn INTEGER_JSON_ADAPTER;
45       if (type == long.classreturn LONG_JSON_ADAPTER;
46       if (type == short.classreturn SHORT_JSON_ADAPTER;
47       if (type == Boolean.classreturn BOOLEAN_JSON_ADAPTER.nullSafe();
48       if (type == Byte.classreturn BYTE_JSON_ADAPTER.nullSafe();
49       if (type == Character.classreturn CHARACTER_JSON_ADAPTER.nullSafe();
50       if (type == Double.classreturn DOUBLE_JSON_ADAPTER.nullSafe();
51       if (type == Float.classreturn FLOAT_JSON_ADAPTER.nullSafe();
52       if (type == Integer.classreturn INTEGER_JSON_ADAPTER.nullSafe();
53       if (type == Long.classreturn LONG_JSON_ADAPTER.nullSafe();
54       if (type == Short.classreturn SHORT_JSON_ADAPTER.nullSafe();
55       if (type == String.classreturn STRING_JSON_ADAPTER.nullSafe();
56       if (type == Object.classreturn new ObjectJsonAdapter(moshi).nullSafe();
57
58       Class<?> rawType = Types.getRawType(type);
59
60       @Nullable JsonAdapter<?> generatedAdapter = generatedAdapter(moshi, type, rawType);
61       if (generatedAdapter != null) {
62         return generatedAdapter;
63       }
64
65       if (rawType.isEnum()) {
66         //noinspection unchecked
67         return new EnumJsonAdapter<>((Class<? extends Enum>) rawType).nullSafe();
68       }
69       return null;
70     }
71   };
72
73   private static final String ERROR_FORMAT = "Expected %s but was %s at path %s";
74
75   static int rangeCheckNextInt(JsonReader reader, String typeMessage, int min, int max)
76       throws IOException {
77     int value = reader.nextInt();
78     if (value < min || value > max) {
79       throw new JsonDataException(
80           String.format(ERROR_FORMAT, typeMessage, value, reader.getPath()));
81     }
82     return value;
83   }
84
85   static final JsonAdapter<Boolean> BOOLEAN_JSON_ADAPTER = new JsonAdapter<Boolean>() {
86     @Override public Boolean fromJson(JsonReader reader) throws IOException {
87       return reader.nextBoolean();
88     }
89
90     @Override public void toJson(JsonWriter writer, Boolean value) throws IOException {
91       writer.value(value.booleanValue());
92     }
93
94     @Override public String toString() {
95       return "JsonAdapter(Boolean)";
96     }
97   };
98
99   static final JsonAdapter<Byte> BYTE_JSON_ADAPTER = new JsonAdapter<Byte>() {
100     @Override public Byte fromJson(JsonReader reader) throws IOException {
101       return (byte) rangeCheckNextInt(reader, "a byte", Byte.MIN_VALUE, 0xff);
102     }
103
104     @Override public void toJson(JsonWriter writer, Byte value) throws IOException {
105       writer.value(value.intValue() & 0xff);
106     }
107
108     @Override public String toString() {
109       return "JsonAdapter(Byte)";
110     }
111   };
112
113   static final JsonAdapter<Character> CHARACTER_JSON_ADAPTER = new JsonAdapter<Character>() {
114     @Override public Character fromJson(JsonReader reader) throws IOException {
115       String value = reader.nextString();
116       if (value.length() > 1) {
117         throw new JsonDataException(
118             String.format(ERROR_FORMAT, "a char", '"' + value + '"', reader.getPath()));
119       }
120       return value.charAt(0);
121     }
122
123     @Override public void toJson(JsonWriter writer, Character value) throws IOException {
124       writer.value(value.toString());
125     }
126
127     @Override public String toString() {
128       return "JsonAdapter(Character)";
129     }
130   };
131
132   static final JsonAdapter<Double> DOUBLE_JSON_ADAPTER = new JsonAdapter<Double>() {
133     @Override public Double fromJson(JsonReader reader) throws IOException {
134       return reader.nextDouble();
135     }
136
137     @Override public void toJson(JsonWriter writer, Double value) throws IOException {
138       writer.value(value.doubleValue());
139     }
140
141     @Override public String toString() {
142       return "JsonAdapter(Double)";
143     }
144   };
145
146   static final JsonAdapter<Float> FLOAT_JSON_ADAPTER = new JsonAdapter<Float>() {
147     @Override public Float fromJson(JsonReader reader) throws IOException {
148       float value = (float) reader.nextDouble();
149       // Double check for infinity after float conversion; many doubles > Float.MAX
150       if (!reader.isLenient() && Float.isInfinite(value)) {
151         throw new JsonDataException("JSON forbids NaN and infinities: " + value
152             + " at path " + reader.getPath());
153       }
154       return value;
155     }
156
157     @Override public void toJson(JsonWriter writer, Float value) throws IOException {
158       // Manual null pointer check for the float.class adapter.
159       if (value == null) {
160         throw new NullPointerException();
161       }
162       // Use the Number overload so we write out float precision instead of double precision.
163       writer.value(value);
164     }
165
166     @Override public String toString() {
167       return "JsonAdapter(Float)";
168     }
169   };
170
171   static final JsonAdapter<Integer> INTEGER_JSON_ADAPTER = new JsonAdapter<Integer>() {
172     @Override public Integer fromJson(JsonReader reader) throws IOException {
173       return reader.nextInt();
174     }
175
176     @Override public void toJson(JsonWriter writer, Integer value) throws IOException {
177       writer.value(value.intValue());
178     }
179
180     @Override public String toString() {
181       return "JsonAdapter(Integer)";
182     }
183   };
184
185   static final JsonAdapter<Long> LONG_JSON_ADAPTER = new JsonAdapter<Long>() {
186     @Override public Long fromJson(JsonReader reader) throws IOException {
187       return reader.nextLong();
188     }
189
190     @Override public void toJson(JsonWriter writer, Long value) throws IOException {
191       writer.value(value.longValue());
192     }
193
194     @Override public String toString() {
195       return "JsonAdapter(Long)";
196     }
197   };
198
199   static final JsonAdapter<Short> SHORT_JSON_ADAPTER = new JsonAdapter<Short>() {
200     @Override public Short fromJson(JsonReader reader) throws IOException {
201       return (short) rangeCheckNextInt(reader, "a short", Short.MIN_VALUE, Short.MAX_VALUE);
202     }
203
204     @Override public void toJson(JsonWriter writer, Short value) throws IOException {
205       writer.value(value.intValue());
206     }
207
208     @Override public String toString() {
209       return "JsonAdapter(Short)";
210     }
211   };
212
213   static final JsonAdapter<String> STRING_JSON_ADAPTER = new JsonAdapter<String>() {
214     @Override public String fromJson(JsonReader reader) throws IOException {
215       return reader.nextString();
216     }
217
218     @Override public void toJson(JsonWriter writer, String value) throws IOException {
219       writer.value(value);
220     }
221
222     @Override public String toString() {
223       return "JsonAdapter(String)";
224     }
225   };
226
227   static final class EnumJsonAdapter<T extends Enum<T>> extends JsonAdapter<T> {
228     private final Class<T> enumType;
229     private final String[] nameStrings;
230     private final T[] constants;
231     private final JsonReader.Options options;
232
233     EnumJsonAdapter(Class<T> enumType) {
234       this.enumType = enumType;
235       try {
236         constants = enumType.getEnumConstants();
237         nameStrings = new String[constants.length];
238         for (int i = 0; i < constants.length; i++) {
239           T constant = constants[i];
240           Json annotation = enumType.getField(constant.name()).getAnnotation(Json.class);
241           String name = annotation != null ? annotation.name() : constant.name();
242           nameStrings[i] = name;
243         }
244         options = JsonReader.Options.of(nameStrings);
245       } catch (NoSuchFieldException e) {
246         throw new AssertionError("Missing field in " + enumType.getName(), e);
247       }
248     }
249
250     @Override public T fromJson(JsonReader reader) throws IOException {
251       int index = reader.selectString(options);
252       if (index != -1) return constants[index];
253
254       // We can consume the string safely, we are terminating anyway.
255       String path = reader.getPath();
256       String name = reader.nextString();
257       throw new JsonDataException("Expected one of "
258           + Arrays.asList(nameStrings) + " but was " + name + " at path " + path);
259     }
260
261     @Override public void toJson(JsonWriter writer, T value) throws IOException {
262       writer.value(nameStrings[value.ordinal()]);
263     }
264
265     @Override public String toString() {
266       return "JsonAdapter(" + enumType.getName() + ")";
267     }
268   }
269
270   /**
271    * This adapter is used when the declared type is {@code java.lang.Object}. Typically the runtime
272    * type is something else, and when encoding JSON this delegates to the runtime type's adapter.
273    * For decoding (where there is no runtime type to inspect), this uses maps and lists.
274    *
275    * <p>This adapter needs a Moshi instance to look up the appropriate adapter for runtime types as
276    * they are encountered.
277    */

278   static final class ObjectJsonAdapter extends JsonAdapter<Object> {
279     private final Moshi moshi;
280     private final JsonAdapter<List> listJsonAdapter;
281     private final JsonAdapter<Map> mapAdapter;
282     private final JsonAdapter<String> stringAdapter;
283     private final JsonAdapter<Double> doubleAdapter;
284     private final JsonAdapter<Boolean> booleanAdapter;
285
286     ObjectJsonAdapter(Moshi moshi) {
287       this.moshi = moshi;
288       this.listJsonAdapter = moshi.adapter(List.class);
289       this.mapAdapter = moshi.adapter(Map.class);
290       this.stringAdapter = moshi.adapter(String.class);
291       this.doubleAdapter = moshi.adapter(Double.class);
292       this.booleanAdapter = moshi.adapter(Boolean.class);
293     }
294
295     @Override public Object fromJson(JsonReader reader) throws IOException {
296       switch (reader.peek()) {
297         case BEGIN_ARRAY:
298           return listJsonAdapter.fromJson(reader);
299
300         case BEGIN_OBJECT:
301           return mapAdapter.fromJson(reader);
302
303         case STRING:
304           return stringAdapter.fromJson(reader);
305
306         case NUMBER:
307           return doubleAdapter.fromJson(reader);
308
309         case BOOLEAN:
310           return booleanAdapter.fromJson(reader);
311
312         case NULL:
313           return reader.nextNull();
314
315         default:
316           throw new IllegalStateException(
317               "Expected a value but was " + reader.peek() + " at path " + reader.getPath());
318       }
319     }
320
321     @Override public void toJson(JsonWriter writer, Object value) throws IOException {
322       Class<?> valueClass = value.getClass();
323       if (valueClass == Object.class) {
324         // Don't recurse infinitely when the runtime type is also Object.class.
325         writer.beginObject();
326         writer.endObject();
327       } else {
328         moshi.adapter(toJsonType(valueClass), Util.NO_ANNOTATIONS).toJson(writer, value);
329       }
330     }
331
332     /**
333      * Returns the type to look up a type adapter for when writing {@code value} to JSON. Without
334      * this, attempts to emit standard types like `LinkedHashMap` would fail because Moshi doesn't
335      * provide built-in adapters for implementation types. It knows how to <strong>write</strong>
336      * those types, but lacks a mechanism to read them because it doesn't know how to find the
337      * appropriate constructor.
338      */

339     private Class<?> toJsonType(Class<?> valueClass) {
340       if (Map.class.isAssignableFrom(valueClass)) return Map.class;
341       if (Collection.class.isAssignableFrom(valueClass)) return Collection.class;
342       return valueClass;
343     }
344
345     @Override public String toString() {
346       return "JsonAdapter(Object)";
347     }
348   }
349 }
350