]> source.dussan.org Git - jackcess.git/commitdiff
add column validator unit tests, work out some wrinkles
authorJames Ahlborn <jtahlborn@yahoo.com>
Mon, 24 Mar 2014 02:39:58 +0000 (02:39 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Mon, 24 Mar 2014 02:39:58 +0000 (02:39 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@851 f203690c-595d-4dc9-a70b-905162fa7fd2

src/main/java/com/healthmarketscience/jackcess/Column.java
src/main/java/com/healthmarketscience/jackcess/Database.java
src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java
src/main/java/com/healthmarketscience/jackcess/util/ColumnValidatorFactory.java
src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java
src/test/java/com/healthmarketscience/jackcess/util/ColumnValidatorTest.java [new file with mode: 0644]

index ee4c770d5d614951e4c069ee9ff743195665fd0a..383f605a1ceae3e6a58691c915dba86e4612263b 100644 (file)
@@ -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<String,Object> rowMap, Object value);
index 4e9d1361b92ca2165bbae94a0de04693121dbf59..24d291d791498b4c69ef6fa16920c52798806182 100644 (file)
@@ -387,7 +387,9 @@ public interface Database extends Iterable<Table>, 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 <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);
index b2a1ffcbe0c5125dd18e378cc1c3a467fc8bdc1c..6cac9fbfec2628724f96c4ff924d8431858d70bb 100644 (file)
@@ -519,9 +519,24 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
   }
   
   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<ColumnImpl> {
     }
   }
 
+  /**
+   * 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).
index f52bb1d562f4e3b2534ca88e52a44c86e2dc1126..4107abb95594d7bf30008dbf1572e15ebf86f18a 100644 (file)
@@ -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);
     }
 
index 3a8a323ff4f86a9d659196d6fa5e12ce8de45210..2f5bcd75e16db3647371c65c79bdb80fdc92c994 100644 (file)
@@ -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);
 }
index 4f2d2c7f387e4c0b5b05d5b4d39cfa435b016404..24fe7584952cc582704bf2f6885df5fb48814796 100644 (file)
@@ -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 (file)
index 0000000..727434c
--- /dev/null
@@ -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<? 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();
+    }
+  }  
+}