package org.inferred.freebuilder.processor; import static org.inferred.freebuilder.processor.util.ModelUtils.findAnnotationMirror; import static org.inferred.freebuilder.processor.util.ModelUtils.findProperty; import com.google.common.annotations.GwtCompatible; import com.google.common.base.Function; import com.google.common.base.Optional; import org.inferred.freebuilder.processor.Metadata.Property; import org.inferred.freebuilder.processor.Metadata.Visibility; import org.inferred.freebuilder.processor.util.Excerpt; import org.inferred.freebuilder.processor.util.Excerpts; import org.inferred.freebuilder.processor.util.QualifiedName; import org.inferred.freebuilder.processor.util.SourceBuilder; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.TypeElement; class GwtSupport { private static final QualifiedName CUSTOM_FIELD_SERIALIZER = QualifiedName.of("com.google.gwt.user.client.rpc", "CustomFieldSerializer"); private static final QualifiedName SERIALIZATION_EXCEPTION = QualifiedName.of("com.google.gwt.user.client.rpc", "SerializationException"); private static final QualifiedName SERIALIZATION_STREAM_READER = QualifiedName.of("com.google.gwt.user.client.rpc", "SerializationStreamReader"); private static final QualifiedName SERIALIZATION_STREAM_WRITER = QualifiedName.of("com.google.gwt.user.client.rpc", "SerializationStreamWriter"); public static Metadata.Builder gwtMetadata(TypeElement type, Metadata metadata) { Metadata.Builder extraMetadata = new Metadata.Builder(); Optional<AnnotationMirror> annotation = findAnnotationMirror(type, GwtCompatible.class); if (annotation.isPresent()) { extraMetadata.addGeneratedBuilderAnnotations(Excerpts.add("@%s%n", GwtCompatible.class)); Optional<AnnotationValue> serializable = findProperty(annotation.get(), "serializable"); if (serializable.isPresent() && serializable.get().getValue().equals(Boolean.TRUE)) { // Due to a bug in GWT's handling of nested types, we have to declare Value as package // scoped so Value_CustomFieldSerializer can access it. extraMetadata.setValueTypeVisibility(Visibility.PACKAGE); extraMetadata.addValueTypeAnnotations(Excerpts.add( "@%s(serializable = true)%n", GwtCompatible.class)); extraMetadata.addNestedClasses(new CustomValueSerializer()); extraMetadata.addNestedClasses(new GwtWhitelist()); QualifiedName builderName = metadata.getGeneratedBuilder().getQualifiedName(); extraMetadata.addVisibleNestedTypes( builderName.nestedType("Value_CustomFieldSerializer"), builderName.nestedType("GwtWhitelist")); } } return extraMetadata; } private static final class CustomValueSerializer implements Function<Metadata, Excerpt> { @Override public Excerpt apply(final Metadata metadata) { return new CustomValueSerializerExcerpt(metadata); } } private static final class CustomValueSerializerExcerpt extends Excerpt { private final Metadata metadata; private CustomValueSerializerExcerpt(Metadata metadata) { this.metadata = metadata; } @Override public void addTo(SourceBuilder code) { code.addLine("") .addLine("@%s", GwtCompatible.class); if (metadata.getType().isParameterized()) { code.addLine("@%s(\"unchecked\")", SuppressWarnings.class); } code.addLine("public static class Value_CustomFieldSerializer") .addLine(" extends %s<%s> {", CUSTOM_FIELD_SERIALIZER, metadata.getValueType()) .addLine("") .addLine(" @%s", Override.class) .addLine(" public void deserializeInstance(%s reader, %s instance) { }", SERIALIZATION_STREAM_READER, metadata.getValueType()) .addLine("") .addLine(" @%s", Override.class) .addLine(" public boolean hasCustomInstantiateInstance() {") .addLine(" return true;") .addLine(" }") .addLine("") .addLine(" @%s", Override.class) .addLine(" public %s instantiateInstance(%s reader)", metadata.getValueType(), SERIALIZATION_STREAM_READER) .addLine(" throws %s {", SERIALIZATION_EXCEPTION) .addLine(" %1$s builder = new %1$s();", metadata.getBuilder()); for (Property property : metadata.getProperties()) { if (property.getType().getKind().isPrimitive()) { code.addLine(" %s %s = reader.read%s();", property.getType(), property.getName(), withInitialCapital(property.getType())); property.getCodeGenerator() .addSetFromResult(code, "builder", property.getName()); } else if (String.class.getName().equals(property.getType().toString())) { code.addLine(" %s %s = reader.readString();", property.getType(), property.getName()); property.getCodeGenerator() .addSetFromResult(code, "builder", property.getName()); } else { code.addLine(" try {"); if (!property.isFullyCheckedCast()) { code.addLine(" @SuppressWarnings(\"unchecked\")"); } code.addLine(" %1$s %2$s = (%1$s) reader.readObject();", property.getType(), property.getName()); property.getCodeGenerator() .addSetFromResult(code, "builder", property.getName()); code.addLine(" } catch (%s e) {", ClassCastException.class) .addLine(" throw new %s(", SERIALIZATION_EXCEPTION) .addLine(" \"Wrong type for property '%s'\", e);", property.getName()) .addLine(" }"); } } code.addLine(" return (%s) builder.build();", metadata.getValueType()) .addLine(" }") .addLine("") .addLine(" @%s", Override.class) .addLine(" public void serializeInstance(%s writer, %s instance)", SERIALIZATION_STREAM_WRITER, metadata.getValueType()) .addLine(" throws %s {", SERIALIZATION_EXCEPTION); for (Property property : metadata.getProperties()) { if (property.getType().getKind().isPrimitive()) { code.add(" writer.write%s(", withInitialCapital(property.getType()), property.getName()); } else if (String.class.getName().equals(property.getType().toString())) { code.add(" writer.writeString(", property.getName()); } else { code.add(" writer.writeObject(", property.getName()); } property.getCodeGenerator().addReadValueFragment(code, "instance." + property.getName()); code.add(");\n"); } code.addLine(" }") .addLine("") .addLine(" private static final Value_CustomFieldSerializer INSTANCE =" + " new Value_CustomFieldSerializer();") .addLine("") .addLine(" public static void deserialize(%s reader, %s instance) {", SERIALIZATION_STREAM_READER, metadata.getValueType()) .addLine(" INSTANCE.deserializeInstance(reader, instance);") .addLine(" }") .addLine("") .addLine(" public static %s instantiate(%s reader)", metadata.getValueType(), SERIALIZATION_STREAM_READER) .addLine(" throws %s {", SERIALIZATION_EXCEPTION) .addLine(" return INSTANCE.instantiateInstance(reader);") .addLine(" }") .addLine("") .addLine(" public static void serialize(%s writer, %s instance)", SERIALIZATION_STREAM_WRITER, metadata.getValueType()) .addLine(" throws %s {", SERIALIZATION_EXCEPTION) .addLine(" INSTANCE.serializeInstance(writer, instance);") .addLine(" }") .addLine("}"); } @Override protected void addFields(FieldReceiver fields) { fields.add("metadata", metadata); } } private static final class GwtWhitelist implements Function<Metadata, Excerpt> { @Override public Excerpt apply(final Metadata metadata) { return new GwtWhitelistExcerpt(metadata); } } private static final class GwtWhitelistExcerpt extends Excerpt { private final Metadata metadata; private GwtWhitelistExcerpt(Metadata metadata) { this.metadata = metadata; } @Override public void addTo(SourceBuilder code) { code.addLine("") .addLine("/** This class exists solely to ensure GWT whitelists all required types. */") .addLine("@%s(serializable = true)", GwtCompatible.class) .addLine("static final class GwtWhitelist%s %s %s {", metadata.getType().declarationParameters(), metadata.isInterfaceType() ? "implements " : "extends ", metadata.getType()) .addLine(""); for (Property property : metadata.getProperties()) { code.addLine(" %s %s;", property.getType(), property.getName()); } code.addLine("") .addLine(" private GwtWhitelist() {") .addLine(" throw new %s();", UnsupportedOperationException.class) .addLine(" }"); for (Property property : metadata.getProperties()) { code.addLine("") .addLine(" @%s", Override.class) .addLine(" public %s %s() {", property.getType(), property.getGetterName()) .addLine(" throw new %s();", UnsupportedOperationException.class) .addLine(" }"); } code.addLine("}"); } @Override protected void addFields(FieldReceiver fields) { fields.add("metadata", metadata); } } private static String withInitialCapital(Object obj) { String s = obj.toString(); return s.substring(0, 1).toUpperCase() + s.substring(1); } }