Key.java

// <editor-fold defaultstate="collapsed" desc="license">
/*
 * Copyright (c) 2009, 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.file;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import net.jarre_de_the.griffin.Util;
import net.jarre_de_the.griffin.types.data.DWordData;
import net.jarre_de_the.griffin.types.data.WordData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Key File
 * <p>
 * TODO: the header fields sometimes have getters/setters which use int ...
 * which is too narrow
 *
 * @author charly4711
 */
public class Key extends PersistableNwnFile {

    public static final String DEFAULT_FILE_TYPE = "KEY ";
    public static final String DEFAULT_FILE_VERSION = "V1  ";

    private static final Logger LOGGER = LoggerFactory.getLogger(Key.class);
    private static final int YEAR_ZERO = 1900;

    private List<FileEntry> files = new ArrayList<FileEntry>();
    private List<KeyEntry> keys = new ArrayList<KeyEntry>();

    public Key() {
        setFileType(DEFAULT_FILE_TYPE);
        setFileVersion(DEFAULT_FILE_VERSION);
    }

    /**
     * Contructor to read a Key file from disk.
     * <p>
     * @param in File on disk.
     * @throws IOException
     */
    public Key(File in)
            throws IOException {
        parseFile(in);
    }

    private void parseFile(File inFile)
            throws IOException {
        try (RandomAccessFile in = new RandomAccessFile(inFile, "r")) {
            // set the header fields
            for (KeyHeaderField f : KeyHeaderField.values()) {
                DWordData hf = DWordData.read(in);
                getHeaderFields().put(f.field(), hf);
            }

            // go to the file table
            in.seek(getFileTableOff());
            for (int i = 0; i < getBifCount(); i++) {
                files.add(new FileEntry(in));
            }

            // go to the key table
            in.seek(getKeyTableOff());
            for (int i = 0; i < getKeyCount(); i++) {
                keys.add(new KeyEntry(in));
            }
        }
    }

    @Override
    public byte[] persist() throws IOException {
        ByteArrayOutputStream fileArray = new ByteArrayOutputStream();
        ByteArrayOutputStream fileNameArray = new ByteArrayOutputStream();
        ByteArrayOutputStream keyArray = new ByteArrayOutputStream();

        DWordData[] header = new DWordData[KeyHeaderField.values().length];

        // file type
        DWordData hf = getHeaderFields().get(KeyHeaderField.FILE_TYPE.field());
        if (hf == null) {
            hf = new DWordData(Util.stringToBitField(DEFAULT_FILE_TYPE));
        }
        header[KeyHeaderField.FILE_TYPE.ordinal()] = hf;
        LOGGER.debug("ft: ");
        for (byte b : hf.getValueAsByteArray()) {
            char c = (char) b;
            LOGGER.debug("" + c);
        }
        LOGGER.debug("");

        // file version
        hf = getHeaderFields().get(KeyHeaderField.FILE_VERSION.field());
        if (hf == null) {
            hf = new DWordData(Util.stringToBitField(DEFAULT_FILE_VERSION));
        }
        header[KeyHeaderField.FILE_VERSION.ordinal()] = hf;
        LOGGER.debug("fv: ");
        for (byte b : hf.getValueAsByteArray()) {
            char c = (char) b;
            LOGGER.debug("" + c);
        }
        LOGGER.debug("");

        header[KeyHeaderField.BIF_COUNT.ordinal()] = new DWordData(files.size());
        header[KeyHeaderField.KEY_COUNT.ordinal()] = new DWordData(keys.size());
        header[KeyHeaderField.FILE_TABLE_OFFSET.ordinal()] = new DWordData(64);

        int fileNameOffset = 64 + (files.size() * FileEntry.LENGTH);

        for (FileEntry fe : files) {
            fileArray.write(new DWordData((int) (fe.getFileSize() & 0xFFFFFFFF)).getValueAsByteArray());
            fileArray.write(new DWordData(fileNameArray.size() + fileNameOffset).getValueAsByteArray());
            fileArray.write(new WordData((short) fe.getFileName().length()).getValueAsByteArray());
            fileArray.write(new WordData((short) fe.getDrive()).getValueAsByteArray());
            fileNameArray.write(fe.getFileName().getBytes(Util.CHARSET_US_ASCII));
            fileNameArray.write((byte) 0);
        }

        header[KeyHeaderField.KEY_TABLE_OFFSET.ordinal()] = new DWordData(64 + fileArray.size() + fileNameArray.size());
        for (KeyEntry key : keys) {
            String resRefString = key.getResRef();
            byte[] resRef = new byte[KeyEntry.RES_REF_LENGTH];
            System.arraycopy(resRefString.getBytes(Util.CHARSET_US_ASCII), 0,
                             resRef, 0, Math.min(resRefString.length(), resRef.length));
            keyArray.write(resRef);

            keyArray.write(new WordData((short) key.getResourceType().id()).getValueAsByteArray());

            // only variable resources for now
            int resId = key.getBif() << 20;
            resId |= key.bifRessourceTblIdx;
            keyArray.write(new DWordData(resId).getValueAsByteArray());
        }

        header[KeyHeaderField.KEY_TABLE_OFFSET.ordinal()] = new DWordData(64 + fileArray.size() + fileNameArray.size());
        header[KeyHeaderField.BUILD_YEAR.ordinal()] = new DWordData(getBuildYear() - YEAR_ZERO);
        header[KeyHeaderField.BUILD_DAY.ordinal()] = new DWordData(getBuildDay());
        header[KeyHeaderField.RESERVED1.ordinal()] = getHeaderFields().get(KeyHeaderField.RESERVED1.name());
        header[KeyHeaderField.RESERVED2.ordinal()] = getHeaderFields().get(KeyHeaderField.RESERVED2.name());
        header[KeyHeaderField.RESERVED3.ordinal()] = getHeaderFields().get(KeyHeaderField.RESERVED3.name());
        header[KeyHeaderField.RESERVED4.ordinal()] = getHeaderFields().get(KeyHeaderField.RESERVED4.name());
        header[KeyHeaderField.RESERVED5.ordinal()] = getHeaderFields().get(KeyHeaderField.RESERVED5.name());
        header[KeyHeaderField.RESERVED6.ordinal()] = getHeaderFields().get(KeyHeaderField.RESERVED6.name());
        header[KeyHeaderField.RESERVED7.ordinal()] = getHeaderFields().get(KeyHeaderField.RESERVED7.name());
        header[KeyHeaderField.RESERVED8.ordinal()] = getHeaderFields().get(KeyHeaderField.RESERVED8.name());

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        for (DWordData d : header) {
            if (null != d) {
                out.write(d.getValueAsByteArray());
            } else {
                out.write(new DWordData(0).getValueAsByteArray());
            }
        }
        out.write(fileArray.toByteArray());
        out.write(fileNameArray.toByteArray());
        out.write(keyArray.toByteArray());

        return out.toByteArray();
    }

    public KeyEntry findResource(String resRef) {
        KeyEntry result = null;
        for (KeyEntry ke : getKeyEntries()) {
            if (ke.getResRef().equals(resRef)) {
                result = ke;
            }
        }
        return result;
    }

    @Override
    public Key clone() throws CloneNotSupportedException {
        Key clone = (Key) super.clone();
        clone.setFileEntries(new ArrayList<FileEntry>());
        clone.setKeyEntries(new ArrayList<KeyEntry>());

        clone.setFileType(getFileType());
        clone.setFileVersion(getFileVersion());
        clone.setFileTableOff(getFileTableOff());
        clone.setBuildYear(getBuildYear());
        clone.setBuildDay(getBuildDay());
        clone.setBifCount(getBifCount());
        clone.setKeyCount(getKeyCount());
        clone.setKeyTableOff(getKeyTableOff());

        if (null != getKeyEntries()) {
            for (Key.KeyEntry k : getKeyEntries()) {
                clone.getKeyEntries().add(k.clone());
            }
        } else {
            clone.setKeyEntries(null);
        }

        if (null != getFileEntries()) {
            for (Key.FileEntry f : getFileEntries()) {
                clone.getFileEntries().add(f.clone());
            }
        } else {
            clone.setFileEntries(null);
        }

        return clone;
    }

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

        if (!super.equals(compare)) {
            return false;
        }

        if (!(compare instanceof Key)) {
            return false;
        }

        Key file = (Key) compare;

        return areNullablePropertiesEqual(this.getFileEntries(), file.getFileEntries())
                && areNullablePropertiesEqual(this.getKeyEntries(), file.getKeyEntries())
                && this.getBifCount() == file.getBifCount()
                && this.getBuildDay() == file.getBuildDay()
                && this.getBuildYear() == file.getBuildYear()
                //                && this.getFileTableOff() == file.getFileTableOff()
                && this.getKeyCount() == file.getKeyCount();
//                && this.getKeyTableOff() == file.getKeyTableOff();
    }

    @Override
    public int hashCode() {
        int hash = super.hashCode();
        hash = 41 * hash + Objects.hashCode(this.files);
        return hash;
    }

    protected enum KeyHeaderField {

        FILE_TYPE(CommonHeaderField.FILE_TYPE.field()),
        FILE_VERSION(CommonHeaderField.FILE_VERSION.field()),
        BIF_COUNT("bifCount"),
        KEY_COUNT("keyCount"),
        FILE_TABLE_OFFSET("fileTableOff"),
        KEY_TABLE_OFFSET("keyTableOff"),
        BUILD_YEAR("buildYear"),
        BUILD_DAY("buildDay"),
        RESERVED1("RESERVED1"),
        RESERVED2("RESERVED2"),
        RESERVED3("RESERVED3"),
        RESERVED4("RESERVED4"),
        RESERVED5("RESERVED5"),
        RESERVED6("RESERVED6"),
        RESERVED7("RESERVED7"),
        RESERVED8("RESERVED8");

        private final String field;

        private KeyHeaderField(String field) {
            this.field = field;
        }

        public String field() {
            return field;
        }
    }

    public long getBifCount() {
        return getHeaderNumber(KeyHeaderField.BIF_COUNT.field(), getFileEntries());
    }

    private void setBifCount(long bifCount) {
        getHeaderFields().put(KeyHeaderField.BIF_COUNT.field(),
                              new DWordData(bifCount));
    }

    public long getKeyCount() {
        return getHeaderNumber(KeyHeaderField.KEY_COUNT.field(), getKeyEntries());
    }

    private void setKeyCount(long keyCount) {
        getHeaderFields().put(KeyHeaderField.KEY_COUNT.field(),
                              new DWordData(keyCount));
    }

    public long getFileTableOff() {
        return getHeaderNumber(KeyHeaderField.FILE_TABLE_OFFSET.field(), null);
    }

    private void setFileTableOff(long fileTableOff) {
        getHeaderFields().put(KeyHeaderField.FILE_TABLE_OFFSET.field(),
                              new DWordData(fileTableOff));
    }

    public long getKeyTableOff() {
        return getHeaderNumber(KeyHeaderField.KEY_TABLE_OFFSET.field(), null);
    }

    private void setKeyTableOff(long keyTableOff) {
        getHeaderFields().put(KeyHeaderField.KEY_TABLE_OFFSET.field(),
                              new DWordData(keyTableOff));
    }

    public long getBuildYear() {
        return getHeaderNumber(KeyHeaderField.BUILD_YEAR.field(), null) + YEAR_ZERO;
    }

    public void setBuildYear(long buildYear) {
        getHeaderFields().put(KeyHeaderField.BUILD_YEAR.field(),
                              new DWordData(buildYear - YEAR_ZERO));
    }

    public long getBuildDay() {
        return getHeaderNumber(KeyHeaderField.BUILD_DAY.field(), null);
    }

    public void setBuildDay(long labelCount) {
        getHeaderFields().put(KeyHeaderField.BUILD_DAY.field(),
                              new DWordData(labelCount));
    }

    public List<FileEntry> getFileEntries() {
        return files;
    }

    public void setFileEntries(List<FileEntry> fileEntries) {
        files = fileEntries;
    }

    public List<KeyEntry> getKeyEntries() {
        return keys;
    }

    public void setKeyEntries(List<KeyEntry> keyEntries) {
        keys = keyEntries;
    }

    /**
     * Key files don't have the same kind of fields GFF files have. Keep the
     * entries here till we need them elsewhere.
     */
    public static class FileEntry implements Cloneable {

        private long fileSize = 0;
        private int drive = 0;
        private String fileName = "";

        public static final int LENGTH = (DWordData.LENGTH * 2) + (WordData.LENGTH * 2);

        public FileEntry() {
        }

        public FileEntry(RandomAccessFile in) throws IOException {
            DWordData fs = DWordData.read(in);
            fileSize = fs.getValueAsNumber();

            DWordData fileNameOffset = DWordData.read(in);
            WordData fileNameSize = WordData.read(in);
            drive = WordData.read(in).getValueAsNumber();

            // remember where we are
            long pos = in.getFilePointer();
            // then go to the file name
            long lPos = fileNameOffset.getValueAsNumber();
            in.seek(lPos);
            byte[] buf = new byte[fileNameSize.getValueAsNumber()];
            // and read the file name
            in.readFully(buf);
            // set it
            fileName = Util.getNullTerminatedString(buf);
            LOGGER.debug("Reading filename: \"" + fileName);

            // go back to the previous position
            in.seek(pos);
        }

        public long getFileSize() {
            return fileSize;
        }

        public void setFileSize(long fileSize) {
            this.fileSize = fileSize;
        }

        /**
         * A number that represents which drives the BIF file is located in.
         * Currently each bit represents a drive letter. e.g., bit 0 = HD0,
         * which is the directory where the BioWare application was installed.
         */
        public int getDrive() {
            return drive;
        }

        public void setDrive(int drive) {
            this.drive = drive;
        }

        public String getFileName() {
            return fileName;
        }

        public void setFileName(String fileName) {
            this.fileName = fileName;
        }

        /**
         * Overrides the {@code java.lang.Object} method to ensure we always get
         * back an instance of {@code FileEntry} rather than just
         * {@code Object}.
         * <p>
         * @return A deep copy of this object.
         * @throws java.lang.CloneNotSupportedException
         */
        @Override
        public FileEntry clone() throws CloneNotSupportedException {
            LOGGER.debug("Cloning object");
            FileEntry copy = (FileEntry) super.clone();
            copy.setDrive(this.getDrive());
            copy.setFileName(this.getFileName());
            copy.setFileSize(this.getFileSize());
            return copy;
        }

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

            if (!(compare instanceof FileEntry)) {
                return false;
            }

            FileEntry fe = (FileEntry) compare;
            if (fe.getDrive() == this.getDrive()
                    && fe.getFileName().equals(this.getFileName())
                    && fe.getFileSize() == this.getFileSize()) {
                return true;
            }

            return false;
        }

        @Override
        public int hashCode() {
            int code = (int) this.getFileSize();
            code = code << 16;
            code |= (this.getDrive() | this.getFileName().length());
            return code;
        }

        @Override
        public String toString() {
            return this.getClass().getSimpleName() + ": " + this.getFileName();
        }
    }

    public static class KeyEntry implements Cloneable {

        private String resRef = "";
        private ResourceType resourceType = ResourceType.GFF;
        private int bifRessourceTblIdx = 0;
        private int bif = 0;

        public static final int RES_REF_LENGTH = 16;
        public static final int LENGTH = DWordData.LENGTH + WordData.LENGTH + RES_REF_LENGTH;

        public KeyEntry() {
        }

        public KeyEntry(RandomAccessFile in) throws IOException {
            byte[] buf = new byte[RES_REF_LENGTH];
            // and read the res ref
            in.readFully(buf);
            // set it
            resRef = Util.getNullTerminatedString(buf);

            // the following is just the implementation for variable resources
            // as per Bioware that's all that's implemented, right now
            // TODO: add fixed resources
            resourceType = ResourceType.getResourceTypeById(WordData.read(in).getValueAsNumber());
            DWordData resId = DWordData.read(in);
            int bf = resId.getValue();
            int mask = Integer.parseInt("11111111111111111111", 2);
            bifRessourceTblIdx = bf & mask;
            bif = bf >>> 20;
        }

        public String getResRef() {
            return resRef;
        }

        public void setResRef(String resRef) {
            this.resRef = resRef;
        }

        public ResourceType getResourceType() {
            return resourceType;
        }

        public void setResourceType(ResourceType resourceType) {
            this.resourceType = resourceType;
        }

        public int getBifRessourceTblIdx() {
            return bifRessourceTblIdx;
        }

        public void setBifRessourceTblIdx(int bifRessourceTblIdx) {
            this.bifRessourceTblIdx = bifRessourceTblIdx;
        }

        public int getBif() {
            return bif;
        }

        public void setBif(int bif) {
            this.bif = bif;
        }

        /**
         * Overrides the {@code java.lang.Object} method to ensure we always get
         * back an instance of {@code FileEntry} rather than just
         * {@code Object}.
         * <p>
         * @return A deep copy of this object.
         * @throws java.lang.CloneNotSupportedException
         */
        @Override
        public KeyEntry clone() throws CloneNotSupportedException {
            LOGGER.debug("Cloning object");
            KeyEntry copy = (KeyEntry) super.clone();
            copy.setBif(this.getBif());
            copy.setResourceType(this.getResourceType());
            copy.setResRef(this.getResRef());
            copy.setBifRessourceTblIdx(this.getBifRessourceTblIdx());
            return copy;
        }

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

            if (!(compare instanceof KeyEntry)) {
                return false;
            }

            KeyEntry ke = (KeyEntry) compare;
            if (ke.getBif() == this.getBif()
                    && ke.getResRef().equals(this.getResRef())
                    && ke.getBifRessourceTblIdx() == this.getBifRessourceTblIdx()
                    && ke.getResourceType().equals(this.getResourceType())) {
                return true;
            }

            return false;
        }

        @Override
        public int hashCode() {
            int resId = getBif() << 20;
            resId |= getBifRessourceTblIdx();
            return resId;
        }

        @Override
        public String toString() {
            return this.getClass().getSimpleName() + ": " + this.resRef
                    + "." + this.getResourceType().extension();
        }
    }
}