Skip to content

Commit

Permalink
Support DTOs with properties typed 'any' through JsonElement
Browse files Browse the repository at this point in the history
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 <tareq.sharafy@sap.com>
  • Loading branch information
tareksha committed Jun 14, 2015
1 parent b6a515b commit 644431d
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 6 deletions.
Expand Up @@ -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;

Expand Down Expand Up @@ -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 (...)).
* <p/>
Expand Down
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -280,6 +282,10 @@ private void emitDelegateMethods(StringBuilder builder) {

private void emitSerializer(List<Method> 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) {
Expand All @@ -296,7 +302,8 @@ private void emitSerializer(List<Method> 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");
Expand Down Expand Up @@ -391,7 +398,7 @@ private void emitSerializerImpl(List<Type> 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");
Expand All @@ -413,12 +420,18 @@ private void emitSerializerImpl(List<Type> 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 +
Expand All @@ -441,7 +454,11 @@ private void emitSerializerImpl(List<Type> expandedTypes, int depth, StringBuild

/** Generates a static factory method that creates a new instance based on a JsonElement. */
private void emitDeserializer(List<Method> 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");
Expand All @@ -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");
}

Expand Down Expand Up @@ -562,18 +580,23 @@ private void emitDeserializerImpl(List<Type> 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('$', '.');
Expand All @@ -582,6 +605,20 @@ private void emitDeserializerImpl(List<Type> 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);
Expand Down Expand Up @@ -754,6 +791,9 @@ private void emitDeepCopyForGetters(List<Type> 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");
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand Down
@@ -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);
}

0 comments on commit 644431d

Please sign in to comment.