diff options
author | James Ahlborn <jtahlborn@yahoo.com> | 2013-03-05 00:16:20 +0000 |
---|---|---|
committer | James Ahlborn <jtahlborn@yahoo.com> | 2013-03-05 00:16:20 +0000 |
commit | 71c3508b8ac2b56698bfe247b95eae5a1f390701 (patch) | |
tree | edb853bb93135673213d42f554f216ed21457a8c /src/java | |
parent | 67833826ba430014698a030667561b537b79fe42 (diff) | |
download | jackcess-71c3508b8ac2b56698bfe247b95eae5a1f390701.tar.gz jackcess-71c3508b8ac2b56698bfe247b95eae5a1f390701.zip |
move internals of Column into ColumnImpl
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jackcess-2@671 f203690c-595d-4dc9-a70b-905162fa7fd2
Diffstat (limited to 'src/java')
33 files changed, 2684 insertions, 2575 deletions
diff --git a/src/java/com/healthmarketscience/jackcess/CaseInsensitiveColumnMatcher.java b/src/java/com/healthmarketscience/jackcess/CaseInsensitiveColumnMatcher.java index a88d0d2..cec63f3 100644 --- a/src/java/com/healthmarketscience/jackcess/CaseInsensitiveColumnMatcher.java +++ b/src/java/com/healthmarketscience/jackcess/CaseInsensitiveColumnMatcher.java @@ -49,8 +49,8 @@ public class CaseInsensitiveColumnMatcher implements ColumnMatcher { // convert both values to Strings and compare case-insensitively try { - CharSequence cs1 = Column.toCharSequence(value1); - CharSequence cs2 = Column.toCharSequence(value2); + CharSequence cs1 = ColumnImpl.toCharSequence(value1); + CharSequence cs2 = ColumnImpl.toCharSequence(value2); return((cs1 == cs2) || ((cs1 != null) && (cs2 != null) && diff --git a/src/java/com/healthmarketscience/jackcess/Column.java b/src/java/com/healthmarketscience/jackcess/Column.java index 4a8ac47..76fcbd9 100644 --- a/src/java/com/healthmarketscience/jackcess/Column.java +++ b/src/java/com/healthmarketscience/jackcess/Column.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2005 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,62 +15,23 @@ 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_ + * + * @author James Ahlborn */ -public class Column implements Comparable<Column> { - - private static final Log LOG = LogFactory.getLog(Column.class); - +public abstract class Column +{ /** * Meaningless placeholder object for inserting values in an autonumber * column. it is not required that this value be used (any passed in value @@ -87,2359 +48,101 @@ public class Column implements Comparable<Column> { 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 hyperlink bit (on memo types) - * @usage _advanced_field_ - */ - public static final byte HYPERLINK_FLAG_MASK = (byte)0x80; - - /** - * 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 TableImpl _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, TableImpl 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(TableImpl 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); - - if(_type == DataType.MEMO) { - // only memo fields can be hyperlinks - _textInfo._hyperlink = ((flags & HYPERLINK_FLAG_MASK) != 0); - } - } - - 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(); - } - } + public abstract Table getTable(); /** * @usage _general_method_ */ - public TableImpl getTable() { - return _table; - } + public abstract Database getDatabase(); /** * @usage _general_method_ */ - public DatabaseImpl getDatabase() { - return getTable().getDatabase(); - } - - /** - * @usage _advanced_method_ - */ - public JetFormat getFormat() { - return getDatabase().getFormat(); - } + public abstract String getName(); /** * @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; - } + public abstract boolean isVariableLength(); /** * @usage _general_method_ */ - public void setAutoNumber(boolean autoNumber) { - _autoNumber = autoNumber; - setAutoNumberGenerator(); - } - - /** - * @usage _advanced_method_ - */ - public short getColumnNumber() { - return _columnNumber; - } + public abstract boolean isAutoNumber(); /** * @usage _advanced_method_ */ - public void setColumnNumber(short newColumnNumber) { - _columnNumber = newColumnNumber; - } + public abstract int getColumnIndex(); /** - * @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()); - } - } + public abstract DataType getType(); /** * @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; - } + public abstract int getSQLType() throws SQLException; /** * @usage _general_method_ */ - public void setCompressedUnicode(boolean newCompessedUnicode) { - modifyTextInfo(); - _textInfo._compressedUnicode = newCompessedUnicode; - } + public abstract boolean isCompressedUnicode(); /** * @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; - } + public abstract byte getPrecision(); /** * @usage _general_method_ */ - public short getLength() { - return _columnLength; - } + public abstract byte getScale(); /** * @usage _general_method_ */ - public void setLengthInUnits(short unitLength) { - setLength((short)getType().fromUnitSize(unitLength)); - } + public abstract short getLength(); /** * @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; - } - - Charset getCharset() { - return getDatabase().getCharset(); - } - - Calendar getCalendar() { - return getDatabase().getCalendar(); - } + public abstract short getLengthInUnits(); /** * 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; - } + public abstract boolean isAppendOnly(); /** * Returns whether or not this is a hyperlink column (only possible for * columns of type MEMO). * @usage _general_method_ */ - public boolean isHyperlink() { - return _textInfo._hyperlink; - } + public abstract boolean isHyperlink(); /** - * @usage _general_method_ - */ - public void setHyperlink(boolean hyperlink) { - modifyTextInfo(); - _textInfo._hyperlink = hyperlink; - } - - /** * 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; - } + public abstract ComplexColumnInfo<? extends ComplexValue> getComplexInfo(); /** * @return the properties for this column * @usage _general_method_ */ - public PropertyMap getProperties() throws IOException { - if(_props == null) { - _props = getTable().getPropertyMaps().get(getName()); - } - return _props; - } + public abstract PropertyMap getProperties() throws IOException; - private void modifyNumericInfo() { - if(_numericInfo == DEFAULT_NUMERIC_INFO) { - _numericInfo = new NumericInfo(); - } - } - - private void modifyTextInfo() { - if(_textInfo == DEFAULT_TEXT_INFO) { - _textInfo = new TextInfo(); - } - } + public abstract Object setRowValue(Object[] rowArray, Object value); - /** - * 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"); - } - DatabaseImpl.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)"); - } - } - - if(isHyperlink()) { - if(getType() != DataType.MEMO) { - throw new IllegalArgumentException( - "Only memo columns can be hyperlinks"); - } - } - } - - 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); - } + public abstract Object setRowValue(Map<String,Object> rowMap, Object value); - /** - * 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 = TableImpl.findRowStart(lvalPage, rowNum, getFormat()); - short rowEnd = TableImpl.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 = TableImpl.findRowStart(lvalPage, rowNum, getFormat()); - short rowEnd = TableImpl.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; - } + public abstract Object getRowValue(Object[] rowArray); - /** - * @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 = ByteUtil.getBytes(buffer, 16); - - 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. - */ - long fromDateDouble(double value) - { - long time = Math.round(value * MILLISECONDS_PER_DAY); - time -= MILLIS_BETWEEN_EPOCH_AND_1900; - time -= getFromLocalTimeZoneOffset(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. - */ - 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 += getToLocalTimeZoneOffset(time); - time += MILLIS_BETWEEN_EPOCH_AND_1900; - return time / MILLISECONDS_PER_DAY; - } - - /** - * Gets the timezone offset from UTC to local time for the given time - * (including DST). - */ - private long getToLocalTimeZoneOffset(long time) - { - Calendar c = getCalendar(); - c.setTimeInMillis(time); - return ((long)c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET)); - } - - /** - * Gets the timezone offset from local time to UTC for the given time - * (including DST). - */ - private long getFromLocalTimeZoneOffset(long time) - { - // getting from local time back to UTC is a little wonky (and not - // guaranteed to get you back to where you started) - Calendar c = getCalendar(); - c.setTimeInMillis(time); - // apply the zone offset first to get us closer to the original time - c.setTimeInMillis(time - c.get(Calendar.ZONE_OFFSET)); - 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 = ByteUtil.getBytes(buffer, 16); - - // 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)TableImpl.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)TableImpl.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(TableImpl.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 = UNKNOWN_FLAG_MASK; - if(!isVariableLength()) { - flags |= FIXED_LEN_FLAG_MASK; - } - if(isAutoNumber()) { - flags |= getAutoNumberGenerator().getColumnFlags(); - } - if(isHyperlink()) { - flags |= HYPERLINK_FLAG_MASK; - } - 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(isHyperlink()) { - rtn.append("\n\tHyperlink: " + isHyperlink()); - } - } - 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 = 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(TableImpl.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) { - TableImpl.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; - /** whether or not this is a hyperlink column (only possible for columns - of type MEMO) */ - private boolean _hyperlink; - } + public abstract Object getRowValue(Map<String,?> rowMap); } diff --git a/src/java/com/healthmarketscience/jackcess/ColumnBuilder.java b/src/java/com/healthmarketscience/jackcess/ColumnBuilder.java index c09ec97..23154a1 100644 --- a/src/java/com/healthmarketscience/jackcess/ColumnBuilder.java +++ b/src/java/com/healthmarketscience/jackcess/ColumnBuilder.java @@ -142,7 +142,7 @@ public class ColumnBuilder { /** * Sets all attributes except name from the given Column template. */ - public ColumnBuilder setFromColumn(Column template) { + public ColumnBuilder setFromColumn(ColumnImpl template) { DataType type = template.getType(); setType(type); setLength(template.getLength()); @@ -168,8 +168,8 @@ public class ColumnBuilder { /** * Creates a new Column with the currently configured attributes. */ - public Column toColumn() { - Column col = new Column(); + public ColumnImpl toColumn() { + ColumnImpl col = new ColumnImpl(); col.setName(_name); col.setType(_type); if(_length != null) { diff --git a/src/java/com/healthmarketscience/jackcess/ColumnImpl.java b/src/java/com/healthmarketscience/jackcess/ColumnImpl.java new file mode 100644 index 0000000..cf824bb --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/ColumnImpl.java @@ -0,0 +1,2393 @@ +/* +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 ColumnImpl extends Column implements Comparable<ColumnImpl> { + + private static final Log LOG = LogFactory.getLog(ColumnImpl.class); + + /** + * 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 hyperlink bit (on memo types) + * @usage _advanced_field_ + */ + public static final byte HYPERLINK_FLAG_MASK = (byte)0x80; + + /** + * 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 TableImpl _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 ColumnImpl() { + this(null); + } + + /** + * @usage _advanced_method_ + */ + public ColumnImpl(JetFormat format) { + _table = null; + } + + /** + * Only used by unit tests + */ + ColumnImpl(boolean testing, TableImpl 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 ColumnImpl(TableImpl 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); + + if(_type == DataType.MEMO) { + // only memo fields can be hyperlinks + _textInfo._hyperlink = ((flags & HYPERLINK_FLAG_MASK) != 0); + } + } + + 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(); + } + } + + @Override + public TableImpl getTable() { + return _table; + } + + @Override + public DatabaseImpl getDatabase() { + return getTable().getDatabase(); + } + + /** + * @usage _advanced_method_ + */ + public JetFormat getFormat() { + return getDatabase().getFormat(); + } + + /** + * @usage _advanced_method_ + */ + public PageChannel getPageChannel() { + return getDatabase().getPageChannel(); + } + + @Override + public String getName() { + return _name; + } + + /** + * @usage _advanced_method_ + */ + public void setName(String name) { + _name = name; + } + + @Override + public boolean isVariableLength() { + return _variableLength; + } + + /** + * @usage _advanced_method_ + */ + public void setVariableLength(boolean variableLength) { + _variableLength = variableLength; + } + + @Override + 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; + } + + @Override + 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()); + } + } + + @Override + public DataType getType() { + return _type; + } + + @Override + 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)); + } + + @Override + public boolean isCompressedUnicode() { + return _textInfo._compressedUnicode; + } + + /** + * @usage _general_method_ + */ + public void setCompressedUnicode(boolean newCompessedUnicode) { + modifyTextInfo(); + _textInfo._compressedUnicode = newCompessedUnicode; + } + + @Override + public byte getPrecision() { + return _numericInfo._precision; + } + + /** + * @usage _general_method_ + */ + public void setPrecision(byte newPrecision) { + modifyNumericInfo(); + _numericInfo._precision = newPrecision; + } + + @Override + 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; + } + + @Override + public short getLength() { + return _columnLength; + } + + /** + * @usage _general_method_ + */ + public void setLengthInUnits(short unitLength) { + setLength((short)getType().fromUnitSize(unitLength)); + } + + @Override + 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; + } + + Charset getCharset() { + return getDatabase().getCharset(); + } + + Calendar getCalendar() { + return getDatabase().getCalendar(); + } + + @Override + public boolean isAppendOnly() { + return (getVersionHistoryColumn() != null); + } + + /** + * Returns the column which tracks the version history for an "append only" + * column. + * @usage _intermediate_method_ + */ + public ColumnImpl getVersionHistoryColumn() { + return _textInfo._versionHistoryCol; + } + + /** + * @usage _advanced_method_ + */ + public void setVersionHistoryColumn(ColumnImpl versionHistoryCol) { + modifyTextInfo(); + _textInfo._versionHistoryCol = versionHistoryCol; + } + + @Override + public boolean isHyperlink() { + return _textInfo._hyperlink; + } + + /** + * @usage _general_method_ + */ + public void setHyperlink(boolean hyperlink) { + modifyTextInfo(); + _textInfo._hyperlink = hyperlink; + } + + @Override + 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; + } + + @Override + 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"); + } + DatabaseImpl.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)"); + } + } + + if(isHyperlink()) { + if(getType() != DataType.MEMO) { + throw new IllegalArgumentException( + "Only memo columns can be hyperlinks"); + } + } + } + + @Override + public Object setRowValue(Object[] rowArray, Object value) { + rowArray[_columnIndex] = value; + return value; + } + + @Override + public Object setRowValue(Map<String,Object> rowMap, Object value) { + rowMap.put(_name, value); + return value; + } + + @Override + public Object getRowValue(Object[] rowArray) { + return rowArray[_columnIndex]; + } + + @Override + 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 = TableImpl.findRowStart(lvalPage, rowNum, getFormat()); + short rowEnd = TableImpl.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 = TableImpl.findRowStart(lvalPage, rowNum, getFormat()); + short rowEnd = TableImpl.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 = ByteUtil.getBytes(buffer, 16); + + 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. + */ + long fromDateDouble(double value) + { + long time = Math.round(value * MILLISECONDS_PER_DAY); + time -= MILLIS_BETWEEN_EPOCH_AND_1900; + time -= getFromLocalTimeZoneOffset(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. + */ + 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 += getToLocalTimeZoneOffset(time); + time += MILLIS_BETWEEN_EPOCH_AND_1900; + return time / MILLISECONDS_PER_DAY; + } + + /** + * Gets the timezone offset from UTC to local time for the given time + * (including DST). + */ + private long getToLocalTimeZoneOffset(long time) + { + Calendar c = getCalendar(); + c.setTimeInMillis(time); + return ((long)c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET)); + } + + /** + * Gets the timezone offset from local time to UTC for the given time + * (including DST). + */ + private long getFromLocalTimeZoneOffset(long time) + { + // getting from local time back to UTC is a little wonky (and not + // guaranteed to get you back to where you started) + Calendar c = getCalendar(); + c.setTimeInMillis(time); + // apply the zone offset first to get us closer to the original time + c.setTimeInMillis(time - c.get(Calendar.ZONE_OFFSET)); + 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 = ByteUtil.getBytes(buffer, 16); + + // 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)TableImpl.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)TableImpl.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(TableImpl.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 = UNKNOWN_FLAG_MASK; + if(!isVariableLength()) { + flags |= FIXED_LEN_FLAG_MASK; + } + if(isAutoNumber()) { + flags |= getAutoNumberGenerator().getColumnFlags(); + } + if(isHyperlink()) { + flags |= HYPERLINK_FLAG_MASK; + } + 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(isHyperlink()) { + rtn.append("\n\tHyperlink: " + isHyperlink()); + } + } + 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(ColumnImpl 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<ColumnImpl> columns) { + short rtn = 0; + for (ColumnImpl 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<ColumnImpl> columns) { + short rtn = 0; + for (ColumnImpl 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<ColumnImpl> 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 = countNonLongVariableLength(columns); + for (ColumnImpl col : columns) { + // record this for later use when writing indexes + col.setColumnNumber(columnNumber); + + int position = buffer.position(); + buffer.put(col.getType().getValue()); + buffer.putInt(TableImpl.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 (ColumnImpl col : columns) { + TableImpl.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(ColumnImpl.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 ColumnImpl _versionHistoryCol; + /** whether or not this is a hyperlink column (only possible for columns + of type MEMO) */ + private boolean _hyperlink; + } +} diff --git a/src/java/com/healthmarketscience/jackcess/Cursor.java b/src/java/com/healthmarketscience/jackcess/Cursor.java index 2b6a9d2..fa586fd 100644 --- a/src/java/com/healthmarketscience/jackcess/Cursor.java +++ b/src/java/com/healthmarketscience/jackcess/Cursor.java @@ -216,8 +216,8 @@ public abstract class Cursor implements Iterable<Map<String, Object>> * desired row * @return the matching row or {@code null} if a match could not be found. */ - public static Object findValue(TableImpl table, Column column, - Column columnPattern, Object valuePattern) + public static Object findValue(TableImpl table, ColumnImpl column, + ColumnImpl columnPattern, Object valuePattern) throws IOException { Cursor cursor = createCursor(table); @@ -269,8 +269,8 @@ public abstract class Cursor implements Iterable<Map<String, Object>> * desired row * @return the matching row or {@code null} if a match could not be found. */ - public static Object findValue(TableImpl table, IndexImpl index, Column column, - Column columnPattern, Object valuePattern) + public static Object findValue(TableImpl table, IndexImpl index, ColumnImpl column, + ColumnImpl columnPattern, Object valuePattern) throws IOException { Cursor cursor = createIndexCursor(table, index); @@ -531,7 +531,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>> * operations, the actual exception will be contained within */ public Iterable<Map<String, Object>> columnMatchIterable( - Column columnPattern, Object valuePattern) + ColumnImpl columnPattern, Object valuePattern) { return columnMatchIterable(null, columnPattern, valuePattern); } @@ -546,7 +546,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>> * operations, the actual exception will be contained within */ public Iterator<Map<String, Object>> columnMatchIterator( - Column columnPattern, Object valuePattern) + ColumnImpl columnPattern, Object valuePattern) { return columnMatchIterator(null, columnPattern, valuePattern); } @@ -559,7 +559,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>> */ public Iterable<Map<String, Object>> columnMatchIterable( final Collection<String> columnNames, - final Column columnPattern, final Object valuePattern) + final ColumnImpl columnPattern, final Object valuePattern) { return new Iterable<Map<String, Object>>() { public Iterator<Map<String, Object>> iterator() { @@ -580,7 +580,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>> * operations, the actual exception will be contained within */ public Iterator<Map<String, Object>> columnMatchIterator( - Collection<String> columnNames, Column columnPattern, Object valuePattern) + Collection<String> columnNames, ColumnImpl columnPattern, Object valuePattern) { return new ColumnMatchIterator(columnNames, columnPattern, valuePattern); } @@ -834,7 +834,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>> * @deprecated renamed to {@link #findFirstRow(Column,Object)} to be more clear */ @Deprecated - public boolean findRow(Column columnPattern, Object valuePattern) + public boolean findRow(ColumnImpl columnPattern, Object valuePattern) throws IOException { return findFirstRow(columnPattern, valuePattern); @@ -856,7 +856,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>> * @return {@code true} if a valid row was found with the given value, * {@code false} if no row was found */ - public boolean findFirstRow(Column columnPattern, Object valuePattern) + public boolean findFirstRow(ColumnImpl columnPattern, Object valuePattern) throws IOException { Position curPos = _curPos; @@ -890,7 +890,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>> * @return {@code true} if a valid row was found with the given value, * {@code false} if no row was found */ - public boolean findNextRow(Column columnPattern, Object valuePattern) + public boolean findNextRow(ColumnImpl columnPattern, Object valuePattern) throws IOException { Position curPos = _curPos; @@ -993,7 +993,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>> * @param valuePattern value which is tested for equality with the * corresponding value in the current row */ - public boolean currentRowMatches(Column columnPattern, Object valuePattern) + public boolean currentRowMatches(ColumnImpl columnPattern, Object valuePattern) throws IOException { return _columnMatcher.matches(getTable(), columnPattern.getName(), @@ -1039,7 +1039,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>> * @return {@code true} if a valid row was found with the given value, * {@code false} if no row was found */ - protected boolean findNextRowImpl(Column columnPattern, Object valuePattern) + protected boolean findNextRowImpl(ColumnImpl columnPattern, Object valuePattern) throws IOException { while(moveToNextRow()) { @@ -1129,7 +1129,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>> /** * Returns the given column from the current row. */ - public Object getCurrentRowValue(Column column) + public Object getCurrentRowValue(ColumnImpl column) throws IOException { return _table.getRowValue(_rowState, _curPos.getRowId(), column); @@ -1140,7 +1140,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>> * @throws IllegalStateException if the current row is not valid (at * beginning or end of table), or deleted. */ - public void setCurrentRowValue(Column column, Object value) + public void setCurrentRowValue(ColumnImpl column, Object value) throws IOException { Object[] row = new Object[_table.getColumnCount()]; @@ -1264,11 +1264,11 @@ public abstract class Cursor implements Iterable<Map<String, Object>> */ private final class ColumnMatchIterator extends BaseIterator { - private final Column _columnPattern; + private final ColumnImpl _columnPattern; private final Object _valuePattern; private ColumnMatchIterator(Collection<String> columnNames, - Column columnPattern, Object valuePattern) + ColumnImpl columnPattern, Object valuePattern) { super(columnNames); _columnPattern = columnPattern; diff --git a/src/java/com/healthmarketscience/jackcess/CursorBuilder.java b/src/java/com/healthmarketscience/jackcess/CursorBuilder.java index df9b3c3..3a56659 100644 --- a/src/java/com/healthmarketscience/jackcess/CursorBuilder.java +++ b/src/java/com/healthmarketscience/jackcess/CursorBuilder.java @@ -126,9 +126,9 @@ public class CursorBuilder { * @throws IllegalArgumentException if no index can be found on the table * with the given columns */ - public CursorBuilder setIndexByColumns(Column... columns) { + public CursorBuilder setIndexByColumns(ColumnImpl... columns) { List<String> colNames = new ArrayList<String>(); - for(Column col : columns) { + for(ColumnImpl col : columns) { colNames.add(col.getName()); } return setIndexByColumns(colNames); diff --git a/src/java/com/healthmarketscience/jackcess/DatabaseImpl.java b/src/java/com/healthmarketscience/jackcess/DatabaseImpl.java index cbb5c7f..2f257e5 100644 --- a/src/java/com/healthmarketscience/jackcess/DatabaseImpl.java +++ b/src/java/com/healthmarketscience/jackcess/DatabaseImpl.java @@ -322,7 +322,7 @@ public class DatabaseImpl extends Database /** timezone to use when handling dates */ private TimeZone _timeZone; /** language sort order to be used for textual columns */ - private Column.SortOrder _defaultSortOrder; + private ColumnImpl.SortOrder _defaultSortOrder; /** default code page to be used for textual columns (in some dbs) */ private Short _defaultCodePage; /** the ordering used for table columns */ @@ -773,7 +773,7 @@ public class DatabaseImpl extends Database * textual columns * @usage _intermediate_method_ */ - public Column.SortOrder getDefaultSortOrder() throws IOException { + public ColumnImpl.SortOrder getDefaultSortOrder() throws IOException { if(_defaultSortOrder == null) { initRootPageInfo(); @@ -801,7 +801,7 @@ public class DatabaseImpl extends Database ByteBuffer buffer = takeSharedBuffer(); try { _pageChannel.readPage(buffer, 0); - _defaultSortOrder = Column.readSortOrder( + _defaultSortOrder = ColumnImpl.readSortOrder( buffer, _format.OFFSET_SORT_ORDER, _format); _defaultCodePage = buffer.getShort(_format.OFFSET_CODE_PAGE); } finally { @@ -956,7 +956,7 @@ public class DatabaseImpl extends Database * @param columns List of Columns in the table * @usage _general_method_ */ - public void createTable(String name, List<Column> columns) + public void createTable(String name, List<ColumnImpl> columns) throws IOException { createTable(name, columns, null); @@ -969,7 +969,7 @@ public class DatabaseImpl extends Database * @param indexes List of IndexBuilders describing indexes for the table * @usage _general_method_ */ - public void createTable(String name, List<Column> columns, + public void createTable(String name, List<ColumnImpl> columns, List<IndexBuilder> indexes) throws IOException { @@ -1030,6 +1030,13 @@ public class DatabaseImpl extends Database public List<Relationship> getRelationships(Table table1, Table table2) throws IOException { + return getRelationships((TableImpl)table1, (TableImpl)table2); + } + + public List<Relationship> getRelationships( + TableImpl table1, TableImpl table2) + throws IOException + { // the relationships table does not get loaded until first accessed if(_relationships == null) { _relationships = getSystemTable(TABLE_SYSTEM_RELATIONSHIPS); @@ -1046,7 +1053,7 @@ public class DatabaseImpl extends Database // we "order" the two tables given so that we will return a collection // of relationships in the same order regardless of whether we are given // (TableFoo, TableBar) or (TableBar, TableFoo). - Table tmp = table1; + TableImpl tmp = table1; table1 = table2; table2 = tmp; } @@ -1219,7 +1226,7 @@ public class DatabaseImpl extends Database return null; } - String pwd = Column.decodeUncompressedText(pwdBytes, getCharset()); + String pwd = ColumnImpl.decodeUncompressedText(pwdBytes, getCharset()); // remove any trailing null chars int idx = pwd.indexOf('\0'); @@ -1238,7 +1245,7 @@ public class DatabaseImpl extends Database * given cursor and adds them to the given list. */ private static void collectRelationships( - Cursor cursor, Table fromTable, Table toTable, + Cursor cursor, TableImpl fromTable, TableImpl toTable, List<Relationship> relationships) { for(Map<String,Object> row : cursor) { @@ -1272,9 +1279,9 @@ public class DatabaseImpl extends Database // add column info int colIdx = (Integer)row.get(REL_COL_COLUMN_INDEX); - Column fromCol = fromTable.getColumn( + ColumnImpl fromCol = fromTable.getColumn( (String)row.get(REL_COL_FROM_COLUMN)); - Column toCol = toTable.getColumn( + ColumnImpl toCol = toTable.getColumn( (String)row.get(REL_COL_TO_COLUMN)); rel.getFromColumns().set(colIdx, fromCol); @@ -1295,10 +1302,10 @@ public class DatabaseImpl extends Database Object[] catalogRow = new Object[_systemCatalog.getColumnCount()]; int idx = 0; Date creationTime = new Date(); - for (Iterator<Column> iter = _systemCatalog.getColumns().iterator(); + for (Iterator<ColumnImpl> iter = _systemCatalog.getColumns().iterator(); iter.hasNext(); idx++) { - Column col = iter.next(); + ColumnImpl col = iter.next(); if (CAT_COL_ID.equals(col.getName())) { catalogRow[idx] = Integer.valueOf(pageNumber); } else if (CAT_COL_NAME.equals(col.getName())) { @@ -1336,11 +1343,11 @@ public class DatabaseImpl extends Database initNewTableSIDs(); } - Table acEntries = getAccessControlEntries(); - Column acmCol = acEntries.getColumn(ACE_COL_ACM); - Column inheritCol = acEntries.getColumn(ACE_COL_F_INHERITABLE); - Column objIdCol = acEntries.getColumn(ACE_COL_OBJECT_ID); - Column sidCol = acEntries.getColumn(ACE_COL_SID); + TableImpl acEntries = getAccessControlEntries(); + ColumnImpl acmCol = acEntries.getColumn(ACE_COL_ACM); + ColumnImpl inheritCol = acEntries.getColumn(ACE_COL_F_INHERITABLE); + ColumnImpl objIdCol = acEntries.getColumn(ACE_COL_OBJECT_ID); + ColumnImpl sidCol = acEntries.getColumn(ACE_COL_SID); // construct a collection of ACE entries mimicing those of our parent, the // "Tables" system object @@ -1804,7 +1811,7 @@ public class DatabaseImpl extends Database if(cur == null) { return null; } - Column idCol = _systemCatalog.getColumn(CAT_COL_ID); + ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID); return (Integer)cur.getCurrentRowValue(idCol); } @@ -1948,7 +1955,7 @@ public class DatabaseImpl extends Database if(!_systemCatalogIdCursor.moveToPreviousRow()) { return Integer.MIN_VALUE; } - Column idCol = _systemCatalog.getColumn(CAT_COL_ID); + ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID); return (Integer)_systemCatalogIdCursor.getCurrentRowValue(idCol); } } @@ -1978,7 +1985,7 @@ public class DatabaseImpl extends Database @Override protected Cursor findRow(Integer objectId) throws IOException { - Column idCol = _systemCatalog.getColumn(CAT_COL_ID); + ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID); return (_systemCatalogCursor.findFirstRow(idCol, objectId) ? _systemCatalogCursor : null); } @@ -2024,7 +2031,7 @@ public class DatabaseImpl extends Database @Override protected int findMaxSyntheticId() throws IOException { // find max id < 0 - Column idCol = _systemCatalog.getColumn(CAT_COL_ID); + ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID); _systemCatalogCursor.reset(); int curMaxSynthId = Integer.MIN_VALUE; while(_systemCatalogCursor.moveToNextRow()) { diff --git a/src/java/com/healthmarketscience/jackcess/ExportFilter.java b/src/java/com/healthmarketscience/jackcess/ExportFilter.java index f145fd5..19faed1 100644 --- a/src/java/com/healthmarketscience/jackcess/ExportFilter.java +++ b/src/java/com/healthmarketscience/jackcess/ExportFilter.java @@ -46,7 +46,7 @@ public interface ExportFilter { * modified and returned * @return the columns to use when creating the export file */ - public List<Column> filterColumns(List<Column> columns) throws IOException; + public List<ColumnImpl> filterColumns(List<ColumnImpl> columns) throws IOException; /** * The desired values for the row. diff --git a/src/java/com/healthmarketscience/jackcess/ExportUtil.java b/src/java/com/healthmarketscience/jackcess/ExportUtil.java index 7ee1c35..1cb7eb7 100644 --- a/src/java/com/healthmarketscience/jackcess/ExportUtil.java +++ b/src/java/com/healthmarketscience/jackcess/ExportUtil.java @@ -303,8 +303,8 @@ public class ExportUtil { "(?:" + Pattern.quote(delimiter) + ")|(?:" + Pattern.quote("" + quote) + ")|(?:[\n\r])"); - List<Column> origCols = cursor.getTable().getColumns(); - List<Column> columns = new ArrayList<Column>(origCols); + List<ColumnImpl> origCols = cursor.getTable().getColumns(); + List<ColumnImpl> columns = new ArrayList<ColumnImpl>(origCols); columns = filter.filterColumns(columns); Collection<String> columnNames = null; @@ -312,14 +312,14 @@ public class ExportUtil { // columns have been filtered columnNames = new HashSet<String>(); - for (Column c : columns) { + for (ColumnImpl c : columns) { columnNames.add(c.getName()); } } // print the header row (if desired) if (header) { - for (Iterator<Column> iter = columns.iterator(); iter.hasNext();) { + for (Iterator<ColumnImpl> iter = columns.iterator(); iter.hasNext();) { writeValue(out, iter.next().getName(), quote, needsQuotePattern); diff --git a/src/java/com/healthmarketscience/jackcess/FKEnforcer.java b/src/java/com/healthmarketscience/jackcess/FKEnforcer.java index 9e148a6..b05a0f4 100644 --- a/src/java/com/healthmarketscience/jackcess/FKEnforcer.java +++ b/src/java/com/healthmarketscience/jackcess/FKEnforcer.java @@ -45,7 +45,7 @@ final class FKEnforcer CaseInsensitiveColumnMatcher.INSTANCE; private final TableImpl _table; - private final List<Column> _cols; + private final List<ColumnImpl> _cols; private List<Joiner> _primaryJoinersChkUp; private List<Joiner> _primaryJoinersChkDel; private List<Joiner> _primaryJoinersDoUp; @@ -56,7 +56,7 @@ final class FKEnforcer _table = table; // at this point, only init the index columns - Set<Column> cols = new TreeSet<Column>(); + Set<ColumnImpl> cols = new TreeSet<ColumnImpl>(); for(IndexImpl idx : _table.getIndexes()) { IndexImpl.ForeignKeyReference ref = idx.getReference(); if(ref != null) { @@ -68,8 +68,8 @@ final class FKEnforcer } } _cols = !cols.isEmpty() ? - Collections.unmodifiableList(new ArrayList<Column>(cols)) : - Collections.<Column>emptyList(); + Collections.unmodifiableList(new ArrayList<ColumnImpl>(cols)) : + Collections.<ColumnImpl>emptyList(); } /** @@ -257,7 +257,7 @@ final class FKEnforcer } private boolean anyUpdates(Object[] oldRow, Object[] newRow) { - for(Column col : _cols) { + for(ColumnImpl col : _cols) { if(!MATCHER.matches(_table, col.getName(), col.getRowValue(oldRow), col.getRowValue(newRow))) { return true; @@ -271,7 +271,7 @@ final class FKEnforcer { Table fromTable = joiner.getFromTable(); for(IndexData.ColumnDescriptor iCol : joiner.getColumns()) { - Column col = iCol.getColumn(); + ColumnImpl col = iCol.getColumn(); if(!MATCHER.matches(fromTable, col.getName(), col.getRowValue(oldRow), col.getRowValue(newRow))) { return true; diff --git a/src/java/com/healthmarketscience/jackcess/GeneralLegacyIndexCodes.java b/src/java/com/healthmarketscience/jackcess/GeneralLegacyIndexCodes.java index 6ddd62d..c5db96e 100644 --- a/src/java/com/healthmarketscience/jackcess/GeneralLegacyIndexCodes.java +++ b/src/java/com/healthmarketscience/jackcess/GeneralLegacyIndexCodes.java @@ -490,7 +490,7 @@ public class GeneralLegacyIndexCodes { throws IOException { // first, convert to string - String str = Column.toCharSequence(value).toString(); + String str = ColumnImpl.toCharSequence(value).toString(); // all text columns (including memos) are only indexed up to the max // number of chars in a VARCHAR column diff --git a/src/java/com/healthmarketscience/jackcess/ImportFilter.java b/src/java/com/healthmarketscience/jackcess/ImportFilter.java index 144b481..ca12b42 100644 --- a/src/java/com/healthmarketscience/jackcess/ImportFilter.java +++ b/src/java/com/healthmarketscience/jackcess/ImportFilter.java @@ -48,7 +48,7 @@ public interface ImportFilter { * JDBC source * @return the columns to use when creating the import table */ - public List<Column> filterColumns(List<Column> destColumns, + public List<ColumnImpl> filterColumns(List<ColumnImpl> destColumns, ResultSetMetaData srcColumns) throws SQLException, IOException; diff --git a/src/java/com/healthmarketscience/jackcess/ImportUtil.java b/src/java/com/healthmarketscience/jackcess/ImportUtil.java index b3ccb94..da2b502 100644 --- a/src/java/com/healthmarketscience/jackcess/ImportUtil.java +++ b/src/java/com/healthmarketscience/jackcess/ImportUtil.java @@ -68,12 +68,12 @@ public class ImportUtil * * @return a List of Columns */ - public static List<Column> toColumns(ResultSetMetaData md) + public static List<ColumnImpl> toColumns(ResultSetMetaData md) throws SQLException { - List<Column> columns = new LinkedList<Column>(); + List<ColumnImpl> columns = new LinkedList<ColumnImpl>(); for (int i = 1; i <= md.getColumnCount(); i++) { - Column column = new Column(); + ColumnImpl column = new ColumnImpl(); column.setName(DatabaseImpl.escapeIdentifier(md.getColumnName(i))); int lengthInUnits = md.getColumnDisplaySize(i); column.setSQLType(md.getColumnType(i), lengthInUnits); @@ -167,7 +167,7 @@ public class ImportUtil name = DatabaseImpl.escapeIdentifier(name); Table table = null; if(!useExistingTable || ((table = db.getTable(name)) == null)) { - List<Column> columns = toColumns(md); + List<ColumnImpl> columns = toColumns(md); table = createUniqueTable(db, name, columns, md, filter); } @@ -457,7 +457,7 @@ public class ImportUtil Table table = null; if(!useExistingTable || ((table = db.getTable(name)) == null)) { - List<Column> columns = new LinkedList<Column>(); + List<ColumnImpl> columns = new LinkedList<ColumnImpl>(); Object[] columnNames = splitLine(line, delimPat, quote, in, 0); for (int i = 0; i < columnNames.length; i++) { @@ -591,7 +591,7 @@ public class ImportUtil * Returns a new table with a unique name and the given table definition. */ private static Table createUniqueTable(DatabaseImpl db, String name, - List<Column> columns, + List<ColumnImpl> columns, ResultSetMetaData md, ImportFilter filter) throws IOException, SQLException diff --git a/src/java/com/healthmarketscience/jackcess/Index.java b/src/java/com/healthmarketscience/jackcess/Index.java index d27e7e1..cffd63b 100644 --- a/src/java/com/healthmarketscience/jackcess/Index.java +++ b/src/java/com/healthmarketscience/jackcess/Index.java @@ -40,7 +40,7 @@ public abstract class Index /** * @return the Columns for this index (unmodifiable) */ - public abstract List<? extends ColumnInfo> getColumns(); + public abstract List<? extends Index.Column> getColumns(); /** * @return the Index referenced by this Index's ForeignKeyReference (if it @@ -49,6 +49,11 @@ public abstract class Index public abstract Index getReferencedIndex() throws IOException; /** + * Whether or not {@code null} values are actually recorded in the index. + */ + public abstract boolean shouldIgnoreNulls(); + + /** * Whether or not index entries must be unique. * <p> * Some notes about uniqueness: @@ -65,9 +70,9 @@ public abstract class Index /** * Information about a Column in an Index */ - public interface ColumnInfo { + public interface Column { - public Column getColumn(); + public com.healthmarketscience.jackcess.Column getColumn(); public boolean isAscending(); diff --git a/src/java/com/healthmarketscience/jackcess/IndexCursor.java b/src/java/com/healthmarketscience/jackcess/IndexCursor.java index b0b41c0..acd129f 100644 --- a/src/java/com/healthmarketscience/jackcess/IndexCursor.java +++ b/src/java/com/healthmarketscience/jackcess/IndexCursor.java @@ -319,7 +319,7 @@ public class IndexCursor extends Cursor } @Override - protected boolean findNextRowImpl(Column columnPattern, Object valuePattern) + protected boolean findNextRowImpl(ColumnImpl columnPattern, Object valuePattern) throws IOException { if(!isBeforeFirst()) { diff --git a/src/java/com/healthmarketscience/jackcess/IndexData.java b/src/java/com/healthmarketscience/jackcess/IndexData.java index d6b411f..965e7d8 100644 --- a/src/java/com/healthmarketscience/jackcess/IndexData.java +++ b/src/java/com/healthmarketscience/jackcess/IndexData.java @@ -402,7 +402,7 @@ public class IndexData { * @param tableBuffer table definition buffer to read from initial info * @param availableColumns Columns that this index may use */ - public void read(ByteBuffer tableBuffer, List<Column> availableColumns) + public void read(ByteBuffer tableBuffer, List<ColumnImpl> availableColumns) throws IOException { ByteUtil.forward(tableBuffer, getFormat().SKIP_BEFORE_INDEX); //Forward past Unknown @@ -413,8 +413,8 @@ public class IndexData { if (columnNumber != COLUMN_UNUSED) { // find the desired column by column number (which is not necessarily // the same as the column index) - Column idxCol = null; - for(Column col : availableColumns) { + ColumnImpl idxCol = null; + for(ColumnImpl col : availableColumns) { if(col.getColumnNumber() == columnNumber) { idxCol = col; break; @@ -483,7 +483,7 @@ public class IndexData { flags = idxCol.getFlags(); // find actual table column number - for(Column col : creator.getColumns()) { + for(ColumnImpl col : creator.getColumns()) { if(col.getName().equalsIgnoreCase(idxCol.getName())) { columnNumber = col.getColumnNumber(); break; @@ -1099,7 +1099,7 @@ public class IndexData { for(ColumnDescriptor col : _columns) { Object value = values[col.getColumnIndex()]; - if(Column.isRawData(value)) { + if(ColumnImpl.isRawData(value)) { // ignore it, we could not parse it continue; } @@ -1169,7 +1169,7 @@ public class IndexData { /** * Writes the value of the given column type to a byte array and returns it. */ - private static byte[] encodeNumberColumnValue(Object value, Column column) + private static byte[] encodeNumberColumnValue(Object value, ColumnImpl column) throws IOException { // always write in big endian order @@ -1186,17 +1186,17 @@ public class IndexData { /** * Constructs a ColumnDescriptor of the relevant type for the given Column. */ - private ColumnDescriptor newColumnDescriptor(Column col, byte flags) + private ColumnDescriptor newColumnDescriptor(ColumnImpl col, byte flags) throws IOException { switch(col.getType()) { case TEXT: case MEMO: - Column.SortOrder sortOrder = col.getTextSortOrder(); - if(Column.GENERAL_LEGACY_SORT_ORDER.equals(sortOrder)) { + ColumnImpl.SortOrder sortOrder = col.getTextSortOrder(); + if(ColumnImpl.GENERAL_LEGACY_SORT_ORDER.equals(sortOrder)) { return new GenLegTextColumnDescriptor(col, flags); } - if(Column.GENERAL_SORT_ORDER.equals(sortOrder)) { + if(ColumnImpl.GENERAL_SORT_ORDER.equals(sortOrder)) { return new GenTextColumnDescriptor(col, flags); } // unsupported sort order @@ -1270,19 +1270,19 @@ public class IndexData { * Information about the columns in an index. Also encodes new index * values. */ - public static abstract class ColumnDescriptor implements Index.ColumnInfo + public static abstract class ColumnDescriptor implements Index.Column { - private final Column _column; + private final ColumnImpl _column; private final byte _flags; - private ColumnDescriptor(Column column, byte flags) + private ColumnDescriptor(ColumnImpl column, byte flags) throws IOException { _column = column; _flags = flags; } - public Column getColumn() { + public ColumnImpl getColumn() { return _column; } @@ -1336,7 +1336,7 @@ public class IndexData { */ private static final class IntegerColumnDescriptor extends ColumnDescriptor { - private IntegerColumnDescriptor(Column column, byte flags) + private IntegerColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); @@ -1368,7 +1368,7 @@ public class IndexData { private static final class FloatingPointColumnDescriptor extends ColumnDescriptor { - private FloatingPointColumnDescriptor(Column column, byte flags) + private FloatingPointColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); @@ -1408,7 +1408,7 @@ public class IndexData { private static class LegacyFixedPointColumnDescriptor extends ColumnDescriptor { - private LegacyFixedPointColumnDescriptor(Column column, byte flags) + private LegacyFixedPointColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); @@ -1459,7 +1459,7 @@ public class IndexData { private static final class FixedPointColumnDescriptor extends LegacyFixedPointColumnDescriptor { - private FixedPointColumnDescriptor(Column column, byte flags) + private FixedPointColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); @@ -1485,7 +1485,7 @@ public class IndexData { */ private static final class ByteColumnDescriptor extends ColumnDescriptor { - private ByteColumnDescriptor(Column column, byte flags) + private ByteColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); @@ -1514,7 +1514,7 @@ public class IndexData { */ private static final class BooleanColumnDescriptor extends ColumnDescriptor { - private BooleanColumnDescriptor(Column column, byte flags) + private BooleanColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); @@ -1531,7 +1531,7 @@ public class IndexData { throws IOException { bout.write( - Column.toBooleanValue(value) ? + ColumnImpl.toBooleanValue(value) ? (isAscending() ? ASC_BOOLEAN_TRUE : DESC_BOOLEAN_TRUE) : (isAscending() ? ASC_BOOLEAN_FALSE : DESC_BOOLEAN_FALSE)); } @@ -1543,7 +1543,7 @@ public class IndexData { private static final class GenLegTextColumnDescriptor extends ColumnDescriptor { - private GenLegTextColumnDescriptor(Column column, byte flags) + private GenLegTextColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); @@ -1564,7 +1564,7 @@ public class IndexData { */ private static final class GenTextColumnDescriptor extends ColumnDescriptor { - private GenTextColumnDescriptor(Column column, byte flags) + private GenTextColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); @@ -1585,7 +1585,7 @@ public class IndexData { */ private static final class GuidColumnDescriptor extends ColumnDescriptor { - private GuidColumnDescriptor(Column column, byte flags) + private GuidColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); @@ -1620,7 +1620,7 @@ public class IndexData { */ private static final class ReadOnlyColumnDescriptor extends ColumnDescriptor { - private ReadOnlyColumnDescriptor(Column column, byte flags) + private ReadOnlyColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); diff --git a/src/java/com/healthmarketscience/jackcess/IndexImpl.java b/src/java/com/healthmarketscience/jackcess/IndexImpl.java index c34bf89..0cfe19e 100644 --- a/src/java/com/healthmarketscience/jackcess/IndexImpl.java +++ b/src/java/com/healthmarketscience/jackcess/IndexImpl.java @@ -45,8 +45,7 @@ import org.apache.commons.logging.LogFactory; */ public class IndexImpl extends Index implements Comparable<IndexImpl> { - - protected static final Log LOG = LogFactory.getLog(Index.class); + protected static final Log LOG = LogFactory.getLog(IndexImpl.class); /** index type for primary key indexes */ static final byte PRIMARY_KEY_INDEX_TYPE = (byte)1; diff --git a/src/java/com/healthmarketscience/jackcess/JetFormat.java b/src/java/com/healthmarketscience/jackcess/JetFormat.java index 2a37120..e4efdb7 100644 --- a/src/java/com/healthmarketscience/jackcess/JetFormat.java +++ b/src/java/com/healthmarketscience/jackcess/JetFormat.java @@ -258,7 +258,7 @@ public abstract class JetFormat { public final boolean LEGACY_NUMERIC_INDEXES; public final Charset CHARSET; - public final Column.SortOrder DEFAULT_SORT_ORDER; + public final ColumnImpl.SortOrder DEFAULT_SORT_ORDER; /** * @param channel the database file. @@ -490,7 +490,7 @@ public abstract class JetFormat { protected abstract int defineMaxIndexNameLength(); protected abstract Charset defineCharset(); - protected abstract Column.SortOrder defineDefaultSortOrder(); + protected abstract ColumnImpl.SortOrder defineDefaultSortOrder(); protected abstract boolean defineLegacyNumericIndexes(); @@ -703,8 +703,8 @@ public abstract class JetFormat { protected Charset defineCharset() { return Charset.defaultCharset(); } @Override - protected Column.SortOrder defineDefaultSortOrder() { - return Column.GENERAL_LEGACY_SORT_ORDER; + protected ColumnImpl.SortOrder defineDefaultSortOrder() { + return ColumnImpl.GENERAL_LEGACY_SORT_ORDER; } @Override @@ -921,8 +921,8 @@ public abstract class JetFormat { protected Charset defineCharset() { return Charset.forName("UTF-16LE"); } @Override - protected Column.SortOrder defineDefaultSortOrder() { - return Column.GENERAL_LEGACY_SORT_ORDER; + protected ColumnImpl.SortOrder defineDefaultSortOrder() { + return ColumnImpl.GENERAL_LEGACY_SORT_ORDER; } @Override @@ -995,8 +995,8 @@ public abstract class JetFormat { } @Override - protected Column.SortOrder defineDefaultSortOrder() { - return Column.GENERAL_SORT_ORDER; + protected ColumnImpl.SortOrder defineDefaultSortOrder() { + return ColumnImpl.GENERAL_SORT_ORDER; } @Override diff --git a/src/java/com/healthmarketscience/jackcess/NullMask.java b/src/java/com/healthmarketscience/jackcess/NullMask.java index 5be5218..6b120e0 100644 --- a/src/java/com/healthmarketscience/jackcess/NullMask.java +++ b/src/java/com/healthmarketscience/jackcess/NullMask.java @@ -73,7 +73,7 @@ public class NullMask { * columns, returns the actual value of the column (where * non-{@code null} == {@code true}) */ - public boolean isNull(Column column) { + public boolean isNull(ColumnImpl column) { int columnNumber = column.getColumnNumber(); // if new columns were added to the table, old null masks may not include // them (meaning the field is null) @@ -89,7 +89,7 @@ public class NullMask { * boolean value is {@code true}). * @param column column to be marked non-{@code null} */ - public void markNotNull(Column column) { + public void markNotNull(ColumnImpl column) { int columnNumber = column.getColumnNumber(); int maskIndex = byteIndex(columnNumber); _mask[maskIndex] = (byte) (_mask[maskIndex] | bitMask(columnNumber)); diff --git a/src/java/com/healthmarketscience/jackcess/PropertyMaps.java b/src/java/com/healthmarketscience/jackcess/PropertyMaps.java index c40f1f3..2371841 100644 --- a/src/java/com/healthmarketscience/jackcess/PropertyMaps.java +++ b/src/java/com/healthmarketscience/jackcess/PropertyMaps.java @@ -253,7 +253,7 @@ public class PropertyMaps implements Iterable<PropertyMap> private String readPropName(ByteBuffer buffer) { int nameLength = buffer.getShort(); byte[] nameBytes = ByteUtil.getBytes(buffer, nameLength); - return Column.decodeUncompressedText(nameBytes, _database.getCharset()); + return ColumnImpl.decodeUncompressedText(nameBytes, _database.getCharset()); } /** @@ -302,7 +302,7 @@ public class PropertyMaps implements Iterable<PropertyMap> /** * Column adapted to work w/out a Table. */ - private class PropColumn extends Column + private class PropColumn extends ColumnImpl { @Override public DatabaseImpl getDatabase() { diff --git a/src/java/com/healthmarketscience/jackcess/Relationship.java b/src/java/com/healthmarketscience/jackcess/Relationship.java index 43dd242..7c68546 100644 --- a/src/java/com/healthmarketscience/jackcess/Relationship.java +++ b/src/java/com/healthmarketscience/jackcess/Relationship.java @@ -59,10 +59,10 @@ public class Relationship { private final Table _toTable; /** the columns in the "from" table in this relationship (aligned w/ toColumns list) */ - private List<Column> _toColumns; + private List<ColumnImpl> _toColumns; /** the columns in the "to" table in this relationship (aligned w/ toColumns list) */ - private List<Column> _fromColumns; + private List<ColumnImpl> _fromColumns; /** the various flags describing this relationship */ private final int _flags; @@ -71,11 +71,11 @@ public class Relationship { { _name = name; _fromTable = fromTable; - _fromColumns = new ArrayList<Column>( - Collections.nCopies(numCols, (Column)null)); + _fromColumns = new ArrayList<ColumnImpl>( + Collections.nCopies(numCols, (ColumnImpl)null)); _toTable = toTable; - _toColumns = new ArrayList<Column>( - Collections.nCopies(numCols, (Column)null)); + _toColumns = new ArrayList<ColumnImpl>( + Collections.nCopies(numCols, (ColumnImpl)null)); _flags = flags; } @@ -87,7 +87,7 @@ public class Relationship { return _fromTable; } - public List<Column> getFromColumns() { + public List<ColumnImpl> getFromColumns() { return _fromColumns; } @@ -95,7 +95,7 @@ public class Relationship { return _toTable; } - public List<Column> getToColumns() { + public List<ColumnImpl> getToColumns() { return _toColumns; } diff --git a/src/java/com/healthmarketscience/jackcess/SimpleExportFilter.java b/src/java/com/healthmarketscience/jackcess/SimpleExportFilter.java index 3669a94..17d0528 100644 --- a/src/java/com/healthmarketscience/jackcess/SimpleExportFilter.java +++ b/src/java/com/healthmarketscience/jackcess/SimpleExportFilter.java @@ -43,7 +43,7 @@ public class SimpleExportFilter implements ExportFilter { public SimpleExportFilter() { } - public List<Column> filterColumns(List<Column> columns) throws IOException { + public List<ColumnImpl> filterColumns(List<ColumnImpl> columns) throws IOException { return columns; } diff --git a/src/java/com/healthmarketscience/jackcess/SimpleImportFilter.java b/src/java/com/healthmarketscience/jackcess/SimpleImportFilter.java index ba7eabb..38387ca 100644 --- a/src/java/com/healthmarketscience/jackcess/SimpleImportFilter.java +++ b/src/java/com/healthmarketscience/jackcess/SimpleImportFilter.java @@ -45,7 +45,7 @@ public class SimpleImportFilter implements ImportFilter { public SimpleImportFilter() { } - public List<Column> filterColumns(List<Column> destColumns, + public List<ColumnImpl> filterColumns(List<ColumnImpl> destColumns, ResultSetMetaData srcColumns) throws SQLException, IOException { diff --git a/src/java/com/healthmarketscience/jackcess/Table.java b/src/java/com/healthmarketscience/jackcess/Table.java index 1b7a359..b0dcf7e 100644 --- a/src/java/com/healthmarketscience/jackcess/Table.java +++ b/src/java/com/healthmarketscience/jackcess/Table.java @@ -84,7 +84,7 @@ public abstract class Table implements Iterable<Map<String, Object>> * @return All of the columns in this table (unmodifiable List) * @usage _general_method_ */ - public abstract List<Column> getColumns(); + public abstract List<? extends Column> getColumns(); /** * @return the column with the given name diff --git a/src/java/com/healthmarketscience/jackcess/TableBuilder.java b/src/java/com/healthmarketscience/jackcess/TableBuilder.java index c1c8496..09b3876 100644 --- a/src/java/com/healthmarketscience/jackcess/TableBuilder.java +++ b/src/java/com/healthmarketscience/jackcess/TableBuilder.java @@ -41,7 +41,7 @@ public class TableBuilder { /** name of the new table */ private String _name; /** columns for the new table */ - private List<Column> _columns = new ArrayList<Column>(); + private List<ColumnImpl> _columns = new ArrayList<ColumnImpl>(); /** indexes for the new table */ private List<IndexBuilder> _indexes = new ArrayList<IndexBuilder>(); /** whether or not table/column/index names are automatically escaped */ @@ -64,7 +64,7 @@ public class TableBuilder { /** * Adds a Column to the new table. */ - public TableBuilder addColumn(Column column) { + public TableBuilder addColumn(ColumnImpl column) { if(_escapeIdentifiers) { column.setName(DatabaseImpl.escapeIdentifier(column.getName())); } @@ -125,10 +125,10 @@ public class TableBuilder { * Creates a new Table in the given Database with the currently configured * attributes. */ - public Table toTable(DatabaseImpl db) + public Table toTable(Database db) throws IOException { - db.createTable(_name, _columns, _indexes); + ((DatabaseImpl)db).createTable(_name, _columns, _indexes); return db.getTable(_name); } diff --git a/src/java/com/healthmarketscience/jackcess/TableCreator.java b/src/java/com/healthmarketscience/jackcess/TableCreator.java index bdf793e..bbe2df9 100644 --- a/src/java/com/healthmarketscience/jackcess/TableCreator.java +++ b/src/java/com/healthmarketscience/jackcess/TableCreator.java @@ -39,7 +39,7 @@ class TableCreator { private final DatabaseImpl _database; private final String _name; - private final List<Column> _columns; + private final List<ColumnImpl> _columns; private final List<IndexBuilder> _indexes; private final Map<IndexBuilder,IndexState> _indexStates = new HashMap<IndexBuilder,IndexState>(); @@ -48,7 +48,7 @@ class TableCreator private int _indexCount; private int _logicalIndexCount; - public TableCreator(DatabaseImpl database, String name, List<Column> columns, + public TableCreator(DatabaseImpl database, String name, List<ColumnImpl> columns, List<IndexBuilder> indexes) { _database = database; _name = name; @@ -77,7 +77,7 @@ class TableCreator return _umapPageNumber; } - public List<Column> getColumns() { + public List<ColumnImpl> getColumns() { return _columns; } @@ -153,7 +153,7 @@ class TableCreator getFormat().MAX_COLUMNS_PER_TABLE + " columns"); } - Column.SortOrder dbSortOrder = null; + ColumnImpl.SortOrder dbSortOrder = null; try { dbSortOrder = _database.getDefaultSortOrder(); } catch(IOException e) { @@ -162,7 +162,7 @@ class TableCreator Set<String> colNames = new HashSet<String>(); // next, validate the column definitions - for(Column column : _columns) { + for(ColumnImpl column : _columns) { // FIXME for now, we can't create complex columns if(column.getType() == DataType.COMPLEX_TYPE) { @@ -182,11 +182,11 @@ class TableCreator } } - List<Column> autoCols = TableImpl.getAutoNumberColumns(_columns); + List<ColumnImpl> autoCols = TableImpl.getAutoNumberColumns(_columns); if(autoCols.size() > 1) { // for most autonumber types, we can only have one of each type Set<DataType> autoTypes = EnumSet.noneOf(DataType.class); - for(Column c : autoCols) { + for(ColumnImpl c : autoCols) { if(!c.getType().isMultipleAutoNumberAllowed() && !autoTypes.add(c.getType())) { throw new IllegalArgumentException( diff --git a/src/java/com/healthmarketscience/jackcess/TableImpl.java b/src/java/com/healthmarketscience/jackcess/TableImpl.java index 5459001..1e6c838 100644 --- a/src/java/com/healthmarketscience/jackcess/TableImpl.java +++ b/src/java/com/healthmarketscience/jackcess/TableImpl.java @@ -81,9 +81,9 @@ public class TableImpl extends Table /** comparator which sorts variable length columns based on their index into the variable length offset table */ - private static final Comparator<Column> VAR_LEN_COLUMN_COMPARATOR = - new Comparator<Column>() { - public int compare(Column c1, Column c2) { + private static final Comparator<ColumnImpl> VAR_LEN_COLUMN_COMPARATOR = + new Comparator<ColumnImpl>() { + public int compare(ColumnImpl c1, ColumnImpl c2) { return ((c1.getVarLenTableIndex() < c2.getVarLenTableIndex()) ? -1 : ((c1.getVarLenTableIndex() > c2.getVarLenTableIndex()) ? 1 : 0)); @@ -91,9 +91,9 @@ public class TableImpl extends Table }; /** comparator which sorts columns based on their display index */ - private static final Comparator<Column> DISPLAY_ORDER_COMPARATOR = - new Comparator<Column>() { - public int compare(Column c1, Column c2) { + private static final Comparator<ColumnImpl> DISPLAY_ORDER_COMPARATOR = + new Comparator<ColumnImpl>() { + public int compare(ColumnImpl c1, ColumnImpl c2) { return ((c1.getDisplayIndex() < c2.getDisplayIndex()) ? -1 : ((c1.getDisplayIndex() > c2.getDisplayIndex()) ? 1 : 0)); @@ -123,11 +123,11 @@ public class TableImpl extends Table /** max Number of variable columns in the table */ private short _maxVarColumnCount; /** List of columns in this table, ordered by column number */ - private List<Column> _columns = new ArrayList<Column>(); + private List<ColumnImpl> _columns = new ArrayList<ColumnImpl>(); /** List of variable length columns in this table, ordered by offset */ - private final List<Column> _varColumns = new ArrayList<Column>(); + private final List<ColumnImpl> _varColumns = new ArrayList<ColumnImpl>(); /** List of autonumber columns in this table, ordered by column number */ - private List<Column> _autoNumColumns; + private List<ColumnImpl> _autoNumColumns; /** List of indexes on this table (multiple logical indexes may be backed by the same index data) */ private final List<IndexImpl> _indexes = new ArrayList<IndexImpl>(); @@ -135,7 +135,7 @@ public class TableImpl extends Table index) */ private final List<IndexData> _indexDatas = new ArrayList<IndexData>(); /** List of columns in this table which are in one or more indexes */ - private final Set<Column> _indexColumns = new LinkedHashSet<Column>(); + private final Set<ColumnImpl> _indexColumns = new LinkedHashSet<ColumnImpl>(); /** Table name as stored in Database */ private final String _name; /** Usage map of pages that this table owns */ @@ -177,7 +177,7 @@ public class TableImpl extends Table * Only used by unit tests */ - TableImpl(boolean testing, List<Column> columns) throws IOException { + TableImpl(boolean testing, List<ColumnImpl> columns) throws IOException { if(!testing) { throw new IllegalArgumentException(); } @@ -302,13 +302,13 @@ public class TableImpl extends Table } @Override - public List<Column> getColumns() { + public List<ColumnImpl> getColumns() { return Collections.unmodifiableList(_columns); } @Override - public Column getColumn(String name) { - for(Column column : _columns) { + public ColumnImpl getColumn(String name) { + for(ColumnImpl column : _columns) { if(column.getName().equalsIgnoreCase(name)) { return column; } @@ -320,12 +320,12 @@ public class TableImpl extends Table /** * Only called by unit tests */ - private void setColumns(List<Column> columns) { + private void setColumns(List<ColumnImpl> columns) { _columns = columns; int colIdx = 0; int varLenIdx = 0; int fixedOffset = 0; - for(Column col : _columns) { + for(ColumnImpl col : _columns) { col.setColumnNumber((short)colIdx); col.setColumnIndex(colIdx++); if(col.isVariableLength()) { @@ -470,7 +470,7 @@ public class TableImpl extends Table // move to row data to get index values rowBuffer = positionAtRowData(rowState, rowId); - for(Column idxCol : _indexColumns) { + for(ColumnImpl idxCol : _indexColumns) { getRowColumn(getFormat(), rowBuffer, idxCol, rowState, null); } @@ -528,7 +528,7 @@ public class TableImpl extends Table * e.g. {@link Cursor#getCurrentRowValue}. * @usage _advanced_method_ */ - public Object getRowValue(RowState rowState, RowId rowId, Column column) + public Object getRowValue(RowState rowState, RowId rowId, ColumnImpl column) throws IOException { if(this != column.getTable()) { @@ -570,13 +570,13 @@ public class TableImpl extends Table JetFormat format, RowState rowState, ByteBuffer rowBuffer, - Collection<Column> columns, + Collection<ColumnImpl> columns, Collection<String> columnNames) throws IOException { Map<String, Object> rtn = new LinkedHashMap<String, Object>( columns.size()); - for(Column column : columns) { + for(ColumnImpl column : columns) { if((columnNames == null) || (columnNames.contains(column.getName()))) { // Add the value to the row data @@ -593,9 +593,9 @@ public class TableImpl extends Table */ private static Object getRowColumn(JetFormat format, ByteBuffer rowBuffer, - Column column, + ColumnImpl column, RowState rowState, - Map<Column,byte[]> rawVarValues) + Map<ColumnImpl,byte[]> rawVarValues) throws IOException { byte[] columnData = null; @@ -675,7 +675,7 @@ public class TableImpl extends Table // cache "raw" row value. see note about caching above rowState.setRowValue(column.getColumnIndex(), - Column.rawDataWrapper(columnData)); + ColumnImpl.rawDataWrapper(columnData)); return rowState.handleRowError(column, columnData, e); } @@ -910,7 +910,7 @@ public class TableImpl extends Table // total up the amount of space used by the column and index names (2 // bytes per char + 2 bytes for the length) - for(Column col : creator.getColumns()) { + for(ColumnImpl col : creator.getColumns()) { int nameByteLen = (col.getName().length() * JetFormat.TEXT_FIELD_UNIT_SIZE); totalTableDefSize += nameByteLen + 2; @@ -935,7 +935,7 @@ public class TableImpl extends Table } // column definitions - Column.writeDefinitions(creator, buffer); + ColumnImpl.writeDefinitions(creator, buffer); if(creator.hasIndexes()) { // index and index data definitions @@ -1011,7 +1011,7 @@ public class TableImpl extends Table TableCreator creator, ByteBuffer buffer, int totalTableDefSize) throws IOException { - List<Column> columns = creator.getColumns(); + List<ColumnImpl> columns = creator.getColumns(); //Start writing the tdef writeTablePageHeader(buffer); @@ -1025,7 +1025,7 @@ public class TableImpl extends Table } buffer.put(TYPE_USER); //Table type buffer.putShort((short) columns.size()); //Max columns a row will have - buffer.putShort(Column.countVariableLength(columns)); //Number of variable columns in table + buffer.putShort(ColumnImpl.countVariableLength(columns)); //Number of variable columns in table buffer.putShort((short) columns.size()); //Number of columns in table buffer.putInt(creator.getLogicalIndexCount()); //Number of logical indexes in table buffer.putInt(creator.getIndexCount()); //Number of indexes in table @@ -1061,7 +1061,7 @@ public class TableImpl extends Table */ static void writeName(ByteBuffer buffer, String name, Charset charset) { - ByteBuffer encName = Column.encodeUncompressedText(name, charset); + ByteBuffer encName = ColumnImpl.encodeUncompressedText(name, charset); buffer.putShort((short) encName.remaining()); buffer.put(encName); } @@ -1206,7 +1206,7 @@ public class TableImpl extends Table _indexCount * getFormat().SIZE_INDEX_DEFINITION; int dispIndex = 0; for (int i = 0; i < columnCount; i++) { - Column column = new Column(this, tableBuffer, + ColumnImpl column = new ColumnImpl(this, tableBuffer, colOffset + (i * getFormat().SIZE_COLUMN_HEADER), dispIndex++); _columns.add(column); if(column.isVariableLength()) { @@ -1218,7 +1218,7 @@ public class TableImpl extends Table tableBuffer.position(colOffset + (columnCount * getFormat().SIZE_COLUMN_HEADER)); for (int i = 0; i < columnCount; i++) { - Column column = _columns.get(i); + ColumnImpl column = _columns.get(i); column.setName(readName(tableBuffer)); } Collections.sort(_columns); @@ -1226,7 +1226,7 @@ public class TableImpl extends Table // setup the data index for the columns int colIdx = 0; - for(Column col : _columns) { + for(ColumnImpl col : _columns) { col.setColumnIndex(colIdx++); } @@ -1261,7 +1261,7 @@ public class TableImpl extends Table Collections.sort(_columns, DISPLAY_ORDER_COMPARATOR); } - for(Column col : _columns) { + for(ColumnImpl col : _columns) { // some columns need to do extra work after the table is completely // loaded col.postTableLoadInit(); @@ -1295,7 +1295,7 @@ public class TableImpl extends Table private String readName(ByteBuffer buffer) { int nameLength = readNameLength(buffer); byte[] nameBytes = ByteUtil.getBytes(buffer, nameLength); - return Column.decodeUncompressedText(nameBytes, + return ColumnImpl.decodeUncompressedText(nameBytes, getDatabase().getCharset()); } @@ -1328,7 +1328,7 @@ public class TableImpl extends Table if(rowMap == null) { return row; } - for(Column col : _columns) { + for(ColumnImpl col : _columns) { if(rowMap.containsKey(col.getName())) { col.setRowValue(row, col.getRowValue(rowMap)); } @@ -1464,10 +1464,10 @@ public class TableImpl extends Table // hang on to the raw values of var length columns we are "keeping". this // will allow us to re-use pre-written var length data, which can save // space for things like long value columns. - Map<Column,byte[]> keepRawVarValues = - (!_varColumns.isEmpty() ? new HashMap<Column,byte[]>() : null); + Map<ColumnImpl,byte[]> keepRawVarValues = + (!_varColumns.isEmpty() ? new HashMap<ColumnImpl,byte[]>() : null); - for(Column column : _columns) { + for(ColumnImpl column : _columns) { if(_autoNumColumns.contains(column)) { // fill in any auto-numbers (we don't allow autonumber values to be // modified) @@ -1675,7 +1675,7 @@ public class TableImpl extends Table ByteBuffer createRow(Object[] rowArray, ByteBuffer buffer) throws IOException { - return createRow(rowArray, buffer, 0, Collections.<Column,byte[]>emptyMap()); + return createRow(rowArray, buffer, 0, Collections.<ColumnImpl,byte[]>emptyMap()); } /** @@ -1689,7 +1689,7 @@ public class TableImpl extends Table * @return the given buffer, filled with the row data */ private ByteBuffer createRow(Object[] rowArray, ByteBuffer buffer, - int minRowSize, Map<Column,byte[]> rawVarValues) + int minRowSize, Map<ColumnImpl,byte[]> rawVarValues) throws IOException { buffer.putShort(_maxColumnCount); @@ -1698,7 +1698,7 @@ public class TableImpl extends Table //Fixed length column data comes first int fixedDataStart = buffer.position(); int fixedDataEnd = fixedDataStart; - for (Column col : _columns) { + for (ColumnImpl col : _columns) { if(col.isVariableLength()) { continue; @@ -1708,7 +1708,7 @@ public class TableImpl extends Table if (col.getType() == DataType.BOOLEAN) { - if(Column.toBooleanValue(rowValue)) { + if(ColumnImpl.toBooleanValue(rowValue)) { //Booleans are stored in the null mask nullMask.markNotNull(col); } @@ -1756,7 +1756,7 @@ public class TableImpl extends Table // for each non-null long value column we need to reserve a small // amount of space so that we don't end up running out of row space // later by being too greedy - for (Column varCol : _varColumns) { + for (ColumnImpl varCol : _varColumns) { if((varCol.getType().isLongValue()) && (varCol.getRowValue(rowArray) != null)) { maxRowSize -= getFormat().SIZE_LONG_VALUE_DEF; @@ -1766,7 +1766,7 @@ public class TableImpl extends Table //Now write out variable length column data short[] varColumnOffsets = new short[_maxVarColumnCount]; int varColumnOffsetsIndex = 0; - for (Column varCol : _varColumns) { + for (ColumnImpl varCol : _varColumns) { short offset = (short) buffer.position(); Object rowValue = varCol.getRowValue(rowArray); if (rowValue != null) { @@ -1844,9 +1844,9 @@ public class TableImpl extends Table } Object complexAutoNumber = null; - for(Column col : _autoNumColumns) { + for(ColumnImpl col : _autoNumColumns) { // ignore given row value, use next autonumber - Column.AutoNumberGenerator autoNumGen = col.getAutoNumberGenerator(); + ColumnImpl.AutoNumberGenerator autoNumGen = col.getAutoNumberGenerator(); Object rowValue = null; if(autoNumGen.getType() != DataType.COMPLEX_TYPE) { rowValue = autoNumGen.getNext(null); @@ -1908,7 +1908,7 @@ public class TableImpl extends Table rtn.append("\nIndex (data) count: " + _indexCount); rtn.append("\nLogical Index count: " + _logicalIndexCount); rtn.append("\nColumns:\n"); - for(Column col : _columns) { + for(ColumnImpl col : _columns) { rtn.append(col); } rtn.append("\nIndexes:\n"); @@ -1937,8 +1937,8 @@ public class TableImpl extends Table public String display(long limit) throws IOException { reset(); StringBuilder rtn = new StringBuilder(); - for(Iterator<Column> iter = _columns.iterator(); iter.hasNext(); ) { - Column col = iter.next(); + for(Iterator<ColumnImpl> iter = _columns.iterator(); iter.hasNext(); ) { + ColumnImpl col = iter.next(); rtn.append(col.getName()); if (iter.hasNext()) { rtn.append("\t"); @@ -2115,14 +2115,16 @@ public class TableImpl extends Table * @return the "AutoNumber" columns in the given collection of columns. * @usage _advanced_method_ */ - public static List<Column> getAutoNumberColumns(Collection<Column> columns) { - List<Column> autoCols = new ArrayList<Column>(1); - for(Column c : columns) { + public static List<ColumnImpl> getAutoNumberColumns( + Collection<ColumnImpl> columns) + { + List<ColumnImpl> autoCols = new ArrayList<ColumnImpl>(1); + for(ColumnImpl c : columns) { if(c.isAutoNumber()) { autoCols.add(c); } } - return (!autoCols.isEmpty() ? autoCols : Collections.<Column>emptyList()); + return (!autoCols.isEmpty() ? autoCols : Collections.<ColumnImpl>emptyList()); } /** @@ -2392,8 +2394,7 @@ public class TableImpl extends Table return _finalRowBuffer; } - private Object handleRowError(Column column, - byte[] columnData, + private Object handleRowError(ColumnImpl column, byte[] columnData, Exception error) throws IOException { diff --git a/src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java index 7335729..7cdc45f 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java +++ b/src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java @@ -25,7 +25,7 @@ import java.util.List; import java.util.Map; import com.healthmarketscience.jackcess.ByteUtil; -import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.ColumnImpl; import com.healthmarketscience.jackcess.TableImpl; @@ -39,27 +39,27 @@ public class AttachmentColumnInfo extends ComplexColumnInfo<Attachment> private static final String FILE_NAME_COL_NAME = "FileName"; private static final String FILE_TYPE_COL_NAME = "FileType"; - private final Column _fileUrlCol; - private final Column _fileNameCol; - private final Column _fileTypeCol; - private final Column _fileDataCol; - private final Column _fileTimeStampCol; - private final Column _fileFlagsCol; + private final ColumnImpl _fileUrlCol; + private final ColumnImpl _fileNameCol; + private final ColumnImpl _fileTypeCol; + private final ColumnImpl _fileDataCol; + private final ColumnImpl _fileTimeStampCol; + private final ColumnImpl _fileFlagsCol; - public AttachmentColumnInfo(Column column, int complexId, + public AttachmentColumnInfo(ColumnImpl column, int complexId, TableImpl typeObjTable, TableImpl flatTable) throws IOException { super(column, complexId, typeObjTable, flatTable); - Column fileUrlCol = null; - Column fileNameCol = null; - Column fileTypeCol = null; - Column fileDataCol = null; - Column fileTimeStampCol = null; - Column fileFlagsCol = null; + ColumnImpl fileUrlCol = null; + ColumnImpl fileNameCol = null; + ColumnImpl fileTypeCol = null; + ColumnImpl fileDataCol = null; + ColumnImpl fileTimeStampCol = null; + ColumnImpl fileFlagsCol = null; - for(Column col : getTypeColumns()) { + for(ColumnImpl col : getTypeColumns()) { switch(col.getType()) { case TEXT: if(FILE_NAME_COL_NAME.equalsIgnoreCase(col.getName())) { @@ -100,27 +100,27 @@ public class AttachmentColumnInfo extends ComplexColumnInfo<Attachment> _fileFlagsCol = fileFlagsCol; } - public Column getFileUrlColumn() { + public ColumnImpl getFileUrlColumn() { return _fileUrlCol; } - public Column getFileNameColumn() { + public ColumnImpl getFileNameColumn() { return _fileNameCol; } - public Column getFileTypeColumn() { + public ColumnImpl getFileTypeColumn() { return _fileTypeCol; } - public Column getFileDataColumn() { + public ColumnImpl getFileDataColumn() { return _fileDataCol; } - public Column getFileTimeStampColumn() { + public ColumnImpl getFileTimeStampColumn() { return _fileTimeStampCol; } - public Column getFileFlagsColumn() { + public ColumnImpl getFileFlagsColumn() { return _fileFlagsCol; } @@ -187,7 +187,7 @@ public class AttachmentColumnInfo extends ComplexColumnInfo<Attachment> // attachment data has these columns FileURL(MEMO), FileName(TEXT), // FileType(TEXT), FileData(OLE), FileTimeStamp(SHORT_DATE_TIME), // FileFlags(LONG) - List<Column> typeCols = typeObjTable.getColumns(); + List<ColumnImpl> typeCols = typeObjTable.getColumns(); if(typeCols.size() < 6) { return false; } @@ -198,7 +198,7 @@ public class AttachmentColumnInfo extends ComplexColumnInfo<Attachment> int numOle= 0; int numLong = 0; - for(Column col : typeCols) { + for(ColumnImpl col : typeCols) { switch(col.getType()) { case TEXT: ++numText; diff --git a/src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java index d78c4a2..73a8894 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java +++ b/src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Map; import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.ColumnImpl; import com.healthmarketscience.jackcess.CursorBuilder; import com.healthmarketscience.jackcess.DataType; import com.healthmarketscience.jackcess.DatabaseImpl; @@ -46,7 +47,7 @@ import org.apache.commons.logging.LogFactory; */ public abstract class ComplexColumnInfo<V extends ComplexValue> { - private static final Log LOG = LogFactory.getLog(Column.class); + private static final Log LOG = LogFactory.getLog(ComplexColumnInfo.class); public static final int INVALID_ID = -1; public static final ComplexValueForeignKey INVALID_COMPLEX_VALUE_ID = @@ -56,16 +57,16 @@ public abstract class ComplexColumnInfo<V extends ComplexValue> private static final String COL_TABLE_ID = "ConceptualTableID"; private static final String COL_FLAT_TABLE_ID = "FlatTableID"; - private final Column _column; + private final ColumnImpl _column; private final int _complexTypeId; private final TableImpl _flatTable; - private final List<Column> _typeCols; - private final Column _pkCol; - private final Column _complexValFkCol; + private final List<ColumnImpl> _typeCols; + private final ColumnImpl _pkCol; + private final ColumnImpl _complexValFkCol; private IndexCursor _pkCursor; private IndexCursor _complexValIdCursor; - protected ComplexColumnInfo(Column column, int complexTypeId, + protected ComplexColumnInfo(ColumnImpl column, int complexTypeId, TableImpl typeObjTable, TableImpl flatTable) throws IOException { @@ -76,15 +77,15 @@ public abstract class ComplexColumnInfo<V extends ComplexValue> // the flat table has all the "value" columns and 2 extra columns, a // primary key for each row, and a LONG value which is essentially a // foreign key to the main table. - List<Column> typeCols = new ArrayList<Column>(); - List<Column> otherCols = new ArrayList<Column>(); + List<ColumnImpl> typeCols = new ArrayList<ColumnImpl>(); + List<ColumnImpl> otherCols = new ArrayList<ColumnImpl>(); diffFlatColumns(typeObjTable, flatTable, typeCols, otherCols); _typeCols = Collections.unmodifiableList(typeCols); - Column pkCol = null; - Column complexValFkCol = null; - for(Column col : otherCols) { + ColumnImpl pkCol = null; + ColumnImpl complexValFkCol = null; + for(ColumnImpl col : otherCols) { if(col.isAutoNumber()) { pkCol = col; } else if(col.getType() == DataType.LONG) { @@ -102,7 +103,7 @@ public abstract class ComplexColumnInfo<V extends ComplexValue> } public static ComplexColumnInfo<? extends ComplexValue> create( - Column column, ByteBuffer buffer, int offset) + ColumnImpl column, ByteBuffer buffer, int offset) throws IOException { int complexTypeId = buffer.getInt( @@ -158,7 +159,7 @@ public abstract class ComplexColumnInfo<V extends ComplexValue> // nothing to do in base class } - public Column getColumn() { + public ColumnImpl getColumn() { return _column; } @@ -174,15 +175,15 @@ public abstract class ComplexColumnInfo<V extends ComplexValue> return getDatabase().getPageChannel(); } - public Column getPrimaryKeyColumn() { + public ColumnImpl getPrimaryKeyColumn() { return _pkCol; } - public Column getComplexValueForeignKeyColumn() { + public ColumnImpl getComplexValueForeignKeyColumn() { return _complexValFkCol; } - protected List<Column> getTypeColumns() { + protected List<ColumnImpl> getTypeColumns() { return _typeCols; } @@ -372,12 +373,12 @@ public abstract class ComplexColumnInfo<V extends ComplexValue> protected static void diffFlatColumns(TableImpl typeObjTable, TableImpl flatTable, - List<Column> typeCols, - List<Column> otherCols) + List<ColumnImpl> typeCols, + List<ColumnImpl> otherCols) { // each "flat"" table has the columns from the "type" table, plus some // others. separate the "flat" columns into these 2 buckets - for(Column col : flatTable.getColumns()) { + for(ColumnImpl col : flatTable.getColumns()) { boolean found = false; try { typeObjTable.getColumn(col.getName()); @@ -433,7 +434,7 @@ public abstract class ComplexColumnInfo<V extends ComplexValue> _complexValueFk = complexValueFk; } - public Column getColumn() { + public ColumnImpl getColumn() { return _complexValueFk.getColumn(); } diff --git a/src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java b/src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java index 80735e3..b1b83a3 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java +++ b/src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java @@ -25,7 +25,7 @@ import java.util.Date; import java.util.List; import java.util.Map; -import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.ColumnImpl; /** * Value which is returned for a complex column. This value corresponds to a @@ -45,11 +45,11 @@ public class ComplexValueForeignKey extends Number { private static final long serialVersionUID = 20110805L; - private transient final Column _column; + private transient final ColumnImpl _column; private final int _value; private transient List<? extends ComplexValue> _values; - public ComplexValueForeignKey(Column column, int value) { + public ComplexValueForeignKey(ColumnImpl column, int value) { _column = column; _value = value; } @@ -58,7 +58,7 @@ public class ComplexValueForeignKey extends Number return _value; } - public Column getColumn() { + public ColumnImpl getColumn() { return _column; } diff --git a/src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java index 2cf504c..f0f449a 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java +++ b/src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java @@ -25,7 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Set; -import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.ColumnImpl; import com.healthmarketscience.jackcess.DataType; import com.healthmarketscience.jackcess.TableImpl; @@ -40,9 +40,9 @@ public class MultiValueColumnInfo extends ComplexColumnInfo<SingleValue> DataType.BYTE, DataType.INT, DataType.LONG, DataType.FLOAT, DataType.DOUBLE, DataType.GUID, DataType.NUMERIC, DataType.TEXT); - private final Column _valueCol; + private final ColumnImpl _valueCol; - public MultiValueColumnInfo(Column column, int complexId, + public MultiValueColumnInfo(ColumnImpl column, int complexId, TableImpl typeObjTable, TableImpl flatTable) throws IOException { @@ -57,7 +57,7 @@ public class MultiValueColumnInfo extends ComplexColumnInfo<SingleValue> return ComplexDataType.MULTI_VALUE; } - public Column getValueColumn() { + public ColumnImpl getValueColumn() { return _valueCol; } @@ -91,7 +91,7 @@ public class MultiValueColumnInfo extends ComplexColumnInfo<SingleValue> public static boolean isMultiValueColumn(TableImpl typeObjTable) { // if we found a single value of a "simple" type, then we are dealing with // a multi-value column - List<Column> typeCols = typeObjTable.getColumns(); + List<ColumnImpl> typeCols = typeObjTable.getColumns(); return ((typeCols.size() == 1) && VALUE_TYPES.contains(typeCols.get(0).getType())); } diff --git a/src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java index 25a28f7..6af1557 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java +++ b/src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java @@ -24,7 +24,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.ColumnImpl; import com.healthmarketscience.jackcess.TableImpl; /** @@ -35,14 +35,14 @@ import com.healthmarketscience.jackcess.TableImpl; public class UnsupportedColumnInfo extends ComplexColumnInfo<UnsupportedValue> { - public UnsupportedColumnInfo(Column column, int complexId, TableImpl typeObjTable, + public UnsupportedColumnInfo(ColumnImpl column, int complexId, TableImpl typeObjTable, TableImpl flatTable) throws IOException { super(column, complexId, typeObjTable, flatTable); } - public List<Column> getValueColumns() { + public List<ColumnImpl> getValueColumns() { return getTypeColumns(); } @@ -60,7 +60,7 @@ public class UnsupportedColumnInfo extends ComplexColumnInfo<UnsupportedValue> int id = (Integer)getPrimaryKeyColumn().getRowValue(rawValue); Map<String,Object> values = new LinkedHashMap<String,Object>(); - for(Column col : getValueColumns()) { + for(ColumnImpl col : getValueColumns()) { col.setRowValue(values, col.getRowValue(rawValue)); } @@ -72,7 +72,7 @@ public class UnsupportedColumnInfo extends ComplexColumnInfo<UnsupportedValue> super.asRow(row, value); Map<String,Object> values = value.getValues(); - for(Column col : getValueColumns()) { + for(ColumnImpl col : getValueColumns()) { col.setRowValue(row, col.getRowValue(values)); } diff --git a/src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java index dedcb53..1ef3d09 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java +++ b/src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java @@ -25,7 +25,7 @@ import java.util.Date; import java.util.List; import java.util.Map; -import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.ColumnImpl; import com.healthmarketscience.jackcess.TableImpl; /** @@ -41,18 +41,18 @@ import com.healthmarketscience.jackcess.TableImpl; */ public class VersionHistoryColumnInfo extends ComplexColumnInfo<Version> { - private final Column _valueCol; - private final Column _modifiedCol; + private final ColumnImpl _valueCol; + private final ColumnImpl _modifiedCol; - public VersionHistoryColumnInfo(Column column, int complexId, + public VersionHistoryColumnInfo(ColumnImpl column, int complexId, TableImpl typeObjTable, TableImpl flatTable) throws IOException { super(column, complexId, typeObjTable, flatTable); - Column valueCol = null; - Column modifiedCol = null; - for(Column col : getTypeColumns()) { + ColumnImpl valueCol = null; + ColumnImpl modifiedCol = null; + for(ColumnImpl col : getTypeColumns()) { switch(col.getType()) { case SHORT_DATE_TIME: modifiedCol = col; @@ -75,16 +75,16 @@ public class VersionHistoryColumnInfo extends ComplexColumnInfo<Version> // link up with the actual versioned column. it should have the same name // as the "value" column in the type table. - Column versionedCol = getColumn().getTable().getColumn( + ColumnImpl versionedCol = getColumn().getTable().getColumn( getValueColumn().getName()); versionedCol.setVersionHistoryColumn(getColumn()); } - public Column getValueColumn() { + public ColumnImpl getValueColumn() { return _valueCol; } - public Column getModifiedDateColumn() { + public ColumnImpl getModifiedDateColumn() { return _modifiedCol; } @@ -154,7 +154,7 @@ public class VersionHistoryColumnInfo extends ComplexColumnInfo<Version> public static boolean isVersionHistoryColumn(TableImpl typeObjTable) { // version history data has these columns <value>(MEMO), // <modified>(SHORT_DATE_TIME) - List<Column> typeCols = typeObjTable.getColumns(); + List<ColumnImpl> typeCols = typeObjTable.getColumns(); if(typeCols.size() < 2) { return false; } @@ -162,7 +162,7 @@ public class VersionHistoryColumnInfo extends ComplexColumnInfo<Version> int numMemo = 0; int numDate = 0; - for(Column col : typeCols) { + for(ColumnImpl col : typeCols) { switch(col.getType()) { case SHORT_DATE_TIME: ++numDate; |