parsing. git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@381 f203690c-595d-4dc9-a70b-905162fa7fd2tags/rel_1_1_18
@@ -6,6 +6,10 @@ | |||
</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> |
@@ -144,11 +144,11 @@ public class Column implements Comparable<Column> { | |||
/** | |||
* Only used by unit tests | |||
*/ | |||
Column(boolean testing) { | |||
Column(boolean testing, Table table) { | |||
if(!testing) { | |||
throw new IllegalArgumentException(); | |||
} | |||
_table = null; | |||
_table = table; | |||
} | |||
/** |
@@ -298,6 +298,22 @@ public abstract class Cursor implements Iterable<Map<String, Object>> | |||
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}. |
@@ -99,6 +99,22 @@ public class Database | |||
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; | |||
@@ -243,6 +259,8 @@ public class Database | |||
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 | |||
@@ -387,6 +405,24 @@ public class Database | |||
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 |
@@ -0,0 +1,80 @@ | |||
/* | |||
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); | |||
} | |||
} |
@@ -0,0 +1,65 @@ | |||
/* | |||
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; | |||
} |
@@ -0,0 +1,68 @@ | |||
/* | |||
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; | |||
} | |||
} |
@@ -127,6 +127,8 @@ public class Table | |||
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 */ | |||
@@ -215,6 +217,24 @@ public class Table | |||
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; | |||
} | |||
@@ -383,7 +403,8 @@ public class Table | |||
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 | |||
@@ -430,7 +451,7 @@ public class Table | |||
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 | |||
@@ -448,51 +469,59 @@ public class Table | |||
*/ | |||
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); | |||
} | |||
} | |||
/** | |||
@@ -1710,7 +1739,9 @@ public class Table | |||
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()]; | |||
@@ -1720,6 +1751,15 @@ public class Table | |||
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; | |||
@@ -1875,6 +1915,21 @@ public class Table | |||
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; | |||
} | |||
} | |||
} |
@@ -959,10 +959,16 @@ public class DatabaseTest extends TestCase { | |||
} | |||
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); |
@@ -0,0 +1,181 @@ | |||
/* | |||
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"); | |||
} | |||
} | |||
} |
@@ -168,7 +168,7 @@ public class TableTest extends TestCase { | |||
} | |||
private Column newTestColumn() { | |||
return new Column(true) { | |||
return new Column(true, null) { | |||
@Override | |||
public Table getTable() { | |||
return _testTable; |