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);
}
}
}