/*
* Copyright 2013 MovingBlocks
*
* 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 org.terasology.persistence.serializers;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.JsonSyntaxException;
import com.google.protobuf.ByteString;
import com.google.protobuf.Descriptors;
import gnu.trove.list.TByteList;
import gnu.trove.list.array.TByteArrayList;
import org.terasology.protobuf.EntityData;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Locale;
import java.util.Map;
/**
* Converts between the EntityData types and JSON.
* <p/>
* This means that serialization between JSON and Entities/Prefabs is a two step process, with EntityData as an
* intermediate step - it was done this way because it is much simpler to write gson handlers for the small number of
* EntityData types than to dynamically build handlers for every component type (and have gson properly handle missing
* types). This can be revisited in the future.
*
* @author Immortius <immortius@gmail.com>
*/
// TODO: More javadoc
public final class EntityDataJSONFormat {
private EntityDataJSONFormat() {
}
public static void write(EntityData.GlobalStore world, BufferedWriter writer) {
newGson().toJson(world, writer);
}
public static void write(EntityData.Prefab prefab, BufferedWriter writer) {
newGson().toJson(prefab, writer);
}
public static String write(EntityData.Entity entity) {
return newGson().toJson(entity);
}
public static EntityData.GlobalStore readWorld(BufferedReader reader) throws IOException {
try {
return newGson().fromJson(reader, EntityData.GlobalStore.class);
} catch (JsonSyntaxException e) {
throw new IOException("Failed to load world", e);
}
}
public static EntityData.Prefab readPrefab(BufferedReader reader) throws IOException {
try {
return newGson().fromJson(reader, EntityData.Prefab.class);
} catch (JsonSyntaxException e) {
throw new IOException("Failed to load prefab", e);
}
}
private static Gson newGson() {
return new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(EntityData.GlobalStore.class, new WorldHandler())
.registerTypeAdapter(EntityData.Entity.class, new EntityHandler())
.registerTypeAdapter(EntityData.Prefab.class, new PrefabHandler())
.registerTypeAdapter(EntityData.Component.class, new ComponentHandler())
.registerTypeAdapter(EntityData.Component.Builder.class, new ComponentBuilderHandler())
.registerTypeAdapter(EntityData.Value.class, new ValueHandler())
.create();
}
private static class WorldHandler implements JsonSerializer<EntityData.GlobalStore>, JsonDeserializer<EntityData.GlobalStore> {
@Override
public JsonElement serialize(EntityData.GlobalStore src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject result = new JsonObject();
for (Map.Entry<Descriptors.FieldDescriptor, Object> field : src.getAllFields().entrySet()) {
result.add(field.getKey().getName(), context.serialize(field.getValue()));
}
return result;
}
@Override
public EntityData.GlobalStore deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
EntityData.GlobalStore.Builder world = EntityData.GlobalStore.newBuilder();
if (json.isJsonObject()) {
JsonObject jsonObject = json.getAsJsonObject();
JsonArray prefabArray = jsonObject.getAsJsonArray("prefab");
if (prefabArray != null) {
for (JsonElement prefabElem : prefabArray) {
world.addPrefab((EntityData.Prefab) context.deserialize(prefabElem, EntityData.Prefab.class));
}
}
JsonArray entityArray = jsonObject.getAsJsonArray("entity");
if (entityArray != null) {
for (JsonElement entityElem : entityArray) {
world.addEntity((EntityData.Entity) context.deserialize(entityElem, EntityData.Entity.class));
}
}
JsonPrimitive nextId = jsonObject.getAsJsonPrimitive("next_entity_id");
if (nextId != null) {
world.setNextEntityId(nextId.getAsInt());
}
JsonArray freedIdArray = jsonObject.getAsJsonArray("freed_entity_id");
if (freedIdArray != null) {
for (JsonElement freedId : freedIdArray) {
world.addFreedEntityId(freedId.getAsInt());
}
}
}
return world.build();
}
}
private static class ComponentHandler implements JsonSerializer<EntityData.Component> {
@Override
public JsonElement serialize(EntityData.Component src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject result = new JsonObject();
for (EntityData.NameValue field : src.getFieldList()) {
result.add(field.getName(), context.serialize(field.getValue()));
}
return result;
}
}
private static class ComponentBuilderHandler implements JsonDeserializer<EntityData.Component.Builder> {
@Override
public EntityData.Component.Builder deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
EntityData.Component.Builder component = EntityData.Component.newBuilder();
JsonObject jsonObject = json.getAsJsonObject();
for (Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) {
EntityData.NameValue.Builder nameValue = EntityData.NameValue.newBuilder();
nameValue.setName(entry.getKey());
EntityData.Value value = context.deserialize(entry.getValue(), EntityData.Value.class);
nameValue.setValue(value);
component.addField(nameValue);
}
return component;
}
}
private static class EntityHandler implements JsonSerializer<EntityData.Entity>, JsonDeserializer<EntityData.Entity> {
@Override
public JsonElement serialize(EntityData.Entity src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject result = new JsonObject();
if (src.hasId()) {
result.addProperty("id", src.getId());
}
if (src.hasParentPrefab() && !src.getParentPrefab().isEmpty()) {
result.addProperty("parentPrefab", src.getParentPrefab());
}
if (src.hasAlwaysRelevant()) {
result.addProperty("alwaysRelevant", src.getAlwaysRelevant());
}
if (src.hasOwner()) {
result.addProperty("owner", src.getOwner());
}
for (EntityData.Component component : src.getComponentList()) {
result.add(component.getType(), context.serialize(component));
}
if (src.getRemovedComponentCount() > 0) {
JsonArray removedComponentArray = new JsonArray();
for (String removedComponent : src.getRemovedComponentList()) {
removedComponentArray.add(new JsonPrimitive(removedComponent));
}
result.add("removedComponent", removedComponentArray);
}
return result;
}
@Override
public EntityData.Entity deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
EntityData.Entity.Builder entity = EntityData.Entity.newBuilder();
JsonObject jsonObject = json.getAsJsonObject();
for (Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) {
String name = entry.getKey().toLowerCase(Locale.ENGLISH);
switch (name) {
case "parentprefab":
if (entry.getValue().isJsonPrimitive()) {
entity.setParentPrefab(entry.getValue().getAsString());
}
break;
case "id":
if (entry.getValue().isJsonPrimitive()) {
entity.setId(entry.getValue().getAsInt());
}
break;
case "removedcomponent":
if (entry.getValue().isJsonArray()) {
for (JsonElement element : entry.getValue().getAsJsonArray()) {
entity.addRemovedComponent(element.getAsString());
}
}
break;
case "owner":
if (entry.getValue().isJsonPrimitive()) {
entity.setOwner(entry.getValue().getAsInt());
}
break;
case "alwaysrelevant":
entity.setAlwaysRelevant(entry.getValue().getAsBoolean());
break;
default:
EntityData.Component.Builder component = context.deserialize(entry.getValue(), EntityData.Component.Builder.class);
component.setType(entry.getKey());
entity.addComponent(component);
}
}
return entity.build();
}
}
private static class PrefabHandler implements JsonSerializer<EntityData.Prefab>, JsonDeserializer<EntityData.Prefab> {
@Override
public JsonElement serialize(EntityData.Prefab src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject result = new JsonObject();
if (src.hasName()) {
result.addProperty("name", src.getName());
}
if (src.hasParentName()) {
result.addProperty("parent", src.getParentName());
}
if (src.hasPersisted()) {
result.addProperty("persisted", src.getPersisted());
}
if (src.hasAlwaysRelevant()) {
result.addProperty("alwaysRelevant", src.getAlwaysRelevant());
}
if (src.getRemovedComponentCount() > 0) {
result.add("removedComponents", context.serialize(src.getRemovedComponentList()));
}
for (EntityData.Component component : src.getComponentList()) {
result.add(component.getType(), context.serialize(component));
}
return result;
}
@Override
public EntityData.Prefab deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
EntityData.Prefab.Builder prefab = EntityData.Prefab.newBuilder();
JsonObject jsonObject = json.getAsJsonObject();
for (Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) {
String name = entry.getKey().toLowerCase(Locale.ENGLISH);
switch (name) {
case "name":
if (entry.getValue().isJsonPrimitive()) {
prefab.setName(entry.getValue().getAsString());
}
break;
case "parent":
if (entry.getValue().isJsonPrimitive()) {
prefab.setParentName(entry.getValue().getAsString());
}
break;
case "removedcomponents":
if (entry.getValue().isJsonPrimitive()) {
prefab.addRemovedComponent(entry.getValue().getAsString());
} else if (entry.getValue().isJsonArray()) {
for (JsonElement element : entry.getValue().getAsJsonArray()) {
prefab.addRemovedComponent(element.getAsString());
}
}
break;
case "persisted":
prefab.setPersisted(entry.getValue().getAsBoolean());
break;
case "alwaysrelevant":
prefab.setAlwaysRelevant(entry.getValue().getAsBoolean());
break;
default:
if (entry.getValue().isJsonObject()) {
EntityData.Component.Builder component = context.deserialize(entry.getValue(), EntityData.Component.Builder.class);
component.setType(entry.getKey());
prefab.addComponent(component);
}
}
}
return prefab.build();
}
}
private static class ValueHandler implements JsonSerializer<EntityData.Value>, JsonDeserializer<EntityData.Value> {
@Override
public JsonElement serialize(EntityData.Value src, Type typeOfSrc, JsonSerializationContext context) {
if (src.getBooleanCount() > 1) {
return context.serialize(src.getBooleanList());
} else if (src.getBooleanCount() == 1) {
return context.serialize(src.getBoolean(0));
} else if (src.getDoubleCount() > 1) {
return context.serialize(src.getDoubleList());
} else if (src.getDoubleCount() == 1) {
return context.serialize(src.getDouble(0));
} else if (src.getFloatCount() > 1) {
return context.serialize(src.getFloatList());
} else if (src.getFloatCount() == 1) {
return context.serialize(src.getFloat(0));
} else if (src.getIntegerCount() > 1) {
return context.serialize(src.getIntegerList());
} else if (src.getIntegerCount() == 1) {
return context.serialize(src.getInteger(0));
} else if (src.getLongCount() > 1) {
return context.serialize(src.getLongList());
} else if (src.getLongCount() == 1) {
return context.serialize(src.getLong(0));
} else if (src.getStringCount() > 1) {
return context.serialize(src.getStringList());
} else if (src.getStringCount() == 1) {
return context.serialize(src.getString(0));
} else if (src.getValueCount() > 0) {
return context.serialize(src.getValueList());
} else if (src.hasBytes()) {
return context.serialize(src.getBytes().toByteArray());
} else if (src.getNameValueCount() > 0) {
JsonObject obj = new JsonObject();
for (EntityData.NameValue nameValue : src.getNameValueList()) {
obj.add(nameValue.getName(), context.serialize(nameValue.getValue()));
}
return obj;
}
return JsonNull.INSTANCE;
}
@Override
public EntityData.Value deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
EntityData.Value.Builder value = EntityData.Value.newBuilder();
if (json.isJsonPrimitive()) {
extractPrimitive(value, json);
} else if (json.isJsonObject()) {
extractMap(json, context, value);
} else if (json.isJsonArray()) {
JsonArray jsonArray = json.getAsJsonArray();
TByteList byteList = new TByteArrayList();
for (JsonElement element : jsonArray) {
if (element.isJsonArray()) {
value.addValue((EntityData.Value) context.deserialize(element, EntityData.Value.class));
} else if (json.isJsonObject()) {
extractMap(json, context, value);
} else if (element.isJsonPrimitive()) {
extractPrimitive(value, element);
if (element.getAsJsonPrimitive().isNumber()) {
try {
byteList.add(element.getAsByte());
} catch (NumberFormatException nfe) {
byteList.add((byte) 0);
}
}
}
}
value.setBytes(ByteString.copyFrom(byteList.toArray()));
}
return value.build();
}
private void extractMap(JsonElement json, JsonDeserializationContext context, EntityData.Value.Builder value) {
JsonObject nameValueObject = json.getAsJsonObject();
for (Map.Entry<String, JsonElement> nameValue : nameValueObject.entrySet()) {
EntityData.Value innerValue = context.deserialize(nameValue.getValue(), EntityData.Value.class);
value.addNameValue(EntityData.NameValue.newBuilder().setName(nameValue.getKey()).setValue(innerValue));
}
}
private void extractPrimitive(EntityData.Value.Builder value, JsonElement element) {
JsonPrimitive primitive = element.getAsJsonPrimitive();
if (primitive.isNumber()) {
value.addDouble(primitive.getAsDouble());
value.addFloat(primitive.getAsFloat());
try {
value.addInteger(primitive.getAsInt());
value.addLong(primitive.getAsLong());
} catch (NumberFormatException e) {
value.addInteger(0);
value.addLong(0);
}
}
if (primitive.isBoolean()) {
value.addBoolean(primitive.getAsBoolean());
}
if (primitive.isString()) {
value.addString(primitive.getAsString());
}
}
}
}