return readCurrencyValue(buffer);
} else if (_type == DataType.OLE) {
if (data.length > 0) {
- return readLongBinaryValue(data);
+ return readLongValue(data);
} else {
return null;
}
* <code>LONG_VALUE_TYPE_*</code>
* @return The LVAL data
*/
- private byte[] readLongBinaryValue(byte[] lvalDefinition)
+ private byte[] readLongValue(byte[] lvalDefinition)
throws IOException
{
ByteBuffer def = ByteBuffer.wrap(lvalDefinition);
}
byte[] rtn = new byte[length];
byte type = def.get();
- switch (type) {
- case LONG_VALUE_TYPE_OTHER_PAGE:
- if (lvalDefinition.length != _format.SIZE_LONG_VALUE_DEF) {
- throw new IOException("Expected " + _format.SIZE_LONG_VALUE_DEF +
- " bytes in long value definition, but found " +
- lvalDefinition.length);
- }
+ 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 != _format.SIZE_LONG_VALUE_DEF) {
+ throw new IOException("Expected " + _format.SIZE_LONG_VALUE_DEF +
+ " bytes in long value definition, but found " +
+ lvalDefinition.length);
+ }
+
+ byte rowNum = def.get();
+ int pageNum = ByteUtil.get3ByteInt(def, def.position());
+ ByteBuffer lvalPage = _pageChannel.createPageBuffer();
+
+ switch (type) {
+ case LONG_VALUE_TYPE_OTHER_PAGE:
{
- byte rowNum = def.get();
- int pageNum = ByteUtil.get3ByteInt(def, def.position());
- ByteBuffer lvalPage = _pageChannel.createPageBuffer();
_pageChannel.readPage(lvalPage, pageNum);
short rowStart = Table.findRowStart(lvalPage, rowNum, _format);
}
break;
- case LONG_VALUE_TYPE_THIS_PAGE:
- def.getInt(); //Skip over lval_dp
- def.getInt(); //Skip over unknown
- def.get(rtn);
- break;
-
case LONG_VALUE_TYPE_OTHER_PAGES:
ByteBuffer rtnBuf = ByteBuffer.wrap(rtn);
-
- if (lvalDefinition.length != _format.SIZE_LONG_VALUE_DEF) {
- throw new IOException("Expected " + _format.SIZE_LONG_VALUE_DEF +
- " bytes in long value definition, but found " +
- lvalDefinition.length);
- }
- byte rowNum = def.get();
- int pageNum = ByteUtil.get3ByteInt(def, def.position());
- ByteBuffer lvalPage = _pageChannel.createPageBuffer();
-
int remainingLen = length;
while(remainingLen > 0) {
lvalPage.clear();
short rowStart = Table.findRowStart(lvalPage, rowNum, _format);
short rowEnd = Table.findRowEnd(lvalPage, rowNum, _format);
-
// read next page information
lvalPage.position(rowStart);
default:
throw new IOException("Unrecognized long value type: " + type);
+ }
}
+
return rtn;
}
private String readLongStringValue(byte[] lvalDefinition)
throws IOException
{
- byte[] binData = readLongBinaryValue(lvalDefinition);
+ byte[] binData = readLongValue(lvalDefinition);
if(binData == null) {
return null;
}
}
/**
- * Write an LVAL column into a ByteBuffer inline (LONG_VALUE_TYPE_THIS_PAGE)
+ * 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 the column value
+ * @return A buffer containing the LVAL definition and (possibly) the column
+ * value (unless written to other pages)
*/
public ByteBuffer writeLongValue(byte[] value,
int remainingRowLength) throws IOException
{
- // FIXME, take remainingRowLength into account (don't always write inline)
-
if(value.length > getType().getMaxSize()) {
throw new IOException("value too big for column");
}
- ByteBuffer def = ByteBuffer.allocate(_format.SIZE_LONG_VALUE_DEF + value.length);
+
+ // determine which type to write
+ byte type = 0;
+ int lvalDefLen = _format.SIZE_LONG_VALUE_DEF;
+ if((_format.SIZE_LONG_VALUE_DEF + value.length) <= remainingRowLength) {
+ type = LONG_VALUE_TYPE_THIS_PAGE;
+ lvalDefLen += value.length;
+ } else if(Table.getRowSpaceUsage(value.length, _format) <=
+ _format.MAX_ROW_SIZE)
+ {
+ type = LONG_VALUE_TYPE_OTHER_PAGE;
+ } else {
+ type = LONG_VALUE_TYPE_OTHER_PAGES;
+ }
+
+ ByteBuffer def = ByteBuffer.allocate(lvalDefLen);
def.order(ByteOrder.LITTLE_ENDIAN);
ByteUtil.put3ByteInt(def, value.length);
- def.put(LONG_VALUE_TYPE_THIS_PAGE);
- def.putInt(0);
- def.putInt(0); //Unknown
- def.put(value);
+ def.put(type);
+
+ if(type == LONG_VALUE_TYPE_THIS_PAGE) {
+ // write long value inline
+ def.putInt(0);
+ def.putInt(0); //Unknown
+ def.put(value);
+ } else {
+
+ int firstLvalPageNum = PageChannel.INVALID_PAGE_NUMBER;
+ byte firstLvalRow = 0;
+
+ ByteBuffer lvalPage = _pageChannel.createPageBuffer();
+
+ // write other page(s)
+ switch(type) {
+ case LONG_VALUE_TYPE_OTHER_PAGE:
+ writeLongValueHeader(lvalPage);
+ firstLvalRow = (byte)Table.addDataPageRow(lvalPage,
+ value.length,
+ _format);
+ lvalPage.put(value);
+ firstLvalPageNum = _pageChannel.writeNewPage(lvalPage);
+ break;
+
+ case LONG_VALUE_TYPE_OTHER_PAGES:
+
+ ByteBuffer buffer = ByteBuffer.wrap(value);
+ int remainingLen = buffer.remaining();
+ buffer.limit(0);
+ int lvalPageNum = _pageChannel.allocateNewPage();
+ byte lvalRow = 0;
+ int nextLvalPageNum = 0;
+ while(remainingLen > 0) {
+ lvalPage.clear();
+ writeLongValueHeader(lvalPage);
+
+ // figure out how much we will put in this page
+ int chunkLength = Math.min(_format.MAX_ROW_SIZE - 4,
+ remainingLen);
+ nextLvalPageNum = ((chunkLength < remainingLen) ?
+ _pageChannel.allocateNewPage() : 0);
+
+ // add row to this page
+ lvalRow = (byte)Table.addDataPageRow(lvalPage, chunkLength + 4,
+ _format);
+
+ // 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
+ _pageChannel.writePage(lvalPage, lvalPageNum);
+
+ // hang onto first page info
+ if(firstLvalPageNum == PageChannel.INVALID_PAGE_NUMBER) {
+ firstLvalPageNum = lvalPageNum;
+ firstLvalRow = lvalRow;
+ }
+
+ // move to next page
+ 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;
+ return def;
}
-
+
/**
- * Write an LVAL column into a ByteBuffer on another page
- * (LONG_VALUE_TYPE_OTHER_PAGE)
- * @param value Value of the LVAL column
- * @return A buffer containing the LVAL definition
+ * Writes the header info for a long value page.
*/
- // FIXME, unused?
- private ByteBuffer writeLongValueInNewPage(byte[] value) throws IOException {
- ByteBuffer lvalPage = _pageChannel.createPageBuffer();
+ private void writeLongValueHeader(ByteBuffer lvalPage)
+ {
lvalPage.put(PageTypes.DATA); //Page type
lvalPage.put((byte) 1); //Unknown
lvalPage.putShort((short) (_format.PAGE_SIZE -
- _format.OFFSET_LVAL_ROW_LOCATION_BLOCK - _format.SIZE_ROW_LOCATION -
- value.length)); //Free space
+ _format.OFFSET_ROW_START)); //Free space
lvalPage.put((byte) 'L');
lvalPage.put((byte) 'V');
lvalPage.put((byte) 'A');
lvalPage.put((byte) 'L');
- int offset = _format.PAGE_SIZE - value.length;
- lvalPage.position(14);
- lvalPage.putShort((short) offset);
- lvalPage.position(offset);
- lvalPage.put(value);
- ByteBuffer def = ByteBuffer.allocate(_format.SIZE_LONG_VALUE_DEF);
- def.order(ByteOrder.LITTLE_ENDIAN);
- ByteUtil.put3ByteInt(def, value.length);
- def.put(LONG_VALUE_TYPE_OTHER_PAGE);
- def.put((byte) 0); //Row number
- ByteUtil.put3ByteInt(def, _pageChannel.writeNewPage(lvalPage)); //Page #
- def.putInt(0); //Unknown
- def.flip();
- return def;
+ lvalPage.putShort((short)0); // num rows in page
+ lvalPage.putInt(0); //unknown
}
/**
// var length column
if(!getType().isLongValue()) {
- // FIXME, take remainingRowLength into account? overflow pages?
-
// this is an "inline" var length field
switch(getType()) {
case NUMERIC:
}
for (int i = 0; i < rowData.length; i++) {
- rowSize = rowData[i].limit();
+ rowSize = rowData[i].remaining();
int rowSpaceUsage = getRowSpaceUsage(rowSize, _format);
short freeSpaceInPage = dataPage.getShort(_format.OFFSET_FREE_SPACE);
if (freeSpaceInPage < rowSpaceUsage) {
pageNumber = newDataPage(dataPage);
freeSpaceInPage = dataPage.getShort(_format.OFFSET_FREE_SPACE);
}
- //Decrease free space record.
- dataPage.putShort(_format.OFFSET_FREE_SPACE, (short) (freeSpaceInPage -
- rowSpaceUsage));
- //Increment row count record.
- short rowCount = dataPage.getShort(_format.OFFSET_NUM_ROWS_ON_DATA_PAGE);
- dataPage.putShort(_format.OFFSET_NUM_ROWS_ON_DATA_PAGE, (short) (rowCount + 1));
- short rowLocation = findRowEnd(dataPage, rowCount, _format);
- rowLocation -= rowSize;
- dataPage.putShort(getRowStartOffset(rowCount, _format), rowLocation);
- dataPage.position(rowLocation);
+
+ // write out the row data
+ int rowNum = addDataPageRow(dataPage, rowSize, _format);
dataPage.put(rowData[i]);
+
+ // update the indexes
Iterator<Index> indIter = _indexes.iterator();
while (indIter.hasNext()) {
Index index = (Index) indIter.next();
- index.addRow((Object[]) rows.get(i), pageNumber, (byte) rowCount);
+ index.addRow((Object[]) rows.get(i), pageNumber, (byte) rowNum);
}
}
writeDataPage(dataPage, pageNumber);
return rtn.toString();
}
+ /**
+ * Updates free space and row info for a new row of the given size in the
+ * given data page. Positions the page for writing the row data.
+ * @return the row number of the new row
+ */
+ public static int addDataPageRow(ByteBuffer dataPage,
+ int rowSize,
+ JetFormat format)
+ {
+ int rowSpaceUsage = getRowSpaceUsage(rowSize, format);
+
+ // Decrease free space record.
+ short freeSpaceInPage = dataPage.getShort(format.OFFSET_FREE_SPACE);
+ dataPage.putShort(format.OFFSET_FREE_SPACE, (short) (freeSpaceInPage -
+ rowSpaceUsage));
+
+ // Increment row count record.
+ short rowCount = dataPage.getShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE);
+ dataPage.putShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE,
+ (short) (rowCount + 1));
+
+ // determine row position
+ short rowLocation = findRowEnd(dataPage, rowCount, format);
+ rowLocation -= rowSize;
+
+ // write row position
+ dataPage.putShort(getRowStartOffset(rowCount, format), rowLocation);
+
+ // set position for row data
+ dataPage.position(rowLocation);
+
+ return rowCount;
+ }
+
public static short findRowStart(ByteBuffer buffer, int rowNum,
JetFormat format)
{
}
static Database create() throws Exception {
+ return create(false);
+ }
+
+ static Database create(boolean keep) throws Exception {
File tmp = File.createTempFile("databaseTest", ".mdb");
- tmp.deleteOnExit();
+ if(keep) {
+ System.out.println("Created " + tmp);
+ } else {
+ tmp.deleteOnExit();
+ }
return Database.create(tmp);
}
assertEquals(2, table.getNextRow().get("D"));
}
- public void testReadMemo() throws Exception {
+ public void testReadLongValue() throws Exception {
Database db = Database.open(new File("test/data/test2.mdb"));
Table table = db.getTable("MSP_PROJECTS");
assertEquals("T", row.get("PROJ_PROP_COMPANY"));
assertEquals("Standard", row.get("PROJ_INFO_CAL_NAME"));
assertEquals("Project1", row.get("PROJ_PROP_TITLE"));
+ byte[] foundBinaryData = (byte[])row.get("RESERVED_BINARY_DATA");
+ System.out.println("FOO found len " + foundBinaryData.length);
+ byte[] expectedBinaryData =
+ toByteArray(new File("test/data/test2BinData.dat"));
+ assertTrue(Arrays.equals(expectedBinaryData, foundBinaryData));
}
- public void testWriteMemo() throws Exception {
+ public void testWriteLongValue() throws Exception {
Database db = create();
col.setName("B");
col.setType(DataType.MEMO);
columns.add(col);
+ col = new Column();
+ col.setName("C");
+ col.setType(DataType.OLE);
+ columns.add(col);
db.createTable("test", columns);
String testStr = "This is a test";
+ StringBuilder strBuf = new StringBuilder();
+ for(int i = 0; i < 2030; ++i) {
+ char c = (char)('a' + (i % 26));
+ strBuf.append(c);
+ }
+ String longMemo = strBuf.toString();
+ byte[] oleValue = toByteArray(new File("test/data/test2BinData.dat"));
+
Table table = db.getTable("Test");
- table.addRow(new Object[]{testStr, testStr});
+ table.addRow(testStr, testStr, null);
+ table.addRow(testStr, longMemo, oleValue);
+
table.reset();
Map<String, Object> row = table.getNextRow();
assertEquals(testStr, row.get("A"));
assertEquals(testStr, row.get("B"));
+ assertNull(row.get("C"));
+
+ row = table.getNextRow();
+
+ assertEquals(testStr, row.get("A"));
+ assertEquals(longMemo, row.get("B"));
+ assertTrue(Arrays.equals(oleValue, (byte[])row.get("C")));
}
ostream.close();
}
}
+
+ static byte[] toByteArray(File file)
+ throws IOException
+ {
+ // FIXME should really be using commons io IOUtils here, but don't want
+ // to add dep for one simple test method
+ FileInputStream istream = new FileInputStream(file);
+ try {
+ byte[] bytes = new byte[(int)file.length()];
+ istream.read(bytes);
+ return bytes;
+ } finally {
+ istream.close();
+ }
+ }
}