sx.kenji.sharpserializerjvm.SharpSerializer.java Source code

Java tutorial

Introduction

Here is the source code for sx.kenji.sharpserializerjvm.SharpSerializer.java

Source

/**
 *  Eternity Keeper, a Pillars of Eternity save game editor.
 *  Copyright (C) 2015 the authors.
 *
 *  Eternity Keeper is free software: you can redistribute it and/or
 *  modify it under the terms of the GNU General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  Eternity Keeper is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package sx.kenji.sharpserializerjvm;

// We host our own implementation of SharpSerializer which is used in
// Pillars of Eternity to serialize game objects into saves.

import com.google.common.io.LittleEndianDataInputStream;
import com.google.common.io.LittleEndianDataOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sx.kenji.sharpserializerjvm.properties.*;

import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

import static java.util.Map.Entry;

public class SharpSerializer {
    private static final Logger logger = LoggerFactory.getLogger(SharpSerializer.class);

    static final Map<String, Class> typeMap = TypeMap.map;
    private Map<Integer, Property> propertyCache = new HashMap<>();

    public static class Elements {
        public static final byte Collection = 1;
        public static final byte ComplexObject = 2;
        public static final byte Dictionary = 3;
        public static final byte MultiArray = 4;
        public static final byte Null = 5;
        public static final byte SimpleObject = 6;
        public static final byte SingleArray = 7;
        public static final byte ComplexObjectWithID = 8;
        public static final byte Reference = 9;
        public static final byte CollectionWithID = 10;
        public static final byte DictionaryWithID = 11;
        public static final byte SingleArrayWithID = 12;
        public static final byte MultiArrayWithID = 13;

        public static boolean isElementWithID(byte elementID) {
            return elementID == ComplexObjectWithID || elementID == CollectionWithID
                    || elementID == DictionaryWithID || elementID == SingleArrayWithID
                    || elementID == MultiArrayWithID;
        }
    }

    private final File targetFile;
    private long position = 0;

    public SharpSerializer(String filePath) throws FileNotFoundException {
        targetFile = new File(filePath);
        if (!targetFile.exists()) {
            throw new FileNotFoundException();
        }
    }

    public Optional<Property> deserialize() {
        try {
            FileInputStream baseStream = new FileInputStream(targetFile);
            try (LittleEndianDataInputStream stream = new LittleEndianDataInputStream(baseStream)) {

                baseStream.getChannel().position(position);
                Deserializer deserializer = new Deserializer(stream, this);
                Property property = deserializer.deserialize();
                position = baseStream.getChannel().position();

                return Optional.ofNullable(createObject(property));
            }
        } catch (IOException e) {
            logger.error("Error opening target file '%s' for deserializing: `{}`.", targetFile, e.getMessage());
        }

        return Optional.empty();
    }

    public void serialize(Property property) {
        try {
            FileOutputStream baseStream = new FileOutputStream(targetFile, true);

            try (LittleEndianDataOutputStream stream = new LittleEndianDataOutputStream(baseStream)) {

                baseStream.getChannel().position(baseStream.getChannel().size());

                Serializer serializer = new Serializer(stream);
                serializer.serialize(property);
            }
        } catch (IOException e) {
            logger.error("Error opening target file '%s' for serializing: `{}`.", targetFile, e.getMessage());
        }
    }

    public void addTypes(Map<String, Class> types) {
        SharpSerializer.typeMap.putAll(types);
    }

    public Optional<Property> followReference(final ReferenceTargetProperty property) {
        if (property.reference == null) {
            return Optional.empty();
        }

        return Optional.ofNullable(propertyCache.get(property.reference.id));
    }

    private Property createObject(Property property) {
        if (property == null) {
            logger.error("Property is null.");
            return null;
        }

        if (property instanceof NullProperty) {
            property.obj = null;
            return property;
        }

        if (property.type == null) {
            logger.error("Tried to create an object from a property with no type.");

            return null;
        }

        if (property instanceof SimpleProperty) {
            if (((SimpleProperty) property).type == null) {
                throw new IllegalArgumentException();
            }

            return createObjectFromSimpleProperty((SimpleProperty) property);
        }

        if (!(property instanceof ReferenceTargetProperty)) {
            logger.error("Don't know what to do with this property.");
            return null;
        }

        ReferenceTargetProperty referenceTarget = (ReferenceTargetProperty) property;

        if (referenceTarget.reference != null && !referenceTarget.reference.isProcessed) {

            return propertyCache.get(referenceTarget.reference.id);
        }

        Property value = createObjectCore(property);
        if (value == null) {
            logger.error("Unimplemented property type.");
            return null;
        }

        return value;
    }

    private Property createObjectCore(Object property) {
        // MultiDimensionalArray

        if (property instanceof SingleDimensionalArrayProperty) {
            return createObjectFromSingleDimensionalArrayProperty((SingleDimensionalArrayProperty) property);
        }

        if (property instanceof DictionaryProperty) {
            return createObjectFromDictionaryProperty((DictionaryProperty) property);
        }

        if (property instanceof CollectionProperty) {
            return createObjectFromCollectionProperty((CollectionProperty) property);
        }

        if (property instanceof ComplexProperty) {
            return createObjectFromComplexProperty((ComplexProperty) property);
        }

        return null;
    }

    private Property createObjectFromCollectionProperty(CollectionProperty property) {

        Class type = property.type.type;
        Object collection = createInstance(type);

        if (property.reference != null) {
            propertyCache.put(property.reference.id, property);
        }

        fillProperties(collection, property.properties);
        try {
            Method addMethod = collection.getClass().getMethod("add", Object.class);

            for (Property item : property.items) {
                Property value = createObject(item);
                addMethod.invoke(collection, value.obj);
            }
        } catch (NoSuchMethodException e) {
            logger.error("Supposed 'Collection' class `{}` had no add method: `{}`.",
                    collection.getClass().getSimpleName(), e.getMessage());
        } catch (InvocationTargetException | IllegalAccessException e) {
            logger.error("Unable to call add method on class `{}`: `{}`.", collection.getClass().getSimpleName(),
                    e.getMessage());
        }

        property.obj = collection;
        return property;
    }

    private Property createObjectFromDictionaryProperty(DictionaryProperty property) {

        Object dictionary = createInstance(property.type.type);

        if (property.reference != null) {
            propertyCache.put(property.reference.id, property);
        }

        fillProperties(dictionary, property.properties);
        try {
            Method putMethod = dictionary.getClass().getMethod("put", Object.class, Object.class);

            for (Entry<Property, Property> item : property.items) {
                Property key = createObject(item.getKey());
                Property value = createObject(item.getValue());
                putMethod.invoke(dictionary, key.obj, value.obj);
            }
        } catch (NoSuchMethodException e) {
            logger.error("Supposed 'Dictionary' class `{}` had no put method: `{}`.",
                    dictionary.getClass().getSimpleName(), e.getMessage());
        } catch (InvocationTargetException | IllegalAccessException e) {
            logger.error("Unable to call put method on class `{}`: `{}`.", dictionary.getClass().getSimpleName(),
                    e.getMessage());
        }

        property.obj = dictionary;
        return property;
    }

    private Property createObjectFromSingleDimensionalArrayProperty(SingleDimensionalArrayProperty property) {

        int itemsCount = property.items.size();
        Object[] array = new Object[itemsCount];

        if (property.reference != null) {
            propertyCache.put(property.reference.id, property);
        }

        for (int index = property.lowerBound; index < property.lowerBound + itemsCount; index++) {
            Property item = (Property) property.items.get(index);
            Property value = createObject(item);
            if (value != null) {
                array[index] = value.obj;
            }
        }

        property.obj = Arrays.copyOf(array, array.length, property.type.type);
        return property;
    }

    private Property createObjectFromComplexProperty(ComplexProperty property) {
        Object obj = createInstance(property.type.type);
        if (obj == null) {
            return null;
        }

        if (property.reference != null) {
            propertyCache.put(property.reference.id, property);
        }

        fillProperties(obj, property.properties);
        property.obj = obj;
        return property;
    }

    private void fillProperties(Object obj, List properties) {
        //noinspection unchecked
        for (Property property : (List<Property>) properties) {
            Field field;

            try {
                field = obj.getClass().getField(property.name);
            } catch (NoSuchFieldException e) {
                logger.error("Class '%s' has no field `{}`: `{}`.", obj.getClass().getSimpleName(), property.name,
                        e.getMessage());

                continue;
            }

            Property value = createObject(property);
            if (value == null) {
                continue;
            }

            try {
                field.set(obj, value.obj);
            } catch (IllegalAccessException | IllegalArgumentException e) {
                logger.error("Unable to set field `{}` of class `{}`: `{}`.", property.name,
                        obj.getClass().getSimpleName(), e.getMessage());
            }
        }
    }

    private Object createInstance(Class type) {
        if (type == null) {
            return null;
        }

        try {
            return type.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            logger.error("Unable to instantiate object of type `{}`: `{}`.", type.getSimpleName(), e.getMessage());
        }

        return null;
    }

    private Property createObjectFromSimpleProperty(SimpleProperty property) {
        property.obj = property.value;
        return property;
    }
}