/*
* Copyright 2016 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.rendering.nui.asset;
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import com.google.gson.JsonParser;
import com.google.gson.JsonElement;
import com.google.gson.GsonBuilder;
import com.google.gson.Gson;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonParseException;
import com.google.gson.JsonObject;
import com.google.gson.JsonArray;
import com.google.gson.stream.JsonReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.assets.ResourceUrn;
import org.terasology.assets.format.AbstractAssetFileFormat;
import org.terasology.assets.format.AssetDataFile;
import org.terasology.assets.module.annotations.RegisterAssetFileFormat;
import org.terasology.i18n.TranslationSystem;
import org.terasology.math.Border;
import org.terasology.persistence.ModuleContext;
import org.terasology.persistence.typeHandling.TypeSerializationLibrary;
import org.terasology.persistence.typeHandling.extensionTypes.AssetTypeHandler;
import org.terasology.persistence.typeHandling.gson.JsonTypeHandlerAdapter;
import org.terasology.persistence.typeHandling.mathTypes.BorderTypeHandler;
import org.terasology.reflection.metadata.ClassMetadata;
import org.terasology.reflection.metadata.FieldMetadata;
import org.terasology.registry.CoreRegistry;
import org.terasology.rendering.nui.LayoutHint;
import org.terasology.rendering.nui.NUIManager;
import org.terasology.rendering.nui.UILayout;
import org.terasology.rendering.nui.UIWidget;
import org.terasology.rendering.nui.skin.UISkin;
import org.terasology.rendering.nui.widgets.UILabel;
import org.terasology.utilities.ReflectionUtil;
import org.terasology.utilities.gson.CaseInsensitiveEnumTypeAdapterFactory;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.Set;
/**
* Handles loading UI widgets from json format files.
*/
@RegisterAssetFileFormat
public class UIFormat extends AbstractAssetFileFormat<UIData> {
public static final String CONTENTS_FIELD = "contents";
public static final String LAYOUT_INFO_FIELD = "layoutInfo";
public static final String ID_FIELD = "id";
public static final String TYPE_FIELD = "type";
private static final Logger logger = LoggerFactory.getLogger(UIFormat.class);
public UIFormat() {
super("ui");
}
@Override
public UIData load(ResourceUrn resourceUrn, List<AssetDataFile> inputs) throws IOException {
try (JsonReader reader = new JsonReader(new InputStreamReader(inputs.get(0).openStream(), Charsets.UTF_8))) {
reader.setLenient(true);
UIData data = load(new JsonParser().parse(reader));
data.setSource(inputs.get(0));
return data;
}
}
public UIData load(JsonElement element) throws IOException {
return load(element, null);
}
public UIData load(JsonElement element, Locale otherLocale) throws IOException {
NUIManager nuiManager = CoreRegistry.get(NUIManager.class);
TranslationSystem translationSystem = CoreRegistry.get(TranslationSystem.class);
TypeSerializationLibrary library = new TypeSerializationLibrary(CoreRegistry.get(TypeSerializationLibrary.class));
library.add(UISkin.class, new AssetTypeHandler<>(UISkin.class));
library.add(Border.class, new BorderTypeHandler());
GsonBuilder gsonBuilder = new GsonBuilder()
.registerTypeAdapterFactory(new CaseInsensitiveEnumTypeAdapterFactory())
.registerTypeAdapter(UIData.class, new UIDataTypeAdapter())
.registerTypeHierarchyAdapter(UIWidget.class, new UIWidgetTypeAdapter(nuiManager));
for (Class<?> handledType : library.getCoreTypes()) {
gsonBuilder.registerTypeAdapter(handledType, new JsonTypeHandlerAdapter<>(library.getHandlerFor(handledType)));
}
// override the String TypeAdapter from the serialization library
gsonBuilder.registerTypeAdapter(String.class, new I18nStringTypeAdapter(translationSystem, otherLocale));
Gson gson = gsonBuilder.create();
return gson.fromJson(element, UIData.class);
}
private static final class I18nStringTypeAdapter implements JsonDeserializer<String> {
private final TranslationSystem translationSystem;
private final Locale otherLocale;
I18nStringTypeAdapter(TranslationSystem translationSystem, Locale otherLocale) {
this.translationSystem = translationSystem;
this.otherLocale = otherLocale;
}
@Override
public String deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
String text = json.getAsString();
return otherLocale == null
? translationSystem.translate(text) : translationSystem.translate(text, otherLocale);
}
}
/**
* Load UIData with a single, root widget
*/
private static final class UIDataTypeAdapter implements JsonDeserializer<UIData> {
@Override
public UIData deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return new UIData((UIWidget) context.deserialize(json, UIWidget.class));
}
}
/**
* Loads a widget. This requires the following custom handling:
* <ul>
* <li>The class of the widget is determined through a URI in the "type" attribute</li>
* <li>If the "id" attribute is present, it is passed to the constructor</li>
* <li>If the widget is a layout, then a "contents" attribute provides a list of widgets for content.
* Each contained widget may have a "layoutInfo" attribute providing the layout hint for its container.</li>
* </ul>
*/
private static final class UIWidgetTypeAdapter implements JsonDeserializer<UIWidget> {
private NUIManager nuiManager;
UIWidgetTypeAdapter(NUIManager nuiManager) {
this.nuiManager = nuiManager;
}
@Override
public UIWidget deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isString()) {
return new UILabel(json.getAsString());
}
JsonObject jsonObject = json.getAsJsonObject();
String type = jsonObject.get(TYPE_FIELD).getAsString();
ClassMetadata<? extends UIWidget, ?> elementMetadata = nuiManager.getWidgetMetadataLibrary().resolve(type, ModuleContext.getContext());
if (elementMetadata == null) {
logger.error("Unknown UIWidget type {}", type);
return null;
}
String id = null;
if (jsonObject.has(ID_FIELD)) {
id = jsonObject.get(ID_FIELD).getAsString();
}
UIWidget element = elementMetadata.newInstance();
if (id != null) {
FieldMetadata<?, ?> fieldMetadata = elementMetadata.getField(ID_FIELD);
if (fieldMetadata == null) {
logger.warn("UIWidget type {} lacks id field", elementMetadata.getUri());
} else {
fieldMetadata.setValue(element, id);
}
}
// Deserialize normal fields.
Set<String> unknownFields = new HashSet<>();
for (Entry<String, JsonElement> entry : jsonObject.entrySet()) {
String name = entry.getKey();
if (!ID_FIELD.equals(name)
&& !CONTENTS_FIELD.equals(name)
&& !TYPE_FIELD.equals(name)
&& !LAYOUT_INFO_FIELD.equals(name)) {
unknownFields.add(name);
}
}
for (FieldMetadata<? extends UIWidget, ?> field : elementMetadata.getFields()) {
if (jsonObject.has(field.getSerializationName())) {
unknownFields.remove(field.getSerializationName());
if (field.getName().equals(CONTENTS_FIELD) && UILayout.class.isAssignableFrom(elementMetadata.getType())) {
continue;
}
try {
if (List.class.isAssignableFrom(field.getType())) {
Type contentType = ReflectionUtil.getTypeParameter(field.getField().getGenericType(), 0);
if (contentType != null) {
List<Object> result = Lists.newArrayList();
JsonArray list = jsonObject.getAsJsonArray(field.getSerializationName());
for (JsonElement item : list) {
result.add(context.deserialize(item, contentType));
}
field.setValue(element, result);
}
} else {
field.setValue(element, context.deserialize(jsonObject.get(field.getSerializationName()), field.getType()));
}
} catch (RuntimeException e) {
logger.error("Failed to deserialize field {} of {}", field.getName(), type, e);
}
}
}
for (String key : unknownFields) {
logger.warn("Field '{}' not recognized for {} in {}", key, typeOfT, json);
}
// Deserialize contents and layout hints
if (UILayout.class.isAssignableFrom(elementMetadata.getType())) {
UILayout<LayoutHint> layout = (UILayout<LayoutHint>) element;
Class<? extends LayoutHint> layoutHintType = (Class<? extends LayoutHint>)
ReflectionUtil.getTypeParameter(elementMetadata.getType().getGenericSuperclass(), 0);
if (jsonObject.has(CONTENTS_FIELD)) {
for (JsonElement child : jsonObject.getAsJsonArray(CONTENTS_FIELD)) {
UIWidget childElement = context.deserialize(child, UIWidget.class);
if (childElement != null) {
LayoutHint hint = null;
if (child.isJsonObject()) {
JsonObject childObject = child.getAsJsonObject();
if (layoutHintType != null && !layoutHintType.isInterface() && !Modifier.isAbstract(layoutHintType.getModifiers())
&& childObject.has(LAYOUT_INFO_FIELD)) {
hint = context.deserialize(childObject.get(LAYOUT_INFO_FIELD), layoutHintType);
}
}
layout.addWidget(childElement, hint);
}
}
}
}
return element;
}
}
}