CExoLocSubStringData.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.data;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.Charset;
import java.util.Arrays;
import net.jarre_de_the.griffin.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Implementation of CEXOLOCSUBSTRING. The GFF CEXOLOCSUBSTRING physically
 * consists of a DWORD for the String ID, a DWORD for the string length, and as
 * many bytes as stated in length.
 * <p>
 * @todo Protect against reading a 0 byte string from file.
 * @author charly4711
 */
public class CExoLocSubStringData extends AbstractData
        implements Cloneable {

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

    private int id = 0;

    private byte[] value;

    /*
     *
     * constructors
     *
     */
    protected CExoLocSubStringData() {
    }

    /**
     * Creates a new instance of {@code CExoLocSubStringData} and sets defined
     * values.
     * <p>
     * @param id String ID to set as the composite of language and gender id.
     * @param buf String contents as a byte array.
     */
    public CExoLocSubStringData(int id,
                                byte[] buf) {
        setId(id);
        setValue(buf);
    }

    /**
     * Creates a new instance of {@code CExoLocSubStringData} and sets defined
     * values. Note that the values passed will not be copied, atm.
     * <p>
     * @param langId The language id.
     * @param female True for female, false for other.
     * @param buf String contents as a byte array.
     * @see #setId(int)
     */
    public CExoLocSubStringData(LangId langId,
                                boolean female,
                                byte[] buf) {
        setId(langId, female);
        setValue(buf);
    }

    /*
     *
     * read from file
     *
     */
    /**
     * Creates a new instance of {@code CExoLocSubStringData} based on the data
     * read from the file passed. Note that the contents are read from whatever
     * position the file pointer is at.
     * <p>
     * @param in An open {@code RandomAccessFile}.
     * @return
     * @throws java.io.IOException If there are any I/O related problems with
     * file access to in.
     */
    public static CExoLocSubStringData read(RandomAccessFile in)
            throws IOException {
        LOGGER.debug("Reading CExoLocSubStringData from file.");
        CExoLocSubStringData ret = new CExoLocSubStringData();

        // read ID
        IntData id = IntData.read(in);
        LOGGER.trace("Id is:" + Integer.toHexString(id.getValueAsNumber()));
        ret.setId(id.getValueAsNumber());
        // read length
        IntData len = IntData.read(in);
        LOGGER.trace("Need to read " + len.getValueAsNumber() + " bytes");
        // we're just hoping we'll never encounter a GFF file where a
        // single buffer is > MAXINT
        byte[] buf = new byte[len.getValueAsNumber()];
        in.readFully(buf);
        if (LOGGER.isDebugEnabled()) {
            LangId l = LangId.getLangIdById((id.getValueAsNumber() & 0xFFFE) >> 1);
            LOGGER.debug("Read: " + Util.getNullTerminatedString(buf, l.charset()));
        }
        ret.setValue(buf);
        return ret;
    }

    /*
     *
     * setter
     *
     */
    /**
     * Sets the composite string ID. The ID consist of 1 for female or 0 for
     * male or other plus the (language ID * 2) taken from this table:
     * <p>
     * <table border='1'>
     * <tr><td>English</td><td>0</td></tr>
     * <tr><td>French</td><td>1</td></tr>
     * <tr><td>German</td><td>2</td></tr>
     * <tr><td>Italian</td><td>3</td></tr>
     * <tr><td>Spanish</td><td>4</td></tr>
     * <tr><td>Polish</td><td>5</td></tr>
     * <tr><td>Korean</td><td>128</td></tr>
     * <tr><td>Chinese Traditional</td><td>129</td></tr>
     * <tr><td>Chinese Simplified</td><td>130</td></tr>
     * <tr><td>Japanese</td><td>131</td></tr>
     * </table>
     * <p>
     * @param id The string ID to set.
     * @see LangId
     */
    private void setId(int id) {
        this.id = id;
    }

    /**
     * Sets the composite string ID from its individual components.
     * <p>
     * @param langId The language ID.
     * @param female True for female, false for other.
     * @see #setId(int)
     */
    private void setId(LangId langId,
                       boolean female) {
        int computedId = computeId(langId, female);
        LOGGER.debug("Setting id to " + computedId + " from lang " + langId.id()
                + " and female == " + female);
        setId(computedId);
    }

    /**
     * Sets the byte array value of this instance. Note that this does not copy
     * {@code buf}.
     * <p>
     * @param buf The byte array containing the string.
     */
    private void setValue(byte[] buf) {
        if (buf != null) {
            value = new byte[buf.length];
            System.arraycopy(buf, 0, value, 0, buf.length);
            LOGGER.debug("Set CExoLocSubStringData value to: "
                    + new String(value, getLangId().charset()));
        } else {
            buf = null;
        }
    }

    /*
     *
     * getter
     *
     */
    /**
     * Retrieves this instance's composite string ID.
     * <p>
     * @return The string ID.
     * @see #setId(int)
     */
    public int getId() {
        return id;
    }

    /**
     * Retrieves this instance's language ID.
     * <p>
     * @return The language ID.
     * @see #setId(int)
     */
    public LangId getLangId() {
        LOGGER.debug("Id " + id + " translates to lang id "
                + ((id & 0xFFFE) >> 1));
        return LangId.getLangIdById((id & 0xFFFE) >> 1);
    }

    /**
     * Checks the gender encoded in this instance's string ID.
     * <p>
     * @return True for female, false for other.
     */
    public boolean isFemale() {
        if ((id & 1) > 0) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Gets this instance's actual string content as a byte array. This ignores
     * the string ID.
     * <p>
     * @return The string content byte array.
     */
    public byte[] getValueAsByteArray() {
        if (null != value) {
            byte[] buf = new byte[value.length];
            System.arraycopy(value, 0, buf, 0, value.length);
            LOGGER.debug("Retrieving byte array "
                    + new String(buf, getLangId().charset()));
            return buf;
        } else {
            return null;
        }
    }

    /*
     *
     * utility
     *
     */
    /**
     * Sets the composite string ID from its individual components.
     * <p>
     * @param langId The language ID.
     * @param female True for female, false for other.
     * @return
     * @see #setId(int)
     */
    public static int computeId(LangId langId,
                                boolean female) {
        int id = 0;
        if (female) {
            id = 1;
        }
        id |= (langId.id() << 1);
        return id;
    }

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

        clone.setId(getLangId(), isFemale());
        byte[] dataCopy;
        if (null != value) {
            dataCopy = new byte[value.length];
            System.arraycopy(value, 0, dataCopy, 0, dataCopy.length);
        } else {
            dataCopy = null;
        }
        clone.setValue(dataCopy);

        return clone;
    }

    /**
     * Overrides the {@code java.lang.Object} method to make test for equality
     * possible for this class.
     * <p>
     * @param compare The Object to compare this object with.
     * @return True if the objects have the same value, false if not.
     */
    @Override
    public boolean equals(Object compare) {
        if (compare == this) {
            return true;
        }

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

        CExoLocSubStringData dw = (CExoLocSubStringData) compare;
        
        boolean valEqual = false;
        if (null == value && null == dw.getValueAsByteArray()) {
            valEqual = true;
        } else if (null == value && dw.getValueAsByteArray().length == 0) {
            valEqual = true;
        } else if (null == dw.getValueAsByteArray() && value.length == 0) {
            valEqual = true;
        } else if ((null != getValueAsByteArray()) && Arrays.equals(value, dw.getValueAsByteArray())) {
            valEqual = true;
        }        
        
        
        return id == dw.getId() && valEqual;
    }

    /**
     * Overrides the {@code java.lang.Object} method to ensure we have a hash
     * code consistent with the test for equality.
     * <p>
     * @return The hash returned is based on the value, but poorly distributed.
     */
    @Override
    public int hashCode() {
        return Arrays.hashCode(value) ^ id;
    }

    @Override
    public String toString() {
        if (value != null && value.length > 0) {
            return new String(value, getLangId().charset());
        }
        return EMPTY_VALUE;
    }

    /**
     * Enumeration of valid language IDs for an {@code CExoLocSubString}.
     */
    public enum LangId {

        EN(0, "windows-1252"),
        FR(1, "windows-1252"),
        DE(2, "windows-1252"),
        IT(3, "windows-1252"),
        ES(4, "windows-1252"),
        PO(5, "windows-1250"),
        KR(128, "x-windows-949"),
        ZH_HK(129, "x-windows-950"),
        ZH_CN(130, "x-mswin-936"),
        JP(131, "windows-31j");

        private final int id;
        private final Charset charSet;

        LangId(int id, String charSetName) {
            this.id = id;
            this.charSet = Charset.forName(charSetName);
        }

        public int id() {
            return id;
        }

        public Charset charset() {
            return charSet;
        }

        public static LangId getLangIdById(int id) {
            LangId lang = LangId.EN;
            for (LangId l : LangId.values()) {
                if (l.id() == id) {
                    lang = l;
                }
            }
            return lang;
        }
    }
}