1 /*
2  * Copyright (C) 2015 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.Field;
22 import java.lang.reflect.InvocationTargetException;
23 import java.lang.reflect.Modifier;
24 import java.lang.reflect.ParameterizedType;
25 import java.lang.reflect.Type;
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.TreeMap;
29 import javax.annotation.Nullable;
30
31 import static com.squareup.moshi.internal.Util.resolve;
32 import static com.squareup.moshi.internal.Util.typeAnnotatedWithAnnotations;
33
34 /**
35  * Emits a regular class as a JSON object by mapping Java fields to JSON object properties.
36  *
37  * <h3>Platform Types</h3>
38  * Fields from platform classes are omitted from both serialization and deserialization unless
39  * they are either public or protected. This includes the following packages and their subpackages:
40  *
41  * <ul>
42  *   <li>android.*
43  *   <li>java.*
44  *   <li>javax.*
45  *   <li>kotlin.*
46  *   <li>scala.*
47  * </ul>
48  */

49 final class ClassJsonAdapter<T> extends JsonAdapter<T> {
50   public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
51     @Override public @Nullable JsonAdapter<?> create(
52         Type type, Set<? extends Annotation> annotations, Moshi moshi) {
53       if (!(type instanceof Class) && !(type instanceof ParameterizedType)) {
54         return null;
55       }
56       Class<?> rawType = Types.getRawType(type);
57       if (rawType.isInterface() || rawType.isEnum()) return null;
58       if (Util.isPlatformType(rawType) && !Types.isAllowedPlatformType(rawType)) {
59         throw new IllegalArgumentException("Platform "
60             + typeAnnotatedWithAnnotations(type, annotations)
61             + " requires explicit JsonAdapter to be registered");
62       }
63       if (!annotations.isEmpty()) return null;
64
65       if (rawType.isAnonymousClass()) {
66         throw new IllegalArgumentException("Cannot serialize anonymous class " + rawType.getName());
67       }
68       if (rawType.isLocalClass()) {
69         throw new IllegalArgumentException("Cannot serialize local class " + rawType.getName());
70       }
71       if (rawType.getEnclosingClass() != null && !Modifier.isStatic(rawType.getModifiers())) {
72         throw new IllegalArgumentException(
73             "Cannot serialize non-static nested class " + rawType.getName());
74       }
75       if (Modifier.isAbstract(rawType.getModifiers())) {
76         throw new IllegalArgumentException("Cannot serialize abstract class " + rawType.getName());
77       }
78
79       ClassFactory<Object> classFactory = ClassFactory.get(rawType);
80       Map<String, FieldBinding<?>> fields = new TreeMap<>();
81       for (Type t = type; t != Object.class; t = Types.getGenericSuperclass(t)) {
82         createFieldBindings(moshi, t, fields);
83       }
84       return new ClassJsonAdapter<>(classFactory, fields).nullSafe();
85     }
86
87     /** Creates a field binding for each of declared field of {@code type}. */
88     private void createFieldBindings(
89         Moshi moshi, Type type, Map<String, FieldBinding<?>> fieldBindings) {
90       Class<?> rawType = Types.getRawType(type);
91       boolean platformType = Util.isPlatformType(rawType);
92       for (Field field : rawType.getDeclaredFields()) {
93         if (!includeField(platformType, field.getModifiers())) continue;
94
95         // Look up a type adapter for this type.
96         Type fieldType = resolve(type, rawType, field.getGenericType());
97         Set<? extends Annotation> annotations = Util.jsonAnnotations(field);
98         String fieldName = field.getName();
99         JsonAdapter<Object> adapter = moshi.adapter(fieldType, annotations, fieldName);
100
101         // Create the binding between field and JSON.
102         field.setAccessible(true);
103
104         // Store it using the field's name. If there was already a field with this name, fail!
105         Json jsonAnnotation = field.getAnnotation(Json.class);
106         String name = jsonAnnotation != null ? jsonAnnotation.name() : fieldName;
107         FieldBinding<Object> fieldBinding = new FieldBinding<>(name, field, adapter);
108         FieldBinding<?> replaced = fieldBindings.put(name, fieldBinding);
109         if (replaced != null) {
110           throw new IllegalArgumentException("Conflicting fields:\n"
111               + "    " + replaced.field + "\n"
112               + "    " + fieldBinding.field);
113         }
114       }
115     }
116
117     /** Returns true if fields with {@code modifiers} are included in the emitted JSON. */
118     private boolean includeField(boolean platformType, int modifiers) {
119       if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers)) return false;
120       return Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers) || !platformType;
121     }
122   };
123
124   private final ClassFactory<T> classFactory;
125   private final FieldBinding<?>[] fieldsArray;
126   private final JsonReader.Options options;
127
128   ClassJsonAdapter(ClassFactory<T> classFactory, Map<String, FieldBinding<?>> fieldsMap) {
129     this.classFactory = classFactory;
130     this.fieldsArray = fieldsMap.values().toArray(new FieldBinding[fieldsMap.size()]);
131     this.options = JsonReader.Options.of(
132         fieldsMap.keySet().toArray(new String[fieldsMap.size()]));
133   }
134
135   @Override public T fromJson(JsonReader reader) throws IOException {
136     T result;
137     try {
138       result = classFactory.newInstance();
139     } catch (InstantiationException e) {
140       throw new RuntimeException(e);
141     } catch (InvocationTargetException e) {
142       throw Util.rethrowCause(e);
143     } catch (IllegalAccessException e) {
144       throw new AssertionError();
145     }
146
147     try {
148       reader.beginObject();
149       while (reader.hasNext()) {
150         int index = reader.selectName(options);
151         if (index == -1) {
152           reader.skipName();
153           reader.skipValue();
154           continue;
155         }
156         fieldsArray[index].read(reader, result);
157       }
158       reader.endObject();
159       return result;
160     } catch (IllegalAccessException e) {
161       throw new AssertionError();
162     }
163   }
164
165   @Override public void toJson(JsonWriter writer, T value) throws IOException {
166     try {
167       writer.beginObject();
168       for (FieldBinding<?> fieldBinding : fieldsArray) {
169         writer.name(fieldBinding.name);
170         fieldBinding.write(writer, value);
171       }
172       writer.endObject();
173     } catch (IllegalAccessException e) {
174       throw new AssertionError();
175     }
176   }
177
178   @Override public String toString() {
179     return "JsonAdapter(" + classFactory + ")";
180   }
181
182   static class FieldBinding<T> {
183     final String name;
184     final Field field;
185     final JsonAdapter<T> adapter;
186
187     FieldBinding(String name, Field field, JsonAdapter<T> adapter) {
188       this.name = name;
189       this.field = field;
190       this.adapter = adapter;
191     }
192
193     void read(JsonReader reader, Object value) throws IOException, IllegalAccessException {
194       T fieldValue = adapter.fromJson(reader);
195       field.set(value, fieldValue);
196     }
197
198     @SuppressWarnings("unchecked"// We require that field's values are of type T.
199     void write(JsonWriter writer, Object value) throws IllegalAccessException, IOException {
200       T fieldValue = (T) field.get(value);
201       adapter.toJson(writer, fieldValue);
202     }
203   }
204 }
205