// 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.SerializationIndex; import com.google.collide.dtogen.shared.ServerToClientDto; import com.google.collide.json.shared.JsonArray; import com.google.collide.json.shared.JsonStringMap; import com.google.common.base.Preconditions; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.List; /** * Generates the source code for a generated Client DTO impl. * */ public class DtoImplClientTemplate extends DtoImpl { private static final String ROUTABLE_DTO_IMPL = RoutableDto.class.getPackage().getName().replace("dtogen.shared", "dtogen.client") + ".RoutableDtoClientImpl"; private static final String JSO_TYPE = "com.google.collide.json.client.Jso"; private static boolean isEnum(Class<?> type) { return type != null && (type.equals(Enum.class) || isEnum(type.getSuperclass())); } DtoImplClientTemplate(DtoTemplate template, int routingType, Class<?> dtoInterface) { super(template, routingType, dtoInterface); } @Override String serialize() { StringBuilder builder = new StringBuilder(); Class<?> dtoInterface = getDtoInterface(); List<Method> methods = getDtoMethods(); emitPreamble(dtoInterface, builder); emitMethods(methods, builder); // Only emit a factory method if the supertype is a ClientToServerDto or is // non-routable. if (DtoTemplate.implementsClientToServerDto(dtoInterface) || getRoutingType() == RoutableDto.NON_ROUTABLE_TYPE) { emitFactoryMethod(builder); } else { builder.append("\n }\n"); // emit testing mock, with factory, as a separate subclass emitMockPreamble(dtoInterface, builder); emitFactoryMethod(builder); } builder.append(" }\n"); return builder.toString(); } /** * Emits a factory method that trivially returns a new Javascript object with * the type set. */ private void emitFactoryMethod(StringBuilder builder) { builder.append("\n public static native "); builder.append(getImplClassName()); builder.append(" make() /*-{\n"); if (isCompactJson()) { builder.append(" return [];"); } else { builder.append(" return {\n"); if (getRoutingType() != RoutableDto.NON_ROUTABLE_TYPE) { emitKeyValue(RoutableDto.TYPE_FIELD, Integer.toString(getRoutingType()), builder); } builder.append("\n };"); } builder.append("\n }-*/;"); } private void emitHasMethod(String methodName, String fieldSelector, StringBuilder builder) { builder.append("\n public final native boolean ").append(methodName).append("() /*-{\n"); builder.append(" return this.hasOwnProperty(").append(fieldSelector).append(");\n"); builder.append(" }-*/;\n"); } private void emitGetter(Method method, String methodName, String fieldSelector, String returnType, StringBuilder builder) { builder.append("\n @Override\n public final native "); builder.append(returnType); builder.append(" "); builder.append(methodName); builder.append("() /*-{\n"); if (isCompactJson()) { // We can omit last members in list it they do not carry any information. // Currently we skip only one member, and only if it is an array. // We can add more cases and more robust "tail" detection in the future. if (isLastMethod(method)) { List<Type> expandedTypes = expandType(method.getGenericReturnType()); if (isJsonArray(getRawClass(expandedTypes.get(0)))) { builder.append(" if (!this.hasOwnProperty(").append(fieldSelector).append(")) {\n"); builder.append(" this[").append(fieldSelector).append("] = [];\n"); builder.append(" }\n"); } } } emitReturn(method, fieldSelector, builder); builder.append(" }-*/;\n"); } private void emitKeyValue(String fieldName, String value, StringBuilder builder) { builder.append(" "); builder.append(fieldName); builder.append(": "); builder.append(value); } private void emitMethods(List<Method> methods, StringBuilder builder) { for (Method method : methods) { if (ignoreMethod(method)) { continue; } String methodName = method.getName(); String fieldName = getFieldName(methodName); String fieldSelector; if (isCompactJson()) { SerializationIndex serializationIndex = Preconditions.checkNotNull( method.getAnnotation(SerializationIndex.class)); fieldSelector = String.valueOf(serializationIndex.value() - 1); } else { fieldSelector = "\"" + getFieldName(methodName) + "\""; } String returnTypeName = method.getGenericReturnType().toString().replace('$', '.').replace("class ", "") .replace("interface ", ""); // Native JSNI Getter. emitGetter(method, methodName, fieldSelector, returnTypeName, builder); // Native JSNI Setter emitSetter(getSetterName(fieldName), fieldName, fieldSelector, method.getGenericReturnType(), returnTypeName, getImplClassName(), builder); emitHasMethod("has" + getCamelCaseName(fieldName), fieldSelector, builder); } } private void emitMockPreamble(Class<?> dtoInterface, StringBuilder builder) { builder.append("\n\n public static class Mock"); builder.append(getImplClassName()); builder.append(" extends "); builder.append(getImplClassName()); builder.append(" {\n protected Mock"); builder.append(getImplClassName()); builder.append("() {}\n"); } private void emitPreamble(Class<?> dtoInterface, StringBuilder builder) { builder.append("\n\n public static class "); builder.append(getImplClassName()); builder.append(" extends "); Class<?> superType = getSuperInterface(); if (superType != null) { // We special case ServerToClientDto and ClientToServerDto since their // impls are not generated. if (superType.equals(ServerToClientDto.class) || superType.equals(ClientToServerDto.class)) { builder.append(ROUTABLE_DTO_IMPL); } else { builder.append(superType.getSimpleName() + "Impl"); } } else { // Just a plain Jso. builder.append(JSO_TYPE); } builder.append(" implements "); builder.append(dtoInterface.getCanonicalName()); builder.append(" {\n protected "); builder.append(getImplClassName()); builder.append("() {}\n"); } private void emitReturn(Method method, String fieldSelector, StringBuilder builder) { Type type = method.getGenericReturnType(); Class<?> rawClass = getRawClass(type); String thisFieldName = "this[" + fieldSelector + "]"; if (type instanceof ParameterizedType) { List<Type> expandedTypes = expandType(type); if (hasEnum(expandedTypes)) { final String tmpVar = "_tmp"; emitReturnEnumReplacement(expandedTypes, 0, thisFieldName, tmpVar, " ", builder); builder.append(" return ").append(tmpVar).append(";\n"); return; } } builder.append(" return "); if (isEnum(method.getReturnType())) { // Gson serializes enums with their toString() representation emitEnumValueOf(rawClass, thisFieldName, builder); } else { builder.append(thisFieldName); } builder.append(";\n"); } private void emitReturnEnumReplacement(List<Type> types, int i, String inVar, String outVar, String indentation, StringBuilder builder) { Class<?> rawClass = getRawClass(types.get(i)); String tmpVar = "tmp" + i; String childInVar = "in" + (i + 1); String childOutVar = "out" + (i + 1); if (isJsonArray(rawClass)) { // tmpVar is the index builder.append(indentation).append(outVar).append(" = [];\n"); builder.append(indentation).append(inVar).append(".forEach(function(").append(childInVar) .append(", ").append(tmpVar).append(") {\n"); } else if (isEnum(rawClass)) { builder.append(indentation).append(outVar).append(" = "); emitEnumValueOf(rawClass, inVar, builder); builder.append(";\n"); } else if (isJsonStringMap(rawClass)) { // TODO: implement when needed throw new IllegalStateException("enums inside JsonStringMaps need to be implemented"); } if (i + 1 < types.size()) { emitReturnEnumReplacement(types, i + 1, childInVar, childOutVar, indentation + " ", builder); } if (isJsonArray(rawClass)) { builder.append(indentation).append(" ").append(outVar).append("[").append(tmpVar) .append("] = ").append(childOutVar).append(";\n"); builder.append(indentation).append("});\n"); } } private void emitEnumValueOf(Class<?> rawClass, String var, StringBuilder builder) { builder.append("@"); builder.append(rawClass.getCanonicalName()); builder.append("::valueOf(Ljava/lang/String;)("); builder.append(var); builder.append(")"); } private void emitSetter(String methodName, String fieldName, String fieldSelector, Type type, String paramTypeName, String returnType, StringBuilder builder) { builder.append("\n public final native "); builder.append(returnType); builder.append(" "); builder.append(methodName); builder.append("("); emitSetterParameterTypeName(getRawClass(type), paramTypeName, builder); builder.append(" "); builder.append(fieldName); builder.append(") /*-{\n"); emitSetterPropertyAssignment(fieldName, fieldSelector, type, builder); builder.append(" return this;\n }-*/;\n"); } private void emitSetterParameterTypeName(Class<?> paramType, String paramTypeName, StringBuilder builder) { /* * For our Json collections, require the concrete client-side type since we * call JSON.stringify on this DTO. */ if (paramType == JsonArray.class) { paramTypeName = paramTypeName.replace("com.google.collide.json.shared.JsonArray", "com.google.collide.json.client.JsoArray"); } else if (paramType == JsonStringMap.class) { paramTypeName = paramTypeName.replace("com.google.collide.json.shared.JsonStringMap", "com.google.collide.json.client.JsoStringMap"); } builder.append(paramTypeName); } private void emitSetterPropertyAssignment( String fieldName, String fieldSelector, Type type, StringBuilder builder) { Class<?> rawClass = getRawClass(type); if (type instanceof ParameterizedType) { List<Type> expandedTypes = expandType(type); if (hasEnum(expandedTypes)) { final String tmpVar = "_tmp"; builder.append(" ").append(tmpVar).append(" = ").append(fieldName).append(";\n"); emitSetterEnumReplacement(expandedTypes, 0, tmpVar, fieldName, " ", builder); } } else if (isEnum(rawClass)) { /*- * codeBlockType = * codeBlockType.@com.google.collide.dto.CodeBlock.Type:: * toString()() */ builder.append(" ").append(fieldName).append(" = "); emitEnumToString(rawClass, fieldName, builder); builder.append(";\n"); } builder.append(" this["); builder.append(fieldSelector); builder.append("] = "); builder.append(fieldName); builder.append(";\n"); } private boolean hasEnum(List<Type> types) { for (Type type : types) { if (isEnum(getRawClass(type))) { return true; } } return false; } private void emitSetterEnumReplacement(List<Type> types, int i, String inVar, String outVar, String indentation, StringBuilder builder) { Class<?> rawClass = getRawClass(types.get(i)); String tmpVar = "tmp" + i; String childInVar = "in" + (i + 1); String childOutVar = "out" + (i + 1); if (isJsonArray(rawClass)) { builder.append(indentation).append(tmpVar).append(" = [];\n"); builder.append(indentation).append(inVar).append(".forEach(function(").append(childInVar) .append(") {\n"); } else if (isEnum(rawClass)) { builder.append(indentation).append(outVar).append(" = "); emitEnumToString(rawClass, inVar, builder); builder.append(";\n"); } else if (isJsonStringMap(rawClass)) { // TODO: implement when needed throw new IllegalStateException("enums inside JsonStringMaps need to be implemented"); } if (i + 1 < types.size()) { emitSetterEnumReplacement(types, i + 1, childInVar, childOutVar, indentation + " ", builder); } if (isJsonArray(rawClass)) { builder.append(indentation).append(" ").append(tmpVar).append(".push(").append(childOutVar) .append(");\n"); builder.append(indentation).append("});\n"); builder.append(indentation).append(outVar).append(" = ").append(tmpVar).append(";\n"); } } private void emitEnumToString(Class<?> enumClass, String fieldName, StringBuilder builder) { builder.append(fieldName); builder.append(".@"); builder.append(enumClass.getCanonicalName()); builder.append("::toString()()"); } }