diff options
author | James Ahlborn <jtahlborn@yahoo.com> | 2015-05-01 00:45:53 +0000 |
---|---|---|
committer | James Ahlborn <jtahlborn@yahoo.com> | 2015-05-01 00:45:53 +0000 |
commit | e8616e647606956573e6b33bfd0c098c63f3886e (patch) | |
tree | 7b0aa2d20a104004f225cd9868dc55696d7056bc /src/main/java | |
parent | ee49db8317232aff01613146e280ad989d4e34bd (diff) | |
download | jackcess-e8616e647606956573e6b33bfd0c098c63f3886e.tar.gz jackcess-e8616e647606956573e6b33bfd0c098c63f3886e.zip |
Allow optional direct insert/update of autonumber values. This is disabled by default, but can be selectively enabled per-jvm (using system property), per-database, and per-table. fixes feature #32
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@941 f203690c-595d-4dc9-a70b-905162fa7fd2
Diffstat (limited to 'src/main/java')
5 files changed, 312 insertions, 61 deletions
diff --git a/src/main/java/com/healthmarketscience/jackcess/Database.java b/src/main/java/com/healthmarketscience/jackcess/Database.java index 410ce0d..1e6d6cf 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Database.java +++ b/src/main/java/com/healthmarketscience/jackcess/Database.java @@ -117,6 +117,13 @@ public interface Database extends Iterable<Table>, Closeable, Flushable public static final String FK_ENFORCE_PROPERTY = "com.healthmarketscience.jackcess.enforceForeignKeys"; + /** system property which can be used to set the default allow auto number + * insert policy. Defaults to {@code false}. + * @usage _general_field_ + */ + public static final String ALLOW_AUTONUM_INSERT_PROPERTY = + "com.healthmarketscience.jackcess.allowAutoNumberInsert"; + /** * Enum which indicates which version of Access created the database. * @usage _general_class_ @@ -393,6 +400,31 @@ public interface Database extends Iterable<Table>, Closeable, Flushable public void setEnforceForeignKeys(Boolean newEnforceForeignKeys); /** + * Gets current allow auto number insert policy. By default, jackcess does + * not allow auto numbers to be inserted or updated directly (they are + * always handled internally by the Table). Setting this policy to {@code + * true} allows the caller to optionally set the value explicitly when + * adding or updating rows (if a value is not provided, it will still be + * handled internally by the Table). This value can be set database-wide + * using {@link #setAllowAutoNumberInsert} and/or on a per-table basis using + * {@link Table#setAllowAutoNumberInsert} (and/or on a jvm-wide using the + * {@link #ALLOW_AUTONUM_INSERT_PROPERTY} system property). Note that + * <i>enabling this feature should be done with care</i> to reduce the + * chances of screwing up the database. + * + * @usage _intermediate_method_ + */ + public boolean isAllowAutoNumberInsert(); + + /** + * Sets the new auto number insert policy for the database (unless + * overridden at the Table level). If {@code null}, resets to the default + * value. + * @usage _intermediate_method_ + */ + public void setAllowAutoNumberInsert(Boolean allowAutoNumInsert); + + /** * Gets currently configured ColumnValidatorFactory (always non-{@code null}). * @usage _intermediate_method_ */ diff --git a/src/main/java/com/healthmarketscience/jackcess/Table.java b/src/main/java/com/healthmarketscience/jackcess/Table.java index 6604491..bbb6885 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Table.java +++ b/src/main/java/com/healthmarketscience/jackcess/Table.java @@ -99,6 +99,20 @@ public interface Table extends Iterable<Row> public void setErrorHandler(ErrorHandler newErrorHandler); /** + * Gets the currently configured auto number insert policy. + * @see Database#isAllowAutoNumberInsert + * @usage _intermediate_method_ + */ + public boolean isAllowAutoNumberInsert(); + + /** + * Sets the new auto number insert policy for the Table. If {@code null}, + * resets to using the policy configured at the Database level. + * @usage _intermediate_method_ + */ + public void setAllowAutoNumberInsert(Boolean allowAutoNumInsert); + + /** * @return All of the columns in this table (unmodifiable List) * @usage _general_method_ */ diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index d6c0908..b4b7cc2 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -151,7 +151,10 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { private static final char MIN_COMPRESS_CHAR = 1; private static final char MAX_COMPRESS_CHAR = 0xFF; - + /** auto numbers must be > 0 */ + static final int INVALID_AUTO_NUMBER = 0; + + /** owning table */ private final TableImpl _table; /** Whether or not the column is of variable length */ @@ -1843,7 +1846,17 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { * <i>Warning, calling this externally will result in this value being * "lost" for the table.</i> */ - public abstract Object getNext(Object prevRowValue); + public abstract Object getNext(TableImpl.WriteRowState writeRowState); + + /** + * Returns a valid autonumber for this generator. + * <p> + * <i>Warning, calling this externally may result in this value being + * "lost" for the table.</i> + */ + public abstract Object handleInsert( + TableImpl.WriteRowState writeRowState, Object inRowValue) + throws IOException; /** * Restores a previous autonumber generated by this generator. @@ -1867,12 +1880,27 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { } @Override - public Object getNext(Object prevRowValue) { + public Object getNext(TableImpl.WriteRowState writeRowState) { // the table stores the last long autonumber used return getTable().getNextLongAutoNumber(); } @Override + public Object handleInsert(TableImpl.WriteRowState writeRowState, + Object inRowValue) + throws IOException + { + int inAutoNum = toNumber(inRowValue).intValue(); + if(inAutoNum <= INVALID_AUTO_NUMBER) { + throw new IOException(withErrorContext( + "Invalid auto number value " + inAutoNum)); + } + // the table stores the last long autonumber used + getTable().adjustLongAutoNumber(inAutoNum); + return inAutoNum; + } + + @Override public void restoreLast(Object last) { if(last instanceof Integer) { getTable().restoreLastLongAutoNumber((Integer)last); @@ -1897,13 +1925,22 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { } @Override - public Object getNext(Object prevRowValue) { + public Object getNext(TableImpl.WriteRowState writeRowState) { // format guids consistently w/ Column.readGUIDValue() _lastAutoNumber = "{" + UUID.randomUUID() + "}"; return _lastAutoNumber; } @Override + public Object handleInsert(TableImpl.WriteRowState writeRowState, + Object inRowValue) + throws IOException + { + _lastAutoNumber = toCharSequence(inRowValue); + return _lastAutoNumber; + } + + @Override public void restoreLast(Object last) { _lastAutoNumber = null; } @@ -1925,14 +1962,54 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { } @Override - public Object getNext(Object prevRowValue) { - int nextComplexAutoNum = - ((prevRowValue == null) ? - // the table stores the last ComplexType autonumber used - getTable().getNextComplexTypeAutoNumber() : - // same value is shared across all ComplexType values in a row - ((ComplexValueForeignKey)prevRowValue).get()); - return new ComplexValueForeignKeyImpl(ColumnImpl.this, nextComplexAutoNum); + public Object getNext(TableImpl.WriteRowState writeRowState) { + // same value is shared across all ComplexType values in a row + int nextComplexAutoNum = writeRowState.getComplexAutoNumber(); + if(nextComplexAutoNum <= INVALID_AUTO_NUMBER) { + // the table stores the last ComplexType autonumber used + nextComplexAutoNum = getTable().getNextComplexTypeAutoNumber(); + writeRowState.setComplexAutoNumber(nextComplexAutoNum); + } + return new ComplexValueForeignKeyImpl(ColumnImpl.this, + nextComplexAutoNum); + } + + @Override + public Object handleInsert(TableImpl.WriteRowState writeRowState, + Object inRowValue) + throws IOException + { + ComplexValueForeignKey inComplexFK = null; + if(inRowValue instanceof ComplexValueForeignKey) { + inComplexFK = (ComplexValueForeignKey)inRowValue; + } else { + inComplexFK = new ComplexValueForeignKeyImpl( + ColumnImpl.this, toNumber(inRowValue).intValue()); + } + + if(inComplexFK.getColumn() != ColumnImpl.this) { + throw new IOException(withErrorContext( + "Wrong column for complex value foreign key, found " + + inComplexFK.getColumn().getName())); + } + if(inComplexFK.get() < 1) { + throw new IOException(withErrorContext( + "Invalid complex value foreign key value " + inComplexFK.get())); + } + // same value is shared across all ComplexType values in a row + int prevRowValue = writeRowState.getComplexAutoNumber(); + if(prevRowValue <= INVALID_AUTO_NUMBER) { + writeRowState.setComplexAutoNumber(inComplexFK.get()); + } else if(prevRowValue != inComplexFK.get()) { + throw new IOException(withErrorContext( + "Inconsistent complex value foreign key values: found " + + prevRowValue + ", given " + inComplexFK)); + } + + // the table stores the last ComplexType autonumber used + getTable().adjustComplexTypeAutoNumber(inComplexFK.get()); + + return inComplexFK; } @Override @@ -1963,7 +2040,13 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { } @Override - public Object getNext(Object prevRowValue) { + public Object getNext(TableImpl.WriteRowState writeRowState) { + throw new UnsupportedOperationException(); + } + + @Override + public Object handleInsert(TableImpl.WriteRowState writeRowState, + Object inRowValue) { throw new UnsupportedOperationException(); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java index 47acb10..8cf809d 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java @@ -301,6 +301,8 @@ public class DatabaseImpl implements Database private Table.ColumnOrder _columnOrder; /** whether or not enforcement of foreign-keys is enabled */ private boolean _enforceForeignKeys; + /** whether or not auto numbers can be directly inserted by the user */ + private boolean _allowAutoNumInsert; /** factory for ColumnValidators */ private ColumnValidatorFactory _validatorFactory = SimpleColumnValidatorFactory.INSTANCE; /** cache of in-use tables */ @@ -501,6 +503,7 @@ public class DatabaseImpl implements Database _charset = ((charset == null) ? getDefaultCharset(_format) : charset); _columnOrder = getDefaultColumnOrder(); _enforceForeignKeys = getDefaultEnforceForeignKeys(); + _allowAutoNumInsert = getDefaultAllowAutoNumberInsert(); _fileFormat = fileFormat; _pageChannel = new PageChannel(channel, closeChannel, _format, autoSync); _timeZone = ((timeZone == null) ? getDefaultTimeZone() : timeZone); @@ -662,6 +665,18 @@ public class DatabaseImpl implements Database _enforceForeignKeys = newEnforceForeignKeys; } + public boolean isAllowAutoNumberInsert() { + return _allowAutoNumInsert; + } + + public void setAllowAutoNumberInsert(Boolean allowAutoNumInsert) { + if(allowAutoNumInsert == null) { + allowAutoNumInsert = getDefaultAllowAutoNumberInsert(); + } + _allowAutoNumInsert = allowAutoNumInsert; + } + + public ColumnValidatorFactory getColumnValidatorFactory() { return _validatorFactory; } @@ -1744,6 +1759,21 @@ public class DatabaseImpl implements Database } /** + * Returns the default allow auto number insert policy. This defaults to + * {@code false}, but can be overridden using the system + * property {@value com.healthmarketscience.jackcess.Database#ALLOW_AUTONUM_INSERT_PROPERTY}. + * @usage _advanced_method_ + */ + public static boolean getDefaultAllowAutoNumberInsert() + { + String prop = System.getProperty(ALLOW_AUTONUM_INSERT_PROPERTY); + if(prop != null) { + return Boolean.TRUE.toString().equalsIgnoreCase(prop); + } + return false; + } + + /** * Copies the given InputStream to the given channel using the most * efficient means possible. */ diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java index 0e0488d..e295316 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java @@ -38,7 +38,6 @@ import com.healthmarketscience.jackcess.Column; import com.healthmarketscience.jackcess.ColumnBuilder; import com.healthmarketscience.jackcess.ConstraintViolationException; import com.healthmarketscience.jackcess.CursorBuilder; -import com.healthmarketscience.jackcess.DataType; import com.healthmarketscience.jackcess.IndexBuilder; import com.healthmarketscience.jackcess.JackcessException; import com.healthmarketscience.jackcess.PropertyMap; @@ -166,6 +165,9 @@ public class TableImpl implements Table private PropertyMap _props; /** properties group for this table (and columns) */ private PropertyMaps _propertyMaps; + /** optional flag indicating whether or not auto numbers can be directly + inserted by the user */ + private Boolean _allowAutoNumInsert; /** foreign-key enforcer for this table */ private final FKEnforcer _fkEnforcer; @@ -356,6 +358,15 @@ public class TableImpl implements Table return _tableDefPageNumber; } + public boolean isAllowAutoNumberInsert() { + return ((_allowAutoNumInsert != null) ? (boolean)_allowAutoNumInsert : + getDatabase().isAllowAutoNumberInsert()); + } + + public void setAllowAutoNumberInsert(Boolean allowAutoNumInsert) { + _allowAutoNumInsert = allowAutoNumInsert; + } + /** * @usage _advanced_method_ */ @@ -1079,7 +1090,6 @@ public class TableImpl implements Table /** * @param buffer Buffer to write to - * @param columns List of Columns in the table */ private static void writeTableDefinitionHeader( TableCreator creator, ByteBuffer buffer, int totalTableDefSize) @@ -1499,7 +1509,7 @@ public class TableImpl implements Table /** * Add multiple rows to this table, only writing to disk after all * rows have been written, and every time a data page is filled. - * @param inRows List of Object[] row values + * @param rows List of Object[] row values */ private List<? extends Object[]> addRows(List<? extends Object[]> rows, final boolean isBatchWrite) @@ -1516,6 +1526,8 @@ public class TableImpl implements Table int pageNumber = PageChannel.INVALID_PAGE_NUMBER; int updateCount = 0; int autoNumAssignCount = 0; + WriteRowState writeRowState = + (!_autoNumColumns.isEmpty() ? new WriteRowState() : null); try { List<Object[]> dupeRows = null; @@ -1549,7 +1561,7 @@ public class TableImpl implements Table } // fill in autonumbers - handleAutoNumbersForAdd(row); + handleAutoNumbersForAdd(row, writeRowState); ++autoNumAssignCount; // write the row of data to a temporary buffer @@ -1758,45 +1770,44 @@ public class TableImpl implements Table // handle various value massaging activities for(ColumnImpl column : _columns) { - - Object rowValue = null; + if(column.isAutoNumber()) { - - // fill in any auto-numbers (we don't allow autonumber values to be - // modified) - rowValue = getRowColumn(getFormat(), rowBuffer, column, rowState, null); - - } else { + // handle these separately (below) + continue; + } - rowValue = column.getRowValue(row); - if(rowValue == Column.KEEP_VALUE) { + Object rowValue = column.getRowValue(row); + if(rowValue == Column.KEEP_VALUE) { - // fill in any "keep value" fields (restore old value) - rowValue = getRowColumn(getFormat(), rowBuffer, column, rowState, - keepRawVarValues); + // fill in any "keep value" fields (restore old value) + rowValue = getRowColumn(getFormat(), rowBuffer, column, rowState, + 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 - oldValue = getRowColumn(getFormat(), rowBuffer, column, rowState, null); - } else { - oldValue = rowState.getRowCacheValue(column.getColumnIndex()); - } + } else { - // 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); - } + // 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 + oldValue = getRowColumn(getFormat(), rowBuffer, column, rowState, + null); + } else { + oldValue = rowState.getRowCacheValue(column.getColumnIndex()); } + + // 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); + } } column.setRowValue(row, rowValue); } + // fill in autonumbers + handleAutoNumbersForUpdate(row, rowBuffer, rowState); + // generate new row bytes ByteBuffer newRowData = createRow( row, _writeRowBufferH.getPageBuffer(getPageChannel()), oldRowSize, @@ -2178,33 +2189,80 @@ public class TableImpl implements Table } /** - * Fill in all autonumber column values. + * Fill in all autonumber column values for add. */ - private void handleAutoNumbersForAdd(Object[] row) + private void handleAutoNumbersForAdd(Object[] row, WriteRowState writeRowState) throws IOException { if(_autoNumColumns.isEmpty()) { return; } - - Object complexAutoNumber = null; + + boolean enableInsert = isAllowAutoNumberInsert(); + writeRowState.resetAutoNumber(); for(ColumnImpl col : _autoNumColumns) { - // ignore given row value, use next autonumber + + // ignore input row value, use original row value (unless explicitly + // enabled) + Object inRowValue = getInputAutoNumberRowValue(enableInsert, col, row); + 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; - } + Object rowValue = ((inRowValue == null) ? + autoNumGen.getNext(writeRowState) : + autoNumGen.handleInsert(writeRowState, inRowValue)); + col.setRowValue(row, rowValue); } } /** + * Fill in all autonumber column values for update. + */ + private void handleAutoNumbersForUpdate(Object[] row, ByteBuffer rowBuffer, + RowState rowState) + throws IOException + { + if(_autoNumColumns.isEmpty()) { + return; + } + + boolean enableInsert = isAllowAutoNumberInsert(); + rowState.resetAutoNumber(); + for(ColumnImpl col : _autoNumColumns) { + + // ignore input row value, use original row value (unless explicitly + // enabled) + Object inRowValue = getInputAutoNumberRowValue(enableInsert, col, row); + + Object rowValue = + ((inRowValue == null) ? + getRowColumn(getFormat(), rowBuffer, col, rowState, null) : + col.getAutoNumberGenerator().handleInsert(rowState, inRowValue)); + + col.setRowValue(row, rowValue); + } + } + + /** + * Optionally get the input autonumber row value for the given column from + * the given row if one was provided. + */ + private static Object getInputAutoNumberRowValue( + boolean enableInsert, ColumnImpl col, Object[] row) + { + if(!enableInsert) { + return null; + } + + Object inRowValue = col.getRowValue(row); + if((inRowValue == Column.KEEP_VALUE) || (inRowValue == Column.AUTO_NUMBER)) { + // these "special" values both behave like nothing was given + inRowValue = null; + } + return inRowValue; + } + + /** * Restores all autonumber column values from a failed add row. */ private void restoreAutoNumbersFromAdd(Object[] row) @@ -2246,6 +2304,12 @@ public class TableImpl implements Table return _lastLongAutoNumber; } + void adjustLongAutoNumber(int inLongAutoNumber) { + if(inLongAutoNumber > _lastLongAutoNumber) { + _lastLongAutoNumber = inLongAutoNumber; + } + } + void restoreLastLongAutoNumber(int lastLongAutoNumber) { // restores the last used auto number _lastLongAutoNumber = lastLongAutoNumber - 1; @@ -2261,6 +2325,12 @@ public class TableImpl implements Table return _lastComplexTypeAutoNumber; } + void adjustComplexTypeAutoNumber(int inComplexTypeAutoNumber) { + if(inComplexTypeAutoNumber > _lastComplexTypeAutoNumber) { + _lastComplexTypeAutoNumber = inComplexTypeAutoNumber; + } + } + void restoreLastComplexTypeAutoNumber(int lastComplexTypeAutoNumber) { // restores the last used auto number _lastComplexTypeAutoNumber = lastComplexTypeAutoNumber - 1; @@ -2501,10 +2571,31 @@ public class TableImpl implements Table } /** - * Maintains the state of reading a row of data. + * Maintains state for writing a new row of data. + */ + protected static class WriteRowState + { + private int _complexAutoNumber = ColumnImpl.INVALID_AUTO_NUMBER; + + public int getComplexAutoNumber() { + return _complexAutoNumber; + } + + public void setComplexAutoNumber(int complexAutoNumber) { + _complexAutoNumber = complexAutoNumber; + } + + public void resetAutoNumber() { + _complexAutoNumber = ColumnImpl.INVALID_AUTO_NUMBER; + } + } + + /** + * Maintains the state of reading/updating a row of data. * @usage _advanced_class_ */ - public final class RowState implements ErrorHandler.Location + public final class RowState extends WriteRowState + implements ErrorHandler.Location { /** Buffer used for reading the header row data pages */ private final TempPageHolder _headerRowBufferH; @@ -2560,6 +2651,7 @@ public class TableImpl implements Table } public void reset() { + resetAutoNumber(); _finalRowId = null; _finalRowBuffer = null; _rowsOnHeaderPage = 0; |