From 393f14089d9f9e95a93352398d183d3f59ba9440 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Mon, 24 Mar 2014 02:39:58 +0000 Subject: [PATCH] add column validator unit tests, work out some wrinkles git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@851 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../healthmarketscience/jackcess/Column.java | 18 ++ .../jackcess/Database.java | 4 +- .../jackcess/impl/ColumnImpl.java | 24 ++ .../jackcess/impl/TableImpl.java | 117 ++++++---- .../jackcess/util/ColumnValidatorFactory.java | 4 +- .../jackcess/DatabaseTest.java | 12 + .../jackcess/util/ColumnValidatorTest.java | 213 ++++++++++++++++++ 7 files changed, 346 insertions(+), 46 deletions(-) create mode 100644 src/test/java/com/healthmarketscience/jackcess/util/ColumnValidatorTest.java diff --git a/src/main/java/com/healthmarketscience/jackcess/Column.java b/src/main/java/com/healthmarketscience/jackcess/Column.java index ee4c770..383f605 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Column.java +++ b/src/main/java/com/healthmarketscience/jackcess/Column.java @@ -25,6 +25,7 @@ import java.util.Map; import com.healthmarketscience.jackcess.complex.ComplexColumnInfo; import com.healthmarketscience.jackcess.complex.ComplexValue; +import com.healthmarketscience.jackcess.util.ColumnValidator; /** * Access database column definition. A {@link Table} has a list of Column @@ -151,6 +152,23 @@ public interface Column */ public Column getVersionHistoryColumn(); + /** + * Gets currently configured ColumnValidator (always non-{@code null}). + * @usage _intermediate_method_ + */ + public ColumnValidator getColumnValidator(); + + /** + * Sets a new ColumnValidator. If {@code null}, resets to the value + * returned from the Database's ColumnValidatorFactory (if the factory + * returns {@code null}, then the default is used). Autonumber columns + * cannot have a validator instance other than the default. + * @throws IllegalArgumentException if an attempt is made to set a + * non-{@code null} ColumnValidator instance on an autonumber column + * @usage _intermediate_method_ + */ + public void setColumnValidator(ColumnValidator newValidator); + public Object setRowValue(Object[] rowArray, Object value); public Object setRowValue(Map rowMap, Object value); diff --git a/src/main/java/com/healthmarketscience/jackcess/Database.java b/src/main/java/com/healthmarketscience/jackcess/Database.java index 4e9d136..24d291d 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Database.java +++ b/src/main/java/com/healthmarketscience/jackcess/Database.java @@ -387,7 +387,9 @@ public interface Database extends Iterable, Closeable, Flushable /** * Sets a new ColumnValidatorFactory. If {@code null}, resets to the - * default value. + * default value. The configured ColumnValidatorFactory will be used to + * create ColumnValidator instances on any user tables loaded from + * this point onward (this will not be used for system tables). * @usage _intermediate_method_ */ public void setColumnValidatorFactory(ColumnValidatorFactory newFactory); diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index b2a1ffc..6cac9fb 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -519,9 +519,24 @@ public class ColumnImpl implements Column, Comparable { } public void setColumnValidator(ColumnValidator newValidator) { + + if(isAutoNumber()) { + // cannot set autonumber validator (autonumber values are controlled + // internally) + if(newValidator != null) { + throw new IllegalArgumentException( + "Cannot set ColumnValidator for autonumber columns"); + } + // just leave default validator instance alone + return; + } + if(newValidator == null) { newValidator = getDatabase().getColumnValidatorFactory() .createValidator(this); + if(newValidator == null) { + newValidator = SimpleColumnValidator.INSTANCE; + } } _validator = newValidator; } @@ -1937,6 +1952,15 @@ public class ColumnImpl implements Column, Comparable { } } + /** + * Returns {@code true} if the value is immutable, {@code false} otherwise. + * This only handles values that are returned from the {@link #read} method. + */ + static boolean isImmutableValue(Object value) { + // for now, the only mutable value this class returns is byte[] + return !(value instanceof byte[]); + } + /** * Date subclass which stashes the original date bits, in case we attempt to * re-write the value (will not lose precision). diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java index f52bb1d..4107abb 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java @@ -304,9 +304,12 @@ public class TableImpl implements Table _fkEnforcer = new FKEnforcer(this); - // after fully constructed, allow column validator to be configured - for(ColumnImpl col : _columns) { - col.setColumnValidator(null); + if(!isSystem()) { + // after fully constructed, allow column validator to be configured (but + // only for user tables) + for(ColumnImpl col : _columns) { + col.setColumnValidator(null); + } } } @@ -578,7 +581,7 @@ public class TableImpl implements Table } // use any read rowValues to help update the indexes - rowValues = rowState.getRowValues(); + rowValues = rowState.getRowCacheValues(); // check foreign keys before proceeding w/ deletion _fkEnforcer.deleteRow(rowValues); @@ -692,13 +695,19 @@ public class TableImpl implements Table if(column.getType() == DataType.BOOLEAN) { // Boolean values are stored in the null mask. see note about // caching below - return rowState.setRowValue(column.getColumnIndex(), - Boolean.valueOf(!isNull)); + return rowState.setRowCacheValue(column.getColumnIndex(), + Boolean.valueOf(!isNull)); } else if(isNull) { // well, that's easy! (no need to update cache w/ null) return null; } + Object cachedValue = rowState.getRowCacheValue(column.getColumnIndex()); + if(cachedValue != null) { + // we already have it, use it + return cachedValue; + } + // reset position to row start rowBuffer.reset(); @@ -754,14 +763,14 @@ public class TableImpl implements Table // to update the index on row deletion. note, most of the returned // values are immutable, except for binary data (returned as byte[]), // but binary data shouldn't be indexed anyway. - return rowState.setRowValue(column.getColumnIndex(), - column.read(columnData)); + return rowState.setRowCacheValue(column.getColumnIndex(), + column.read(columnData)); } catch(Exception e) { // cache "raw" row value. see note about caching above - rowState.setRowValue(column.getColumnIndex(), - ColumnImpl.rawDataWrapper(columnData)); + rowState.setRowCacheValue(column.getColumnIndex(), + ColumnImpl.rawDataWrapper(columnData)); return rowState.handleRowError(column, columnData, e); } @@ -1538,34 +1547,15 @@ public class TableImpl implements Table } // handle various value massaging activities - Object complexAutoNumber = null; for(ColumnImpl column : _columns) { - - Object rowValue = null; - if(column.isAutoNumber()) { - - // fill in autonumbers, ignore given row value, use next - // autonumber - ColumnImpl.AutoNumberGenerator autoNumGen = - column.getAutoNumberGenerator(); - if(autoNumGen.getType() != DataType.COMPLEX_TYPE) { - rowValue = autoNumGen.getNext(null); - } else { - // complex type auto numbers are shared across all complex - // columns in the row - complexAutoNumber = autoNumGen.getNext(complexAutoNumber); - rowValue = complexAutoNumber; - } - - } else { - + if(!column.isAutoNumber()) { // pass input value through column validator - rowValue = column.validate(column.getRowValue(row)); + column.setRowValue(row, column.validate(column.getRowValue(row))); } - - column.setRowValue(row, rowValue); } - + + // fill in autonumbers + handleAutoNumbersForAdd(row); ++autoNumAssignCount; // write the row of data to a temporary buffer @@ -1779,7 +1769,7 @@ public class TableImpl implements Table rowValue = getRowColumn(getFormat(), rowBuffer, column, rowState, null); } else { - + rowValue = column.getRowValue(row); if(rowValue == Column.KEEP_VALUE) { @@ -1788,14 +1778,21 @@ public class TableImpl implements Table keepRawVarValues); } else { - + + // set oldValue to something that could not possibly be a real value + Object oldValue = Column.KEEP_VALUE; if(_indexColumns.contains(column)) { // read (old) row value to help update indexes - getRowColumn(getFormat(), rowBuffer, column, rowState, null); + oldValue = getRowColumn(getFormat(), rowBuffer, column, rowState, null); + } else { + oldValue = rowState.getRowCacheValue(column.getColumnIndex()); } - - // pass input value through column validator - rowValue = column.validate(rowValue); + + // if the old value was passed back in, we don't need to validate + if(oldValue != rowValue) { + // pass input value through column validator + rowValue = column.validate(rowValue); + } } } @@ -1817,7 +1814,7 @@ public class TableImpl implements Table IndexData.PendingChange idxChange = null; try { - Object[] oldRowValues = rowState.getRowValues(); + Object[] oldRowValues = rowState.getRowCacheValues(); // check foreign keys before actually updating _fkEnforcer.updateRow(oldRowValues, row); @@ -2183,6 +2180,33 @@ public class TableImpl implements Table return buffer; } + /** + * Fill in all autonumber column values. + */ + private void handleAutoNumbersForAdd(Object[] row) + throws IOException + { + if(_autoNumColumns.isEmpty()) { + return; + } + + Object complexAutoNumber = null; + for(ColumnImpl col : _autoNumColumns) { + // ignore given row value, use next autonumber + ColumnImpl.AutoNumberGenerator autoNumGen = col.getAutoNumberGenerator(); + Object rowValue = null; + if(autoNumGen.getType() != DataType.COMPLEX_TYPE) { + rowValue = autoNumGen.getNext(null); + } else { + // complex type auto numbers are shared across all complex columns + // in the row + complexAutoNumber = autoNumGen.getNext(complexAutoNumber); + rowValue = complexAutoNumber; + } + col.setRowValue(row, rowValue); + } + } + /** * Restores all autonumber column values from a failed add row. */ @@ -2607,13 +2631,20 @@ public class TableImpl implements Table return(_status.ordinal() >= RowStateStatus.AT_FINAL.ordinal()); } - private Object setRowValue(int idx, Object value) { + private Object setRowCacheValue(int idx, Object value) { _haveRowValues = true; _rowValues[idx] = value; return value; } + + private Object getRowCacheValue(int idx) { + Object value = _rowValues[idx]; + // only return immutable values. mutable values could have been + // modified externally and therefore could return an incorrect value + return(ColumnImpl.isImmutableValue(value) ? value : null); + } - public Object[] getRowValues() { + public Object[] getRowCacheValues() { return dupeRow(_rowValues, _rowValues.length); } diff --git a/src/main/java/com/healthmarketscience/jackcess/util/ColumnValidatorFactory.java b/src/main/java/com/healthmarketscience/jackcess/util/ColumnValidatorFactory.java index 3a8a323..2f5bcd7 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/ColumnValidatorFactory.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/ColumnValidatorFactory.java @@ -31,8 +31,8 @@ import com.healthmarketscience.jackcess.Column; public interface ColumnValidatorFactory { /** - * Returns a ColumnValidator instance for the given column, must be - * non-{@code null}. + * Returns a ColumnValidator instance for the given column, or {@code null} + * if the default should be used. */ public ColumnValidator createValidator(Column col); } diff --git a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java index 4f2d2c7..24fe758 100644 --- a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java @@ -36,6 +36,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; +import java.lang.reflect.Field; import java.math.BigDecimal; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; @@ -1678,6 +1679,17 @@ public class DatabaseTest extends TestCase return tmp; } + public static void clearTableCache(Database db) throws Exception + { + Field f = db.getClass().getDeclaredField("_tableCache"); + f.setAccessible(true); + Object val = f.get(db); + f = val.getClass().getDeclaredField("_tables"); + f.setAccessible(true); + val = f.get(val); + ((Map)val).clear(); + } + public static byte[] toByteArray(File file) throws IOException { diff --git a/src/test/java/com/healthmarketscience/jackcess/util/ColumnValidatorTest.java b/src/test/java/com/healthmarketscience/jackcess/util/ColumnValidatorTest.java new file mode 100644 index 0000000..727434c --- /dev/null +++ b/src/test/java/com/healthmarketscience/jackcess/util/ColumnValidatorTest.java @@ -0,0 +1,213 @@ +/* +Copyright (c) 2014 James Ahlborn + +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 +*/ + +package com.healthmarketscience.jackcess.util; + +import java.util.List; +import java.util.Map; + +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.ColumnBuilder; +import com.healthmarketscience.jackcess.CursorBuilder; +import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.Database; +import static com.healthmarketscience.jackcess.Database.*; +import static com.healthmarketscience.jackcess.DatabaseTest.*; +import com.healthmarketscience.jackcess.IndexCursor; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.TableBuilder; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; +import junit.framework.TestCase; + +/** + * + * @author James Ahlborn + */ +public class ColumnValidatorTest extends TestCase +{ + + public ColumnValidatorTest(String name) { + super(name); + } + + public void testValidate() throws Exception { + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = create(fileFormat); + + ColumnValidatorFactory initFact = db.getColumnValidatorFactory(); + assertNotNull(initFact); + + Table table = new TableBuilder("Test") + .addColumn(new ColumnBuilder("id", DataType.LONG).setAutoNumber(true)) + .addColumn(new ColumnBuilder("data", DataType.TEXT)) + .addColumn(new ColumnBuilder("num", DataType.LONG)) + .setPrimaryKey("id") + .toTable(db); + + for(Column col : table.getColumns()) { + assertSame(SimpleColumnValidator.INSTANCE, col.getColumnValidator()); + } + + int val = -1; + for(int i = 1; i <= 3; ++i) { + table.addRow(Column.AUTO_NUMBER, "row" + i, val++); + } + + table = null; + + // force table to be reloaded + clearTableCache(db); + + final ColumnValidator cv = new ColumnValidator() { + public Object validate(Column col, Object v1) { + Number num = (Number)v1; + if((num == null) || (num.intValue() < 0)) { + throw new IllegalArgumentException("not gonna happen"); + } + return v1; + } + }; + + ColumnValidatorFactory fact = new ColumnValidatorFactory() { + public ColumnValidator createValidator(Column col) { + Table t = col.getTable(); + assertFalse(t.isSystem()); + if(!"Test".equals(t.getName())) { + return null; + } + + if(col.getType() == DataType.LONG) { + return cv; + } + + return null; + } + }; + + db.setColumnValidatorFactory(fact); + + table = db.getTable("Test"); + + for(Column col : table.getColumns()) { + ColumnValidator cur = col.getColumnValidator(); + assertNotNull(cur); + if("num".equals(col.getName())) { + assertSame(cv, cur); + } else { + assertSame(SimpleColumnValidator.INSTANCE, cur); + } + } + + Column idCol = table.getColumn("id"); + Column dataCol = table.getColumn("data"); + Column numCol = table.getColumn("num"); + + try { + idCol.setColumnValidator(cv); + fail("IllegalArgumentException should have been thrown"); + } catch(IllegalArgumentException e) { + // success + } + assertSame(SimpleColumnValidator.INSTANCE, idCol.getColumnValidator()); + + try { + table.addRow(Column.AUTO_NUMBER, "row4", -3); + fail("IllegalArgumentException should have been thrown"); + } catch(IllegalArgumentException e) { + assertEquals("not gonna happen", e.getMessage()); + } + + table.addRow(Column.AUTO_NUMBER, "row4", 4); + + List> expectedRows = + createExpectedTable( + createExpectedRow("id", 1, "data", "row1", "num", -1), + createExpectedRow("id", 2, "data", "row2", "num", 0), + createExpectedRow("id", 3, "data", "row3", "num", 1), + createExpectedRow("id", 4, "data", "row4", "num", 4)); + + assertTable(expectedRows, table); + + IndexCursor pkCursor = CursorBuilder.createPrimaryKeyCursor(table); + assertNotNull(pkCursor.findRowByEntry(1)); + + pkCursor.setCurrentRowValue(dataCol, "row1_mod"); + + assertEquals(createExpectedRow("id", 1, "data", "row1_mod", "num", -1), + pkCursor.getCurrentRow()); + + try { + pkCursor.setCurrentRowValue(numCol, -2); + fail("IllegalArgumentException should have been thrown"); + } catch(IllegalArgumentException e) { + assertEquals("not gonna happen", e.getMessage()); + } + + assertEquals(createExpectedRow("id", 1, "data", "row1_mod", "num", -1), + pkCursor.getCurrentRow()); + + Row row3 = CursorBuilder.findRowByPrimaryKey(table, 3); + + row3.put("num", -2); + + try { + table.updateRow(row3); + fail("IllegalArgumentException should have been thrown"); + } catch(IllegalArgumentException e) { + assertEquals("not gonna happen", e.getMessage()); + } + + assertEquals(createExpectedRow("id", 3, "data", "row3", "num", 1), + CursorBuilder.findRowByPrimaryKey(table, 3)); + + final ColumnValidator cv2 = new ColumnValidator() { + public Object validate(Column col, Object v1) { + Number num = (Number)v1; + if((num == null) || (num.intValue() < 0)) { + return 0; + } + return v1; + } + }; + + numCol.setColumnValidator(cv2); + + table.addRow(Column.AUTO_NUMBER, "row5", -5); + + expectedRows = + createExpectedTable( + createExpectedRow("id", 1, "data", "row1_mod", "num", -1), + createExpectedRow("id", 2, "data", "row2", "num", 0), + createExpectedRow("id", 3, "data", "row3", "num", 1), + createExpectedRow("id", 4, "data", "row4", "num", 4), + createExpectedRow("id", 5, "data", "row5", "num", 0)); + + assertTable(expectedRows, table); + + assertNotNull(pkCursor.findRowByEntry(3)); + pkCursor.setCurrentRowValue(numCol, -10); + + assertEquals(createExpectedRow("id", 3, "data", "row3", "num", 0), + pkCursor.getCurrentRow()); + + db.close(); + } + } +} -- 2.39.5