// Copyright 2012 Google Inc. All Rights Reserved. // // 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.collide.dtogen; import com.google.collide.dtogen.shared.ClientToServerDto; import com.google.collide.dtogen.shared.RoutableDto; import com.google.collide.dtogen.shared.RoutingType; import com.google.collide.dtogen.shared.ServerToClientDto; import com.google.common.base.Preconditions; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Base template for the generated output file that contains all the DTOs. * * Note that we generate client and server DTOs in separate runs of the * generator. * * The directionality of the DTOs only affects whether or not we expose methods * to construct an instance of the DTO. We need both client and server versions * of all DTOs (irrespective of direction), but you aren't allowed to construct * a new {@link ServerToClientDto} on the client. And similarly, you aren't * allowed to construct a {@link ClientToServerDto} on the server. * */ public class DtoTemplate { public static class MalformedDtoInterfaceException extends RuntimeException { public MalformedDtoInterfaceException(String msg) { super(msg); } } // We keep a whitelist of allowed non-DTO generic types. static final Set<Class<?>> jreWhitelist = new HashSet<Class<?>>(Arrays.asList( new Class<?>[] {String.class, Integer.class, Double.class, Float.class, Boolean.class})); private final List<DtoImpl> dtoInterfaces = new ArrayList<DtoImpl>(); private final String packageName; private final String className; private final boolean isServerType; private final String apiHash; /** * @return whether or not the specified interface implements * {@link ClientToServerDto}. */ static boolean implementsClientToServerDto(Class<?> i) { return implementsInterface(i, ClientToServerDto.class); } /** * @return whether or not the specified interface implements * {@link ServerToClientDto}. */ static boolean implementsServerToClientDto(Class<?> i) { return implementsInterface(i, ServerToClientDto.class); } /** * Walks the superinterface hierarchy to determine if a Class implements some * target interface transitively. */ static boolean implementsInterface(Class<?> i, Class<?> target) { if (i.equals(target)) { return true; } boolean rtn = false; Class<?>[] superInterfaces = i.getInterfaces(); for (Class<?> superInterface : superInterfaces) { rtn = rtn || implementsInterface(superInterface, target); } return rtn; } /** * @return whether or not the specified interface implements * {@link RoutableDto}. */ private static boolean implementsRoutableDto(Class<?> i) { return implementsInterface(i, RoutableDto.class); } /** * Constructor. * * @param packageName The name of the package for the outer DTO class. * @param className The name of the outer DTO class. * @param isServerType Whether or not the DTO impls are client or server. */ DtoTemplate(String packageName, String className, String apiHash, boolean isServerType) { this.packageName = packageName; this.className = className; this.apiHash = apiHash; this.isServerType = isServerType; } /** * Adds an interface to the DtoTemplate for code generation. * * @param i */ public void addInterface(Class<?> i) { getDtoInterfaces().add(createDtoImplTemplate(i)); } /** * @return the dtoInterfaces */ public List<DtoImpl> getDtoInterfaces() { return dtoInterfaces; } /** * Returns the source code for a class that contains all the DTO impls for any * intefaces that were added via the {@link #addInterface(Class)} method. */ @Override public String toString() { StringBuilder builder = new StringBuilder(); emitPreamble(builder); emitClientFrontendApiVersion(builder); emitDtos(builder); emitPostamble(builder); return builder.toString(); } /** * Tests whether or not a given class is a part of our dto jar, and thus will * eventually have a generated Impl that is serializable (thus allowing it to * be a generic type). */ boolean isDtoInterface(Class<?> potentialDto) { for (DtoImpl dto : dtoInterfaces) { if (dto.getDtoInterface().equals(potentialDto)) { return true; } } return false; } /** * Will initialize the routing ID to be RoutableDto.INVALID_TYPE if it is not * routable. This is a small abuse of the intent of that value, but it allows * us to simply omit it from the routing type enumeration later. * * @param i the super interface type * @return a new DtoServerTemplate or a new DtoClientTemplate depending on * isServerImpl. */ private DtoImpl createDtoImplTemplate(Class<?> i) { int routingId = implementsRoutableDto(i) ? getRoutingId(i) : RoutableDto.NON_ROUTABLE_TYPE; return isServerType ? new DtoImplServerTemplate(this, routingId, i) : new DtoImplClientTemplate( this, routingId, i); } private void emitDtos(StringBuilder builder) { for (DtoImpl dto : getDtoInterfaces()) { builder.append(dto.serialize()); } } private void emitPostamble(StringBuilder builder) { builder.append("\n}"); } private void emitPreamble(StringBuilder builder) { builder.append("// GENERATED SOURCE. DO NOT EDIT.\npackage "); builder.append(packageName); builder.append(";\n\n"); if (isServerType) { builder.append("import com.google.collide.dtogen.server.JsonSerializable;\n"); builder.append("\n"); builder.append("import com.google.gson.Gson;\n"); builder.append("import com.google.gson.GsonBuilder;\n"); builder.append("import com.google.gson.JsonArray;\n"); builder.append("import com.google.gson.JsonElement;\n"); builder.append("import com.google.gson.JsonNull;\n"); builder.append("import com.google.gson.JsonObject;\n"); builder.append("import com.google.gson.JsonParser;\n"); builder.append("import com.google.gson.JsonPrimitive;\n"); builder.append("\n"); builder.append("import java.util.List;\n"); builder.append("import java.util.Map;\n"); } builder.append("\n\n@SuppressWarnings({\"unchecked\", \"cast\"})\n"); // Note that we always use fully qualified path names when referencing Types // so we need not add any import statements for anything. builder.append("public class "); builder.append(className); builder.append(" {\n\n"); if (isServerType) { builder.append(" private static final Gson gson = " + "new GsonBuilder().serializeNulls().create();\n\n"); } builder.append(" private "); builder.append(className); builder.append("() {}\n"); } /** * Emits a static variable that is the hash of all the classnames, methodnames, and return types * to be used as a version hash between client and server. */ private void emitClientFrontendApiVersion(StringBuilder builder) { builder.append("\n public static final String CLIENT_SERVER_PROTOCOL_HASH = \""); builder.append(getApiHash()); builder.append("\";\n"); } private String getApiHash() { return apiHash; } /** * Extracts the {@link RoutingType} annotation to derive the stable * routing type. */ private int getRoutingId(Class<?> i) { RoutingType routingTypeAnnotation = i.getAnnotation(RoutingType.class); Preconditions.checkNotNull(routingTypeAnnotation, "RoutingType annotation must be specified for all subclasses of RoutableDto. " + i.getName()); return routingTypeAnnotation.type(); } }