</properties>
<body>
<release version="1.1.18" date="TBD">
+ <action dev="jahlborn" type="add">
+ Add ErrorHandler utility for customizing error handling during row
+ parsing.
+ </action>
<action dev="jahlborn" type="add">
Add RowFilter contributed by Patricia Donaldson.
</action>
/**
* Only used by unit tests
*/
- Column(boolean testing) {
+ Column(boolean testing, Table table) {
if(!testing) {
throw new IllegalArgumentException();
}
- _table = null;
+ _table = table;
}
/**
return getTable().getPageChannel();
}
+ /**
+ * Gets the currently configured ErrorHandler (always non-{@code null}).
+ * This will be used to handle all errors.
+ */
+ public ErrorHandler getErrorHandler() {
+ return _rowState.getErrorHandler();
+ }
+
+ /**
+ * Sets a new ErrorHandler. If {@code null}, resets to using the
+ * ErrorHandler configured at the Table level.
+ */
+ public void setErrorHandler(ErrorHandler newErrorHandler) {
+ _rowState.setErrorHandler(newErrorHandler);
+ }
+
/**
* Returns the current state of the cursor which can be restored at a future
* point in time by a call to {@link #restoreSavepoint}.
default. */
public static final String USE_BIG_INDEX_PROPERTY =
"com.healthmarketscience.jackcess.bigIndex";
+
+ /** default error handler used if none provided (just rethrows exception) */
+ public static final ErrorHandler DEFAULT_ERROR_HANDLER = new ErrorHandler() {
+ public Object handleRowError(Column column,
+ byte[] columnData,
+ Table.RowState rowState,
+ Exception error)
+ throws IOException
+ {
+ // really can only be RuntimeException or IOException
+ if(error instanceof IOException) {
+ throw (IOException)error;
+ }
+ throw (RuntimeException)error;
+ }
+ };
/** Batch commit size for copying other result sets into this database */
private static final int COPY_TABLE_BATCH_SIZE = 200;
private final List<byte[]> _newTableSIDs = new ArrayList<byte[]>();
/** for now, "big index support" is optional */
private boolean _useBigIndex;
+ /** optional error handler to use when row errors are encountered */
+ private ErrorHandler _dbErrorHandler;
/**
* Open an existing Database. If the existing file is not writeable, the
public void setUseBigIndex(boolean useBigIndex) {
_useBigIndex = useBigIndex;
}
+
+ /**
+ * Gets the currently configured ErrorHandler (always non-{@code null}).
+ * This will be used to handle all errors unless overridden at the Table or
+ * Cursor level.
+ */
+ public ErrorHandler getErrorHandler() {
+ return((_dbErrorHandler != null) ? _dbErrorHandler :
+ DEFAULT_ERROR_HANDLER);
+ }
+
+ /**
+ * Sets a new ErrorHandler. If {@code null}, resets to the
+ * {@link #DEFAULT_ERROR_HANDLER}.
+ */
+ public void setErrorHandler(ErrorHandler newErrorHandler) {
+ _dbErrorHandler = newErrorHandler;
+ }
/**
* Read the system catalog
--- /dev/null
+/*
+Copyright (c) 2008 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.IOException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Implementation of ErrorHandler which is useful for generating debug
+ * information about bad row data (great for bug reports!). After logging a
+ * debug entry for the failed column, it will return some sort of replacement
+ * value, see {@link ReplacementErrorHandler}.
+ *
+ * @author James Ahlborn
+ */
+public class DebugErrorHandler extends ReplacementErrorHandler
+{
+ private static final Log LOG = LogFactory.getLog(DebugErrorHandler.class);
+
+ /**
+ * Constructs a DebugErrorHandler which replaces all errored values with
+ * {@code null}.
+ */
+ public DebugErrorHandler() {
+ }
+
+ /**
+ * Constructs a DebugErrorHandler which replaces all errored values with the
+ * given Object.
+ */
+ public DebugErrorHandler(Object replacement) {
+ super(replacement);
+ }
+
+ @Override
+ public Object handleRowError(Column column,
+ byte[] columnData,
+ Table.RowState rowState,
+ Exception error)
+ throws IOException
+ {
+ if(LOG.isDebugEnabled()) {
+ LOG.debug("Failed reading column " + column + ", row " +
+ rowState + ", bytes " +
+ ((columnData != null) ?
+ ByteUtil.toHexString(columnData) : "null"),
+ error);
+ }
+
+ return super.handleRowError(column, columnData, rowState, error);
+ }
+
+}
--- /dev/null
+/*
+Copyright (c) 2008 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.IOException;
+
+/**
+ * Handler for errors encountered while reading a column of row data from a
+ * Table. An instance of this class may be configured at the Database, Table,
+ * or Cursor level to customize error handling as desired. The default
+ * instance used is {@link Database#DEFAULT_ERROR_HANDLER}, which just
+ * rethrows any exceptions encountered.
+ *
+ * @author James Ahlborn
+ */
+public interface ErrorHandler
+{
+
+ /**
+ * Handles an error encountered while reading a column of data from a Table
+ * row. Handler may either throw an exception (which will be propagated
+ * back to the caller) or return a replacement for this row's column value
+ * (in which case the row will continue to be read normally).
+ *
+ * @param column the info for the column being read
+ * @param columnData the actual column data for the column being read (which
+ * may be {@code null} dependening on when the exception
+ * was thrown during the reading process)
+ * @param rowState the current row state for the caller
+ * @param error the error that was encountered
+ *
+ * @return replacement for this row's column
+ */
+ public Object handleRowError(Column column,
+ byte[] columnData,
+ Table.RowState rowState,
+ Exception error)
+ throws IOException;
+
+}
--- /dev/null
+/*
+Copyright (c) 2008 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.IOException;
+
+/**
+ * Simple implementation of an ErrorHandler which always returns the
+ * configured object.
+ *
+ * @author James Ahlborn
+ */
+public class ReplacementErrorHandler implements ErrorHandler
+{
+
+ private final Object _replacement;
+
+ /**
+ * Constructs a ReplacementErrorHandler which replaces all errored values
+ * with {@code null}.
+ */
+ public ReplacementErrorHandler() {
+ this(null);
+ }
+
+ /**
+ * Constructs a ReplacementErrorHandler which replaces all errored values
+ * with the given Object.
+ */
+ public ReplacementErrorHandler(Object replacement) {
+ _replacement = replacement;
+ }
+
+ public Object handleRowError(Column column,
+ byte[] columnData,
+ Table.RowState rowState,
+ Exception error)
+ throws IOException
+ {
+ return _replacement;
+ }
+
+}
TempPageHolder.newHolder(TempBufferHolder.Type.SOFT);
/** for now, "big index support" is optional */
private final boolean _useBigIndex;
+ /** optional error handler to use when row errors are encountered */
+ private ErrorHandler _tableErrorHandler;
/** common cursor for iterating through the table, kept here for historic
reasons */
return getDatabase().getPageChannel();
}
+ /**
+ * Gets the currently configured ErrorHandler (always non-{@code null}).
+ * This will be used to handle all errors unless overridden at the Cursor
+ * level.
+ */
+ public ErrorHandler getErrorHandler() {
+ return((_tableErrorHandler != null) ? _tableErrorHandler :
+ getDatabase().getErrorHandler());
+ }
+
+ /**
+ * Sets a new ErrorHandler. If {@code null}, resets to using the
+ * ErrorHandler configured at the Database level.
+ */
+ public void setErrorHandler(ErrorHandler newErrorHandler) {
+ _tableErrorHandler = newErrorHandler;
+ }
+
protected int getTableDefPageNumber() {
return _tableDefPageNumber;
}
ByteBuffer rowBuffer = positionAtRowData(rowState, rowId);
requireNonDeletedRow(rowState, rowId);
- Object value = getRowColumn(rowBuffer, getRowNullMask(rowBuffer), column);
+ Object value = getRowColumn(rowBuffer, getRowNullMask(rowBuffer), column,
+ rowState);
// cache the row values in order to be able to update the index on row
// deletion. note, most of the returned values are immutable, except
if((columnNames == null) || (columnNames.contains(column.getName()))) {
// Add the value to the row data
- Object value = getRowColumn(rowBuffer, nullMask, column);
+ Object value = getRowColumn(rowBuffer, nullMask, column, rowState);
rtn.put(column.getName(), value);
// cache the row values in order to be able to update the index on row
*/
private static Object getRowColumn(ByteBuffer rowBuffer,
NullMask nullMask,
- Column column)
+ Column column,
+ RowState rowState)
throws IOException
{
- boolean isNull = nullMask.isNull(column);
- if(column.getType() == DataType.BOOLEAN) {
- return Boolean.valueOf(!isNull); //Boolean values are stored in the null mask
- } else if(isNull) {
- // well, that's easy!
- return null;
- }
+ byte[] columnData = null;
+ try {
+
+ boolean isNull = nullMask.isNull(column);
+ if(column.getType() == DataType.BOOLEAN) {
+ return Boolean.valueOf(!isNull); //Boolean values are stored in the null mask
+ } else if(isNull) {
+ // well, that's easy!
+ return null;
+ }
- // reset position to row start
- rowBuffer.reset();
+ // reset position to row start
+ rowBuffer.reset();
- // locate the column data bytes
- int rowStart = rowBuffer.position();
- int colDataPos = 0;
- int colDataLen = 0;
- if(!column.isVariableLength()) {
-
- // read fixed length value (non-boolean at this point)
- int dataStart = rowStart + 2;
- colDataPos = dataStart + column.getFixedDataOffset();
- colDataLen = column.getType().getFixedSize();
+ // locate the column data bytes
+ int rowStart = rowBuffer.position();
+ int colDataPos = 0;
+ int colDataLen = 0;
+ if(!column.isVariableLength()) {
+
+ // read fixed length value (non-boolean at this point)
+ int dataStart = rowStart + 2;
+ colDataPos = dataStart + column.getFixedDataOffset();
+ colDataLen = column.getType().getFixedSize();
- } else {
+ } else {
- // read var length value
- int varColumnOffsetPos =
- (rowBuffer.limit() - nullMask.byteSize() - 4) -
- (column.getVarLenTableIndex() * 2);
+ // read var length value
+ int varColumnOffsetPos =
+ (rowBuffer.limit() - nullMask.byteSize() - 4) -
+ (column.getVarLenTableIndex() * 2);
- short varDataStart = rowBuffer.getShort(varColumnOffsetPos);
- short varDataEnd = rowBuffer.getShort(varColumnOffsetPos - 2);
- colDataPos = rowStart + varDataStart;
- colDataLen = varDataEnd - varDataStart;
- }
+ short varDataStart = rowBuffer.getShort(varColumnOffsetPos);
+ short varDataEnd = rowBuffer.getShort(varColumnOffsetPos - 2);
+ colDataPos = rowStart + varDataStart;
+ colDataLen = varDataEnd - varDataStart;
+ }
- // grab the column data
- byte[] columnData = new byte[colDataLen];
- rowBuffer.position(colDataPos);
- rowBuffer.get(columnData);
+ // grab the column data
+ columnData = new byte[colDataLen];
+ rowBuffer.position(colDataPos);
+ rowBuffer.get(columnData);
- // parse the column data
- return column.read(columnData);
+ // parse the column data
+ return column.read(columnData);
+
+ } catch(Exception e) {
+ return rowState.handleRowError(column, columnData, e);
+ }
}
/**
rowState can detect updates to the table and re-read any buffered
data */
private int _lastModCount;
-
+ /** optional error handler to use when row errors are encountered */
+ private ErrorHandler _errorHandler;
+
private RowState(TempBufferHolder.Type headerType) {
_headerRowBufferH = TempPageHolder.newHolder(headerType);
_rowValues = new Object[Table.this.getColumnCount()];
public Table getTable() {
return Table.this;
}
+
+ public ErrorHandler getErrorHandler() {
+ return((_errorHandler != null) ? _errorHandler :
+ getTable().getErrorHandler());
+ }
+
+ public void setErrorHandler(ErrorHandler newErrorHandler) {
+ _errorHandler = newErrorHandler;
+ }
public void reset() {
_finalRowId = null;
return _finalRowBuffer;
}
+ private Object handleRowError(Column column,
+ byte[] columnData,
+ Exception error)
+ throws IOException
+ {
+ return getErrorHandler().handleRowError(column, columnData,
+ this, error);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "RowState: headerRowId = " + _headerRowId + ", finalRowId = " +
+ _finalRowId;
+ }
}
}
}
static void assertTable(List<Map<String, Object>> expectedTable, Table table)
+ {
+ assertCursor(expectedTable, Cursor.createCursor(table));
+ }
+
+ static void assertCursor(List<Map<String, Object>> expectedTable,
+ Cursor cursor)
{
List<Map<String, Object>> foundTable =
new ArrayList<Map<String, Object>>();
- for(Map<String, Object> row : Cursor.createCursor(table)) {
+ for(Map<String, Object> row : cursor) {
foundTable.add(row);
}
assertEquals(expectedTable, foundTable);
--- /dev/null
+/*
+Copyright (c) 2007 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.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.nio.ByteOrder;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+import static com.healthmarketscience.jackcess.DatabaseTest.*;
+
+/**
+ * @author James Ahlborn
+ */
+public class ErrorHandlerTest extends TestCase
+{
+
+ public ErrorHandlerTest(String name) {
+ super(name);
+ }
+
+ public void testErrorHandler() throws Exception
+ {
+ Database db = create();
+
+ Table table =
+ new TableBuilder("test")
+ .addColumn(new ColumnBuilder("col", DataType.TEXT).toColumn())
+ .addColumn(new ColumnBuilder("val", DataType.LONG).toColumn())
+ .toTable(db);
+
+ table.addRow("row1", 1);
+ table.addRow("row2", 2);
+ table.addRow("row3", 3);
+
+ assertTable(createExpectedTable(
+ createExpectedRow("col", "row1",
+ "val", 1),
+ createExpectedRow("col", "row2",
+ "val", 2),
+ createExpectedRow("col", "row3",
+ "val", 3)),
+ table);
+
+
+ replaceColumn(table, "val");
+
+ table.reset();
+ try {
+ table.getNextRow();
+ fail("IOException should have been thrown");
+ } catch(IOException e) {
+ // success
+ }
+
+ table.reset();
+ table.setErrorHandler(new ReplacementErrorHandler());
+
+ assertTable(createExpectedTable(
+ createExpectedRow("col", "row1",
+ "val", null),
+ createExpectedRow("col", "row2",
+ "val", null),
+ createExpectedRow("col", "row3",
+ "val", null)),
+ table);
+
+ Cursor c1 = Cursor.createCursor(table);
+ Cursor c2 = Cursor.createCursor(table);
+ Cursor c3 = Cursor.createCursor(table);
+
+ c2.setErrorHandler(new DebugErrorHandler("#error"));
+ c3.setErrorHandler(Database.DEFAULT_ERROR_HANDLER);
+
+ assertCursor(createExpectedTable(
+ createExpectedRow("col", "row1",
+ "val", null),
+ createExpectedRow("col", "row2",
+ "val", null),
+ createExpectedRow("col", "row3",
+ "val", null)),
+ c1);
+
+ assertCursor(createExpectedTable(
+ createExpectedRow("col", "row1",
+ "val", "#error"),
+ createExpectedRow("col", "row2",
+ "val", "#error"),
+ createExpectedRow("col", "row3",
+ "val", "#error")),
+ c2);
+
+ try {
+ c3.getNextRow();
+ fail("IOException should have been thrown");
+ } catch(IOException e) {
+ // success
+ }
+
+ table.setErrorHandler(null);
+ c1.setErrorHandler(null);
+ c1.reset();
+ try {
+ c1.getNextRow();
+ fail("IOException should have been thrown");
+ } catch(IOException e) {
+ // success
+ }
+
+
+ db.close();
+ }
+
+ @SuppressWarnings("unchecked")
+ private void replaceColumn(Table t, String colName) throws Exception
+ {
+ Field colsField = Table.class.getDeclaredField("_columns");
+ colsField.setAccessible(true);
+ List<Column> cols = (List<Column>)colsField.get(t);
+
+ Column srcCol = null;
+ Column destCol = new BogusColumn(t);
+ for(int i = 0; i < cols.size(); ++i) {
+ srcCol = cols.get(i);
+ if(srcCol.getName().equals(colName)) {
+ cols.set(i, destCol);
+ break;
+ }
+ }
+
+ // copy fields from source to dest
+ for(Field f : Column.class.getDeclaredFields()) {
+ if(!Modifier.isFinal(f.getModifiers())) {
+ f.setAccessible(true);
+ f.set(destCol, f.get(srcCol));
+ }
+ }
+
+ }
+
+ private static class BogusColumn extends Column
+ {
+ private BogusColumn(Table table) {
+ super(true, table);
+ }
+
+ @Override
+ public Object read(byte[] data, ByteOrder order) throws IOException {
+ throw new IOException("bogus column");
+ }
+ }
+
+}
}
private Column newTestColumn() {
- return new Column(true) {
+ return new Column(true, null) {
@Override
public Table getTable() {
return _testTable;