meyfa/scratchlib

View on GitHub
src/main/java/scratchlib/objects/ScratchObjectStore.java

Summary

Maintainability
A
0 mins
Test Coverage
A
95%
package scratchlib.objects;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import scratchlib.project.ScratchProject;
import scratchlib.reader.ScratchInputStream;
import scratchlib.writer.ScratchOutputStream;


/**
 * Main container for project data: both the project's information and its stage
 * are wrapped in this structure.
 *
 * <p>
 * Object stores have one root object, followed by zero or more reference types
 * required by that root object. In other words, they simply wrap one main
 * object, but in a way that flattens its structure.
 *
 * <p>
 * They start with the header: "ObjS" + 0x01 + "Stch" + 0x01. Next, there is a
 * 32-bit integer describing the store's flattened size (number of elements).
 * Finally, the main object and its referenced objects are written out.
 *
 * <p>
 * Due to some undocumented reason, Scratch and BYOB include "orphaned" objects
 * after the main object and its referenced fields for the project's info store.
 * "Orphaned" here means that they are written out, included in the store
 * length, and have logical significance, but are not available by traversing
 * the store root. Examples include the transparent color and a magic byte
 * array.
 */
public class ScratchObjectStore
{
    /**
     * The object store header, consisting of "ObjS" + 0x01 + "Stch" + 0x01.
     */
    public static final String HEADER = "ObjS" + (char) 1 + "Stch" + (char) 1;

    private ScratchObject object;
    private List<ScratchObject> orphanedFields;

    /**
     * @param object The object contained in this store.
     */
    public ScratchObjectStore(ScratchObject object)
    {
        this.object = object;
        this.orphanedFields = new ArrayList<>();
    }

    /**
     * @param object The object contained in this store.
     * @param orphans Non-referenced objects stored in addition to the main one.
     */
    public ScratchObjectStore(ScratchObject object, List<ScratchObject> orphans)
    {
        this.object = object;
        this.orphanedFields = new ArrayList<>(orphans);
    }

    /**
     * @return The object contained in this store.
     */
    public ScratchObject get()
    {
        return object;
    }

    /**
     * Changes the object contained in this store.
     *
     * @param object The new object.
     */
    public void set(ScratchObject object)
    {
        this.object = object;
    }

    /**
     * @return A writable list containing this store's "orphaned" fields (fields
     *         not referenced from the root).
     */
    public List<ScratchObject> getOrphanedFields()
    {
        return orphanedFields;
    }

    /**
     * Sets this store's list of "orphaned" fields (fields not referenced from
     * the root).
     *
     * @param orphans The new list of orphaned fields.
     */
    public void setOrphanedFields(List<ScratchObject> orphans)
    {
        this.orphanedFields = orphans;
    }

    /**
     * Writes this object store to the given {@link ScratchOutputStream}.
     *
     * @param out The stream to write to.
     * @param project The project this store belongs to, for version info.
     * @throws IOException
     */
    public void writeTo(ScratchOutputStream out, ScratchProject project) throws IOException
    {
        // create reference table
        ScratchReferenceTable refTable = new ScratchReferenceTable();
        object.createReferences(refTable, project);
        for (ScratchObject object : orphanedFields) {
            object.createReferences(refTable, project);
        }

        // write header + size
        out.writeString(HEADER);
        out.write32bitUnsignedInt(refTable.size());

        // write objects from reference table
        for (ScratchObject object : refTable) {
            object.writeTo(out, refTable, project);
        }

        out.flush();
    }

    /**
     * Reads an object store from the given input stream. The stream must be
     * positioned <b>before</b> the object store header.
     *
     * @param in The input stream to read from.
     * @param project The project reading for.
     * @return The instance read.
     * @throws IOException
     */
    public static ScratchObjectStore readFrom(ScratchInputStream in, ScratchProject project) throws IOException
    {
        String header = in.readString(10);
        if (!header.equals(HEADER)) {
            throw new IOException("invalid object store header");
        }

        int size = in.read32bitUnsignedInt();

        // read objects
        List<ScratchObject> objectList = new ArrayList<>();
        ScratchReferenceTable refTable = new ScratchReferenceTable();
        for (int i = 0; i < size; ++i) {
            ScratchObject obj = ScratchObjects.read(in, project).get();
            objectList.add(obj);
            refTable.insert(obj);
        }

        // resolve references
        for (ScratchObject obj : objectList) {
            obj.resolveReferences(refTable);
        }

        // find orphaned fields
        ScratchReferenceTable reverseRefTable = new ScratchReferenceTable();
        objectList.get(0).createReferences(reverseRefTable, project);

        int oStart = reverseRefTable.size(), oEnd = objectList.size();
        List<ScratchObject> orphaned = objectList.subList(oStart, oEnd);

        return new ScratchObjectStore(objectList.get(0), orphaned);
    }
}