0001: /*
0002: Copyright (c) 2005 Health Market Science, Inc.
0003:
0004: This library is free software; you can redistribute it and/or
0005: modify it under the terms of the GNU Lesser General Public
0006: License as published by the Free Software Foundation; either
0007: version 2.1 of the License, or (at your option) any later version.
0008:
0009: This library is distributed in the hope that it will be useful,
0010: but WITHOUT ANY WARRANTY; without even the implied warranty of
0011: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
0012: Lesser General Public License for more details.
0013:
0014: You should have received a copy of the GNU Lesser General Public
0015: License along with this library; if not, write to the Free Software
0016: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
0017: USA
0018:
0019: You can contact Health Market Science at info@healthmarketscience.com
0020: or at the following address:
0021:
0022: Health Market Science
0023: 2700 Horizon Drive
0024: Suite 200
0025: King of Prussia, PA 19406
0026: */
0027:
0028: package com.healthmarketscience.jackcess;
0029:
0030: import java.io.IOException;
0031: import java.io.ObjectStreamException;
0032: import java.math.BigDecimal;
0033: import java.math.BigInteger;
0034: import java.nio.ByteBuffer;
0035: import java.nio.ByteOrder;
0036: import java.nio.CharBuffer;
0037: import java.sql.SQLException;
0038: import java.util.Calendar;
0039: import java.util.Date;
0040: import java.util.List;
0041: import java.util.regex.Matcher;
0042: import java.util.regex.Pattern;
0043:
0044: import com.healthmarketscience.jackcess.scsu.EndOfInputException;
0045: import com.healthmarketscience.jackcess.scsu.Expand;
0046: import com.healthmarketscience.jackcess.scsu.IllegalInputException;
0047: import org.apache.commons.logging.Log;
0048: import org.apache.commons.logging.LogFactory;
0049:
0050: /**
0051: * Access database column definition
0052: * @author Tim McCune
0053: */
0054: public class Column implements Comparable<Column> {
0055:
0056: private static final Log LOG = LogFactory.getLog(Column.class);
0057:
0058: /**
0059: * Meaningless placeholder object for inserting values in an autonumber
0060: * column. it is not required that this value be used (any passed in value
0061: * is ignored), but using this placeholder may make code more obvious.
0062: */
0063: public static final Object AUTO_NUMBER = "<AUTO_NUMBER>";
0064:
0065: /**
0066: * Access stores numeric dates in days. Java stores them in milliseconds.
0067: */
0068: private static final double MILLISECONDS_PER_DAY = (24L * 60L * 60L * 1000L);
0069:
0070: /**
0071: * Access starts counting dates at Jan 1, 1900. Java starts counting
0072: * at Jan 1, 1970. This is the # of millis between them for conversion.
0073: */
0074: private static final long MILLIS_BETWEEN_EPOCH_AND_1900 = 25569L * (long) MILLISECONDS_PER_DAY;
0075:
0076: /**
0077: * Long value (LVAL) type that indicates that the value is stored on the same page
0078: */
0079: private static final byte LONG_VALUE_TYPE_THIS_PAGE = (byte) 0x80;
0080: /**
0081: * Long value (LVAL) type that indicates that the value is stored on another page
0082: */
0083: private static final byte LONG_VALUE_TYPE_OTHER_PAGE = (byte) 0x40;
0084: /**
0085: * Long value (LVAL) type that indicates that the value is stored on multiple other pages
0086: */
0087: private static final byte LONG_VALUE_TYPE_OTHER_PAGES = (byte) 0x00;
0088:
0089: /** mask for the fixed len bit */
0090: public static final byte FIXED_LEN_FLAG_MASK = (byte) 0x01;
0091:
0092: /** mask for the auto number bit */
0093: public static final byte AUTO_NUMBER_FLAG_MASK = (byte) 0x04;
0094:
0095: /** mask for the unknown bit */
0096: public static final byte UNKNOWN_FLAG_MASK = (byte) 0x02;
0097:
0098: private static final Pattern GUID_PATTERN = Pattern
0099: .compile("\\s*[{]?([\\p{XDigit}]{8})-([\\p{XDigit}]{4})-([\\p{XDigit}]{4})-([\\p{XDigit}]{4})-([\\p{XDigit}]{12})[}]?\\s*");
0100:
0101: /** owning table */
0102: private final Table _table;
0103: /** For text columns, whether or not they are compressed */
0104: private boolean _compressedUnicode = false;
0105: /** Whether or not the column is of variable length */
0106: private boolean _variableLength;
0107: /** Whether or not the column is an autonumber column */
0108: private boolean _autoNumber;
0109: /** Numeric precision */
0110: private byte _precision;
0111: /** Numeric scale */
0112: private byte _scale;
0113: /** Data type */
0114: private DataType _type;
0115: /** Maximum column length */
0116: private short _columnLength;
0117: /** 0-based column number */
0118: private short _columnNumber;
0119: /** index of the data for this column within a list of row data */
0120: private int _columnIndex;
0121: /** Column name */
0122: private String _name;
0123: /** the offset of the fixed data in the row */
0124: private int _fixedDataOffset;
0125: /** the index of the variable length data in the var len offset table */
0126: private int _varLenTableIndex;
0127:
0128: public Column() {
0129: this (JetFormat.VERSION_4);
0130: }
0131:
0132: public Column(JetFormat format) {
0133: _table = null;
0134: }
0135:
0136: /**
0137: * Only used by unit tests
0138: */
0139: Column(boolean testing) {
0140: if (!testing) {
0141: throw new IllegalArgumentException();
0142: }
0143: _table = null;
0144: }
0145:
0146: /**
0147: * Read a column definition in from a buffer
0148: * @param table owning table
0149: * @param buffer Buffer containing column definition
0150: * @param offset Offset in the buffer at which the column definition starts
0151: */
0152: public Column(Table table, ByteBuffer buffer, int offset)
0153: throws IOException {
0154: _table = table;
0155: if (LOG.isDebugEnabled()) {
0156: LOG.debug("Column def block:\n"
0157: + ByteUtil.toHexString(buffer, offset, 25));
0158: }
0159: setType(DataType.fromByte(buffer.get(offset
0160: + getFormat().OFFSET_COLUMN_TYPE)));
0161: _columnNumber = buffer.getShort(offset
0162: + getFormat().OFFSET_COLUMN_NUMBER);
0163: _columnLength = buffer.getShort(offset
0164: + getFormat().OFFSET_COLUMN_LENGTH);
0165: if (_type.getHasScalePrecision()) {
0166: _precision = buffer.get(offset
0167: + getFormat().OFFSET_COLUMN_PRECISION);
0168: _scale = buffer.get(offset
0169: + getFormat().OFFSET_COLUMN_SCALE);
0170: }
0171: byte flags = buffer.get(offset
0172: + getFormat().OFFSET_COLUMN_FLAGS);
0173: _variableLength = ((flags & FIXED_LEN_FLAG_MASK) == 0);
0174: _autoNumber = ((flags & AUTO_NUMBER_FLAG_MASK) != 0);
0175: _compressedUnicode = ((buffer.get(offset
0176: + getFormat().OFFSET_COLUMN_COMPRESSED_UNICODE) & 1) == 1);
0177:
0178: if (_variableLength) {
0179: _varLenTableIndex = buffer.getShort(offset
0180: + getFormat().OFFSET_COLUMN_VARIABLE_TABLE_INDEX);
0181: } else {
0182: _fixedDataOffset = buffer.getShort(offset
0183: + getFormat().OFFSET_COLUMN_FIXED_DATA_OFFSET);
0184: }
0185: }
0186:
0187: public Table getTable() {
0188: return _table;
0189: }
0190:
0191: public JetFormat getFormat() {
0192: return getTable().getFormat();
0193: }
0194:
0195: public PageChannel getPageChannel() {
0196: return getTable().getPageChannel();
0197: }
0198:
0199: public String getName() {
0200: return _name;
0201: }
0202:
0203: public void setName(String name) {
0204: _name = name;
0205: }
0206:
0207: public boolean isVariableLength() {
0208: return _variableLength;
0209: }
0210:
0211: public void setVariableLength(boolean variableLength) {
0212: _variableLength = variableLength;
0213: }
0214:
0215: public boolean isAutoNumber() {
0216: return _autoNumber;
0217: }
0218:
0219: public void setAutoNumber(boolean autoNumber) {
0220: _autoNumber = autoNumber;
0221: }
0222:
0223: public short getColumnNumber() {
0224: return _columnNumber;
0225: }
0226:
0227: public void setColumnNumber(short newColumnNumber) {
0228: _columnNumber = newColumnNumber;
0229: }
0230:
0231: public int getColumnIndex() {
0232: return _columnIndex;
0233: }
0234:
0235: public void setColumnIndex(int newColumnIndex) {
0236: _columnIndex = newColumnIndex;
0237: }
0238:
0239: /**
0240: * Also sets the length and the variable length flag, inferred from the
0241: * type. For types with scale/precision, sets the scale and precision to
0242: * default values.
0243: */
0244: public void setType(DataType type) {
0245: _type = type;
0246: if (!type.isVariableLength()) {
0247: setLength((short) type.getFixedSize());
0248: } else if (!type.isLongValue()) {
0249: setLength((short) type.getDefaultSize());
0250: }
0251: setVariableLength(type.isVariableLength());
0252: if (type.getHasScalePrecision()) {
0253: setScale((byte) type.getDefaultScale());
0254: setPrecision((byte) type.getDefaultPrecision());
0255: }
0256: }
0257:
0258: public DataType getType() {
0259: return _type;
0260: }
0261:
0262: public int getSQLType() throws SQLException {
0263: return _type.getSQLType();
0264: }
0265:
0266: public void setSQLType(int type) throws SQLException {
0267: setSQLType(type, 0);
0268: }
0269:
0270: public void setSQLType(int type, int lengthInUnits)
0271: throws SQLException {
0272: setType(DataType.fromSQLType(type, lengthInUnits));
0273: }
0274:
0275: public boolean isCompressedUnicode() {
0276: return _compressedUnicode;
0277: }
0278:
0279: public byte getPrecision() {
0280: return _precision;
0281: }
0282:
0283: public void setPrecision(byte newPrecision) {
0284: _precision = newPrecision;
0285: }
0286:
0287: public byte getScale() {
0288: return _scale;
0289: }
0290:
0291: public void setScale(byte newScale) {
0292: _scale = newScale;
0293: }
0294:
0295: public void setLength(short length) {
0296: _columnLength = length;
0297: }
0298:
0299: public short getLength() {
0300: return _columnLength;
0301: }
0302:
0303: public void setLengthInUnits(short unitLength) {
0304: setLength((short) (getType().getUnitSize() * unitLength));
0305: }
0306:
0307: public short getLengthInUnits() {
0308: return (short) (getLength() / getType().getUnitSize());
0309: }
0310:
0311: public void setVarLenTableIndex(int idx) {
0312: _varLenTableIndex = idx;
0313: }
0314:
0315: public int getVarLenTableIndex() {
0316: return _varLenTableIndex;
0317: }
0318:
0319: public void setFixedDataOffset(int newOffset) {
0320: _fixedDataOffset = newOffset;
0321: }
0322:
0323: public int getFixedDataOffset() {
0324: return _fixedDataOffset;
0325: }
0326:
0327: /**
0328: * Checks that this column definition is valid.
0329: *
0330: * @throws IllegalArgumentException if this column definition is invalid.
0331: */
0332: public void validate(JetFormat format) {
0333: if (getType() == null) {
0334: throw new IllegalArgumentException("must have type");
0335: }
0336: if ((getName() == null) || (getName().trim().length() == 0)) {
0337: throw new IllegalArgumentException("must have valid name");
0338: }
0339: if (isVariableLength() != getType().isVariableLength()) {
0340: throw new IllegalArgumentException(
0341: "invalid variable length setting");
0342: }
0343:
0344: if (!isVariableLength()) {
0345: if (getLength() != getType().getFixedSize()) {
0346: throw new IllegalArgumentException(
0347: "invalid fixed length size");
0348: }
0349: } else if (!getType().isLongValue()) {
0350: if (!getType().isValidSize(getLength())) {
0351: throw new IllegalArgumentException(
0352: "var length out of range");
0353: }
0354: }
0355:
0356: if (getType().getHasScalePrecision()) {
0357: if (!getType().isValidScale(getScale())) {
0358: throw new IllegalArgumentException(
0359: "Scale must be from " + getType().getMinScale()
0360: + " to " + getType().getMaxScale()
0361: + " inclusive");
0362: }
0363: if (!getType().isValidPrecision(getPrecision())) {
0364: throw new IllegalArgumentException(
0365: "Precision must be from "
0366: + getType().getMinPrecision() + " to "
0367: + getType().getMaxPrecision()
0368: + " inclusive");
0369: }
0370: }
0371:
0372: if (isAutoNumber()) {
0373: if (getType() != DataType.LONG) {
0374: throw new IllegalArgumentException(
0375: "Auto number column must be long integer");
0376: }
0377: }
0378: }
0379:
0380: /**
0381: * Deserialize a raw byte value for this column into an Object
0382: * @param data The raw byte value
0383: * @return The deserialized Object
0384: */
0385: public Object read(byte[] data) throws IOException {
0386: return read(data, ByteOrder.LITTLE_ENDIAN);
0387: }
0388:
0389: /**
0390: * Deserialize a raw byte value for this column into an Object
0391: * @param data The raw byte value
0392: * @param order Byte order in which the raw value is stored
0393: * @return The deserialized Object
0394: */
0395: public Object read(byte[] data, ByteOrder order) throws IOException {
0396: ByteBuffer buffer = ByteBuffer.wrap(data);
0397: buffer.order(order);
0398: if (_type == DataType.BOOLEAN) {
0399: throw new IOException(
0400: "Tried to read a boolean from data instead of null mask.");
0401: } else if (_type == DataType.BYTE) {
0402: return Byte.valueOf(buffer.get());
0403: } else if (_type == DataType.INT) {
0404: return Short.valueOf(buffer.getShort());
0405: } else if (_type == DataType.LONG) {
0406: return Integer.valueOf(buffer.getInt());
0407: } else if (_type == DataType.DOUBLE) {
0408: return Double.valueOf(buffer.getDouble());
0409: } else if (_type == DataType.FLOAT) {
0410: return Float.valueOf(buffer.getFloat());
0411: } else if (_type == DataType.SHORT_DATE_TIME) {
0412: return readDateValue(buffer);
0413: } else if (_type == DataType.BINARY) {
0414: return data;
0415: } else if (_type == DataType.TEXT) {
0416: return decodeTextValue(data);
0417: } else if (_type == DataType.MONEY) {
0418: return readCurrencyValue(buffer);
0419: } else if (_type == DataType.OLE) {
0420: if (data.length > 0) {
0421: return readLongValue(data);
0422: }
0423: return null;
0424: } else if (_type == DataType.MEMO) {
0425: if (data.length > 0) {
0426: return readLongStringValue(data);
0427: }
0428: return null;
0429: } else if (_type == DataType.NUMERIC) {
0430: return readNumericValue(buffer);
0431: } else if (_type == DataType.GUID) {
0432: return readGUIDValue(buffer);
0433: } else if (_type == DataType.UNKNOWN_0D) {
0434: return null;
0435: } else {
0436: throw new IOException("Unrecognized data type: " + _type);
0437: }
0438: }
0439:
0440: /**
0441: * @param lvalDefinition Column value that points to an LVAL record
0442: * @return The LVAL data
0443: */
0444: private byte[] readLongValue(byte[] lvalDefinition)
0445: throws IOException {
0446: ByteBuffer def = ByteBuffer.wrap(lvalDefinition);
0447: def.order(ByteOrder.LITTLE_ENDIAN);
0448: int length = ByteUtil.get3ByteInt(def);
0449: // bail out gracefully here as we don't understand the format
0450: if (length < 0) {
0451: return null;
0452: }
0453: byte[] rtn = new byte[length];
0454: byte type = def.get();
0455:
0456: if (type == LONG_VALUE_TYPE_THIS_PAGE) {
0457:
0458: // inline long value
0459: def.getInt(); //Skip over lval_dp
0460: def.getInt(); //Skip over unknown
0461: def.get(rtn);
0462:
0463: } else {
0464:
0465: // long value on other page(s)
0466: if (lvalDefinition.length != getFormat().SIZE_LONG_VALUE_DEF) {
0467: throw new IOException("Expected "
0468: + getFormat().SIZE_LONG_VALUE_DEF
0469: + " bytes in long value definition, but found "
0470: + lvalDefinition.length);
0471: }
0472:
0473: int rowNum = ByteUtil.getUnsignedByte(def);
0474: int pageNum = ByteUtil.get3ByteInt(def, def.position());
0475: ByteBuffer lvalPage = getPageChannel().createPageBuffer();
0476:
0477: switch (type) {
0478: case LONG_VALUE_TYPE_OTHER_PAGE: {
0479: getPageChannel().readPage(lvalPage, pageNum);
0480:
0481: short rowStart = Table.findRowStart(lvalPage, rowNum,
0482: getFormat());
0483: short rowEnd = Table.findRowEnd(lvalPage, rowNum,
0484: getFormat());
0485:
0486: if ((rowEnd - rowStart) != length) {
0487: throw new IOException("Unexpected lval row length");
0488: }
0489:
0490: lvalPage.position(rowStart);
0491: lvalPage.get(rtn);
0492: }
0493: break;
0494:
0495: case LONG_VALUE_TYPE_OTHER_PAGES:
0496:
0497: ByteBuffer rtnBuf = ByteBuffer.wrap(rtn);
0498: int remainingLen = length;
0499: while (remainingLen > 0) {
0500: lvalPage.clear();
0501: getPageChannel().readPage(lvalPage, pageNum);
0502:
0503: short rowStart = Table.findRowStart(lvalPage,
0504: rowNum, getFormat());
0505: short rowEnd = Table.findRowEnd(lvalPage, rowNum,
0506: getFormat());
0507:
0508: // read next page information
0509: lvalPage.position(rowStart);
0510: rowNum = ByteUtil.getUnsignedByte(lvalPage);
0511: pageNum = ByteUtil.get3ByteInt(lvalPage);
0512:
0513: // update rowEnd and remainingLen based on chunkLength
0514: int chunkLength = (rowEnd - rowStart) - 4;
0515: if (chunkLength > remainingLen) {
0516: rowEnd = (short) (rowEnd - (chunkLength - remainingLen));
0517: chunkLength = remainingLen;
0518: }
0519: remainingLen -= chunkLength;
0520:
0521: lvalPage.limit(rowEnd);
0522: rtnBuf.put(lvalPage);
0523: }
0524:
0525: break;
0526:
0527: default:
0528: throw new IOException("Unrecognized long value type: "
0529: + type);
0530: }
0531: }
0532:
0533: return rtn;
0534: }
0535:
0536: /**
0537: * @param lvalDefinition Column value that points to an LVAL record
0538: * @return The LVAL data
0539: */
0540: private String readLongStringValue(byte[] lvalDefinition)
0541: throws IOException {
0542: byte[] binData = readLongValue(lvalDefinition);
0543: if (binData == null) {
0544: return null;
0545: }
0546: return decodeTextValue(binData);
0547: }
0548:
0549: /**
0550: * Decodes "Currency" values.
0551: *
0552: * @param buffer Column value that points to currency data
0553: * @return BigDecimal representing the monetary value
0554: * @throws IOException if the value cannot be parsed
0555: */
0556: private BigDecimal readCurrencyValue(ByteBuffer buffer)
0557: throws IOException {
0558: if (buffer.remaining() != 8) {
0559: throw new IOException("Invalid money value.");
0560: }
0561:
0562: return new BigDecimal(BigInteger.valueOf(buffer.getLong(0)), 4);
0563: }
0564:
0565: /**
0566: * Writes "Currency" values.
0567: */
0568: private void writeCurrencyValue(ByteBuffer buffer, Object value)
0569: throws IOException {
0570: try {
0571: BigDecimal decVal = toBigDecimal(value);
0572:
0573: // adjust scale (will cause the an ArithmeticException if number has too
0574: // many decimal places)
0575: decVal = decVal.setScale(4);
0576:
0577: // now, remove scale and convert to long (this will throw if the value is
0578: // too big)
0579: buffer.putLong(decVal.movePointRight(4).longValueExact());
0580: } catch (ArithmeticException e) {
0581: throw (IOException) new IOException(
0582: "Currency value out of range").initCause(e);
0583: }
0584: }
0585:
0586: /**
0587: * Decodes a NUMERIC field.
0588: */
0589: private BigDecimal readNumericValue(ByteBuffer buffer) {
0590: boolean negate = (buffer.get() != 0);
0591:
0592: byte[] tmpArr = new byte[16];
0593: buffer.get(tmpArr);
0594:
0595: if (buffer.order() != ByteOrder.BIG_ENDIAN) {
0596: fixNumericByteOrder(tmpArr);
0597: }
0598:
0599: BigInteger intVal = new BigInteger(tmpArr);
0600: if (negate) {
0601: intVal = intVal.negate();
0602: }
0603: return new BigDecimal(intVal, getScale());
0604: }
0605:
0606: /**
0607: * Writes a numeric value.
0608: */
0609: private void writeNumericValue(ByteBuffer buffer, Object value)
0610: throws IOException {
0611: try {
0612: BigDecimal decVal = toBigDecimal(value);
0613:
0614: boolean negative = (decVal.compareTo(BigDecimal.ZERO) < 0);
0615: if (negative) {
0616: decVal = decVal.negate();
0617: }
0618:
0619: // write sign byte
0620: buffer.put(negative ? (byte) 0x80 : (byte) 0);
0621:
0622: // adjust scale according to this column type (will cause the an
0623: // ArithmeticException if number has too many decimal places)
0624: decVal = decVal.setScale(getScale());
0625:
0626: // check precision
0627: if (decVal.precision() > getPrecision()) {
0628: throw new IOException(
0629: "Numeric value is too big for specified precision "
0630: + getPrecision() + ": " + decVal);
0631: }
0632:
0633: // convert to unscaled BigInteger, big-endian bytes
0634: byte[] intValBytes = decVal.unscaledValue().toByteArray();
0635: int maxByteLen = getType().getFixedSize() - 1;
0636: if (intValBytes.length > maxByteLen) {
0637: throw new IOException(
0638: "Too many bytes for valid BigInteger?");
0639: }
0640: if (intValBytes.length < maxByteLen) {
0641: byte[] tmpBytes = new byte[maxByteLen];
0642: System.arraycopy(intValBytes, 0, tmpBytes,
0643: (maxByteLen - intValBytes.length),
0644: intValBytes.length);
0645: intValBytes = tmpBytes;
0646: }
0647: if (buffer.order() != ByteOrder.BIG_ENDIAN) {
0648: fixNumericByteOrder(intValBytes);
0649: }
0650: buffer.put(intValBytes);
0651: } catch (ArithmeticException e) {
0652: throw (IOException) new IOException(
0653: "Numeric value out of range").initCause(e);
0654: }
0655: }
0656:
0657: /**
0658: * Decodes a date value.
0659: */
0660: private Date readDateValue(ByteBuffer buffer) {
0661: // seems access stores dates in the local timezone. guess you just hope
0662: // you read it in the same timezone in which it was written!
0663: long dateBits = buffer.getLong();
0664: long time = (long) (Double.longBitsToDouble(dateBits) * MILLISECONDS_PER_DAY);
0665: time -= MILLIS_BETWEEN_EPOCH_AND_1900;
0666: time -= getTimeZoneOffset(time);
0667: return new DateExt(time, dateBits);
0668: }
0669:
0670: /**
0671: * Writes a date value.
0672: */
0673: private void writeDateValue(ByteBuffer buffer, Object value) {
0674: if (value == null) {
0675: buffer.putDouble(0d);
0676: }
0677: if (value instanceof DateExt) {
0678:
0679: // this is a Date value previously read from readDateValue(). use the
0680: // original bits to store the value so we don't lose any precision
0681: buffer.putLong(((DateExt) value).getDateBits());
0682:
0683: } else {
0684:
0685: // seems access stores dates in the local timezone. guess you just
0686: // hope you read it in the same timezone in which it was written!
0687: long time = ((value instanceof Date) ? ((Date) value)
0688: .getTime() : ((Number) value).longValue());
0689: time += getTimeZoneOffset(time);
0690: time += MILLIS_BETWEEN_EPOCH_AND_1900;
0691: double dTime = time / MILLISECONDS_PER_DAY;
0692: buffer.putDouble(dTime);
0693: }
0694: }
0695:
0696: /**
0697: * Gets the timezone offset from UTC for the given time (including DST).
0698: */
0699: private static long getTimeZoneOffset(long time) {
0700: Calendar c = Calendar.getInstance();
0701: c.setTimeInMillis(time);
0702: return ((long) c.get(Calendar.ZONE_OFFSET) + c
0703: .get(Calendar.DST_OFFSET));
0704: }
0705:
0706: /**
0707: * Decodes a GUID value.
0708: */
0709: private String readGUIDValue(ByteBuffer buffer) {
0710: StringBuilder sb = new StringBuilder(22);
0711: sb.append("{");
0712: sb.append(ByteUtil.toHexString(buffer, 0, 4, false));
0713: sb.append("-");
0714: sb.append(ByteUtil.toHexString(buffer, 4, 2, false));
0715: sb.append("-");
0716: sb.append(ByteUtil.toHexString(buffer, 6, 2, false));
0717: sb.append("-");
0718: sb.append(ByteUtil.toHexString(buffer, 8, 2, false));
0719: sb.append("-");
0720: sb.append(ByteUtil.toHexString(buffer, 10, 6, false));
0721: sb.append("}");
0722: return (sb.toString());
0723: }
0724:
0725: /**
0726: * Writes a GUID value.
0727: */
0728: private void writeGUIDValue(ByteBuffer buffer, Object value)
0729: throws IOException {
0730: Matcher m = GUID_PATTERN.matcher(toCharSequence(value));
0731: if (m.matches()) {
0732: ByteUtil.writeHexString(buffer, m.group(1));
0733: ByteUtil.writeHexString(buffer, m.group(2));
0734: ByteUtil.writeHexString(buffer, m.group(3));
0735: ByteUtil.writeHexString(buffer, m.group(4));
0736: ByteUtil.writeHexString(buffer, m.group(5));
0737: } else {
0738: throw new IOException("Invalid GUID: " + value);
0739: }
0740: }
0741:
0742: /**
0743: * Write an LVAL column into a ByteBuffer inline if it fits, otherwise in
0744: * other data page(s).
0745: * @param value Value of the LVAL column
0746: * @return A buffer containing the LVAL definition and (possibly) the column
0747: * value (unless written to other pages)
0748: */
0749: public ByteBuffer writeLongValue(byte[] value,
0750: int remainingRowLength) throws IOException {
0751: if (value.length > getType().getMaxSize()) {
0752: throw new IOException("value too big for column");
0753: }
0754:
0755: // determine which type to write
0756: byte type = 0;
0757: int lvalDefLen = getFormat().SIZE_LONG_VALUE_DEF;
0758: if (((getFormat().SIZE_LONG_VALUE_DEF + value.length) <= remainingRowLength)
0759: && (value.length <= getFormat().MAX_INLINE_LONG_VALUE_SIZE)) {
0760: type = LONG_VALUE_TYPE_THIS_PAGE;
0761: lvalDefLen += value.length;
0762: } else if (Table.getRowSpaceUsage(value.length, getFormat()) <= getFormat().MAX_ROW_SIZE) {
0763: type = LONG_VALUE_TYPE_OTHER_PAGE;
0764: } else {
0765: type = LONG_VALUE_TYPE_OTHER_PAGES;
0766: }
0767:
0768: ByteBuffer def = getPageChannel().createBuffer(lvalDefLen);
0769: ByteUtil.put3ByteInt(def, value.length);
0770: def.put(type);
0771:
0772: if (type == LONG_VALUE_TYPE_THIS_PAGE) {
0773: // write long value inline
0774: def.putInt(0);
0775: def.putInt(0); //Unknown
0776: def.put(value);
0777: } else {
0778:
0779: int firstLvalPageNum = PageChannel.INVALID_PAGE_NUMBER;
0780: byte firstLvalRow = 0;
0781:
0782: ByteBuffer lvalPage = getPageChannel().createPageBuffer();
0783:
0784: // write other page(s)
0785: switch (type) {
0786: case LONG_VALUE_TYPE_OTHER_PAGE:
0787: writeLongValueHeader(lvalPage);
0788: firstLvalRow = (byte) Table.addDataPageRow(lvalPage,
0789: value.length, getFormat());
0790: lvalPage.put(value);
0791: firstLvalPageNum = getPageChannel().writeNewPage(
0792: lvalPage);
0793: break;
0794:
0795: case LONG_VALUE_TYPE_OTHER_PAGES:
0796:
0797: ByteBuffer buffer = ByteBuffer.wrap(value);
0798: int remainingLen = buffer.remaining();
0799: buffer.limit(0);
0800: int lvalPageNum = getPageChannel().allocateNewPage();
0801: byte lvalRow = 0;
0802: int nextLvalPageNum = 0;
0803: while (remainingLen > 0) {
0804: lvalPage.clear();
0805: writeLongValueHeader(lvalPage);
0806:
0807: // figure out how much we will put in this page
0808: int chunkLength = Math.min(
0809: getFormat().MAX_ROW_SIZE - 4, remainingLen);
0810: nextLvalPageNum = ((chunkLength < remainingLen) ? getPageChannel()
0811: .allocateNewPage()
0812: : 0);
0813:
0814: // add row to this page
0815: lvalRow = (byte) Table.addDataPageRow(lvalPage,
0816: chunkLength + 4, getFormat());
0817:
0818: // write next page info (we'll always be writing into row 0 for
0819: // newly created pages)
0820: lvalPage.put((byte) 0); // row number
0821: ByteUtil.put3ByteInt(lvalPage, nextLvalPageNum); // page number
0822:
0823: // write this page's chunk of data
0824: buffer.limit(buffer.limit() + chunkLength);
0825: lvalPage.put(buffer);
0826: remainingLen -= chunkLength;
0827:
0828: // write new page to database
0829: getPageChannel().writePage(lvalPage, lvalPageNum);
0830:
0831: // hang onto first page info
0832: if (firstLvalPageNum == PageChannel.INVALID_PAGE_NUMBER) {
0833: firstLvalPageNum = lvalPageNum;
0834: firstLvalRow = lvalRow;
0835: }
0836:
0837: // move to next page
0838: lvalPageNum = nextLvalPageNum;
0839: }
0840: break;
0841:
0842: default:
0843: throw new IOException("Unrecognized long value type: "
0844: + type);
0845: }
0846:
0847: // update def
0848: def.put(firstLvalRow);
0849: ByteUtil.put3ByteInt(def, firstLvalPageNum);
0850: def.putInt(0); //Unknown
0851:
0852: }
0853:
0854: def.flip();
0855: return def;
0856: }
0857:
0858: /**
0859: * Writes the header info for a long value page.
0860: */
0861: private void writeLongValueHeader(ByteBuffer lvalPage) {
0862: lvalPage.put(PageTypes.DATA); //Page type
0863: lvalPage.put((byte) 1); //Unknown
0864: lvalPage
0865: .putShort((short) (getFormat().PAGE_SIZE - getFormat().OFFSET_ROW_START)); //Free space
0866: lvalPage.put((byte) 'L');
0867: lvalPage.put((byte) 'V');
0868: lvalPage.put((byte) 'A');
0869: lvalPage.put((byte) 'L');
0870: lvalPage.putShort((short) 0); // num rows in page
0871: lvalPage.putInt(0); //unknown
0872: }
0873:
0874: /**
0875: * Serialize an Object into a raw byte value for this column in little endian order
0876: * @param obj Object to serialize
0877: * @return A buffer containing the bytes
0878: */
0879: public ByteBuffer write(Object obj, int remainingRowLength)
0880: throws IOException {
0881: return write(obj, remainingRowLength, ByteOrder.LITTLE_ENDIAN);
0882: }
0883:
0884: /**
0885: * Serialize an Object into a raw byte value for this column
0886: * @param obj Object to serialize
0887: * @param order Order in which to serialize
0888: * @return A buffer containing the bytes
0889: */
0890: public ByteBuffer write(Object obj, int remainingRowLength,
0891: ByteOrder order) throws IOException {
0892: if (!isVariableLength()) {
0893: return writeFixedLengthField(obj, order);
0894: }
0895:
0896: // var length column
0897: if (!getType().isLongValue()) {
0898:
0899: // this is an "inline" var length field
0900: switch (getType()) {
0901: case NUMERIC:
0902: // don't ask me why numerics are "var length" columns...
0903: ByteBuffer buffer = getPageChannel().createBuffer(
0904: getType().getFixedSize(), order);
0905: writeNumericValue(buffer, obj);
0906: buffer.flip();
0907: return buffer;
0908:
0909: case TEXT:
0910: CharSequence text = toCharSequence(obj);
0911: int maxChars = getLengthInUnits();
0912: if (text.length() > maxChars) {
0913: throw new IOException("Text is too big for column");
0914: }
0915: byte[] encodedData = encodeUncompressedText(text,
0916: getFormat()).array();
0917: obj = encodedData;
0918: break;
0919:
0920: case BINARY:
0921: // should already be "encoded"
0922: break;
0923: default:
0924: throw new RuntimeException(
0925: "unexpected inline var length type: "
0926: + getType());
0927: }
0928:
0929: ByteBuffer buffer = ByteBuffer.wrap((byte[]) obj);
0930: buffer.order(order);
0931: return buffer;
0932: }
0933:
0934: // var length, long value column
0935: switch (getType()) {
0936: case OLE:
0937: // should already be "encoded"
0938: break;
0939: case MEMO:
0940: obj = encodeUncompressedText(toCharSequence(obj),
0941: getFormat()).array();
0942: break;
0943: default:
0944: throw new RuntimeException(
0945: "unexpected var length, long value type: "
0946: + getType());
0947: }
0948:
0949: // create long value buffer
0950: return writeLongValue((byte[]) obj, remainingRowLength);
0951: }
0952:
0953: /**
0954: * Serialize an Object into a raw byte value for this column
0955: * @param obj Object to serialize
0956: * @param order Order in which to serialize
0957: * @return A buffer containing the bytes
0958: */
0959: public ByteBuffer writeFixedLengthField(Object obj, ByteOrder order)
0960: throws IOException {
0961: int size = getType().getFixedSize();
0962:
0963: // create buffer for data
0964: ByteBuffer buffer = getPageChannel().createBuffer(size, order);
0965:
0966: // since booleans are not written by this method, it's safe to convert any
0967: // incoming boolean into an integer.
0968: obj = booleanToInteger(obj);
0969:
0970: switch (getType()) {
0971: case BOOLEAN:
0972: //Do nothing
0973: break;
0974: case BYTE:
0975: buffer.put(toNumber(obj).byteValue());
0976: break;
0977: case INT:
0978: buffer.putShort(toNumber(obj).shortValue());
0979: break;
0980: case LONG:
0981: buffer.putInt(toNumber(obj).intValue());
0982: break;
0983: case DOUBLE:
0984: buffer.putDouble(toNumber(obj).doubleValue());
0985: break;
0986: case FLOAT:
0987: buffer.putFloat(toNumber(obj).floatValue());
0988: break;
0989: case SHORT_DATE_TIME:
0990: writeDateValue(buffer, obj);
0991: break;
0992: case MONEY:
0993: writeCurrencyValue(buffer, obj);
0994: break;
0995: case GUID:
0996: writeGUIDValue(buffer, obj);
0997: break;
0998: case NUMERIC:
0999: // yes, that's right, occasionally numeric values are written as fixed
1000: // length...
1001: writeNumericValue(buffer, obj);
1002: break;
1003: default:
1004: throw new IOException("Unsupported data type: " + getType());
1005: }
1006: buffer.flip();
1007: return buffer;
1008: }
1009:
1010: /**
1011: * Decodes a compressed or uncompressed text value.
1012: */
1013: private String decodeTextValue(byte[] data) throws IOException {
1014: try {
1015:
1016: // see if data is compressed. the 0xFF, 0xFE sequence indicates that
1017: // compression is used (sort of, see algorithm below)
1018: boolean isCompressed = ((data.length > 1)
1019: && (data[0] == (byte) 0xFF) && (data[1] == (byte) 0xFE));
1020: if (isCompressed) {
1021:
1022: Expand expander = new Expand();
1023:
1024: // this is a whacky compression combo that switches back and forth
1025: // between compressed/uncompressed using a 0x00 byte (starting in
1026: // compressed mode)
1027: StringBuilder textBuf = new StringBuilder(data.length);
1028: // start after two bytes indicating compression use
1029: int dataStart = 2;
1030: int dataEnd = dataStart;
1031: boolean inCompressedMode = true;
1032: while (dataEnd < data.length) {
1033: if (data[dataEnd] == (byte) 0x00) {
1034:
1035: // handle current segment
1036: decodeTextSegment(data, dataStart, dataEnd,
1037: inCompressedMode, expander, textBuf);
1038: inCompressedMode = !inCompressedMode;
1039: ++dataEnd;
1040: dataStart = dataEnd;
1041:
1042: } else {
1043: ++dataEnd;
1044: }
1045: }
1046: // handle last segment
1047: decodeTextSegment(data, dataStart, dataEnd,
1048: inCompressedMode, expander, textBuf);
1049:
1050: return textBuf.toString();
1051:
1052: }
1053:
1054: return decodeUncompressedText(data, getFormat());
1055:
1056: } catch (IllegalInputException e) {
1057: throw (IOException) new IOException(
1058: "Can't expand text column").initCause(e);
1059: } catch (EndOfInputException e) {
1060: throw (IOException) new IOException(
1061: "Can't expand text column").initCause(e);
1062: }
1063: }
1064:
1065: /**
1066: * Decodes a segnment of a text value into the given buffer according to the
1067: * given status of the segment (compressed/uncompressed).
1068: */
1069: private void decodeTextSegment(byte[] data, int dataStart,
1070: int dataEnd, boolean inCompressedMode, Expand expander,
1071: StringBuilder textBuf) throws IllegalInputException,
1072: EndOfInputException {
1073: if (dataEnd <= dataStart) {
1074: // no data
1075: return;
1076: }
1077: int dataLength = dataEnd - dataStart;
1078: if (inCompressedMode) {
1079: // handle compressed data
1080: byte[] tmpData = new byte[dataLength];
1081: System.arraycopy(data, dataStart, tmpData, 0, dataLength);
1082: expander.reset();
1083: textBuf.append(expander.expand(tmpData));
1084: } else {
1085: // handle uncompressed data
1086: textBuf.append(decodeUncompressedText(data, dataStart,
1087: dataLength, getFormat()));
1088: }
1089: }
1090:
1091: /**
1092: * @param textBytes bytes of text to decode
1093: * @return the decoded string
1094: */
1095: private static CharBuffer decodeUncompressedText(byte[] textBytes,
1096: int startPos, int length, JetFormat format) {
1097: return format.CHARSET.decode(ByteBuffer.wrap(textBytes,
1098: startPos, length));
1099: }
1100:
1101: @Override
1102: public String toString() {
1103: StringBuilder rtn = new StringBuilder();
1104: rtn.append("\tName: (" + _table.getName() + ") " + _name);
1105: rtn.append("\n\tType: 0x"
1106: + Integer.toHexString(_type.getValue()) + " (" + _type
1107: + ")");
1108: rtn.append("\n\tNumber: " + _columnNumber);
1109: rtn.append("\n\tLength: " + _columnLength);
1110: rtn.append("\n\tVariable length: " + _variableLength);
1111: if (_variableLength) {
1112: rtn.append("\n\tCompressed Unicode: " + _compressedUnicode);
1113: }
1114: if (_autoNumber) {
1115: rtn.append("\n\tNext AutoNumber: "
1116: + (_table.getLastAutoNumber() + 1));
1117: }
1118: rtn.append("\n\n");
1119: return rtn.toString();
1120: }
1121:
1122: /**
1123: * @param textBytes bytes of text to decode
1124: * @param format relevant db format
1125: * @return the decoded string
1126: */
1127: public static String decodeUncompressedText(byte[] textBytes,
1128: JetFormat format) {
1129: return decodeUncompressedText(textBytes, 0, textBytes.length,
1130: format).toString();
1131: }
1132:
1133: /**
1134: * @param text Text to encode
1135: * @param format relevant db format
1136: * @return A buffer with the text encoded
1137: */
1138: public static ByteBuffer encodeUncompressedText(CharSequence text,
1139: JetFormat format) {
1140: return format.CHARSET.encode(CharBuffer.wrap(text));
1141: }
1142:
1143: public int compareTo(Column other) {
1144: if (_columnNumber > other.getColumnNumber()) {
1145: return 1;
1146: } else if (_columnNumber < other.getColumnNumber()) {
1147: return -1;
1148: } else {
1149: return 0;
1150: }
1151: }
1152:
1153: /**
1154: * @param columns A list of columns in a table definition
1155: * @return The number of variable length columns found in the list
1156: */
1157: public static short countVariableLength(List<Column> columns) {
1158: short rtn = 0;
1159: for (Column col : columns) {
1160: if (col.isVariableLength()) {
1161: rtn++;
1162: }
1163: }
1164: return rtn;
1165: }
1166:
1167: /**
1168: * @param columns A list of columns in a table definition
1169: * @return The number of variable length columns which are not long values
1170: * found in the list
1171: */
1172: public static short countNonLongVariableLength(List<Column> columns) {
1173: short rtn = 0;
1174: for (Column col : columns) {
1175: if (col.isVariableLength() && !col.getType().isLongValue()) {
1176: rtn++;
1177: }
1178: }
1179: return rtn;
1180: }
1181:
1182: /**
1183: * @return an appropriate BigDecimal representation of the given object.
1184: * <code>null</code> is returned as 0 and Numbers are converted
1185: * using their double representation.
1186: */
1187: private static BigDecimal toBigDecimal(Object value) {
1188: if (value == null) {
1189: return BigDecimal.ZERO;
1190: } else if (value instanceof BigDecimal) {
1191: return (BigDecimal) value;
1192: } else if (value instanceof BigInteger) {
1193: return new BigDecimal((BigInteger) value);
1194: } else if (value instanceof Number) {
1195: return new BigDecimal(((Number) value).doubleValue());
1196: }
1197: return new BigDecimal(value.toString());
1198: }
1199:
1200: /**
1201: * @return an appropriate Number representation of the given object.
1202: * <code>null</code> is returned as 0 and Strings are parsed as
1203: * Doubles.
1204: */
1205: private static Number toNumber(Object value) {
1206: if (value == null) {
1207: return BigDecimal.ZERO;
1208: }
1209: if (value instanceof Number) {
1210: return (Number) value;
1211: }
1212: return Double.valueOf(value.toString());
1213: }
1214:
1215: /**
1216: * @return an appropriate CharSequence representation of the given object.
1217: */
1218: public static CharSequence toCharSequence(Object value) {
1219: if (value == null) {
1220: return null;
1221: } else if (value instanceof CharSequence) {
1222: return (CharSequence) value;
1223: }
1224: return value.toString();
1225: }
1226:
1227: /**
1228: * Interpret a boolean value (null == false)
1229: */
1230: public static boolean toBooleanValue(Object obj) {
1231: return ((obj != null) && ((Boolean) obj).booleanValue());
1232: }
1233:
1234: /**
1235: * Swaps the bytes of the given numeric in place.
1236: */
1237: private static void fixNumericByteOrder(byte[] bytes) {
1238: // fix endianness of each 4 byte segment
1239: for (int i = 0; i < 4; ++i) {
1240: int idx = i * 4;
1241: byte b = bytes[idx + 0];
1242: bytes[idx + 0] = bytes[idx + 3];
1243: bytes[idx + 3] = b;
1244: b = bytes[idx + 1];
1245: bytes[idx + 1] = bytes[idx + 2];
1246: bytes[idx + 2] = b;
1247: }
1248: }
1249:
1250: /**
1251: * Treat booleans as integers (C-style).
1252: */
1253: private static Object booleanToInteger(Object obj) {
1254: if (obj instanceof Boolean) {
1255: obj = ((Boolean) obj) ? 1 : 0;
1256: }
1257: return obj;
1258: }
1259:
1260: /**
1261: * Date subclass which stashes the original date bits, in case we attempt to
1262: * re-write the value (will not lose precision).
1263: */
1264: private static final class DateExt extends Date {
1265: private static final long serialVersionUID = 0L;
1266:
1267: /** cached bits of the original date value */
1268: private transient final long _dateBits;
1269:
1270: private DateExt(long time, long dateBits) {
1271: super (time);
1272: _dateBits = dateBits;
1273: }
1274:
1275: public long getDateBits() {
1276: return _dateBits;
1277: }
1278:
1279: private Object writeReplace() throws ObjectStreamException {
1280: // if we are going to serialize this Date, convert it back to a normal
1281: // Date (in case it is restored outside of the context of jackcess)
1282: return new Date(super.getTime());
1283: }
1284: }
1285:
1286: }
|