Skip to content

Commit

Permalink
Fix flattening serializer for maps (Azure#232)
Browse files Browse the repository at this point in the history
* Fix flattening serializer for maps

* Add test
  • Loading branch information
jianghaolu authored Oct 4, 2017
1 parent 34666d3 commit cc553df
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

package com.microsoft.rest.serializer;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonMappingException;
Expand All @@ -16,17 +17,21 @@
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.ser.ResolvableSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.microsoft.rest.DateTimeRfc1123;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.Period;

import java.io.IOException;
import java.util.Iterator;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;

/**
* Custom serializer for serializing types with wrapped properties.
Expand Down Expand Up @@ -78,65 +83,99 @@ public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescri
return module;
}

@Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
if (value == null) {
jgen.writeNull();
return;
private List<Field> getAllDeclaredFields(Class<?> clazz) {
List<Field> fields = new ArrayList<Field>();
while (clazz != null && !clazz.equals(Object.class)) {
for (Field f : clazz.getDeclaredFields()) {
int mod = f.getModifiers();
if (!Modifier.isFinal(mod) && !Modifier.isStatic(mod)) {
fields.add(f);
}
}
clazz = clazz.getSuperclass();
}
return fields;
}

private JsonNode serializePartial(Object value) {
if (value.getClass().isPrimitive()
|| value.getClass().isEnum()
|| value instanceof LocalDate
|| value instanceof DateTime
|| value instanceof String
|| value instanceof DateTimeRfc1123
|| value instanceof Period) {
return mapper.valueToTree(value);
}

int mod = value.getClass().getModifiers();
if (Modifier.isFinal(mod) || Modifier.isStatic(mod)) {
return mapper.valueToTree(value);
}

// BFS for all collapsed properties
ObjectNode root = mapper.valueToTree(value);
ObjectNode res = root.deepCopy();
Queue<ObjectNode> source = new LinkedBlockingQueue<ObjectNode>();
Queue<ObjectNode> target = new LinkedBlockingQueue<ObjectNode>();
source.add(root);
target.add(res);
while (!source.isEmpty()) {
ObjectNode current = source.poll();
ObjectNode resCurrent = target.poll();
Iterator<Map.Entry<String, JsonNode>> fields = current.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> field = fields.next();
ObjectNode node = resCurrent;
String key = field.getKey();
JsonNode outNode = resCurrent.get(key);
if (field.getKey().matches(".+[^\\\\]\\..+")) {
String[] values = field.getKey().split("((?<!\\\\))\\.");
for (int i = 0; i < values.length; ++i) {
values[i] = values[i].replace("\\.", ".");
if (i == values.length - 1) {
break;
}
String val = values[i];
if (node.has(val)) {
node = (ObjectNode) node.get(val);
} else {
ObjectNode child = new ObjectNode(JsonNodeFactory.instance);
node.put(val, child);
node = child;
}
if (value instanceof List<?>) {
ArrayNode node = new ArrayNode(mapper.getNodeFactory());
for (Object val : ((List<?>) value)) {
node.add(serializePartial(val));
}
return node;
}

if (value instanceof Map<?, ?>) {
ObjectNode node = new ObjectNode(mapper.getNodeFactory());
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
node.set((String) entry.getKey(), serializePartial(entry.getValue()));
}
return node;
}

ObjectNode res = new ObjectNode(mapper.getNodeFactory());
for (Field f : getAllDeclaredFields(value.getClass())) {
f.setAccessible(true);
String wireName = f.getName();
ObjectNode pointer = res;
JsonProperty property = f.getAnnotation(JsonProperty.class);
if (property != null && !property.value().isEmpty()) {
wireName = f.getAnnotation(JsonProperty.class).value();
}
if (value.getClass().isAnnotationPresent(JsonFlatten.class) && wireName.matches(".+[^\\\\]\\..+")) {
String[] values = wireName.split("((?<!\\\\))\\.");
for (int i = 0; i < values.length; ++i) {
values[i] = values[i].replace("\\.", ".");
if (i == values.length - 1) {
break;
}
node.set(values[values.length - 1], resCurrent.get(field.getKey()));
resCurrent.remove(field.getKey());
outNode = node.get(values[values.length - 1]);
}
if (field.getValue() instanceof ObjectNode) {
source.add((ObjectNode) field.getValue());
target.add((ObjectNode) outNode);
} else if (field.getValue() instanceof ArrayNode
&& (field.getValue()).size() > 0
&& (field.getValue()).get(0) instanceof ObjectNode) {
Iterator<JsonNode> sourceIt = field.getValue().elements();
Iterator<JsonNode> targetIt = outNode.elements();
while (sourceIt.hasNext()) {
source.add((ObjectNode) sourceIt.next());
target.add((ObjectNode) targetIt.next());
String val = values[i];
if (!pointer.has(val)) {
ObjectNode child = new ObjectNode(mapper.getNodeFactory());
pointer.set(val, child);
pointer = child;
} else {
pointer = (ObjectNode) pointer.get(val);
}
}
wireName = values[values.length - 1];
}
try {
Object val = f.get(value);
if (val != null) {
pointer.set(wireName, serializePartial(val));
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
jgen.writeTree(res);
return res;
}

@Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
if (value == null) {
jgen.writeNull();
return;
}

jgen.writeTree(serializePartial(value));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for
* license information.
*/

package com.microsoft.rest;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.microsoft.rest.serializer.JacksonAdapter;
import com.microsoft.rest.serializer.JsonFlatten;
import org.junit.Assert;
import org.junit.Test;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class FlatteningSerializerTests {
@Test
public void canFlatten() throws Exception {
Foo foo = new Foo();
foo.bar = "hello.world";
foo.baz = new ArrayList<>();
foo.baz.add("hello");
foo.baz.add("hello.world");
foo.qux = new HashMap<>();
foo.qux.put("hello", "world");
foo.qux.put("a.b", "c.d");

String serialized = new JacksonAdapter().serialize(foo);
Assert.assertEquals("{\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"a.b\":\"c.d\",\"hello\":\"world\"}}}}}", serialized);
}

@JsonFlatten
private class Foo {
@JsonProperty(value = "properties.bar")
private String bar;
@JsonProperty(value = "properties.props.baz")
private List<String> baz;
@JsonProperty(value = "properties.props.q.qux")
private Map<String, String> qux;
}
}

0 comments on commit cc553df

Please sign in to comment.