com.healthmarketscience.jackcess.Column.java Source code

Java tutorial

Introduction

Here is the source code for com.healthmarketscience.jackcess.Column.java

Source

/*
Copyright (c) 2005 Health Market Science, Inc.
    
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
    
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.
    
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
USA
    
You can contact Health Market Science at info@healthmarketscience.com
or at the following address:
    
Health Market Science
2700 Horizon Drive
Suite 200
King of Prussia, PA 19406
*/

package com.healthmarketscience.jackcess;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.Reader;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.SQLException;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.healthmarketscience.jackcess.complex.ComplexColumnInfo;
import com.healthmarketscience.jackcess.complex.ComplexValue;
import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey;
import com.healthmarketscience.jackcess.scsu.Compress;
import com.healthmarketscience.jackcess.scsu.EndOfInputException;
import com.healthmarketscience.jackcess.scsu.Expand;
import com.healthmarketscience.jackcess.scsu.IllegalInputException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Access database column definition
 * @author Tim McCune
 * @usage _general_class_
 */
public class Column implements Comparable<Column> {

    private static final Log LOG = LogFactory.getLog(Column.class);

    /**
     * Meaningless placeholder object for inserting values in an autonumber
     * column.  it is not required that this value be used (any passed in value
     * is ignored), but using this placeholder may make code more obvious.
     * @usage _general_field_
     */
    public static final Object AUTO_NUMBER = "<AUTO_NUMBER>";

    /**
     * Meaningless placeholder object for updating rows which indicates that a
     * given column should keep its existing value.
     * @usage _general_field_
     */
    public static final Object KEEP_VALUE = "<KEEP_VALUE>";

    /**
     * Access stores numeric dates in days.  Java stores them in milliseconds.
     */
    private static final double MILLISECONDS_PER_DAY = (24L * 60L * 60L * 1000L);

    /**
     * Access starts counting dates at Jan 1, 1900.  Java starts counting
     * at Jan 1, 1970.  This is the # of millis between them for conversion.
     */
    private static final long MILLIS_BETWEEN_EPOCH_AND_1900 = 25569L * (long) MILLISECONDS_PER_DAY;

    /**
     * Long value (LVAL) type that indicates that the value is stored on the
     * same page
     */
    private static final byte LONG_VALUE_TYPE_THIS_PAGE = (byte) 0x80;
    /**
     * Long value (LVAL) type that indicates that the value is stored on another
     * page
     */
    private static final byte LONG_VALUE_TYPE_OTHER_PAGE = (byte) 0x40;
    /**
     * Long value (LVAL) type that indicates that the value is stored on
     * multiple other pages
     */
    private static final byte LONG_VALUE_TYPE_OTHER_PAGES = (byte) 0x00;
    /**
     * Mask to apply the long length in order to get the flag bits (only the
     * first 2 bits are type flags).
     */
    private static final int LONG_VALUE_TYPE_MASK = 0xC0000000;

    /**
     * mask for the fixed len bit
     * @usage _advanced_field_
     */
    public static final byte FIXED_LEN_FLAG_MASK = (byte) 0x01;

    /**
     * mask for the auto number bit
     * @usage _advanced_field_
     */
    public static final byte AUTO_NUMBER_FLAG_MASK = (byte) 0x04;

    /**
     * mask for the auto number guid bit
     * @usage _advanced_field_
     */
    public static final byte AUTO_NUMBER_GUID_FLAG_MASK = (byte) 0x40;

    /**
     * mask for the unknown bit (possible "can be null"?)
     * @usage _advanced_field_
     */
    public static final byte UNKNOWN_FLAG_MASK = (byte) 0x02;

    // some other flags?
    // 0x10: replication related field (or hidden?)
    // 0x80: hyperlink (some memo based thing)

    /** the value for the "general" sort order */
    private static final short GENERAL_SORT_ORDER_VALUE = 1033;

    /**
     * the "general" text sort order, legacy version (access 2000-2007)
     * @usage _intermediate_field_
     */
    public static final SortOrder GENERAL_LEGACY_SORT_ORDER = new SortOrder(GENERAL_SORT_ORDER_VALUE, (byte) 0);

    /**
     * the "general" text sort order, latest version (access 2010+)
     * @usage _intermediate_field_
     */
    public static final SortOrder GENERAL_SORT_ORDER = new SortOrder(GENERAL_SORT_ORDER_VALUE, (byte) 1);

    /** pattern matching textual guid strings (allows for optional surrounding
        '{' and '}') */
    private static final Pattern GUID_PATTERN = Pattern.compile(
            "\\s*[{]?([\\p{XDigit}]{8})-([\\p{XDigit}]{4})-([\\p{XDigit}]{4})-([\\p{XDigit}]{4})-([\\p{XDigit}]{12})[}]?\\s*");

    /** header used to indicate unicode text compression */
    private static final byte[] TEXT_COMPRESSION_HEADER = { (byte) 0xFF, (byte) 0XFE };

    /** placeholder for column which is not numeric */
    private static final NumericInfo DEFAULT_NUMERIC_INFO = new NumericInfo();

    /** placeholder for column which is not textual */
    private static final TextInfo DEFAULT_TEXT_INFO = new TextInfo();

    /** owning table */
    private final Table _table;
    /** Whether or not the column is of variable length */
    private boolean _variableLength;
    /** Whether or not the column is an autonumber column */
    private boolean _autoNumber;
    /** Data type */
    private DataType _type;
    /** Maximum column length */
    private short _columnLength;
    /** 0-based column number */
    private short _columnNumber;
    /** index of the data for this column within a list of row data */
    private int _columnIndex;
    /** display index of the data for this column */
    private int _displayIndex;
    /** Column name */
    private String _name;
    /** the offset of the fixed data in the row */
    private int _fixedDataOffset;
    /** the index of the variable length data in the var len offset table */
    private int _varLenTableIndex;
    /** information specific to numeric columns */
    private NumericInfo _numericInfo = DEFAULT_NUMERIC_INFO;
    /** information specific to text columns */
    private TextInfo _textInfo = DEFAULT_TEXT_INFO;
    /** the auto number generator for this column (if autonumber column) */
    private AutoNumberGenerator _autoNumberGenerator;
    /** additional information specific to complex columns */
    private ComplexColumnInfo<? extends ComplexValue> _complexInfo;
    /** properties for this column, if any */
    private PropertyMap _props;

    /**
     * @usage _general_method_
     */
    public Column() {
        this(null);
    }

    /**
     * @usage _advanced_method_
     */
    public Column(JetFormat format) {
        _table = null;
    }

    /**
     * Only used by unit tests
     */
    Column(boolean testing, Table table) {
        if (!testing) {
            throw new IllegalArgumentException();
        }
        _table = table;
    }

    /**
     * Read a column definition in from a buffer
     * @param table owning table
     * @param buffer Buffer containing column definition
     * @param offset Offset in the buffer at which the column definition starts
     * @usage _advanced_method_
     */
    public Column(Table table, ByteBuffer buffer, int offset, int displayIndex) throws IOException {
        _table = table;
        _displayIndex = displayIndex;
        if (LOG.isDebugEnabled()) {
            LOG.debug("Column def block:\n" + ByteUtil.toHexString(buffer, offset, 25));
        }

        byte colType = buffer.get(offset + getFormat().OFFSET_COLUMN_TYPE);
        _columnNumber = buffer.getShort(offset + getFormat().OFFSET_COLUMN_NUMBER);
        _columnLength = buffer.getShort(offset + getFormat().OFFSET_COLUMN_LENGTH);

        byte flags = buffer.get(offset + getFormat().OFFSET_COLUMN_FLAGS);
        _variableLength = ((flags & FIXED_LEN_FLAG_MASK) == 0);
        _autoNumber = ((flags & (AUTO_NUMBER_FLAG_MASK | AUTO_NUMBER_GUID_FLAG_MASK)) != 0);

        try {
            _type = DataType.fromByte(colType);
        } catch (IOException e) {
            LOG.warn("Unsupported column type " + colType);
            _type = (_variableLength ? DataType.UNSUPPORTED_VARLEN : DataType.UNSUPPORTED_FIXEDLEN);
            setUnknownDataType(colType);
        }

        if (_type.getHasScalePrecision()) {
            modifyNumericInfo();
            _numericInfo._precision = buffer.get(offset + getFormat().OFFSET_COLUMN_PRECISION);
            _numericInfo._scale = buffer.get(offset + getFormat().OFFSET_COLUMN_SCALE);
        } else if (_type.isTextual()) {
            modifyTextInfo();

            // co-located w/ precision/scale
            _textInfo._sortOrder = readSortOrder(buffer, offset + getFormat().OFFSET_COLUMN_SORT_ORDER,
                    getFormat());
            int cpOffset = getFormat().OFFSET_COLUMN_CODE_PAGE;
            if (cpOffset >= 0) {
                _textInfo._codePage = buffer.getShort(offset + cpOffset);
            }

            _textInfo._compressedUnicode = ((buffer.get(offset + getFormat().OFFSET_COLUMN_COMPRESSED_UNICODE)
                    & 1) == 1);
        }

        setAutoNumberGenerator();

        if (_variableLength) {
            _varLenTableIndex = buffer.getShort(offset + getFormat().OFFSET_COLUMN_VARIABLE_TABLE_INDEX);
        } else {
            _fixedDataOffset = buffer.getShort(offset + getFormat().OFFSET_COLUMN_FIXED_DATA_OFFSET);
        }

        // load complex info
        if (_type == DataType.COMPLEX_TYPE) {
            _complexInfo = ComplexColumnInfo.create(this, buffer, offset);
        }
    }

    /**
     * Secondary column initialization after the table is fully loaded.
     */
    void postTableLoadInit() throws IOException {
        if (_complexInfo != null) {
            _complexInfo.postTableLoadInit();
        }
    }

    /**
     * @usage _general_method_
     */
    public Table getTable() {
        return _table;
    }

    /**
     * @usage _general_method_
     */
    public Database getDatabase() {
        return getTable().getDatabase();
    }

    /**
     * @usage _advanced_method_
     */
    public JetFormat getFormat() {
        return getDatabase().getFormat();
    }

    /**
     * @usage _advanced_method_
     */
    public PageChannel getPageChannel() {
        return getDatabase().getPageChannel();
    }

    /**
     * @usage _general_method_
     */
    public String getName() {
        return _name;
    }

    /**
     * @usage _advanced_method_
     */
    public void setName(String name) {
        _name = name;
    }

    /**
     * @usage _advanced_method_
     */
    public boolean isVariableLength() {
        return _variableLength;
    }

    /**
     * @usage _advanced_method_
     */
    public void setVariableLength(boolean variableLength) {
        _variableLength = variableLength;
    }

    /**
     * @usage _general_method_
     */
    public boolean isAutoNumber() {
        return _autoNumber;
    }

    /**
     * @usage _general_method_
     */
    public void setAutoNumber(boolean autoNumber) {
        _autoNumber = autoNumber;
        setAutoNumberGenerator();
    }

    /**
     * @usage _advanced_method_
     */
    public short getColumnNumber() {
        return _columnNumber;
    }

    /**
     * @usage _advanced_method_
     */
    public void setColumnNumber(short newColumnNumber) {
        _columnNumber = newColumnNumber;
    }

    /**
     * @usage _advanced_method_
     */
    public int getColumnIndex() {
        return _columnIndex;
    }

    /**
     * @usage _advanced_method_
     */
    public void setColumnIndex(int newColumnIndex) {
        _columnIndex = newColumnIndex;
    }

    /**
     * @usage _advanced_method_
     */
    public int getDisplayIndex() {
        return _displayIndex;
    }

    /**
     * Also sets the length and the variable length flag, inferred from the
     * type.  For types with scale/precision, sets the scale and precision to
     * default values.
     * @usage _general_method_
     */
    public void setType(DataType type) {
        _type = type;
        if (!type.isVariableLength()) {
            setLength((short) type.getFixedSize());
        } else if (!type.isLongValue()) {
            setLength((short) type.getDefaultSize());
        }
        setVariableLength(type.isVariableLength());
        if (type.getHasScalePrecision()) {
            setScale((byte) type.getDefaultScale());
            setPrecision((byte) type.getDefaultPrecision());
        }
    }

    /**
     * @usage _general_method_
     */
    public DataType getType() {
        return _type;
    }

    /**
     * @usage _general_method_
     */
    public int getSQLType() throws SQLException {
        return _type.getSQLType();
    }

    /**
     * @usage _general_method_
     */
    public void setSQLType(int type) throws SQLException {
        setSQLType(type, 0);
    }

    /**
     * @usage _general_method_
     */
    public void setSQLType(int type, int lengthInUnits) throws SQLException {
        setType(DataType.fromSQLType(type, lengthInUnits));
    }

    /**
     * @usage _general_method_
     */
    public boolean isCompressedUnicode() {
        return _textInfo._compressedUnicode;
    }

    /**
     * @usage _general_method_
     */
    public void setCompressedUnicode(boolean newCompessedUnicode) {
        modifyTextInfo();
        _textInfo._compressedUnicode = newCompessedUnicode;
    }

    /**
     * @usage _general_method_
     */
    public byte getPrecision() {
        return _numericInfo._precision;
    }

    /**
     * @usage _general_method_
     */
    public void setPrecision(byte newPrecision) {
        modifyNumericInfo();
        _numericInfo._precision = newPrecision;
    }

    /**
     * @usage _general_method_
     */
    public byte getScale() {
        return _numericInfo._scale;
    }

    /**
     * @usage _general_method_
     */
    public void setScale(byte newScale) {
        modifyNumericInfo();
        _numericInfo._scale = newScale;
    }

    /**
     * @usage _intermediate_method_
     */
    public SortOrder getTextSortOrder() {
        return _textInfo._sortOrder;
    }

    /**
     * @usage _advanced_method_
     */
    public void setTextSortOrder(SortOrder newTextSortOrder) {
        modifyTextInfo();
        _textInfo._sortOrder = newTextSortOrder;
    }

    /**
     * @usage _intermediate_method_
     */
    public short getTextCodePage() {
        return _textInfo._codePage;
    }

    /**
     * @usage _general_method_
     */
    public void setLength(short length) {
        _columnLength = length;
    }

    /**
     * @usage _general_method_
     */
    public short getLength() {
        return _columnLength;
    }

    /**
     * @usage _general_method_
     */
    public void setLengthInUnits(short unitLength) {
        setLength((short) getType().fromUnitSize(unitLength));
    }

    /**
     * @usage _general_method_
     */
    public short getLengthInUnits() {
        return (short) getType().toUnitSize(getLength());
    }

    /**
     * @usage _advanced_method_
     */
    public void setVarLenTableIndex(int idx) {
        _varLenTableIndex = idx;
    }

    /**
     * @usage _advanced_method_
     */
    public int getVarLenTableIndex() {
        return _varLenTableIndex;
    }

    /**
     * @usage _advanced_method_
     */
    public void setFixedDataOffset(int newOffset) {
        _fixedDataOffset = newOffset;
    }

    /**
     * @usage _advanced_method_
     */
    public int getFixedDataOffset() {
        return _fixedDataOffset;
    }

    protected Charset getCharset() {
        return getDatabase().getCharset();
    }

    protected TimeZone getTimeZone() {
        return getDatabase().getTimeZone();
    }

    /**
     * Whether or not this column is "append only" (its history is tracked by a
     * separate version history column).
     * @usage _general_method_
     */
    public boolean isAppendOnly() {
        return (getVersionHistoryColumn() != null);
    }

    /**
     * Returns the column which tracks the version history for an "append only"
     * column.
     * @usage _intermediate_method_
     */
    public Column getVersionHistoryColumn() {
        return _textInfo._versionHistoryCol;
    }

    /**
     * @usage _advanced_method_
     */
    public void setVersionHistoryColumn(Column versionHistoryCol) {
        modifyTextInfo();
        _textInfo._versionHistoryCol = versionHistoryCol;
    }

    /**
     * Returns extended functionality for "complex" columns.
     * @usage _general_method_
     */
    public ComplexColumnInfo<? extends ComplexValue> getComplexInfo() {
        return _complexInfo;
    }

    private void setUnknownDataType(byte type) {
        // slight hack, stash the original type in the _scale
        modifyNumericInfo();
        _numericInfo._scale = type;
    }

    private byte getUnknownDataType() {
        // slight hack, we stashed the real type in the _scale
        return _numericInfo._scale;
    }

    private void setAutoNumberGenerator() {
        if (!_autoNumber || (_type == null)) {
            _autoNumberGenerator = null;
            return;
        }

        if ((_autoNumberGenerator != null) && (_autoNumberGenerator.getType() == _type)) {
            // keep existing
            return;
        }

        switch (_type) {
        case LONG:
            _autoNumberGenerator = new LongAutoNumberGenerator();
            break;
        case GUID:
            _autoNumberGenerator = new GuidAutoNumberGenerator();
            break;
        case COMPLEX_TYPE:
            _autoNumberGenerator = new ComplexTypeAutoNumberGenerator();
            break;
        default:
            LOG.warn("Unknown auto number column type " + _type);
            _autoNumberGenerator = new UnsupportedAutoNumberGenerator(_type);
        }
    }

    /**
     * Returns the AutoNumberGenerator for this column if this is an autonumber
     * column, {@code null} otherwise.
     * @usage _advanced_method_
     */
    public AutoNumberGenerator getAutoNumberGenerator() {
        return _autoNumberGenerator;
    }

    /**
     * @return the properties for this column
     * @usage _general_method_
     */
    public PropertyMap getProperties() throws IOException {
        if (_props == null) {
            _props = getTable().getPropertyMaps().get(getName());
        }
        return _props;
    }

    private void modifyNumericInfo() {
        if (_numericInfo == DEFAULT_NUMERIC_INFO) {
            _numericInfo = new NumericInfo();
        }
    }

    private void modifyTextInfo() {
        if (_textInfo == DEFAULT_TEXT_INFO) {
            _textInfo = new TextInfo();
        }
    }

    /**
     * Checks that this column definition is valid.
     *
     * @throws IllegalArgumentException if this column definition is invalid.
     * @usage _advanced_method_
     */
    public void validate(JetFormat format) {
        if (getType() == null) {
            throw new IllegalArgumentException("must have type");
        }
        Database.validateIdentifierName(getName(), format.MAX_COLUMN_NAME_LENGTH, "column");

        if (getType().isUnsupported()) {
            throw new IllegalArgumentException("Cannot create column with unsupported type " + getType());
        }
        if (!format.isSupportedDataType(getType())) {
            throw new IllegalArgumentException("Database format " + format + " does not support type " + getType());
        }

        if (isVariableLength() != getType().isVariableLength()) {
            throw new IllegalArgumentException("invalid variable length setting");
        }

        if (!isVariableLength()) {
            if (getLength() != getType().getFixedSize()) {
                if (getLength() < getType().getFixedSize()) {
                    throw new IllegalArgumentException("invalid fixed length size");
                }
                LOG.warn("Column length " + getLength() + " longer than expected fixed size "
                        + getType().getFixedSize());
            }
        } else if (!getType().isLongValue()) {
            if (!getType().isValidSize(getLength())) {
                throw new IllegalArgumentException("var length out of range");
            }
        }

        if (getType().getHasScalePrecision()) {
            if (!getType().isValidScale(getScale())) {
                throw new IllegalArgumentException("Scale must be from " + getType().getMinScale() + " to "
                        + getType().getMaxScale() + " inclusive");
            }
            if (!getType().isValidPrecision(getPrecision())) {
                throw new IllegalArgumentException("Precision must be from " + getType().getMinPrecision() + " to "
                        + getType().getMaxPrecision() + " inclusive");
            }
        }

        if (isAutoNumber()) {
            if (!getType().mayBeAutoNumber()) {
                throw new IllegalArgumentException("Auto number column must be long integer or guid");
            }
        }

        if (isCompressedUnicode()) {
            if (!getType().isTextual()) {
                throw new IllegalArgumentException("Only textual columns allow unicode compression (text/memo)");
            }
        }
    }

    public Object setRowValue(Object[] rowArray, Object value) {
        rowArray[_columnIndex] = value;
        return value;
    }

    public Object setRowValue(Map<String, Object> rowMap, Object value) {
        rowMap.put(_name, value);
        return value;
    }

    public Object getRowValue(Object[] rowArray) {
        return rowArray[_columnIndex];
    }

    public Object getRowValue(Map<String, ?> rowMap) {
        return rowMap.get(_name);
    }

    /**
     * Deserialize a raw byte value for this column into an Object
     * @param data The raw byte value
     * @return The deserialized Object
     * @usage _advanced_method_
     */
    public Object read(byte[] data) throws IOException {
        return read(data, PageChannel.DEFAULT_BYTE_ORDER);
    }

    /**
     * Deserialize a raw byte value for this column into an Object
     * @param data The raw byte value
     * @param order Byte order in which the raw value is stored
     * @return The deserialized Object
     * @usage _advanced_method_
     */
    public Object read(byte[] data, ByteOrder order) throws IOException {
        ByteBuffer buffer = ByteBuffer.wrap(data);
        buffer.order(order);
        if (_type == DataType.BOOLEAN) {
            throw new IOException("Tried to read a boolean from data instead of null mask.");
        } else if (_type == DataType.BYTE) {
            return Byte.valueOf(buffer.get());
        } else if (_type == DataType.INT) {
            return Short.valueOf(buffer.getShort());
        } else if (_type == DataType.LONG) {
            return Integer.valueOf(buffer.getInt());
        } else if (_type == DataType.DOUBLE) {
            return Double.valueOf(buffer.getDouble());
        } else if (_type == DataType.FLOAT) {
            return Float.valueOf(buffer.getFloat());
        } else if (_type == DataType.SHORT_DATE_TIME) {
            return readDateValue(buffer);
        } else if (_type == DataType.BINARY) {
            return data;
        } else if (_type == DataType.TEXT) {
            return decodeTextValue(data);
        } else if (_type == DataType.MONEY) {
            return readCurrencyValue(buffer);
        } else if (_type == DataType.OLE) {
            if (data.length > 0) {
                return readLongValue(data);
            }
            return null;
        } else if (_type == DataType.MEMO) {
            if (data.length > 0) {
                return readLongStringValue(data);
            }
            return null;
        } else if (_type == DataType.NUMERIC) {
            return readNumericValue(buffer);
        } else if (_type == DataType.GUID) {
            return readGUIDValue(buffer, order);
        } else if ((_type == DataType.UNKNOWN_0D) || (_type == DataType.UNKNOWN_11)) {
            // treat like "binary" data
            return data;
        } else if (_type == DataType.COMPLEX_TYPE) {
            return new ComplexValueForeignKey(this, buffer.getInt());
        } else if (_type.isUnsupported()) {
            return rawDataWrapper(data);
        } else {
            throw new IOException("Unrecognized data type: " + _type);
        }
    }

    /**
     * @param lvalDefinition Column value that points to an LVAL record
     * @return The LVAL data
     */
    private byte[] readLongValue(byte[] lvalDefinition) throws IOException {
        ByteBuffer def = ByteBuffer.wrap(lvalDefinition).order(PageChannel.DEFAULT_BYTE_ORDER);
        int lengthWithFlags = def.getInt();
        int length = lengthWithFlags & (~LONG_VALUE_TYPE_MASK);

        byte[] rtn = new byte[length];
        byte type = (byte) ((lengthWithFlags & LONG_VALUE_TYPE_MASK) >>> 24);

        if (type == LONG_VALUE_TYPE_THIS_PAGE) {

            // inline long value
            def.getInt(); //Skip over lval_dp
            def.getInt(); //Skip over unknown
            def.get(rtn);

        } else {

            // long value on other page(s)
            if (lvalDefinition.length != getFormat().SIZE_LONG_VALUE_DEF) {
                throw new IOException("Expected " + getFormat().SIZE_LONG_VALUE_DEF
                        + " bytes in long value definition, but found " + lvalDefinition.length);
            }

            int rowNum = ByteUtil.getUnsignedByte(def);
            int pageNum = ByteUtil.get3ByteInt(def, def.position());
            ByteBuffer lvalPage = getPageChannel().createPageBuffer();

            switch (type) {
            case LONG_VALUE_TYPE_OTHER_PAGE: {
                getPageChannel().readPage(lvalPage, pageNum);

                short rowStart = Table.findRowStart(lvalPage, rowNum, getFormat());
                short rowEnd = Table.findRowEnd(lvalPage, rowNum, getFormat());

                if ((rowEnd - rowStart) != length) {
                    throw new IOException("Unexpected lval row length");
                }

                lvalPage.position(rowStart);
                lvalPage.get(rtn);
            }
                break;

            case LONG_VALUE_TYPE_OTHER_PAGES:

                ByteBuffer rtnBuf = ByteBuffer.wrap(rtn);
                int remainingLen = length;
                while (remainingLen > 0) {
                    lvalPage.clear();
                    getPageChannel().readPage(lvalPage, pageNum);

                    short rowStart = Table.findRowStart(lvalPage, rowNum, getFormat());
                    short rowEnd = Table.findRowEnd(lvalPage, rowNum, getFormat());

                    // read next page information
                    lvalPage.position(rowStart);
                    rowNum = ByteUtil.getUnsignedByte(lvalPage);
                    pageNum = ByteUtil.get3ByteInt(lvalPage);

                    // update rowEnd and remainingLen based on chunkLength
                    int chunkLength = (rowEnd - rowStart) - 4;
                    if (chunkLength > remainingLen) {
                        rowEnd = (short) (rowEnd - (chunkLength - remainingLen));
                        chunkLength = remainingLen;
                    }
                    remainingLen -= chunkLength;

                    lvalPage.limit(rowEnd);
                    rtnBuf.put(lvalPage);
                }

                break;

            default:
                throw new IOException("Unrecognized long value type: " + type);
            }
        }

        return rtn;
    }

    /**
     * @param lvalDefinition Column value that points to an LVAL record
     * @return The LVAL data
     */
    private String readLongStringValue(byte[] lvalDefinition) throws IOException {
        byte[] binData = readLongValue(lvalDefinition);
        if (binData == null) {
            return null;
        }
        return decodeTextValue(binData);
    }

    /**
     * Decodes "Currency" values.
     * 
     * @param buffer Column value that points to currency data
     * @return BigDecimal representing the monetary value
     * @throws IOException if the value cannot be parsed 
     */
    private static BigDecimal readCurrencyValue(ByteBuffer buffer) throws IOException {
        if (buffer.remaining() != 8) {
            throw new IOException("Invalid money value.");
        }

        return new BigDecimal(BigInteger.valueOf(buffer.getLong(0)), 4);
    }

    /**
     * Writes "Currency" values.
     */
    private static void writeCurrencyValue(ByteBuffer buffer, Object value) throws IOException {
        Object inValue = value;
        try {
            BigDecimal decVal = toBigDecimal(value);
            inValue = decVal;

            // adjust scale (will cause the an ArithmeticException if number has too
            // many decimal places)
            decVal = decVal.setScale(4);

            // now, remove scale and convert to long (this will throw if the value is
            // too big)
            buffer.putLong(decVal.movePointRight(4).longValueExact());
        } catch (ArithmeticException e) {
            throw (IOException) new IOException("Currency value '" + inValue + "' out of range").initCause(e);
        }
    }

    /**
     * Decodes a NUMERIC field.
     */
    private BigDecimal readNumericValue(ByteBuffer buffer) {
        boolean negate = (buffer.get() != 0);

        byte[] tmpArr = new byte[16];
        buffer.get(tmpArr);

        if (buffer.order() != ByteOrder.BIG_ENDIAN) {
            fixNumericByteOrder(tmpArr);
        }

        BigInteger intVal = new BigInteger(tmpArr);
        if (negate) {
            intVal = intVal.negate();
        }
        return new BigDecimal(intVal, getScale());
    }

    /**
     * Writes a numeric value.
     */
    private void writeNumericValue(ByteBuffer buffer, Object value) throws IOException {
        Object inValue = value;
        try {
            BigDecimal decVal = toBigDecimal(value);
            inValue = decVal;

            boolean negative = (decVal.compareTo(BigDecimal.ZERO) < 0);
            if (negative) {
                decVal = decVal.negate();
            }

            // write sign byte
            buffer.put(negative ? (byte) 0x80 : (byte) 0);

            // adjust scale according to this column type (will cause the an
            // ArithmeticException if number has too many decimal places)
            decVal = decVal.setScale(getScale());

            // check precision
            if (decVal.precision() > getPrecision()) {
                throw new IOException(
                        "Numeric value is too big for specified precision " + getPrecision() + ": " + decVal);
            }

            // convert to unscaled BigInteger, big-endian bytes
            byte[] intValBytes = decVal.unscaledValue().toByteArray();
            int maxByteLen = getType().getFixedSize() - 1;
            if (intValBytes.length > maxByteLen) {
                throw new IOException("Too many bytes for valid BigInteger?");
            }
            if (intValBytes.length < maxByteLen) {
                byte[] tmpBytes = new byte[maxByteLen];
                System.arraycopy(intValBytes, 0, tmpBytes, (maxByteLen - intValBytes.length), intValBytes.length);
                intValBytes = tmpBytes;
            }
            if (buffer.order() != ByteOrder.BIG_ENDIAN) {
                fixNumericByteOrder(intValBytes);
            }
            buffer.put(intValBytes);
        } catch (ArithmeticException e) {
            throw (IOException) new IOException("Numeric value '" + inValue + "' out of range").initCause(e);
        }
    }

    /**
     * Decodes a date value.
     */
    private Date readDateValue(ByteBuffer buffer) {
        // seems access stores dates in the local timezone.  guess you just hope
        // you read it in the same timezone in which it was written!
        long dateBits = buffer.getLong();
        long time = fromDateDouble(Double.longBitsToDouble(dateBits));
        return new DateExt(time, dateBits);
    }

    /**
     * Returns a java long time value converted from an access date double.
     */
    private long fromDateDouble(double value) {
        long time = Math.round(value * MILLISECONDS_PER_DAY);
        time -= MILLIS_BETWEEN_EPOCH_AND_1900;
        time -= getTimeZoneOffset(time);
        return time;
    }

    /**
     * Writes a date value.
     */
    private void writeDateValue(ByteBuffer buffer, Object value) {
        if (value == null) {
            buffer.putDouble(0d);
        } else if (value instanceof DateExt) {

            // this is a Date value previously read from readDateValue().  use the
            // original bits to store the value so we don't lose any precision
            buffer.putLong(((DateExt) value).getDateBits());

        } else {

            buffer.putDouble(toDateDouble(value));
        }
    }

    /**
     * Returns an access date double converted from a java Date/Calendar/Number
     * time value.
     */
    private double toDateDouble(Object value) {
        // seems access stores dates in the local timezone.  guess you just
        // hope you read it in the same timezone in which it was written!
        long time = ((value instanceof Date) ? ((Date) value).getTime()
                : ((value instanceof Calendar) ? ((Calendar) value).getTimeInMillis()
                        : ((Number) value).longValue()));
        time += getTimeZoneOffset(time);
        time += MILLIS_BETWEEN_EPOCH_AND_1900;
        return time / MILLISECONDS_PER_DAY;
    }

    /**
     * Gets the timezone offset from UTC for the given time (including DST).
     */
    private long getTimeZoneOffset(long time) {
        Calendar c = Calendar.getInstance(getTimeZone());
        c.setTimeInMillis(time);
        return ((long) c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET));
    }

    /**
     * Decodes a GUID value.
     */
    private static String readGUIDValue(ByteBuffer buffer, ByteOrder order) {
        if (order != ByteOrder.BIG_ENDIAN) {
            byte[] tmpArr = new byte[16];
            buffer.get(tmpArr);

            // the first 3 guid components are integer components which need to
            // respect endianness, so swap 4-byte int, 2-byte int, 2-byte int
            ByteUtil.swap4Bytes(tmpArr, 0);
            ByteUtil.swap2Bytes(tmpArr, 4);
            ByteUtil.swap2Bytes(tmpArr, 6);
            buffer = ByteBuffer.wrap(tmpArr);
        }

        StringBuilder sb = new StringBuilder(22);
        sb.append("{");
        sb.append(ByteUtil.toHexString(buffer, 0, 4, false));
        sb.append("-");
        sb.append(ByteUtil.toHexString(buffer, 4, 2, false));
        sb.append("-");
        sb.append(ByteUtil.toHexString(buffer, 6, 2, false));
        sb.append("-");
        sb.append(ByteUtil.toHexString(buffer, 8, 2, false));
        sb.append("-");
        sb.append(ByteUtil.toHexString(buffer, 10, 6, false));
        sb.append("}");
        return (sb.toString());
    }

    /**
     * Writes a GUID value.
     */
    private static void writeGUIDValue(ByteBuffer buffer, Object value, ByteOrder order) throws IOException {
        Matcher m = GUID_PATTERN.matcher(toCharSequence(value));
        if (m.matches()) {
            ByteBuffer origBuffer = null;
            byte[] tmpBuf = null;
            if (order != ByteOrder.BIG_ENDIAN) {
                // write to a temp buf so we can do some swapping below
                origBuffer = buffer;
                tmpBuf = new byte[16];
                buffer = ByteBuffer.wrap(tmpBuf);
            }

            ByteUtil.writeHexString(buffer, m.group(1));
            ByteUtil.writeHexString(buffer, m.group(2));
            ByteUtil.writeHexString(buffer, m.group(3));
            ByteUtil.writeHexString(buffer, m.group(4));
            ByteUtil.writeHexString(buffer, m.group(5));

            if (tmpBuf != null) {
                // the first 3 guid components are integer components which need to
                // respect endianness, so swap 4-byte int, 2-byte int, 2-byte int
                ByteUtil.swap4Bytes(tmpBuf, 0);
                ByteUtil.swap2Bytes(tmpBuf, 4);
                ByteUtil.swap2Bytes(tmpBuf, 6);
                origBuffer.put(tmpBuf);
            }

        } else {
            throw new IOException("Invalid GUID: " + value);
        }
    }

    /**
     * Write an LVAL column into a ByteBuffer inline if it fits, otherwise in
     * other data page(s).
     * @param value Value of the LVAL column
     * @return A buffer containing the LVAL definition and (possibly) the column
     *         value (unless written to other pages)
     * @usage _advanced_method_
     */
    public ByteBuffer writeLongValue(byte[] value, int remainingRowLength) throws IOException {
        if (value.length > getType().getMaxSize()) {
            throw new IOException(
                    "value too big for column, max " + getType().getMaxSize() + ", got " + value.length);
        }

        // determine which type to write
        byte type = 0;
        int lvalDefLen = getFormat().SIZE_LONG_VALUE_DEF;
        if (((getFormat().SIZE_LONG_VALUE_DEF + value.length) <= remainingRowLength)
                && (value.length <= getFormat().MAX_INLINE_LONG_VALUE_SIZE)) {
            type = LONG_VALUE_TYPE_THIS_PAGE;
            lvalDefLen += value.length;
        } else if (value.length <= getFormat().MAX_LONG_VALUE_ROW_SIZE) {
            type = LONG_VALUE_TYPE_OTHER_PAGE;
        } else {
            type = LONG_VALUE_TYPE_OTHER_PAGES;
        }

        ByteBuffer def = getPageChannel().createBuffer(lvalDefLen);
        // take length and apply type to first byte
        int lengthWithFlags = value.length | (type << 24);
        def.putInt(lengthWithFlags);

        if (type == LONG_VALUE_TYPE_THIS_PAGE) {
            // write long value inline
            def.putInt(0);
            def.putInt(0); //Unknown
            def.put(value);
        } else {

            TempPageHolder lvalBufferH = getTable().getLongValueBuffer();
            ByteBuffer lvalPage = null;
            int firstLvalPageNum = PageChannel.INVALID_PAGE_NUMBER;
            byte firstLvalRow = 0;

            // write other page(s)
            switch (type) {
            case LONG_VALUE_TYPE_OTHER_PAGE:
                lvalPage = getLongValuePage(value.length, lvalBufferH);
                firstLvalPageNum = lvalBufferH.getPageNumber();
                firstLvalRow = (byte) Table.addDataPageRow(lvalPage, value.length, getFormat(), 0);
                lvalPage.put(value);
                getPageChannel().writePage(lvalPage, firstLvalPageNum);
                break;

            case LONG_VALUE_TYPE_OTHER_PAGES:

                ByteBuffer buffer = ByteBuffer.wrap(value);
                int remainingLen = buffer.remaining();
                buffer.limit(0);
                lvalPage = getLongValuePage(getFormat().MAX_LONG_VALUE_ROW_SIZE, lvalBufferH);
                firstLvalPageNum = lvalBufferH.getPageNumber();
                int lvalPageNum = firstLvalPageNum;
                ByteBuffer nextLvalPage = null;
                int nextLvalPageNum = 0;
                while (remainingLen > 0) {
                    lvalPage.clear();

                    // figure out how much we will put in this page (we need 4 bytes for
                    // the next page pointer)
                    int chunkLength = Math.min(getFormat().MAX_LONG_VALUE_ROW_SIZE - 4, remainingLen);

                    // figure out if we will need another page, and if so, allocate it
                    if (chunkLength < remainingLen) {
                        // force a new page to be allocated
                        lvalBufferH.clear();
                        nextLvalPage = getLongValuePage(getFormat().MAX_LONG_VALUE_ROW_SIZE, lvalBufferH);
                        nextLvalPageNum = lvalBufferH.getPageNumber();
                    } else {
                        nextLvalPage = null;
                        nextLvalPageNum = 0;
                    }

                    // add row to this page
                    byte lvalRow = (byte) Table.addDataPageRow(lvalPage, chunkLength + 4, getFormat(), 0);

                    // write next page info (we'll always be writing into row 0 for
                    // newly created pages)
                    lvalPage.put((byte) 0); // row number
                    ByteUtil.put3ByteInt(lvalPage, nextLvalPageNum); // page number

                    // write this page's chunk of data
                    buffer.limit(buffer.limit() + chunkLength);
                    lvalPage.put(buffer);
                    remainingLen -= chunkLength;

                    // write new page to database
                    getPageChannel().writePage(lvalPage, lvalPageNum);

                    if (lvalPageNum == firstLvalPageNum) {
                        // save initial row info
                        firstLvalRow = lvalRow;
                    } else {
                        // check assertion that we wrote to row 0 for all subsequent pages
                        if (lvalRow != (byte) 0) {
                            throw new IllegalStateException("Expected row 0, but was " + lvalRow);
                        }
                    }

                    // move to next page
                    lvalPage = nextLvalPage;
                    lvalPageNum = nextLvalPageNum;
                }
                break;

            default:
                throw new IOException("Unrecognized long value type: " + type);
            }

            // update def
            def.put(firstLvalRow);
            ByteUtil.put3ByteInt(def, firstLvalPageNum);
            def.putInt(0); //Unknown

        }

        def.flip();
        return def;
    }

    /**
     * Writes the header info for a long value page.
     */
    private void writeLongValueHeader(ByteBuffer lvalPage) {
        lvalPage.put(PageTypes.DATA); //Page type
        lvalPage.put((byte) 1); //Unknown
        lvalPage.putShort((short) getFormat().DATA_PAGE_INITIAL_FREE_SPACE); //Free space
        lvalPage.put((byte) 'L');
        lvalPage.put((byte) 'V');
        lvalPage.put((byte) 'A');
        lvalPage.put((byte) 'L');
        lvalPage.putInt(0); //unknown
        lvalPage.putShort((short) 0); // num rows in page
    }

    /**
     * Returns a long value data page with space for data of the given length.
     */
    private ByteBuffer getLongValuePage(int dataLength, TempPageHolder lvalBufferH) throws IOException {
        ByteBuffer lvalPage = null;
        if (lvalBufferH.getPageNumber() != PageChannel.INVALID_PAGE_NUMBER) {
            lvalPage = lvalBufferH.getPage(getPageChannel());
            if (Table.rowFitsOnDataPage(dataLength, lvalPage, getFormat())) {
                // the current page has space
                return lvalPage;
            }
        }

        // need new page
        lvalPage = lvalBufferH.setNewPage(getPageChannel());
        writeLongValueHeader(lvalPage);
        return lvalPage;
    }

    /**
     * Serialize an Object into a raw byte value for this column in little
     * endian order
     * @param obj Object to serialize
     * @return A buffer containing the bytes
     * @usage _advanced_method_
     */
    public ByteBuffer write(Object obj, int remainingRowLength) throws IOException {
        return write(obj, remainingRowLength, PageChannel.DEFAULT_BYTE_ORDER);
    }

    /**
     * Serialize an Object into a raw byte value for this column
     * @param obj Object to serialize
     * @param order Order in which to serialize
     * @return A buffer containing the bytes
     * @usage _advanced_method_
     */
    public ByteBuffer write(Object obj, int remainingRowLength, ByteOrder order) throws IOException {
        if (isRawData(obj)) {
            // just slap it right in (not for the faint of heart!)
            return ByteBuffer.wrap(((RawData) obj).getBytes());
        }

        if (!isVariableLength() || !getType().isVariableLength()) {
            return writeFixedLengthField(obj, order);
        }

        // var length column
        if (!getType().isLongValue()) {

            // this is an "inline" var length field
            switch (getType()) {
            case NUMERIC:
                // don't ask me why numerics are "var length" columns...
                ByteBuffer buffer = getPageChannel().createBuffer(getType().getFixedSize(), order);
                writeNumericValue(buffer, obj);
                buffer.flip();
                return buffer;

            case TEXT:
                byte[] encodedData = encodeTextValue(obj, 0, getLengthInUnits(), false).array();
                obj = encodedData;
                break;

            case BINARY:
            case UNKNOWN_0D:
            case UNSUPPORTED_VARLEN:
                // should already be "encoded"
                break;
            default:
                throw new RuntimeException("unexpected inline var length type: " + getType());
            }

            ByteBuffer buffer = ByteBuffer.wrap(toByteArray(obj));
            buffer.order(order);
            return buffer;
        }

        // var length, long value column
        switch (getType()) {
        case OLE:
            // should already be "encoded"
            break;
        case MEMO:
            int maxMemoChars = DataType.MEMO.toUnitSize(DataType.MEMO.getMaxSize());
            obj = encodeTextValue(obj, 0, maxMemoChars, false).array();
            break;
        default:
            throw new RuntimeException("unexpected var length, long value type: " + getType());
        }

        // create long value buffer
        return writeLongValue(toByteArray(obj), remainingRowLength);
    }

    /**
     * Serialize an Object into a raw byte value for this column
     * @param obj Object to serialize
     * @param order Order in which to serialize
     * @return A buffer containing the bytes
     * @usage _advanced_method_
     */
    public ByteBuffer writeFixedLengthField(Object obj, ByteOrder order) throws IOException {
        int size = getType().getFixedSize(_columnLength);

        // create buffer for data
        ByteBuffer buffer = getPageChannel().createBuffer(size, order);

        // since booleans are not written by this method, it's safe to convert any
        // incoming boolean into an integer.
        obj = booleanToInteger(obj);

        switch (getType()) {
        case BOOLEAN:
            //Do nothing
            break;
        case BYTE:
            buffer.put(toNumber(obj).byteValue());
            break;
        case INT:
            buffer.putShort(toNumber(obj).shortValue());
            break;
        case LONG:
            buffer.putInt(toNumber(obj).intValue());
            break;
        case MONEY:
            writeCurrencyValue(buffer, obj);
            break;
        case FLOAT:
            buffer.putFloat(toNumber(obj).floatValue());
            break;
        case DOUBLE:
            buffer.putDouble(toNumber(obj).doubleValue());
            break;
        case SHORT_DATE_TIME:
            writeDateValue(buffer, obj);
            break;
        case TEXT:
            // apparently text numeric values are also occasionally written as fixed
            // length...
            int numChars = getLengthInUnits();
            // force uncompressed encoding for fixed length text
            buffer.put(encodeTextValue(obj, numChars, numChars, true));
            break;
        case GUID:
            writeGUIDValue(buffer, obj, order);
            break;
        case NUMERIC:
            // yes, that's right, occasionally numeric values are written as fixed
            // length...
            writeNumericValue(buffer, obj);
            break;
        case BINARY:
        case UNKNOWN_0D:
        case UNKNOWN_11:
        case COMPLEX_TYPE:
            buffer.putInt(toNumber(obj).intValue());
            break;
        case UNSUPPORTED_FIXEDLEN:
            byte[] bytes = toByteArray(obj);
            if (bytes.length != getLength()) {
                throw new IOException(
                        "Invalid fixed size binary data, size " + getLength() + ", got " + bytes.length);
            }
            buffer.put(bytes);
            break;
        default:
            throw new IOException("Unsupported data type: " + getType());
        }
        buffer.flip();
        return buffer;
    }

    /**
     * Decodes a compressed or uncompressed text value.
     */
    private String decodeTextValue(byte[] data) throws IOException {
        try {

            // see if data is compressed.  the 0xFF, 0xFE sequence indicates that
            // compression is used (sort of, see algorithm below)
            boolean isCompressed = ((data.length > 1) && (data[0] == TEXT_COMPRESSION_HEADER[0])
                    && (data[1] == TEXT_COMPRESSION_HEADER[1]));

            if (isCompressed) {

                Expand expander = new Expand();

                // this is a whacky compression combo that switches back and forth
                // between compressed/uncompressed using a 0x00 byte (starting in
                // compressed mode)
                StringBuilder textBuf = new StringBuilder(data.length);
                // start after two bytes indicating compression use
                int dataStart = TEXT_COMPRESSION_HEADER.length;
                int dataEnd = dataStart;
                boolean inCompressedMode = true;
                while (dataEnd < data.length) {
                    if (data[dataEnd] == (byte) 0x00) {

                        // handle current segment
                        decodeTextSegment(data, dataStart, dataEnd, inCompressedMode, expander, textBuf);
                        inCompressedMode = !inCompressedMode;
                        ++dataEnd;
                        dataStart = dataEnd;

                    } else {
                        ++dataEnd;
                    }
                }
                // handle last segment
                decodeTextSegment(data, dataStart, dataEnd, inCompressedMode, expander, textBuf);

                return textBuf.toString();

            }

            return decodeUncompressedText(data, getCharset());

        } catch (IllegalInputException e) {
            throw (IOException) new IOException("Can't expand text column").initCause(e);
        } catch (EndOfInputException e) {
            throw (IOException) new IOException("Can't expand text column").initCause(e);
        }
    }

    /**
     * Decodes a segnment of a text value into the given buffer according to the
     * given status of the segment (compressed/uncompressed).
     */
    private void decodeTextSegment(byte[] data, int dataStart, int dataEnd, boolean inCompressedMode,
            Expand expander, StringBuilder textBuf) throws IllegalInputException, EndOfInputException {
        if (dataEnd <= dataStart) {
            // no data
            return;
        }
        int dataLength = dataEnd - dataStart;
        if (inCompressedMode) {
            // handle compressed data
            byte[] tmpData = ByteUtil.copyOf(data, dataStart, dataLength);
            expander.reset();
            textBuf.append(expander.expand(tmpData));
        } else {
            // handle uncompressed data
            textBuf.append(decodeUncompressedText(data, dataStart, dataLength, getCharset()));
        }
    }

    /**
     * @param textBytes bytes of text to decode
     * @return the decoded string
     */
    private static CharBuffer decodeUncompressedText(byte[] textBytes, int startPos, int length, Charset charset) {
        return charset.decode(ByteBuffer.wrap(textBytes, startPos, length));
    }

    /**
     * Encodes a text value, possibly compressing.
     */
    private ByteBuffer encodeTextValue(Object obj, int minChars, int maxChars, boolean forceUncompressed)
            throws IOException {
        CharSequence text = toCharSequence(obj);
        if ((text.length() > maxChars) || (text.length() < minChars)) {
            throw new IOException("Text is wrong length for " + getType() + " column, max " + maxChars + ", min "
                    + minChars + ", got " + text.length());
        }

        // may only compress if column type allows it
        if (!forceUncompressed && isCompressedUnicode()) {

            // for now, only do very simple compression (only compress text which is
            // all ascii text)
            if (isAsciiCompressible(text)) {

                byte[] encodedChars = new byte[TEXT_COMPRESSION_HEADER.length + text.length()];
                encodedChars[0] = TEXT_COMPRESSION_HEADER[0];
                encodedChars[1] = TEXT_COMPRESSION_HEADER[1];
                for (int i = 0; i < text.length(); ++i) {
                    encodedChars[i + TEXT_COMPRESSION_HEADER.length] = (byte) text.charAt(i);
                }
                return ByteBuffer.wrap(encodedChars);
            }
        }

        return encodeUncompressedText(text, getCharset());
    }

    /**
     * Returns {@code true} if the given text can be compressed using simple
     * ASCII encoding, {@code false} otherwise.
     */
    private static boolean isAsciiCompressible(CharSequence text) {
        // only attempt to compress > 2 chars (compressing less than 3 chars would
        // not result in a space savings due to the 2 byte compression header)
        if (text.length() <= TEXT_COMPRESSION_HEADER.length) {
            return false;
        }
        // now, see if it is all printable ASCII
        for (int i = 0; i < text.length(); ++i) {
            char c = text.charAt(i);
            if (!Compress.isAsciiCrLfOrTab(c)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Constructs a byte containing the flags for this column.
     */
    private byte getColumnBitFlags() {
        byte flags = Column.UNKNOWN_FLAG_MASK;
        if (!isVariableLength()) {
            flags |= Column.FIXED_LEN_FLAG_MASK;
        }
        if (isAutoNumber()) {
            flags |= getAutoNumberGenerator().getColumnFlags();
        }
        return flags;
    }

    @Override
    public String toString() {
        StringBuilder rtn = new StringBuilder();
        rtn.append("\tName: (" + _table.getName() + ") " + _name);
        byte typeValue = _type.getValue();
        if (_type.isUnsupported()) {
            typeValue = getUnknownDataType();
        }
        rtn.append("\n\tType: 0x" + Integer.toHexString(typeValue) + " (" + _type + ")");
        rtn.append("\n\tNumber: " + _columnNumber);
        rtn.append("\n\tLength: " + _columnLength);
        rtn.append("\n\tVariable length: " + _variableLength);
        if (_type.isTextual()) {
            rtn.append("\n\tCompressed Unicode: " + _textInfo._compressedUnicode);
            rtn.append("\n\tText Sort order: " + _textInfo._sortOrder);
            if (_textInfo._codePage > 0) {
                rtn.append("\n\tText Code Page: " + _textInfo._codePage);
            }
            if (isAppendOnly()) {
                rtn.append("\n\tAppend only: " + isAppendOnly());
            }
        }
        if (_autoNumber) {
            rtn.append("\n\tLast AutoNumber: " + _autoNumberGenerator.getLast());
        }
        if (_complexInfo != null) {
            rtn.append("\n\tComplexInfo: " + _complexInfo);
        }
        rtn.append("\n\n");
        return rtn.toString();
    }

    /**
     * @param textBytes bytes of text to decode
     * @param charset relevant charset
     * @return the decoded string
     * @usage _advanced_method_
     */
    public static String decodeUncompressedText(byte[] textBytes, Charset charset) {
        return decodeUncompressedText(textBytes, 0, textBytes.length, charset).toString();
    }

    /**
     * @param text Text to encode
     * @param charset database charset
     * @return A buffer with the text encoded
     * @usage _advanced_method_
     */
    public static ByteBuffer encodeUncompressedText(CharSequence text, Charset charset) {
        CharBuffer cb = ((text instanceof CharBuffer) ? (CharBuffer) text : CharBuffer.wrap(text));
        return charset.encode(cb);
    }

    /**
     * Orders Columns by column number.
     * @usage _general_method_
     */
    public int compareTo(Column other) {
        if (_columnNumber > other.getColumnNumber()) {
            return 1;
        } else if (_columnNumber < other.getColumnNumber()) {
            return -1;
        } else {
            return 0;
        }
    }

    /**
     * @param columns A list of columns in a table definition
     * @return The number of variable length columns found in the list
     * @usage _advanced_method_
     */
    public static short countVariableLength(List<Column> columns) {
        short rtn = 0;
        for (Column col : columns) {
            if (col.isVariableLength()) {
                rtn++;
            }
        }
        return rtn;
    }

    /**
     * @param columns A list of columns in a table definition
     * @return The number of variable length columns which are not long values
     *         found in the list
     * @usage _advanced_method_
     */
    public static short countNonLongVariableLength(List<Column> columns) {
        short rtn = 0;
        for (Column col : columns) {
            if (col.isVariableLength() && !col.getType().isLongValue()) {
                rtn++;
            }
        }
        return rtn;
    }

    /**
     * @return an appropriate BigDecimal representation of the given object.
     *         <code>null</code> is returned as 0 and Numbers are converted
     *         using their double representation.
     */
    private static BigDecimal toBigDecimal(Object value) {
        if (value == null) {
            return BigDecimal.ZERO;
        } else if (value instanceof BigDecimal) {
            return (BigDecimal) value;
        } else if (value instanceof BigInteger) {
            return new BigDecimal((BigInteger) value);
        } else if (value instanceof Number) {
            return new BigDecimal(((Number) value).doubleValue());
        }
        return new BigDecimal(value.toString());
    }

    /**
     * @return an appropriate Number representation of the given object.
     *         <code>null</code> is returned as 0 and Strings are parsed as
     *         Doubles.
     */
    private static Number toNumber(Object value) {
        if (value == null) {
            return BigDecimal.ZERO;
        }
        if (value instanceof Number) {
            return (Number) value;
        }
        return Double.valueOf(value.toString());
    }

    /**
     * @return an appropriate CharSequence representation of the given object.
     * @usage _advanced_method_
     */
    public static CharSequence toCharSequence(Object value) throws IOException {
        if (value == null) {
            return null;
        } else if (value instanceof CharSequence) {
            return (CharSequence) value;
        } else if (value instanceof Clob) {
            try {
                Clob c = (Clob) value;
                // note, start pos is 1-based
                return c.getSubString(1L, (int) c.length());
            } catch (SQLException e) {
                throw (IOException) (new IOException(e.getMessage())).initCause(e);
            }
        } else if (value instanceof Reader) {
            char[] buf = new char[8 * 1024];
            StringBuilder sout = new StringBuilder();
            Reader in = (Reader) value;
            int read = 0;
            while ((read = in.read(buf)) != -1) {
                sout.append(buf, 0, read);
            }
            return sout;
        }

        return value.toString();
    }

    /**
     * @return an appropriate byte[] representation of the given object.
     * @usage _advanced_method_
     */
    public static byte[] toByteArray(Object value) throws IOException {
        if (value == null) {
            return null;
        } else if (value instanceof byte[]) {
            return (byte[]) value;
        } else if (value instanceof Blob) {
            try {
                Blob b = (Blob) value;
                // note, start pos is 1-based
                return b.getBytes(1L, (int) b.length());
            } catch (SQLException e) {
                throw (IOException) (new IOException(e.getMessage())).initCause(e);
            }
        }

        ByteArrayOutputStream bout = new ByteArrayOutputStream();

        if (value instanceof InputStream) {
            byte[] buf = new byte[8 * 1024];
            InputStream in = (InputStream) value;
            int read = 0;
            while ((read = in.read(buf)) != -1) {
                bout.write(buf, 0, read);
            }
        } else {
            // if all else fails, serialize it
            ObjectOutputStream oos = new ObjectOutputStream(bout);
            oos.writeObject(value);
            oos.close();
        }

        return bout.toByteArray();
    }

    /**
     * Interpret a boolean value (null == false)
     * @usage _advanced_method_
     */
    public static boolean toBooleanValue(Object obj) {
        return ((obj != null) && ((Boolean) obj).booleanValue());
    }

    /**
     * Swaps the bytes of the given numeric in place.
     */
    private static void fixNumericByteOrder(byte[] bytes) {
        // fix endianness of each 4 byte segment
        for (int i = 0; i < 4; ++i) {
            ByteUtil.swap4Bytes(bytes, i * 4);
        }
    }

    /**
     * Treat booleans as integers (C-style).
     */
    protected static Object booleanToInteger(Object obj) {
        if (obj instanceof Boolean) {
            obj = ((Boolean) obj) ? 1 : 0;
        }
        return obj;
    }

    /**
     * Returns a wrapper for raw column data that can be written without
     * understanding the data.  Useful for wrapping unparseable data for
     * re-writing.
     */
    static RawData rawDataWrapper(byte[] bytes) {
        return new RawData(bytes);
    }

    /**
     * Returs {@code true} if the given value is "raw" column data,
     * {@code false} otherwise.
     */
    static boolean isRawData(Object value) {
        return (value instanceof RawData);
    }

    /**
     * Writes the column definitions into a table definition buffer.
     * @param buffer Buffer to write to
     * @param columns List of Columns to write definitions for
     */
    protected static void writeDefinitions(TableCreator creator, ByteBuffer buffer) throws IOException {
        List<Column> columns = creator.getColumns();
        short columnNumber = (short) 0;
        short fixedOffset = (short) 0;
        short variableOffset = (short) 0;
        // we specifically put the "long variable" values after the normal
        // variable length values so that we have a better chance of fitting it
        // all (because "long variable" values can go in separate pages)
        short longVariableOffset = Column.countNonLongVariableLength(columns);
        for (Column col : columns) {
            // record this for later use when writing indexes
            col.setColumnNumber(columnNumber);

            int position = buffer.position();
            buffer.put(col.getType().getValue());
            buffer.putInt(Table.MAGIC_TABLE_NUMBER); //constant magic number
            buffer.putShort(columnNumber); //Column Number
            if (col.isVariableLength()) {
                if (!col.getType().isLongValue()) {
                    buffer.putShort(variableOffset++);
                } else {
                    buffer.putShort(longVariableOffset++);
                }
            } else {
                buffer.putShort((short) 0);
            }
            buffer.putShort(columnNumber); //Column Number again
            if (col.getType().isTextual()) {
                // this will write 4 bytes (note we don't support writing dbs which
                // use the text code page)
                writeSortOrder(buffer, col.getTextSortOrder(), creator.getFormat());
            } else {
                if (col.getType().getHasScalePrecision()) {
                    buffer.put(col.getPrecision()); // numeric precision
                    buffer.put(col.getScale()); // numeric scale
                } else {
                    buffer.put((byte) 0x00); //unused
                    buffer.put((byte) 0x00); //unused
                }
                buffer.putShort((short) 0); //Unknown
            }
            buffer.put(col.getColumnBitFlags()); // misc col flags
            if (col.isCompressedUnicode()) { //Compressed
                buffer.put((byte) 1);
            } else {
                buffer.put((byte) 0);
            }
            buffer.putInt(0); //Unknown, but always 0.
            //Offset for fixed length columns
            if (col.isVariableLength()) {
                buffer.putShort((short) 0);
            } else {
                buffer.putShort(fixedOffset);
                fixedOffset += col.getType().getFixedSize(col.getLength());
            }
            if (!col.getType().isLongValue()) {
                buffer.putShort(col.getLength()); //Column length
            } else {
                buffer.putShort((short) 0x0000); // unused
            }
            columnNumber++;
            if (LOG.isDebugEnabled()) {
                LOG.debug("Creating new column def block\n"
                        + ByteUtil.toHexString(buffer, position, creator.getFormat().SIZE_COLUMN_DEF_BLOCK));
            }
        }
        for (Column col : columns) {
            Table.writeName(buffer, col.getName(), creator.getCharset());
        }
    }

    /**
     * Reads the sort order info from the given buffer from the given position.
     */
    static SortOrder readSortOrder(ByteBuffer buffer, int position, JetFormat format) {
        short value = buffer.getShort(position);
        byte version = 0;
        if (format.SIZE_SORT_ORDER == 4) {
            version = buffer.get(position + 3);
        }

        if (value == 0) {
            // probably a file we wrote, before handling sort order
            return format.DEFAULT_SORT_ORDER;
        }

        if (value == GENERAL_SORT_ORDER_VALUE) {
            if (version == GENERAL_LEGACY_SORT_ORDER.getVersion()) {
                return GENERAL_LEGACY_SORT_ORDER;
            }
            if (version == GENERAL_SORT_ORDER.getVersion()) {
                return GENERAL_SORT_ORDER;
            }
        }
        return new SortOrder(value, version);
    }

    /**
     * Writes the sort order info to the given buffer at the current position.
     */
    private static void writeSortOrder(ByteBuffer buffer, SortOrder sortOrder, JetFormat format) {
        if (sortOrder == null) {
            sortOrder = format.DEFAULT_SORT_ORDER;
        }
        buffer.putShort(sortOrder.getValue());
        if (format.SIZE_SORT_ORDER == 4) {
            buffer.put((byte) 0x00); // unknown
            buffer.put(sortOrder.getVersion());
        }
    }

    /**
     * Date subclass which stashes the original date bits, in case we attempt to
     * re-write the value (will not lose precision).
     */
    private static final class DateExt extends Date {
        private static final long serialVersionUID = 0L;

        /** cached bits of the original date value */
        private transient final long _dateBits;

        private DateExt(long time, long dateBits) {
            super(time);
            _dateBits = dateBits;
        }

        public long getDateBits() {
            return _dateBits;
        }

        private Object writeReplace() throws ObjectStreamException {
            // if we are going to serialize this Date, convert it back to a normal
            // Date (in case it is restored outside of the context of jackcess)
            return new Date(super.getTime());
        }
    }

    /**
     * Wrapper for raw column data which can be re-written.
     */
    private static class RawData implements Serializable {
        private static final long serialVersionUID = 0L;

        private final byte[] _bytes;

        private RawData(byte[] bytes) {
            _bytes = bytes;
        }

        private byte[] getBytes() {
            return _bytes;
        }

        @Override
        public String toString() {
            return "RawData: " + ByteUtil.toHexString(getBytes());
        }

        private Object writeReplace() throws ObjectStreamException {
            // if we are going to serialize this, convert it back to a normal
            // byte[] (in case it is restored outside of the context of jackcess)
            return getBytes();
        }
    }

    /**
     * Base class for the supported autonumber types.
     * @usage _advanced_class_
     */
    public abstract class AutoNumberGenerator {
        protected AutoNumberGenerator() {
        }

        /**
         * Returns the last autonumber generated by this generator.  Only valid
         * after a call to {@link Table#addRow}, otherwise undefined.
         */
        public abstract Object getLast();

        /**
         * Returns the next autonumber for this generator.
         * <p>
         * <i>Warning, calling this externally will result in this value being
         * "lost" for the table.</i>
         */
        public abstract Object getNext(Object prevRowValue);

        /**
         * Returns the flags used when writing this column.
         */
        public abstract int getColumnFlags();

        /**
         * Returns the type of values generated by this generator.
         */
        public abstract DataType getType();
    }

    private final class LongAutoNumberGenerator extends AutoNumberGenerator {
        private LongAutoNumberGenerator() {
        }

        @Override
        public Object getLast() {
            // the table stores the last long autonumber used
            return getTable().getLastLongAutoNumber();
        }

        @Override
        public Object getNext(Object prevRowValue) {
            // the table stores the last long autonumber used
            return getTable().getNextLongAutoNumber();
        }

        @Override
        public int getColumnFlags() {
            return AUTO_NUMBER_FLAG_MASK;
        }

        @Override
        public DataType getType() {
            return DataType.LONG;
        }
    }

    private final class GuidAutoNumberGenerator extends AutoNumberGenerator {
        private Object _lastAutoNumber;

        private GuidAutoNumberGenerator() {
        }

        @Override
        public Object getLast() {
            return _lastAutoNumber;
        }

        @Override
        public Object getNext(Object prevRowValue) {
            // format guids consistently w/ Column.readGUIDValue()
            _lastAutoNumber = "{" + UUID.randomUUID() + "}";
            return _lastAutoNumber;
        }

        @Override
        public int getColumnFlags() {
            return AUTO_NUMBER_GUID_FLAG_MASK;
        }

        @Override
        public DataType getType() {
            return DataType.GUID;
        }
    }

    private final class ComplexTypeAutoNumberGenerator extends AutoNumberGenerator {
        private ComplexTypeAutoNumberGenerator() {
        }

        @Override
        public Object getLast() {
            // the table stores the last ComplexType autonumber used
            return getTable().getLastComplexTypeAutoNumber();
        }

        @Override
        public Object getNext(Object prevRowValue) {
            int nextComplexAutoNum = ((prevRowValue == null) ?
            // the table stores the last ComplexType autonumber used
                    getTable().getNextComplexTypeAutoNumber() :
                    // same value is shared across all ComplexType values in a row
                    ((ComplexValueForeignKey) prevRowValue).get());
            return new ComplexValueForeignKey(Column.this, nextComplexAutoNum);
        }

        @Override
        public int getColumnFlags() {
            return AUTO_NUMBER_FLAG_MASK;
        }

        @Override
        public DataType getType() {
            return DataType.COMPLEX_TYPE;
        }
    }

    private final class UnsupportedAutoNumberGenerator extends AutoNumberGenerator {
        private final DataType _genType;

        private UnsupportedAutoNumberGenerator(DataType genType) {
            _genType = genType;
        }

        @Override
        public Object getLast() {
            return null;
        }

        @Override
        public Object getNext(Object prevRowValue) {
            throw new UnsupportedOperationException();
        }

        @Override
        public int getColumnFlags() {
            throw new UnsupportedOperationException();
        }

        @Override
        public DataType getType() {
            return _genType;
        }
    }

    /**
     * Information about the sort order (collation) for a textual column.
     * @usage _intermediate_class_
     */
    public static final class SortOrder {
        private final short _value;
        private final byte _version;

        public SortOrder(short value, byte version) {
            _value = value;
            _version = version;
        }

        public short getValue() {
            return _value;
        }

        public byte getVersion() {
            return _version;
        }

        @Override
        public int hashCode() {
            return _value;
        }

        @Override
        public boolean equals(Object o) {
            return ((this == o) || ((o != null) && (getClass() == o.getClass())
                    && (_value == ((SortOrder) o)._value) && (_version == ((SortOrder) o)._version)));
        }

        @Override
        public String toString() {
            return _value + "(" + _version + ")";
        }
    }

    /**
     * Information specific to numeric types.
     */
    private static final class NumericInfo {
        /** Numeric precision */
        private byte _precision;
        /** Numeric scale */
        private byte _scale;
    }

    /**
     * Information specific to textual types.
     */
    private static final class TextInfo {
        /** whether or not they are compressed */
        private boolean _compressedUnicode;
        /** the collating sort order for a text field */
        private SortOrder _sortOrder;
        /** the code page for a text field (for certain db versions) */
        private short _codePage;
        /** complex column which tracks the version history for this "append only"
            column */
        private Column _versionHistoryCol;
    }
}