From 644431da16434ef227c6d14c09c3c635e605e9e8 Mon Sep 17 00:00:00 2001 From: Tareq Sharafy Date: Sun, 14 Jun 2015 11:23:58 +0300 Subject: [PATCH] Support DTOs with properties typed 'any' through JsonElement 1- Serialzie and de-serialize JsonElement properties as raw JSONs 2- Deep-copy them in the generated copy constructors 3- Avoid redundant copying in fromJsonString() and toJson() cases Signed-off-by: Tareq Sharafy --- .../eclipse/che/dto/generator/DtoImpl.java | 5 ++ .../dto/generator/DtoImplServerTemplate.java | 52 ++++++++++++++++--- .../org/eclipse/che/dto/ServerDtoTest.java | 29 +++++++++++ .../che/dto/definitions/DtoWithAny.java | 31 +++++++++++ 4 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 platform-api/che-core-api-dto/src/test/java/org/eclipse/che/dto/definitions/DtoWithAny.java diff --git a/platform-api/che-core-api-dto/src/main/java/org/eclipse/che/dto/generator/DtoImpl.java b/platform-api/che-core-api-dto/src/main/java/org/eclipse/che/dto/generator/DtoImpl.java index 87ba257f4..1f0198a28 100644 --- a/platform-api/che-core-api-dto/src/main/java/org/eclipse/che/dto/generator/DtoImpl.java +++ b/platform-api/che-core-api-dto/src/main/java/org/eclipse/che/dto/generator/DtoImpl.java @@ -17,6 +17,7 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import com.google.gson.JsonElement; import com.google.gson.JsonParser; import com.google.gson.stream.JsonWriter; @@ -254,6 +255,10 @@ public static boolean isMap(Class returnType) { return returnType.equals(Map.class); } + public static boolean isAny(Class returnType) { + return returnType.equals(JsonElement.class); + } + /** * Expands the type and its first generic parameter (which can also have a first generic parameter (...)). *

diff --git a/platform-api/che-core-api-dto/src/main/java/org/eclipse/che/dto/generator/DtoImplServerTemplate.java b/platform-api/che-core-api-dto/src/main/java/org/eclipse/che/dto/generator/DtoImplServerTemplate.java index 73f195d52..4c972c8e4 100644 --- a/platform-api/che-core-api-dto/src/main/java/org/eclipse/che/dto/generator/DtoImplServerTemplate.java +++ b/platform-api/che-core-api-dto/src/main/java/org/eclipse/che/dto/generator/DtoImplServerTemplate.java @@ -22,6 +22,7 @@ import org.eclipse.che.dto.shared.SerializationIndex; import com.google.common.base.Preconditions; import com.google.common.primitives.Primitives; +import com.google.gson.JsonElement; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; @@ -40,6 +41,7 @@ public class DtoImplServerTemplate extends DtoImpl { private static final String JSON_ARRAY_IMPL = JsonArrayImpl.class.getCanonicalName(); private static final String JSON_MAP_IMPL = JsonStringMapImpl.class.getCanonicalName(); private static final String SERVER_DTO_MARKER = " @" + DTOImpl.class.getCanonicalName() + "(\"server\")\n"; + private static final String COPY_JSONS_PARAM = "copyJsons"; DtoImplServerTemplate(DtoTemplate template, Class superInterface) { super(template, superInterface); @@ -280,6 +282,10 @@ private void emitDelegateMethods(StringBuilder builder) { private void emitSerializer(List getters, StringBuilder builder) { builder.append(" public JsonElement toJsonElement() {\n"); + // The default toJsonElement() returns JSONs for unsafe use thus 'any' properties should be copied + builder.append(" return toJsonElement(true);\n"); + builder.append(" }\n"); + builder.append(" public JsonElement toJsonElement(boolean ").append(COPY_JSONS_PARAM).append(") {\n"); if (isCompactJson()) { builder.append(" JsonArray result = new JsonArray();\n"); for (Method method : getters) { @@ -296,7 +302,8 @@ private void emitSerializer(List getters, StringBuilder builder) { builder.append("\n"); builder.append(" @Override\n"); builder.append(" public String toJson() {\n"); - builder.append(" return gson.toJson(toJsonElement());\n"); + // The default toJson() creates its own JSON for internal printing, thus keeping JSONs values is safe + builder.append(" return gson.toJson(toJsonElement(false));\n"); builder.append(" }\n"); builder.append("\n"); builder.append(" @Override\n"); @@ -391,7 +398,7 @@ private void emitSerializerImpl(List expandedTypes, int depth, StringBuild } else if (getEnclosingTemplate().isDtoInterface(rawClass)) { builder.append(i).append("JsonElement ").append(outVar).append(" = ").append(depth == 0 ? "this." + inVar : inVar).append( " == null ? JsonNull.INSTANCE : ((").append(getImplNameForDto((Class)expandedTypes.get(depth))).append(")") - .append(depth == 0 ? "this." + inVar : inVar).append(").toJsonElement();\n"); + .append(depth == 0 ? "this." + inVar : inVar).append(").toJsonElement(").append(COPY_JSONS_PARAM).append(");\n"); } else if (rawClass.equals(String.class)) { builder.append(i).append("JsonElement ").append(outVar).append(" = (").append(depth == 0 ? "this." + inVar : inVar).append( " == null) ? JsonNull.INSTANCE : new JsonPrimitive(").append(depth == 0 ? "this." + inVar : inVar).append(");\n"); @@ -413,12 +420,18 @@ private void emitSerializerImpl(List expandedTypes, int depth, StringBuild || rawClass == Byte.class) { builder.append(i).append("JsonElement ").append(outVar).append(depth == 0 ? " = this." + inVar : inVar).append( " == null ? JsonNull.INSTANCE : new JsonPrimitive(").append(depth == 0 ? "this." + inVar : inVar).append(");\n"); + } else if (isAny(rawClass)) { + // TODO JsonElement.deepCopy() is package-protected, JSONs are serialized to strings then parsed for copying them + // outVar = inVar == null ? JsonNull.INSTNACE : (copyJsons ? new JsonParser().parse(inVar) : inVar); + builder.append(i).append("JsonElement ").append(outVar).append(depth == 0 ? " = this." + inVar : inVar) + .append(" == null ? JsonNull.INSTANCE : ("); + appendCopyJsonExpression(inVar, builder).append(");\n"); } else { final Class dtoImplementation = getEnclosingTemplate().getDtoImplementation(rawClass); if (dtoImplementation != null) { builder.append(i).append("JsonElement ").append(outVar).append(" = ").append(depth == 0 ? "this." + inVar : inVar).append( " == null ? JsonNull.INSTANCE : ((").append(dtoImplementation.getCanonicalName()).append(")") - .append(depth == 0 ? "this." + inVar : inVar).append(").toJsonElement();\n"); + .append(depth == 0 ? "this." + inVar : inVar).append(").toJsonElement(").append(COPY_JSONS_PARAM).append(");\n"); } else { throw new IllegalArgumentException("Unable to generate server implementation for DTO interface " + getDtoInterface().getCanonicalName() + ". Type " + rawClass + @@ -441,7 +454,11 @@ private void emitSerializerImpl(List expandedTypes, int depth, StringBuild /** Generates a static factory method that creates a new instance based on a JsonElement. */ private void emitDeserializer(List getters, StringBuilder builder) { + // The default fromJsonElement(json) works in unsafe mode and clones the JSON's for 'any' properties builder.append(" public static ").append(getImplClassName()).append(" fromJsonElement(JsonElement jsonElem) {\n"); + builder.append(" return fromJsonElement(jsonElem, true);\n"); + builder.append(" }\n"); + builder.append(" public static ").append(getImplClassName()).append(" fromJsonElement(JsonElement jsonElem, boolean ").append(COPY_JSONS_PARAM).append(") {\n"); builder.append(" if (jsonElem == null || jsonElem.isJsonNull()) {\n"); builder.append(" return null;\n"); builder.append(" }\n\n"); @@ -468,7 +485,8 @@ private void emitDeserializerShortcut(StringBuilder builder) { builder.append(" if (jsonString == null) {\n"); builder.append(" return null;\n"); builder.append(" }\n\n"); - builder.append(" return fromJsonElement(new JsonParser().parse(jsonString));\n"); + // The default fromJsonElement(json) creates its own JSON thus keeping parts of its as value is OK + builder.append(" return fromJsonElement(new JsonParser().parse(jsonString), false);\n"); builder.append(" }\n\n"); } @@ -562,18 +580,23 @@ private void emitDeserializerImpl(List expandedTypes, int depth, StringBui } else if (getEnclosingTemplate().isDtoInterface(rawClass)) { String className = getImplName(rawClass, false); builder.append(i).append(className).append(" ").append(outVar).append(" = ").append(getImplNameForDto(rawClass)) - .append(".fromJsonElement(").append(inVar).append(");\n"); + .append(".fromJsonElement(").append(inVar).append(", ").append(COPY_JSONS_PARAM).append(");\n"); } else if (rawClass.isPrimitive()) { String primitiveName = rawClass.getSimpleName(); String primitiveNameCap = primitiveName.substring(0, 1).toUpperCase() + primitiveName.substring(1); builder.append(i).append(primitiveName).append(" ").append(outVar).append(" = ").append(inVar).append( ".getAs").append(primitiveNameCap).append("();\n"); + } else if (isAny(rawClass)) { + // TODO JsonElement.deepCopy() is package-protected, JSONs are serialized to strings then parsed for copying them + // outVar = copyJsons ? new JsonParser().parse(inVar) : inVar; + builder.append(i).append("JsonElement ").append(outVar).append(" = "); + appendCopyJsonExpression(inVar, builder).append(";\n"); } else { final Class dtoImplementation = getEnclosingTemplate().getDtoImplementation(rawClass); if (dtoImplementation != null) { String className = getImplName(rawClass, false); builder.append(i).append(className).append(" ").append(outVar).append(" = ") - .append(dtoImplementation.getCanonicalName()).append(".fromJsonElement(").append(inVar).append(");\n"); + .append(dtoImplementation.getCanonicalName()).append(".fromJsonElement(").append(inVar).append(", ").append(COPY_JSONS_PARAM).append(");\n"); } else { // Use gson to handle all other types. String rawClassName = rawClass.getName().replace('$', '.'); @@ -582,6 +605,20 @@ private void emitDeserializerImpl(List expandedTypes, int depth, StringBui } } } + + /** + * Append the expression that clones the given JsonElement variable into a new value. If the copyJons run-time + * parameter is set to false, then the expression won't perform a clone but instead will reuse the variable by + * reference. + */ + private static StringBuilder appendCopyJsonExpression(String inVar, StringBuilder builder) { + builder.append(COPY_JSONS_PARAM).append(" ? "); + appendNaiveCopyJsonExpression(inVar, builder).append(" : ").append(inVar); + return builder; + } + private static StringBuilder appendNaiveCopyJsonExpression(String inValue, StringBuilder builder) { + return builder.append("new JsonParser().parse((").append(inValue).append(").toString())"); + } private void emitPreamble(Class dtoInterface, StringBuilder builder) { builder.append(SERVER_DTO_MARKER); @@ -754,6 +791,9 @@ private void emitDeepCopyForGetters(List expandedTypes, int depth, StringB emitDeepCopyCollections(expandedTypes, depth, builder, fieldNameIn, fieldNameOut, i); builder.append(i).append(" ").append("this.").append(fieldName).append(" = ").append(fieldNameOut).append(";\n"); builder.append(i).append("}\n"); + } else if (isAny(rawClass)) { + builder.append(i).append("this.").append(fieldName).append(" = "); + appendNaiveCopyJsonExpression(origin + "." + getterName + "()", builder).append(";\n"); } else if (getEnclosingTemplate().isDtoInterface(rawClass)) { builder.append(i).append(rawTypeName).append(" ").append(fieldNameIn).append(" = ").append(origin).append(".") .append(getterName).append("();\n"); diff --git a/platform-api/che-core-api-dto/src/test/java/org/eclipse/che/dto/ServerDtoTest.java b/platform-api/che-core-api-dto/src/test/java/org/eclipse/che/dto/ServerDtoTest.java index 1981c3dbb..9125230e2 100644 --- a/platform-api/che-core-api-dto/src/test/java/org/eclipse/che/dto/ServerDtoTest.java +++ b/platform-api/che-core-api-dto/src/test/java/org/eclipse/che/dto/ServerDtoTest.java @@ -11,11 +11,14 @@ package org.eclipse.che.dto; import org.eclipse.che.dto.definitions.ComplicatedDto; +import org.eclipse.che.dto.definitions.DtoWithAny; import org.eclipse.che.dto.definitions.DtoWithDelegate; import org.eclipse.che.dto.definitions.DtoWithFieldNames; import org.eclipse.che.dto.definitions.SimpleDto; import org.eclipse.che.dto.server.DtoFactory; + import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; @@ -110,6 +113,32 @@ public void testDeerializerWithFieldNames() throws Exception { Assert.assertEquals(dto.getTheDefault(), _default); } + @Test + public void testSerializerWithAny() throws Exception { + DtoWithAny dto = dtoFactory.createDto(DtoWithAny.class).withStuff(createTestValueForAny()); + final String json = dtoFactory.toJson(dto); + + JsonObject jsonObject = new JsonParser().parse(json).getAsJsonObject(); + Assert.assertEquals(jsonObject.get("stuff"), createTestValueForAny()); + } + + @Test + public void testDeerializerWithAny() throws Exception { + final JsonElement stuff = createTestValueForAny(); + + JsonObject json = new JsonObject(); + json.add("stuff", stuff); + + DtoWithAny dto = dtoFactory.createDtoFromJson(json.toString(), DtoWithAny.class); + + Assert.assertEquals(dto.getStuff(), createTestValueForAny()); + } + + /** Intentionally call several times to ensure non-reference equality */ + private static JsonElement createTestValueForAny() { + return new JsonParser().parse("{a:100,b:{c:'blah',d:null}}"); + } + @Test public void testListSimpleDtoDeserializer() throws Exception { final String fooString_1 = "Something 1"; diff --git a/platform-api/che-core-api-dto/src/test/java/org/eclipse/che/dto/definitions/DtoWithAny.java b/platform-api/che-core-api-dto/src/test/java/org/eclipse/che/dto/definitions/DtoWithAny.java new file mode 100644 index 000000000..afd33c6a3 --- /dev/null +++ b/platform-api/che-core-api-dto/src/test/java/org/eclipse/che/dto/definitions/DtoWithAny.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2012-2015 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.dto.definitions; + +import org.eclipse.che.dto.shared.DTO; + +import com.google.gson.JsonElement; + +/** + * Makes use of the 'any' (JsonElement) property type feature. + * + * @author Tareq Sharafy (tareq.sharafy@sap.com) + */ +@DTO +public interface DtoWithAny { + int getId(); + + JsonElement getStuff(); + + void setStuff(JsonElement stuff); + + DtoWithAny withStuff(JsonElement stuff); +}