AbstractField.java

// <editor-fold defaultstate="collapsed" desc="license">
/*
 * Copyright (c) 2014, Karl H. Beckers
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 * * Neither the name of the <ORGANIZATION> nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 **/
// </editor-fold>
package net.jarre_de_the.griffin.types.field;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.RandomAccessFile;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import net.jarre_de_the.griffin.Util;
import net.jarre_de_the.griffin.exception.NoSuchFieldTypeException;
import net.jarre_de_the.griffin.file.Gff;
import net.jarre_de_the.griffin.types.data.AbstractData;
import net.jarre_de_the.griffin.types.data.AbstractFixedLengthData;
import net.jarre_de_the.griffin.types.data.Container;
import net.jarre_de_the.griffin.types.data.DWordData;
import net.jarre_de_the.griffin.types.data.StructData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author charly4711
 * @param <T>
 */
public abstract class AbstractField<T extends AbstractData> {

    public static final int LABEL_LENGTH = 16;

    public static final int FIELD_LENGTH = 12;

    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractField.class);

    private final FieldType type;

    private final byte[] label = new byte[LABEL_LENGTH];

    private T data;

    /*
     *
     * constructors
     *
     */
    protected AbstractField(FieldType t,
                            T d) {
        type = t;
        data = d;
    }

    public AbstractField(RandomAccessFile in,
                         Gff file,
                         Class<T> dataClass)
            throws IOException {
        // type
        // read type value
        DWordData readType = DWordData.read(in);
        // try to find FieldType enum constant
        FieldType t = FieldType.DWORD;
        boolean foundSmth = false;
        for (FieldType ft : FieldType.values()) {
            if (ft.id() == readType.getValue()) {
                t = ft;
                foundSmth = true;
                break;
            }
        }
        if (!foundSmth) {
            LOGGER.debug("Error parsing Field Type DWord " + Integer.toHexString(
                    readType.getValue()) + "as a valid field type. Keeping default"
                    + " field type DWord.");
        }
        // set type if found
        this.type = t;

        // label
        // read pointer to label
        DWordData labelIndex = DWordData.read(in);

        // remember where we are
        long pos = in.getFilePointer();
        // then go to the label array
        long lPos = (labelIndex.getValueAsNumber() * LABEL_LENGTH)
                + file.getLabelOff() + file.getEmbeddedOffset();
        in.seek(lPos);
        byte[] buf = new byte[LABEL_LENGTH];
        // and read the label
        in.readFully(buf);
        // set it
        setLabel(buf);
        LOGGER.debug("Reading field of type \"" + getType() + "\" labelled: "
                + getLabelString());

        // go back to the field array to read the value
        in.seek(pos);

        // the actual data
        if (type.complex()) {
            // if we have a complex field, the data is just a pointer
            DWordData off = DWordData.read(in);
            switch (type) {
                case List:
                    // the pointer is a byte offset into the list index
                    in.seek(off.getValueAsNumber() + file.getListIdxOff() + file.getEmbeddedOffset());
                    break;
                case Struct:
                    // the pointer is an index into the struct array
                    in.seek((off.getValueAsNumber() * StructData.STRUCT_LENGTH)
                            + file.getStructOff() + file.getEmbeddedOffset());
                    break;
                default:
                    // the pointer is a byte offset into the field data block
                    in.seek(off.getValueAsNumber() + file.getFieldDataOff() + file.getEmbeddedOffset());
                    break;
            }
        }
        AbstractData readData = null;
        try {
            if (Container.class.isAssignableFrom(dataClass)) {
                Method m = dataClass.getMethod("read", RandomAccessFile.class, Gff.class);
                readData = (AbstractData) m.invoke(null, in, file);
            } else {
                Method m = dataClass.getMethod("read", RandomAccessFile.class);
                readData = (AbstractData) m.invoke(null, in);
                // the read in data may have been less than the 4 bytes that are reserved
                // for the DWORD. We need to compensate.
                if (readData != null && readData instanceof AbstractFixedLengthData && ((AbstractFixedLengthData) readData).
                        length() < DWordData.LENGTH) {
                    int skip = DWordData.LENGTH - ((AbstractFixedLengthData) readData).length();
                    in.seek(in.getFilePointer() + skip);
                }
            }
        } catch (NoSuchMethodException ex) {
            LOGGER.error(null, ex);
        } catch (SecurityException ex) {
            LOGGER.error(null, ex);
        } catch (IllegalAccessException ex) {
            LOGGER.error(null, ex);
        } catch (IllegalArgumentException ex) {
            LOGGER.error(null, ex);
        } catch (InvocationTargetException ex) {
            LOGGER.error(null, ex);
        }
        this.data = (T) readData;
    }

    /*
     *
     * abstract
     *
     */
    abstract public AbstractData getValue();

    abstract public void printString(PrintStream out,
                                     int align);

    /**
     * Writes the instance to byte array data structures as used in a physical
     * GFF file
     * <p>
     * @param structArray
     * @param fieldArray
     * @param labelArrayList
     * @param fieldIndicesArray
     * @param listIndicesArray
     * @param fieldData
     * @throws java.io.IOException
     */
    abstract public void persist(ByteArrayOutputStream structArray,
                                 ByteArrayOutputStream fieldArray,
                                 List<AbstractField.LabelWrapper> labelArrayList,
                                 ByteArrayOutputStream fieldIndicesArray,
                                 ByteArrayOutputStream listIndicesArray,
                                 ByteArrayOutputStream fieldData)
            throws IOException;

    protected DWordData[] persistTypeAndLabel(
            List<AbstractField.LabelWrapper> labelArrayList) {
        // buffer for the field itself
        DWordData fBuf[] = new DWordData[FIELD_LENGTH / DWordData.LENGTH];
        // store type
        fBuf[0] = new DWordData(getType().id());

        // store label pointer
        byte[] lbl = getLabel();
        int idx = labelArrayList.indexOf(new AbstractField.LabelWrapper(lbl));

        if (idx >= 0) {
            fBuf[1] = new DWordData(idx);
        } else {
            fBuf[1] = new DWordData(labelArrayList.size());
            labelArrayList.add(new AbstractField.LabelWrapper(lbl));
        }
        return fBuf;
    }

    /*
     *
     * read / write
     *
     */
    public static AbstractField readField(RandomAccessFile in,
                                          Gff file)
            throws IOException {
        long pos = in.getFilePointer();
        DWordData typeData = DWordData.read(in);
        // we just assume we'll never get more types than MAXINT
        int type = (int) typeData.getValue();
        // return to the type position
        in.seek(pos);

        FieldType ft = FieldType.getFieldTypeById(type);
        LOGGER.debug("Read type " + type + " and resolved it to FieldType: "
                + ft.name());
        Class<? extends AbstractField> fieldClass = ft.getFieldClassOfType();

        AbstractField f = null;
        try {
            Constructor<? extends AbstractField> c
                    = fieldClass.getConstructor(RandomAccessFile.class,
                                                Gff.class);
            f = c.newInstance(in, file);
        } catch (InstantiationException ex) {
            LOGGER.error("", ex);
        } catch (IllegalAccessException ex) {
            LOGGER.error("", ex);
        } catch (IllegalArgumentException ex) {
            LOGGER.error("", ex);
        } catch (InvocationTargetException ex) {
            LOGGER.error("", ex);
        } catch (NoSuchMethodException ex) {
            LOGGER.error("", ex);
        } catch (SecurityException ex) {
            LOGGER.error("", ex);
        }

        return f;
    }

    /*
     *
     * setter
     *
     */
    public void setLabel(byte[] label) {
        int len = label.length;
        if (len > LABEL_LENGTH) {
            LOGGER.debug("Label length (" + len + ") is larger than "
                    + "the supported maximum length of " + LABEL_LENGTH
                    + ", truncating label.");
            len = LABEL_LENGTH;
        }
        System.arraycopy(label, 0,
                         this.label, 0, len);
    }

    public void setData(T data) {
        this.data = data;
    }

    /*
     *
     * getter
     *
     */
    // label is treated as primitive (by copy) because we want to enforce
    // length restrictions
    public byte[] getLabel() {
        byte[] buf = new byte[LABEL_LENGTH];
        System.arraycopy(this.label, 0,
                         buf, 0, this.label.length);
        return buf;
    }

    public String getLabelString() {
        return Util.getNullTerminatedString(this.label);
    }

    public FieldType getType() {
        return type;
    }

    public T getData() {
        return data;
    }

    /*
     *
     * utility
     *
     */
    /**
     * The child classes do all the work.
     *
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    public AbstractField clone() throws CloneNotSupportedException {
        AbstractField clone = (AbstractField) super.clone();
        clone.setLabel(getLabel()); // getLabel() returns a copy
        return clone;
    }

    @Override
    public boolean equals(Object compare) {
        if (compare == this) {
            return true;
        }

        if (null == compare || !(compare instanceof AbstractField)) {
            return false;
        }

        if (!(this.getClass().isAssignableFrom(compare.getClass()))) {
            return false;
        }

        AbstractField f = (AbstractField) compare;
        if (!Arrays.equals(label, f.getLabel())) {
            return false;
        }

        // no need to compare the type again, here
        return (null == f.getValue() && null == this.getValue())
                || (null != this.getValue() && this.getValue().equals(f.getValue()));
    }

    @Override
    public int hashCode() {
        return this.getValue().hashCode();
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append((null != this.getType() ? this.getType().name() : "null"));
        sb.append("(");
        sb.append(this.getLabelString());
        sb.append("): ");

        sb.append((null != this.getValue() ? this.getValue().toString() : "null"));
        return sb.toString();
    }

    public static enum FieldType {

        BYTE(0, false, 1),
        CHAR(1, false, 1),
        WORD(2, false, 2),
        SHORT(3, false, 2),
        DWORD(4, false, 4),
        INT(5, false, 4),
        DWORD64(6, true, 4),
        INT64(7, true, 4),
        FLOAT(8, false, 4),
        DOUBLE(9, true, 4),
        CExoString(10, true, 4),
        CResRef(11, true, 4),
        CExoLocString(12, true, 4),
        VOID(13, true, 4),
        Struct(14, true, 4),
        List(15, true, 4);

        private final int id;

        private final boolean complex;

        private final int length;

        FieldType(int id,
                  boolean complex,
                  int len) {
            this.id = id;
            this.complex = complex;
            this.length = len;
        }

        public int id() {
            return id;
        }

        public boolean complex() {
            return complex;
        }

        public int length() {
            return length;
        }

        public Class<? extends AbstractField> getFieldClassOfType() {
            if (this.id() == FieldType.BYTE.id()) {
                return ByteField.class;
            } else if (this.id() == FieldType.CHAR.id()) {
                return CharField.class;
            } else if (this.id() == FieldType.WORD.id()) {
                return WordField.class;
            } else if (this.id() == FieldType.SHORT.id()) {
                return ShortField.class;
            } else if (this.id() == FieldType.DWORD.id()) {
                return DWordField.class;
            } else if (this.id() == FieldType.INT.id()) {
                return IntField.class;
            } else if (this.id() == FieldType.DWORD64.id()) {
                return DWord64Field.class;
            } else if (this.id() == FieldType.INT64.id()) {
                return Int64Field.class;
            } else if (this.id() == FieldType.FLOAT.id()) {
                return FloatField.class;
            } else if (this.id() == FieldType.DOUBLE.id()) {
                return DoubleField.class;
            } else if (this.id() == FieldType.CExoString.id()) {
                return CExoStringField.class;
            } else if (this.id() == FieldType.CResRef.id()) {
                return CResRefField.class;
            } else if (this.id() == FieldType.CExoLocString.id()) {
                return CExoLocStringField.class;
            } else if (this.id() == FieldType.VOID.id()) {
                return VoidField.class;
            } else if (this.id() == FieldType.Struct.id()) {
                return StructField.class;
            } else if (this.id() == FieldType.List.id()) {
                return ListField.class;
            } else {
                throw new NoSuchFieldTypeException(this);
            }
        }

        public static FieldType getFieldTypeById(int id) {
            FieldType f = FieldType.BYTE;
            for (FieldType ft : FieldType.values()) {
                if (ft.id() == id) {
                    f = ft;
                    return f;
                }
            }
            LOGGER.debug("Error finding Field Type by id: " + id
                    + ", keeping default Byte type.");
            return f;
        }
    }

    // wrapper around byte[] to be able to store byte arrays in a list and
    // treat byte[]s with same content as identical objects
    public static class LabelWrapper {

        private final byte[] _data;

        public LabelWrapper(byte[] data) {
            if (null != data) {
                _data = new byte[data.length];
                System.arraycopy(data, 0, _data, 0, data.length);
            } else {
                _data = null;
            }
        }

        @Override
        public boolean equals(Object obj) {
            if (null == obj) {
                return false;
            } else if (this == obj) {
                return true;
            } else if (obj instanceof AbstractField.LabelWrapper) {
                LabelWrapper compare = (LabelWrapper) obj;

                if (null == _data && null == compare._data) {
                    return true;
                }

                return Arrays.equals(compare._data, this._data);
            } else {
                return false;
            }
        }

        @Override
        public int hashCode() {
            return Arrays.hashCode(_data);
        }

        public void persist(ByteArrayOutputStream out)
                throws IOException {
            out.write(_data);
        }

        @Override
        public String toString() {
            return new String(_data, Util.CHARSET_US_ASCII);
        }

    }
}