/* * 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.common.base.Objects; import com.google.common.collect.BiMap; import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terasology.entitySystem.Component; import org.terasology.entitySystem.metadata.ComponentLibrary; import org.terasology.entitySystem.metadata.ComponentMetadata; import org.terasology.entitySystem.metadata.ReplicatedFieldMetadata; import org.terasology.module.Module; import org.terasology.persistence.typeHandling.DeserializationContext; import org.terasology.persistence.typeHandling.PersistedData; import org.terasology.persistence.typeHandling.Serializer; import org.terasology.persistence.typeHandling.TypeSerializationLibrary; import org.terasology.persistence.typeHandling.protobuf.ProtobufDeserializationContext; import org.terasology.persistence.typeHandling.protobuf.ProtobufPersistedData; import org.terasology.persistence.typeHandling.protobuf.ProtobufSerializationContext; import org.terasology.protobuf.EntityData; import org.terasology.reflection.metadata.FieldMetadata; import java.util.Map; /** * ComponentSerializer provides the ability to serialize and deserialize between Components and the protobuf * EntityData.Component * <br><br> * If provided with a idTable, then the components will be serialized and deserialized using those ids rather * than the names of each component, saving some space. * <br><br> * When serializing, a FieldSerializeCheck can be provided to determine whether each field should be serialized or not * */ public class ComponentSerializer { private static final Logger logger = LoggerFactory.getLogger(ComponentSerializer.class); private ComponentLibrary componentLibrary; private BiMap<Class<? extends Component>, Integer> idTable = ImmutableBiMap.<Class<? extends Component>, Integer>builder().build(); private boolean usingFieldIds; private TypeSerializationLibrary typeSerializationLibrary; private ProtobufSerializationContext serializationContext; private ProtobufDeserializationContext deserializationContext; /** * Creates the component serializer. * * @param componentLibrary The component library used to provide information on each component and its fields. */ public ComponentSerializer(ComponentLibrary componentLibrary, TypeSerializationLibrary typeSerializationLibrary) { this.componentLibrary = componentLibrary; this.typeSerializationLibrary = typeSerializationLibrary; this.serializationContext = new ProtobufSerializationContext(typeSerializationLibrary); this.deserializationContext = new ProtobufDeserializationContext(typeSerializationLibrary); } public void setUsingFieldIds(boolean usingFieldIds) { this.usingFieldIds = usingFieldIds; } public boolean isUsingFieldIds() { return usingFieldIds; } /** * Sets the mapping between component classes and the ids that are used for serialization * * @param table */ public void setIdMapping(Map<Class<? extends Component>, Integer> table) { idTable = ImmutableBiMap.copyOf(table); } /** * Clears the mapping between component classes and ids. This causes components to be serialized with their component * class name instead. */ public void removeIdMapping() { idTable = ImmutableBiMap.<Class<? extends Component>, Integer>builder().build(); } /** * @param componentData * @return The component described by the componentData, or null if it couldn't be deserialized */ public Component deserialize(EntityData.Component componentData) { return deserialize(componentData, null); } /** * @param componentData * @param context The module this component belongs to, or null if it is not being loaded from a module * @return The component described by the componentData, or null if it couldn't be deserialized */ public Component deserialize(EntityData.Component componentData, Module context) { ComponentMetadata<? extends Component> componentMetadata = getComponentMetadata(componentData, context); if (componentMetadata != null) { Component component = componentMetadata.newInstance(); return deserializeOnto(component, componentData, componentMetadata, FieldSerializeCheck.NullCheck.<Component>newInstance()); } else { logger.warn("Unable to deserialize unknown component type: {}", componentData.getType()); } return null; } /** * Deserializes the componentData on top of the target component. Any fields that are not present in the componentData, * or which cannot be deserialized, are left unaltered. * * @param target * @param componentData * @return The target component. */ public Component deserializeOnto(Component target, EntityData.Component componentData) { return deserializeOnto(target, componentData, FieldSerializeCheck.NullCheck.<Component>newInstance(), null); } /** * Deserializes the componentData on top of the target component. Any fields that are not present in the componentData, * or which cannot be deserialized, are left unaltered. * * @param target * @param componentData * @param context The module that contains the component being deserialized. May be null if it is not contained in a module. * @return The target component. */ public Component deserializeOnto(Component target, EntityData.Component componentData, Module context) { return deserializeOnto(target, componentData, FieldSerializeCheck.NullCheck.<Component>newInstance(), context); } /** * Deserializes the componentData on top of the target component. Any fields that are not present in the componentData, * or which cannot be deserialized, are left unaltered. * * @param target * @param componentData * @param fieldCheck * @return The target component. */ public Component deserializeOnto(Component target, EntityData.Component componentData, FieldSerializeCheck<Component> fieldCheck) { return deserializeOnto(target, componentData, fieldCheck, null); } /** * Deserializes the componentData on top of the target component. Any fields that are not present in the componentData, * or which cannot be deserialized, are left unaltered. * * @param target * @param componentData * @param fieldCheck * @param context The module this component is being deserialized from, or null if it isn't within a module * @return The target component. */ public Component deserializeOnto(Component target, EntityData.Component componentData, FieldSerializeCheck<Component> fieldCheck, Module context) { ComponentMetadata<? extends Component> componentMetadata = getComponentMetadata(componentData, context); if (componentMetadata != null) { return deserializeOnto(target, componentData, componentMetadata, fieldCheck); } else { logger.warn("Unable to deserialize unknown component type: {}", componentData.getType()); } return target; } private <T extends Component> Component deserializeOnto(Component targetComponent, EntityData.Component componentData, ComponentMetadata<T> componentMetadata, FieldSerializeCheck<Component> fieldCheck) { Serializer serializer = typeSerializationLibrary.getSerializerFor(componentMetadata); DeserializationContext context = new ProtobufDeserializationContext(typeSerializationLibrary); Map<FieldMetadata<?, ?>, PersistedData> dataMap = Maps.newHashMapWithExpectedSize(componentData.getFieldCount()); for (EntityData.NameValue field : componentData.getFieldList()) { FieldMetadata<?, ?> fieldInfo = null; if (field.hasNameIndex()) { fieldInfo = componentMetadata.getField(field.getNameIndex()); } else if (field.hasName()) { fieldInfo = componentMetadata.getField(field.getName()); } if (fieldInfo != null) { dataMap.put(fieldInfo, new ProtobufPersistedData(field.getValue())); } else if (field.hasName()) { logger.warn("Cannot deserialize unknown field '{}' onto '{}'", field.getName(), componentMetadata.getUri()); } } serializer.deserializeOnto(targetComponent, dataMap, context, fieldCheck); return targetComponent; } /** * Serializes a component. * * @param component * @return The serialized component, or null if it could not be serialized. */ public EntityData.Component serialize(Component component) { return serialize(component, FieldSerializeCheck.NullCheck.<Component>newInstance()); } /** * Serializes a component. * * @param component * @param check A check to use to see if each field should be serialized. * @return The serialized component, or null if it could not be serialized. */ public EntityData.Component serialize(Component component, FieldSerializeCheck<Component> check) { ComponentMetadata<?> componentMetadata = componentLibrary.getMetadata(component.getClass()); if (componentMetadata == null) { logger.error("Unregistered component type: {}", component.getClass()); return null; } EntityData.Component.Builder componentMessage = EntityData.Component.newBuilder(); serializeComponentType(componentMetadata, componentMessage); Serializer serializer = typeSerializationLibrary.getSerializerFor(componentMetadata); for (ReplicatedFieldMetadata<?, ?> field : componentMetadata.getFields()) { if (check.shouldSerializeField(field, component)) { PersistedData result = serializer.serialize(field, component, serializationContext); if (!result.isNull()) { EntityData.Value itemValue = ((ProtobufPersistedData) result).getValue(); if (usingFieldIds) { componentMessage.addField(EntityData.NameValue.newBuilder().setNameIndex(field.getId()).setValue(itemValue)); } else { componentMessage.addField(EntityData.NameValue.newBuilder().setName(field.getName()).setValue(itemValue)); } } } } return componentMessage.build(); } private void serializeComponentType(ComponentMetadata<?> componentMetadata, EntityData.Component.Builder componentMessage) { Integer compId = idTable.get(componentMetadata.getType()); if (compId != null) { componentMessage.setTypeIndex(compId); } else { componentMessage.setType(componentMetadata.getUri().toString()); } } /** * Serializes the differences between two components. * * @param base The base component to compare against. * @param delta The component whose differences will be serialized * @return The serialized component, or null if it could not be serialized */ public EntityData.Component serialize(Component base, Component delta) { return serialize(base, delta, FieldSerializeCheck.NullCheck.<Component>newInstance()); } /** * Serializes the differences between two components. * * @param base The base component to compare against. * @param delta The component whose differences will be serialized * @param check A check to use to see if each field should be serialized. * @return The serialized component, or null if it could not be serialized */ public EntityData.Component serialize(Component base, Component delta, FieldSerializeCheck<Component> check) { ComponentMetadata<?> componentMetadata = componentLibrary.getMetadata(base.getClass()); if (componentMetadata == null) { logger.error("Unregistered component type: {}", base.getClass()); return null; } EntityData.Component.Builder componentMessage = EntityData.Component.newBuilder(); serializeComponentType(componentMetadata, componentMessage); Serializer serializer = typeSerializationLibrary.getSerializerFor(componentMetadata); boolean changed = false; for (ReplicatedFieldMetadata field : componentMetadata.getFields()) { if (check.shouldSerializeField(field, delta) && serializer.getHandlerFor(field) != null) { Object origValue = field.getValue(base); Object deltaValue = field.getValue(delta); if (!Objects.equal(origValue, deltaValue)) { PersistedData value = serializer.serializeValue(field, deltaValue, serializationContext); if (!value.isNull()) { EntityData.Value dataValue = ((ProtobufPersistedData) value).getValue(); if (usingFieldIds) { componentMessage.addField(EntityData.NameValue.newBuilder().setNameIndex(field.getId()).setValue(dataValue).build()); } else { componentMessage.addField(EntityData.NameValue.newBuilder().setName(field.getName()).setValue(dataValue).build()); } changed = true; } } } } if (changed) { return componentMessage.build(); } return null; } /** * Determines the component class that the serialized component is for. * * @param componentData * @return The component class the given componentData describes, or null if it is unknown. */ public ComponentMetadata<? extends Component> getComponentMetadata(EntityData.Component componentData) { return getComponentMetadata(componentData, null); } /** * Determines the component class that the serialized component is for. * * @param componentData * @param context the module this component is being loaded from * @return The component class the given componentData describes, or null if it is unknown. */ public ComponentMetadata<? extends Component> getComponentMetadata(EntityData.Component componentData, Module context) { if (componentData.hasTypeIndex()) { ComponentMetadata<? extends Component> metadata = null; if (!idTable.isEmpty()) { Class<? extends Component> componentClass = idTable.inverse().get(componentData.getTypeIndex()); if (componentClass != null) { metadata = componentLibrary.getMetadata(componentClass); } } if (metadata == null) { logger.warn("Unable to deserialize unknown component with id: {}", componentData.getTypeIndex()); return null; } return metadata; } else if (componentData.hasType()) { ComponentMetadata<? extends Component> metadata; if (context != null) { metadata = componentLibrary.resolve(componentData.getType(), context); } else { metadata = componentLibrary.resolve(componentData.getType()); } if (metadata == null) { logger.warn("Unable to deserialize unknown component type: {}", componentData.getType()); return null; } return metadata; } logger.warn("Unable to deserialize component, no type provided."); return null; } /** * @return An immutable copy of the id mapping */ public Map<Class<? extends Component>, Integer> getIdMapping() { return ImmutableMap.copyOf(idTable); } }