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
*/
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<String,Object> rowMap, Object value);
/**
* 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 <i>user</i> tables loaded from
+ * this point onward (this will not be used for system tables).
* @usage _intermediate_method_
*/
public void setColumnValidatorFactory(ColumnValidatorFactory newFactory);
}
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;
}
}
}
+ /**
+ * 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).
_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);
+ }
}
}
}
// 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);
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();
// 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);
}
}
// 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
rowValue = getRowColumn(getFormat(), rowBuffer, column, rowState, null);
} else {
-
+
rowValue = column.getRowValue(row);
if(rowValue == Column.KEEP_VALUE) {
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);
+ }
}
}
IndexData.PendingChange idxChange = null;
try {
- Object[] oldRowValues = rowState.getRowValues();
+ Object[] oldRowValues = rowState.getRowCacheValues();
// check foreign keys before actually updating
_fkEnforcer.updateRow(oldRowValues, row);
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.
*/
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);
}
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);
}
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;
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
{
--- /dev/null
+/*
+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<? extends Map<String, Object>> 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();
+ }
+ }
+}