aboutsummaryrefslogtreecommitdiff
path: root/value/src/main/java/com/google/auto/value/processor/GwtSerialization.java
blob: 30ad09265cd99f933af9cc0c8d220e7e8ac941c0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
/*
 * Copyright 2014 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.auto.value.processor;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;

import com.google.auto.value.processor.AutoValueishProcessor.GetterProperty;
import com.google.auto.value.processor.PropertyBuilderClassifier.PropertyBuilder;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Multimap;
import com.google.escapevelocity.Template;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.zip.CRC32;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;

/**
 * Generates GWT serialization code for {@code @AutoValue} classes also marked
 * {@code @GwtCompatible(serializable = true)}.
 *
 * @author Éamonn McManus
 */
class GwtSerialization {
  private final GwtCompatibility gwtCompatibility;
  private final ProcessingEnvironment processingEnv;
  private final TypeElement type;

  GwtSerialization(
      GwtCompatibility gwtCompatibility, ProcessingEnvironment processingEnv, TypeElement type) {
    this.gwtCompatibility = gwtCompatibility;
    this.processingEnv = processingEnv;
    this.type = type;
  }

  private boolean shouldWriteGwtSerializer() {
    Optional<AnnotationMirror> optionalGwtCompatible = gwtCompatibility.gwtCompatibleAnnotation();
    if (optionalGwtCompatible.isPresent()) {
      AnnotationMirror gwtCompatible = optionalGwtCompatible.get();
      for (Map.Entry<ExecutableElement, AnnotationValue> entry :
          GwtCompatibility.getElementValues(gwtCompatible).entrySet()) {
        if (entry.getKey().getSimpleName().contentEquals("serializable")
            && entry.getValue().getValue().equals(true)) {
          return true;
        }
      }
    }
    return false;
  }

  /**
   * Writes the GWT serializer for the given type, if appropriate. An {@code @AutoValue} class gets
   * a GWT serializer if it is annotated with {@code @GwtCompatible(serializable = true)}, where the
   * {@code @GwtCompatible} annotation can come from any package.
   *
   * <p>If the type is com.example.Foo then the generated AutoValue subclass is
   * com.example.AutoValue_Foo and the GWT serializer is
   * com.example.AutoValue_Foo_CustomFieldSerializer.
   *
   * @param autoVars the template variables defined for this type.
   * @param finalSubclass the simple name of the AutoValue class being generated, AutoValue_Foo
   *     in the example.
   */
  void maybeWriteGwtSerializer(AutoValueTemplateVars autoVars, String finalSubclass) {
    if (shouldWriteGwtSerializer()) {
      GwtTemplateVars vars = new GwtTemplateVars();
      vars.pkg = autoVars.pkg;
      vars.subclass = finalSubclass;
      vars.formalTypes = autoVars.formalTypes;
      vars.actualTypes = autoVars.actualTypes;
      vars.useBuilder = !autoVars.builderTypeName.isEmpty();
      vars.builderSetters = autoVars.builderSetters;
      vars.builderPropertyBuilders = autoVars.builderPropertyBuilders;
      vars.generated = autoVars.generated;
      String className =
          (vars.pkg.isEmpty() ? "" : vars.pkg + ".") + vars.subclass + "_CustomFieldSerializer";
      vars.serializerClass = TypeSimplifier.simpleNameOf(className);
      vars.props =
          autoVars.props.stream().map(p -> new Property((GetterProperty) p)).collect(toList());
      vars.classHashString = computeClassHash(autoVars.props, vars.pkg);
      String text = vars.toText();
      text = TypeEncoder.decode(text, processingEnv, vars.pkg, type.asType());
      writeSourceFile(className, text, type);
    }
  }

  public static class Property {
    private final GetterProperty property;
    private final boolean isCastingUnchecked;

    Property(GetterProperty property) {
      this.property = property;
      this.isCastingUnchecked = TypeSimplifier.isCastingUnchecked(property.getTypeMirror());
    }

    @Override
    public String toString() {
      return property.toString();
    }

    public String getGetter() {
      return property.getGetter();
    }

    public String getType() {
      return property.getType();
    }

    public String getName() {
      return property.getName();
    }

    /**
     * Returns the suffix in serializer method names for values of the given type. For example, if
     * the type is "int" then the returned value will be "Int" because the serializer methods are
     * called readInt and writeInt. There are methods for all primitive types and String; every
     * other type uses readObject and writeObject.
     */
    public String getGwtType() {
      TypeMirror typeMirror = property.getTypeMirror();
      String type = typeMirror.toString();
      if (property.getKind().isPrimitive()) {
        return Character.toUpperCase(type.charAt(0)) + type.substring(1);
      } else if (type.equals("java.lang.String")) {
        return "String";
      } else {
        return "Object";
      }
    }

    /**
     * Returns a string to be inserted before the call to the readFoo() call so that the expression
     * can be assigned to the given type. For primitive types and String, the readInt() etc methods
     * already return the right type so the string is empty. For other types, the string is a cast
     * like "(Foo) ".
     */
    public String getGwtCast() {
      if (property.getKind().isPrimitive() || getType().equals("String")) {
        return "";
      } else {
        return "(" + getType() + ") ";
      }
    }

    public boolean isCastingUnchecked() {
      return isCastingUnchecked;
    }
  }

  @SuppressWarnings("unused") // some fields are only read through reflection
  static class GwtTemplateVars extends TemplateVars {
    /** The properties defined by the parent class's abstract methods. */
    List<Property> props;

    /**
     * The package of the class with the {@code @AutoValue} annotation and its generated subclass.
     */
    String pkg;

    /** The simple name of the generated subclass. */
    String subclass;

    /**
     * The formal generic signature of the class with the {@code @AutoValue} annotation and its
     * generated subclass. This is empty, or contains type variables with optional bounds, for
     * example {@code <K, V extends K>}.
     */
    String formalTypes;
    /**
     * The generic signature used by the generated subclass for its superclass reference. This is
     * empty, or contains only type variables with no bounds, for example {@code <K, V>}.
     */
    String actualTypes;

    /** True if the {@code @AutoValue} class is constructed using a generated builder. */
    Boolean useBuilder;

    /**
     * A multimap from property names (like foo) to the corresponding setter methods (foo or
     * setFoo).
     */
    Multimap<String, BuilderSpec.PropertySetter> builderSetters;

    /**
     * A map from property names to information about the associated property builder. A property
     * called foo (defined by a method foo() or getFoo()) can have a property builder called
     * fooBuilder(). The type of foo must be a type that has an associated builder following certain
     * conventions. Guava immutable types such as ImmutableList follow those conventions, as do many
     * {@code @AutoValue} types.
     */
    ImmutableMap<String, PropertyBuilder> builderPropertyBuilders = ImmutableMap.of();

    /** The simple name of the generated GWT serializer class. */
    String serializerClass;

    /**
     * The encoding of the {@code Generated} class. Empty if no {@code Generated} class is
     * available.
     */
    String generated;

    /** A string that should change if any salient details of the serialized class change. */
    String classHashString;

    private static final Template TEMPLATE = parsedTemplateForResource("gwtserializer.vm");

    @Override
    Template parsedTemplate() {
      return TEMPLATE;
    }
  }

  private void writeSourceFile(String className, String text, TypeElement originatingType) {
    try {
      JavaFileObject sourceFile =
          processingEnv.getFiler().createSourceFile(className, originatingType);
      try (Writer writer = sourceFile.openWriter()) {
        writer.write(text);
      }
    } catch (IOException e) {
      processingEnv
          .getMessager()
          .printMessage(
              Diagnostic.Kind.WARNING, "Could not write generated class " + className + ": " + e);
      // A warning rather than an error for the reason explained in
      // AutoValueishProcessor.writeSourceFile.
    }
  }

  // Compute a hash that is guaranteed to change if the names, types, or order of the fields
  // change. We use TypeEncoder so that we can get a defined string for types, since
  // TypeMirror.toString() isn't guaranteed to remain the same.
  private String computeClassHash(Iterable<AutoValueishProcessor.Property> props, String pkg) {
    CRC32 crc = new CRC32();
    String encodedType = TypeEncoder.encode(type.asType()) + ":";
    String decodedType = TypeEncoder.decode(encodedType, processingEnv, "", null);
    if (!decodedType.startsWith(pkg)) {
      // This is for compatibility with the way an earlier version did things. Preserving hash
      // codes probably isn't vital, since client and server should be in sync.
      decodedType = pkg + "." + decodedType;
    }
    crc.update(decodedType.getBytes(UTF_8));
    for (AutoValueishProcessor.Property prop : props) {
      String encodedProp = prop + ":" + TypeEncoder.encode(prop.getTypeMirror()) + ";";
      String decodedProp = TypeEncoder.decode(encodedProp, processingEnv, pkg, null);
      crc.update(decodedProp.getBytes(UTF_8));
    }
    return String.format("%08x", crc.getValue());
  }
}