/* * Copyright 2015 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.module; import com.google.common.base.Preconditions; import com.google.common.collect.Maps; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import org.terasology.i18n.I18nMap; import org.terasology.i18n.gson.I18nMapTypeAdapter; import org.terasology.naming.Name; import org.terasology.naming.Version; import org.terasology.naming.gson.NameTypeAdapter; import org.terasology.naming.gson.VersionTypeAdapter; import java.io.Reader; import java.io.Writer; import java.lang.reflect.Type; import java.util.List; import java.util.Map; import java.util.Set; /** * Reads/writes ModuleMetadata to and from a json format. * An example of this format is: * <pre> * { * "id": "Core", * "version": "0.1.0", * "displayName": { * "en": "Short Name" * }, * "description": { * "en": "A longer description of the module" * }, * "dependencies": [ * { * "id": "baseModule", * "minVersion": "1.0.0", * "maxVersion": "2.0.0" * } * ] * } * </pre> * Extensions can be registered, as a mapping of identifier to Type, and optionally with a JsonSerializer/JsonDeserializer or TypeAdapter. * * @author Immortius */ public class ModuleMetadataJsonAdapter { private static final Type DEPENDENCY_LIST_TYPE = new TypeToken<List<DependencyInfo>>() { }.getType(); private static final Type STRING_SET_TYPE = new TypeToken<Set<String>>() { }.getType(); private final GsonBuilder builder; private volatile Gson cachedGson; private final Map<String, Type> extensionMap = Maps.newHashMap(); public ModuleMetadataJsonAdapter() { this.builder = new GsonBuilder() .setPrettyPrinting() .setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX") .registerTypeAdapter(Version.class, new VersionTypeAdapter()) .registerTypeAdapter(Name.class, new NameTypeAdapter()) .registerTypeAdapter(I18nMap.class, new I18nMapTypeAdapter()) .registerTypeAdapter(ModuleMetadata.class, new ModuleMetadataTypeAdapter()); } /** * Registers an extension for serialization. An entry in the json file with a matching identifier to the extension id will be serialized with the given type * and stored as an extension in the metadata. * * @param extensionId The identifier for this extension. * @param extensionType The type of object this extension holds. */ public void registerExtension(String extensionId, Type extensionType) { Preconditions.checkArgument(!ModuleMetadata.RESERVED_IDS.contains(extensionId), "Id '" + extensionId + "' is a reserved id and cannot be used for an extension"); extensionMap.put(extensionId, extensionType); } /** * Registers an extension for serialization. An entry in the json file with a matching identifier to the extension id will be serialized with the given type * and stored as an extension in the resultant metadata. Additionally, the given gson-compatiable typeAdapter will be used to serialize the extensionType. * * @param extensionId The identifier for this extension. * @param extensionType The type of object this extension holds. * @param typeAdapter The Gson compatible type handler to use for the extension type. */ public void registerExtension(String extensionId, Type extensionType, Object typeAdapter) { registerExtension(extensionId, extensionType); addTypeHandler(extensionType, typeAdapter); } /** * Adds a type handler to use when serializing the given type * * @param type The type to handle * @param typeHandler The handler to handle type */ public void addTypeHandler(Type type, Object typeHandler) { if (cachedGson != null) { cachedGson = null; } builder.registerTypeAdapter(type, typeHandler); } /** * @param reader A reader providing the json metadata * @return The ModuleMetadata represented by the JSON * @throws com.google.gson.JsonIOException if there was a problem reading from the Reader * @throws com.google.gson.JsonSyntaxException if json is not valid */ public ModuleMetadata read(Reader reader) { if (cachedGson == null) { cachedGson = builder.create(); } return cachedGson.fromJson(reader, ModuleMetadata.class); } /** * @param reader A reader providing the json metadata * @return The ModuleMetadata represented by the JSON * @throws com.google.gson.JsonIOException if there was a problem reading from the Reader * @throws com.google.gson.JsonSyntaxException if json is not valid */ public ModuleMetadata read(JsonReader reader) { if (cachedGson == null) { cachedGson = builder.create(); } return cachedGson.fromJson(reader, ModuleMetadata.class); } /** * @param writer A writer that receives the json metadata * @param data the module metadata that should be written * @throws com.google.gson.JsonIOException if there was a problem writing to the Writer */ public void write(ModuleMetadata data, Writer writer) { if (cachedGson == null) { cachedGson = builder.create(); } cachedGson.toJson(data, writer); } /** * @param writer A writer that receives the json metadata * @param data the module metadata that should be written * @throws com.google.gson.JsonIOException if there was a problem writing to the Writer */ public void write(ModuleMetadata data, JsonWriter writer) { if (cachedGson == null) { cachedGson = builder.create(); } cachedGson.toJson(data, ModuleMetadata.class, writer); } private class ModuleMetadataTypeAdapter implements JsonDeserializer<ModuleMetadata>, JsonSerializer<ModuleMetadata> { @SuppressWarnings("unchecked") @Override public ModuleMetadata deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { JsonObject metadataObject = json.getAsJsonObject(); ModuleMetadata metadata = new ModuleMetadata(); for (Map.Entry<String, JsonElement> entry : metadataObject.entrySet()) { switch (entry.getKey()) { case ModuleMetadata.ID: metadata.setId(context.<Name>deserialize(entry.getValue(), Name.class)); break; case ModuleMetadata.VERSION: metadata.setVersion(context.<Version>deserialize(entry.getValue(), Version.class)); break; case ModuleMetadata.DISPLAY_NAME: metadata.setDisplayName(context.<I18nMap>deserialize(entry.getValue(), I18nMap.class)); break; case ModuleMetadata.DESCRIPTION: metadata.setDescription(context.<I18nMap>deserialize(entry.getValue(), I18nMap.class)); break; case ModuleMetadata.DEPENDENCIES: metadata.getDependencies().addAll((List<DependencyInfo>) context.deserialize(entry.getValue(), DEPENDENCY_LIST_TYPE)); break; case ModuleMetadata.REQUIRED_PERMISSIONS: metadata.getRequiredPermissions().addAll((Set<String>) context.deserialize(entry.getValue(), STRING_SET_TYPE)); break; default: Type extensionType = extensionMap.get(entry.getKey()); if (extensionType != null) { Object extensionObject = context.deserialize(entry.getValue(), extensionType); metadata.setExtension(entry.getKey(), extensionObject); } break; } } return metadata; } @Override public JsonElement serialize(ModuleMetadata src, Type typeOfSrc, JsonSerializationContext context) { JsonObject json = new JsonObject(); json.add(ModuleMetadata.ID, context.serialize(src.getId())); json.add(ModuleMetadata.VERSION, context.serialize(src.getVersion())); json.add(ModuleMetadata.DISPLAY_NAME, context.serialize(src.getDisplayName())); json.add(ModuleMetadata.DESCRIPTION, context.serialize(src.getDescription())); json.add(ModuleMetadata.DEPENDENCIES, context.serialize(src.getDependencies())); json.add(ModuleMetadata.REQUIRED_PERMISSIONS, context.serialize(src.getRequiredPermissions())); for (String extKey : extensionMap.keySet()) { Object extValue = src.getExtension(extKey, Object.class); if (extValue != null) { json.add(extKey, context.serialize(extValue)); } } return json; } } }