From d1a79d0064632cca220409abb799ab1757c6caa7 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Tue, 30 Jul 2013 02:17:15 +0000 Subject: [PATCH] merge branch jackcess-2 changes through r759 git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@760 f203690c-595d-4dc9-a70b-905162fa7fd2 --- TODO.txt | 54 + pom.xml | 15 +- project.properties | 26 - project.xml | 118 - .../jackcess/BigIndexData.java | 86 - .../healthmarketscience/jackcess/Column.java | 2461 +-------------- .../jackcess/ColumnBuilder.java | 214 +- .../healthmarketscience/jackcess/Cursor.java | 1407 +-------- .../jackcess/CursorBuilder.java | 228 +- .../jackcess/DataType.java | 2 + .../jackcess/Database.java | 2712 +---------------- .../jackcess/DatabaseBuilder.java | 57 +- .../healthmarketscience/jackcess/Index.java | 439 +-- .../jackcess/IndexBuilder.java | 7 +- .../jackcess/IndexCursor.java | 554 +--- .../jackcess/PropertyMap.java | 122 +- .../jackcess/Relationship.java | 123 +- .../jackcess/{LinkResolver.java => Row.java} | 14 +- .../healthmarketscience/jackcess/RowId.java | 112 +- .../jackcess/RuntimeIOException.java | 42 + .../jackcess/SimpleIndexData.java | 241 -- .../healthmarketscience/jackcess/Table.java | 2553 +--------------- .../jackcess/TableBuilder.java | 103 +- .../complex/AttachmentColumnInfo.java | 493 +-- .../jackcess/complex/ComplexColumnInfo.java | 425 +-- .../jackcess/complex/ComplexValue.java | 91 +- .../complex/ComplexValueForeignKey.java | 323 +- .../complex/MultiValueColumnInfo.java | 110 +- .../complex/UnsupportedColumnInfo.java | 107 +- .../complex/VersionHistoryColumnInfo.java | 212 +- .../jackcess/{ => impl}/ByteUtil.java | 4 +- .../jackcess/{ => impl}/CodecHandler.java | 18 +- .../jackcess/{ => impl}/CodecProvider.java | 2 +- .../jackcess/impl/ColumnImpl.java | 2280 ++++++++++++++ .../jackcess/impl/ComplexColumnSupport.java | 201 ++ .../jackcess/impl/CursorImpl.java | 961 ++++++ .../jackcess/impl/DatabaseImpl.java | 2114 +++++++++++++ .../{ => impl}/DefaultCodecProvider.java | 20 +- .../jackcess/{ => impl}/FKEnforcer.java | 40 +- .../{ => impl}/GeneralIndexCodes.java | 6 +- .../{ => impl}/GeneralLegacyIndexCodes.java | 12 +- .../jackcess/{ => impl}/IndexCodes.java | 2 +- .../jackcess/impl/IndexCursorImpl.java | 510 ++++ .../jackcess/{ => impl}/IndexData.java | 278 +- .../jackcess/impl/IndexImpl.java | 458 +++ .../jackcess/{ => impl}/IndexPageCache.java | 78 +- .../jackcess/{ => impl}/JetFormat.java | 29 +- .../jackcess/{ => impl}/NullMask.java | 6 +- .../jackcess/{ => impl}/PageChannel.java | 111 +- .../jackcess/{ => impl}/PageTypes.java | 2 +- .../jackcess/impl/PropertyMapImpl.java | 146 + .../jackcess/{ => impl}/PropertyMaps.java | 63 +- .../jackcess/impl/RelationshipImpl.java | 152 + .../jackcess/impl/RowIdImpl.java | 138 + .../jackcess/impl/RowImpl.java | 65 + .../jackcess/{ => impl}/TableCreator.java | 76 +- .../jackcess/impl/TableImpl.java | 2589 ++++++++++++++++ .../jackcess/impl/TableScanCursor.java | 220 ++ .../jackcess/{ => impl}/TempBufferHolder.java | 2 +- .../jackcess/{ => impl}/TempPageHolder.java | 2 +- .../{ => impl}/UnsupportedCodecException.java | 5 +- .../jackcess/{ => impl}/UsageMap.java | 56 +- .../complex/AttachmentColumnInfoImpl.java | 482 +++ .../impl/complex/ComplexColumnInfoImpl.java | 419 +++ .../complex/ComplexValueForeignKeyImpl.java | 289 ++ .../complex/MultiValueColumnInfoImpl.java | 125 + .../complex/UnsupportedColumnInfoImpl.java | 141 + .../complex/VersionHistoryColumnInfoImpl.java | 224 ++ .../jackcess/impl/query/AppendQueryImpl.java | 92 + .../impl/query/BaseSelectQueryImpl.java | 177 ++ .../impl/query/CrossTabQueryImpl.java | 100 + .../impl/query/DataDefinitionQueryImpl.java | 65 + .../jackcess/impl/query/DeleteQueryImpl.java | 54 + .../impl/query/MakeTableQueryImpl.java | 73 + .../impl/query/PassthroughQueryImpl.java | 69 + .../{ => impl}/query/QueryFormat.java | 2 +- .../jackcess/impl/query/QueryImpl.java | 721 +++++ .../jackcess/impl/query/SelectQueryImpl.java | 53 + .../jackcess/impl/query/UnionQueryImpl.java | 96 + .../jackcess/impl/query/UpdateQueryImpl.java | 94 + .../jackcess/{ => impl}/scsu/Compress.java | 2 +- .../jackcess/{ => impl}/scsu/Debug.java | 2 +- .../{ => impl}/scsu/EndOfInputException.java | 2 +- .../{ => impl}/scsu/EndOfOutputException.java | 2 +- .../jackcess/{ => impl}/scsu/Expand.java | 2 +- .../scsu/IllegalInputException.java | 2 +- .../jackcess/{ => impl}/scsu/SCSU.java | 2 +- .../jackcess/query/AppendQuery.java | 64 +- .../jackcess/query/BaseSelectQuery.java | 165 +- .../jackcess/query/CrossTabQuery.java | 71 +- .../jackcess/query/DataDefinitionQuery.java | 38 +- .../jackcess/query/DeleteQuery.java | 26 +- .../jackcess/query/MakeTableQuery.java | 45 +- .../jackcess/query/PassthroughQuery.java | 44 +- .../jackcess/query/Query.java | 665 +--- .../jackcess/query/SelectQuery.java | 26 +- .../jackcess/query/UnionQuery.java | 69 +- .../jackcess/query/UpdateQuery.java | 66 +- .../CaseInsensitiveColumnMatcher.java | 15 +- .../jackcess/{ => util}/ColumnMatcher.java | 4 +- .../{ => util}/DebugErrorHandler.java | 16 +- .../jackcess/util/EntryIterableBuilder.java | 114 + .../jackcess/{ => util}/ErrorHandler.java | 44 +- .../jackcess/{ => util}/ExportFilter.java | 6 +- .../jackcess/{ => util}/ExportUtil.java | 15 +- .../jackcess/{ => util}/ImportFilter.java | 7 +- .../jackcess/{ => util}/ImportUtil.java | 31 +- .../jackcess/util/IterableBuilder.java | 186 ++ .../jackcess/{ => util}/Joiner.java | 61 +- .../jackcess/util/LinkResolver.java | 54 + .../jackcess/{ => util}/MemFileChannel.java | 14 +- .../{ => util}/ReplacementErrorHandler.java | 10 +- .../jackcess/{ => util}/RowFilter.java | 48 +- .../{ => util}/SimpleColumnMatcher.java | 4 +- .../{ => util}/SimpleExportFilter.java | 3 +- .../{ => util}/SimpleImportFilter.java | 10 +- .../jackcess/BigIndexTest.java | 63 +- .../jackcess/ComplexColumnTest.java | 13 +- .../jackcess/CursorBuilderTest.java | 25 +- .../jackcess/CursorTest.java | 211 +- .../jackcess/DatabaseTest.java | 210 +- .../jackcess/IndexTest.java | 90 +- .../jackcess/PropertiesTest.java | 17 +- .../jackcess/RelationshipTest.java | 9 +- .../jackcess/TableTest.java | 86 +- .../jackcess/{ => impl}/CodecHandlerTest.java | 47 +- .../jackcess/{ => impl}/FKEnforcerTest.java | 28 +- .../jackcess/{ => impl}/IndexCodesTest.java | 40 +- .../jackcess/{ => impl}/JetFormatTest.java | 29 +- .../jackcess/{ => impl}/UsageMapTest.java | 13 +- .../{ => impl}/scsu/CompressMain.java | 2 +- .../{ => impl}/scsu/CompressTest.java | 2 +- .../jackcess/query/QueryTest.java | 21 +- .../jackcess/{ => util}/ErrorHandlerTest.java | 35 +- .../jackcess/{ => util}/ExportTest.java | 13 +- .../jackcess/{ => util}/ImportTest.java | 20 +- .../jackcess/{ => util}/JoinerTest.java | 47 +- .../{ => util}/MemFileChannelTest.java | 4 +- .../jackcess/{ => util}/RowFilterTest.java | 36 +- 139 files changed, 16179 insertions(+), 14190 deletions(-) delete mode 100644 project.properties delete mode 100644 project.xml delete mode 100644 src/java/com/healthmarketscience/jackcess/BigIndexData.java rename src/java/com/healthmarketscience/jackcess/{LinkResolver.java => Row.java} (70%) create mode 100644 src/java/com/healthmarketscience/jackcess/RuntimeIOException.java delete mode 100644 src/java/com/healthmarketscience/jackcess/SimpleIndexData.java rename src/java/com/healthmarketscience/jackcess/{ => impl}/ByteUtil.java (99%) rename src/java/com/healthmarketscience/jackcess/{ => impl}/CodecHandler.java (76%) rename src/java/com/healthmarketscience/jackcess/{ => impl}/CodecProvider.java (97%) create mode 100644 src/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/ComplexColumnSupport.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/CursorImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java rename src/java/com/healthmarketscience/jackcess/{ => impl}/DefaultCodecProvider.java (88%) rename src/java/com/healthmarketscience/jackcess/{ => impl}/FKEnforcer.java (87%) rename src/java/com/healthmarketscience/jackcess/{ => impl}/GeneralIndexCodes.java (92%) rename src/java/com/healthmarketscience/jackcess/{ => impl}/GeneralLegacyIndexCodes.java (98%) rename src/java/com/healthmarketscience/jackcess/{ => impl}/IndexCodes.java (97%) create mode 100644 src/java/com/healthmarketscience/jackcess/impl/IndexCursorImpl.java rename src/java/com/healthmarketscience/jackcess/{ => impl}/IndexData.java (90%) create mode 100644 src/java/com/healthmarketscience/jackcess/impl/IndexImpl.java rename src/java/com/healthmarketscience/jackcess/{ => impl}/IndexPageCache.java (95%) rename src/java/com/healthmarketscience/jackcess/{ => impl}/JetFormat.java (97%) rename src/java/com/healthmarketscience/jackcess/{ => impl}/NullMask.java (95%) rename src/java/com/healthmarketscience/jackcess/{ => impl}/PageChannel.java (85%) rename src/java/com/healthmarketscience/jackcess/{ => impl}/PageTypes.java (97%) create mode 100644 src/java/com/healthmarketscience/jackcess/impl/PropertyMapImpl.java rename src/java/com/healthmarketscience/jackcess/{ => impl}/PropertyMaps.java (84%) create mode 100644 src/java/com/healthmarketscience/jackcess/impl/RelationshipImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/RowIdImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/RowImpl.java rename src/java/com/healthmarketscience/jackcess/{ => impl}/TableCreator.java (82%) create mode 100644 src/java/com/healthmarketscience/jackcess/impl/TableImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/TableScanCursor.java rename src/java/com/healthmarketscience/jackcess/{ => impl}/TempBufferHolder.java (99%) rename src/java/com/healthmarketscience/jackcess/{ => impl}/TempPageHolder.java (99%) rename src/java/com/healthmarketscience/jackcess/{ => impl}/UnsupportedCodecException.java (90%) rename src/java/com/healthmarketscience/jackcess/{ => impl}/UsageMap.java (95%) create mode 100644 src/java/com/healthmarketscience/jackcess/impl/complex/AttachmentColumnInfoImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/complex/ComplexColumnInfoImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/complex/ComplexValueForeignKeyImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/complex/MultiValueColumnInfoImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/complex/UnsupportedColumnInfoImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/complex/VersionHistoryColumnInfoImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/query/AppendQueryImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/query/BaseSelectQueryImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/query/CrossTabQueryImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/query/DataDefinitionQueryImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/query/DeleteQueryImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/query/MakeTableQueryImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/query/PassthroughQueryImpl.java rename src/java/com/healthmarketscience/jackcess/{ => impl}/query/QueryFormat.java (99%) create mode 100644 src/java/com/healthmarketscience/jackcess/impl/query/QueryImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/query/SelectQueryImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/query/UnionQueryImpl.java create mode 100644 src/java/com/healthmarketscience/jackcess/impl/query/UpdateQueryImpl.java rename src/java/com/healthmarketscience/jackcess/{ => impl}/scsu/Compress.java (99%) rename src/java/com/healthmarketscience/jackcess/{ => impl}/scsu/Debug.java (98%) rename src/java/com/healthmarketscience/jackcess/{ => impl}/scsu/EndOfInputException.java (96%) rename src/java/com/healthmarketscience/jackcess/{ => impl}/scsu/EndOfOutputException.java (96%) rename src/java/com/healthmarketscience/jackcess/{ => impl}/scsu/Expand.java (99%) rename src/java/com/healthmarketscience/jackcess/{ => impl}/scsu/IllegalInputException.java (96%) rename src/java/com/healthmarketscience/jackcess/{ => impl}/scsu/SCSU.java (99%) rename src/java/com/healthmarketscience/jackcess/{ => util}/CaseInsensitiveColumnMatcher.java (78%) rename src/java/com/healthmarketscience/jackcess/{ => util}/ColumnMatcher.java (93%) rename src/java/com/healthmarketscience/jackcess/{ => util}/DebugErrorHandler.java (81%) create mode 100644 src/java/com/healthmarketscience/jackcess/util/EntryIterableBuilder.java rename src/java/com/healthmarketscience/jackcess/{ => util}/ErrorHandler.java (63%) rename src/java/com/healthmarketscience/jackcess/{ => util}/ExportFilter.java (91%) rename src/java/com/healthmarketscience/jackcess/{ => util}/ExportUtil.java (96%) rename src/java/com/healthmarketscience/jackcess/{ => util}/ImportFilter.java (88%) rename src/java/com/healthmarketscience/jackcess/{ => util}/ImportUtil.java (95%) create mode 100644 src/java/com/healthmarketscience/jackcess/util/IterableBuilder.java rename src/java/com/healthmarketscience/jackcess/{ => util}/Joiner.java (83%) create mode 100644 src/java/com/healthmarketscience/jackcess/util/LinkResolver.java rename src/java/com/healthmarketscience/jackcess/{ => util}/MemFileChannel.java (96%) rename src/java/com/healthmarketscience/jackcess/{ => util}/ReplacementErrorHandler.java (86%) rename src/java/com/healthmarketscience/jackcess/{ => util}/RowFilter.java (79%) rename src/java/com/healthmarketscience/jackcess/{ => util}/SimpleColumnMatcher.java (89%) rename src/java/com/healthmarketscience/jackcess/{ => util}/SimpleExportFilter.java (93%) rename src/java/com/healthmarketscience/jackcess/{ => util}/SimpleImportFilter.java (83%) rename test/src/java/com/healthmarketscience/jackcess/{ => impl}/CodecHandlerTest.java (83%) rename test/src/java/com/healthmarketscience/jackcess/{ => impl}/FKEnforcerTest.java (80%) rename test/src/java/com/healthmarketscience/jackcess/{ => impl}/IndexCodesTest.java (95%) rename test/src/java/com/healthmarketscience/jackcess/{ => impl}/JetFormatTest.java (87%) rename test/src/java/com/healthmarketscience/jackcess/{ => impl}/UsageMapTest.java (84%) rename test/src/java/com/healthmarketscience/jackcess/{ => impl}/scsu/CompressMain.java (99%) rename test/src/java/com/healthmarketscience/jackcess/{ => impl}/scsu/CompressTest.java (95%) rename test/src/java/com/healthmarketscience/jackcess/{ => util}/ErrorHandlerTest.java (80%) rename test/src/java/com/healthmarketscience/jackcess/{ => util}/ExportTest.java (92%) rename test/src/java/com/healthmarketscience/jackcess/{ => util}/ImportTest.java (94%) rename test/src/java/com/healthmarketscience/jackcess/{ => util}/JoinerTest.java (80%) rename test/src/java/com/healthmarketscience/jackcess/{ => util}/MemFileChannelTest.java (97%) rename test/src/java/com/healthmarketscience/jackcess/{ => util}/RowFilterTest.java (72%) diff --git a/TODO.txt b/TODO.txt index d8472b8..04a0aea 100644 --- a/TODO.txt +++ b/TODO.txt @@ -20,3 +20,57 @@ Missing pieces: * EASY - figure out how msaccess manages page/row locks * MEDIUM + +Refactor goals: +- simplify public API (separate "internal" and "external" api) +* separate table creation objects from existing metadata objects +* remove "simple" index support? +* remove "table traversal methods" from Table? +* enable integrity by default? +* remove import/export methods from Database? +* move database open/create options to DBBuilder +* tweak how import filters work to make them more flexible? +- tweak lookup apis (specify column vs column name) +* separate classes into more packages (api,builder,util,impl) +* remove debug log blocks +* add Row interface +* change savepoint to use table number instead of name? +* don't use columnimpl for creating tables + * clean up columnimpl/tableimpl constructors +* add updateCurrentRow(Map), add updateRow(Row) +* sort out query types +- clean up javadocs + - enhance public api classes + - add @usage tags to util classes +* add unit tests for Row update/delete methods, add/update *FromMap methods +* add reason to unsupop throws for indexes +* remove static methods in CursorImpl/IndexCursorImpl +* create ComplexValue.Id and keep RowId +* remove DatabaseImpl from util classes +- remove unnecessary iterator class from impl classes? (what does this mean?) +* change CodecHandler usage to handle not-inline decoding + - pass filename to CodecHandler, enable pwd callbacks CallbackHandler + - pass custom context to CodecHandler? + - rework CryptCodecProvider to have custom + javax.security.auth.callback.CallbackHandler which is only invoked if + password is definitely required. +* rework attachment data handling +- implement page buffering in PageChannel + * need to implement logical flushing in update code (startUpdate/finishUpdate) +* limit size of IndexPageCache? +- make non-thread-safeness more explicit +- refactor free-space handlers Table/Column? + +* public api final cleanup: + * Database + +- changes + - simple index support gone + - foreign key constraints enforced by default + - "main" classes became interfaces + - advanced API functionality still remains in impl classes + - all new instance construction via builders + - iterable methods went away, iterable builder + - util classes moved to util package + - Row is now an interface + diff --git a/pom.xml b/pom.xml index f9c7020..3f46735 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ jackcess Jackcess A pure Java library for reading from and writing to MS Access databases. - 1.2.15-SNAPSHOT + 2.0.0-SNAPSHOT http://jackcess.sf.net 2005 @@ -118,8 +118,11 @@ org.apache.maven.plugins maven-surefire-plugin + 2.15 once + classes + 1 -Xmx256M -server @@ -164,7 +167,7 @@ - com/healthmarketscience/jackcess/scsu/** + com/healthmarketscience/jackcess/impl/scsu/** @@ -231,7 +234,7 @@ junit junit - 4.0 + 4.11 test @@ -254,11 +257,11 @@ 128m 512 - http://download.oracle.com/javase/1.5.0/docs/api - http://download.oracle.com/javaee/5/api + http://docs.oracle.com/javase/1.5.0/docs/api/ + http://docs.oracle.com/javaee/5/api/ 1.5 - com.healthmarketscience.jackcess.scsu + com.healthmarketscience.jackcess.impl.scsu public ${basedir}/src/site/javadoc/stylesheet.css diff --git a/project.properties b/project.properties deleted file mode 100644 index 7156f93..0000000 --- a/project.properties +++ /dev/null @@ -1,26 +0,0 @@ -maven.announcement.mail.server=localhost -maven.announcement.mail.to=jackcess-users@lists.sourceforge.net -maven.artifact.legacy=true -maven.changes.issue.template=http://sf.net/tracker/index.php?func=detail&aid=%ISSUE%&group_id=134943&atid=731445 -maven.compile.compilerargs=-Xlint:all -maven.compile.source=1.5 -maven.compile.target=1.5 -maven.javadoc.excludepackagenames=com.healthmarketscience.jackcess.scsu -maven.javadoc.links=http://java.sun.com/j2se/1.5.0/docs/api -maven.javadoc.package=false -maven.javadoc.public=true -maven.javadoc.source=1.5 -maven.junit.fork=on -maven.junit.jvmargs=-Xmx256M -server -maven.junit.sysproperties=log4j.configuration -maven.repo.remote=http://www.ibiblio.org/maven,http://maven-plugins.sf.net/maven -maven.site.stage.address=shell.sourceforge.net -maven.sourceforge.project.groupId=134943 -maven.sourceforge.username=javajedi -maven.sourceforge.project.submitNewsItem=true -maven.sourceforge.publish.includes=distributions/*-src.zip,*.jar -maven.sourceforge.project.releaseNotes=${maven.build.dir}/CHANGES.txt -maven.test.source=1.5 -maven.username=javajedi -log4j.configuration=com/healthmarketscience/jackcess/log4j.properties -statcvs.include=**/*.java;**/*.xml diff --git a/project.xml b/project.xml deleted file mode 100644 index fa5f6c7..0000000 --- a/project.xml +++ /dev/null @@ -1,118 +0,0 @@ - - - 3 - jackcess - jackcess - Jackcess - 1.1.10 - - Health Market Science, Inc. - http://www.healthmarketscience.com - http://www.healthmarketscience.com/images/HMS_logo.gif - - 2005 - com.healthmarketscience.jackcess - A pure Java library for reading from and writing to MS Access databases. - http://jackcess.sf.net - http://sf.net/tracker/?group_id=134943&atid=731445 - jackcess.sf.net - /home/groups/j/ja/jackcess/htdocs - - scm:cvs:pserver:anonymous@jackcess.cvs.sf.net:/cvsroot/jackcess:jackcess - http://jackcess.cvs.sourceforge.net/jackcess/jackcess/ - - - - jackcess-users - http://lists.sf.net/lists/listinfo/jackcess-users - http://lists.sf.net/lists/listinfo/jackcess-users - http://sf.net/mailarchive/forum.php?forum=jackcess-users - - - - - Tim McCune - javajedi - javajedi@users.sf.net - Health Market Science, Inc. - -5 - - - James Ahlborn - jahlborn - jahlborn@users.sf.net - Health Market Science, Inc. - -5 - - - Rob Di Marco - Health Market Science, Inc. - -5 - - - Dan Rollo - bhamail - bhamail@users.sf.net - Composite Software, Inc. - -5 - - - - - GNU Lesser General Public License - http://www.gnu.org/copyleft/lesser.txt - manual - - - - src/java - test/src/java - - - src/resources - - - - - - commons-collections - commons-collections - 3.0 - - - commons-lang - commons-lang - 2.0 - - - commons-logging - commons-logging - 1.0.3 - - - log4j - log4j - 1.2.7 - - - maven-plugins - maven-sourceforge-plugin - 1.3 - plugin - - - statcvs - maven-statcvs-plugin - 2.5 - plugin - - - - maven-faq-plugin - maven-changes-plugin - maven-javadoc-plugin - maven-jxr-plugin - maven-jdepend-plugin - maven-statcvs-plugin - - diff --git a/src/java/com/healthmarketscience/jackcess/BigIndexData.java b/src/java/com/healthmarketscience/jackcess/BigIndexData.java deleted file mode 100644 index c06af26..0000000 --- a/src/java/com/healthmarketscience/jackcess/BigIndexData.java +++ /dev/null @@ -1,86 +0,0 @@ -/* -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; - - -/** - * Implementation of an Access table index which supports large indexes. - * @author James Ahlborn - */ -public class BigIndexData extends IndexData { - - /** Cache which manages the index pages */ - private final IndexPageCache _pageCache; - - public BigIndexData(Table table, int number, int uniqueEntryCount, - int uniqueEntryCountOffset) { - super(table, number, uniqueEntryCount, uniqueEntryCountOffset); - _pageCache = new IndexPageCache(this); - } - - @Override - protected void updateImpl() throws IOException { - _pageCache.write(); - } - - @Override - protected void readIndexEntries() - throws IOException - { - _pageCache.setRootPageNumber(getRootPageNumber()); - } - - @Override - protected DataPage findDataPage(Entry entry) - throws IOException - { - return _pageCache.findCacheDataPage(entry); - } - - @Override - protected DataPage getDataPage(int pageNumber) - throws IOException - { - return _pageCache.getCacheDataPage(pageNumber); - } - - @Override - public String toString() { - return super.toString() + "\n" + _pageCache.toString(); - } - - /** - * Used by unit tests to validate the internal status of the index. - */ - void validate() throws IOException { - _pageCache.validate(); - } - -} diff --git a/src/java/com/healthmarketscience/jackcess/Column.java b/src/java/com/healthmarketscience/jackcess/Column.java index 69412a8..82268e5 100644 --- a/src/java/com/healthmarketscience/jackcess/Column.java +++ b/src/java/com/healthmarketscience/jackcess/Column.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2005 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,61 +15,24 @@ 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.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.ObjectOutputStream; -import java.io.ObjectStreamException; -import java.io.Reader; -import java.io.Serializable; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.CharBuffer; -import java.nio.charset.Charset; -import java.sql.Blob; -import java.sql.Clob; import java.sql.SQLException; -import java.util.Calendar; -import java.util.Date; -import java.util.List; import java.util.Map; -import java.util.UUID; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import com.healthmarketscience.jackcess.complex.ComplexColumnInfo; import com.healthmarketscience.jackcess.complex.ComplexValue; -import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; -import com.healthmarketscience.jackcess.scsu.Compress; -import com.healthmarketscience.jackcess.scsu.EndOfInputException; -import com.healthmarketscience.jackcess.scsu.Expand; -import com.healthmarketscience.jackcess.scsu.IllegalInputException; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; /** * Access database column definition - * @author Tim McCune - * @usage _general_class_ + * + * @author James Ahlborn */ -public class Column implements Comparable { - - private static final Log LOG = LogFactory.getLog(Column.class); - +public interface Column +{ /** * Meaningless placeholder object for inserting values in an autonumber * column. it is not required that this value be used (any passed in value @@ -85,2473 +48,109 @@ public class Column implements Comparable { */ public static final Object KEEP_VALUE = ""; - /** - * Access stores numeric dates in days. Java stores them in milliseconds. - */ - private static final double MILLISECONDS_PER_DAY = - (24L * 60L * 60L * 1000L); - - /** - * Access starts counting dates at Jan 1, 1900. Java starts counting - * at Jan 1, 1970. This is the # of millis between them for conversion. - */ - private static final long MILLIS_BETWEEN_EPOCH_AND_1900 = - 25569L * (long)MILLISECONDS_PER_DAY; - - /** - * Long value (LVAL) type that indicates that the value is stored on the - * same page - */ - private static final byte LONG_VALUE_TYPE_THIS_PAGE = (byte) 0x80; - /** - * Long value (LVAL) type that indicates that the value is stored on another - * page - */ - private static final byte LONG_VALUE_TYPE_OTHER_PAGE = (byte) 0x40; - /** - * Long value (LVAL) type that indicates that the value is stored on - * multiple other pages - */ - private static final byte LONG_VALUE_TYPE_OTHER_PAGES = (byte) 0x00; - /** - * Mask to apply the long length in order to get the flag bits (only the - * first 2 bits are type flags). - */ - private static final int LONG_VALUE_TYPE_MASK = 0xC0000000; - - /** - * mask for the fixed len bit - * @usage _advanced_field_ - */ - public static final byte FIXED_LEN_FLAG_MASK = (byte)0x01; - - /** - * mask for the auto number bit - * @usage _advanced_field_ - */ - public static final byte AUTO_NUMBER_FLAG_MASK = (byte)0x04; - - /** - * mask for the auto number guid bit - * @usage _advanced_field_ - */ - public static final byte AUTO_NUMBER_GUID_FLAG_MASK = (byte)0x40; - - /** - * mask for the hyperlink bit (on memo types) - * @usage _advanced_field_ - */ - public static final byte HYPERLINK_FLAG_MASK = (byte)0x80; - - /** - * mask for the unknown bit (possible "can be null"?) - * @usage _advanced_field_ - */ - public static final byte UNKNOWN_FLAG_MASK = (byte)0x02; - - // some other flags? - // 0x10: replication related field (or hidden?) - // 0x80: hyperlink (some memo based thing) - - /** the value for the "general" sort order */ - private static final short GENERAL_SORT_ORDER_VALUE = 1033; - - /** - * the "general" text sort order, legacy version (access 2000-2007) - * @usage _intermediate_field_ - */ - public static final SortOrder GENERAL_LEGACY_SORT_ORDER = - new SortOrder(GENERAL_SORT_ORDER_VALUE, (byte)0); - - /** - * the "general" text sort order, latest version (access 2010+) - * @usage _intermediate_field_ - */ - public static final SortOrder GENERAL_SORT_ORDER = - new SortOrder(GENERAL_SORT_ORDER_VALUE, (byte)1); - - /** pattern matching textual guid strings (allows for optional surrounding - '{' and '}') */ - private static final Pattern GUID_PATTERN = Pattern.compile("\\s*[{]?([\\p{XDigit}]{8})-([\\p{XDigit}]{4})-([\\p{XDigit}]{4})-([\\p{XDigit}]{4})-([\\p{XDigit}]{12})[}]?\\s*"); - - /** header used to indicate unicode text compression */ - private static final byte[] TEXT_COMPRESSION_HEADER = - { (byte)0xFF, (byte)0XFE }; - - /** placeholder for column which is not numeric */ - private static final NumericInfo DEFAULT_NUMERIC_INFO = new NumericInfo(); - - /** placeholder for column which is not textual */ - private static final TextInfo DEFAULT_TEXT_INFO = new TextInfo(); - - - /** owning table */ - private final Table _table; - /** Whether or not the column is of variable length */ - private boolean _variableLength; - /** Whether or not the column is an autonumber column */ - private boolean _autoNumber; - /** Data type */ - private DataType _type; - /** Maximum column length */ - private short _columnLength; - /** 0-based column number */ - private short _columnNumber; - /** index of the data for this column within a list of row data */ - private int _columnIndex; - /** display index of the data for this column */ - private int _displayIndex; - /** Column name */ - private String _name; - /** the offset of the fixed data in the row */ - private int _fixedDataOffset; - /** the index of the variable length data in the var len offset table */ - private int _varLenTableIndex; - /** information specific to numeric columns */ - private NumericInfo _numericInfo = DEFAULT_NUMERIC_INFO; - /** information specific to text columns */ - private TextInfo _textInfo = DEFAULT_TEXT_INFO; - /** the auto number generator for this column (if autonumber column) */ - private AutoNumberGenerator _autoNumberGenerator; - /** additional information specific to complex columns */ - private ComplexColumnInfo _complexInfo; - /** properties for this column, if any */ - private PropertyMap _props; - /** Holds additional info for writing long values */ - private LongValueBufferHolder _lvalBufferH; - - /** - * @usage _general_method_ - */ - public Column() { - this(null); - } - - /** - * @usage _advanced_method_ - */ - public Column(JetFormat format) { - _table = null; - } - - /** - * Only used by unit tests - */ - Column(boolean testing, Table table) { - if(!testing) { - throw new IllegalArgumentException(); - } - _table = table; - } - - /** - * Read a column definition in from a buffer - * @param table owning table - * @param buffer Buffer containing column definition - * @param offset Offset in the buffer at which the column definition starts - * @usage _advanced_method_ - */ - public Column(Table table, ByteBuffer buffer, int offset, int displayIndex) - throws IOException - { - _table = table; - _displayIndex = displayIndex; - if (LOG.isDebugEnabled()) { - LOG.debug("Column def block:\n" + ByteUtil.toHexString(buffer, offset, 25)); - } - - byte colType = buffer.get(offset + getFormat().OFFSET_COLUMN_TYPE); - _columnNumber = buffer.getShort(offset + getFormat().OFFSET_COLUMN_NUMBER); - _columnLength = buffer.getShort(offset + getFormat().OFFSET_COLUMN_LENGTH); - - byte flags = buffer.get(offset + getFormat().OFFSET_COLUMN_FLAGS); - _variableLength = ((flags & FIXED_LEN_FLAG_MASK) == 0); - _autoNumber = ((flags & (AUTO_NUMBER_FLAG_MASK | AUTO_NUMBER_GUID_FLAG_MASK)) != 0); - - try { - _type = DataType.fromByte(colType); - } catch(IOException e) { - LOG.warn("Unsupported column type " + colType); - _type = (_variableLength ? DataType.UNSUPPORTED_VARLEN : - DataType.UNSUPPORTED_FIXEDLEN); - setUnknownDataType(colType); - } - - if (_type.getHasScalePrecision()) { - modifyNumericInfo(); - _numericInfo._precision = buffer.get(offset + - getFormat().OFFSET_COLUMN_PRECISION); - _numericInfo._scale = buffer.get(offset + getFormat().OFFSET_COLUMN_SCALE); - } else if(_type.isTextual()) { - modifyTextInfo(); - - // co-located w/ precision/scale - _textInfo._sortOrder = readSortOrder( - buffer, offset + getFormat().OFFSET_COLUMN_SORT_ORDER, getFormat()); - int cpOffset = getFormat().OFFSET_COLUMN_CODE_PAGE; - if(cpOffset >= 0) { - _textInfo._codePage = buffer.getShort(offset + cpOffset); - } - - _textInfo._compressedUnicode = ((buffer.get(offset + - getFormat().OFFSET_COLUMN_COMPRESSED_UNICODE) & 1) == 1); - - if(_type == DataType.MEMO) { - // only memo fields can be hyperlinks - _textInfo._hyperlink = ((flags & HYPERLINK_FLAG_MASK) != 0); - } - } - - setAutoNumberGenerator(); - - if(_variableLength) { - _varLenTableIndex = buffer.getShort(offset + getFormat().OFFSET_COLUMN_VARIABLE_TABLE_INDEX); - } else { - _fixedDataOffset = buffer.getShort(offset + getFormat().OFFSET_COLUMN_FIXED_DATA_OFFSET); - } - - // load complex info - if(_type == DataType.COMPLEX_TYPE) { - _complexInfo = ComplexColumnInfo.create(this, buffer, offset); - } - } - - /** - * Sets the usage maps for this column. - */ - void setUsageMaps(UsageMap ownedPages, UsageMap freeSpacePages) { - _lvalBufferH = new UmapLongValueBufferHolder(ownedPages, freeSpacePages); - } - - /** - * Secondary column initialization after the table is fully loaded. - */ - void postTableLoadInit() throws IOException { - if(getType().isLongValue() && (_lvalBufferH == null)) { - _lvalBufferH = new LegacyLongValueBufferHolder(); - } - if(_complexInfo != null) { - _complexInfo.postTableLoadInit(); - } - } - /** * @usage _general_method_ */ - public Table getTable() { - return _table; - } + public Table getTable(); /** * @usage _general_method_ */ - public Database getDatabase() { - return getTable().getDatabase(); - } - - /** - * @usage _advanced_method_ - */ - public JetFormat getFormat() { - return getDatabase().getFormat(); - } + public Database getDatabase(); - /** - * @usage _advanced_method_ - */ - public PageChannel getPageChannel() { - return getDatabase().getPageChannel(); - } - /** * @usage _general_method_ */ - public String getName() { - return _name; - } + public String getName(); /** * @usage _advanced_method_ */ - public void setName(String name) { - _name = name; - } - - /** - * @usage _advanced_method_ - */ - public boolean isVariableLength() { - return _variableLength; - } - - /** - * @usage _advanced_method_ - */ - public void setVariableLength(boolean variableLength) { - _variableLength = variableLength; - } - - /** - * @usage _general_method_ - */ - public boolean isAutoNumber() { - return _autoNumber; - } + public boolean isVariableLength(); /** * @usage _general_method_ */ - public void setAutoNumber(boolean autoNumber) { - _autoNumber = autoNumber; - setAutoNumberGenerator(); - } + public boolean isAutoNumber(); /** * @usage _advanced_method_ */ - public short getColumnNumber() { - return _columnNumber; - } - - /** - * @usage _advanced_method_ - */ - public void setColumnNumber(short newColumnNumber) { - _columnNumber = newColumnNumber; - } - - /** - * @usage _advanced_method_ - */ - public int getColumnIndex() { - return _columnIndex; - } - - /** - * @usage _advanced_method_ - */ - public void setColumnIndex(int newColumnIndex) { - _columnIndex = newColumnIndex; - } - - /** - * @usage _advanced_method_ - */ - public int getDisplayIndex() { - return _displayIndex; - } - - /** - * Also sets the length and the variable length flag, inferred from the - * type. For types with scale/precision, sets the scale and precision to - * default values. - * @usage _general_method_ - */ - public void setType(DataType type) { - _type = type; - if(!type.isVariableLength()) { - setLength((short)type.getFixedSize()); - } else if(!type.isLongValue()) { - setLength((short)type.getDefaultSize()); - } - setVariableLength(type.isVariableLength()); - if(type.getHasScalePrecision()) { - setScale((byte)type.getDefaultScale()); - setPrecision((byte)type.getDefaultPrecision()); - } - } - - /** - * @usage _general_method_ - */ - public DataType getType() { - return _type; - } - - /** - * @usage _general_method_ - */ - public int getSQLType() throws SQLException { - return _type.getSQLType(); - } - - /** - * @usage _general_method_ - */ - public void setSQLType(int type) throws SQLException { - setSQLType(type, 0); - } - - /** - * @usage _general_method_ - */ - public void setSQLType(int type, int lengthInUnits) throws SQLException { - setType(DataType.fromSQLType(type, lengthInUnits)); - } - - /** - * @usage _general_method_ - */ - public boolean isCompressedUnicode() { - return _textInfo._compressedUnicode; - } + public int getColumnIndex(); /** * @usage _general_method_ */ - public void setCompressedUnicode(boolean newCompessedUnicode) { - modifyTextInfo(); - _textInfo._compressedUnicode = newCompessedUnicode; - } + public DataType getType(); /** * @usage _general_method_ */ - public byte getPrecision() { - return _numericInfo._precision; - } - - /** - * @usage _general_method_ - */ - public void setPrecision(byte newPrecision) { - modifyNumericInfo(); - _numericInfo._precision = newPrecision; - } - - /** - * @usage _general_method_ - */ - public byte getScale() { - return _numericInfo._scale; - } + public int getSQLType() throws SQLException; /** * @usage _general_method_ */ - public void setScale(byte newScale) { - modifyNumericInfo(); - _numericInfo._scale = newScale; - } - - /** - * @usage _intermediate_method_ - */ - public SortOrder getTextSortOrder() { - return _textInfo._sortOrder; - } + public boolean isCompressedUnicode(); - /** - * @usage _advanced_method_ - */ - public void setTextSortOrder(SortOrder newTextSortOrder) { - modifyTextInfo(); - _textInfo._sortOrder = newTextSortOrder; - } - - /** - * @usage _intermediate_method_ - */ - public short getTextCodePage() { - return _textInfo._codePage; - } - /** * @usage _general_method_ */ - public void setLength(short length) { - _columnLength = length; - } + public byte getPrecision(); /** * @usage _general_method_ */ - public short getLength() { - return _columnLength; - } + public byte getScale(); /** * @usage _general_method_ */ - public void setLengthInUnits(short unitLength) { - setLength((short)getType().fromUnitSize(unitLength)); - } + public short getLength(); /** * @usage _general_method_ */ - public short getLengthInUnits() { - return (short)getType().toUnitSize(getLength()); - } - - /** - * @usage _advanced_method_ - */ - public void setVarLenTableIndex(int idx) { - _varLenTableIndex = idx; - } - - /** - * @usage _advanced_method_ - */ - public int getVarLenTableIndex() { - return _varLenTableIndex; - } - - /** - * @usage _advanced_method_ - */ - public void setFixedDataOffset(int newOffset) { - _fixedDataOffset = newOffset; - } - - /** - * @usage _advanced_method_ - */ - public int getFixedDataOffset() { - return _fixedDataOffset; - } - - Charset getCharset() { - return getDatabase().getCharset(); - } - - Calendar getCalendar() { - return getDatabase().getCalendar(); - } - - /** - * Returns the number of database pages owned by this column. - * @usage _intermediate_method_ - */ - public int getOwnedPageCount() { - return ((_lvalBufferH == null) ? 0 : _lvalBufferH.getOwnedPageCount()); - } + public short getLengthInUnits(); /** * Whether or not this column is "append only" (its history is tracked by a * separate version history column). * @usage _general_method_ */ - public boolean isAppendOnly() { - return (getVersionHistoryColumn() != null); - } - - /** - * Returns the column which tracks the version history for an "append only" - * column. - * @usage _intermediate_method_ - */ - public Column getVersionHistoryColumn() { - return _textInfo._versionHistoryCol; - } - - /** - * @usage _advanced_method_ - */ - public void setVersionHistoryColumn(Column versionHistoryCol) { - modifyTextInfo(); - _textInfo._versionHistoryCol = versionHistoryCol; - } + public boolean isAppendOnly(); /** * Returns whether or not this is a hyperlink column (only possible for * columns of type MEMO). * @usage _general_method_ */ - public boolean isHyperlink() { - return _textInfo._hyperlink; - } + public boolean isHyperlink(); - /** - * @usage _general_method_ - */ - public void setHyperlink(boolean hyperlink) { - modifyTextInfo(); - _textInfo._hyperlink = hyperlink; - } - /** * Returns extended functionality for "complex" columns. * @usage _general_method_ */ - public ComplexColumnInfo getComplexInfo() { - return _complexInfo; - } - - private void setUnknownDataType(byte type) { - // slight hack, stash the original type in the _scale - modifyNumericInfo(); - _numericInfo._scale = type; - } - - private byte getUnknownDataType() { - // slight hack, we stashed the real type in the _scale - return _numericInfo._scale; - } - - private void setAutoNumberGenerator() - { - if(!_autoNumber || (_type == null)) { - _autoNumberGenerator = null; - return; - } - - if((_autoNumberGenerator != null) && - (_autoNumberGenerator.getType() == _type)) { - // keep existing - return; - } - - switch(_type) { - case LONG: - _autoNumberGenerator = new LongAutoNumberGenerator(); - break; - case GUID: - _autoNumberGenerator = new GuidAutoNumberGenerator(); - break; - case COMPLEX_TYPE: - _autoNumberGenerator = new ComplexTypeAutoNumberGenerator(); - break; - default: - LOG.warn("Unknown auto number column type " + _type); - _autoNumberGenerator = new UnsupportedAutoNumberGenerator(_type); - } - } - - /** - * Returns the AutoNumberGenerator for this column if this is an autonumber - * column, {@code null} otherwise. - * @usage _advanced_method_ - */ - public AutoNumberGenerator getAutoNumberGenerator() { - return _autoNumberGenerator; - } + public ComplexColumnInfo getComplexInfo(); /** * @return the properties for this column * @usage _general_method_ */ - public PropertyMap getProperties() throws IOException { - if(_props == null) { - _props = getTable().getPropertyMaps().get(getName()); - } - return _props; - } - - private void modifyNumericInfo() { - if(_numericInfo == DEFAULT_NUMERIC_INFO) { - _numericInfo = new NumericInfo(); - } - } - - private void modifyTextInfo() { - if(_textInfo == DEFAULT_TEXT_INFO) { - _textInfo = new TextInfo(); - } - } - - /** - * Checks that this column definition is valid. - * - * @throws IllegalArgumentException if this column definition is invalid. - * @usage _advanced_method_ - */ - public void validate(JetFormat format) { - if(getType() == null) { - throw new IllegalArgumentException("must have type"); - } - Database.validateIdentifierName(getName(), format.MAX_COLUMN_NAME_LENGTH, - "column"); - - if(getType().isUnsupported()) { - throw new IllegalArgumentException( - "Cannot create column with unsupported type " + getType()); - } - if(!format.isSupportedDataType(getType())) { - throw new IllegalArgumentException( - "Database format " + format + " does not support type " + getType()); - } - - if(isVariableLength() != getType().isVariableLength()) { - throw new IllegalArgumentException("invalid variable length setting"); - } - - if(!isVariableLength()) { - if(getLength() != getType().getFixedSize()) { - if(getLength() < getType().getFixedSize()) { - throw new IllegalArgumentException("invalid fixed length size"); - } - LOG.warn("Column length " + getLength() + - " longer than expected fixed size " + - getType().getFixedSize()); - } - } else if(!getType().isLongValue()) { - if(!getType().isValidSize(getLength())) { - throw new IllegalArgumentException("var length out of range"); - } - } - - if(getType().getHasScalePrecision()) { - if(!getType().isValidScale(getScale())) { - throw new IllegalArgumentException( - "Scale must be from " + getType().getMinScale() + " to " + - getType().getMaxScale() + " inclusive"); - } - if(!getType().isValidPrecision(getPrecision())) { - throw new IllegalArgumentException( - "Precision must be from " + getType().getMinPrecision() + " to " + - getType().getMaxPrecision() + " inclusive"); - } - } - - if(isAutoNumber()) { - if(!getType().mayBeAutoNumber()) { - throw new IllegalArgumentException( - "Auto number column must be long integer or guid"); - } - } - - if(isCompressedUnicode()) { - if(!getType().isTextual()) { - throw new IllegalArgumentException( - "Only textual columns allow unicode compression (text/memo)"); - } - } - - if(isHyperlink()) { - if(getType() != DataType.MEMO) { - throw new IllegalArgumentException( - "Only memo columns can be hyperlinks"); - } - } - } - - public Object setRowValue(Object[] rowArray, Object value) { - rowArray[_columnIndex] = value; - return value; - } - - public Object setRowValue(Map rowMap, Object value) { - rowMap.put(_name, value); - return value; - } - - public Object getRowValue(Object[] rowArray) { - return rowArray[_columnIndex]; - } - - public Object getRowValue(Map rowMap) { - return rowMap.get(_name); - } - - /** - * Deserialize a raw byte value for this column into an Object - * @param data The raw byte value - * @return The deserialized Object - * @usage _advanced_method_ - */ - public Object read(byte[] data) throws IOException { - return read(data, PageChannel.DEFAULT_BYTE_ORDER); - } - - /** - * Deserialize a raw byte value for this column into an Object - * @param data The raw byte value - * @param order Byte order in which the raw value is stored - * @return The deserialized Object - * @usage _advanced_method_ - */ - public Object read(byte[] data, ByteOrder order) throws IOException { - ByteBuffer buffer = ByteBuffer.wrap(data); - buffer.order(order); - if (_type == DataType.BOOLEAN) { - throw new IOException("Tried to read a boolean from data instead of null mask."); - } else if (_type == DataType.BYTE) { - return Byte.valueOf(buffer.get()); - } else if (_type == DataType.INT) { - return Short.valueOf(buffer.getShort()); - } else if (_type == DataType.LONG) { - return Integer.valueOf(buffer.getInt()); - } else if (_type == DataType.DOUBLE) { - return Double.valueOf(buffer.getDouble()); - } else if (_type == DataType.FLOAT) { - return Float.valueOf(buffer.getFloat()); - } else if (_type == DataType.SHORT_DATE_TIME) { - return readDateValue(buffer); - } else if (_type == DataType.BINARY) { - return data; - } else if (_type == DataType.TEXT) { - return decodeTextValue(data); - } else if (_type == DataType.MONEY) { - return readCurrencyValue(buffer); - } else if (_type == DataType.OLE) { - if (data.length > 0) { - return readLongValue(data); - } - return null; - } else if (_type == DataType.MEMO) { - if (data.length > 0) { - return readLongStringValue(data); - } - return null; - } else if (_type == DataType.NUMERIC) { - return readNumericValue(buffer); - } else if (_type == DataType.GUID) { - return readGUIDValue(buffer, order); - } else if ((_type == DataType.UNKNOWN_0D) || - (_type == DataType.UNKNOWN_11)) { - // treat like "binary" data - return data; - } else if (_type == DataType.COMPLEX_TYPE) { - return new ComplexValueForeignKey(this, buffer.getInt()); - } else if(_type.isUnsupported()) { - return rawDataWrapper(data); - } else { - throw new IOException("Unrecognized data type: " + _type); - } - } - - /** - * @param lvalDefinition Column value that points to an LVAL record - * @return The LVAL data - */ - private byte[] readLongValue(byte[] lvalDefinition) - throws IOException - { - ByteBuffer def = PageChannel.wrap(lvalDefinition); - int lengthWithFlags = def.getInt(); - int length = lengthWithFlags & (~LONG_VALUE_TYPE_MASK); - - byte[] rtn = new byte[length]; - byte type = (byte)((lengthWithFlags & LONG_VALUE_TYPE_MASK) >>> 24); - - if(type == LONG_VALUE_TYPE_THIS_PAGE) { - - // inline long value - def.getInt(); //Skip over lval_dp - def.getInt(); //Skip over unknown - def.get(rtn); - - } else { - - // long value on other page(s) - if (lvalDefinition.length != getFormat().SIZE_LONG_VALUE_DEF) { - throw new IOException("Expected " + getFormat().SIZE_LONG_VALUE_DEF + - " bytes in long value definition, but found " + - lvalDefinition.length); - } - - int rowNum = ByteUtil.getUnsignedByte(def); - int pageNum = ByteUtil.get3ByteInt(def, def.position()); - ByteBuffer lvalPage = getPageChannel().createPageBuffer(); - - switch (type) { - case LONG_VALUE_TYPE_OTHER_PAGE: - { - getPageChannel().readPage(lvalPage, pageNum); - - short rowStart = Table.findRowStart(lvalPage, rowNum, getFormat()); - short rowEnd = Table.findRowEnd(lvalPage, rowNum, getFormat()); - - if((rowEnd - rowStart) != length) { - throw new IOException("Unexpected lval row length"); - } - - lvalPage.position(rowStart); - lvalPage.get(rtn); - } - break; - - case LONG_VALUE_TYPE_OTHER_PAGES: - - ByteBuffer rtnBuf = ByteBuffer.wrap(rtn); - int remainingLen = length; - while(remainingLen > 0) { - lvalPage.clear(); - getPageChannel().readPage(lvalPage, pageNum); - - short rowStart = Table.findRowStart(lvalPage, rowNum, getFormat()); - short rowEnd = Table.findRowEnd(lvalPage, rowNum, getFormat()); - - // read next page information - lvalPage.position(rowStart); - rowNum = ByteUtil.getUnsignedByte(lvalPage); - pageNum = ByteUtil.get3ByteInt(lvalPage); - - // update rowEnd and remainingLen based on chunkLength - int chunkLength = (rowEnd - rowStart) - 4; - if(chunkLength > remainingLen) { - rowEnd = (short)(rowEnd - (chunkLength - remainingLen)); - chunkLength = remainingLen; - } - remainingLen -= chunkLength; - - lvalPage.limit(rowEnd); - rtnBuf.put(lvalPage); - } - - break; - - default: - throw new IOException("Unrecognized long value type: " + type); - } - } - - return rtn; - } - - /** - * @param lvalDefinition Column value that points to an LVAL record - * @return The LVAL data - */ - private String readLongStringValue(byte[] lvalDefinition) - throws IOException - { - byte[] binData = readLongValue(lvalDefinition); - if(binData == null) { - return null; - } - return decodeTextValue(binData); - } - - /** - * Decodes "Currency" values. - * - * @param buffer Column value that points to currency data - * @return BigDecimal representing the monetary value - * @throws IOException if the value cannot be parsed - */ - private static BigDecimal readCurrencyValue(ByteBuffer buffer) - throws IOException - { - if(buffer.remaining() != 8) { - throw new IOException("Invalid money value."); - } - - return new BigDecimal(BigInteger.valueOf(buffer.getLong(0)), 4); - } - - /** - * Writes "Currency" values. - */ - private static void writeCurrencyValue(ByteBuffer buffer, Object value) - throws IOException - { - Object inValue = value; - try { - BigDecimal decVal = toBigDecimal(value); - inValue = decVal; - - // adjust scale (will cause the an ArithmeticException if number has too - // many decimal places) - decVal = decVal.setScale(4); - - // now, remove scale and convert to long (this will throw if the value is - // too big) - buffer.putLong(decVal.movePointRight(4).longValueExact()); - } catch(ArithmeticException e) { - throw (IOException) - new IOException("Currency value '" + inValue + "' out of range") - .initCause(e); - } - } - - /** - * Decodes a NUMERIC field. - */ - private BigDecimal readNumericValue(ByteBuffer buffer) - { - boolean negate = (buffer.get() != 0); - - byte[] tmpArr = ByteUtil.getBytes(buffer, 16); - - if(buffer.order() != ByteOrder.BIG_ENDIAN) { - fixNumericByteOrder(tmpArr); - } - - BigInteger intVal = new BigInteger(tmpArr); - if(negate) { - intVal = intVal.negate(); - } - return new BigDecimal(intVal, getScale()); - } - - /** - * Writes a numeric value. - */ - private void writeNumericValue(ByteBuffer buffer, Object value) - throws IOException - { - Object inValue = value; - try { - BigDecimal decVal = toBigDecimal(value); - inValue = decVal; - - boolean negative = (decVal.compareTo(BigDecimal.ZERO) < 0); - if(negative) { - decVal = decVal.negate(); - } - - // write sign byte - buffer.put(negative ? (byte)0x80 : (byte)0); - - // adjust scale according to this column type (will cause the an - // ArithmeticException if number has too many decimal places) - decVal = decVal.setScale(getScale()); - - // check precision - if(decVal.precision() > getPrecision()) { - throw new IOException( - "Numeric value is too big for specified precision " - + getPrecision() + ": " + decVal); - } - - // convert to unscaled BigInteger, big-endian bytes - byte[] intValBytes = decVal.unscaledValue().toByteArray(); - int maxByteLen = getType().getFixedSize() - 1; - if(intValBytes.length > maxByteLen) { - throw new IOException("Too many bytes for valid BigInteger?"); - } - if(intValBytes.length < maxByteLen) { - byte[] tmpBytes = new byte[maxByteLen]; - System.arraycopy(intValBytes, 0, tmpBytes, - (maxByteLen - intValBytes.length), - intValBytes.length); - intValBytes = tmpBytes; - } - if(buffer.order() != ByteOrder.BIG_ENDIAN) { - fixNumericByteOrder(intValBytes); - } - buffer.put(intValBytes); - } catch(ArithmeticException e) { - throw (IOException) - new IOException("Numeric value '" + inValue + "' out of range") - .initCause(e); - } - } - - /** - * Decodes a date value. - */ - private Date readDateValue(ByteBuffer buffer) - { - // seems access stores dates in the local timezone. guess you just hope - // you read it in the same timezone in which it was written! - long dateBits = buffer.getLong(); - long time = fromDateDouble(Double.longBitsToDouble(dateBits)); - return new DateExt(time, dateBits); - } - - /** - * Returns a java long time value converted from an access date double. - */ - long fromDateDouble(double value) - { - long time = Math.round(value * MILLISECONDS_PER_DAY); - time -= MILLIS_BETWEEN_EPOCH_AND_1900; - time -= getFromLocalTimeZoneOffset(time); - return time; - } - - /** - * Writes a date value. - */ - private void writeDateValue(ByteBuffer buffer, Object value) - { - if(value == null) { - buffer.putDouble(0d); - } else if(value instanceof DateExt) { - - // this is a Date value previously read from readDateValue(). use the - // original bits to store the value so we don't lose any precision - buffer.putLong(((DateExt)value).getDateBits()); - - } else { - - buffer.putDouble(toDateDouble(value)); - } - } - - /** - * Returns an access date double converted from a java Date/Calendar/Number - * time value. - */ - double toDateDouble(Object value) - { - // seems access stores dates in the local timezone. guess you just - // hope you read it in the same timezone in which it was written! - long time = ((value instanceof Date) ? - ((Date)value).getTime() : - ((value instanceof Calendar) ? - ((Calendar)value).getTimeInMillis() : - ((Number)value).longValue())); - time += getToLocalTimeZoneOffset(time); - time += MILLIS_BETWEEN_EPOCH_AND_1900; - return time / MILLISECONDS_PER_DAY; - } - - /** - * Gets the timezone offset from UTC to local time for the given time - * (including DST). - */ - private long getToLocalTimeZoneOffset(long time) - { - Calendar c = getCalendar(); - c.setTimeInMillis(time); - return ((long)c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET)); - } - - /** - * Gets the timezone offset from local time to UTC for the given time - * (including DST). - */ - private long getFromLocalTimeZoneOffset(long time) - { - // getting from local time back to UTC is a little wonky (and not - // guaranteed to get you back to where you started) - Calendar c = getCalendar(); - c.setTimeInMillis(time); - // apply the zone offset first to get us closer to the original time - c.setTimeInMillis(time - c.get(Calendar.ZONE_OFFSET)); - return ((long)c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET)); - } - - /** - * Decodes a GUID value. - */ - private static String readGUIDValue(ByteBuffer buffer, ByteOrder order) - { - if(order != ByteOrder.BIG_ENDIAN) { - byte[] tmpArr = ByteUtil.getBytes(buffer, 16); - - // the first 3 guid components are integer components which need to - // respect endianness, so swap 4-byte int, 2-byte int, 2-byte int - ByteUtil.swap4Bytes(tmpArr, 0); - ByteUtil.swap2Bytes(tmpArr, 4); - ByteUtil.swap2Bytes(tmpArr, 6); - buffer = ByteBuffer.wrap(tmpArr); - } - - StringBuilder sb = new StringBuilder(22); - sb.append("{"); - sb.append(ByteUtil.toHexString(buffer, 0, 4, - false)); - sb.append("-"); - sb.append(ByteUtil.toHexString(buffer, 4, 2, - false)); - sb.append("-"); - sb.append(ByteUtil.toHexString(buffer, 6, 2, - false)); - sb.append("-"); - sb.append(ByteUtil.toHexString(buffer, 8, 2, - false)); - sb.append("-"); - sb.append(ByteUtil.toHexString(buffer, 10, 6, - false)); - sb.append("}"); - return (sb.toString()); - } - - /** - * Writes a GUID value. - */ - private static void writeGUIDValue(ByteBuffer buffer, Object value, - ByteOrder order) - throws IOException - { - Matcher m = GUID_PATTERN.matcher(toCharSequence(value)); - if(m.matches()) { - ByteBuffer origBuffer = null; - byte[] tmpBuf = null; - if(order != ByteOrder.BIG_ENDIAN) { - // write to a temp buf so we can do some swapping below - origBuffer = buffer; - tmpBuf = new byte[16]; - buffer = ByteBuffer.wrap(tmpBuf); - } - - ByteUtil.writeHexString(buffer, m.group(1)); - ByteUtil.writeHexString(buffer, m.group(2)); - ByteUtil.writeHexString(buffer, m.group(3)); - ByteUtil.writeHexString(buffer, m.group(4)); - ByteUtil.writeHexString(buffer, m.group(5)); - - if(tmpBuf != null) { - // the first 3 guid components are integer components which need to - // respect endianness, so swap 4-byte int, 2-byte int, 2-byte int - ByteUtil.swap4Bytes(tmpBuf, 0); - ByteUtil.swap2Bytes(tmpBuf, 4); - ByteUtil.swap2Bytes(tmpBuf, 6); - origBuffer.put(tmpBuf); - } - - } else { - throw new IOException("Invalid GUID: " + value); - } - } - - /** - * Write an LVAL column into a ByteBuffer inline if it fits, otherwise in - * other data page(s). - * @param value Value of the LVAL column - * @return A buffer containing the LVAL definition and (possibly) the column - * value (unless written to other pages) - * @usage _advanced_method_ - */ - public ByteBuffer writeLongValue(byte[] value, - int remainingRowLength) throws IOException - { - if(value.length > getType().getMaxSize()) { - throw new IOException("value too big for column, max " + - getType().getMaxSize() + ", got " + - value.length); - } - - // determine which type to write - byte type = 0; - int lvalDefLen = getFormat().SIZE_LONG_VALUE_DEF; - if(((getFormat().SIZE_LONG_VALUE_DEF + value.length) <= remainingRowLength) - && (value.length <= getFormat().MAX_INLINE_LONG_VALUE_SIZE)) { - type = LONG_VALUE_TYPE_THIS_PAGE; - lvalDefLen += value.length; - } else if(value.length <= getFormat().MAX_LONG_VALUE_ROW_SIZE) { - type = LONG_VALUE_TYPE_OTHER_PAGE; - } else { - type = LONG_VALUE_TYPE_OTHER_PAGES; - } - - ByteBuffer def = getPageChannel().createBuffer(lvalDefLen); - // take length and apply type to first byte - int lengthWithFlags = value.length | (type << 24); - def.putInt(lengthWithFlags); - - if(type == LONG_VALUE_TYPE_THIS_PAGE) { - // write long value inline - def.putInt(0); - def.putInt(0); //Unknown - def.put(value); - } else { - - ByteBuffer lvalPage = null; - int firstLvalPageNum = PageChannel.INVALID_PAGE_NUMBER; - byte firstLvalRow = 0; - - // write other page(s) - switch(type) { - case LONG_VALUE_TYPE_OTHER_PAGE: - lvalPage = _lvalBufferH.getLongValuePage(value.length); - firstLvalPageNum = _lvalBufferH.getPageNumber(); - firstLvalRow = (byte)Table.addDataPageRow(lvalPage, value.length, - getFormat(), 0); - lvalPage.put(value); - getPageChannel().writePage(lvalPage, firstLvalPageNum); - break; - - case LONG_VALUE_TYPE_OTHER_PAGES: - - ByteBuffer buffer = ByteBuffer.wrap(value); - int remainingLen = buffer.remaining(); - buffer.limit(0); - lvalPage = _lvalBufferH.getLongValuePage(remainingLen); - firstLvalPageNum = _lvalBufferH.getPageNumber(); - firstLvalRow = (byte)Table.getRowsOnDataPage(lvalPage, getFormat()); - int lvalPageNum = firstLvalPageNum; - ByteBuffer nextLvalPage = null; - int nextLvalPageNum = 0; - int nextLvalRowNum = 0; - while(remainingLen > 0) { - lvalPage.clear(); - - // figure out how much we will put in this page (we need 4 bytes for - // the next page pointer) - int chunkLength = Math.min(getFormat().MAX_LONG_VALUE_ROW_SIZE - 4, - remainingLen); - - // figure out if we will need another page, and if so, allocate it - if(chunkLength < remainingLen) { - // force a new page to be allocated for the chunk after this - _lvalBufferH.clear(); - nextLvalPage = _lvalBufferH.getLongValuePage( - (remainingLen - chunkLength) + 4); - nextLvalPageNum = _lvalBufferH.getPageNumber(); - nextLvalRowNum = Table.getRowsOnDataPage(nextLvalPage, - getFormat()); - } else { - nextLvalPage = null; - nextLvalPageNum = 0; - nextLvalRowNum = 0; - } - - // add row to this page - byte lvalRow = (byte)Table.addDataPageRow(lvalPage, chunkLength + 4, - getFormat(), 0); - - // write next page info - lvalPage.put((byte)nextLvalRowNum); // row number - ByteUtil.put3ByteInt(lvalPage, nextLvalPageNum); // page number - - // write this page's chunk of data - buffer.limit(buffer.limit() + chunkLength); - lvalPage.put(buffer); - remainingLen -= chunkLength; - - // write new page to database - getPageChannel().writePage(lvalPage, lvalPageNum); - - // move to next page - lvalPage = nextLvalPage; - lvalPageNum = nextLvalPageNum; - } - break; - - default: - throw new IOException("Unrecognized long value type: " + type); - } - - // update def - def.put(firstLvalRow); - ByteUtil.put3ByteInt(def, firstLvalPageNum); - def.putInt(0); //Unknown - - } - - def.flip(); - return def; - } - - /** - * Writes the header info for a long value page. - */ - private void writeLongValueHeader(ByteBuffer lvalPage) - { - lvalPage.put(PageTypes.DATA); //Page type - lvalPage.put((byte) 1); //Unknown - lvalPage.putShort((short)getFormat().DATA_PAGE_INITIAL_FREE_SPACE); //Free space - lvalPage.put((byte) 'L'); - lvalPage.put((byte) 'V'); - lvalPage.put((byte) 'A'); - lvalPage.put((byte) 'L'); - lvalPage.putInt(0); //unknown - lvalPage.putShort((short)0); // num rows in page - } - - /** - * Serialize an Object into a raw byte value for this column in little - * endian order - * @param obj Object to serialize - * @return A buffer containing the bytes - * @usage _advanced_method_ - */ - public ByteBuffer write(Object obj, int remainingRowLength) - throws IOException - { - return write(obj, remainingRowLength, PageChannel.DEFAULT_BYTE_ORDER); - } - - /** - * Serialize an Object into a raw byte value for this column - * @param obj Object to serialize - * @param order Order in which to serialize - * @return A buffer containing the bytes - * @usage _advanced_method_ - */ - public ByteBuffer write(Object obj, int remainingRowLength, ByteOrder order) - throws IOException - { - if(isRawData(obj)) { - // just slap it right in (not for the faint of heart!) - return ByteBuffer.wrap(((RawData)obj).getBytes()); - } - - if(!isVariableLength() || !getType().isVariableLength()) { - return writeFixedLengthField(obj, order); - } - - // var length column - if(!getType().isLongValue()) { - - // this is an "inline" var length field - switch(getType()) { - case NUMERIC: - // don't ask me why numerics are "var length" columns... - ByteBuffer buffer = getPageChannel().createBuffer( - getType().getFixedSize(), order); - writeNumericValue(buffer, obj); - buffer.flip(); - return buffer; - - case TEXT: - byte[] encodedData = encodeTextValue( - obj, 0, getLengthInUnits(), false).array(); - obj = encodedData; - break; - - case BINARY: - case UNKNOWN_0D: - case UNSUPPORTED_VARLEN: - // should already be "encoded" - break; - default: - throw new RuntimeException("unexpected inline var length type: " + - getType()); - } - - ByteBuffer buffer = ByteBuffer.wrap(toByteArray(obj)); - buffer.order(order); - return buffer; - } - - // var length, long value column - switch(getType()) { - case OLE: - // should already be "encoded" - break; - case MEMO: - int maxMemoChars = DataType.MEMO.toUnitSize(DataType.MEMO.getMaxSize()); - obj = encodeTextValue(obj, 0, maxMemoChars, false).array(); - break; - default: - throw new RuntimeException("unexpected var length, long value type: " + - getType()); - } - - // create long value buffer - return writeLongValue(toByteArray(obj), remainingRowLength); - } - - /** - * Serialize an Object into a raw byte value for this column - * @param obj Object to serialize - * @param order Order in which to serialize - * @return A buffer containing the bytes - * @usage _advanced_method_ - */ - public ByteBuffer writeFixedLengthField(Object obj, ByteOrder order) - throws IOException - { - int size = getType().getFixedSize(_columnLength); - - // create buffer for data - ByteBuffer buffer = getPageChannel().createBuffer(size, order); - - // since booleans are not written by this method, it's safe to convert any - // incoming boolean into an integer. - obj = booleanToInteger(obj); - - switch(getType()) { - case BOOLEAN: - //Do nothing - break; - case BYTE: - buffer.put(toNumber(obj).byteValue()); - break; - case INT: - buffer.putShort(toNumber(obj).shortValue()); - break; - case LONG: - buffer.putInt(toNumber(obj).intValue()); - break; - case MONEY: - writeCurrencyValue(buffer, obj); - break; - case FLOAT: - buffer.putFloat(toNumber(obj).floatValue()); - break; - case DOUBLE: - buffer.putDouble(toNumber(obj).doubleValue()); - break; - case SHORT_DATE_TIME: - writeDateValue(buffer, obj); - break; - case TEXT: - // apparently text numeric values are also occasionally written as fixed - // length... - int numChars = getLengthInUnits(); - // force uncompressed encoding for fixed length text - buffer.put(encodeTextValue(obj, numChars, numChars, true)); - break; - case GUID: - writeGUIDValue(buffer, obj, order); - break; - case NUMERIC: - // yes, that's right, occasionally numeric values are written as fixed - // length... - writeNumericValue(buffer, obj); - break; - case BINARY: - case UNKNOWN_0D: - case UNKNOWN_11: - case COMPLEX_TYPE: - buffer.putInt(toNumber(obj).intValue()); - break; - case UNSUPPORTED_FIXEDLEN: - byte[] bytes = toByteArray(obj); - if(bytes.length != getLength()) { - throw new IOException("Invalid fixed size binary data, size " - + getLength() + ", got " + bytes.length); - } - buffer.put(bytes); - break; - default: - throw new IOException("Unsupported data type: " + getType()); - } - buffer.flip(); - return buffer; - } - - /** - * Decodes a compressed or uncompressed text value. - */ - private String decodeTextValue(byte[] data) - throws IOException - { - try { - - // see if data is compressed. the 0xFF, 0xFE sequence indicates that - // compression is used (sort of, see algorithm below) - boolean isCompressed = ((data.length > 1) && - (data[0] == TEXT_COMPRESSION_HEADER[0]) && - (data[1] == TEXT_COMPRESSION_HEADER[1])); - - if(isCompressed) { - - Expand expander = new Expand(); - - // this is a whacky compression combo that switches back and forth - // between compressed/uncompressed using a 0x00 byte (starting in - // compressed mode) - StringBuilder textBuf = new StringBuilder(data.length); - // start after two bytes indicating compression use - int dataStart = TEXT_COMPRESSION_HEADER.length; - int dataEnd = dataStart; - boolean inCompressedMode = true; - while(dataEnd < data.length) { - if(data[dataEnd] == (byte)0x00) { - - // handle current segment - decodeTextSegment(data, dataStart, dataEnd, inCompressedMode, - expander, textBuf); - inCompressedMode = !inCompressedMode; - ++dataEnd; - dataStart = dataEnd; - - } else { - ++dataEnd; - } - } - // handle last segment - decodeTextSegment(data, dataStart, dataEnd, inCompressedMode, - expander, textBuf); - - return textBuf.toString(); - - } - - return decodeUncompressedText(data, getCharset()); - - } catch (IllegalInputException e) { - throw (IOException) - new IOException("Can't expand text column").initCause(e); - } catch (EndOfInputException e) { - throw (IOException) - new IOException("Can't expand text column").initCause(e); - } - } - - /** - * Decodes a segnment of a text value into the given buffer according to the - * given status of the segment (compressed/uncompressed). - */ - private void decodeTextSegment(byte[] data, int dataStart, int dataEnd, - boolean inCompressedMode, Expand expander, - StringBuilder textBuf) - throws IllegalInputException, EndOfInputException - { - if(dataEnd <= dataStart) { - // no data - return; - } - int dataLength = dataEnd - dataStart; - if(inCompressedMode) { - // handle compressed data - byte[] tmpData = ByteUtil.copyOf(data, dataStart, dataLength); - expander.reset(); - textBuf.append(expander.expand(tmpData)); - } else { - // handle uncompressed data - textBuf.append(decodeUncompressedText(data, dataStart, dataLength, - getCharset())); - } - } - - /** - * @param textBytes bytes of text to decode - * @return the decoded string - */ - private static CharBuffer decodeUncompressedText( - byte[] textBytes, int startPos, int length, Charset charset) - { - return charset.decode(ByteBuffer.wrap(textBytes, startPos, length)); - } - - /** - * Encodes a text value, possibly compressing. - */ - private ByteBuffer encodeTextValue(Object obj, int minChars, int maxChars, - boolean forceUncompressed) - throws IOException - { - CharSequence text = toCharSequence(obj); - if((text.length() > maxChars) || (text.length() < minChars)) { - throw new IOException("Text is wrong length for " + getType() + - " column, max " + maxChars - + ", min " + minChars + ", got " + text.length()); - } - - // may only compress if column type allows it - if(!forceUncompressed && isCompressedUnicode() && - (text.length() <= getFormat().MAX_COMPRESSED_UNICODE_SIZE)) { - - // for now, only do very simple compression (only compress text which is - // all ascii text) - if(isAsciiCompressible(text)) { - - byte[] encodedChars = new byte[TEXT_COMPRESSION_HEADER.length + - text.length()]; - encodedChars[0] = TEXT_COMPRESSION_HEADER[0]; - encodedChars[1] = TEXT_COMPRESSION_HEADER[1]; - for(int i = 0; i < text.length(); ++i) { - encodedChars[i + TEXT_COMPRESSION_HEADER.length] = - (byte)text.charAt(i); - } - return ByteBuffer.wrap(encodedChars); - } - } - - return encodeUncompressedText(text, getCharset()); - } - - /** - * Returns {@code true} if the given text can be compressed using simple - * ASCII encoding, {@code false} otherwise. - */ - private static boolean isAsciiCompressible(CharSequence text) { - // only attempt to compress > 2 chars (compressing less than 3 chars would - // not result in a space savings due to the 2 byte compression header) - if(text.length() <= TEXT_COMPRESSION_HEADER.length) { - return false; - } - // now, see if it is all printable ASCII - for(int i = 0; i < text.length(); ++i) { - char c = text.charAt(i); - if(!Compress.isAsciiCrLfOrTab(c)) { - return false; - } - } - return true; - } - - /** - * Constructs a byte containing the flags for this column. - */ - private byte getColumnBitFlags() { - byte flags = UNKNOWN_FLAG_MASK; - if(!isVariableLength()) { - flags |= FIXED_LEN_FLAG_MASK; - } - if(isAutoNumber()) { - flags |= getAutoNumberGenerator().getColumnFlags(); - } - if(isHyperlink()) { - flags |= HYPERLINK_FLAG_MASK; - } - return flags; - } - - @Override - public String toString() { - StringBuilder rtn = new StringBuilder(); - rtn.append("\tName: (" + _table.getName() + ") " + _name); - byte typeValue = _type.getValue(); - if(_type.isUnsupported()) { - typeValue = getUnknownDataType(); - } - rtn.append("\n\tType: 0x" + Integer.toHexString(typeValue) + - " (" + _type + ")"); - rtn.append("\n\tNumber: " + _columnNumber); - rtn.append("\n\tLength: " + _columnLength); - rtn.append("\n\tVariable length: " + _variableLength); - if(_type.isTextual()) { - rtn.append("\n\tCompressed Unicode: " + _textInfo._compressedUnicode); - rtn.append("\n\tText Sort order: " + _textInfo._sortOrder); - if(_textInfo._codePage > 0) { - rtn.append("\n\tText Code Page: " + _textInfo._codePage); - } - if(isAppendOnly()) { - rtn.append("\n\tAppend only: " + isAppendOnly()); - } - if(isHyperlink()) { - rtn.append("\n\tHyperlink: " + isHyperlink()); - } - } - if(_autoNumber) { - rtn.append("\n\tLast AutoNumber: " + _autoNumberGenerator.getLast()); - } - if(_complexInfo != null) { - rtn.append("\n\tComplexInfo: " + _complexInfo); - } - rtn.append("\n\n"); - return rtn.toString(); - } - - /** - * @param textBytes bytes of text to decode - * @param charset relevant charset - * @return the decoded string - * @usage _advanced_method_ - */ - public static String decodeUncompressedText(byte[] textBytes, - Charset charset) - { - return decodeUncompressedText(textBytes, 0, textBytes.length, charset) - .toString(); - } - - /** - * @param text Text to encode - * @param charset database charset - * @return A buffer with the text encoded - * @usage _advanced_method_ - */ - public static ByteBuffer encodeUncompressedText(CharSequence text, - Charset charset) - { - CharBuffer cb = ((text instanceof CharBuffer) ? - (CharBuffer)text : CharBuffer.wrap(text)); - return charset.encode(cb); - } - - - /** - * Orders Columns by column number. - * @usage _general_method_ - */ - public int compareTo(Column other) { - if (_columnNumber > other.getColumnNumber()) { - return 1; - } else if (_columnNumber < other.getColumnNumber()) { - return -1; - } else { - return 0; - } - } - - /** - * @param columns A list of columns in a table definition - * @return The number of variable length columns found in the list - * @usage _advanced_method_ - */ - public static short countVariableLength(List columns) { - short rtn = 0; - for (Column col : columns) { - if (col.isVariableLength()) { - rtn++; - } - } - return rtn; - } - - /** - * @param columns A list of columns in a table definition - * @return The number of variable length columns which are not long values - * found in the list - * @usage _advanced_method_ - */ - public static short countNonLongVariableLength(List columns) { - short rtn = 0; - for (Column col : columns) { - if (col.isVariableLength() && !col.getType().isLongValue()) { - rtn++; - } - } - return rtn; - } - - /** - * @return an appropriate BigDecimal representation of the given object. - * null is returned as 0 and Numbers are converted - * using their double representation. - */ - private static BigDecimal toBigDecimal(Object value) - { - if(value == null) { - return BigDecimal.ZERO; - } else if(value instanceof BigDecimal) { - return (BigDecimal)value; - } else if(value instanceof BigInteger) { - return new BigDecimal((BigInteger)value); - } else if(value instanceof Number) { - return new BigDecimal(((Number)value).doubleValue()); - } - return new BigDecimal(value.toString()); - } - - /** - * @return an appropriate Number representation of the given object. - * null is returned as 0 and Strings are parsed as - * Doubles. - */ - private static Number toNumber(Object value) - { - if(value == null) { - return BigDecimal.ZERO; - } if(value instanceof Number) { - return (Number)value; - } - return Double.valueOf(value.toString()); - } + public PropertyMap getProperties() throws IOException; /** - * @return an appropriate CharSequence representation of the given object. - * @usage _advanced_method_ - */ - public static CharSequence toCharSequence(Object value) - throws IOException - { - if(value == null) { - return null; - } else if(value instanceof CharSequence) { - return (CharSequence)value; - } else if(value instanceof Clob) { - try { - Clob c = (Clob)value; - // note, start pos is 1-based - return c.getSubString(1L, (int)c.length()); - } catch(SQLException e) { - throw (IOException)(new IOException(e.getMessage())).initCause(e); - } - } else if(value instanceof Reader) { - char[] buf = new char[8 * 1024]; - StringBuilder sout = new StringBuilder(); - Reader in = (Reader)value; - int read = 0; - while((read = in.read(buf)) != -1) { - sout.append(buf, 0, read); - } - return sout; - } - - return value.toString(); - } - - /** - * @return an appropriate byte[] representation of the given object. - * @usage _advanced_method_ + * Returns the column which tracks the version history for an "append only" + * column. + * @usage _intermediate_method_ */ - public static byte[] toByteArray(Object value) - throws IOException - { - if(value == null) { - return null; - } else if(value instanceof byte[]) { - return (byte[])value; - } else if(value instanceof Blob) { - try { - Blob b = (Blob)value; - // note, start pos is 1-based - return b.getBytes(1L, (int)b.length()); - } catch(SQLException e) { - throw (IOException)(new IOException(e.getMessage())).initCause(e); - } - } - - ByteArrayOutputStream bout = new ByteArrayOutputStream(); - - if(value instanceof InputStream) { - byte[] buf = new byte[8 * 1024]; - InputStream in = (InputStream)value; - int read = 0; - while((read = in.read(buf)) != -1) { - bout.write(buf, 0, read); - } - } else { - // if all else fails, serialize it - ObjectOutputStream oos = new ObjectOutputStream(bout); - oos.writeObject(value); - oos.close(); - } + public Column getVersionHistoryColumn(); - return bout.toByteArray(); - } - - /** - * Interpret a boolean value (null == false) - * @usage _advanced_method_ - */ - public static boolean toBooleanValue(Object obj) { - return ((obj != null) && ((Boolean)obj).booleanValue()); - } + public Object setRowValue(Object[] rowArray, Object value); - /** - * Swaps the bytes of the given numeric in place. - */ - private static void fixNumericByteOrder(byte[] bytes) - { - // fix endianness of each 4 byte segment - for(int i = 0; i < 4; ++i) { - ByteUtil.swap4Bytes(bytes, i * 4); - } - } - - /** - * Treat booleans as integers (C-style). - */ - protected static Object booleanToInteger(Object obj) { - if (obj instanceof Boolean) { - obj = ((Boolean) obj) ? 1 : 0; - } - return obj; - } - - /** - * Returns a wrapper for raw column data that can be written without - * understanding the data. Useful for wrapping unparseable data for - * re-writing. - */ - static RawData rawDataWrapper(byte[] bytes) { - return new RawData(bytes); - } - - /** - * Returs {@code true} if the given value is "raw" column data, - * {@code false} otherwise. - */ - static boolean isRawData(Object value) { - return(value instanceof RawData); - } - - /** - * Writes the column definitions into a table definition buffer. - * @param buffer Buffer to write to - * @param columns List of Columns to write definitions for - */ - protected static void writeDefinitions( - TableCreator creator, ByteBuffer buffer) - throws IOException - { - List columns = creator.getColumns(); - short fixedOffset = (short) 0; - short variableOffset = (short) 0; - // we specifically put the "long variable" values after the normal - // variable length values so that we have a better chance of fitting it - // all (because "long variable" values can go in separate pages) - short longVariableOffset = countNonLongVariableLength(columns); - for (Column col : columns) { - - int position = buffer.position(); - buffer.put(col.getType().getValue()); - buffer.putInt(Table.MAGIC_TABLE_NUMBER); //constant magic number - buffer.putShort(col.getColumnNumber()); //Column Number - if (col.isVariableLength()) { - if(!col.getType().isLongValue()) { - buffer.putShort(variableOffset++); - } else { - buffer.putShort(longVariableOffset++); - } - } else { - buffer.putShort((short) 0); - } - buffer.putShort(col.getColumnNumber()); //Column Number again - if(col.getType().isTextual()) { - // this will write 4 bytes (note we don't support writing dbs which - // use the text code page) - writeSortOrder(buffer, col.getTextSortOrder(), creator.getFormat()); - } else { - if(col.getType().getHasScalePrecision()) { - buffer.put(col.getPrecision()); // numeric precision - buffer.put(col.getScale()); // numeric scale - } else { - buffer.put((byte) 0x00); //unused - buffer.put((byte) 0x00); //unused - } - buffer.putShort((short) 0); //Unknown - } - buffer.put(col.getColumnBitFlags()); // misc col flags - if (col.isCompressedUnicode()) { //Compressed - buffer.put((byte) 1); - } else { - buffer.put((byte) 0); - } - buffer.putInt(0); //Unknown, but always 0. - //Offset for fixed length columns - if (col.isVariableLength()) { - buffer.putShort((short) 0); - } else { - buffer.putShort(fixedOffset); - fixedOffset += col.getType().getFixedSize(col.getLength()); - } - if(!col.getType().isLongValue()) { - buffer.putShort(col.getLength()); //Column length - } else { - buffer.putShort((short)0x0000); // unused - } - if (LOG.isDebugEnabled()) { - LOG.debug("Creating new column def block\n" + ByteUtil.toHexString( - buffer, position, creator.getFormat().SIZE_COLUMN_DEF_BLOCK)); - } - } - for (Column col : columns) { - Table.writeName(buffer, col.getName(), creator.getCharset()); - } - } - - /** - * Reads the sort order info from the given buffer from the given position. - */ - static SortOrder readSortOrder(ByteBuffer buffer, int position, - JetFormat format) - { - short value = buffer.getShort(position); - byte version = 0; - if(format.SIZE_SORT_ORDER == 4) { - version = buffer.get(position + 3); - } - - if(value == 0) { - // probably a file we wrote, before handling sort order - return format.DEFAULT_SORT_ORDER; - } - - if(value == GENERAL_SORT_ORDER_VALUE) { - if(version == GENERAL_LEGACY_SORT_ORDER.getVersion()) { - return GENERAL_LEGACY_SORT_ORDER; - } - if(version == GENERAL_SORT_ORDER.getVersion()) { - return GENERAL_SORT_ORDER; - } - } - return new SortOrder(value, version); - } - - /** - * Writes the sort order info to the given buffer at the current position. - */ - private static void writeSortOrder(ByteBuffer buffer, SortOrder sortOrder, - JetFormat format) { - if(sortOrder == null) { - sortOrder = format.DEFAULT_SORT_ORDER; - } - buffer.putShort(sortOrder.getValue()); - if(format.SIZE_SORT_ORDER == 4) { - buffer.put((byte)0x00); // unknown - buffer.put(sortOrder.getVersion()); - } - } - - /** - * Date subclass which stashes the original date bits, in case we attempt to - * re-write the value (will not lose precision). - */ - private static final class DateExt extends Date - { - private static final long serialVersionUID = 0L; - - /** cached bits of the original date value */ - private transient final long _dateBits; - - private DateExt(long time, long dateBits) { - super(time); - _dateBits = dateBits; - } - - public long getDateBits() { - return _dateBits; - } - - private Object writeReplace() throws ObjectStreamException { - // if we are going to serialize this Date, convert it back to a normal - // Date (in case it is restored outside of the context of jackcess) - return new Date(super.getTime()); - } - } - - /** - * Wrapper for raw column data which can be re-written. - */ - private static class RawData implements Serializable - { - private static final long serialVersionUID = 0L; - - private final byte[] _bytes; - - private RawData(byte[] bytes) { - _bytes = bytes; - } - - private byte[] getBytes() { - return _bytes; - } - - @Override - public String toString() { - return "RawData: " + ByteUtil.toHexString(getBytes()); - } - - private Object writeReplace() throws ObjectStreamException { - // if we are going to serialize this, convert it back to a normal - // byte[] (in case it is restored outside of the context of jackcess) - return getBytes(); - } - } - - /** - * Base class for the supported autonumber types. - * @usage _advanced_class_ - */ - public abstract class AutoNumberGenerator - { - protected AutoNumberGenerator() {} - - /** - * Returns the last autonumber generated by this generator. Only valid - * after a call to {@link Table#addRow}, otherwise undefined. - */ - public abstract Object getLast(); - - /** - * Returns the next autonumber for this generator. - *

- * Warning, calling this externally will result in this value being - * "lost" for the table. - */ - public abstract Object getNext(Object prevRowValue); - - /** - * Returns the flags used when writing this column. - */ - public abstract int getColumnFlags(); - - /** - * Returns the type of values generated by this generator. - */ - public abstract DataType getType(); - } - - private final class LongAutoNumberGenerator extends AutoNumberGenerator - { - private LongAutoNumberGenerator() {} - - @Override - public Object getLast() { - // the table stores the last long autonumber used - return getTable().getLastLongAutoNumber(); - } - - @Override - public Object getNext(Object prevRowValue) { - // the table stores the last long autonumber used - return getTable().getNextLongAutoNumber(); - } - - @Override - public int getColumnFlags() { - return AUTO_NUMBER_FLAG_MASK; - } - - @Override - public DataType getType() { - return DataType.LONG; - } - } - - private final class GuidAutoNumberGenerator extends AutoNumberGenerator - { - private Object _lastAutoNumber; - - private GuidAutoNumberGenerator() {} - - @Override - public Object getLast() { - return _lastAutoNumber; - } - - @Override - public Object getNext(Object prevRowValue) { - // format guids consistently w/ Column.readGUIDValue() - _lastAutoNumber = "{" + UUID.randomUUID() + "}"; - return _lastAutoNumber; - } - - @Override - public int getColumnFlags() { - return AUTO_NUMBER_GUID_FLAG_MASK; - } - - @Override - public DataType getType() { - return DataType.GUID; - } - } - - private final class ComplexTypeAutoNumberGenerator extends AutoNumberGenerator - { - private ComplexTypeAutoNumberGenerator() {} - - @Override - public Object getLast() { - // the table stores the last ComplexType autonumber used - return getTable().getLastComplexTypeAutoNumber(); - } - - @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 ComplexValueForeignKey(Column.this, nextComplexAutoNum); - } - - @Override - public int getColumnFlags() { - return AUTO_NUMBER_FLAG_MASK; - } - - @Override - public DataType getType() { - return DataType.COMPLEX_TYPE; - } - } + public Object setRowValue(Map rowMap, Object value); - private final class UnsupportedAutoNumberGenerator extends AutoNumberGenerator - { - private final DataType _genType; - - private UnsupportedAutoNumberGenerator(DataType genType) { - _genType = genType; - } - - @Override - public Object getLast() { - return null; - } - - @Override - public Object getNext(Object prevRowValue) { - throw new UnsupportedOperationException(); - } - - @Override - public int getColumnFlags() { - throw new UnsupportedOperationException(); - } - - @Override - public DataType getType() { - return _genType; - } - } - + public Object getRowValue(Object[] rowArray); - /** - * Information about the sort order (collation) for a textual column. - * @usage _intermediate_class_ - */ - public static final class SortOrder - { - private final short _value; - private final byte _version; - - public SortOrder(short value, byte version) { - _value = value; - _version = version; - } - - public short getValue() { - return _value; - } - - public byte getVersion() { - return _version; - } - - @Override - public int hashCode() { - return _value; - } - - @Override - public boolean equals(Object o) { - return ((this == o) || - ((o != null) && (getClass() == o.getClass()) && - (_value == ((SortOrder)o)._value) && - (_version == ((SortOrder)o)._version))); - } - - @Override - public String toString() { - return _value + "(" + _version + ")"; - } - } - - /** - * Information specific to numeric types. - */ - private static final class NumericInfo - { - /** Numeric precision */ - private byte _precision; - /** Numeric scale */ - private byte _scale; - } - - /** - * Information specific to textual types. - */ - private static final class TextInfo - { - /** whether or not they are compressed */ - private boolean _compressedUnicode; - /** the collating sort order for a text field */ - private SortOrder _sortOrder; - /** the code page for a text field (for certain db versions) */ - private short _codePage; - /** complex column which tracks the version history for this "append only" - column */ - private Column _versionHistoryCol; - /** whether or not this is a hyperlink column (only possible for columns - of type MEMO) */ - private boolean _hyperlink; - } - - /** - * Manages secondary page buffers for long value writing. - */ - private abstract class LongValueBufferHolder - { - /** - * Returns a long value data page with space for data of the given length. - */ - public ByteBuffer getLongValuePage(int dataLength) throws IOException { - - TempPageHolder lvalBufferH = getBufferHolder(); - dataLength = Math.min(dataLength, getFormat().MAX_LONG_VALUE_ROW_SIZE); - - ByteBuffer lvalPage = null; - if(lvalBufferH.getPageNumber() != PageChannel.INVALID_PAGE_NUMBER) { - lvalPage = lvalBufferH.getPage(getPageChannel()); - if(Table.rowFitsOnDataPage(dataLength, lvalPage, getFormat())) { - // the current page has space - return lvalPage; - } - } - - // need new page - return findNewPage(dataLength); - } - - protected ByteBuffer findNewPage(int dataLength) throws IOException { - ByteBuffer lvalPage = getBufferHolder().setNewPage(getPageChannel()); - writeLongValueHeader(lvalPage); - return lvalPage; - } - - public int getOwnedPageCount() { - return 0; - } - - /** - * Returns the page number of the current long value data page. - */ - public int getPageNumber() { - return getBufferHolder().getPageNumber(); - } - - /** - * Discards the current the current long value data page. - */ - public void clear() throws IOException { - getBufferHolder().clear(); - } - - protected abstract TempPageHolder getBufferHolder(); - } - - /** - * Manages a common, shared extra page for long values. This is legacy - * behavior from before it was understood that there were additional usage - * maps for each columns. - */ - private final class LegacyLongValueBufferHolder extends LongValueBufferHolder - { - @Override - protected TempPageHolder getBufferHolder() { - return getTable().getLongValueBuffer(); - } - } - - /** - * Manages the column usage maps for long values. - */ - private final class UmapLongValueBufferHolder extends LongValueBufferHolder - { - /** Usage map of pages that this column owns */ - private final UsageMap _ownedPages; - /** Usage map of pages that this column owns with free space on them */ - private final UsageMap _freeSpacePages; - /** page buffer used to write "long value" data */ - private final TempPageHolder _longValueBufferH = - TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); - - private UmapLongValueBufferHolder(UsageMap ownedPages, - UsageMap freeSpacePages) { - _ownedPages = ownedPages; - _freeSpacePages = freeSpacePages; - } - - @Override - protected TempPageHolder getBufferHolder() { - return _longValueBufferH; - } - - @Override - public int getOwnedPageCount() { - return _ownedPages.getPageCount(); - } - - @Override - protected ByteBuffer findNewPage(int dataLength) throws IOException { - - // grab last owned page and check for free space. - ByteBuffer newPage = Table.findFreeRowSpace( - _ownedPages, _freeSpacePages, _longValueBufferH); - - if(newPage != null) { - if(Table.rowFitsOnDataPage(dataLength, newPage, getFormat())) { - return newPage; - } - // discard this page and allocate a new one - clear(); - } - - // nothing found on current pages, need new page - newPage = super.findNewPage(dataLength); - int pageNumber = getPageNumber(); - _ownedPages.addPageNumber(pageNumber); - _freeSpacePages.addPageNumber(pageNumber); - return newPage; - } - - @Override - public void clear() throws IOException { - int pageNumber = getPageNumber(); - if(pageNumber != PageChannel.INVALID_PAGE_NUMBER) { - _freeSpacePages.removePageNumber(pageNumber, true); - } - super.clear(); - } - } + public Object getRowValue(Map rowMap); } diff --git a/src/java/com/healthmarketscience/jackcess/ColumnBuilder.java b/src/java/com/healthmarketscience/jackcess/ColumnBuilder.java index befff67..90ddc34 100644 --- a/src/java/com/healthmarketscience/jackcess/ColumnBuilder.java +++ b/src/java/com/healthmarketscience/jackcess/ColumnBuilder.java @@ -29,6 +29,12 @@ package com.healthmarketscience.jackcess; import java.sql.SQLException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import com.healthmarketscience.jackcess.impl.ColumnImpl; +import com.healthmarketscience.jackcess.impl.JetFormat; +import com.healthmarketscience.jackcess.impl.DatabaseImpl; + /** * Builder style class for constructing a Column. * @@ -36,20 +42,29 @@ import java.sql.SQLException; */ public class ColumnBuilder { + private static final Log LOG = LogFactory.getLog(ColumnBuilder.class); + /** name of the new column */ private String _name; /** the type of the new column */ private DataType _type; /** optional length for the new column */ - private Integer _length; + private Short _length; /** optional precision for the new column */ - private Integer _precision; + private Byte _precision; /** optional scale for the new column */ - private Integer _scale; + private Byte _scale; /** whether or not the column is auto-number */ private boolean _autoNumber; /** whether or not the column allows compressed unicode */ - private Boolean _compressedUnicode; + private boolean _compressedUnicode; + /** whether or not the column is a hyperlink (memo only) */ + private boolean _hyperlink; + /** 0-based column number */ + private short _columnNumber; + /** the collating sort order for a text field */ + private ColumnImpl.SortOrder _sortOrder; + public ColumnBuilder(String name) { this(name, null); @@ -60,6 +75,10 @@ public class ColumnBuilder { _type = type; } + public String getName() { + return _name; + } + /** * Sets the type for the new column. */ @@ -68,6 +87,10 @@ public class ColumnBuilder { return this; } + public DataType getType() { + return _type; + } + /** * Sets the type for the new column based on the given SQL type. */ @@ -89,26 +112,42 @@ public class ColumnBuilder { * Sets the precision for the new column. */ public ColumnBuilder setPrecision(int newPrecision) { - _precision = newPrecision; + _precision = (byte)newPrecision; return this; } + public byte getPrecision() { + return ((_precision != null) ? _precision : + (byte)(_type.getHasScalePrecision() ? _type.getDefaultPrecision() : 0)); + } + /** * Sets the scale for the new column. */ public ColumnBuilder setScale(int newScale) { - _scale = newScale; + _scale = (byte)newScale; return this; } + public byte getScale() { + return ((_scale != null) ? _scale : + (byte)(_type.getHasScalePrecision() ? _type.getDefaultScale() : 0)); + } + /** * Sets the length (in bytes) for the new column. */ public ColumnBuilder setLength(int length) { - _length = length; + _length = (short)length; return this; } + public short getLength() { + return ((_length != null) ? _length : + (short)(!_type.isVariableLength() ? _type.getFixedSize() : + (!_type.isLongValue() ? _type.getDefaultSize() : 0))); + } + /** * Sets the length (in type specific units) for the new column. */ @@ -131,6 +170,10 @@ public class ColumnBuilder { return this; } + public boolean isAutoNumber() { + return _autoNumber; + } + /** * Sets whether of not the new column allows unicode compression. */ @@ -139,6 +182,22 @@ public class ColumnBuilder { return this; } + public boolean isCompressedUnicode() { + return _compressedUnicode; + } + + /** + * Sets whether of not the new column allows unicode compression. + */ + public ColumnBuilder setHyperlink(boolean hyperlink) { + _hyperlink = hyperlink; + return this; + } + + public boolean isHyperlink() { + return _hyperlink; + } + /** * Sets all attributes except name from the given Column template. */ @@ -152,42 +211,143 @@ public class ColumnBuilder { setPrecision(template.getPrecision()); } setCompressedUnicode(template.isCompressedUnicode()); + setHyperlink(template.isHyperlink()); return this; } /** - * Escapes the new column's name using {@link Database#escapeIdentifier}. + * Sets all attributes except name from the given Column template. */ - public ColumnBuilder escapeName() - { - _name = Database.escapeIdentifier(_name); + public ColumnBuilder setFromColumn(ColumnBuilder template) { + DataType type = template.getType(); + setType(type); + setLength(template.getLength()); + setAutoNumber(template.isAutoNumber()); + if(type.getHasScalePrecision()) { + setScale(template.getScale()); + setPrecision(template.getPrecision()); + } + setCompressedUnicode(template.isCompressedUnicode()); + setHyperlink(template.isHyperlink()); + return this; } /** - * Creates a new Column with the currently configured attributes. + * Escapes the new column's name using {@link TableBuilder#escapeIdentifier}. + */ + public ColumnBuilder escapeName() { + _name = TableBuilder.escapeIdentifier(_name); + return this; + } + + /** + * @usage _advanced_method_ + */ + public short getColumnNumber() { + return _columnNumber; + } + + /** + * @usage _advanced_method_ */ - public Column toColumn() { - Column col = new Column(); - col.setName(_name); - col.setType(_type); - if(_length != null) { - col.setLength(_length.shortValue()); + public void setColumnNumber(short newColumnNumber) { + _columnNumber = newColumnNumber; + } + + /** + * @usage _advanced_method_ + */ + public ColumnImpl.SortOrder getTextSortOrder() { + return _sortOrder; + } + + /** + * @usage _advanced_method_ + */ + public void setTextSortOrder(ColumnImpl.SortOrder newTextSortOrder) { + _sortOrder = newTextSortOrder; + } + + /** + * Checks that this column definition is valid. + * + * @throws IllegalArgumentException if this column definition is invalid. + * @usage _advanced_method_ + */ + public void validate(JetFormat format) { + if(getType() == null) { + throw new IllegalArgumentException("must have type"); + } + DatabaseImpl.validateIdentifierName( + getName(), format.MAX_COLUMN_NAME_LENGTH, "column"); + + if(getType().isUnsupported()) { + throw new IllegalArgumentException( + "Cannot create column with unsupported type " + getType()); } - if(_precision != null) { - col.setPrecision(_precision.byteValue()); + if(!format.isSupportedDataType(getType())) { + throw new IllegalArgumentException( + "Database format " + format + " does not support type " + getType()); + } + + if(!getType().isVariableLength()) { + if(getLength() != getType().getFixedSize()) { + if(getLength() < getType().getFixedSize()) { + throw new IllegalArgumentException("invalid fixed length size"); + } + LOG.warn("Column length " + getLength() + + " longer than expected fixed size " + + getType().getFixedSize()); + } + } else if(!getType().isLongValue()) { + if(!getType().isValidSize(getLength())) { + throw new IllegalArgumentException("var length out of range"); + } + } + + if(getType().getHasScalePrecision()) { + if(!getType().isValidScale(getScale())) { + throw new IllegalArgumentException( + "Scale must be from " + getType().getMinScale() + " to " + + getType().getMaxScale() + " inclusive"); + } + if(!getType().isValidPrecision(getPrecision())) { + throw new IllegalArgumentException( + "Precision must be from " + getType().getMinPrecision() + " to " + + getType().getMaxPrecision() + " inclusive"); + } } - if(_scale != null) { - col.setScale(_scale.byteValue()); + + if(isAutoNumber()) { + if(!getType().mayBeAutoNumber()) { + throw new IllegalArgumentException( + "Auto number column must be long integer or guid"); + } } - if(_autoNumber) { - col.setAutoNumber(true); + + if(isCompressedUnicode()) { + if(!getType().isTextual()) { + throw new IllegalArgumentException( + "Only textual columns allow unicode compression (text/memo)"); + } } - if(_compressedUnicode != null) { - col.setCompressedUnicode(_compressedUnicode); + + if(isHyperlink()) { + if(getType() != DataType.MEMO) { + throw new IllegalArgumentException( + "Only memo columns can be hyperlinks"); + } } - return col; + } + + /** + * Creates a new Column with the currently configured attributes. + */ + public ColumnBuilder toColumn() { + // for backwards compat w/ old code + return this; } } diff --git a/src/java/com/healthmarketscience/jackcess/Cursor.java b/src/java/com/healthmarketscience/jackcess/Cursor.java index 042241a..5c6d12f 100644 --- a/src/java/com/healthmarketscience/jackcess/Cursor.java +++ b/src/java/com/healthmarketscience/jackcess/Cursor.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2007 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,30 +15,18 @@ 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.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.Map; -import java.util.NoSuchElementException; - -import com.healthmarketscience.jackcess.Table.RowState; -import org.apache.commons.lang.ObjectUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import com.healthmarketscience.jackcess.util.ColumnMatcher; +import com.healthmarketscience.jackcess.util.ErrorHandler; +import com.healthmarketscience.jackcess.util.IterableBuilder; /** * Manages iteration for a Table. Different cursors provide different methods @@ -47,295 +35,43 @@ import org.apache.commons.logging.LogFactory; * traversed, row updates may or may not be seen). Multiple cursors may * traverse the same table simultaneously. *

- * The Cursor provides a variety of static utility methods to construct - * cursors with given characteristics or easily search for specific values. - * For even friendlier and more flexible construction, see - * {@link CursorBuilder}. + * The {@link CursorBuilder} provides a variety of static utility methods to + * construct cursors with given characteristics or easily search for specific + * values as well as friendly and flexible construction options. *

* Is not thread-safe. * * @author James Ahlborn */ -public abstract class Cursor implements Iterable> -{ - private static final Log LOG = LogFactory.getLog(Cursor.class); - - /** boolean value indicating forward movement */ - public static final boolean MOVE_FORWARD = true; - /** boolean value indicating reverse movement */ - public static final boolean MOVE_REVERSE = false; - - /** first position for the TableScanCursor */ - private static final ScanPosition FIRST_SCAN_POSITION = - new ScanPosition(RowId.FIRST_ROW_ID); - /** last position for the TableScanCursor */ - private static final ScanPosition LAST_SCAN_POSITION = - new ScanPosition(RowId.LAST_ROW_ID); - - /** identifier for this cursor */ - private final Id _id; - /** owning table */ - private final Table _table; - /** State used for reading the table rows */ - private final RowState _rowState; - /** the first (exclusive) row id for this cursor */ - private final Position _firstPos; - /** the last (exclusive) row id for this cursor */ - private final Position _lastPos; - /** the previous row */ - protected Position _prevPos; - /** the current row */ - protected Position _curPos; - /** ColumnMatcher to be used when matching column values */ - protected ColumnMatcher _columnMatcher = SimpleColumnMatcher.INSTANCE; - - protected Cursor(Id id, Table table, Position firstPos, Position lastPos) { - _id = id; - _table = table; - _rowState = _table.createRowState(); - _firstPos = firstPos; - _lastPos = lastPos; - _curPos = firstPos; - _prevPos = firstPos; - } +public interface Cursor extends Iterable +{ - /** - * Creates a normal, un-indexed cursor for the given table. - * @param table the table over which this cursor will traverse - */ - public static Cursor createCursor(Table table) { - return new TableScanCursor(table); - } + public Id getId(); - /** - * Creates an indexed cursor for the given table. - *

- * Note, index based table traversal may not include all rows, as certain - * types of indexes do not include all entries (namely, some indexes ignore - * null entries, see {@link Index#shouldIgnoreNulls}). - * - * @param table the table over which this cursor will traverse - * @param index index for the table which will define traversal order as - * well as enhance certain lookups - */ - public static Cursor createIndexCursor(Table table, Index index) - throws IOException - { - return IndexCursor.createCursor(table, index); - } - - /** - * Creates an indexed cursor for the given table, narrowed to the given - * range. - *

- * Note, index based table traversal may not include all rows, as certain - * types of indexes do not include all entries (namely, some indexes ignore - * null entries, see {@link Index#shouldIgnoreNulls}). - * - * @param table the table over which this cursor will traverse - * @param index index for the table which will define traversal order as - * well as enhance certain lookups - * @param startRow the first row of data for the cursor (inclusive), or - * {@code null} for the first entry - * @param endRow the last row of data for the cursor (inclusive), or - * {@code null} for the last entry - */ - public static Cursor createIndexCursor(Table table, Index index, - Object[] startRow, Object[] endRow) - throws IOException - { - return IndexCursor.createCursor(table, index, startRow, endRow); - } - - /** - * Creates an indexed cursor for the given table, narrowed to the given - * range. - *

- * Note, index based table traversal may not include all rows, as certain - * types of indexes do not include all entries (namely, some indexes ignore - * null entries, see {@link Index#shouldIgnoreNulls}). - * - * @param table the table over which this cursor will traverse - * @param index index for the table which will define traversal order as - * well as enhance certain lookups - * @param startRow the first row of data for the cursor, or {@code null} for - * the first entry - * @param startInclusive whether or not startRow is inclusive or exclusive - * @param endRow the last row of data for the cursor, or {@code null} for - * the last entry - * @param endInclusive whether or not endRow is inclusive or exclusive - */ - public static Cursor createIndexCursor(Table table, Index index, - Object[] startRow, - boolean startInclusive, - Object[] endRow, - boolean endInclusive) - throws IOException - { - return IndexCursor.createCursor(table, index, startRow, startInclusive, - endRow, endInclusive); - } - - /** - * Convenience method for finding a specific row in a table which matches a - * given row "pattern". See {@link #findFirstRow(Map)} for details on the - * rowPattern. - *

- * Warning, this method always starts searching from the beginning of - * the Table (you cannot use it to find successive matches). - * - * @param table the table to search - * @param rowPattern pattern to be used to find the row - * @return the matching row or {@code null} if a match could not be found. - */ - public static Map findRow(Table table, - Map rowPattern) - throws IOException - { - Cursor cursor = createCursor(table); - if(cursor.findFirstRow(rowPattern)) { - return cursor.getCurrentRow(); - } - return null; - } - - /** - * Convenience method for finding a specific row in a table which matches a - * given row "pattern". See {@link #findFirstRow(Column,Object)} for - * details on the pattern. - *

- * Note, a {@code null} result value is ambiguous in that it could imply no - * match or a matching row with {@code null} for the desired value. If - * distinguishing this situation is important, you will need to use a Cursor - * directly instead of this convenience method. - * - * @param table the table to search - * @param column column whose value should be returned - * @param columnPattern column being matched by the valuePattern - * @param valuePattern value from the columnPattern which will match the - * desired row - * @return the matching row or {@code null} if a match could not be found. - */ - public static Object findValue(Table table, Column column, - Column columnPattern, Object valuePattern) - throws IOException - { - Cursor cursor = createCursor(table); - if(cursor.findFirstRow(columnPattern, valuePattern)) { - return cursor.getCurrentRowValue(column); - } - return null; - } - - /** - * Convenience method for finding a specific row in an indexed table which - * matches a given row "pattern". See {@link #findFirstRow(Map)} for - * details on the rowPattern. - *

- * Warning, this method always starts searching from the beginning of - * the Table (you cannot use it to find successive matches). - * - * @param table the table to search - * @param index index to assist the search - * @param rowPattern pattern to be used to find the row - * @return the matching row or {@code null} if a match could not be found. - */ - public static Map findRow(Table table, Index index, - Map rowPattern) - throws IOException - { - Cursor cursor = createIndexCursor(table, index); - if(cursor.findFirstRow(rowPattern)) { - return cursor.getCurrentRow(); - } - return null; - } - - /** - * Convenience method for finding a specific row in a table which matches a - * given row "pattern". See {@link #findFirstRow(Column,Object)} for - * details on the pattern. - *

- * Note, a {@code null} result value is ambiguous in that it could imply no - * match or a matching row with {@code null} for the desired value. If - * distinguishing this situation is important, you will need to use a Cursor - * directly instead of this convenience method. - * - * @param table the table to search - * @param index index to assist the search - * @param column column whose value should be returned - * @param columnPattern column being matched by the valuePattern - * @param valuePattern value from the columnPattern which will match the - * desired row - * @return the matching row or {@code null} if a match could not be found. - */ - public static Object findValue(Table table, Index index, Column column, - Column columnPattern, Object valuePattern) - throws IOException - { - Cursor cursor = createIndexCursor(table, index); - if(cursor.findFirstRow(columnPattern, valuePattern)) { - return cursor.getCurrentRowValue(column); - } - return null; - } - - public Id getId() { - return _id; - } - - public Table getTable() { - return _table; - } - - public JetFormat getFormat() { - return getTable().getFormat(); - } - - public PageChannel getPageChannel() { - return getTable().getPageChannel(); - } + public Table getTable(); /** * Gets the currently configured ErrorHandler (always non-{@code null}). * This will be used to handle all errors. */ - public ErrorHandler getErrorHandler() { - return _rowState.getErrorHandler(); - } + public ErrorHandler 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); - } + public void setErrorHandler(ErrorHandler newErrorHandler); /** * Returns the currently configured ColumnMatcher, always non-{@code null}. */ - public ColumnMatcher getColumnMatcher() { - return _columnMatcher; - } - - /** - * Sets a new ColumnMatcher. If {@code null}, resets to using the - * default matcher, {@link SimpleColumnMatcher#INSTANCE}. - */ - public void setColumnMatcher(ColumnMatcher columnMatcher) { - if(columnMatcher == null) { - columnMatcher = getDefaultColumnMatcher(); - } - _columnMatcher = columnMatcher; - } + public ColumnMatcher getColumnMatcher(); /** - * Returns the default ColumnMatcher for this Cursor. + * Sets a new ColumnMatcher. If {@code null}, resets to using the default + * matcher (default depends on Cursor type). */ - protected ColumnMatcher getDefaultColumnMatcher() { - return SimpleColumnMatcher.INSTANCE; - } + public void setColumnMatcher(ColumnMatcher columnMatcher); /** * Returns the current state of the cursor which can be restored at a future @@ -344,9 +80,7 @@ public abstract class Cursor implements Iterable> * Savepoints may be used across different cursor instances for the same * table, but they must have the same {@link Id}. */ - public Savepoint getSavepoint() { - return new Savepoint(_id, _curPos, _prevPos); - } + public Savepoint getSavepoint(); /** * Moves the cursor to a savepoint previously returned from @@ -355,323 +89,95 @@ public abstract class Cursor implements Iterable> * cursorId equal to this cursor's id */ public void restoreSavepoint(Savepoint savepoint) - throws IOException - { - if(!_id.equals(savepoint.getCursorId())) { - throw new IllegalArgumentException( - "Savepoint " + savepoint + " is not valid for this cursor with id " - + _id); - } - restorePosition(savepoint.getCurrentPosition(), - savepoint.getPreviousPosition()); - } - - /** - * Returns the first row id (exclusive) as defined by this cursor. - */ - protected Position getFirstPosition() { - return _firstPos; - } - - /** - * Returns the last row id (exclusive) as defined by this cursor. - */ - protected Position getLastPosition() { - return _lastPos; - } + throws IOException; /** * Resets this cursor for forward traversal. Calls {@link #beforeFirst}. */ - public void reset() { - beforeFirst(); - } + public void reset(); /** * Resets this cursor for forward traversal (sets cursor to before the first * row). */ - public void beforeFirst() { - reset(MOVE_FORWARD); - } - + public void beforeFirst(); + /** * Resets this cursor for reverse traversal (sets cursor to after the last * row). */ - public void afterLast() { - reset(MOVE_REVERSE); - } + public void afterLast(); /** * Returns {@code true} if the cursor is currently positioned before the * first row, {@code false} otherwise. */ - public boolean isBeforeFirst() - throws IOException - { - if(getFirstPosition().equals(_curPos)) { - return !recheckPosition(MOVE_REVERSE); - } - return false; - } - + public boolean isBeforeFirst() throws IOException; + /** * Returns {@code true} if the cursor is currently positioned after the * last row, {@code false} otherwise. */ - public boolean isAfterLast() - throws IOException - { - if(getLastPosition().equals(_curPos)) { - return !recheckPosition(MOVE_FORWARD); - } - return false; - } + public boolean isAfterLast() throws IOException; /** * Returns {@code true} if the row at which the cursor is currently * positioned is deleted, {@code false} otherwise (including invalid rows). */ - public boolean isCurrentRowDeleted() - throws IOException - { - // we need to ensure that the "deleted" flag has been read for this row - // (or re-read if the table has been recently modified) - Table.positionAtRowData(_rowState, _curPos.getRowId()); - return _rowState.isDeleted(); - } - - /** - * Resets this cursor for traversing the given direction. - */ - protected void reset(boolean moveForward) { - _curPos = getDirHandler(moveForward).getBeginningPosition(); - _prevPos = _curPos; - _rowState.reset(); - } + public boolean isCurrentRowDeleted() throws IOException; /** - * Returns an Iterable whose iterator() method calls afterLast - * on this cursor and returns a modifiable Iterator which will iterate - * through all the rows of this table in reverse order. Use of the Iterator - * follows the same restrictions as a call to getPreviousRow. - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within - */ - public Iterable> reverseIterable() { - return reverseIterable(null); - } - - /** - * Returns an Iterable whose iterator() method calls afterLast - * on this table and returns a modifiable Iterator which will iterate - * through all the rows of this table in reverse order, returning only the - * given columns. Use of the Iterator follows the same restrictions as a - * call to getPreviousRow. - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within - */ - public Iterable> reverseIterable( - final Collection columnNames) - { - return new Iterable>() { - public Iterator> iterator() { - return new RowIterator(columnNames, MOVE_REVERSE); - } - }; - } - - /** - * Calls beforeFirst on this cursor and returns a modifiable + * Calls {@link #beforeFirst} on this cursor and returns a modifiable * Iterator which will iterate through all the rows of this table. Use of * the Iterator follows the same restrictions as a call to - * getNextRow. - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within - */ - public Iterator> iterator() - { - return iterator(null); - } - - /** - * Returns an Iterable whose iterator() method returns the result of a call - * to {@link #iterator(Collection)} - * @throws IllegalStateException if an IOException is thrown by one of the + * {@link #getNextRow}. + *

+ * For more flexible iteration see {@link #newIterable}. + * @throws RuntimeIOException if an IOException is thrown by one of the * operations, the actual exception will be contained within */ - public Iterable> iterable( - final Collection columnNames) - { - return new Iterable>() { - public Iterator> iterator() { - return Cursor.this.iterator(columnNames); - } - }; - } - - /** - * Calls beforeFirst on this table and returns a modifiable - * Iterator which will iterate through all the rows of this table, returning - * only the given columns. Use of the Iterator follows the same - * restrictions as a call to getNextRow. - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within - */ - public Iterator> iterator(Collection columnNames) - { - return new RowIterator(columnNames, MOVE_FORWARD); - } + public Iterator iterator(); /** - * Returns an Iterable whose iterator() method returns the result of a call - * to {@link #columnMatchIterable(Column,Object)} - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within - */ - public Iterable> columnMatchIterable( - Column columnPattern, Object valuePattern) - { - return columnMatchIterable(null, columnPattern, valuePattern); - } - - /** - * Calls beforeFirst on this cursor and returns a modifiable - * Iterator which will iterate through all the rows of this table which - * match the given column pattern. Use of the Iterator follows the same - * restrictions as a call to getNextRow. See - * {@link #findFirstRow(Column,Object)} for details on the columnPattern. - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within - */ - public Iterator> columnMatchIterator( - Column columnPattern, Object valuePattern) - { - return columnMatchIterator(null, columnPattern, valuePattern); - } - - /** - * Returns an Iterable whose iterator() method returns the result of a call - * to {@link #columnMatchIterator(Collection,Column,Object)} - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within - */ - public Iterable> columnMatchIterable( - final Collection columnNames, - final Column columnPattern, final Object valuePattern) - { - return new Iterable>() { - public Iterator> iterator() { - return Cursor.this.columnMatchIterator( - columnNames, columnPattern, valuePattern); - } - }; - } - - /** - * Calls beforeFirst on this table and returns a modifiable - * Iterator which will iterate through all the rows of this table which - * match the given column pattern, returning only the given columns. Use of - * the Iterator follows the same restrictions as a call to - * getNextRow. See {@link #findFirstRow(Column,Object)} for - * details on the columnPattern. - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within + * Convenience method for constructing a new IterableBuilder for this + * cursor. An IterableBuilder provides a variety of options for more + * flexible iteration. */ - public Iterator> columnMatchIterator( - Collection columnNames, Column columnPattern, Object valuePattern) - { - return new ColumnMatchIterator(columnNames, columnPattern, valuePattern); - } + public IterableBuilder newIterable(); /** - * Returns an Iterable whose iterator() method returns the result of a call - * to {@link #rowMatchIterator(Map)} - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within - */ - public Iterable> rowMatchIterable( - Map rowPattern) - { - return rowMatchIterable(null, rowPattern); - } - - /** - * Calls beforeFirst on this cursor and returns a modifiable - * Iterator which will iterate through all the rows of this table which - * match the given row pattern. Use of the Iterator follows the same - * restrictions as a call to getNextRow. See - * {@link #findFirstRow(Map)} for details on the rowPattern. - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within - */ - public Iterator> rowMatchIterator( - Map rowPattern) - { - return rowMatchIterator(null, rowPattern); - } - - /** - * Returns an Iterable whose iterator() method returns the result of a call - * to {@link #rowMatchIterator(Collection,Map)} - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within - */ - public Iterable> rowMatchIterable( - final Collection columnNames, - final Map rowPattern) - { - return new Iterable>() { - public Iterator> iterator() { - return Cursor.this.rowMatchIterator( - columnNames, rowPattern); - } - }; - } - - /** - * Calls beforeFirst on this table and returns a modifiable - * Iterator which will iterate through all the rows of this table which - * match the given row pattern, returning only the given columns. Use of - * the Iterator follows the same restrictions as a call to - * getNextRow. See {@link #findFirstRow(Map)} for details on - * the rowPattern. - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within + * Delete the current row. + *

+ * Note, re-deleting an already deleted row is allowed (it does nothing). + * @throws IllegalStateException if the current row is not valid (at + * beginning or end of table) */ - public Iterator> rowMatchIterator( - Collection columnNames, Map rowPattern) - { - return new RowMatchIterator(columnNames, rowPattern); - } + public void deleteCurrentRow() throws IOException; /** - * Delete the current row. + * Update the current row. + * @return the given row values if long enough, otherwise a new array, + * updated with the current row values * @throws IllegalStateException if the current row is not valid (at - * beginning or end of table), or already deleted. + * beginning or end of table), or deleted. */ - public void deleteCurrentRow() throws IOException { - _table.deleteRow(_rowState, _curPos.getRowId()); - } + public Object[] updateCurrentRow(Object... row) throws IOException; /** * Update the current row. + * @return the given row, updated with the current row values * @throws IllegalStateException if the current row is not valid (at * beginning or end of table), or deleted. */ - public void updateCurrentRow(Object... row) throws IOException { - _table.updateRow(_rowState, _curPos.getRowId(), row); - } + public > M updateCurrentRowFromMap(M row) + throws IOException; /** * Moves to the next row in the table and returns it. * @return The next row in this table (Column name -> Column value), or * {@code null} if no next row is found */ - public Map getNextRow() throws IOException { - return getNextRow(null); - } + public Row getNextRow() throws IOException; /** * Moves to the next row in the table and returns it. @@ -679,20 +185,15 @@ public abstract class Cursor implements Iterable> * @return The next row in this table (Column name -> Column value), or * {@code null} if no next row is found */ - public Map getNextRow(Collection columnNames) - throws IOException - { - return getAnotherRow(columnNames, MOVE_FORWARD); - } + public Row getNextRow(Collection columnNames) + throws IOException; /** * Moves to the previous row in the table and returns it. * @return The previous row in this table (Column name -> Column value), or * {@code null} if no previous row is found */ - public Map getPreviousRow() throws IOException { - return getPreviousRow(null); - } + public Row getPreviousRow() throws IOException; /** * Moves to the previous row in the table and returns it. @@ -700,145 +201,22 @@ public abstract class Cursor implements Iterable> * @return The previous row in this table (Column name -> Column value), or * {@code null} if no previous row is found */ - public Map getPreviousRow(Collection columnNames) - throws IOException - { - return getAnotherRow(columnNames, MOVE_REVERSE); - } - - - /** - * Moves to another row in the table based on the given direction and - * returns it. - * @param columnNames Only column names in this collection will be returned - * @return another row in this table (Column name -> Column value), where - * "next" may be backwards if moveForward is {@code false}, or - * {@code null} if there is not another row in the given direction. - */ - private Map getAnotherRow(Collection columnNames, - boolean moveForward) - throws IOException - { - if(moveToAnotherRow(moveForward)) { - return getCurrentRow(columnNames); - } - return null; - } + public Row getPreviousRow(Collection columnNames) + throws IOException; /** * Moves to the next row as defined by this cursor. * @return {@code true} if a valid next row was found, {@code false} * otherwise */ - public boolean moveToNextRow() - throws IOException - { - return moveToAnotherRow(MOVE_FORWARD); - } + public boolean moveToNextRow() throws IOException; /** * Moves to the previous row as defined by this cursor. * @return {@code true} if a valid previous row was found, {@code false} * otherwise */ - public boolean moveToPreviousRow() - throws IOException - { - return moveToAnotherRow(MOVE_REVERSE); - } - - /** - * Moves to another row in the given direction as defined by this cursor. - * @return {@code true} if another valid row was found in the given - * direction, {@code false} otherwise - */ - private boolean moveToAnotherRow(boolean moveForward) - throws IOException - { - if(_curPos.equals(getDirHandler(moveForward).getEndPosition())) { - // already at end, make sure nothing has changed - return recheckPosition(moveForward); - } - - return moveToAnotherRowImpl(moveForward); - } - - /** - * Restores a current position for the cursor (current position becomes - * previous position). - */ - protected void restorePosition(Position curPos) - throws IOException - { - restorePosition(curPos, _curPos); - } - - /** - * Restores a current and previous position for the cursor if the given - * positions are different from the current positions. - */ - protected final void restorePosition(Position curPos, Position prevPos) - throws IOException - { - if(!curPos.equals(_curPos) || !prevPos.equals(_prevPos)) { - restorePositionImpl(curPos, prevPos); - } - } - - /** - * Restores a current and previous position for the cursor. - */ - protected void restorePositionImpl(Position curPos, Position prevPos) - throws IOException - { - // make the current position previous, and the new position current - _prevPos = _curPos; - _curPos = curPos; - _rowState.reset(); - } - - /** - * Rechecks the current position if the underlying data structures have been - * modified. - * @return {@code true} if the cursor ended up in a new position, - * {@code false} otherwise. - */ - private boolean recheckPosition(boolean moveForward) - throws IOException - { - if(isUpToDate()) { - // nothing has changed - return false; - } - - // move the cursor back to the previous position - restorePosition(_prevPos); - return moveToAnotherRowImpl(moveForward); - } - - /** - * Does the grunt work of moving the cursor to another position in the given - * direction. - */ - private boolean moveToAnotherRowImpl(boolean moveForward) - throws IOException - { - _rowState.reset(); - _prevPos = _curPos; - _curPos = findAnotherPosition(_rowState, _curPos, moveForward); - Table.positionAtRowHeader(_rowState, _curPos.getRowId()); - return(!_curPos.equals(getDirHandler(moveForward).getEndPosition())); - } - - /** - * @deprecated renamed to {@link #findFirstRow(Column,Object)} to be more clear - */ - @Deprecated - public boolean findRow(Column columnPattern, Object valuePattern) - throws IOException - { - return findFirstRow(columnPattern, valuePattern); - } + public boolean moveToPreviousRow() throws IOException; /** * Moves to the first row (as defined by the cursor) where the given column @@ -857,26 +235,8 @@ public abstract class Cursor implements Iterable> * {@code false} if no row was found */ public boolean findFirstRow(Column columnPattern, Object valuePattern) - throws IOException - { - Position curPos = _curPos; - Position prevPos = _prevPos; - boolean found = false; - try { - beforeFirst(); - found = findNextRowImpl(columnPattern, valuePattern); - return found; - } finally { - if(!found) { - try { - restorePosition(curPos, prevPos); - } catch(IOException e) { - LOG.error("Failed restoring position", e); - } - } - } - } - + throws IOException; + /** * Moves to the next row (as defined by the cursor) where the given column * has the given value. This may be more efficient on some cursors than @@ -891,34 +251,7 @@ public abstract class Cursor implements Iterable> * {@code false} if no row was found */ public boolean findNextRow(Column columnPattern, Object valuePattern) - throws IOException - { - Position curPos = _curPos; - Position prevPos = _prevPos; - boolean found = false; - try { - found = findNextRowImpl(columnPattern, valuePattern); - return found; - } finally { - if(!found) { - try { - restorePosition(curPos, prevPos); - } catch(IOException e) { - LOG.error("Failed restoring position", e); - } - } - } - } - - /** - * @deprecated renamed to {@link #findFirstRow(Map)} to be more clear - */ - @Deprecated - public boolean findRow(Map rowPattern) - throws IOException - { - return findFirstRow(rowPattern); - } + throws IOException; /** * Moves to the first row (as defined by the cursor) where the given columns @@ -934,26 +267,7 @@ public abstract class Cursor implements Iterable> * @return {@code true} if a valid row was found with the given values, * {@code false} if no row was found */ - public boolean findFirstRow(Map rowPattern) - throws IOException - { - Position curPos = _curPos; - Position prevPos = _prevPos; - boolean found = false; - try { - beforeFirst(); - found = findNextRowImpl(rowPattern); - return found; - } finally { - if(!found) { - try { - restorePosition(curPos, prevPos); - } catch(IOException e) { - LOG.error("Failed restoring position", e); - } - } - } - } + public boolean findFirstRow(Map rowPattern) throws IOException; /** * Moves to the next row (as defined by the cursor) where the given columns @@ -966,25 +280,7 @@ public abstract class Cursor implements Iterable> * @return {@code true} if a valid row was found with the given values, * {@code false} if no row was found */ - public boolean findNextRow(Map rowPattern) - throws IOException - { - Position curPos = _curPos; - Position prevPos = _prevPos; - boolean found = false; - try { - found = findNextRowImpl(rowPattern); - return found; - } finally { - if(!found) { - try { - restorePosition(curPos, prevPos); - } catch(IOException e) { - LOG.error("Failed restoring position", e); - } - } - } - } + public boolean findNextRow(Map rowPattern) throws IOException; /** * Returns {@code true} if the current row matches the given pattern. @@ -994,146 +290,43 @@ public abstract class Cursor implements Iterable> * corresponding value in the current row */ public boolean currentRowMatches(Column columnPattern, Object valuePattern) - throws IOException - { - return _columnMatcher.matches(getTable(), columnPattern.getName(), - valuePattern, - getCurrentRowValue(columnPattern)); - } - + throws IOException; + /** * Returns {@code true} if the current row matches the given pattern. * @param rowPattern column names and values which must be equal to the * corresponding values in the current row */ - public boolean currentRowMatches(Map rowPattern) - throws IOException - { - Map row = getCurrentRow(rowPattern.keySet()); - - if(rowPattern.size() != row.size()) { - return false; - } - - for(Map.Entry e : row.entrySet()) { - String columnName = e.getKey(); - if(!_columnMatcher.matches(getTable(), columnName, - rowPattern.get(columnName), e.getValue())) { - return false; - } - } - - return true; - } - - /** - * Moves to the next row (as defined by the cursor) where the given column - * has the given value. Caller manages save/restore on failure. - *

- * Default implementation scans the table from beginning to end. - * - * @param columnPattern column from the table for this cursor which is being - * matched by the valuePattern - * @param valuePattern value which is equal to the corresponding value in - * the matched row - * @return {@code true} if a valid row was found with the given value, - * {@code false} if no row was found - */ - protected boolean findNextRowImpl(Column columnPattern, Object valuePattern) - throws IOException - { - while(moveToNextRow()) { - if(currentRowMatches(columnPattern, valuePattern)) { - return true; - } - } - return false; - } - - /** - * Moves to the next row (as defined by the cursor) where the given columns - * have the given values. Caller manages save/restore on failure. - *

- * Default implementation scans the table from beginning to end. - * - * @param rowPattern column names and values which must be equal to the - * corresponding values in the matched row - * @return {@code true} if a valid row was found with the given values, - * {@code false} if no row was found - */ - protected boolean findNextRowImpl(Map rowPattern) - throws IOException - { - while(moveToNextRow()) { - if(currentRowMatches(rowPattern)) { - return true; - } - } - return false; - } + public boolean currentRowMatches(Map rowPattern) throws IOException; /** * Moves forward as many rows as possible up to the given number of rows. * @return the number of rows moved. */ - public int moveNextRows(int numRows) - throws IOException - { - return moveSomeRows(numRows, MOVE_FORWARD); - } + public int moveNextRows(int numRows) throws IOException; /** * Moves backward as many rows as possible up to the given number of rows. * @return the number of rows moved. */ - public int movePreviousRows(int numRows) - throws IOException - { - return moveSomeRows(numRows, MOVE_REVERSE); - } - - /** - * Moves as many rows as possible in the given direction up to the given - * number of rows. - * @return the number of rows moved. - */ - private int moveSomeRows(int numRows, boolean moveForward) - throws IOException - { - int numMovedRows = 0; - while((numMovedRows < numRows) && moveToAnotherRow(moveForward)) { - ++numMovedRows; - } - return numMovedRows; - } + public int movePreviousRows(int numRows) throws IOException; /** * Returns the current row in this cursor (Column name -> Column value). */ - public Map getCurrentRow() - throws IOException - { - return getCurrentRow(null); - } + public Row getCurrentRow() throws IOException; /** * Returns the current row in this cursor (Column name -> Column value). * @param columnNames Only column names in this collection will be returned */ - public Map getCurrentRow(Collection columnNames) - throws IOException - { - return _table.getRow(_rowState, _curPos.getRowId(), columnNames); - } + public Row getCurrentRow(Collection columnNames) + throws IOException; /** * Returns the given column from the current row. */ - public Object getCurrentRowValue(Column column) - throws IOException - { - return _table.getRowValue(_rowState, _curPos.getRowId(), column); - } + public Object getCurrentRowValue(Column column) throws IOException; /** * Updates a single value in the current row. @@ -1141,467 +334,39 @@ public abstract class Cursor implements Iterable> * beginning or end of table), or deleted. */ public void setCurrentRowValue(Column column, Object value) - throws IOException - { - Object[] row = new Object[_table.getColumnCount()]; - Arrays.fill(row, Column.KEEP_VALUE); - column.setRowValue(row, value); - _table.updateRow(_rowState, _curPos.getRowId(), row); - } - - /** - * Returns {@code true} if this cursor is up-to-date with respect to the - * relevant table and related table objects, {@code false} otherwise. - */ - protected boolean isUpToDate() { - return _rowState.isUpToDate(); - } - - @Override - public String toString() { - return getClass().getSimpleName() + " CurPosition " + _curPos + - ", PrevPosition " + _prevPos; - } - - /** - * Finds the next non-deleted row after the given row (as defined by this - * cursor) and returns the id of the row, where "next" may be backwards if - * moveForward is {@code false}. If there are no more rows, the returned - * rowId should equal the value returned by {@link #getLastPosition} if - * moving forward and {@link #getFirstPosition} if moving backward. - */ - protected abstract Position findAnotherPosition(RowState rowState, - Position curPos, - boolean moveForward) throws IOException; - /** - * Returns the DirHandler for the given movement direction. - */ - protected abstract DirHandler getDirHandler(boolean moveForward); - - - /** - * Base implementation of iterator for this cursor, modifiable. - */ - protected abstract class BaseIterator - implements Iterator> - { - protected final Collection _columnNames; - protected Boolean _hasNext; - protected boolean _validRow; - - protected BaseIterator(Collection columnNames) - { - _columnNames = columnNames; - } - - public boolean hasNext() { - if(_hasNext == null) { - try { - _hasNext = findNext(); - _validRow = _hasNext; - } catch(IOException e) { - throw new IllegalStateException(e); - } - } - return _hasNext; - } - - public Map next() { - if(!hasNext()) { - throw new NoSuchElementException(); - } - try { - Map rtn = getCurrentRow(_columnNames); - _hasNext = null; - return rtn; - } catch(IOException e) { - throw new IllegalStateException(e); - } - } - - public void remove() { - if(_validRow) { - try { - deleteCurrentRow(); - _validRow = false; - } catch(IOException e) { - throw new IllegalStateException(e); - } - } else { - throw new IllegalStateException("Not at valid row"); - } - } - - protected abstract boolean findNext() throws IOException; - } - - - /** - * Row iterator for this cursor, modifiable. - */ - private final class RowIterator extends BaseIterator - { - private final boolean _moveForward; - - private RowIterator(Collection columnNames, boolean moveForward) - { - super(columnNames); - _moveForward = moveForward; - reset(_moveForward); - } - - @Override - protected boolean findNext() throws IOException { - return moveToAnotherRow(_moveForward); - } - } - - - /** - * Row iterator for this cursor, modifiable. - */ - private final class ColumnMatchIterator extends BaseIterator - { - private final Column _columnPattern; - private final Object _valuePattern; - - private ColumnMatchIterator(Collection columnNames, - Column columnPattern, Object valuePattern) - { - super(columnNames); - _columnPattern = columnPattern; - _valuePattern = valuePattern; - beforeFirst(); - } - - @Override - protected boolean findNext() throws IOException { - return findNextRow(_columnPattern, _valuePattern); - } - } - - - /** - * Row iterator for this cursor, modifiable. - */ - private final class RowMatchIterator extends BaseIterator - { - private final Map _rowPattern; - - private RowMatchIterator(Collection columnNames, - Map rowPattern) - { - super(columnNames); - _rowPattern = rowPattern; - beforeFirst(); - } - - @Override - protected boolean findNext() throws IOException { - return findNextRow(_rowPattern); - } - } - - - /** - * Handles moving the cursor in a given direction. Separates cursor - * logic from value storage. - */ - protected abstract class DirHandler - { - public abstract Position getBeginningPosition(); - public abstract Position getEndPosition(); - } - - - /** - * Simple un-indexed cursor. - */ - private static final class TableScanCursor extends Cursor - { - /** ScanDirHandler for forward traversal */ - private final ScanDirHandler _forwardDirHandler = - new ForwardScanDirHandler(); - /** ScanDirHandler for backward traversal */ - private final ScanDirHandler _reverseDirHandler = - new ReverseScanDirHandler(); - /** Cursor over the pages that this table owns */ - private final UsageMap.PageCursor _ownedPagesCursor; - - private TableScanCursor(Table table) { - super(new Id(table, null), table, - FIRST_SCAN_POSITION, LAST_SCAN_POSITION); - _ownedPagesCursor = table.getOwnedPagesCursor(); - } - - @Override - protected ScanDirHandler getDirHandler(boolean moveForward) { - return (moveForward ? _forwardDirHandler : _reverseDirHandler); - } - - @Override - protected boolean isUpToDate() { - return(super.isUpToDate() && _ownedPagesCursor.isUpToDate()); - } - - @Override - protected void reset(boolean moveForward) { - _ownedPagesCursor.reset(moveForward); - super.reset(moveForward); - } - - @Override - protected void restorePositionImpl(Position curPos, Position prevPos) - throws IOException - { - if(!(curPos instanceof ScanPosition) || - !(prevPos instanceof ScanPosition)) { - throw new IllegalArgumentException( - "Restored positions must be scan positions"); - } - _ownedPagesCursor.restorePosition(curPos.getRowId().getPageNumber(), - prevPos.getRowId().getPageNumber()); - super.restorePositionImpl(curPos, prevPos); - } - - @Override - protected Position findAnotherPosition(RowState rowState, Position curPos, - boolean moveForward) - throws IOException - { - ScanDirHandler handler = getDirHandler(moveForward); - - // figure out how many rows are left on this page so we can find the - // next row - RowId curRowId = curPos.getRowId(); - Table.positionAtRowHeader(rowState, curRowId); - int currentRowNumber = curRowId.getRowNumber(); - - // loop until we find the next valid row or run out of pages - while(true) { - - currentRowNumber = handler.getAnotherRowNumber(currentRowNumber); - curRowId = new RowId(curRowId.getPageNumber(), currentRowNumber); - Table.positionAtRowHeader(rowState, curRowId); - - if(!rowState.isValid()) { - - // load next page - curRowId = new RowId(handler.getAnotherPageNumber(), - RowId.INVALID_ROW_NUMBER); - Table.positionAtRowHeader(rowState, curRowId); - - if(!rowState.isHeaderPageNumberValid()) { - //No more owned pages. No more rows. - return handler.getEndPosition(); - } - - // update row count and initial row number - currentRowNumber = handler.getInitialRowNumber( - rowState.getRowsOnHeaderPage()); - - } else if(!rowState.isDeleted()) { - - // we found a valid, non-deleted row, return it - return new ScanPosition(curRowId); - } - - } - } - - /** - * Handles moving the table scan cursor in a given direction. Separates - * cursor logic from value storage. - */ - private abstract class ScanDirHandler extends DirHandler { - public abstract int getAnotherRowNumber(int curRowNumber); - public abstract int getAnotherPageNumber(); - public abstract int getInitialRowNumber(int rowsOnPage); - } - - /** - * Handles moving the table scan cursor forward. - */ - private final class ForwardScanDirHandler extends ScanDirHandler { - @Override - public Position getBeginningPosition() { - return getFirstPosition(); - } - @Override - public Position getEndPosition() { - return getLastPosition(); - } - @Override - public int getAnotherRowNumber(int curRowNumber) { - return curRowNumber + 1; - } - @Override - public int getAnotherPageNumber() { - return _ownedPagesCursor.getNextPage(); - } - @Override - public int getInitialRowNumber(int rowsOnPage) { - return -1; - } - } - - /** - * Handles moving the table scan cursor backward. - */ - private final class ReverseScanDirHandler extends ScanDirHandler { - @Override - public Position getBeginningPosition() { - return getLastPosition(); - } - @Override - public Position getEndPosition() { - return getFirstPosition(); - } - @Override - public int getAnotherRowNumber(int curRowNumber) { - return curRowNumber - 1; - } - @Override - public int getAnotherPageNumber() { - return _ownedPagesCursor.getPreviousPage(); - } - @Override - public int getInitialRowNumber(int rowsOnPage) { - return rowsOnPage; - } - } - - } - - /** * Identifier for a cursor. Will be equal to any other cursor of the same * type for the same table. Primarily used to check the validity of a * Savepoint. */ - public static final class Id - { - private final String _tableName; - private final String _indexName; - - protected Id(Table table, Index index) { - _tableName = table.getName(); - _indexName = ((index != null) ? index.getName() : null); - } - - @Override - public int hashCode() { - return _tableName.hashCode(); - } - - @Override - public boolean equals(Object o) { - return((this == o) || - ((o != null) && (getClass() == o.getClass()) && - ObjectUtils.equals(_tableName, ((Id)o)._tableName) && - ObjectUtils.equals(_indexName, ((Id)o)._indexName))); - } - - @Override - public String toString() { - return getClass().getSimpleName() + " " + _tableName + ":" + _indexName; - } + public interface Id + { } - /** - * Value object which represents a complete save state of the cursor. - */ - public static final class Savepoint - { - private final Id _cursorId; - private final Position _curPos; - private final Position _prevPos; - - private Savepoint(Id cursorId, Position curPos, Position prevPos) { - _cursorId = cursorId; - _curPos = curPos; - _prevPos = prevPos; - } - - public Id getCursorId() { - return _cursorId; - } - - public Position getCurrentPosition() { - return _curPos; - } - - private Position getPreviousPosition() { - return _prevPos; - } - - @Override - public String toString() { - return getClass().getSimpleName() + " " + _cursorId + " CurPosition " + - _curPos + ", PrevPosition " + _prevPos; - } - } - /** * Value object which maintains the current position of the cursor. */ - public static abstract class Position - { - protected Position() { - } - - @Override - public final int hashCode() { - return getRowId().hashCode(); - } - - @Override - public final boolean equals(Object o) { - return((this == o) || - ((o != null) && (getClass() == o.getClass()) && equalsImpl(o))); - } - + public interface Position + { /** * Returns the unique RowId of the position of the cursor. */ - public abstract RowId getRowId(); - - /** - * Returns {@code true} if the subclass specific info in a Position is - * equal, {@code false} otherwise. - * @param o object being tested for equality, guaranteed to be the same - * class as this object - */ - protected abstract boolean equalsImpl(Object o); + public RowId getRowId(); } /** - * Value object which maintains the current position of a TableScanCursor. + * Value object which represents a complete save state of the cursor. + * Savepoints are created by calling {@link Cursor#getSavepoint} and used by + * calling {@link Cursor#restoreSavepoint} to return the the cursor state at + * the time the Savepoint was created. */ - private static final class ScanPosition extends Position + public interface Savepoint { - private final RowId _rowId; - - private ScanPosition(RowId rowId) { - _rowId = rowId; - } + public Id getCursorId(); - @Override - public RowId getRowId() { - return _rowId; - } - - @Override - protected boolean equalsImpl(Object o) { - return getRowId().equals(((ScanPosition)o).getRowId()); - } - - @Override - public String toString() { - return "RowId = " + getRowId(); - } + public Position getCurrentPosition(); } - + } diff --git a/src/java/com/healthmarketscience/jackcess/CursorBuilder.java b/src/java/com/healthmarketscience/jackcess/CursorBuilder.java index 4e955d0..9485090 100644 --- a/src/java/com/healthmarketscience/jackcess/CursorBuilder.java +++ b/src/java/com/healthmarketscience/jackcess/CursorBuilder.java @@ -33,6 +33,13 @@ import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.Map; + +import com.healthmarketscience.jackcess.impl.TableImpl; +import com.healthmarketscience.jackcess.impl.IndexImpl; +import com.healthmarketscience.jackcess.impl.CursorImpl; +import com.healthmarketscience.jackcess.impl.IndexCursorImpl; +import com.healthmarketscience.jackcess.util.ColumnMatcher; /** @@ -44,9 +51,9 @@ import java.util.List; */ public class CursorBuilder { /** the table which the cursor will traverse */ - private final Table _table; + private final TableImpl _table; /** optional index to use in traversal */ - private Index _index; + private IndexImpl _index; /** optional start row for an index cursor */ private Object[] _startRow; /** whether or not start row for an index cursor is inclusive */ @@ -63,7 +70,7 @@ public class CursorBuilder { private ColumnMatcher _columnMatcher; public CursorBuilder(Table table) { - _table = table; + _table = (TableImpl)table; } /** @@ -96,7 +103,7 @@ public class CursorBuilder { * Sets an index to use for the cursor. */ public CursorBuilder setIndex(Index index) { - _index = index; + _index = (IndexImpl)index; return this; } @@ -139,14 +146,14 @@ public class CursorBuilder { */ private CursorBuilder setIndexByColumns(List searchColumns) { boolean found = false; - for(Index index : _table.getIndexes()) { + for(IndexImpl index : _table.getIndexes()) { - Collection indexColumns = index.getColumns(); + Collection indexColumns = index.getColumns(); if(indexColumns.size() != searchColumns.size()) { continue; } Iterator sIter = searchColumns.iterator(); - Iterator iIter = indexColumns.iterator(); + Iterator iIter = indexColumns.iterator(); boolean matches = true; while(sIter.hasNext()) { String sColName = sIter.next(); @@ -177,7 +184,7 @@ public class CursorBuilder { *

* A valid index must be specified before calling this method. */ - public CursorBuilder setSpecificRow(Object[] specificRow) { + public CursorBuilder setSpecificRow(Object... specificRow) { setStartRow(specificRow); setEndRow(specificRow); return this; @@ -202,7 +209,7 @@ public class CursorBuilder { *

* A valid index must be specified before calling this method. */ - public CursorBuilder setStartRow(Object[] startRow) { + public CursorBuilder setStartRow(Object... startRow) { _startRow = startRow; return this; } @@ -234,7 +241,7 @@ public class CursorBuilder { *

* A valid index must be specified before calling this method. */ - public CursorBuilder setEndRow(Object[] endRow) { + public CursorBuilder setEndRow(Object... endRow) { _endRow = endRow; return this; } @@ -273,16 +280,15 @@ public class CursorBuilder { * Returns a new cursor for the table, constructed to the given * specifications. */ - public Cursor toCursor() - throws IOException + public Cursor toCursor() throws IOException { - Cursor cursor = null; + CursorImpl cursor = null; if(_index == null) { - cursor = Cursor.createCursor(_table); + cursor = CursorImpl.createCursor(_table); } else { - cursor = Cursor.createIndexCursor(_table, _index, - _startRow, _startRowInclusive, - _endRow, _endRowInclusive); + cursor = IndexCursorImpl.createCursor(_table, _index, + _startRow, _startRowInclusive, + _endRow, _endRowInclusive); } cursor.setColumnMatcher(_columnMatcher); if(_savepoint == null) { @@ -299,10 +305,194 @@ public class CursorBuilder { * Returns a new index cursor for the table, constructed to the given * specifications. */ - public IndexCursor toIndexCursor() + public IndexCursor toIndexCursor() throws IOException + { + return (IndexCursorImpl)toCursor(); + } + + /** + * Creates a normal, un-indexed cursor for the given table. + * @param table the table over which this cursor will traverse + */ + public static Cursor createCursor(Table table) throws IOException { + return table.newCursor().toCursor(); + } + + /** + * Creates an indexed cursor for the given table. + *

+ * Note, index based table traversal may not include all rows, as certain + * types of indexes do not include all entries (namely, some indexes ignore + * null entries, see {@link Index#shouldIgnoreNulls}). + * + * @param table the table over which this cursor will traverse + * @param index index for the table which will define traversal order as + * well as enhance certain lookups + */ + public static IndexCursor createCursor(Table table, Index index) throws IOException { - return (IndexCursor)toCursor(); + return table.newCursor().setIndex(index).toIndexCursor(); + } + + /** + * Creates an indexed cursor for the given table, narrowed to the given + * range. + *

+ * Note, index based table traversal may not include all rows, as certain + * types of indexes do not include all entries (namely, some indexes ignore + * null entries, see {@link Index#shouldIgnoreNulls}). + * + * @param table the table over which this cursor will traverse + * @param index index for the table which will define traversal order as + * well as enhance certain lookups + * @param startRow the first row of data for the cursor (inclusive), or + * {@code null} for the first entry + * @param endRow the last row of data for the cursor (inclusive), or + * {@code null} for the last entry + */ + public static IndexCursor createCursor(Table table, Index index, + Object[] startRow, Object[] endRow) + throws IOException + { + return table.newCursor().setIndex(index) + .setStartRow(startRow) + .setEndRow(endRow) + .toIndexCursor(); + } + + /** + * Creates an indexed cursor for the given table, narrowed to the given + * range. + *

+ * Note, index based table traversal may not include all rows, as certain + * types of indexes do not include all entries (namely, some indexes ignore + * null entries, see {@link Index#shouldIgnoreNulls}). + * + * @param table the table over which this cursor will traverse + * @param index index for the table which will define traversal order as + * well as enhance certain lookups + * @param startRow the first row of data for the cursor, or {@code null} for + * the first entry + * @param startInclusive whether or not startRow is inclusive or exclusive + * @param endRow the last row of data for the cursor, or {@code null} for + * the last entry + * @param endInclusive whether or not endRow is inclusive or exclusive + */ + public static IndexCursor createCursor(Table table, Index index, + Object[] startRow, + boolean startInclusive, + Object[] endRow, + boolean endInclusive) + throws IOException + { + return table.newCursor().setIndex(index) + .setStartRow(startRow) + .setStartRowInclusive(startInclusive) + .setEndRow(endRow) + .setEndRowInclusive(endInclusive) + .toIndexCursor(); } + /** + * Convenience method for finding a specific row in a table which matches a + * given row "pattern". See {@link Cursor#findFirstRow(Map)} for details on + * the rowPattern. + *

+ * Warning, this method always starts searching from the beginning of + * the Table (you cannot use it to find successive matches). + * + * @param table the table to search + * @param rowPattern pattern to be used to find the row + * @return the matching row or {@code null} if a match could not be found. + */ + public static Row findRow(Table table, Map rowPattern) + throws IOException + { + Cursor cursor = createCursor(table); + if(cursor.findFirstRow(rowPattern)) { + return cursor.getCurrentRow(); + } + return null; + } + + /** + * Convenience method for finding a specific row in a table which matches a + * given row "pattern". See {@link Cursor#findFirstRow(Column,Object)} for + * details on the pattern. + *

+ * Note, a {@code null} result value is ambiguous in that it could imply no + * match or a matching row with {@code null} for the desired value. If + * distinguishing this situation is important, you will need to use a Cursor + * directly instead of this convenience method. + * + * @param table the table to search + * @param column column whose value should be returned + * @param columnPattern column being matched by the valuePattern + * @param valuePattern value from the columnPattern which will match the + * desired row + * @return the matching row or {@code null} if a match could not be found. + */ + public static Object findValue(Table table, Column column, + Column columnPattern, Object valuePattern) + throws IOException + { + Cursor cursor = createCursor(table); + if(cursor.findFirstRow(columnPattern, valuePattern)) { + return cursor.getCurrentRowValue(column); + } + return null; + } + + /** + * Convenience method for finding a specific row in an indexed table which + * matches a given row "pattern". See {@link Cursor#findFirstRow(Map)} for + * details on the rowPattern. + *

+ * Warning, this method always starts searching from the beginning of + * the Table (you cannot use it to find successive matches). + * + * @param table the table to search + * @param index index to assist the search + * @param rowPattern pattern to be used to find the row + * @return the matching row or {@code null} if a match could not be found. + */ + public static Row findRow(Table table, Index index, Map rowPattern) + throws IOException + { + Cursor cursor = createCursor(table, index); + if(cursor.findFirstRow(rowPattern)) { + return cursor.getCurrentRow(); + } + return null; + } + + /** + * Convenience method for finding a specific row in a table which matches a + * given row "pattern". See {@link Cursor#findFirstRow(Column,Object)} for + * details on the pattern. + *

+ * Note, a {@code null} result value is ambiguous in that it could imply no + * match or a matching row with {@code null} for the desired value. If + * distinguishing this situation is important, you will need to use a Cursor + * directly instead of this convenience method. + * + * @param table the table to search + * @param index index to assist the search + * @param column column whose value should be returned + * @param columnPattern column being matched by the valuePattern + * @param valuePattern value from the columnPattern which will match the + * desired row + * @return the matching row or {@code null} if a match could not be found. + */ + public static Object findValue(Table table, Index index, Column column, + Column columnPattern, Object valuePattern) + throws IOException + { + Cursor cursor = createCursor(table, index); + if(cursor.findFirstRow(columnPattern, valuePattern)) { + return cursor.getCurrentRowValue(column); + } + return null; + } } diff --git a/src/java/com/healthmarketscience/jackcess/DataType.java b/src/java/com/healthmarketscience/jackcess/DataType.java index 9a5a8fb..586d16e 100644 --- a/src/java/com/healthmarketscience/jackcess/DataType.java +++ b/src/java/com/healthmarketscience/jackcess/DataType.java @@ -36,6 +36,8 @@ import java.util.Date; import java.math.BigDecimal; import java.math.BigInteger; +import com.healthmarketscience.jackcess.impl.JetFormat; + /** * Access data type * @author Tim McCune diff --git a/src/java/com/healthmarketscience/jackcess/Database.java b/src/java/com/healthmarketscience/jackcess/Database.java index 450c9b9..71a6d59 100644 --- a/src/java/com/healthmarketscience/jackcess/Database.java +++ b/src/java/com/healthmarketscience/jackcess/Database.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2005 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,104 +15,51 @@ 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.BufferedReader; import java.io.Closeable; import java.io.File; -import java.io.FileNotFoundException; import java.io.Flushable; import java.io.IOException; -import java.io.InputStream; -import java.io.RandomAccessFile; -import java.lang.ref.ReferenceQueue; -import java.lang.ref.WeakReference; -import java.nio.ByteBuffer; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; -import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Collection; -import java.util.Collections; import java.util.ConcurrentModificationException; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; import java.util.Set; import java.util.TimeZone; -import java.util.TreeSet; import com.healthmarketscience.jackcess.query.Query; -import org.apache.commons.lang.builder.ToStringBuilder; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import com.healthmarketscience.jackcess.impl.DatabaseImpl; +import com.healthmarketscience.jackcess.util.ErrorHandler; +import com.healthmarketscience.jackcess.util.LinkResolver; /** - * An Access database. + * An Access database instance. A new instance can be instantiated by opening + * an existing database file ({@link DatabaseBuilder#open(File)}) or creating + * a new database file ({@link DatabaseBuilder#create(Database.FileFormat,File)}) (for + * more advanced opening/creating use {@link DatabaseBuilder}). Once a + * Database has been opened, you can interact with the data via the relevant + * {@link Table}. When a Database instance is no longer useful, it should + * always be closed ({@link #close}) to avoid corruption. *

- * There is optional support for large indexes (enabled by default). This - * optional support can be disabled via a few different means: - *

    - *
  • Setting the system property {@value #USE_BIG_INDEX_PROPERTY} to - * {@code "false"} will disable "large" index support across the jvm
  • - *
  • Calling {@link #setUseBigIndex} on a Database instance will override - * any system property setting for "large" index support for all tables - * subsequently created from that instance
  • - *
  • Calling {@link #getTable(String,boolean)} can selectively - * enable/disable "large" index support on a per-table basis (overriding - * any Database or system property setting)
  • - *
+ * Note, Database instances (and all the related objects) are not + * thread-safe. However, separate Database instances (and their respective + * objects) can be used by separate threads without a problem. * - * @author Tim McCune + * @author James Ahlborn * @usage _general_class_ */ -public class Database - implements Iterable, Closeable, Flushable +public interface Database extends Iterable
, Closeable, Flushable { - - private static final Log LOG = LogFactory.getLog(Database.class); - - /** this is the default "userId" used if we cannot find existing info. this - seems to be some standard "Admin" userId for access files */ - private static final byte[] SYS_DEFAULT_SID = new byte[2]; - static { - SYS_DEFAULT_SID[0] = (byte) 0xA6; - SYS_DEFAULT_SID[1] = (byte) 0x33; - } - /** default value for the auto-sync value ({@code true}). this is slower, * but leaves more chance of a useable database in the face of failures. * @usage _general_field_ */ public static final boolean DEFAULT_AUTO_SYNC = true; - /** the default value for the resource path used to load classpath - * resources. - * @usage _general_field_ - */ - public static final String DEFAULT_RESOURCE_PATH = - "com/healthmarketscience/jackcess/"; - /** * the default sort order for table columns. * @usage _intermediate_field_ @@ -120,13 +67,6 @@ public class Database public static final Table.ColumnOrder DEFAULT_COLUMN_ORDER = Table.ColumnOrder.DATA; - /** (boolean) system property which can be used to disable the default big - * index support. - * @usage _general_field_ - */ - public static final String USE_BIG_INDEX_PROPERTY = - "com.healthmarketscience.jackcess.bigIndex"; - /** system property which can be used to set the default TimeZone used for * date calculations. * @usage _general_field_ @@ -143,7 +83,8 @@ public class Database /** system property which can be used to set the path from which classpath * resources are loaded (must end with a "/" if non-empty). Default value - * is {@link #DEFAULT_RESOURCE_PATH} if unspecified. + * is {@value com.healthmarketscience.jackcess.impl.DatabaseImpl#DEFAULT_RESOURCE_PATH} + * if unspecified. * @usage _general_field_ */ public static final String RESOURCE_PATH_PROPERTY = @@ -151,7 +92,7 @@ public class Database /** (boolean) system property which can be used to indicate that the current * vm has a poor nio implementation (specifically for - * FileChannel.transferFrom) + * {@code FileChannel.transferFrom}) * @usage _intermediate_field_ */ public static final String BROKEN_NIO_PROPERTY = @@ -166,838 +107,184 @@ public class Database "com.healthmarketscience.jackcess.columnOrder"; /** system property which can be used to set the default enforcement of - * foreign-key relationships. Defaults to {@code false}. + * foreign-key relationships. Defaults to {@code true}. * @usage _general_field_ */ public static final String FK_ENFORCE_PROPERTY = "com.healthmarketscience.jackcess.enforceForeignKeys"; - /** - * default error handler used if none provided (just rethrows exception) - * @usage _general_field_ - */ - 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; - } - }; - - /** - * default link resolver used if none provided - * @usage _general_field_ - */ - public static final LinkResolver DEFAULT_LINK_RESOLVER = new LinkResolver() { - public Database resolveLinkedDatabase(Database linkerDb, - String linkeeFileName) - throws IOException - { - return Database.open(new File(linkeeFileName)); - } - }; - - /** the resource path to be used when loading classpath resources */ - static final String RESOURCE_PATH = - System.getProperty(RESOURCE_PATH_PROPERTY, DEFAULT_RESOURCE_PATH); - - /** whether or not this jvm has "broken" nio support */ - static final boolean BROKEN_NIO = Boolean.TRUE.toString().equalsIgnoreCase( - System.getProperty(BROKEN_NIO_PROPERTY)); - - /** System catalog always lives on page 2 */ - private static final int PAGE_SYSTEM_CATALOG = 2; - /** Name of the system catalog */ - private static final String TABLE_SYSTEM_CATALOG = "MSysObjects"; - - /** this is the access control bit field for created tables. the value used - is equivalent to full access (Visual Basic DAO PermissionEnum constant: - dbSecFullAccess) */ - private static final Integer SYS_FULL_ACCESS_ACM = 1048575; - - /** ACE table column name of the actual access control entry */ - private static final String ACE_COL_ACM = "ACM"; - /** ACE table column name of the inheritable attributes flag */ - private static final String ACE_COL_F_INHERITABLE = "FInheritable"; - /** ACE table column name of the relevant objectId */ - private static final String ACE_COL_OBJECT_ID = "ObjectId"; - /** ACE table column name of the relevant userId */ - private static final String ACE_COL_SID = "SID"; - - /** Relationship table column name of the column count */ - private static final String REL_COL_COLUMN_COUNT = "ccolumn"; - /** Relationship table column name of the flags */ - private static final String REL_COL_FLAGS = "grbit"; - /** Relationship table column name of the index of the columns */ - private static final String REL_COL_COLUMN_INDEX = "icolumn"; - /** Relationship table column name of the "to" column name */ - private static final String REL_COL_TO_COLUMN = "szColumn"; - /** Relationship table column name of the "to" table name */ - private static final String REL_COL_TO_TABLE = "szObject"; - /** Relationship table column name of the "from" column name */ - private static final String REL_COL_FROM_COLUMN = "szReferencedColumn"; - /** Relationship table column name of the "from" table name */ - private static final String REL_COL_FROM_TABLE = "szReferencedObject"; - /** Relationship table column name of the relationship */ - private static final String REL_COL_NAME = "szRelationship"; - - /** System catalog column name of the page on which system object definitions - are stored */ - private static final String CAT_COL_ID = "Id"; - /** System catalog column name of the name of a system object */ - private static final String CAT_COL_NAME = "Name"; - private static final String CAT_COL_OWNER = "Owner"; - /** System catalog column name of a system object's parent's id */ - private static final String CAT_COL_PARENT_ID = "ParentId"; - /** System catalog column name of the type of a system object */ - private static final String CAT_COL_TYPE = "Type"; - /** System catalog column name of the date a system object was created */ - private static final String CAT_COL_DATE_CREATE = "DateCreate"; - /** System catalog column name of the date a system object was updated */ - private static final String CAT_COL_DATE_UPDATE = "DateUpdate"; - /** System catalog column name of the flags column */ - private static final String CAT_COL_FLAGS = "Flags"; - /** System catalog column name of the properties column */ - private static final String CAT_COL_PROPS = "LvProp"; - /** System catalog column name of the remote database */ - private static final String CAT_COL_DATABASE = "Database"; - /** System catalog column name of the remote table name */ - private static final String CAT_COL_FOREIGN_NAME = "ForeignName"; - - /** top-level parentid for a database */ - private static final int DB_PARENT_ID = 0xF000000; - - /** the maximum size of any of the included "empty db" resources */ - private static final long MAX_EMPTYDB_SIZE = 350000L; - - /** this object is a "system" object */ - static final int SYSTEM_OBJECT_FLAG = 0x80000000; - /** this object is another type of "system" object */ - static final int ALT_SYSTEM_OBJECT_FLAG = 0x02; - /** this object is hidden */ - static final int HIDDEN_OBJECT_FLAG = 0x08; - /** all flags which seem to indicate some type of system object */ - static final int SYSTEM_OBJECT_FLAGS = - SYSTEM_OBJECT_FLAG | ALT_SYSTEM_OBJECT_FLAG; - - /** read-only channel access mode */ - static final String RO_CHANNEL_MODE = "r"; - /** read/write channel access mode */ - static final String RW_CHANNEL_MODE = "rw"; - /** * Enum which indicates which version of Access created the database. * @usage _general_class_ */ - public static enum FileFormat { + public enum FileFormat { - V1997(null, JetFormat.VERSION_3), - V2000(RESOURCE_PATH + "empty.mdb", JetFormat.VERSION_4), - V2003(RESOURCE_PATH + "empty2003.mdb", JetFormat.VERSION_4), - V2007(RESOURCE_PATH + "empty2007.accdb", JetFormat.VERSION_12, ".accdb"), - V2010(RESOURCE_PATH + "empty2010.accdb", JetFormat.VERSION_14, ".accdb"), - MSISAM(null, JetFormat.VERSION_MSISAM, ".mny"); + V1997(".mdb"), + V2000(".mdb"), + V2003(".mdb"), + V2007(".accdb"), + V2010(".accdb"), + MSISAM(".mny"); - private final String _emptyFile; - private final JetFormat _format; private final String _ext; - private FileFormat(String emptyDBFile, JetFormat jetFormat) { - this(emptyDBFile, jetFormat, ".mdb"); - } - - private FileFormat(String emptyDBFile, JetFormat jetFormat, String ext) { - _emptyFile = emptyDBFile; - _format = jetFormat; + private FileFormat(String ext) { _ext = ext; } - public JetFormat getJetFormat() { return _format; } - + /** + * @return the file extension used for database files with this format. + */ public String getFileExtension() { return _ext; } @Override - public String toString() { return name() + ", jetFormat: " + getJetFormat(); } + public String toString() { + return name() + " [" + DatabaseImpl.getFileFormatDetails(this).getFormat() + "]"; + } } - /** Prefix for column or table names that are reserved words */ - private static final String ESCAPE_PREFIX = "x"; - /** Name of the system object that is the parent of all tables */ - private static final String SYSTEM_OBJECT_NAME_TABLES = "Tables"; - /** Name of the system object that is the parent of all databases */ - private static final String SYSTEM_OBJECT_NAME_DATABASES = "Databases"; - /** Name of the system object that is the parent of all relationships */ - private static final String SYSTEM_OBJECT_NAME_RELATIONSHIPS = - "Relationships"; - /** Name of the table that contains system access control entries */ - private static final String TABLE_SYSTEM_ACES = "MSysACEs"; - /** Name of the table that contains table relationships */ - private static final String TABLE_SYSTEM_RELATIONSHIPS = "MSysRelationships"; - /** Name of the table that contains queries */ - private static final String TABLE_SYSTEM_QUERIES = "MSysQueries"; - /** Name of the table that contains complex type information */ - private static final String TABLE_SYSTEM_COMPLEX_COLS = "MSysComplexColumns"; - /** Name of the main database properties object */ - private static final String OBJECT_NAME_DB_PROPS = "MSysDb"; - /** Name of the summary properties object */ - private static final String OBJECT_NAME_SUMMARY_PROPS = "SummaryInfo"; - /** Name of the user-defined properties object */ - private static final String OBJECT_NAME_USERDEF_PROPS = "UserDefined"; - /** System object type for table definitions */ - static final Short TYPE_TABLE = 1; - /** System object type for query definitions */ - private static final Short TYPE_QUERY = 5; - /** System object type for linked table definitions */ - private static final Short TYPE_LINKED_TABLE = 6; - - /** max number of table lookups to cache */ - private static final int MAX_CACHED_LOOKUP_TABLES = 50; - - /** the columns to read when reading system catalog normally */ - private static Collection SYSTEM_CATALOG_COLUMNS = - new HashSet(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID, - CAT_COL_FLAGS, CAT_COL_DATABASE, - CAT_COL_FOREIGN_NAME)); - /** the columns to read when finding table names */ - private static Collection SYSTEM_CATALOG_TABLE_NAME_COLUMNS = - new HashSet(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID, - CAT_COL_FLAGS, CAT_COL_PARENT_ID)); - /** the columns to read when getting object propertyes */ - private static Collection SYSTEM_CATALOG_PROPS_COLUMNS = - new HashSet(Arrays.asList(CAT_COL_ID, CAT_COL_PROPS)); - - /** - * All of the reserved words in Access that should be escaped when creating - * table or column names + * Returns the File underlying this Database */ - private static final Set RESERVED_WORDS = new HashSet(); - static { - //Yup, there's a lot. - RESERVED_WORDS.addAll(Arrays.asList( - "add", "all", "alphanumeric", "alter", "and", "any", "application", "as", - "asc", "assistant", "autoincrement", "avg", "between", "binary", "bit", - "boolean", "by", "byte", "char", "character", "column", "compactdatabase", - "constraint", "container", "count", "counter", "create", "createdatabase", - "createfield", "creategroup", "createindex", "createobject", "createproperty", - "createrelation", "createtabledef", "createuser", "createworkspace", - "currency", "currentuser", "database", "date", "datetime", "delete", - "desc", "description", "disallow", "distinct", "distinctrow", "document", - "double", "drop", "echo", "else", "end", "eqv", "error", "exists", "exit", - "false", "field", "fields", "fillcache", "float", "float4", "float8", - "foreign", "form", "forms", "from", "full", "function", "general", - "getobject", "getoption", "gotopage", "group", "group by", "guid", "having", - "idle", "ieeedouble", "ieeesingle", "if", "ignore", "imp", "in", "index", - "indexes", "inner", "insert", "inserttext", "int", "integer", "integer1", - "integer2", "integer4", "into", "is", "join", "key", "lastmodified", "left", - "level", "like", "logical", "logical1", "long", "longbinary", "longtext", - "macro", "match", "max", "min", "mod", "memo", "module", "money", "move", - "name", "newpassword", "no", "not", "null", "number", "numeric", "object", - "oleobject", "off", "on", "openrecordset", "option", "or", "order", "outer", - "owneraccess", "parameter", "parameters", "partial", "percent", "pivot", - "primary", "procedure", "property", "queries", "query", "quit", "real", - "recalc", "recordset", "references", "refresh", "refreshlink", - "registerdatabase", "relation", "repaint", "repairdatabase", "report", - "reports", "requery", "right", "screen", "section", "select", "set", - "setfocus", "setoption", "short", "single", "smallint", "some", "sql", - "stdev", "stdevp", "string", "sum", "table", "tabledef", "tabledefs", - "tableid", "text", "time", "timestamp", "top", "transform", "true", "type", - "union", "unique", "update", "user", "value", "values", "var", "varp", - "varbinary", "varchar", "where", "with", "workspace", "xor", "year", "yes", - "yesno" - )); - } + public File getFile(); - /** the File of the database */ - private final File _file; - /** Buffer to hold database pages */ - private ByteBuffer _buffer; - /** ID of the Tables system object */ - private Integer _tableParentId; - /** Format that the containing database is in */ - private final JetFormat _format; /** - * Cache map of UPPERCASE table names to page numbers containing their - * definition and their stored table name (max size - * MAX_CACHED_LOOKUP_TABLES). + * @return The names of all of the user tables + * @usage _general_method_ */ - private final Map _tableLookup = - new LinkedHashMap() { - private static final long serialVersionUID = 0L; - @Override - protected boolean removeEldestEntry(Map.Entry e) { - return(size() > MAX_CACHED_LOOKUP_TABLES); - } - }; - /** set of table names as stored in the mdb file, created on demand */ - private Set _tableNames; - /** Reads and writes database pages */ - private final PageChannel _pageChannel; - /** System catalog table */ - private Table _systemCatalog; - /** utility table finder */ - private TableFinder _tableFinder; - /** System access control entries table (initialized on first use) */ - private Table _accessControlEntries; - /** System relationships table (initialized on first use) */ - private Table _relationships; - /** System queries table (initialized on first use) */ - private Table _queries; - /** System complex columns table (initialized on first use) */ - private Table _complexCols; - /** SIDs to use for the ACEs added for new tables */ - private final List _newTableSIDs = new ArrayList(); - /** "big index support" is optional, but enabled by default */ - private Boolean _useBigIndex; - /** optional error handler to use when row errors are encountered */ - private ErrorHandler _dbErrorHandler; - /** the file format of the database */ - private FileFormat _fileFormat; - /** charset to use when handling text */ - private Charset _charset; - /** timezone to use when handling dates */ - private TimeZone _timeZone; - /** language sort order to be used for textual columns */ - private Column.SortOrder _defaultSortOrder; - /** default code page to be used for textual columns (in some dbs) */ - private Short _defaultCodePage; - /** the ordering used for table columns */ - private Table.ColumnOrder _columnOrder; - /** whether or not enforcement of foreign-keys is enabled */ - private boolean _enforceForeignKeys; - /** cache of in-use tables */ - private final TableCache _tableCache = new TableCache(); - /** handler for reading/writing properteies */ - private PropertyMaps.Handler _propsHandler; - /** ID of the Databases system object */ - private Integer _dbParentId; - /** core database properties */ - private PropertyMaps _dbPropMaps; - /** summary properties */ - private PropertyMaps _summaryPropMaps; - /** user-defined properties */ - private PropertyMaps _userDefPropMaps; - /** linked table resolver */ - private LinkResolver _linkResolver; - /** any linked databases which have been opened */ - private Map _linkedDbs; - /** shared state used when enforcing foreign keys */ - private final FKEnforcer.SharedState _fkEnforcerSharedState = - FKEnforcer.initSharedState(); - /** Calendar for use interpreting dates/times in Columns */ - private Calendar _calendar; + public Set getTableNames() throws IOException; /** - * Open an existing Database. If the existing file is not writeable, the - * file will be opened read-only. Auto-syncing is enabled for the returned - * Database. - *

- * Equivalent to: - * {@code open(mdbFile, false);} - * - * @param mdbFile File containing the database - * - * @see #open(File,boolean) - * @see DatabaseBuilder for more flexible Database opening - * @usage _general_method_ + * @return The names of all of the system tables (String). Note, in order + * to read these tables, you must use {@link #getSystemTable}. + * Extreme care should be taken if modifying these tables + * directly!. + * @usage _intermediate_method_ */ - public static Database open(File mdbFile) throws IOException { - return open(mdbFile, false); - } - + public Set getSystemTableNames() throws IOException; + /** - * Open an existing Database. If the existing file is not writeable or the - * readOnly flag is {@code true}, the file will be opened read-only. - * Auto-syncing is enabled for the returned Database. - *

- * Equivalent to: - * {@code open(mdbFile, readOnly, DEFAULT_AUTO_SYNC);} - * - * @param mdbFile File containing the database - * @param readOnly iff {@code true}, force opening file in read-only - * mode - * - * @see #open(File,boolean,boolean) - * @see DatabaseBuilder for more flexible Database opening + * @return an unmodifiable Iterator of the user Tables in this Database. + * @throws RuntimeIOException if an IOException is thrown by one of the + * operations, the actual exception will be contained within + * @throws ConcurrentModificationException if a table is added to the + * database while an Iterator is in use. * @usage _general_method_ */ - public static Database open(File mdbFile, boolean readOnly) - throws IOException - { - return open(mdbFile, readOnly, DEFAULT_AUTO_SYNC); - } - + public Iterator

iterator(); + /** - * Open an existing Database. If the existing file is not writeable or the - * readOnly flag is {@code true}, the file will be opened read-only. - * @param mdbFile File containing the database - * @param readOnly iff {@code true}, force opening file in read-only - * mode - * @param autoSync whether or not to enable auto-syncing on write. if - * {@code true}, writes will be immediately flushed to disk. - * This leaves the database in a (fairly) consistent state - * on each write, but can be very inefficient for many - * updates. if {@code false}, flushing to disk happens at - * the jvm's leisure, which can be much faster, but may - * leave the database in an inconsistent state if failures - * are encountered during writing. Writes may be flushed at - * any time using {@link #flush}. - * @see DatabaseBuilder for more flexible Database opening + * @param name Table name (case-insensitive) + * @return The table, or null if it doesn't exist * @usage _general_method_ */ - public static Database open(File mdbFile, boolean readOnly, boolean autoSync) - throws IOException - { - return open(mdbFile, readOnly, autoSync, null, null); - } + public Table getTable(String name) throws IOException; /** - * Open an existing Database. If the existing file is not writeable or the - * readOnly flag is {@code true}, the file will be opened read-only. - * @param mdbFile File containing the database - * @param readOnly iff {@code true}, force opening file in read-only - * mode - * @param autoSync whether or not to enable auto-syncing on write. if - * {@code true}, writes will be immediately flushed to disk. - * This leaves the database in a (fairly) consistent state - * on each write, but can be very inefficient for many - * updates. if {@code false}, flushing to disk happens at - * the jvm's leisure, which can be much faster, but may - * leave the database in an inconsistent state if failures - * are encountered during writing. Writes may be flushed at - * any time using {@link #flush}. - * @param charset Charset to use, if {@code null}, uses default - * @param timeZone TimeZone to use, if {@code null}, uses default - * @see DatabaseBuilder for more flexible Database opening + * Finds all the relationships in the database between the given tables. * @usage _intermediate_method_ */ - public static Database open(File mdbFile, boolean readOnly, boolean autoSync, - Charset charset, TimeZone timeZone) - throws IOException - { - return open(mdbFile, readOnly, autoSync, charset, timeZone, null); - } + public List getRelationships(Table table1, Table table2) + throws IOException; /** - * Open an existing Database. If the existing file is not writeable or the - * readOnly flag is {@code true}, the file will be opened read-only. - * @param mdbFile File containing the database - * @param readOnly iff {@code true}, force opening file in read-only - * mode - * @param autoSync whether or not to enable auto-syncing on write. if - * {@code true}, writes will be immediately flushed to disk. - * This leaves the database in a (fairly) consistent state - * on each write, but can be very inefficient for many - * updates. if {@code false}, flushing to disk happens at - * the jvm's leisure, which can be much faster, but may - * leave the database in an inconsistent state if failures - * are encountered during writing. Writes may be flushed at - * any time using {@link #flush}. - * @param charset Charset to use, if {@code null}, uses default - * @param timeZone TimeZone to use, if {@code null}, uses default - * @param provider CodecProvider for handling page encoding/decoding, may be - * {@code null} if no special encoding is necessary - * @see DatabaseBuilder for more flexible Database opening + * Finds all the relationships in the database for the given table. * @usage _intermediate_method_ */ - public static Database open(File mdbFile, boolean readOnly, boolean autoSync, - Charset charset, TimeZone timeZone, - CodecProvider provider) - throws IOException - { - return open(mdbFile, readOnly, null, autoSync, charset, timeZone, - provider); - } - - /** - * Open an existing Database. If the existing file is not writeable or the - * readOnly flag is {@code true}, the file will be opened read-only. - * @param mdbFile File containing the database - * @param readOnly iff {@code true}, force opening file in read-only - * mode - * @param channel pre-opened FileChannel. if provided explicitly, it will - * not be closed by this Database instance - * @param autoSync whether or not to enable auto-syncing on write. if - * {@code true}, writes will be immediately flushed to disk. - * This leaves the database in a (fairly) consistent state - * on each write, but can be very inefficient for many - * updates. if {@code false}, flushing to disk happens at - * the jvm's leisure, which can be much faster, but may - * leave the database in an inconsistent state if failures - * are encountered during writing. Writes may be flushed at - * any time using {@link #flush}. - * @param charset Charset to use, if {@code null}, uses default - * @param timeZone TimeZone to use, if {@code null}, uses default - * @param provider CodecProvider for handling page encoding/decoding, may be - * {@code null} if no special encoding is necessary - * @usage _advanced_method_ - */ - static Database open(File mdbFile, boolean readOnly, FileChannel channel, - boolean autoSync, Charset charset, TimeZone timeZone, - CodecProvider provider) - throws IOException - { - boolean closeChannel = false; - if(channel == null) { - if(!mdbFile.exists() || !mdbFile.canRead()) { - throw new FileNotFoundException("given file does not exist: " + - mdbFile); - } - - // force read-only for non-writable files - readOnly |= !mdbFile.canWrite(); - - // open file channel - channel = openChannel(mdbFile, readOnly); - closeChannel = true; - } - - boolean success = false; - try { - - if(!readOnly) { + public List getRelationships(Table table) throws IOException; - // verify that format supports writing - JetFormat jetFormat = JetFormat.getFormat(channel); - - if(jetFormat.READ_ONLY) { - throw new IOException("jet format '" + jetFormat + - "' does not support writing"); - } - } - - Database db = new Database(mdbFile, channel, closeChannel, autoSync, - null, charset, timeZone, provider); - success = true; - return db; - - } finally { - if(!success && closeChannel) { - // something blew up, shutdown the channel (quietly) - try { - channel.close(); - } catch(Exception ignored) { - // we don't care - } - } - } - } - - /** - * Create a new Access 2000 Database - *

- * Equivalent to: - * {@code create(FileFormat.V2000, mdbFile, DEFAULT_AUTO_SYNC);} - * - * @param mdbFile Location to write the new database to. If this file - * already exists, it will be overwritten. - * - * @see #create(File,boolean) - * @see DatabaseBuilder for more flexible Database creation - * @usage _general_method_ - */ - public static Database create(File mdbFile) throws IOException { - return create(mdbFile, DEFAULT_AUTO_SYNC); - } - - /** - * Create a new Database for the given fileFormat - *

- * Equivalent to: - * {@code create(fileFormat, mdbFile, DEFAULT_AUTO_SYNC);} - * - * @param fileFormat version of new database. - * @param mdbFile Location to write the new database to. If this file - * already exists, it will be overwritten. - * - * @see #create(File,boolean) - * @see DatabaseBuilder for more flexible Database creation - * @usage _general_method_ - */ - public static Database create(FileFormat fileFormat, File mdbFile) - throws IOException - { - return create(fileFormat, mdbFile, DEFAULT_AUTO_SYNC); - } - /** - * Create a new Access 2000 Database - *

- * Equivalent to: - * {@code create(FileFormat.V2000, mdbFile, DEFAULT_AUTO_SYNC);} - * - * @param mdbFile Location to write the new database to. If this file - * already exists, it will be overwritten. - * @param autoSync whether or not to enable auto-syncing on write. if - * {@code true}, writes will be immediately flushed to disk. - * This leaves the database in a (fairly) consistent state - * on each write, but can be very inefficient for many - * updates. if {@code false}, flushing to disk happens at - * the jvm's leisure, which can be much faster, but may - * leave the database in an inconsistent state if failures - * are encountered during writing. Writes may be flushed at - * any time using {@link #flush}. - * @see DatabaseBuilder for more flexible Database creation - * @usage _general_method_ + * Finds all the relationships in the database in non-system tables. + *

+ * Warning, this may load all the Tables (metadata, not data) in the + * database which could cause memory issues. + * @usage _intermediate_method_ */ - public static Database create(File mdbFile, boolean autoSync) - throws IOException - { - return create(FileFormat.V2000, mdbFile, autoSync); - } + public List getRelationships() throws IOException; /** - * Create a new Database for the given fileFormat - * @param fileFormat version of new database. - * @param mdbFile Location to write the new database to. If this file - * already exists, it will be overwritten. - * @param autoSync whether or not to enable auto-syncing on write. if - * {@code true}, writes will be immediately flushed to disk. - * This leaves the database in a (fairly) consistent state - * on each write, but can be very inefficient for many - * updates. if {@code false}, flushing to disk happens at - * the jvm's leisure, which can be much faster, but may - * leave the database in an inconsistent state if failures - * are encountered during writing. Writes may be flushed at - * any time using {@link #flush}. - * @see DatabaseBuilder for more flexible Database creation - * @usage _general_method_ + * Finds all the relationships in the database, including system + * tables. + *

+ * Warning, this may load all the Tables (metadata, not data) in the + * database which could cause memory issues. + * @usage _intermediate_method_ */ - public static Database create(FileFormat fileFormat, File mdbFile, - boolean autoSync) - throws IOException - { - return create(fileFormat, mdbFile, autoSync, null, null); - } + public List getSystemRelationships() + throws IOException; /** - * Create a new Database for the given fileFormat - * @param fileFormat version of new database. - * @param mdbFile Location to write the new database to. If this file - * already exists, it will be overwritten. - * @param autoSync whether or not to enable auto-syncing on write. if - * {@code true}, writes will be immediately flushed to disk. - * This leaves the database in a (fairly) consistent state - * on each write, but can be very inefficient for many - * updates. if {@code false}, flushing to disk happens at - * the jvm's leisure, which can be much faster, but may - * leave the database in an inconsistent state if failures - * are encountered during writing. Writes may be flushed at - * any time using {@link #flush}. - * @param charset Charset to use, if {@code null}, uses default - * @param timeZone TimeZone to use, if {@code null}, uses default - * @see DatabaseBuilder for more flexible Database creation + * Finds all the queries in the database. * @usage _intermediate_method_ */ - public static Database create(FileFormat fileFormat, File mdbFile, - boolean autoSync, Charset charset, - TimeZone timeZone) - throws IOException - { - return create(fileFormat, mdbFile, null, autoSync, charset, timeZone); - } + public List getQueries() throws IOException; /** - * Create a new Database for the given fileFormat - * @param fileFormat version of new database. - * @param mdbFile Location to write the new database to. If this file - * already exists, it will be overwritten. - * @param channel pre-opened FileChannel. if provided explicitly, it will - * not be closed by this Database instance - * @param autoSync whether or not to enable auto-syncing on write. if - * {@code true}, writes will be immediately flushed to disk. - * This leaves the database in a (fairly) consistent state - * on each write, but can be very inefficient for many - * updates. if {@code false}, flushing to disk happens at - * the jvm's leisure, which can be much faster, but may - * leave the database in an inconsistent state if failures - * are encountered during writing. Writes may be flushed at - * any time using {@link #flush}. - * @param charset Charset to use, if {@code null}, uses default - * @param timeZone TimeZone to use, if {@code null}, uses default - * @usage _advanced_method_ + * Returns a reference to any available table in this access + * database, including system tables. + *

+ * Warning, this method is not designed for common use, only for the + * occassional time when access to a system table is necessary. Messing + * with system tables can strip the paint off your house and give your whole + * family a permanent, orange afro. You have been warned. + * + * @param tableName Table name, may be a system table + * @return The table, or {@code null} if it doesn't exist + * @usage _intermediate_method_ */ - static Database create(FileFormat fileFormat, File mdbFile, - FileChannel channel, boolean autoSync, - Charset charset, TimeZone timeZone) - throws IOException - { - if (fileFormat.getJetFormat().READ_ONLY) { - throw new IOException("jet format '" + fileFormat.getJetFormat() + "' does not support writing"); - } - - boolean closeChannel = false; - if(channel == null) { - channel = openChannel(mdbFile, false); - closeChannel = true; - } - - boolean success = false; - try { - channel.truncate(0); - transferFrom(channel, getResourceAsStream(fileFormat._emptyFile)); - channel.force(true); - Database db = new Database(mdbFile, channel, closeChannel, autoSync, - fileFormat, charset, timeZone, null); - success = true; - return db; - } finally { - if(!success && closeChannel) { - // something blew up, shutdown the channel (quietly) - try { - channel.close(); - } catch(Exception ignored) { - // we don't care - } - } - } - } + public Table getSystemTable(String tableName) throws IOException; /** - * Package visible only to support unit tests via DatabaseTest.openChannel(). - * @param mdbFile file to open - * @param readOnly true if read-only - * @return a FileChannel on the given file. - * @exception FileNotFoundException - * if the mode is "r" but the given file object does - * not denote an existing regular file, or if the mode begins - * with "rw" but the given file object does not denote - * an existing, writable regular file and a new regular file of - * that name cannot be created, or if some other error occurs - * while opening or creating the file - */ - static FileChannel openChannel(final File mdbFile, final boolean readOnly) - throws FileNotFoundException - { - final String mode = (readOnly ? RO_CHANNEL_MODE : RW_CHANNEL_MODE); - return new RandomAccessFile(mdbFile, mode).getChannel(); - } - - /** - * Create a new database by reading it in from a FileChannel. - * @param file the File to which the channel is connected - * @param channel File channel of the database. This needs to be a - * FileChannel instead of a ReadableByteChannel because we need to - * randomly jump around to various points in the file. - * @param autoSync whether or not to enable auto-syncing on write. if - * {@code true}, writes will be immediately flushed to disk. - * This leaves the database in a (fairly) consistent state - * on each write, but can be very inefficient for many - * updates. if {@code false}, flushing to disk happens at - * the jvm's leisure, which can be much faster, but may - * leave the database in an inconsistent state if failures - * are encountered during writing. Writes may be flushed at - * any time using {@link #flush}. - * @param fileFormat version of new database (if known) - * @param charset Charset to use, if {@code null}, uses default - * @param timeZone TimeZone to use, if {@code null}, uses default + * @return the core properties for the database + * @usage _general_method_ */ - protected Database(File file, FileChannel channel, boolean closeChannel, - boolean autoSync, FileFormat fileFormat, Charset charset, - TimeZone timeZone, CodecProvider provider) - throws IOException - { - _file = file; - _format = JetFormat.getFormat(channel); - _charset = ((charset == null) ? getDefaultCharset(_format) : charset); - _columnOrder = getDefaultColumnOrder(); - _enforceForeignKeys = getDefaultEnforceForeignKeys(); - _fileFormat = fileFormat; - _pageChannel = new PageChannel(channel, closeChannel, _format, autoSync); - _timeZone = ((timeZone == null) ? getDefaultTimeZone() : timeZone); - if(provider == null) { - provider = DefaultCodecProvider.INSTANCE; - } - // note, it's slighly sketchy to pass ourselves along partially - // constructed, but only our _format and _pageChannel refs should be - // needed - _pageChannel.initialize(this, provider); - _buffer = _pageChannel.createPageBuffer(); - readSystemCatalog(); - } + public PropertyMap getDatabaseProperties() throws IOException; /** - * Returns the File underlying this Database + * @return the summary properties for the database + * @usage _general_method_ */ - public File getFile() { - return _file; - } + public PropertyMap getSummaryProperties() throws IOException; /** - * @usage _advanced_method_ + * @return the user-defined properties for the database + * @usage _general_method_ */ - public PageChannel getPageChannel() { - return _pageChannel; - } + public PropertyMap getUserDefinedProperties() throws IOException; /** - * @usage _advanced_method_ - */ - public JetFormat getFormat() { - return _format; - } - - /** - * @return The system catalog table - * @usage _advanced_method_ - */ - public Table getSystemCatalog() { - return _systemCatalog; - } - - /** - * @return The system Access Control Entries table (loaded on demand) - * @usage _advanced_method_ + * @return the current database password, or {@code null} if none set. + * @usage _general_method_ */ - public Table getAccessControlEntries() throws IOException { - if(_accessControlEntries == null) { - _accessControlEntries = getSystemTable(TABLE_SYSTEM_ACES); - if(_accessControlEntries == null) { - throw new IOException("Could not find system table " + - TABLE_SYSTEM_ACES); - } - - } - return _accessControlEntries; - } + public String getDatabasePassword() throws IOException; /** - * @return the complex column system table (loaded on demand) - * @usage _advanced_method_ + * Create a new table in this database + * @param name Name of the table to create in this database + * @param linkedDbName path to the linked database + * @param linkedTableName name of the table in the linked database + * @usage _general_method_ */ - public Table getSystemComplexColumns() throws IOException { - if(_complexCols == null) { - _complexCols = getSystemTable(TABLE_SYSTEM_COMPLEX_COLS); - if(_complexCols == null) { - throw new IOException("Could not find system table " + - TABLE_SYSTEM_COMPLEX_COLS); - } - } - return _complexCols; - } - + public void createLinkedTable(String name, String linkedDbName, + String linkedTableName) + throws IOException; + /** - * Whether or not big index support is enabled for tables. - * @usage _advanced_method_ + * Flushes any current changes to the database file (and any linked + * databases) to disk. + * @usage _general_method_ */ - public boolean doUseBigIndex() { - return (_useBigIndex != null ? _useBigIndex : true); - } + public void flush() throws IOException; /** - * Set whether or not big index support is enabled for tables. - * @usage _intermediate_method_ + * Close the database file (and any linked databases). A Database + * must be closed after use or changes could be lost and the Database + * file corrupted. A Database instance should be treated like any other + * external resource which would be closed in a finally block (e.g. an + * OutputStream or jdbc Connection). + * @usage _general_method_ */ - public void setUseBigIndex(boolean useBigIndex) { - _useBigIndex = useBigIndex; - } + public void close() throws IOException; /** * Gets the currently configured ErrorHandler (always non-{@code null}). @@ -1005,37 +292,28 @@ public class Database * Cursor level. * @usage _intermediate_method_ */ - public ErrorHandler getErrorHandler() { - return((_dbErrorHandler != null) ? _dbErrorHandler : - DEFAULT_ERROR_HANDLER); - } + public ErrorHandler getErrorHandler(); /** * Sets a new ErrorHandler. If {@code null}, resets to the - * {@link #DEFAULT_ERROR_HANDLER}. + * {@link ErrorHandler#DEFAULT}. * @usage _intermediate_method_ */ - public void setErrorHandler(ErrorHandler newErrorHandler) { - _dbErrorHandler = newErrorHandler; - } + public void setErrorHandler(ErrorHandler newErrorHandler); /** * Gets the currently configured LinkResolver (always non-{@code null}). * This will be used to handle all linked database loading. * @usage _intermediate_method_ */ - public LinkResolver getLinkResolver() { - return((_linkResolver != null) ? _linkResolver : DEFAULT_LINK_RESOLVER); - } + public LinkResolver getLinkResolver(); /** * Sets a new LinkResolver. If {@code null}, resets to the - * {@link #DEFAULT_LINK_RESOLVER}. + * {@link LinkResolver#DEFAULT}. * @usage _intermediate_method_ */ - public void setLinkResolver(LinkResolver newLinkResolver) { - _linkResolver = newLinkResolver; - } + public void setLinkResolver(LinkResolver newLinkResolver); /** * Returns an unmodifiable view of the currently loaded linked databases, @@ -1043,123 +321,57 @@ public class Database * information may be useful for implementing a LinkResolver. * @usage _intermediate_method_ */ - public Map getLinkedDatabases() { - return ((_linkedDbs == null) ? Collections.emptyMap() : - Collections.unmodifiableMap(_linkedDbs)); - } + public Map getLinkedDatabases(); /** * Gets currently configured TimeZone (always non-{@code null}). * @usage _intermediate_method_ */ - public TimeZone getTimeZone() { - return _timeZone; - } + public TimeZone getTimeZone(); /** - * Sets a new TimeZone. If {@code null}, resets to the value returned by - * {@link #getDefaultTimeZone}. + * Sets a new TimeZone. If {@code null}, resets to the default value. * @usage _intermediate_method_ */ - public void setTimeZone(TimeZone newTimeZone) { - if(newTimeZone == null) { - newTimeZone = getDefaultTimeZone(); - } - _timeZone = newTimeZone; - // clear cached calendar when timezone is changed - _calendar = null; - } + public void setTimeZone(TimeZone newTimeZone); /** * Gets currently configured Charset (always non-{@code null}). * @usage _intermediate_method_ */ - public Charset getCharset() - { - return _charset; - } + public Charset getCharset(); /** - * Sets a new Charset. If {@code null}, resets to the value returned by - * {@link #getDefaultCharset}. + * Sets a new Charset. If {@code null}, resets to the default value. * @usage _intermediate_method_ */ - public void setCharset(Charset newCharset) { - if(newCharset == null) { - newCharset = getDefaultCharset(getFormat()); - } - _charset = newCharset; - } + public void setCharset(Charset newCharset); /** * Gets currently configured {@link Table.ColumnOrder} (always non-{@code * null}). * @usage _intermediate_method_ */ - public Table.ColumnOrder getColumnOrder() { - return _columnOrder; - } + public Table.ColumnOrder getColumnOrder(); /** - * Sets a new Table.ColumnOrder. If {@code null}, resets to the value - * returned by {@link #getDefaultColumnOrder}. + * Sets a new Table.ColumnOrder. If {@code null}, resets to the default value. * @usage _intermediate_method_ */ - public void setColumnOrder(Table.ColumnOrder newColumnOrder) { - if(newColumnOrder == null) { - newColumnOrder = getDefaultColumnOrder(); - } - _columnOrder = newColumnOrder; - } + public void setColumnOrder(Table.ColumnOrder newColumnOrder); /** * Gets currently foreign-key enforcement policy. * @usage _intermediate_method_ */ - public boolean isEnforceForeignKeys() { - return _enforceForeignKeys; - } + public boolean isEnforceForeignKeys(); /** * Sets a new foreign-key enforcement policy. If {@code null}, resets to - * the value returned by {@link #isEnforceForeignKeys}. + * the default value. * @usage _intermediate_method_ */ - public void setEnforceForeignKeys(Boolean newEnforceForeignKeys) { - if(newEnforceForeignKeys == null) { - newEnforceForeignKeys = getDefaultEnforceForeignKeys(); - } - _enforceForeignKeys = newEnforceForeignKeys; - } - - /** - * @usage _advanced_method_ - */ - FKEnforcer.SharedState getFKEnforcerSharedState() { - return _fkEnforcerSharedState; - } - - /** - * @usage _advanced_method_ - */ - Calendar getCalendar() - { - if(_calendar == null) { - _calendar = Calendar.getInstance(_timeZone); - } - return _calendar; - } - - /** - * @returns the current handler for reading/writing properties, creating if - * necessary - */ - private PropertyMaps.Handler getPropsHandler() { - if(_propsHandler == null) { - _propsHandler = new PropertyMaps.Handler(this); - } - return _propsHandler; - } + public void setEnforceForeignKeys(Boolean newEnforceForeignKeys); /** * Returns the FileFormat of this database (which may involve inspecting the @@ -1167,1648 +379,6 @@ public class Database * @throws IllegalStateException if the file format cannot be determined * @usage _general_method_ */ - public FileFormat getFileFormat() throws IOException { + public FileFormat getFileFormat() throws IOException; - if(_fileFormat == null) { - - Map possibleFileFormats = - getFormat().getPossibleFileFormats(); - - if(possibleFileFormats.size() == 1) { - - // single possible format (null key), easy enough - _fileFormat = possibleFileFormats.get(null); - - } else { - - // need to check the "AccessVersion" property - String accessVersion = (String)getDatabaseProperties().getValue( - PropertyMap.ACCESS_VERSION_PROP); - - _fileFormat = possibleFileFormats.get(accessVersion); - - if(_fileFormat == null) { - throw new IllegalStateException("Could not determine FileFormat"); - } - } - } - return _fileFormat; - } - - /** - * @return a (possibly cached) page ByteBuffer for internal use. the - * returned buffer should be released using - * {@link #releaseSharedBuffer} when no longer in use - */ - private ByteBuffer takeSharedBuffer() { - // we try to re-use a single shared _buffer, but occassionally, it may be - // needed by multiple operations at the same time (e.g. loading a - // secondary table while loading a primary table). this method ensures - // that we don't corrupt the _buffer, but instead force the second caller - // to use a new buffer. - if(_buffer != null) { - ByteBuffer curBuffer = _buffer; - _buffer = null; - return curBuffer; - } - return _pageChannel.createPageBuffer(); - } - - /** - * Relinquishes use of a page ByteBuffer returned by - * {@link #takeSharedBuffer}. - */ - private void releaseSharedBuffer(ByteBuffer buffer) { - // we always stuff the returned buffer back into _buffer. it doesn't - // really matter if multiple values over-write, at the end of the day, we - // just need one shared buffer - _buffer = buffer; - } - - /** - * @return the currently configured database default language sort order for - * textual columns - * @usage _intermediate_method_ - */ - public Column.SortOrder getDefaultSortOrder() throws IOException { - - if(_defaultSortOrder == null) { - initRootPageInfo(); - } - return _defaultSortOrder; - } - - /** - * @return the currently configured database default code page for textual - * data (may not be relevant to all database versions) - * @usage _intermediate_method_ - */ - public short getDefaultCodePage() throws IOException { - - if(_defaultCodePage == null) { - initRootPageInfo(); - } - return _defaultCodePage; - } - - /** - * Reads various config info from the db page 0. - */ - private void initRootPageInfo() throws IOException { - ByteBuffer buffer = takeSharedBuffer(); - try { - _pageChannel.readPage(buffer, 0); - _defaultSortOrder = Column.readSortOrder( - buffer, _format.OFFSET_SORT_ORDER, _format); - _defaultCodePage = buffer.getShort(_format.OFFSET_CODE_PAGE); - } finally { - releaseSharedBuffer(buffer); - } - } - - /** - * @return a PropertyMaps instance decoded from the given bytes (always - * returns non-{@code null} result). - * @usage _intermediate_method_ - */ - public PropertyMaps readProperties(byte[] propsBytes, int objectId) - throws IOException - { - return getPropsHandler().read(propsBytes, objectId); - } - - /** - * Read the system catalog - */ - private void readSystemCatalog() throws IOException { - _systemCatalog = readTable(TABLE_SYSTEM_CATALOG, PAGE_SYSTEM_CATALOG, - SYSTEM_OBJECT_FLAGS, defaultUseBigIndex()); - - try { - _tableFinder = new DefaultTableFinder( - new CursorBuilder(_systemCatalog) - .setIndexByColumnNames(CAT_COL_PARENT_ID, CAT_COL_NAME) - .setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE) - .toIndexCursor()); - } catch(IllegalArgumentException e) { - LOG.info("Could not find expected index on table " + - _systemCatalog.getName()); - // use table scan instead - _tableFinder = new FallbackTableFinder( - new CursorBuilder(_systemCatalog) - .setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE) - .toCursor()); - } - - _tableParentId = _tableFinder.findObjectId(DB_PARENT_ID, - SYSTEM_OBJECT_NAME_TABLES); - - if(_tableParentId == null) { - throw new IOException("Did not find required parent table id"); - } - - if (LOG.isDebugEnabled()) { - LOG.debug("Finished reading system catalog. Tables: " + - getTableNames()); - } - } - - /** - * @return The names of all of the user tables (String) - * @usage _general_method_ - */ - public Set getTableNames() throws IOException { - if(_tableNames == null) { - Set tableNames = - new TreeSet(String.CASE_INSENSITIVE_ORDER); - _tableFinder.getTableNames(tableNames, false); - _tableNames = tableNames; - } - return _tableNames; - } - - /** - * @return The names of all of the system tables (String). Note, in order - * to read these tables, you must use {@link #getSystemTable}. - * Extreme care should be taken if modifying these tables - * directly!. - * @usage _intermediate_method_ - */ - public Set getSystemTableNames() throws IOException { - Set sysTableNames = - new TreeSet(String.CASE_INSENSITIVE_ORDER); - _tableFinder.getTableNames(sysTableNames, true); - return sysTableNames; - } - - /** - * @return an unmodifiable Iterator of the user Tables in this Database. - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within - * @throws ConcurrentModificationException if a table is added to the - * database while an Iterator is in use. - * @usage _general_method_ - */ - public Iterator

iterator() { - return new TableIterator(); - } - - /** - * @param name Table name - * @return The table, or null if it doesn't exist - * @usage _general_method_ - */ - public Table getTable(String name) throws IOException { - return getTable(name, defaultUseBigIndex()); - } - - /** - * @param name Table name - * @param useBigIndex whether or not "big index support" should be enabled - * for the table (this value will override any other - * settings) - * @return The table, or null if it doesn't exist - * @usage _intermediate_method_ - */ - public Table getTable(String name, boolean useBigIndex) throws IOException { - return getTable(name, false, useBigIndex); - } - - /** - * @param tableDefPageNumber the page number of a table definition - * @return The table, or null if it doesn't exist - * @usage _advanced_method_ - */ - public Table getTable(int tableDefPageNumber) throws IOException { - - // first, check for existing table - Table table = _tableCache.get(tableDefPageNumber); - if(table != null) { - return table; - } - - // lookup table info from system catalog - Map objectRow = _tableFinder.getObjectRow( - tableDefPageNumber, SYSTEM_CATALOG_COLUMNS); - if(objectRow == null) { - return null; - } - - String name = (String)objectRow.get(CAT_COL_NAME); - int flags = (Integer)objectRow.get(CAT_COL_FLAGS); - - return readTable(name, tableDefPageNumber, flags, defaultUseBigIndex()); - } - - /** - * @param name Table name - * @param includeSystemTables whether to consider returning a system table - * @param useBigIndex whether or not "big index support" should be enabled - * for the table (this value will override any other - * settings) - * @return The table, or null if it doesn't exist - */ - private Table getTable(String name, boolean includeSystemTables, - boolean useBigIndex) - throws IOException - { - TableInfo tableInfo = lookupTable(name); - - if ((tableInfo == null) || (tableInfo.pageNumber == null)) { - return null; - } - if(!includeSystemTables && isSystemObject(tableInfo.flags)) { - return null; - } - - if(tableInfo.isLinked()) { - - if(_linkedDbs == null) { - _linkedDbs = new HashMap(); - } - - String linkedDbName = ((LinkedTableInfo)tableInfo).linkedDbName; - String linkedTableName = ((LinkedTableInfo)tableInfo).linkedTableName; - Database linkedDb = _linkedDbs.get(linkedDbName); - if(linkedDb == null) { - linkedDb = getLinkResolver().resolveLinkedDatabase(this, linkedDbName); - _linkedDbs.put(linkedDbName, linkedDb); - } - - return linkedDb.getTable(linkedTableName, includeSystemTables, - useBigIndex); - } - - return readTable(tableInfo.tableName, tableInfo.pageNumber, - tableInfo.flags, useBigIndex); - } - - /** - * Create a new table in this database - * @param name Name of the table to create - * @param columns List of Columns in the table - * @usage _general_method_ - */ - public void createTable(String name, List columns) - throws IOException - { - createTable(name, columns, null); - } - - /** - * Create a new table in this database - * @param name Name of the table to create - * @param columns List of Columns in the table - * @param indexes List of IndexBuilders describing indexes for the table - * @usage _general_method_ - */ - public void createTable(String name, List columns, - List indexes) - throws IOException - { - if(lookupTable(name) != null) { - throw new IllegalArgumentException( - "Cannot create table with name of existing table"); - } - - new TableCreator(this, name, columns, indexes).createTable(); - } - - /** - * Create a new table in this database - * @param name Name of the table to create - * @usage _general_method_ - */ - public void createLinkedTable(String name, String linkedDbName, - String linkedTableName) - throws IOException - { - if(lookupTable(name) != null) { - throw new IllegalArgumentException( - "Cannot create linked table with name of existing table"); - } - - validateIdentifierName(name, getFormat().MAX_TABLE_NAME_LENGTH, "table"); - validateIdentifierName(linkedDbName, DataType.MEMO.getMaxSize(), - "linked database"); - validateIdentifierName(linkedTableName, getFormat().MAX_TABLE_NAME_LENGTH, - "linked table"); - - int linkedTableId = _tableFinder.getNextFreeSyntheticId(); - - addNewTable(name, linkedTableId, TYPE_LINKED_TABLE, linkedDbName, - linkedTableName); - } - - /** - * Adds a newly created table to the relevant internal database structures. - */ - void addNewTable(String name, int tdefPageNumber, Short type, - String linkedDbName, String linkedTableName) - throws IOException - { - //Add this table to our internal list. - addTable(name, Integer.valueOf(tdefPageNumber), type, linkedDbName, - linkedTableName); - - //Add this table to system tables - addToSystemCatalog(name, tdefPageNumber, type, linkedDbName, - linkedTableName); - addToAccessControlEntries(tdefPageNumber); - } - - /** - * Finds all the relationships in the database between the given tables. - * @usage _intermediate_method_ - */ - public List getRelationships(Table table1, Table table2) - throws IOException - { - int nameCmp = table1.getName().compareTo(table2.getName()); - if(nameCmp == 0) { - throw new IllegalArgumentException("Must provide two different tables"); - } - if(nameCmp > 0) { - // we "order" the two tables given so that we will return a collection - // of relationships in the same order regardless of whether we are given - // (TableFoo, TableBar) or (TableBar, TableFoo). - Table tmp = table1; - table1 = table2; - table2 = tmp; - } - - return getRelationshipsImpl(table1, table2, true); - } - - /** - * Finds all the relationships in the database for the given table. - * @usage _intermediate_method_ - */ - public List getRelationships(Table table) - throws IOException - { - if(table == null) { - throw new IllegalArgumentException("Must provide a table"); - } - // since we are getting relationships specific to certain table include - // all tables - return getRelationshipsImpl(table, null, true); - } - - /** - * Finds all the relationships in the database in non-system tables. - *

- * Warning, this may load all the Tables (metadata, not data) in the - * database which could cause memory issues. - * @usage _intermediate_method_ - */ - public List getRelationships() - throws IOException - { - return getRelationshipsImpl(null, null, false); - } - - /** - * Finds all the relationships in the database, including system - * tables. - *

- * Warning, this may load all the Tables (metadata, not data) in the - * database which could cause memory issues. - * @usage _intermediate_method_ - */ - public List getSystemRelationships() - throws IOException - { - return getRelationshipsImpl(null, null, true); - } - - private List getRelationshipsImpl(Table table1, Table table2, - boolean includeSystemTables) - throws IOException - { - // the relationships table does not get loaded until first accessed - if(_relationships == null) { - _relationships = getSystemTable(TABLE_SYSTEM_RELATIONSHIPS); - if(_relationships == null) { - throw new IOException("Could not find system relationships table"); - } - } - - List relationships = new ArrayList(); - - if(table1 != null) { - Cursor cursor = createCursorWithOptionalIndex( - _relationships, REL_COL_FROM_TABLE, table1.getName()); - collectRelationships(cursor, table1, table2, relationships, - includeSystemTables); - cursor = createCursorWithOptionalIndex( - _relationships, REL_COL_TO_TABLE, table1.getName()); - collectRelationships(cursor, table2, table1, relationships, - includeSystemTables); - } else { - collectRelationships(new CursorBuilder(_relationships).toCursor(), - null, null, relationships, includeSystemTables); - } - - return relationships; - } - - /** - * Finds all the queries in the database. - * @usage _intermediate_method_ - */ - public List getQueries() - throws IOException - { - // the queries table does not get loaded until first accessed - if(_queries == null) { - _queries = getSystemTable(TABLE_SYSTEM_QUERIES); - if(_queries == null) { - throw new IOException("Could not find system queries table"); - } - } - - // find all the queries from the system catalog - List> queryInfo = new ArrayList>(); - Map> queryRowMap = - new HashMap>(); - for(Map row : - Cursor.createCursor(_systemCatalog).iterable(SYSTEM_CATALOG_COLUMNS)) - { - String name = (String) row.get(CAT_COL_NAME); - if (name != null && TYPE_QUERY.equals(row.get(CAT_COL_TYPE))) { - queryInfo.add(row); - Integer id = (Integer)row.get(CAT_COL_ID); - queryRowMap.put(id, new ArrayList()); - } - } - - // find all the query rows - for(Map row : Cursor.createCursor(_queries)) { - Query.Row queryRow = new Query.Row(row); - List queryRows = queryRowMap.get(queryRow.objectId); - if(queryRows == null) { - LOG.warn("Found rows for query with id " + queryRow.objectId + - " missing from system catalog"); - continue; - } - queryRows.add(queryRow); - } - - // lastly, generate all the queries - List queries = new ArrayList(); - for(Map row : queryInfo) { - String name = (String) row.get(CAT_COL_NAME); - Integer id = (Integer)row.get(CAT_COL_ID); - int flags = (Integer)row.get(CAT_COL_FLAGS); - List queryRows = queryRowMap.get(id); - queries.add(Query.create(flags, name, queryRows, id)); - } - - return queries; - } - - /** - * Returns a reference to any available table in this access - * database, including system tables. - *

- * Warning, this method is not designed for common use, only for the - * occassional time when access to a system table is necessary. Messing - * with system tables can strip the paint off your house and give your whole - * family a permanent, orange afro. You have been warned. - * - * @param tableName Table name, may be a system table - * @return The table, or {@code null} if it doesn't exist - * @usage _intermediate_method_ - */ - public Table getSystemTable(String tableName) - throws IOException - { - return getTable(tableName, true, defaultUseBigIndex()); - } - - /** - * @return the core properties for the database - * @usage _general_method_ - */ - public PropertyMap getDatabaseProperties() throws IOException { - if(_dbPropMaps == null) { - _dbPropMaps = getPropertiesForDbObject(OBJECT_NAME_DB_PROPS); - } - return _dbPropMaps.getDefault(); - } - - /** - * @return the summary properties for the database - * @usage _general_method_ - */ - public PropertyMap getSummaryProperties() throws IOException { - if(_summaryPropMaps == null) { - _summaryPropMaps = getPropertiesForDbObject(OBJECT_NAME_SUMMARY_PROPS); - } - return _summaryPropMaps.getDefault(); - } - - /** - * @return the user-defined properties for the database - * @usage _general_method_ - */ - public PropertyMap getUserDefinedProperties() throws IOException { - if(_userDefPropMaps == null) { - _userDefPropMaps = getPropertiesForDbObject(OBJECT_NAME_USERDEF_PROPS); - } - return _userDefPropMaps.getDefault(); - } - - /** - * @return the PropertyMaps for the object with the given id - * @usage _advanced_method_ - */ - public PropertyMaps getPropertiesForObject(int objectId) - throws IOException - { - Map objectRow = _tableFinder.getObjectRow( - objectId, SYSTEM_CATALOG_PROPS_COLUMNS); - byte[] propsBytes = null; - if(objectRow != null) { - propsBytes = (byte[])objectRow.get(CAT_COL_PROPS); - } - return readProperties(propsBytes, objectId); - } - - /** - * @return property group for the given "database" object - */ - private PropertyMaps getPropertiesForDbObject(String dbName) - throws IOException - { - if(_dbParentId == null) { - // need the parent if of the databases objects - _dbParentId = _tableFinder.findObjectId(DB_PARENT_ID, - SYSTEM_OBJECT_NAME_DATABASES); - if(_dbParentId == null) { - throw new IOException("Did not find required parent db id"); - } - } - - Map objectRow = _tableFinder.getObjectRow( - _dbParentId, dbName, SYSTEM_CATALOG_PROPS_COLUMNS); - byte[] propsBytes = null; - int objectId = -1; - if(objectRow != null) { - propsBytes = (byte[])objectRow.get(CAT_COL_PROPS); - objectId = (Integer)objectRow.get(CAT_COL_ID); - } - return readProperties(propsBytes, objectId); - } - - /** - * @return the current database password, or {@code null} if none set. - * @usage _general_method_ - */ - public String getDatabasePassword() throws IOException - { - ByteBuffer buffer = takeSharedBuffer(); - try { - _pageChannel.readPage(buffer, 0); - - byte[] pwdBytes = new byte[_format.SIZE_PASSWORD]; - buffer.position(_format.OFFSET_PASSWORD); - buffer.get(pwdBytes); - - // de-mask password using extra password mask if necessary (the extra - // password mask is generated from the database creation date stored in - // the header) - byte[] pwdMask = getPasswordMask(buffer, _format); - if(pwdMask != null) { - for(int i = 0; i < pwdBytes.length; ++i) { - pwdBytes[i] ^= pwdMask[i % pwdMask.length]; - } - } - - boolean hasPassword = false; - for(int i = 0; i < pwdBytes.length; ++i) { - if(pwdBytes[i] != 0) { - hasPassword = true; - break; - } - } - - if(!hasPassword) { - return null; - } - - String pwd = Column.decodeUncompressedText(pwdBytes, getCharset()); - - // remove any trailing null chars - int idx = pwd.indexOf('\0'); - if(idx >= 0) { - pwd = pwd.substring(0, idx); - } - - return pwd; - } finally { - releaseSharedBuffer(buffer); - } - } - - /** - * Finds the relationships matching the given from and to tables from the - * given cursor and adds them to the given list. - */ - private void collectRelationships( - Cursor cursor, Table fromTable, Table toTable, - List relationships, boolean includeSystemTables) - throws IOException - { - String fromTableName = ((fromTable != null) ? fromTable.getName() : null); - String toTableName = ((toTable != null) ? toTable.getName() : null); - - for(Map row : cursor) { - String fromName = (String)row.get(REL_COL_FROM_TABLE); - String toName = (String)row.get(REL_COL_TO_TABLE); - - if(((fromTableName == null) || - fromTableName.equalsIgnoreCase(fromName)) && - ((toTableName == null) || - toTableName.equalsIgnoreCase(toName))) { - - String relName = (String)row.get(REL_COL_NAME); - - // found more info for a relationship. see if we already have some - // info for this relationship - Relationship rel = null; - for(Relationship tmp : relationships) { - if(tmp.getName().equalsIgnoreCase(relName)) { - rel = tmp; - break; - } - } - - Table relFromTable = fromTable; - if(relFromTable == null) { - relFromTable = getTable(fromName, includeSystemTables, - defaultUseBigIndex()); - if(relFromTable == null) { - // invalid table or ignoring system tables, just ignore - continue; - } - } - Table relToTable = toTable; - if(relToTable == null) { - relToTable = getTable(toName, includeSystemTables, - defaultUseBigIndex()); - if(relToTable == null) { - // invalid table or ignoring system tables, just ignore - continue; - } - } - - if(rel == null) { - // new relationship - int numCols = (Integer)row.get(REL_COL_COLUMN_COUNT); - int flags = (Integer)row.get(REL_COL_FLAGS); - rel = new Relationship(relName, relFromTable, relToTable, - flags, numCols); - relationships.add(rel); - } - - // add column info - int colIdx = (Integer)row.get(REL_COL_COLUMN_INDEX); - Column fromCol = relFromTable.getColumn( - (String)row.get(REL_COL_FROM_COLUMN)); - Column toCol = relToTable.getColumn( - (String)row.get(REL_COL_TO_COLUMN)); - - rel.getFromColumns().set(colIdx, fromCol); - rel.getToColumns().set(colIdx, toCol); - } - } - } - - /** - * Add a new table to the system catalog - * @param name Table name - * @param pageNumber Page number that contains the table definition - */ - private void addToSystemCatalog(String name, int pageNumber, Short type, - String linkedDbName, String linkedTableName) - throws IOException - { - Object[] catalogRow = new Object[_systemCatalog.getColumnCount()]; - int idx = 0; - Date creationTime = new Date(); - for (Iterator iter = _systemCatalog.getColumns().iterator(); - iter.hasNext(); idx++) - { - Column col = iter.next(); - if (CAT_COL_ID.equals(col.getName())) { - catalogRow[idx] = Integer.valueOf(pageNumber); - } else if (CAT_COL_NAME.equals(col.getName())) { - catalogRow[idx] = name; - } else if (CAT_COL_TYPE.equals(col.getName())) { - catalogRow[idx] = type; - } else if (CAT_COL_DATE_CREATE.equals(col.getName()) || - CAT_COL_DATE_UPDATE.equals(col.getName())) { - catalogRow[idx] = creationTime; - } else if (CAT_COL_PARENT_ID.equals(col.getName())) { - catalogRow[idx] = _tableParentId; - } else if (CAT_COL_FLAGS.equals(col.getName())) { - catalogRow[idx] = Integer.valueOf(0); - } else if (CAT_COL_OWNER.equals(col.getName())) { - byte[] owner = new byte[2]; - catalogRow[idx] = owner; - owner[0] = (byte) 0xcf; - owner[1] = (byte) 0x5f; - } else if (CAT_COL_DATABASE.equals(col.getName())) { - catalogRow[idx] = linkedDbName; - } else if (CAT_COL_FOREIGN_NAME.equals(col.getName())) { - catalogRow[idx] = linkedTableName; - } - } - _systemCatalog.addRow(catalogRow); - } - - /** - * Add a new table to the system's access control entries - * @param pageNumber Page number that contains the table definition - */ - private void addToAccessControlEntries(int pageNumber) throws IOException { - - if(_newTableSIDs.isEmpty()) { - initNewTableSIDs(); - } - - Table acEntries = getAccessControlEntries(); - Column acmCol = acEntries.getColumn(ACE_COL_ACM); - Column inheritCol = acEntries.getColumn(ACE_COL_F_INHERITABLE); - Column objIdCol = acEntries.getColumn(ACE_COL_OBJECT_ID); - Column sidCol = acEntries.getColumn(ACE_COL_SID); - - // construct a collection of ACE entries mimicing those of our parent, the - // "Tables" system object - List aceRows = new ArrayList(_newTableSIDs.size()); - for(byte[] sid : _newTableSIDs) { - Object[] aceRow = new Object[acEntries.getColumnCount()]; - acmCol.setRowValue(aceRow, SYS_FULL_ACCESS_ACM); - inheritCol.setRowValue(aceRow, Boolean.FALSE); - objIdCol.setRowValue(aceRow, Integer.valueOf(pageNumber)); - sidCol.setRowValue(aceRow, sid); - aceRows.add(aceRow); - } - acEntries.addRows(aceRows); - } - - /** - * Determines the collection of SIDs which need to be added to new tables. - */ - private void initNewTableSIDs() throws IOException - { - // search for ACEs matching the tableParentId. use the index on the - // objectId column if found (should be there) - Cursor cursor = createCursorWithOptionalIndex( - getAccessControlEntries(), ACE_COL_OBJECT_ID, _tableParentId); - - for(Map row : cursor) { - Integer objId = (Integer)row.get(ACE_COL_OBJECT_ID); - if(_tableParentId.equals(objId)) { - _newTableSIDs.add((byte[])row.get(ACE_COL_SID)); - } - } - - if(_newTableSIDs.isEmpty()) { - // if all else fails, use the hard-coded default - _newTableSIDs.add(SYS_DEFAULT_SID); - } - } - - /** - * Reads a table with the given name from the given pageNumber. - */ - private Table readTable(String name, int pageNumber, int flags, - boolean useBigIndex) - throws IOException - { - // first, check for existing table - Table table = _tableCache.get(pageNumber); - if(table != null) { - return table; - } - - ByteBuffer buffer = takeSharedBuffer(); - try { - // need to load table from db - _pageChannel.readPage(buffer, pageNumber); - byte pageType = buffer.get(0); - if (pageType != PageTypes.TABLE_DEF) { - throw new IOException( - "Looking for " + name + " at page " + pageNumber + - ", but page type is " + pageType); - } - return _tableCache.put( - new Table(this, buffer, pageNumber, name, flags, useBigIndex)); - } finally { - releaseSharedBuffer(buffer); - } - } - - /** - * Creates a Cursor restricted to the given column value if possible (using - * an existing index), otherwise a simple table cursor. - */ - private static Cursor createCursorWithOptionalIndex( - Table table, String colName, Object colValue) - throws IOException - { - try { - return new CursorBuilder(table) - .setIndexByColumns(table.getColumn(colName)) - .setSpecificEntry(colValue) - .toCursor(); - } catch(IllegalArgumentException e) { - LOG.info("Could not find expected index on table " + table.getName()); - } - // use table scan instead - return Cursor.createCursor(table); - } - - /** - * Copy an existing JDBC ResultSet into a new table in this database - * - * @param name Name of the new table to create - * @param source ResultSet to copy from - * - * @return the name of the copied table - * - * @see ImportUtil#importResultSet(ResultSet,Database,String) - * @usage _general_method_ - */ - public String copyTable(String name, ResultSet source) - throws SQLException, IOException - { - return ImportUtil.importResultSet(source, this, name); - } - - /** - * Copy an existing JDBC ResultSet into a new table in this database - * - * @param name Name of the new table to create - * @param source ResultSet to copy from - * @param filter valid import filter - * - * @return the name of the imported table - * - * @see ImportUtil#importResultSet(ResultSet,Database,String,ImportFilter) - * @usage _general_method_ - */ - public String copyTable(String name, ResultSet source, ImportFilter filter) - throws SQLException, IOException - { - return ImportUtil.importResultSet(source, this, name, filter); - } - - /** - * Copy a delimited text file into a new table in this database - * - * @param name Name of the new table to create - * @param f Source file to import - * @param delim Regular expression representing the delimiter string. - * - * @return the name of the imported table - * - * @see ImportUtil#importFile(File,Database,String,String) - * @usage _general_method_ - */ - public String importFile(String name, File f, String delim) - throws IOException - { - return ImportUtil.importFile(f, this, name, delim); - } - - /** - * Copy a delimited text file into a new table in this database - * - * @param name Name of the new table to create - * @param f Source file to import - * @param delim Regular expression representing the delimiter string. - * @param filter valid import filter - * - * @return the name of the imported table - * - * @see ImportUtil#importFile(File,Database,String,String,ImportFilter) - * @usage _general_method_ - */ - public String importFile(String name, File f, String delim, - ImportFilter filter) - throws IOException - { - return ImportUtil.importFile(f, this, name, delim, filter); - } - - /** - * Copy a delimited text file into a new table in this database - * - * @param name Name of the new table to create - * @param in Source reader to import - * @param delim Regular expression representing the delimiter string. - * - * @return the name of the imported table - * - * @see ImportUtil#importReader(BufferedReader,Database,String,String) - * @usage _general_method_ - */ - public String importReader(String name, BufferedReader in, String delim) - throws IOException - { - return ImportUtil.importReader(in, this, name, delim); - } - - /** - * Copy a delimited text file into a new table in this database - * @param name Name of the new table to create - * @param in Source reader to import - * @param delim Regular expression representing the delimiter string. - * @param filter valid import filter - * - * @return the name of the imported table - * - * @see ImportUtil#importReader(BufferedReader,Database,String,String,ImportFilter) - * @usage _general_method_ - */ - public String importReader(String name, BufferedReader in, String delim, - ImportFilter filter) - throws IOException - { - return ImportUtil.importReader(in, this, name, delim, filter); - } - - /** - * Flushes any current changes to the database file (and any linked - * databases) to disk. - * @usage _general_method_ - */ - public void flush() throws IOException { - if(_linkedDbs != null) { - for(Database linkedDb : _linkedDbs.values()) { - linkedDb.flush(); - } - } - _pageChannel.flush(); - } - - /** - * Close the database file (and any linked databases) - * @usage _general_method_ - */ - public void close() throws IOException { - if(_linkedDbs != null) { - for(Database linkedDb : _linkedDbs.values()) { - linkedDb.close(); - } - } - _pageChannel.close(); - } - - /** - * @return A table or column name escaped for Access - * @usage _general_method_ - */ - public static String escapeIdentifier(String s) { - if (isReservedWord(s)) { - return ESCAPE_PREFIX + s; - } - return s; - } - - /** - * @return {@code true} if the given string is a reserved word, - * {@code false} otherwise - * @usage _general_method_ - */ - public static boolean isReservedWord(String s) { - return RESERVED_WORDS.contains(s.toLowerCase()); - } - - /** - * Validates an identifier name. - * @usage _advanced_method_ - */ - public static void validateIdentifierName(String name, - int maxLength, - String identifierType) - { - if((name == null) || (name.trim().length() == 0)) { - throw new IllegalArgumentException( - identifierType + " must have non-empty name"); - } - if(name.length() > maxLength) { - throw new IllegalArgumentException( - identifierType + " name is longer than max length of " + maxLength + - ": " + name); - } - } - - @Override - public String toString() { - return ToStringBuilder.reflectionToString(this); - } - - /** - * Adds a table to the _tableLookup and resets the _tableNames set - */ - private void addTable(String tableName, Integer pageNumber, Short type, - String linkedDbName, String linkedTableName) - { - _tableLookup.put(toLookupName(tableName), - createTableInfo(tableName, pageNumber, 0, type, - linkedDbName, linkedTableName)); - // clear this, will be created next time needed - _tableNames = null; - } - - /** - * Creates a TableInfo instance appropriate for the given table data. - */ - private static TableInfo createTableInfo( - String tableName, Integer pageNumber, int flags, Short type, - String linkedDbName, String linkedTableName) - { - if(TYPE_LINKED_TABLE.equals(type)) { - return new LinkedTableInfo(pageNumber, tableName, flags, linkedDbName, - linkedTableName); - } - return new TableInfo(pageNumber, tableName, flags); - } - - /** - * @return the tableInfo of the given table, if any - */ - private TableInfo lookupTable(String tableName) throws IOException { - - String lookupTableName = toLookupName(tableName); - TableInfo tableInfo = _tableLookup.get(lookupTableName); - if(tableInfo != null) { - return tableInfo; - } - - tableInfo = _tableFinder.lookupTable(tableName); - - if(tableInfo != null) { - // cache for later - _tableLookup.put(lookupTableName, tableInfo); - } - - return tableInfo; - } - - /** - * @return a string usable in the _tableLookup map. - */ - static String toLookupName(String name) { - return ((name != null) ? name.toUpperCase() : null); - } - - /** - * @return {@code true} if the given flags indicate that an object is some - * sort of system object, {@code false} otherwise. - */ - private static boolean isSystemObject(int flags) { - return ((flags & SYSTEM_OBJECT_FLAGS) != 0); - } - - /** - * Returns {@code false} if "big index support" has been disabled explicity - * on the this Database or via a system property, {@code true} otherwise. - * @usage _advanced_method_ - */ - public boolean defaultUseBigIndex() { - if(_useBigIndex != null) { - return _useBigIndex; - } - String prop = System.getProperty(USE_BIG_INDEX_PROPERTY); - if(prop != null) { - return Boolean.TRUE.toString().equalsIgnoreCase(prop); - } - return true; - } - - /** - * Returns the default TimeZone. This is normally the platform default - * TimeZone as returned by {@link TimeZone#getDefault}, but can be - * overridden using the system property {@value #TIMEZONE_PROPERTY}. - * @usage _advanced_method_ - */ - public static TimeZone getDefaultTimeZone() - { - String tzProp = System.getProperty(TIMEZONE_PROPERTY); - if(tzProp != null) { - tzProp = tzProp.trim(); - if(tzProp.length() > 0) { - return TimeZone.getTimeZone(tzProp); - } - } - - // use system default - return TimeZone.getDefault(); - } - - /** - * Returns the default Charset for the given JetFormat. This may or may not - * be platform specific, depending on the format, but can be overridden - * using a system property composed of the prefix - * {@value #CHARSET_PROPERTY_PREFIX} followed by the JetFormat version to - * which the charset should apply, e.g. {@code - * "com.healthmarketscience.jackcess.charset.VERSION_3"}. - * @usage _advanced_method_ - */ - public static Charset getDefaultCharset(JetFormat format) - { - String csProp = System.getProperty(CHARSET_PROPERTY_PREFIX + format); - if(csProp != null) { - csProp = csProp.trim(); - if(csProp.length() > 0) { - return Charset.forName(csProp); - } - } - - // use format default - return format.CHARSET; - } - - /** - * Returns the default Table.ColumnOrder. This defaults to - * {@link #DEFAULT_COLUMN_ORDER}, but can be overridden using the system - * property {@value #COLUMN_ORDER_PROPERTY}. - * @usage _advanced_method_ - */ - public static Table.ColumnOrder getDefaultColumnOrder() - { - String coProp = System.getProperty(COLUMN_ORDER_PROPERTY); - if(coProp != null) { - coProp = coProp.trim(); - if(coProp.length() > 0) { - return Table.ColumnOrder.valueOf(coProp); - } - } - - // use default order - return DEFAULT_COLUMN_ORDER; - } - - /** - * Returns the default enforce foreign-keys policy. This defaults to - * {@code false}, but can be overridden using the system - * property {@value #FK_ENFORCE_PROPERTY}. - * @usage _advanced_method_ - */ - public static boolean getDefaultEnforceForeignKeys() - { - String prop = System.getProperty(FK_ENFORCE_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. - */ - private static void transferFrom(FileChannel channel, InputStream in) - throws IOException - { - ReadableByteChannel readChannel = Channels.newChannel(in); - if(!BROKEN_NIO) { - // sane implementation - channel.transferFrom(readChannel, 0, MAX_EMPTYDB_SIZE); - } else { - // do things the hard way for broken vms - ByteBuffer bb = ByteBuffer.allocate(8096); - while(readChannel.read(bb) >= 0) { - bb.flip(); - channel.write(bb); - bb.clear(); - } - } - } - - /** - * Returns the password mask retrieved from the given header page and - * format, or {@code null} if this format does not use a password mask. - */ - static byte[] getPasswordMask(ByteBuffer buffer, JetFormat format) - { - // get extra password mask if necessary (the extra password mask is - // generated from the database creation date stored in the header) - int pwdMaskPos = format.OFFSET_HEADER_DATE; - if(pwdMaskPos < 0) { - return null; - } - - buffer.position(pwdMaskPos); - double dateVal = Double.longBitsToDouble(buffer.getLong()); - - byte[] pwdMask = new byte[4]; - PageChannel.wrap(pwdMask).putInt((int)dateVal); - - return pwdMask; - } - - static InputStream getResourceAsStream(String resourceName) - throws IOException - { - InputStream stream = Database.class.getClassLoader() - .getResourceAsStream(resourceName); - - if(stream == null) { - - stream = Thread.currentThread().getContextClassLoader() - .getResourceAsStream(resourceName); - - if(stream == null) { - throw new IOException("Could not load jackcess resource " + - resourceName); - } - } - - return stream; - } - - private static boolean isTableType(Short objType) { - return(TYPE_TABLE.equals(objType) || TYPE_LINKED_TABLE.equals(objType)); - } - - /** - * Utility class for storing table page number and actual name. - */ - private static class TableInfo - { - public final Integer pageNumber; - public final String tableName; - public final int flags; - - private TableInfo(Integer newPageNumber, String newTableName, int newFlags) { - pageNumber = newPageNumber; - tableName = newTableName; - flags = newFlags; - } - - public boolean isLinked() { - return false; - } - } - - /** - * Utility class for storing linked table info - */ - private static class LinkedTableInfo extends TableInfo - { - private final String linkedDbName; - private final String linkedTableName; - - private LinkedTableInfo(Integer newPageNumber, String newTableName, - int newFlags, String newLinkedDbName, - String newLinkedTableName) { - super(newPageNumber, newTableName, newFlags); - linkedDbName = newLinkedDbName; - linkedTableName = newLinkedTableName; - } - - @Override - public boolean isLinked() { - return true; - } - } - - /** - * Table iterator for this database, unmodifiable. - */ - private class TableIterator implements Iterator

- { - private Iterator _tableNameIter; - - private TableIterator() { - try { - _tableNameIter = getTableNames().iterator(); - } catch(IOException e) { - throw new IllegalStateException(e); - } - } - - public boolean hasNext() { - return _tableNameIter.hasNext(); - } - - public void remove() { - throw new UnsupportedOperationException(); - } - - public Table next() { - if(!hasNext()) { - throw new NoSuchElementException(); - } - try { - return getTable(_tableNameIter.next()); - } catch(IOException e) { - throw new IllegalStateException(e); - } - } - } - - /** - * Utility class for handling table lookups. - */ - private abstract class TableFinder - { - public Integer findObjectId(Integer parentId, String name) - throws IOException - { - Cursor cur = findRow(parentId, name); - if(cur == null) { - return null; - } - Column idCol = _systemCatalog.getColumn(CAT_COL_ID); - return (Integer)cur.getCurrentRowValue(idCol); - } - - public Map getObjectRow(Integer parentId, String name, - Collection columns) - throws IOException - { - Cursor cur = findRow(parentId, name); - return ((cur != null) ? cur.getCurrentRow(columns) : null); - } - - public Map getObjectRow( - Integer objectId, Collection columns) - throws IOException - { - Cursor cur = findRow(objectId); - return ((cur != null) ? cur.getCurrentRow(columns) : null); - } - - public void getTableNames(Set tableNames, - boolean systemTables) - throws IOException - { - for(Map row : getTableNamesCursor().iterable( - SYSTEM_CATALOG_TABLE_NAME_COLUMNS)) { - - String tableName = (String)row.get(CAT_COL_NAME); - int flags = (Integer)row.get(CAT_COL_FLAGS); - Short type = (Short)row.get(CAT_COL_TYPE); - int parentId = (Integer)row.get(CAT_COL_PARENT_ID); - - if((parentId == _tableParentId) && isTableType(type) && - (isSystemObject(flags) == systemTables)) { - tableNames.add(tableName); - } - } - } - - protected abstract Cursor findRow(Integer parentId, String name) - throws IOException; - - protected abstract Cursor findRow(Integer objectId) - throws IOException; - - protected abstract Cursor getTableNamesCursor() throws IOException; - - public abstract TableInfo lookupTable(String tableName) - throws IOException; - - protected abstract int findMaxSyntheticId() throws IOException; - - public int getNextFreeSyntheticId() throws IOException - { - int maxSynthId = findMaxSyntheticId(); - if(maxSynthId >= -1) { - // bummer, no more ids available - throw new IllegalStateException("Too many database objects!"); - } - return maxSynthId + 1; - } - } - - /** - * Normal table lookup handler, using catalog table index. - */ - private final class DefaultTableFinder extends TableFinder - { - private final IndexCursor _systemCatalogCursor; - private IndexCursor _systemCatalogIdCursor; - - private DefaultTableFinder(IndexCursor systemCatalogCursor) { - _systemCatalogCursor = systemCatalogCursor; - } - - private void initIdCursor() throws IOException { - if(_systemCatalogIdCursor == null) { - _systemCatalogIdCursor = new CursorBuilder(_systemCatalog) - .setIndexByColumnNames(CAT_COL_ID) - .toIndexCursor(); - } - } - - @Override - protected Cursor findRow(Integer parentId, String name) - throws IOException - { - return (_systemCatalogCursor.findFirstRowByEntry(parentId, name) ? - _systemCatalogCursor : null); - } - - @Override - protected Cursor findRow(Integer objectId) throws IOException - { - initIdCursor(); - return (_systemCatalogIdCursor.findFirstRowByEntry(objectId) ? - _systemCatalogIdCursor : null); - } - - @Override - public TableInfo lookupTable(String tableName) throws IOException { - - if(findRow(_tableParentId, tableName) == null) { - return null; - } - - Map row = _systemCatalogCursor.getCurrentRow( - SYSTEM_CATALOG_COLUMNS); - Integer pageNumber = (Integer)row.get(CAT_COL_ID); - String realName = (String)row.get(CAT_COL_NAME); - int flags = (Integer)row.get(CAT_COL_FLAGS); - Short type = (Short)row.get(CAT_COL_TYPE); - - if(!isTableType(type)) { - return null; - } - - String linkedDbName = (String)row.get(CAT_COL_DATABASE); - String linkedTableName = (String)row.get(CAT_COL_FOREIGN_NAME); - - return createTableInfo(realName, pageNumber, flags, type, linkedDbName, - linkedTableName); - } - - @Override - protected Cursor getTableNamesCursor() throws IOException { - return new CursorBuilder(_systemCatalog) - .setIndex(_systemCatalogCursor.getIndex()) - .setStartEntry(_tableParentId, IndexData.MIN_VALUE) - .setEndEntry(_tableParentId, IndexData.MAX_VALUE) - .toIndexCursor(); - } - - @Override - protected int findMaxSyntheticId() throws IOException { - initIdCursor(); - _systemCatalogIdCursor.reset(); - - // synthetic ids count up from min integer. so the current, highest, - // in-use synthetic id is the max id < 0. - _systemCatalogIdCursor.findClosestRowByEntry(0); - if(!_systemCatalogIdCursor.moveToPreviousRow()) { - return Integer.MIN_VALUE; - } - Column idCol = _systemCatalog.getColumn(CAT_COL_ID); - return (Integer)_systemCatalogIdCursor.getCurrentRowValue(idCol); - } - } - - /** - * Fallback table lookup handler, using catalog table scans. - */ - private final class FallbackTableFinder extends TableFinder - { - private final Cursor _systemCatalogCursor; - - private FallbackTableFinder(Cursor systemCatalogCursor) { - _systemCatalogCursor = systemCatalogCursor; - } - - @Override - protected Cursor findRow(Integer parentId, String name) - throws IOException - { - Map rowPat = new HashMap(); - rowPat.put(CAT_COL_PARENT_ID, parentId); - rowPat.put(CAT_COL_NAME, name); - return (_systemCatalogCursor.findFirstRow(rowPat) ? - _systemCatalogCursor : null); - } - - @Override - protected Cursor findRow(Integer objectId) throws IOException - { - Column idCol = _systemCatalog.getColumn(CAT_COL_ID); - return (_systemCatalogCursor.findFirstRow(idCol, objectId) ? - _systemCatalogCursor : null); - } - - @Override - public TableInfo lookupTable(String tableName) throws IOException { - - for(Map row : _systemCatalogCursor.iterable( - SYSTEM_CATALOG_TABLE_NAME_COLUMNS)) { - - Short type = (Short)row.get(CAT_COL_TYPE); - if(!isTableType(type)) { - continue; - } - - int parentId = (Integer)row.get(CAT_COL_PARENT_ID); - if(parentId != _tableParentId) { - continue; - } - - String realName = (String)row.get(CAT_COL_NAME); - if(!tableName.equalsIgnoreCase(realName)) { - continue; - } - - Integer pageNumber = (Integer)row.get(CAT_COL_ID); - int flags = (Integer)row.get(CAT_COL_FLAGS); - String linkedDbName = (String)row.get(CAT_COL_DATABASE); - String linkedTableName = (String)row.get(CAT_COL_FOREIGN_NAME); - - return createTableInfo(realName, pageNumber, flags, type, linkedDbName, - linkedTableName); - } - - return null; - } - - @Override - protected Cursor getTableNamesCursor() throws IOException { - return _systemCatalogCursor; - } - - @Override - protected int findMaxSyntheticId() throws IOException { - // find max id < 0 - Column idCol = _systemCatalog.getColumn(CAT_COL_ID); - _systemCatalogCursor.reset(); - int curMaxSynthId = Integer.MIN_VALUE; - while(_systemCatalogCursor.moveToNextRow()) { - int id = (Integer)_systemCatalogCursor.getCurrentRowValue(idCol); - if((id > curMaxSynthId) && (id < 0)) { - curMaxSynthId = id; - } - } - return curMaxSynthId; - } - } - - /** - * WeakReference for a Table which holds the table pageNumber (for later - * cache purging). - */ - private static final class WeakTableReference extends WeakReference
- { - private final Integer _pageNumber; - - private WeakTableReference(Integer pageNumber, Table table, - ReferenceQueue
queue) { - super(table, queue); - _pageNumber = pageNumber; - } - - public Integer getPageNumber() { - return _pageNumber; - } - } - - /** - * Cache of currently in-use tables, allows re-use of existing tables. - */ - private static final class TableCache - { - private final Map _tables = - new HashMap(); - private final ReferenceQueue
_queue = new ReferenceQueue
(); - - public Table get(Integer pageNumber) { - WeakTableReference ref = _tables.get(pageNumber); - return ((ref != null) ? ref.get() : null); - } - - public Table put(Table table) { - purgeOldRefs(); - - Integer pageNumber = table.getTableDefPageNumber(); - WeakTableReference ref = new WeakTableReference( - pageNumber, table, _queue); - _tables.put(pageNumber, ref); - - return table; - } - - private void purgeOldRefs() { - WeakTableReference oldRef = null; - while((oldRef = (WeakTableReference)_queue.poll()) != null) { - _tables.remove(oldRef.getPageNumber()); - } - } - } } diff --git a/src/java/com/healthmarketscience/jackcess/DatabaseBuilder.java b/src/java/com/healthmarketscience/jackcess/DatabaseBuilder.java index fa0a394..e9ea26e 100644 --- a/src/java/com/healthmarketscience/jackcess/DatabaseBuilder.java +++ b/src/java/com/healthmarketscience/jackcess/DatabaseBuilder.java @@ -25,6 +25,10 @@ import java.nio.channels.FileChannel; import java.nio.charset.Charset; import java.util.TimeZone; +import com.healthmarketscience.jackcess.impl.DatabaseImpl; +import com.healthmarketscience.jackcess.impl.CodecProvider; +import com.healthmarketscience.jackcess.util.MemFileChannel; + /** * Builder style class for opening/creating a Database. * @@ -81,12 +85,13 @@ public class DatabaseBuilder /** * Sets whether or not to enable auto-syncing on write. if {@code true}, - * writes will be immediately flushed to disk. This leaves the database in - * a (fairly) consistent state on each write, but can be very inefficient - * for many updates. if {@code false}, flushing to disk happens at the - * jvm's leisure, which can be much faster, but may leave the database in an - * inconsistent state if failures are encountered during writing. Writes - * may be flushed at any time using {@link Database#flush}. + * write operations will be immediately flushed to disk upon completion. + * This leaves the database in a (fairly) consistent state on each write, + * but can be very inefficient for many updates. if {@code false}, flushing + * to disk happens at the jvm's leisure, which can be much faster, but may + * leave the database in an inconsistent state if failures are encountered + * during writing. Writes may be flushed at any time using {@link + * Database#flush}. * @usage _intermediate_method_ */ public DatabaseBuilder setAutoSync(boolean autoSync) { @@ -149,15 +154,47 @@ public class DatabaseBuilder * Opens an existingnew Database using the configured information. */ public Database open() throws IOException { - return Database.open(_mdbFile, _readOnly, _channel, _autoSync, _charset, - _timeZone, _codecProvider); + return DatabaseImpl.open(_mdbFile, _readOnly, _channel, _autoSync, _charset, + _timeZone, _codecProvider); } /** * Creates a new Database using the configured information. */ public Database create() throws IOException { - return Database.create(_fileFormat, _mdbFile, _channel, _autoSync, _charset, - _timeZone); + return DatabaseImpl.create(_fileFormat, _mdbFile, _channel, _autoSync, + _charset, _timeZone); + } + + /** + * Open an existing Database. If the existing file is not writeable, the + * file will be opened read-only. Auto-syncing is enabled for the returned + * Database. + * + * @param mdbFile File containing the database + * + * @see DatabaseBuilder for more flexible Database opening + * @usage _general_method_ + */ + public static Database open(File mdbFile) throws IOException { + return new DatabaseBuilder(mdbFile).open(); } + + /** + * Create a new Database for the given fileFormat + * + * @param fileFormat version of new database. + * @param mdbFile Location to write the new database to. If this file + * already exists, it will be overwritten. + * + * @see DatabaseBuilder for more flexible Database creation + * @usage _general_method_ + */ + public static Database create(Database.FileFormat fileFormat, File mdbFile) + throws IOException + { + return new DatabaseBuilder(mdbFile).setFileFormat(fileFormat).create(); + } + + } diff --git a/src/java/com/healthmarketscience/jackcess/Index.java b/src/java/com/healthmarketscience/jackcess/Index.java index 9fc24c3..4054c66 100644 --- a/src/java/com/healthmarketscience/jackcess/Index.java +++ b/src/java/com/healthmarketscience/jackcess/Index.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2005 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,207 +15,43 @@ 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.nio.ByteBuffer; -import java.util.Collections; import java.util.List; -import java.util.Map; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - /** - * Access table (logical) index. Logical indexes are backed for IndexData, - * where one or more logical indexes could be backed by the same data. - * - * @author Tim McCune + * + * @author James Ahlborn */ -public class Index implements Comparable { - - protected static final Log LOG = LogFactory.getLog(Index.class); - - /** index type for primary key indexes */ - static final byte PRIMARY_KEY_INDEX_TYPE = (byte)1; - - /** index type for foreign key indexes */ - static final byte FOREIGN_KEY_INDEX_TYPE = (byte)2; - - /** flag for indicating that updates should cascade in a foreign key index */ - private static final byte CASCADE_UPDATES_FLAG = (byte)1; - /** flag for indicating that deletes should cascade in a foreign key index */ - private static final byte CASCADE_DELETES_FLAG = (byte)1; - - /** index table type for the "primary" table in a foreign key index */ - private static final byte PRIMARY_TABLE_TYPE = (byte)1; - - /** indicate an invalid index number for foreign key field */ - private static final int INVALID_INDEX_NUMBER = -1; - - /** the actual data backing this index (more than one index may be backed by - the same data */ - private final IndexData _data; - /** 0-based index number */ - private final int _indexNumber; - /** the type of the index */ - private final byte _indexType; - /** Index name */ - private String _name; - /** foreign key reference info, if any */ - private final ForeignKeyReference _reference; - - protected Index(ByteBuffer tableBuffer, List indexDatas, - JetFormat format) - throws IOException - { - - ByteUtil.forward(tableBuffer, format.SKIP_BEFORE_INDEX_SLOT); //Forward past Unknown - _indexNumber = tableBuffer.getInt(); - int indexDataNumber = tableBuffer.getInt(); - - // read foreign key reference info - byte relIndexType = tableBuffer.get(); - int relIndexNumber = tableBuffer.getInt(); - int relTablePageNumber = tableBuffer.getInt(); - byte cascadeUpdatesFlag = tableBuffer.get(); - byte cascadeDeletesFlag = tableBuffer.get(); - - _indexType = tableBuffer.get(); - - if((_indexType == FOREIGN_KEY_INDEX_TYPE) && - (relIndexNumber != INVALID_INDEX_NUMBER)) { - _reference = new ForeignKeyReference( - relIndexType, relIndexNumber, relTablePageNumber, - (cascadeUpdatesFlag == CASCADE_UPDATES_FLAG), - (cascadeDeletesFlag == CASCADE_DELETES_FLAG)); - } else { - _reference = null; - } - - ByteUtil.forward(tableBuffer, format.SKIP_AFTER_INDEX_SLOT); //Skip past Unknown - - _data = indexDatas.get(indexDataNumber); - - _data.addIndex(this); - } - - public IndexData getIndexData() { - return _data; - } - - public Table getTable() { - return getIndexData().getTable(); - } - - public JetFormat getFormat() { - return getTable().getFormat(); - } - - public PageChannel getPageChannel() { - return getTable().getPageChannel(); - } - - public int getIndexNumber() { - return _indexNumber; - } - - public byte getIndexFlags() { - return getIndexData().getIndexFlags(); - } - - public int getUniqueEntryCount() { - return getIndexData().getUniqueEntryCount(); - } +public interface Index +{ - public int getUniqueEntryCountOffset() { - return getIndexData().getUniqueEntryCountOffset(); - } + public Table getTable(); - public String getName() { - return _name; - } - - public void setName(String name) { - _name = name; - } + public String getName(); - public boolean isPrimaryKey() { - return _indexType == PRIMARY_KEY_INDEX_TYPE; - } + public boolean isPrimaryKey(); - public boolean isForeignKey() { - return _indexType == FOREIGN_KEY_INDEX_TYPE; - } + public boolean isForeignKey(); - public ForeignKeyReference getReference() { - return _reference; - } + /** + * @return the Columns for this index (unmodifiable) + */ + public List getColumns(); /** * @return the Index referenced by this Index's ForeignKeyReference (if it * has one), otherwise {@code null}. */ - public Index getReferencedIndex() throws IOException { - - if(_reference == null) { - return null; - } - - Table refTable = getTable().getDatabase().getTable( - _reference.getOtherTablePageNumber()); - - if(refTable == null) { - throw new IOException("Reference to missing table " + - _reference.getOtherTablePageNumber()); - } - - Index refIndex = null; - int idxNumber = _reference.getOtherIndexNumber(); - for(Index idx : refTable.getIndexes()) { - if(idx.getIndexNumber() == idxNumber) { - refIndex = idx; - break; - } - } - - if(refIndex == null) { - throw new IOException("Reference to missing index " + idxNumber + - " on table " + refTable.getName()); - } - - // finally verify that we found the expected index (should reference this - // index) - ForeignKeyReference otherRef = refIndex.getReference(); - if((otherRef == null) || - (otherRef.getOtherTablePageNumber() != - getTable().getTableDefPageNumber()) || - (otherRef.getOtherIndexNumber() != _indexNumber)) { - throw new IOException("Found unexpected index " + refIndex.getName() + - " on table " + refTable.getName() + - " with reference " + otherRef); - } - - return refIndex; - } + public Index getReferencedIndex() throws IOException; /** * Whether or not {@code null} values are actually recorded in the index. */ - public boolean shouldIgnoreNulls() { - return getIndexData().shouldIgnoreNulls(); - } + public boolean shouldIgnoreNulls(); /** * Whether or not index entries must be unique. @@ -229,250 +65,19 @@ public class Index implements Comparable { * case will violate the unique constraint * */ - public boolean isUnique() { - return getIndexData().isUnique(); - } - - /** - * Returns the Columns for this index (unmodifiable) - */ - public List getColumns() { - return getIndexData().getColumns(); - } + public boolean isUnique(); /** - * Whether or not the complete index state has been read. + * Information about a Column in an Index */ - public boolean isInitialized() { - return getIndexData().isInitialized(); - } - - /** - * Forces initialization of this index (actual parsing of index pages). - * normally, the index will not be initialized until the entries are - * actually needed. - */ - public void initialize() throws IOException { - getIndexData().initialize(); - } + public interface Column { - /** - * Writes the current index state to the database. - *

- * Forces index initialization. - */ - public void update() throws IOException { - getIndexData().update(); - } + public com.healthmarketscience.jackcess.Column getColumn(); - /** - * Adds a row to this index - *

- * Forces index initialization. - * - * @param row Row to add - * @param rowId rowId of the row to be added - */ - public void addRow(Object[] row, RowId rowId) - throws IOException - { - getIndexData().addRow(row, rowId); - } - - /** - * Removes a row from this index - *

- * Forces index initialization. - * - * @param row Row to remove - * @param rowId rowId of the row to be removed - */ - public void deleteRow(Object[] row, RowId rowId) - throws IOException - { - getIndexData().deleteRow(row, rowId); - } - - /** - * Gets a new cursor for this index. - *

- * Forces index initialization. - */ - public IndexData.EntryCursor cursor() - throws IOException - { - return cursor(null, true, null, true); - } - - /** - * Gets a new cursor for this index, narrowed to the range defined by the - * given startRow and endRow. - *

- * Forces index initialization. - * - * @param startRow the first row of data for the cursor, or {@code null} for - * the first entry - * @param startInclusive whether or not startRow is inclusive or exclusive - * @param endRow the last row of data for the cursor, or {@code null} for - * the last entry - * @param endInclusive whether or not endRow is inclusive or exclusive - */ - public IndexData.EntryCursor cursor(Object[] startRow, - boolean startInclusive, - Object[] endRow, - boolean endInclusive) - throws IOException - { - return getIndexData().cursor(startRow, startInclusive, endRow, - endInclusive); - } + public boolean isAscending(); - /** - * Constructs an array of values appropriate for this index from the given - * column values, expected to match the columns for this index. - * @return the appropriate sparse array of data - * @throws IllegalArgumentException if the wrong number of values are - * provided - */ - public Object[] constructIndexRowFromEntry(Object... values) - { - return getIndexData().constructIndexRowFromEntry(values); - } + public int getColumnIndex(); - /** - * Constructs an array of values appropriate for this index from the given - * column value. - * @return the appropriate sparse array of data or {@code null} if not all - * columns for this index were provided - */ - public Object[] constructIndexRow(String colName, Object value) - { - return constructIndexRow(Collections.singletonMap(colName, value)); - } - - /** - * Constructs an array of values appropriate for this index from the given - * column values. - * @return the appropriate sparse array of data or {@code null} if not all - * columns for this index were provided - */ - public Object[] constructIndexRow(Map row) - { - return getIndexData().constructIndexRow(row); - } - - @Override - public String toString() { - StringBuilder rtn = new StringBuilder(); - rtn.append("\tName: (").append(getTable().getName()).append(") ") - .append(_name); - rtn.append("\n\tNumber: ").append(_indexNumber); - rtn.append("\n\tIs Primary Key: ").append(isPrimaryKey()); - rtn.append("\n\tIs Foreign Key: ").append(isForeignKey()); - if(_reference != null) { - rtn.append("\n\tForeignKeyReference: ").append(_reference); - } - rtn.append(_data.toString()); - rtn.append("\n\n"); - return rtn.toString(); - } - - public int compareTo(Index other) { - if (_indexNumber > other.getIndexNumber()) { - return 1; - } else if (_indexNumber < other.getIndexNumber()) { - return -1; - } else { - return 0; - } - } - - /** - * Writes the logical index definitions into a table definition buffer. - * @param buffer Buffer to write to - * @param indexes List of IndexBuilders to write definitions for - */ - protected static void writeDefinitions( - TableCreator creator, ByteBuffer buffer) - throws IOException - { - // write logical index information - for(IndexBuilder idx : creator.getIndexes()) { - TableCreator.IndexState idxState = creator.getIndexState(idx); - buffer.putInt(Table.MAGIC_TABLE_NUMBER); // seemingly constant magic value which matches the table def - buffer.putInt(idxState.getIndexNumber()); // index num - buffer.putInt(idxState.getIndexDataNumber()); // index data num - buffer.put((byte)0); // related table type - buffer.putInt(INVALID_INDEX_NUMBER); // related index num - buffer.putInt(0); // related table definition page number - buffer.put((byte)0); // cascade updates flag - buffer.put((byte)0); // cascade deletes flag - buffer.put(idx.getType()); // index type flags - buffer.putInt(0); // unknown - } - - // write index names - for(IndexBuilder idx : creator.getIndexes()) { - Table.writeName(buffer, idx.getName(), creator.getCharset()); - } - } - - /** - * Information about a foreign key reference defined in an index (when - * referential integrity should be enforced). - */ - public static class ForeignKeyReference - { - private final byte _tableType; - private final int _otherIndexNumber; - private final int _otherTablePageNumber; - private final boolean _cascadeUpdates; - private final boolean _cascadeDeletes; - - public ForeignKeyReference( - byte tableType, int otherIndexNumber, int otherTablePageNumber, - boolean cascadeUpdates, boolean cascadeDeletes) - { - _tableType = tableType; - _otherIndexNumber = otherIndexNumber; - _otherTablePageNumber = otherTablePageNumber; - _cascadeUpdates = cascadeUpdates; - _cascadeDeletes = cascadeDeletes; - } - - public byte getTableType() { - return _tableType; - } - - public boolean isPrimaryTable() { - return(getTableType() == PRIMARY_TABLE_TYPE); - } - - public int getOtherIndexNumber() { - return _otherIndexNumber; - } - - public int getOtherTablePageNumber() { - return _otherTablePageNumber; - } - - public boolean isCascadeUpdates() { - return _cascadeUpdates; - } - - public boolean isCascadeDeletes() { - return _cascadeDeletes; - } - - @Override - public String toString() { - return new StringBuilder() - .append("\n\t\tOther Index Number: ").append(_otherIndexNumber) - .append("\n\t\tOther Table Page Num: ").append(_otherTablePageNumber) - .append("\n\t\tIs Primary Table: ").append(isPrimaryTable()) - .append("\n\t\tIs Cascade Updates: ").append(isCascadeUpdates()) - .append("\n\t\tIs Cascade Deletes: ").append(isCascadeDeletes()) - .toString(); - } + public String getName(); } } diff --git a/src/java/com/healthmarketscience/jackcess/IndexBuilder.java b/src/java/com/healthmarketscience/jackcess/IndexBuilder.java index 07ddd77..5e05e5a 100644 --- a/src/java/com/healthmarketscience/jackcess/IndexBuilder.java +++ b/src/java/com/healthmarketscience/jackcess/IndexBuilder.java @@ -24,6 +24,9 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import com.healthmarketscience.jackcess.impl.IndexData; +import com.healthmarketscience.jackcess.impl.IndexImpl; + /** * Builder style class for constructing an Index. * @@ -61,7 +64,7 @@ public class IndexBuilder } public boolean isPrimaryKey() { - return (getType() == Index.PRIMARY_KEY_INDEX_TYPE); + return (getType() == IndexImpl.PRIMARY_KEY_INDEX_TYPE); } public boolean isUnique() { @@ -108,7 +111,7 @@ public class IndexBuilder * unique). */ public IndexBuilder setPrimaryKey() { - _type = Index.PRIMARY_KEY_INDEX_TYPE; + _type = IndexImpl.PRIMARY_KEY_INDEX_TYPE; return setUnique(); } diff --git a/src/java/com/healthmarketscience/jackcess/IndexCursor.java b/src/java/com/healthmarketscience/jackcess/IndexCursor.java index aa77b65..7871b65 100644 --- a/src/java/com/healthmarketscience/jackcess/IndexCursor.java +++ b/src/java/com/healthmarketscience/jackcess/IndexCursor.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2011 James Ahlborn +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -20,152 +20,18 @@ USA package com.healthmarketscience.jackcess; import java.io.IOException; -import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; -import com.healthmarketscience.jackcess.Table.RowState; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import com.healthmarketscience.jackcess.util.EntryIterableBuilder; /** * Cursor backed by an index with extended traversal options. * * @author James Ahlborn */ -public class IndexCursor extends Cursor +public interface IndexCursor extends Cursor { - private static final Log LOG = LogFactory.getLog(IndexCursor.class); - /** IndexDirHandler for forward traversal */ - private final IndexDirHandler _forwardDirHandler = - new ForwardIndexDirHandler(); - /** IndexDirHandler for backward traversal */ - private final IndexDirHandler _reverseDirHandler = - new ReverseIndexDirHandler(); - /** logical index which this cursor is using */ - private final Index _index; - /** Cursor over the entries of the relevant index */ - private final IndexData.EntryCursor _entryCursor; - /** column names for the index entry columns */ - private Set _indexEntryPattern; - - private IndexCursor(Table table, Index index, - IndexData.EntryCursor entryCursor) - throws IOException - { - super(new Id(table, index), table, - new IndexPosition(entryCursor.getFirstEntry()), - new IndexPosition(entryCursor.getLastEntry())); - _index = index; - _index.initialize(); - _entryCursor = entryCursor; - } - - /** - * Creates an indexed cursor for the given table. - *

- * Note, index based table traversal may not include all rows, as certain - * types of indexes do not include all entries (namely, some indexes ignore - * null entries, see {@link Index#shouldIgnoreNulls}). - * - * @param table the table over which this cursor will traverse - * @param index index for the table which will define traversal order as - * well as enhance certain lookups - */ - public static IndexCursor createCursor(Table table, Index index) - throws IOException - { - return createCursor(table, index, null, null); - } - - /** - * Creates an indexed cursor for the given table, narrowed to the given - * range. - *

- * Note, index based table traversal may not include all rows, as certain - * types of indexes do not include all entries (namely, some indexes ignore - * null entries, see {@link Index#shouldIgnoreNulls}). - * - * @param table the table over which this cursor will traverse - * @param index index for the table which will define traversal order as - * well as enhance certain lookups - * @param startRow the first row of data for the cursor (inclusive), or - * {@code null} for the first entry - * @param endRow the last row of data for the cursor (inclusive), or - * {@code null} for the last entry - */ - public static IndexCursor createCursor( - Table table, Index index, Object[] startRow, Object[] endRow) - throws IOException - { - return createCursor(table, index, startRow, true, endRow, true); - } - - /** - * Creates an indexed cursor for the given table, narrowed to the given - * range. - *

- * Note, index based table traversal may not include all rows, as certain - * types of indexes do not include all entries (namely, some indexes ignore - * null entries, see {@link Index#shouldIgnoreNulls}). - * - * @param table the table over which this cursor will traverse - * @param index index for the table which will define traversal order as - * well as enhance certain lookups - * @param startRow the first row of data for the cursor, or {@code null} for - * the first entry - * @param startInclusive whether or not startRow is inclusive or exclusive - * @param endRow the last row of data for the cursor, or {@code null} for - * the last entry - * @param endInclusive whether or not endRow is inclusive or exclusive - */ - public static IndexCursor createCursor(Table table, Index index, - Object[] startRow, - boolean startInclusive, - Object[] endRow, - boolean endInclusive) - throws IOException - { - if(table != index.getTable()) { - throw new IllegalArgumentException( - "Given index is not for given table: " + index + ", " + table); - } - if(!table.getFormat().INDEXES_SUPPORTED) { - throw new IllegalArgumentException( - "JetFormat " + table.getFormat() + - " does not currently support index lookups"); - } - if(index.getIndexData().isReadOnly()) { - throw new IllegalArgumentException( - "Given index " + index + - " is not usable for indexed lookups because it is read-only"); - } - IndexCursor cursor = new IndexCursor(table, index, - index.cursor(startRow, startInclusive, - endRow, endInclusive)); - // init the column matcher appropriately for the index type - cursor.setColumnMatcher(null); - return cursor; - } - - public Index getIndex() { - return _index; - } - - /** - * @deprecated renamed to {@link #findFirstRowByEntry(Object...)} to be more - * clear - */ - @Deprecated - public boolean findRowByEntry(Object... entryValues) - throws IOException - { - return findFirstRowByEntry(entryValues); - } + public Index getIndex(); /** * Moves to the first row (as defined by the cursor) where the index entries @@ -180,24 +46,7 @@ public class IndexCursor extends Cursor * {@code false} if no row was found */ public boolean findFirstRowByEntry(Object... entryValues) - throws IOException - { - Position curPos = _curPos; - Position prevPos = _prevPos; - boolean found = false; - try { - found = findFirstRowByEntryImpl(toRowValues(entryValues), true); - return found; - } finally { - if(!found) { - try { - restorePosition(curPos, prevPos); - } catch(IOException e) { - LOG.error("Failed restoring position", e); - } - } - } - } + throws IOException; /** * Moves to the first row (as defined by the cursor) where the index entries @@ -207,24 +56,7 @@ public class IndexCursor extends Cursor * @param entryValues the column values for the index's columns. */ public void findClosestRowByEntry(Object... entryValues) - throws IOException - { - Position curPos = _curPos; - Position prevPos = _prevPos; - boolean found = false; - try { - findFirstRowByEntryImpl(toRowValues(entryValues), false); - found = true; - } finally { - if(!found) { - try { - restorePosition(curPos, prevPos); - } catch(IOException e) { - LOG.error("Failed restoring position", e); - } - } - } - } + throws IOException; /** * Returns {@code true} if the current row matches the given index entries. @@ -232,374 +64,14 @@ public class IndexCursor extends Cursor * @param entryValues the column values for the index's columns. */ public boolean currentRowMatchesEntry(Object... entryValues) - throws IOException - { - return currentRowMatchesEntryImpl(toRowValues(entryValues)); - } - - /** - * Returns a modifiable Iterator which will iterate through all the rows of - * this table which match the given index entries. - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within - */ - public Iterator> entryIterator(Object... entryValues) - { - return entryIterator((Collection)null, entryValues); - } - - /** - * Returns a modifiable Iterator which will iterate through all the rows of - * this table which match the given index entries, returning only the given - * columns. - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within - */ - public Iterator> entryIterator( - Collection columnNames, Object... entryValues) - { - return new EntryIterator(columnNames, toRowValues(entryValues)); - } - - /** - * Returns an Iterable whose iterator() method returns the result of a call - * to {@link #entryIterator(Object...)} - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within - */ - public Iterable> entryIterable(Object... entryValues) - { - return entryIterable((Collection)null, entryValues); - } - - /** - * Returns an Iterable whose iterator() method returns the result of a call - * to {@link #entryIterator(Collection,Object...)} - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within - */ - public Iterable> entryIterable( - final Collection columnNames, final Object... entryValues) - { - return new Iterable>() { - public Iterator> iterator() { - return new EntryIterator(columnNames, toRowValues(entryValues)); - } - }; - } - - @Override - protected IndexDirHandler getDirHandler(boolean moveForward) { - return (moveForward ? _forwardDirHandler : _reverseDirHandler); - } - - @Override - protected boolean isUpToDate() { - return(super.isUpToDate() && _entryCursor.isUpToDate()); - } - - @Override - protected void reset(boolean moveForward) { - _entryCursor.reset(moveForward); - super.reset(moveForward); - } - - @Override - protected void restorePositionImpl(Position curPos, Position prevPos) - throws IOException - { - if(!(curPos instanceof IndexPosition) || - !(prevPos instanceof IndexPosition)) { - throw new IllegalArgumentException( - "Restored positions must be index positions"); - } - _entryCursor.restorePosition(((IndexPosition)curPos).getEntry(), - ((IndexPosition)prevPos).getEntry()); - super.restorePositionImpl(curPos, prevPos); - } - - @Override - protected boolean findNextRowImpl(Column columnPattern, Object valuePattern) - throws IOException - { - if(!isBeforeFirst()) { - // use the default table scan for finding rows mid-cursor - return super.findNextRowImpl(columnPattern, valuePattern); - } - - // searching for the first match - Object[] rowValues = _entryCursor.getIndexData().constructIndexRow( - columnPattern.getName(), valuePattern); - - if(rowValues == null) { - // bummer, use the default table scan - return super.findNextRowImpl(columnPattern, valuePattern); - } - - // sweet, we can use our index - if(!findPotentialRow(rowValues, true)) { - return false; - } - - // either we found a row with the given value, or none exist in the - // table - return currentRowMatches(columnPattern, valuePattern); - } - - /** - * Moves to the first row (as defined by the cursor) where the index entries - * match the given values. Caller manages save/restore on failure. - * - * @param rowValues the column values built from the index column values - * @param requireMatch whether or not an exact match is found - * @return {@code true} if a valid row was found with the given values, - * {@code false} if no row was found - */ - protected boolean findFirstRowByEntryImpl(Object[] rowValues, - boolean requireMatch) - throws IOException - { - if(!findPotentialRow(rowValues, requireMatch)) { - return false; - } else if(!requireMatch) { - // nothing more to do, we have moved to the closest row - return true; - } - - return currentRowMatchesEntryImpl(rowValues); - } - - @Override - protected boolean findNextRowImpl(Map rowPattern) - throws IOException - { - if(!isBeforeFirst()) { - // use the default table scan for finding rows mid-cursor - return super.findNextRowImpl(rowPattern); - } - - // searching for the first match - IndexData indexData = _entryCursor.getIndexData(); - Object[] rowValues = indexData.constructIndexRow(rowPattern); - - if(rowValues == null) { - // bummer, use the default table scan - return super.findNextRowImpl(rowPattern); - } - - // sweet, we can use our index - if(!findPotentialRow(rowValues, true)) { - // at end of index, no potential matches - return false; - } - - // find actual matching row - Map indexRowPattern = null; - if(rowPattern.size() == indexData.getColumns().size()) { - // the rowPattern matches our index columns exactly, so we can - // streamline our testing below - indexRowPattern = rowPattern; - } else { - // the rowPattern has more columns than just the index, so we need to - // do more work when testing below - Map tmpRowPattern = new LinkedHashMap(); - indexRowPattern = tmpRowPattern; - for(IndexData.ColumnDescriptor idxCol : indexData.getColumns()) { - tmpRowPattern.put(idxCol.getName(), rowValues[idxCol.getColumnIndex()]); - } - } - - // there may be multiple columns which fit the pattern subset used by - // the index, so we need to keep checking until our index values no - // longer match - do { - - if(!currentRowMatches(indexRowPattern)) { - // there are no more rows which could possibly match - break; - } - - // note, if rowPattern == indexRowPattern, no need to do an extra - // comparison with the current row - if((rowPattern == indexRowPattern) || currentRowMatches(rowPattern)) { - // found it! - return true; - } - - } while(moveToNextRow()); - - // none of the potential rows matched - return false; - } - - private boolean currentRowMatchesEntryImpl(Object[] rowValues) - throws IOException - { - if(_indexEntryPattern == null) { - // init our set of index column names - _indexEntryPattern = new HashSet(); - for(IndexData.ColumnDescriptor col : getIndex().getColumns()) { - _indexEntryPattern.add(col.getName()); - } - } - - // check the next row to see if it actually matches - Map row = getCurrentRow(_indexEntryPattern); - - for(IndexData.ColumnDescriptor col : getIndex().getColumns()) { - String columnName = col.getName(); - Object patValue = rowValues[col.getColumnIndex()]; - Object rowValue = row.get(columnName); - if(!_columnMatcher.matches(getTable(), columnName, - patValue, rowValue)) { - return false; - } - } - - return true; - } - - private boolean findPotentialRow(Object[] rowValues, boolean requireMatch) - throws IOException - { - _entryCursor.beforeEntry(rowValues); - IndexData.Entry startEntry = _entryCursor.getNextEntry(); - if(requireMatch && !startEntry.getRowId().isValid()) { - // at end of index, no potential matches - return false; - } - // move to position and check it out - restorePosition(new IndexPosition(startEntry)); - return true; - } - - private Object[] toRowValues(Object[] entryValues) - { - return _entryCursor.getIndexData().constructIndexRowFromEntry(entryValues); - } - - @Override - protected Position findAnotherPosition(RowState rowState, Position curPos, - boolean moveForward) - throws IOException - { - IndexDirHandler handler = getDirHandler(moveForward); - IndexPosition endPos = (IndexPosition)handler.getEndPosition(); - IndexData.Entry entry = handler.getAnotherEntry(); - return ((!entry.equals(endPos.getEntry())) ? - new IndexPosition(entry) : endPos); - } - - @Override - protected ColumnMatcher getDefaultColumnMatcher() { - if(getIndex().isUnique()) { - // text indexes are case-insensitive, therefore we should always use a - // case-insensitive matcher for unique indexes. - return CaseInsensitiveColumnMatcher.INSTANCE; - } - return SimpleColumnMatcher.INSTANCE; - } + throws IOException; /** - * Handles moving the table index cursor in a given direction. Separates - * cursor logic from value storage. - */ - private abstract class IndexDirHandler extends DirHandler { - public abstract IndexData.Entry getAnotherEntry() - throws IOException; - } - - /** - * Handles moving the table index cursor forward. - */ - private final class ForwardIndexDirHandler extends IndexDirHandler { - @Override - public Position getBeginningPosition() { - return getFirstPosition(); - } - @Override - public Position getEndPosition() { - return getLastPosition(); - } - @Override - public IndexData.Entry getAnotherEntry() throws IOException { - return _entryCursor.getNextEntry(); - } - } - - /** - * Handles moving the table index cursor backward. - */ - private final class ReverseIndexDirHandler extends IndexDirHandler { - @Override - public Position getBeginningPosition() { - return getLastPosition(); - } - @Override - public Position getEndPosition() { - return getFirstPosition(); - } - @Override - public IndexData.Entry getAnotherEntry() throws IOException { - return _entryCursor.getPreviousEntry(); - } - } - - /** - * Value object which maintains the current position of an IndexCursor. - */ - private static final class IndexPosition extends Position - { - private final IndexData.Entry _entry; - - private IndexPosition(IndexData.Entry entry) { - _entry = entry; - } - - @Override - public RowId getRowId() { - return getEntry().getRowId(); - } - - public IndexData.Entry getEntry() { - return _entry; - } - - @Override - protected boolean equalsImpl(Object o) { - return getEntry().equals(((IndexPosition)o).getEntry()); - } - - @Override - public String toString() { - return "Entry = " + getEntry(); - } - } - - /** - * Row iterator (by matching entry) for this cursor, modifiable. + * Convenience method for constructing a new EntryIterableBuilder for this + * cursor. An EntryIterableBuilder provides a variety of options for more + * flexible iteration based on a specific index entry. + * + * @param entryValues the column values for the index's columns. */ - private final class EntryIterator extends BaseIterator - { - private final Object[] _rowValues; - - private EntryIterator(Collection columnNames, Object[] rowValues) - { - super(columnNames); - _rowValues = rowValues; - try { - _hasNext = findFirstRowByEntryImpl(rowValues, true); - _validRow = _hasNext; - } catch(IOException e) { - throw new IllegalStateException(e); - } - } - - @Override - protected boolean findNext() throws IOException { - return (moveToNextRow() && currentRowMatchesEntryImpl(_rowValues)); - } - } - - + public EntryIterableBuilder newEntryIterable(Object... entryValues); } diff --git a/src/java/com/healthmarketscience/jackcess/PropertyMap.java b/src/java/com/healthmarketscience/jackcess/PropertyMap.java index dc25dc0..5faa3d8 100644 --- a/src/java/com/healthmarketscience/jackcess/PropertyMap.java +++ b/src/java/com/healthmarketscience/jackcess/PropertyMap.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2011 James Ahlborn +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -19,16 +19,12 @@ USA package com.healthmarketscience.jackcess; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.Map; - /** - * Map of properties for a given database object. + * Map of properties for a database object. * * @author James Ahlborn */ -public class PropertyMap implements Iterable +public interface PropertyMap extends Iterable { public static final String ACCESS_VERSION_PROP = "AccessVersion"; public static final String TITLE_PROP = "Title"; @@ -47,124 +43,40 @@ public class PropertyMap implements Iterable public static final String GUID_PROP = "GUID"; public static final String DESCRIPTION_PROP = "Description"; - private final String _mapName; - private final short _mapType; - private final Map _props = - new LinkedHashMap(); - - PropertyMap(String name, short type) { - _mapName = name; - _mapType = type; - } - public String getName() { - return _mapName; - } + public String getName(); - public short getType() { - return _mapType; - } + public int getSize(); - public int getSize() { - return _props.size(); - } - - public boolean isEmpty() { - return _props.isEmpty(); - } + public boolean isEmpty(); /** * @return the property with the given name, if any */ - public Property get(String name) { - return _props.get(Database.toLookupName(name)); - } + public Property get(String name); /** * @return the value of the property with the given name, if any */ - public Object getValue(String name) { - return getValue(name, null); - } + public Object getValue(String name); /** * @return the value of the property with the given name, if any, otherwise * the given defaultValue */ - public Object getValue(String name, Object defaultValue) { - Property prop = get(name); - Object value = defaultValue; - if((prop != null) && (prop.getValue() != null)) { - value = prop.getValue(); - } - return value; - } - - /** - * Puts a property into this map with the given information. - */ - public void put(String name, DataType type, byte flag, Object value) { - _props.put(Database.toLookupName(name), - new Property(name, type, flag, value)); - } - - public Iterator iterator() { - return _props.values().iterator(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(PropertyMaps.DEFAULT_NAME.equals(getName()) ? - "" : getName()) - .append(" {"); - for(Iterator iter = iterator(); iter.hasNext(); ) { - sb.append(iter.next()); - if(iter.hasNext()) { - sb.append(","); - } - } - sb.append("}"); - return sb.toString(); - } + public Object getValue(String name, Object defaultValue); /** * Info about a property defined in a PropertyMap. */ - public static final class Property + public interface Property { - private final String _name; - private final DataType _type; - private final byte _flag; - private final Object _value; - - private Property(String name, DataType type, byte flag, Object value) { - _name = name; - _type = type; - _flag = flag; - _value = value; - } - - public String getName() { - return _name; - } - - public DataType getType() { - return _type; - } - - public Object getValue() { - return _value; - } - - @Override - public String toString() { - Object val = getValue(); - if(val instanceof byte[]) { - val = ByteUtil.toHexString((byte[])val); - } - return getName() + "[" + getType() + ":" + _flag + "]=" + val; - } - } + public String getName(); + + public DataType getType(); + + public Object getValue(); + + } } diff --git a/src/java/com/healthmarketscience/jackcess/Relationship.java b/src/java/com/healthmarketscience/jackcess/Relationship.java index 43dd242..2adb7cb 100644 --- a/src/java/com/healthmarketscience/jackcess/Relationship.java +++ b/src/java/com/healthmarketscience/jackcess/Relationship.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2008 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,133 +15,38 @@ 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.util.Collections; import java.util.List; -import java.util.ArrayList; /** * Information about a relationship between two tables in the database. * * @author James Ahlborn */ -public class Relationship { - - /** flag indicating one-to-one relationship */ - private static final int ONE_TO_ONE_FLAG = 0x00000001; - /** flag indicating no referential integrity */ - private static final int NO_REFERENTIAL_INTEGRITY_FLAG = 0x00000002; - /** flag indicating cascading updates (requires referential integrity) */ - private static final int CASCADE_UPDATES_FLAG = 0x00000100; - /** flag indicating cascading deletes (requires referential integrity) */ - private static final int CASCADE_DELETES_FLAG = 0x00001000; - /** flag indicating left outer join */ - private static final int LEFT_OUTER_JOIN_FLAG = 0x01000000; - /** flag indicating right outer join */ - private static final int RIGHT_OUTER_JOIN_FLAG = 0x02000000; - - /** the name of this relationship */ - private final String _name; - /** the "from" table in this relationship */ - private final Table _fromTable; - /** the "to" table in this relationship */ - private final Table _toTable; - /** the columns in the "from" table in this relationship (aligned w/ - toColumns list) */ - private List _toColumns; - /** the columns in the "to" table in this relationship (aligned w/ - toColumns list) */ - private List _fromColumns; - /** the various flags describing this relationship */ - private final int _flags; - - public Relationship(String name, Table fromTable, Table toTable, int flags, - int numCols) - { - _name = name; - _fromTable = fromTable; - _fromColumns = new ArrayList( - Collections.nCopies(numCols, (Column)null)); - _toTable = toTable; - _toColumns = new ArrayList( - Collections.nCopies(numCols, (Column)null)); - _flags = flags; - } - - public String getName() { - return _name; - } +public interface Relationship +{ + public String getName(); - public Table getFromTable() { - return _fromTable; - } - - public List getFromColumns() { - return _fromColumns; - } + public Table getFromTable(); - public Table getToTable() { - return _toTable; - } + public List getFromColumns(); - public List getToColumns() { - return _toColumns; - } + public Table getToTable(); - public int getFlags() { - return _flags; - } + public List getToColumns(); - public boolean isOneToOne() { - return hasFlag(ONE_TO_ONE_FLAG); - } + public boolean isOneToOne(); - public boolean hasReferentialIntegrity() { - return !hasFlag(NO_REFERENTIAL_INTEGRITY_FLAG); - } + public boolean hasReferentialIntegrity(); - public boolean cascadeUpdates() { - return hasFlag(CASCADE_UPDATES_FLAG); - } + public boolean cascadeUpdates(); - public boolean cascadeDeletes() { - return hasFlag(CASCADE_DELETES_FLAG); - } + public boolean cascadeDeletes(); - public boolean isLeftOuterJoin() { - return hasFlag(LEFT_OUTER_JOIN_FLAG); - } + public boolean isLeftOuterJoin(); - public boolean isRightOuterJoin() { - return hasFlag(RIGHT_OUTER_JOIN_FLAG); - } - - private boolean hasFlag(int flagMask) { - return((getFlags() & flagMask) != 0); - } - - @Override - public String toString() { - StringBuilder rtn = new StringBuilder(); - rtn.append("\tName: " + _name); - rtn.append("\n\tFromTable: " + _fromTable.getName()); - rtn.append("\n\tFromColumns: " + _fromColumns); - rtn.append("\n\tToTable: " + _toTable.getName()); - rtn.append("\n\tToColumns: " + _toColumns); - rtn.append("\n\tFlags: " + Integer.toHexString(_flags)); - rtn.append("\n\n"); - return rtn.toString(); - } - + public boolean isRightOuterJoin(); } diff --git a/src/java/com/healthmarketscience/jackcess/LinkResolver.java b/src/java/com/healthmarketscience/jackcess/Row.java similarity index 70% rename from src/java/com/healthmarketscience/jackcess/LinkResolver.java rename to src/java/com/healthmarketscience/jackcess/Row.java index 3ce7315..00fa09f 100644 --- a/src/java/com/healthmarketscience/jackcess/LinkResolver.java +++ b/src/java/com/healthmarketscience/jackcess/Row.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2011 James Ahlborn +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -19,19 +19,17 @@ USA package com.healthmarketscience.jackcess; -import java.io.IOException; +import java.util.Map; /** - * Resolver for linked databases. + * A row of data as column->value pairs. * * @author James Ahlborn */ -public interface LinkResolver +public interface Row extends Map { /** - * Returns the appropriate Database instance for the linkeeFileName from the - * given linkerDb. + * @return the id of this row */ - public Database resolveLinkedDatabase(Database linkerDb, String linkeeFileName) - throws IOException; + public RowId getId(); } diff --git a/src/java/com/healthmarketscience/jackcess/RowId.java b/src/java/com/healthmarketscience/jackcess/RowId.java index 1217538..c8b5ab8 100644 --- a/src/java/com/healthmarketscience/jackcess/RowId.java +++ b/src/java/com/healthmarketscience/jackcess/RowId.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2007 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,119 +15,19 @@ 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 org.apache.commons.lang.builder.CompareToBuilder; - - /** - * Uniquely identifies a row of data within the access database. + * Uniquely identifies a row of data within the access database. While RowIds + * are largely opaque identifiers, they are comparable to each other (within + * the same table) and have valid {@code equals()}, {@code hashCode()} and + * {@code toString()} methods. * * @author James Ahlborn */ -public class RowId implements Comparable +public interface RowId extends Comparable { - /** special page number which will sort before any other valid page - number */ - public static final int FIRST_PAGE_NUMBER = -1; - /** special page number which will sort after any other valid page - number */ - public static final int LAST_PAGE_NUMBER = -2; - - /** special row number representing an invalid row number */ - public static final int INVALID_ROW_NUMBER = -1; - - /** type attributes for RowIds which simplify comparisons */ - public enum Type { - /** comparable type indicating this RowId should always compare less than - normal RowIds */ - ALWAYS_FIRST, - /** comparable type indicating this RowId should always compare - normally */ - NORMAL, - /** comparable type indicating this RowId should always compare greater - than normal RowIds */ - ALWAYS_LAST; - } - - /** special rowId which will sort before any other valid rowId */ - public static final RowId FIRST_ROW_ID = new RowId( - FIRST_PAGE_NUMBER, INVALID_ROW_NUMBER); - - /** special rowId which will sort after any other valid rowId */ - public static final RowId LAST_ROW_ID = new RowId( - LAST_PAGE_NUMBER, INVALID_ROW_NUMBER); - - private final int _pageNumber; - private final int _rowNumber; - private final Type _type; - - /** - * Creates a new RowId instance. - * - */ - public RowId(int pageNumber,int rowNumber) { - _pageNumber = pageNumber; - _rowNumber = rowNumber; - _type = ((_pageNumber == FIRST_PAGE_NUMBER) ? Type.ALWAYS_FIRST : - ((_pageNumber == LAST_PAGE_NUMBER) ? Type.ALWAYS_LAST : - Type.NORMAL)); - } - - public int getPageNumber() { - return _pageNumber; - } - - public int getRowNumber() { - return _rowNumber; - } - - /** - * Returns {@code true} if this rowId potentially represents an actual row - * of data, {@code false} otherwise. - */ - public boolean isValid() { - return((getRowNumber() >= 0) && (getPageNumber() >= 0)); - } - - public Type getType() { - return _type; - } - - public int compareTo(RowId other) { - return new CompareToBuilder() - .append(getType(), other.getType()) - .append(getPageNumber(), other.getPageNumber()) - .append(getRowNumber(), other.getRowNumber()) - .toComparison(); - } - - @Override - public int hashCode() { - return getPageNumber() ^ getRowNumber(); - } - @Override - public boolean equals(Object o) { - return ((this == o) || - ((o != null) && (getClass() == o.getClass()) && - (getPageNumber() == ((RowId)o).getPageNumber()) && - (getRowNumber() == ((RowId)o).getRowNumber()))); - } - - @Override - public String toString() { - return getPageNumber() + ":" + getRowNumber(); - } - } diff --git a/src/java/com/healthmarketscience/jackcess/RuntimeIOException.java b/src/java/com/healthmarketscience/jackcess/RuntimeIOException.java new file mode 100644 index 0000000..3ffc9e6 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/RuntimeIOException.java @@ -0,0 +1,42 @@ +/* +Copyright (c) 2013 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; + +import java.io.IOException; + +/** + * RuntimeException wrapper around an IOException + * + * @author James Ahlborn + */ +public class RuntimeIOException extends IllegalStateException +{ + private static final long serialVersionUID = 20130315L; + + public RuntimeIOException(IOException e) + { + this(((e != null) ? e.getMessage() : null), e); + } + + public RuntimeIOException(String msg, IOException e) + { + super(msg, e); + } +} diff --git a/src/java/com/healthmarketscience/jackcess/SimpleIndexData.java b/src/java/com/healthmarketscience/jackcess/SimpleIndexData.java deleted file mode 100644 index 7a662e7..0000000 --- a/src/java/com/healthmarketscience/jackcess/SimpleIndexData.java +++ /dev/null @@ -1,241 +0,0 @@ -/* -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 java.util.Collections; -import java.util.List; - - -/** - * Simple implementation of an Access table index - * @author Tim McCune - */ -public class SimpleIndexData extends IndexData -{ - - static final DataPage NEW_ROOT_DATA_PAGE = - new SimpleDataPage(0, true, Collections.emptyList()); - - - /** data for the single index page. if this data came from multiple pages, - the index is read-only. */ - private SimpleDataPage _dataPage; - - public SimpleIndexData(Table table, int number, int uniqueEntryCount, - int uniqueEntryCountOffset) - { - super(table, number, uniqueEntryCount, uniqueEntryCountOffset); - } - - @Override - protected void updateImpl() throws IOException { - writeDataPage(_dataPage); - } - - @Override - protected void readIndexEntries() - throws IOException - { - // find first leaf page - int nextPageNumber = getRootPageNumber(); - SimpleDataPage indexPage = null; - while(true) { - indexPage = new SimpleDataPage(nextPageNumber); - readDataPage(indexPage); - - if(!indexPage.isLeaf()) { - // FIXME we can't modify this index at this point in time - setReadOnly(); - - // found another node page - if(!indexPage.getEntries().isEmpty()) { - nextPageNumber = indexPage.getEntries().get(0).getSubPageNumber(); - } else { - // try tail page - nextPageNumber = indexPage.getChildTailPageNumber(); - } - indexPage = null; - } else { - // found first leaf - break; - } - } - - // save the first leaf page - _dataPage = indexPage; - nextPageNumber = indexPage.getNextPageNumber(); - _dataPage.setNextPageNumber(INVALID_INDEX_PAGE_NUMBER); - indexPage = null; - - // read all leaf pages. - while(nextPageNumber != INVALID_INDEX_PAGE_NUMBER) { - - // FIXME we can't modify this index at this point in time - setReadOnly(); - - // found another one - indexPage = new SimpleDataPage(nextPageNumber); - readDataPage(indexPage); - - // since we read all the entries in sort order, we can insert them - // directly into the entries list - _dataPage.getEntries().addAll(indexPage.getEntries()); - int totalSize = (_dataPage.getTotalEntrySize() + - indexPage.getTotalEntrySize()); - _dataPage.setTotalEntrySize(totalSize); - nextPageNumber = indexPage.getNextPageNumber(); - } - - // check the entry order, just to be safe - List entries = _dataPage.getEntries(); - for(int i = 0; i < (entries.size() - 1); ++i) { - Entry e1 = entries.get(i); - Entry e2 = entries.get(i + 1); - if(e1.compareTo(e2) > 0) { - throw new IOException("Unexpected order in index entries, " + - e1 + " is greater than " + e2); - } - } - } - - @Override - protected DataPage findDataPage(Entry entry) - throws IOException - { - return _dataPage; - } - - @Override - protected DataPage getDataPage(int pageNumber) - throws IOException - { - throw new UnsupportedOperationException(); - } - - /** - * Simple implementation of a DataPage - */ - private static final class SimpleDataPage extends DataPage { - private final int _pageNumber; - private boolean _leaf; - private int _nextPageNumber; - private int _totalEntrySize; - private int _childTailPageNumber; - private List _entries; - - private SimpleDataPage(int pageNumber) { - this(pageNumber, false, null); - } - - private SimpleDataPage(int pageNumber, boolean leaf, List entries) - { - _pageNumber = pageNumber; - _leaf = leaf; - _entries = entries; - } - - @Override - public int getPageNumber() { - return _pageNumber; - } - - @Override - public boolean isLeaf() { - return _leaf; - } - @Override - public void setLeaf(boolean isLeaf) { - _leaf = isLeaf; - } - - @Override - public int getPrevPageNumber() { return 0; } - @Override - public void setPrevPageNumber(int pageNumber) { - // ignored - } - @Override - public int getNextPageNumber() { - return _nextPageNumber; - } - @Override - public void setNextPageNumber(int pageNumber) { - _nextPageNumber = pageNumber; - } - @Override - public int getChildTailPageNumber() { - return _childTailPageNumber; - } - @Override - public void setChildTailPageNumber(int pageNumber) { - _childTailPageNumber = pageNumber; - } - - @Override - public int getTotalEntrySize() { - return _totalEntrySize; - } - @Override - public void setTotalEntrySize(int totalSize) { - _totalEntrySize = totalSize; - } - @Override - public byte[] getEntryPrefix() { - return EMPTY_PREFIX; - } - @Override - public void setEntryPrefix(byte[] entryPrefix) { - // ignored - } - - @Override - public List getEntries() { - return _entries; - } - - @Override - public void setEntries(List entries) { - _entries = entries; - } - - @Override - public void addEntry(int idx, Entry entry) { - _entries.add(idx, entry); - _totalEntrySize += entry.size(); - } - - @Override - public void removeEntry(int idx) { - Entry oldEntry = _entries.remove(idx); - _totalEntrySize -= oldEntry.size(); - } - - } - -} diff --git a/src/java/com/healthmarketscience/jackcess/Table.java b/src/java/com/healthmarketscience/jackcess/Table.java index 9cb8933..a9fb4e7 100644 --- a/src/java/com/healthmarketscience/jackcess/Table.java +++ b/src/java/com/healthmarketscience/jackcess/Table.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2005 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,72 +15,31 @@ 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.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Set; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import com.healthmarketscience.jackcess.util.ErrorHandler; /** - * A single database table + * A single database table. A Table instance is retrieved from a Database + * instance. The Table instance provides access to the table metadata as well + * as the table data. There are basic data operations on the Table interface, + * but for advanced search and data manipulation a {@link Cursor} instance + * should be used. *

* Is not thread-safe. - * - * @author Tim McCune + * + * @author James Ahlborn * @usage _general_class_ */ -public class Table - implements Iterable> +public interface Table extends Iterable { - - private static final Log LOG = LogFactory.getLog(Table.class); - - private static final short OFFSET_MASK = (short)0x1FFF; - - private static final short DELETED_ROW_MASK = (short)0x8000; - - private static final short OVERFLOW_ROW_MASK = (short)0x4000; - - static final int MAGIC_TABLE_NUMBER = 1625; - - private static final int MAX_BYTE = 256; - - /** - * Table type code for system tables - * @usage _intermediate_class_ - */ - public static final byte TYPE_SYSTEM = 0x53; - /** - * Table type code for user tables - * @usage _intermediate_class_ - */ - public static final byte TYPE_USER = 0x4e; - /** * enum which controls the ordering of the columns in a table. * @usage _intermediate_class_ @@ -94,196 +53,27 @@ public class Table DISPLAY; } - /** comparator which sorts variable length columns based on their index into - the variable length offset table */ - private static final Comparator VAR_LEN_COLUMN_COMPARATOR = - new Comparator() { - public int compare(Column c1, Column c2) { - return ((c1.getVarLenTableIndex() < c2.getVarLenTableIndex()) ? -1 : - ((c1.getVarLenTableIndex() > c2.getVarLenTableIndex()) ? 1 : - 0)); - } - }; - - /** comparator which sorts columns based on their display index */ - private static final Comparator DISPLAY_ORDER_COMPARATOR = - new Comparator() { - public int compare(Column c1, Column c2) { - return ((c1.getDisplayIndex() < c2.getDisplayIndex()) ? -1 : - ((c1.getDisplayIndex() > c2.getDisplayIndex()) ? 1 : - 0)); - } - }; - - /** owning database */ - private final Database _database; - /** additional table flags from the catalog entry */ - private int _flags; - /** Type of the table (either TYPE_SYSTEM or TYPE_USER) */ - private byte _tableType; - /** Number of actual indexes on the table */ - private int _indexCount; - /** Number of logical indexes for the table */ - private int _logicalIndexCount; - /** Number of rows in the table */ - private int _rowCount; - /** last long auto number for the table */ - private int _lastLongAutoNumber; - /** last complex type auto number for the table */ - private int _lastComplexTypeAutoNumber; - /** page number of the definition of this table */ - private final int _tableDefPageNumber; - /** max Number of columns in the table (includes previous deletions) */ - private short _maxColumnCount; - /** max Number of variable columns in the table */ - private short _maxVarColumnCount; - /** List of columns in this table, ordered by column number */ - private List _columns = new ArrayList(); - /** List of variable length columns in this table, ordered by offset */ - private final List _varColumns = new ArrayList(); - /** List of autonumber columns in this table, ordered by column number */ - private List _autoNumColumns; - /** List of indexes on this table (multiple logical indexes may be backed by - the same index data) */ - private final List _indexes = new ArrayList(); - /** List of index datas on this table (the actual backing data for an - index) */ - private final List _indexDatas = new ArrayList(); - /** List of columns in this table which are in one or more indexes */ - private final Set _indexColumns = new LinkedHashSet(); - /** Table name as stored in Database */ - private final String _name; - /** Usage map of pages that this table owns */ - private UsageMap _ownedPages; - /** Usage map of pages that this table owns with free space on them */ - private UsageMap _freeSpacePages; - /** modification count for the table, keeps row-states up-to-date */ - private int _modCount; - /** page buffer used to update data pages when adding rows */ - private final TempPageHolder _addRowBufferH = - TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); - /** page buffer used to update the table def page */ - private final TempPageHolder _tableDefBufferH = - TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); - /** buffer used to writing single rows of data */ - private final TempBufferHolder _singleRowBufferH = - TempBufferHolder.newHolder(TempBufferHolder.Type.SOFT, true); - /** "buffer" used to writing multi rows of data (will create new buffer on - every call) */ - private final TempBufferHolder _multiRowBufferH = - TempBufferHolder.newHolder(TempBufferHolder.Type.NONE, true); - /** page buffer used to write out-of-row "long value" data */ - private final TempPageHolder _longValueBufferH = - TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); - /** "big index support" is optional */ - private final boolean _useBigIndex; - /** optional error handler to use when row errors are encountered */ - private ErrorHandler _tableErrorHandler; - /** properties for this table */ - private PropertyMap _props; - /** properties group for this table (and columns) */ - private PropertyMaps _propertyMaps; - /** foreign-key enforcer for this table */ - private final FKEnforcer _fkEnforcer; - - /** common cursor for iterating through the table, kept here for historic - reasons */ - private Cursor _cursor; - - /** - * Only used by unit tests - - */ - Table(boolean testing, List columns) throws IOException { - if(!testing) { - throw new IllegalArgumentException(); - } - _database = null; - _tableDefPageNumber = PageChannel.INVALID_PAGE_NUMBER; - _name = null; - _useBigIndex = true; - setColumns(columns); - _fkEnforcer = null; - } - - /** - * @param database database which owns this table - * @param tableBuffer Buffer to read the table with - * @param pageNumber Page number of the table definition - * @param name Table name - * @param useBigIndex whether or not "big index support" should be enabled - * for the table - */ - protected Table(Database database, ByteBuffer tableBuffer, - int pageNumber, String name, int flags, boolean useBigIndex) - throws IOException - { - _database = database; - _tableDefPageNumber = pageNumber; - _name = name; - _flags = flags; - _useBigIndex = useBigIndex; - readTableDefinition(loadCompleteTableDefinitionBuffer(tableBuffer)); - _fkEnforcer = new FKEnforcer(this); - } - /** * @return The name of the table * @usage _general_method_ */ - public String getName() { - return _name; - } + public String getName(); /** * Whether or not this table has been marked as hidden. * @usage _general_method_ */ - public boolean isHidden() { - return((_flags & Database.HIDDEN_OBJECT_FLAG) != 0); - } - - /** - * @usage _advanced_method_ - */ - public boolean doUseBigIndex() { - return _useBigIndex; - } + public boolean isHidden(); - /** - * @usage _advanced_method_ - */ - public int getMaxColumnCount() { - return _maxColumnCount; - } - - /** - * @usage _general_method_ - */ - public int getColumnCount() { - return _columns.size(); - } - /** * @usage _general_method_ */ - public Database getDatabase() { - return _database; - } - - /** - * @usage _advanced_method_ - */ - public JetFormat getFormat() { - return getDatabase().getFormat(); - } + public int getColumnCount(); /** - * @usage _advanced_method_ + * @usage _general_method_ */ - public PageChannel getPageChannel() { - return getDatabase().getPageChannel(); - } + public Database getDatabase(); /** * Gets the currently configured ErrorHandler (always non-{@code null}). @@ -291,161 +81,45 @@ public class Table * level. * @usage _intermediate_method_ */ - public ErrorHandler getErrorHandler() { - return((_tableErrorHandler != null) ? _tableErrorHandler : - getDatabase().getErrorHandler()); - } + public ErrorHandler getErrorHandler(); /** * Sets a new ErrorHandler. If {@code null}, resets to using the * ErrorHandler configured at the Database level. * @usage _intermediate_method_ */ - public void setErrorHandler(ErrorHandler newErrorHandler) { - _tableErrorHandler = newErrorHandler; - } - - public int getTableDefPageNumber() { - return _tableDefPageNumber; - } - - /** - * @usage _advanced_method_ - */ - public RowState createRowState() { - return new RowState(TempBufferHolder.Type.HARD); - } - - protected UsageMap.PageCursor getOwnedPagesCursor() { - return _ownedPages.cursor(); - } - - /** - * Returns the approximate number of database pages owned by this - * table and all related indexes (this number does not take into - * account pages used for large OLE/MEMO fields). - *

- * To calculate the approximate number of bytes owned by a table: - * - * int approxTableBytes = (table.getApproximateOwnedPageCount() * - * table.getFormat().PAGE_SIZE); - * - * @usage _intermediate_method_ - */ - public int getApproximateOwnedPageCount() { - - // add a page for the table def (although that might actually be more than - // one page) - int count = _ownedPages.getPageCount() + 1; - - for(Column col : _columns) { - count += col.getOwnedPageCount(); - } - - // note, we count owned pages from _physical_ indexes, not logical indexes - // (otherwise we could double count pages) - for(IndexData indexData : _indexDatas) { - count += indexData.getOwnedPageCount(); - } - - return count; - } - - protected TempPageHolder getLongValueBuffer() { - return _longValueBufferH; - } + public void setErrorHandler(ErrorHandler newErrorHandler); /** * @return All of the columns in this table (unmodifiable List) * @usage _general_method_ */ - public List getColumns() { - return Collections.unmodifiableList(_columns); - } + public List getColumns(); /** * @return the column with the given name * @usage _general_method_ */ - public Column getColumn(String name) { - for(Column column : _columns) { - if(column.getName().equalsIgnoreCase(name)) { - return column; - } - } - throw new IllegalArgumentException("Column with name " + name + - " does not exist in this table"); - } - - /** - * Only called by unit tests - */ - private void setColumns(List columns) { - _columns = columns; - int colIdx = 0; - int varLenIdx = 0; - int fixedOffset = 0; - for(Column col : _columns) { - col.setColumnNumber((short)colIdx); - col.setColumnIndex(colIdx++); - if(col.isVariableLength()) { - col.setVarLenTableIndex(varLenIdx++); - _varColumns.add(col); - } else { - col.setFixedDataOffset(fixedOffset); - fixedOffset += col.getType().getFixedSize(); - } - } - _maxColumnCount = (short)_columns.size(); - _maxVarColumnCount = (short)_varColumns.size(); - _autoNumColumns = getAutoNumberColumns(columns); - } + public Column getColumn(String name); /** * @return the properties for this table * @usage _general_method_ */ - public PropertyMap getProperties() throws IOException { - if(_props == null) { - _props = getPropertyMaps().getDefault(); - } - return _props; - } + public PropertyMap getProperties() throws IOException; - /** - * @return all PropertyMaps for this table (and columns) - * @usage _general_method_ - */ - protected PropertyMaps getPropertyMaps() throws IOException { - if(_propertyMaps == null) { - _propertyMaps = getDatabase().getPropertiesForObject( - _tableDefPageNumber); - } - return _propertyMaps; - } - /** * @return All of the Indexes on this table (unmodifiable List) * @usage _intermediate_method_ */ - public List getIndexes() { - return Collections.unmodifiableList(_indexes); - } + public List getIndexes(); /** * @return the index with the given name * @throws IllegalArgumentException if there is no index with the given name * @usage _intermediate_method_ */ - public Index getIndex(String name) { - for(Index index : _indexes) { - if(index.getName().equalsIgnoreCase(name)) { - return index; - } - } - throw new IllegalArgumentException("Index with name " + name + - " does not exist on this table"); - } + public Index getIndex(String name); /** * @return the primary key index for this table @@ -453,1066 +127,43 @@ public class Table * table * @usage _intermediate_method_ */ - public Index getPrimaryKeyIndex() { - for(Index index : _indexes) { - if(index.isPrimaryKey()) { - return index; - } - } - throw new IllegalArgumentException("Table " + getName() + - " does not have a primary key index"); - } - + public Index getPrimaryKeyIndex(); + /** * @return the foreign key index joining this table to the given other table * @throws IllegalArgumentException if there is no relationship between this * table and the given table * @usage _intermediate_method_ */ - public Index getForeignKeyIndex(Table otherTable) { - for(Index index : _indexes) { - if(index.isForeignKey() && (index.getReference() != null) && - (index.getReference().getOtherTablePageNumber() == - otherTable.getTableDefPageNumber())) { - return index; - } - } - throw new IllegalArgumentException( - "Table " + getName() + " does not have a foreign key reference to " + - otherTable.getName()); - } - - /** - * @return All of the IndexData on this table (unmodifiable List) - */ - List getIndexDatas() { - return Collections.unmodifiableList(_indexDatas); - } - - /** - * Only called by unit tests - */ - int getLogicalIndexCount() { - return _logicalIndexCount; - } - - private Cursor getInternalCursor() { - if(_cursor == null) { - _cursor = Cursor.createCursor(this); - } - return _cursor; - } - - /** - * After calling this method, getNextRow will return the first row in the - * table, see {@link Cursor#reset}. - * @usage _general_method_ - */ - public void reset() { - getInternalCursor().reset(); - } - - /** - * Delete the current row (retrieved by a call to {@link #getNextRow()}). - * @usage _general_method_ - */ - public void deleteCurrentRow() throws IOException { - getInternalCursor().deleteCurrentRow(); - } - - /** - * Delete the row on which the given rowState is currently positioned. - *

- * Note, this method is not generally meant to be used directly. You should - * use the {@link #deleteCurrentRow} method or use the Cursor class, which - * allows for more complex table interactions. - * @usage _advanced_method_ - */ - public void deleteRow(RowState rowState, RowId rowId) throws IOException { - requireValidRowId(rowId); - - // ensure that the relevant row state is up-to-date - ByteBuffer rowBuffer = positionAtRowHeader(rowState, rowId); - - requireNonDeletedRow(rowState, rowId); - - // delete flag always gets set in the "header" row (even if data is on - // overflow row) - int pageNumber = rowState.getHeaderRowId().getPageNumber(); - int rowNumber = rowState.getHeaderRowId().getRowNumber(); - - // attempt to fill in index column values - Object[] rowValues = null; - if(!_indexDatas.isEmpty()) { - - // move to row data to get index values - rowBuffer = positionAtRowData(rowState, rowId); - - for(Column idxCol : _indexColumns) { - getRowColumn(getFormat(), rowBuffer, idxCol, rowState, null); - } - - // use any read rowValues to help update the indexes - rowValues = rowState.getRowValues(); - - // check foreign keys before proceeding w/ deletion - _fkEnforcer.deleteRow(rowValues); - - // move back to the header - rowBuffer = positionAtRowHeader(rowState, rowId); - } - - // finally, pull the trigger - int rowIndex = getRowStartOffset(rowNumber, getFormat()); - rowBuffer.putShort(rowIndex, (short)(rowBuffer.getShort(rowIndex) - | DELETED_ROW_MASK | OVERFLOW_ROW_MASK)); - writeDataPage(rowBuffer, pageNumber); - - // update the indexes - for(IndexData indexData : _indexDatas) { - indexData.deleteRow(rowValues, rowId); - } - - // make sure table def gets updated - updateTableDefinition(-1); - } - - /** - * @return The next row in this table (Column name -> Column value) - * @usage _general_method_ - */ - public Map getNextRow() throws IOException { - return getNextRow(null); - } - - /** - * @param columnNames Only column names in this collection will be returned - * @return The next row in this table (Column name -> Column value) - * @usage _general_method_ - */ - public Map getNextRow(Collection columnNames) - throws IOException - { - return getInternalCursor().getNextRow(columnNames); - } - - /** - * Reads a single column from the given row. - *

- * Note, this method is not generally meant to be used directly. Instead - * use the Cursor class, which allows for more complex table interactions, - * e.g. {@link Cursor#getCurrentRowValue}. - * @usage _advanced_method_ - */ - public Object getRowValue(RowState rowState, RowId rowId, Column column) - throws IOException - { - if(this != column.getTable()) { - throw new IllegalArgumentException( - "Given column " + column + " is not from this table"); - } - requireValidRowId(rowId); - - // position at correct row - ByteBuffer rowBuffer = positionAtRowData(rowState, rowId); - requireNonDeletedRow(rowState, rowId); - - return getRowColumn(getFormat(), rowBuffer, column, rowState, null); - } - - /** - * Reads some columns from the given row. - * @param columnNames Only column names in this collection will be returned - * @usage _advanced_method_ - */ - public Map getRow( - RowState rowState, RowId rowId, Collection columnNames) - throws IOException - { - requireValidRowId(rowId); - - // position at correct row - ByteBuffer rowBuffer = positionAtRowData(rowState, rowId); - requireNonDeletedRow(rowState, rowId); - - return getRow(getFormat(), rowState, rowBuffer, _columns, columnNames); - } - - /** - * Reads the row data from the given row buffer. Leaves limit unchanged. - * Saves parsed row values to the given rowState. - */ - private static Map getRow( - JetFormat format, - RowState rowState, - ByteBuffer rowBuffer, - Collection columns, - Collection columnNames) - throws IOException - { - Map rtn = new LinkedHashMap( - columns.size()); - for(Column column : columns) { - - if((columnNames == null) || (columnNames.contains(column.getName()))) { - // Add the value to the row data - column.setRowValue( - rtn, getRowColumn(format, rowBuffer, column, rowState, null)); - } - } - return rtn; - } - - /** - * Reads the column data from the given row buffer. Leaves limit unchanged. - * Caches the returned value in the rowState. - */ - private static Object getRowColumn(JetFormat format, - ByteBuffer rowBuffer, - Column column, - RowState rowState, - Map rawVarValues) - throws IOException - { - byte[] columnData = null; - try { - - NullMask nullMask = rowState.getNullMask(rowBuffer); - boolean isNull = nullMask.isNull(column); - 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)); - } else if(isNull) { - // well, that's easy! (no need to update cache w/ null) - return null; - } - - // 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 + format.OFFSET_COLUMN_FIXED_DATA_ROW_OFFSET; - colDataPos = dataStart + column.getFixedDataOffset(); - colDataLen = column.getType().getFixedSize(column.getLength()); - - } else { - int varDataStart; - int varDataEnd; - - if(format.SIZE_ROW_VAR_COL_OFFSET == 2) { - - // read simple var length value - int varColumnOffsetPos = - (rowBuffer.limit() - nullMask.byteSize() - 4) - - (column.getVarLenTableIndex() * 2); - - varDataStart = rowBuffer.getShort(varColumnOffsetPos); - varDataEnd = rowBuffer.getShort(varColumnOffsetPos - 2); - - } else { - - // read jump-table based var length values - short[] varColumnOffsets = readJumpTableVarColOffsets( - rowState, rowBuffer, rowStart, nullMask); - - varDataStart = varColumnOffsets[column.getVarLenTableIndex()]; - varDataEnd = varColumnOffsets[column.getVarLenTableIndex() + 1]; - } - - colDataPos = rowStart + varDataStart; - colDataLen = varDataEnd - varDataStart; - } - - // grab the column data - rowBuffer.position(colDataPos); - columnData = ByteUtil.getBytes(rowBuffer, colDataLen); - - if((rawVarValues != null) && column.isVariableLength()) { - // caller wants raw value as well - rawVarValues.put(column, columnData); - } - - // parse the column data. we 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 for binary data (returned as byte[]), - // but binary data shouldn't be indexed anyway. - return rowState.setRowValue(column.getColumnIndex(), - column.read(columnData)); - - } catch(Exception e) { - - // cache "raw" row value. see note about caching above - rowState.setRowValue(column.getColumnIndex(), - Column.rawDataWrapper(columnData)); - - return rowState.handleRowError(column, columnData, e); - } - } - - private static short[] readJumpTableVarColOffsets( - RowState rowState, ByteBuffer rowBuffer, int rowStart, - NullMask nullMask) - { - short[] varColOffsets = rowState.getVarColOffsets(); - if(varColOffsets != null) { - return varColOffsets; - } - - // calculate offsets using jump-table info - int nullMaskSize = nullMask.byteSize(); - int rowEnd = rowStart + rowBuffer.remaining() - 1; - int numVarCols = ByteUtil.getUnsignedByte(rowBuffer, - rowEnd - nullMaskSize); - varColOffsets = new short[numVarCols + 1]; - - int rowLen = rowEnd - rowStart + 1; - int numJumps = (rowLen - 1) / MAX_BYTE; - int colOffset = rowEnd - nullMaskSize - numJumps - 1; - - // If last jump is a dummy value, ignore it - if(((colOffset - rowStart - numVarCols) / MAX_BYTE) < numJumps) { - numJumps--; - } - - int jumpsUsed = 0; - for(int i = 0; i < numVarCols + 1; i++) { - - while((jumpsUsed < numJumps) && - (i == ByteUtil.getUnsignedByte( - rowBuffer, rowEnd - nullMaskSize-jumpsUsed - 1))) { - jumpsUsed++; - } - - varColOffsets[i] = (short) - (ByteUtil.getUnsignedByte(rowBuffer, colOffset - i) - + (jumpsUsed * MAX_BYTE)); - } - - rowState.setVarColOffsets(varColOffsets); - return varColOffsets; - } - - /** - * Reads the null mask from the given row buffer. Leaves limit unchanged. - */ - private NullMask getRowNullMask(ByteBuffer rowBuffer) - throws IOException - { - // reset position to row start - rowBuffer.reset(); - - // Number of columns in this row - int columnCount = ByteUtil.getUnsignedVarInt( - rowBuffer, getFormat().SIZE_ROW_COLUMN_COUNT); - - // read null mask - NullMask nullMask = new NullMask(columnCount); - rowBuffer.position(rowBuffer.limit() - nullMask.byteSize()); //Null mask at end - nullMask.read(rowBuffer); - - return nullMask; - } - - /** - * Sets a new buffer to the correct row header page using the given rowState - * according to the given rowId. Deleted state is - * determined, but overflow row pointers are not followed. - * - * @return a ByteBuffer of the relevant page, or null if row was invalid - * @usage _advanced_method_ - */ - public static ByteBuffer positionAtRowHeader(RowState rowState, RowId rowId) - throws IOException - { - ByteBuffer rowBuffer = rowState.setHeaderRow(rowId); - - if(rowState.isAtHeaderRow()) { - // this task has already been accomplished - return rowBuffer; - } - - if(!rowState.isValid()) { - // this was an invalid page/row - rowState.setStatus(RowStateStatus.AT_HEADER); - return null; - } - - // note, we don't use findRowStart here cause we need the unmasked value - short rowStart = rowBuffer.getShort( - getRowStartOffset(rowId.getRowNumber(), - rowState.getTable().getFormat())); - - // check the deleted, overflow flags for the row (the "real" flags are - // always set on the header row) - RowStatus rowStatus = RowStatus.NORMAL; - if(isDeletedRow(rowStart)) { - rowStatus = RowStatus.DELETED; - } else if(isOverflowRow(rowStart)) { - rowStatus = RowStatus.OVERFLOW; - } - - rowState.setRowStatus(rowStatus); - rowState.setStatus(RowStateStatus.AT_HEADER); - return rowBuffer; - } - - /** - * Sets the position and limit in a new buffer using the given rowState - * according to the given row number and row end, following overflow row - * pointers as necessary. - * - * @return a ByteBuffer narrowed to the actual row data, or null if row was - * invalid or deleted - * @usage _advanced_method_ - */ - public static ByteBuffer positionAtRowData(RowState rowState, RowId rowId) - throws IOException - { - positionAtRowHeader(rowState, rowId); - if(!rowState.isValid() || rowState.isDeleted()) { - // row is invalid or deleted - rowState.setStatus(RowStateStatus.AT_FINAL); - return null; - } - - ByteBuffer rowBuffer = rowState.getFinalPage(); - int rowNum = rowState.getFinalRowId().getRowNumber(); - JetFormat format = rowState.getTable().getFormat(); - - if(rowState.isAtFinalRow()) { - // we've already found the final row data - return PageChannel.narrowBuffer( - rowBuffer, - findRowStart(rowBuffer, rowNum, format), - findRowEnd(rowBuffer, rowNum, format)); - } - - while(true) { - - // note, we don't use findRowStart here cause we need the unmasked value - short rowStart = rowBuffer.getShort(getRowStartOffset(rowNum, format)); - short rowEnd = findRowEnd(rowBuffer, rowNum, format); - - // note, at this point we know the row is not deleted, so ignore any - // subsequent deleted flags (as overflow rows are always marked deleted - // anyway) - boolean overflowRow = isOverflowRow(rowStart); - - // now, strip flags from rowStart offset - rowStart = (short)(rowStart & OFFSET_MASK); - - if (overflowRow) { - - if((rowEnd - rowStart) < 4) { - throw new IOException("invalid overflow row info"); - } - - // Overflow page. the "row" data in the current page points to - // another page/row - int overflowRowNum = ByteUtil.getUnsignedByte(rowBuffer, rowStart); - int overflowPageNum = ByteUtil.get3ByteInt(rowBuffer, rowStart + 1); - rowBuffer = rowState.setOverflowRow( - new RowId(overflowPageNum, overflowRowNum)); - rowNum = overflowRowNum; - - } else { - - rowState.setStatus(RowStateStatus.AT_FINAL); - return PageChannel.narrowBuffer(rowBuffer, rowStart, rowEnd); - } - } - } - - - /** - * Calls reset on this table and returns a modifiable - * Iterator which will iterate through all the rows of this table. Use of - * the Iterator follows the same restrictions as a call to - * getNextRow. - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within - * @usage _general_method_ - */ - public Iterator> iterator() - { - return iterator(null); - } - - /** - * Calls reset on this table and returns a modifiable - * Iterator which will iterate through all the rows of this table, returning - * only the given columns. Use of the Iterator follows the same - * restrictions as a call to getNextRow. - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within - * @usage _general_method_ - */ - public Iterator> iterator(Collection columnNames) - { - reset(); - return getInternalCursor().iterator(columnNames); - } - - /** - * Writes a new table defined by the given TableCreator to the database. - * @usage _advanced_method_ - */ - protected static void writeTableDefinition(TableCreator creator) - throws IOException - { - // first, create the usage map page - createUsageMapDefinitionBuffer(creator); - - // next, determine how big the table def will be (in case it will be more - // than one page) - JetFormat format = creator.getFormat(); - int idxDataLen = (creator.getIndexCount() * - (format.SIZE_INDEX_DEFINITION + - format.SIZE_INDEX_COLUMN_BLOCK)) + - (creator.getLogicalIndexCount() * format.SIZE_INDEX_INFO_BLOCK); - int colUmapLen = creator.getLongValueColumns().size() * 10; - int totalTableDefSize = format.SIZE_TDEF_HEADER + - (format.SIZE_COLUMN_DEF_BLOCK * creator.getColumns().size()) + - idxDataLen + colUmapLen + format.SIZE_TDEF_TRAILER; - - // total up the amount of space used by the column and index names (2 - // bytes per char + 2 bytes for the length) - for(Column col : creator.getColumns()) { - int nameByteLen = (col.getName().length() * - JetFormat.TEXT_FIELD_UNIT_SIZE); - totalTableDefSize += nameByteLen + 2; - } - - for(IndexBuilder idx : creator.getIndexes()) { - int nameByteLen = (idx.getName().length() * - JetFormat.TEXT_FIELD_UNIT_SIZE); - totalTableDefSize += nameByteLen + 2; - } - - - // now, create the table definition - PageChannel pageChannel = creator.getPageChannel(); - ByteBuffer buffer = pageChannel .createBuffer(Math.max(totalTableDefSize, - format.PAGE_SIZE)); - writeTableDefinitionHeader(creator, buffer, totalTableDefSize); - - if(creator.hasIndexes()) { - // index row counts - IndexData.writeRowCountDefinitions(creator, buffer); - } - - // column definitions - Column.writeDefinitions(creator, buffer); - - if(creator.hasIndexes()) { - // index and index data definitions - IndexData.writeDefinitions(creator, buffer); - Index.writeDefinitions(creator, buffer); - } - - // write long value column usage map references - for(Column lvalCol : creator.getLongValueColumns()) { - buffer.putShort(lvalCol.getColumnNumber()); - TableCreator.ColumnState colState = - creator.getColumnState(lvalCol); - - // owned pages umap (both are on same page) - buffer.put(colState.getUmapOwnedRowNumber()); - ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber()); - // free space pages umap - buffer.put(colState.getUmapFreeRowNumber()); - ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber()); - } - - //End of tabledef - buffer.put((byte) 0xff); - buffer.put((byte) 0xff); - - // write table buffer to database - if(totalTableDefSize <= format.PAGE_SIZE) { - - // easy case, fits on one page - buffer.putShort(format.OFFSET_FREE_SPACE, - (short)(buffer.remaining() - 8)); // overwrite page free space - // Write the tdef page to disk. - pageChannel.writePage(buffer, creator.getTdefPageNumber()); - - } else { - - // need to split across multiple pages - ByteBuffer partialTdef = pageChannel.createPageBuffer(); - buffer.rewind(); - int nextTdefPageNumber = PageChannel.INVALID_PAGE_NUMBER; - while(buffer.hasRemaining()) { - - // reset for next write - partialTdef.clear(); - - if(nextTdefPageNumber == PageChannel.INVALID_PAGE_NUMBER) { - - // this is the first page. note, the first page already has the - // page header, so no need to write it here - nextTdefPageNumber = creator.getTdefPageNumber(); - - } else { - - // write page header - writeTablePageHeader(partialTdef); - } - - // copy the next page of tdef bytes - int curTdefPageNumber = nextTdefPageNumber; - int writeLen = Math.min(partialTdef.remaining(), buffer.remaining()); - partialTdef.put(buffer.array(), buffer.position(), writeLen); - ByteUtil.forward(buffer, writeLen); - - if(buffer.hasRemaining()) { - // need a next page - nextTdefPageNumber = pageChannel.allocateNewPage(); - partialTdef.putInt(format.OFFSET_NEXT_TABLE_DEF_PAGE, - nextTdefPageNumber); - } - - // update page free space - partialTdef.putShort(format.OFFSET_FREE_SPACE, - (short)(partialTdef.remaining() - 8)); // overwrite page free space - - // write partial page to disk - pageChannel.writePage(partialTdef, curTdefPageNumber); - } - - } - } - - /** - * @param buffer Buffer to write to - * @param columns List of Columns in the table - */ - private static void writeTableDefinitionHeader( - TableCreator creator, ByteBuffer buffer, int totalTableDefSize) - throws IOException - { - List columns = creator.getColumns(); - - //Start writing the tdef - writeTablePageHeader(buffer); - buffer.putInt(totalTableDefSize); //Length of table def - buffer.putInt(MAGIC_TABLE_NUMBER); // seemingly constant magic value - buffer.putInt(0); //Number of rows - buffer.putInt(0); //Last Autonumber - buffer.put((byte) 1); // this makes autonumbering work in access - for (int i = 0; i < 15; i++) { //Unknown - buffer.put((byte) 0); - } - buffer.put(Table.TYPE_USER); //Table type - buffer.putShort((short) columns.size()); //Max columns a row will have - buffer.putShort(Column.countVariableLength(columns)); //Number of variable columns in table - buffer.putShort((short) columns.size()); //Number of columns in table - buffer.putInt(creator.getLogicalIndexCount()); //Number of logical indexes in table - buffer.putInt(creator.getIndexCount()); //Number of indexes in table - buffer.put((byte) 0); //Usage map row number - ByteUtil.put3ByteInt(buffer, creator.getUmapPageNumber()); //Usage map page number - buffer.put((byte) 1); //Free map row number - ByteUtil.put3ByteInt(buffer, creator.getUmapPageNumber()); //Free map page number - if (LOG.isDebugEnabled()) { - int position = buffer.position(); - buffer.rewind(); - LOG.debug("Creating new table def block:\n" + ByteUtil.toHexString( - buffer, creator.getFormat().SIZE_TDEF_HEADER)); - buffer.position(position); - } - } - - /** - * Writes the page header for a table definition page - * @param buffer Buffer to write to - */ - private static void writeTablePageHeader(ByteBuffer buffer) - { - buffer.put(PageTypes.TABLE_DEF); //Page type - buffer.put((byte) 0x01); //Unknown - buffer.put((byte) 0); //Unknown - buffer.put((byte) 0); //Unknown - buffer.putInt(0); //Next TDEF page pointer - } - - /** - * Writes the given name into the given buffer in the format as expected by - * {@link #readName}. - */ - static void writeName(ByteBuffer buffer, String name, Charset charset) - { - ByteBuffer encName = Column.encodeUncompressedText(name, charset); - buffer.putShort((short) encName.remaining()); - buffer.put(encName); - } - - /** - * Create the usage map definition page buffer. The "used pages" map is in - * row 0, the "pages with free space" map is in row 1. Index usage maps are - * in subsequent rows. - */ - private static void createUsageMapDefinitionBuffer(TableCreator creator) - throws IOException - { - List lvalCols = creator.getLongValueColumns(); - - // 2 table usage maps plus 1 for each index and 2 for each lval col - int indexUmapEnd = 2 + creator.getIndexCount(); - int umapNum = indexUmapEnd + (lvalCols.size() * 2); - - JetFormat format = creator.getFormat(); - int umapRowLength = format.OFFSET_USAGE_MAP_START + - format.USAGE_MAP_TABLE_BYTE_LENGTH; - int umapSpaceUsage = getRowSpaceUsage(umapRowLength, format); - PageChannel pageChannel = creator.getPageChannel(); - int umapPageNumber = PageChannel.INVALID_PAGE_NUMBER; - ByteBuffer umapBuf = null; - int freeSpace = 0; - int rowStart = 0; - int umapRowNum = 0; - - for(int i = 0; i < umapNum; ++i) { - - if(umapBuf == null) { - - // need new page for usage maps - if(umapPageNumber == PageChannel.INVALID_PAGE_NUMBER) { - // first umap page has already been reserved - umapPageNumber = creator.getUmapPageNumber(); - } else { - // need another umap page - umapPageNumber = creator.reservePageNumber(); - } - - freeSpace = format.DATA_PAGE_INITIAL_FREE_SPACE; - - umapBuf = pageChannel.createPageBuffer(); - umapBuf.put(PageTypes.DATA); - umapBuf.put((byte) 0x1); //Unknown - umapBuf.putShort((short)freeSpace); //Free space in page - umapBuf.putInt(0); //Table definition - umapBuf.putInt(0); //Unknown - umapBuf.putShort((short)0); //Number of records on this page - - rowStart = findRowEnd(umapBuf, 0, format) - umapRowLength; - umapRowNum = 0; - } - - umapBuf.putShort(getRowStartOffset(umapRowNum, format), (short)rowStart); - - if(i == 0) { - - // table "owned pages" map definition - umapBuf.put(rowStart, UsageMap.MAP_TYPE_REFERENCE); - - } else if(i == 1) { - - // table "free space pages" map definition - umapBuf.put(rowStart, UsageMap.MAP_TYPE_INLINE); - - } else if(i < indexUmapEnd) { - - // index umap - int indexIdx = i - 2; - IndexBuilder idx = creator.getIndexes().get(indexIdx); - - // allocate root page for the index - int rootPageNumber = pageChannel.allocateNewPage(); - - // stash info for later use - TableCreator.IndexState idxState = creator.getIndexState(idx); - idxState.setRootPageNumber(rootPageNumber); - idxState.setUmapRowNumber((byte)umapRowNum); - idxState.setUmapPageNumber(umapPageNumber); - - // index map definition, including initial root page - umapBuf.put(rowStart, UsageMap.MAP_TYPE_INLINE); - umapBuf.putInt(rowStart + 1, rootPageNumber); - umapBuf.put(rowStart + 5, (byte)1); - - } else { - - // long value column umaps - int lvalColIdx = i - indexUmapEnd; - int umapType = lvalColIdx % 2; - lvalColIdx /= 2; - - Column lvalCol = lvalCols.get(lvalColIdx); - TableCreator.ColumnState colState = - creator.getColumnState(lvalCol); - - umapBuf.put(rowStart, UsageMap.MAP_TYPE_INLINE); - - if((umapType == 1) && - (umapPageNumber != colState.getUmapPageNumber())) { - // we want to force both usage maps for a column to be on the same - // data page, so just discard the previous one we wrote - --i; - umapType = 0; - } - - if(umapType == 0) { - // lval column "owned pages" usage map - colState.setUmapOwnedRowNumber((byte)umapRowNum); - colState.setUmapPageNumber(umapPageNumber); - } else { - // lval column "free space pages" usage map (always on same page) - colState.setUmapFreeRowNumber((byte)umapRowNum); - } - } - - rowStart -= umapRowLength; - freeSpace -= umapSpaceUsage; - ++umapRowNum; - - if((freeSpace <= umapSpaceUsage) || (i == (umapNum - 1))) { - // finish current page - umapBuf.putShort(format.OFFSET_FREE_SPACE, (short)freeSpace); - umapBuf.putShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE, - (short)umapRowNum); - pageChannel.writePage(umapBuf, umapPageNumber); - umapBuf = null; - } - } - } - - /** - * Returns a single ByteBuffer which contains the entire table definition - * (which may span multiple database pages). - */ - private ByteBuffer loadCompleteTableDefinitionBuffer(ByteBuffer tableBuffer) - throws IOException - { - int nextPage = tableBuffer.getInt(getFormat().OFFSET_NEXT_TABLE_DEF_PAGE); - ByteBuffer nextPageBuffer = null; - while (nextPage != 0) { - if (nextPageBuffer == null) { - nextPageBuffer = getPageChannel().createPageBuffer(); - } - getPageChannel().readPage(nextPageBuffer, nextPage); - nextPage = nextPageBuffer.getInt(getFormat().OFFSET_NEXT_TABLE_DEF_PAGE); - ByteBuffer newBuffer = getPageChannel().createBuffer( - tableBuffer.capacity() + getFormat().PAGE_SIZE - 8); - newBuffer.put(tableBuffer); - newBuffer.put(nextPageBuffer.array(), 8, getFormat().PAGE_SIZE - 8); - tableBuffer = newBuffer; - tableBuffer.flip(); - } - return tableBuffer; - } - - /** - * Read the table definition - */ - private void readTableDefinition(ByteBuffer tableBuffer) throws IOException - { - if (LOG.isDebugEnabled()) { - tableBuffer.rewind(); - LOG.debug("Table def block:\n" + ByteUtil.toHexString(tableBuffer, - getFormat().SIZE_TDEF_HEADER)); - } - _rowCount = tableBuffer.getInt(getFormat().OFFSET_NUM_ROWS); - _lastLongAutoNumber = tableBuffer.getInt(getFormat().OFFSET_NEXT_AUTO_NUMBER); - if(getFormat().OFFSET_NEXT_COMPLEX_AUTO_NUMBER >= 0) { - _lastComplexTypeAutoNumber = tableBuffer.getInt( - getFormat().OFFSET_NEXT_COMPLEX_AUTO_NUMBER); - } - _tableType = tableBuffer.get(getFormat().OFFSET_TABLE_TYPE); - _maxColumnCount = tableBuffer.getShort(getFormat().OFFSET_MAX_COLS); - _maxVarColumnCount = tableBuffer.getShort(getFormat().OFFSET_NUM_VAR_COLS); - short columnCount = tableBuffer.getShort(getFormat().OFFSET_NUM_COLS); - _logicalIndexCount = tableBuffer.getInt(getFormat().OFFSET_NUM_INDEX_SLOTS); - _indexCount = tableBuffer.getInt(getFormat().OFFSET_NUM_INDEXES); - - tableBuffer.position(getFormat().OFFSET_OWNED_PAGES); - _ownedPages = UsageMap.read(getDatabase(), tableBuffer, false); - tableBuffer.position(getFormat().OFFSET_FREE_SPACE_PAGES); - _freeSpacePages = UsageMap.read(getDatabase(), tableBuffer, false); - - for (int i = 0; i < _indexCount; i++) { - _indexDatas.add(IndexData.create(this, tableBuffer, i, getFormat())); - } - - int colOffset = getFormat().OFFSET_INDEX_DEF_BLOCK + - _indexCount * getFormat().SIZE_INDEX_DEFINITION; - int dispIndex = 0; - for (int i = 0; i < columnCount; i++) { - Column column = new Column(this, tableBuffer, - colOffset + (i * getFormat().SIZE_COLUMN_HEADER), dispIndex++); - _columns.add(column); - if(column.isVariableLength()) { - // also shove it in the variable columns list, which is ordered - // differently from the _columns list - _varColumns.add(column); - } - } - tableBuffer.position(colOffset + - (columnCount * getFormat().SIZE_COLUMN_HEADER)); - for (int i = 0; i < columnCount; i++) { - Column column = _columns.get(i); - column.setName(readName(tableBuffer)); - } - Collections.sort(_columns); - _autoNumColumns = getAutoNumberColumns(_columns); - - // setup the data index for the columns - int colIdx = 0; - for(Column col : _columns) { - col.setColumnIndex(colIdx++); - } - - // sort variable length columns based on their index into the variable - // length offset table, because we will write the columns in this order - Collections.sort(_varColumns, VAR_LEN_COLUMN_COMPARATOR); - - // read index column information - for (int i = 0; i < _indexCount; i++) { - IndexData idxData = _indexDatas.get(i); - idxData.read(tableBuffer, _columns); - // keep track of all columns involved in indexes - for(IndexData.ColumnDescriptor iCol : idxData.getColumns()) { - _indexColumns.add(iCol.getColumn()); - } - } - - // read logical index info (may be more logical indexes than index datas) - for (int i = 0; i < _logicalIndexCount; i++) { - _indexes.add(new Index(tableBuffer, _indexDatas, getFormat())); - } + public Index getForeignKeyIndex(Table otherTable); - // read logical index names - for (int i = 0; i < _logicalIndexCount; i++) { - _indexes.get(i).setName(readName(tableBuffer)); - } - - Collections.sort(_indexes); - - // read column usage map info - while(tableBuffer.remaining() >= 2) { - - short umapColNum = tableBuffer.getShort(); - if(umapColNum == IndexData.COLUMN_UNUSED) { - break; - } - - UsageMap colOwnedPages = UsageMap.read( - getDatabase(), tableBuffer, false); - UsageMap colFreeSpacePages = UsageMap.read( - getDatabase(), tableBuffer, false); - - for(Column col : _columns) { - if(col.getColumnNumber() == umapColNum) { - col.setUsageMaps(colOwnedPages, colFreeSpacePages); - break; - } - } - } - - // re-sort columns if necessary - if(getDatabase().getColumnOrder() != ColumnOrder.DATA) { - Collections.sort(_columns, DISPLAY_ORDER_COMPARATOR); - } - - for(Column col : _columns) { - // some columns need to do extra work after the table is completely - // loaded - col.postTableLoadInit(); - } - } - - /** - * Writes the given page data to the given page number, clears any other - * relevant buffers. - */ - private void writeDataPage(ByteBuffer pageBuffer, int pageNumber) - throws IOException - { - // write the page data - getPageChannel().writePage(pageBuffer, pageNumber); - - // possibly invalidate the add row buffer if a different data buffer is - // being written (e.g. this happens during deleteRow) - _addRowBufferH.possiblyInvalidate(pageNumber, pageBuffer); - - // update modification count so any active RowStates can keep themselves - // up-to-date - ++_modCount; - } - - /** - * Returns a name read from the buffer at the current position. The - * expected name format is the name length followed by the name - * encoded using the {@link JetFormat#CHARSET} - */ - private String readName(ByteBuffer buffer) { - int nameLength = readNameLength(buffer); - byte[] nameBytes = ByteUtil.getBytes(buffer, nameLength); - return Column.decodeUncompressedText(nameBytes, - getDatabase().getCharset()); - } - - /** - * Returns a name length read from the buffer at the current position. - */ - private int readNameLength(ByteBuffer buffer) { - return ByteUtil.getUnsignedVarInt(buffer, getFormat().SIZE_NAME_LENGTH); - } - /** * Converts a map of columnName -> columnValue to an array of row values * appropriate for a call to {@link #addRow(Object...)}. * @usage _general_method_ */ - public Object[] asRow(Map rowMap) { - return asRow(rowMap, null); - } - + public Object[] asRow(Map rowMap); + /** * Converts a map of columnName -> columnValue to an array of row values - * appropriate for a call to {@link #updateCurrentRow(Object...)}. + * appropriate for a call to {@link Cursor#updateCurrentRow(Object...)}. * @usage _general_method_ */ - public Object[] asUpdateRow(Map rowMap) { - return asRow(rowMap, Column.KEEP_VALUE); - } + public Object[] asUpdateRow(Map rowMap); /** - * Converts a map of columnName -> columnValue to an array of row values. + * @usage _general_method_ */ - private Object[] asRow(Map rowMap, Object defaultValue) - { - Object[] row = new Object[_columns.size()]; - if(defaultValue != null) { - Arrays.fill(row, defaultValue); - } - if(rowMap == null) { - return row; - } - for(Column col : _columns) { - if(rowMap.containsKey(col.getName())) { - col.setRowValue(row, col.getRowValue(rowMap)); - } - } - return row; - } - + public int getRowCount(); + /** * Adds a single row to this table and writes it to disk. The values are * expected to be given in the order that the Columns are listed by the * {@link #getColumns} method. This is by default the storage order of the * Columns in the database, however this order can be influenced by setting - * the ColumnOrder via {@link Database#setColumnOrder} prior to opening the - * Table. The {@link #asRow} method can be used to easily convert a row Map into the - * appropriate row array for this Table. + * the ColumnOrder via {@link Database#setColumnOrder} prior to opening + * the Table. The {@link #asRow} method can be used to easily convert a row + * Map into the appropriate row array for this Table. *

* Note, if this table has an auto-number column, the value generated will be * put back into the given row array (assuming the given row array is at @@ -1521,16 +172,28 @@ public class Table * @param row row values for a single row. the given row array will be * modified if this table contains an auto-number column, * otherwise it will not be modified. + * @return the given row values if long enough, otherwise a new array. the + * returned array will contain any autonumbers generated * @usage _general_method_ */ - public void addRow(Object... row) throws IOException { - addRows(Collections.singletonList(row), _singleRowBufferH); - } - + public Object[] addRow(Object... row) throws IOException; + + /** + * Calls {@link #asRow} on the given row map and passes the result to {@link + * #addRow}. + *

+ * Note, if this table has an auto-number column, the value generated will be + * put back into the given row map. + * @return the given row map, which will contain any autonumbers generated + * @usage _general_method_ + */ + public > M addRowFromMap(M row) + throws IOException; + /** * Add multiple rows to this table, only writing to disk after all * rows have been written, and every time a data page is filled. This - * is much more efficient than calling addRow multiple times. + * is much more efficient than calling {@link #addRow} multiple times. *

* Note, if this table has an auto-number column, the values written will be * put back into the given row arrays (assuming the given row array is at @@ -1541,1095 +204,83 @@ public class Table * @param rows List of Object[] row values. the rows will be modified if * this table contains an auto-number column, otherwise they * will not be modified. + * @return the given row values list (unless row values were to small), with + * appropriately sized row values (the ones passed in if long + * enough). the returned arrays will contain any autonumbers + * generated * @usage _general_method_ */ - public void addRows(List rows) throws IOException { - addRows(rows, _multiRowBufferH); - } - - /** - * 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 writeRowBufferH TempBufferHolder used to generate buffers for - * writing the row data - */ - private void addRows(List inRows, - TempBufferHolder writeRowBufferH) - throws IOException - { - if(inRows.isEmpty()) { - return; - } - - // copy the input rows to a modifiable list so we can update the elements - List rows = new ArrayList(inRows); - ByteBuffer[] rowData = new ByteBuffer[rows.size()]; - for (int i = 0; i < rows.size(); i++) { - - // we need to make sure the row is the right length and is an Object[] - // (fill with null if too short). note, if the row is copied the caller - // will not be able to access any generated auto-number value, but if - // they need that info they should use a row array of the right - // size/type! - Object[] row = rows.get(i); - if((row.length < _columns.size()) || (row.getClass() != Object[].class)) { - row = dupeRow(row, _columns.size()); - // we copied the row, so put the copy back into the rows list - rows.set(i, row); - } - - // fill in autonumbers - handleAutoNumbersForAdd(row); - - // write the row of data to a temporary buffer - rowData[i] = createRow(row, writeRowBufferH.getPageBuffer(getPageChannel())); - - if (rowData[i].limit() > getFormat().MAX_ROW_SIZE) { - throw new IOException("Row size " + rowData[i].limit() + - " is too large"); - } - } - - ByteBuffer dataPage = null; - int pageNumber = PageChannel.INVALID_PAGE_NUMBER; - - for (int i = 0; i < rowData.length; i++) { - int rowSize = rowData[i].remaining(); - Object[] row = rows.get(i); - - // handle foreign keys before adding to table - _fkEnforcer.addRow(row); - - // get page with space - dataPage = findFreeRowSpace(rowSize, dataPage, pageNumber); - pageNumber = _addRowBufferH.getPageNumber(); - - // write out the row data - int rowNum = addDataPageRow(dataPage, rowSize, getFormat(), 0); - dataPage.put(rowData[i]); - - // update the indexes - RowId rowId = new RowId(pageNumber, rowNum); - for(IndexData indexData : _indexDatas) { - indexData.addRow(row, rowId); - } - } - - writeDataPage(dataPage, pageNumber); - - // Update tdef page - updateTableDefinition(rows.size()); - } + public List addRows(List rows) + throws IOException; /** - * Updates the current row to the new values. - *

- * Note, if this table has an auto-number column(s), the existing value(s) - * will be maintained, unchanged. - * - * @param row new row values for the current row. + * Calls {@link #asRow} on the given row maps and passes the results to + * {@link #addRows}. + *

+ * Note, if this table has an auto-number column, the values generated will + * be put back into the appropriate row maps. + * @return the given row map list, where the row maps will contain any + * autonumbers generated * @usage _general_method_ */ - public void updateCurrentRow(Object... row) throws IOException { - getInternalCursor().updateCurrentRow(row); - } - - /** - * Update the row on which the given rowState is currently positioned. - *

- * Note, this method is not generally meant to be used directly. You should - * use the {@link #updateCurrentRow} method or use the Cursor class, which - * allows for more complex table interactions, e.g. - * {@link Cursor#setCurrentRowValue} and {@link Cursor#updateCurrentRow}. - * @usage _advanced_method_ - */ - public void updateRow(RowState rowState, RowId rowId, Object... row) - throws IOException - { - requireValidRowId(rowId); - - // ensure that the relevant row state is up-to-date - ByteBuffer rowBuffer = positionAtRowData(rowState, rowId); - int oldRowSize = rowBuffer.remaining(); - - requireNonDeletedRow(rowState, rowId); - - // we need to make sure the row is the right length & type (fill with - // null if too short). - if((row.length < _columns.size()) || (row.getClass() != Object[].class)) { - row = dupeRow(row, _columns.size()); - } - - // hang on to the raw values of var length columns we are "keeping". this - // will allow us to re-use pre-written var length data, which can save - // space for things like long value columns. - Map keepRawVarValues = - (!_varColumns.isEmpty() ? new HashMap() : null); - - for(Column column : _columns) { - if(_autoNumColumns.contains(column)) { - // fill in any auto-numbers (we don't allow autonumber values to be - // modified) - column.setRowValue(row, getRowColumn(getFormat(), rowBuffer, column, - rowState, null)); - } else if(column.getRowValue(row) == Column.KEEP_VALUE) { - // fill in any "keep value" fields - column.setRowValue(row, getRowColumn(getFormat(), rowBuffer, column, - rowState, keepRawVarValues)); - } else if(_indexColumns.contains(column)) { - // read row value to help update indexes - getRowColumn(getFormat(), rowBuffer, column, rowState, null); - } - } - - // generate new row bytes - ByteBuffer newRowData = createRow( - row, _singleRowBufferH.getPageBuffer(getPageChannel()), oldRowSize, - keepRawVarValues); - - if (newRowData.limit() > getFormat().MAX_ROW_SIZE) { - throw new IOException("Row size " + newRowData.limit() + - " is too large"); - } - - if(!_indexDatas.isEmpty()) { - - Object[] oldRowValues = rowState.getRowValues(); - - // check foreign keys before actually updating - _fkEnforcer.updateRow(oldRowValues, row); - - // delete old values from indexes - for(IndexData indexData : _indexDatas) { - indexData.deleteRow(oldRowValues, rowId); - } - } - - // see if we can squeeze the new row data into the existing row - rowBuffer.reset(); - int rowSize = newRowData.remaining(); - - ByteBuffer dataPage = null; - int pageNumber = PageChannel.INVALID_PAGE_NUMBER; - - if(oldRowSize >= rowSize) { - - // awesome, slap it in! - rowBuffer.put(newRowData); - - // grab the page we just updated - dataPage = rowState.getFinalPage(); - pageNumber = rowState.getFinalRowId().getPageNumber(); - - } else { - - // bummer, need to find a new page for the data - dataPage = findFreeRowSpace(rowSize, null, - PageChannel.INVALID_PAGE_NUMBER); - pageNumber = _addRowBufferH.getPageNumber(); - - RowId headerRowId = rowState.getHeaderRowId(); - ByteBuffer headerPage = rowState.getHeaderPage(); - if(pageNumber == headerRowId.getPageNumber()) { - // new row is on the same page as header row, share page - dataPage = headerPage; - } - - // write out the new row data (set the deleted flag on the new data row - // so that it is ignored during normal table traversal) - int rowNum = addDataPageRow(dataPage, rowSize, getFormat(), - DELETED_ROW_MASK); - dataPage.put(newRowData); + public > List addRowsFromMaps(List rows) + throws IOException; - // write the overflow info into the header row and clear out the - // remaining header data - rowBuffer = PageChannel.narrowBuffer( - headerPage, - findRowStart(headerPage, headerRowId.getRowNumber(), getFormat()), - findRowEnd(headerPage, headerRowId.getRowNumber(), getFormat())); - rowBuffer.put((byte)rowNum); - ByteUtil.put3ByteInt(rowBuffer, pageNumber); - ByteUtil.clearRemaining(rowBuffer); - - // set the overflow flag on the header row - int headerRowIndex = getRowStartOffset(headerRowId.getRowNumber(), - getFormat()); - headerPage.putShort(headerRowIndex, - (short)(headerPage.getShort(headerRowIndex) - | OVERFLOW_ROW_MASK)); - if(pageNumber != headerRowId.getPageNumber()) { - writeDataPage(headerPage, headerRowId.getPageNumber()); - } - } - - // update the indexes - for(IndexData indexData : _indexDatas) { - indexData.addRow(row, rowId); - } - - writeDataPage(dataPage, pageNumber); - - updateTableDefinition(0); - } - - private ByteBuffer findFreeRowSpace(int rowSize, ByteBuffer dataPage, - int pageNumber) - throws IOException - { - // assume incoming page is modified - boolean modifiedPage = true; - - if(dataPage == null) { - - // find owned page w/ free space - dataPage = findFreeRowSpace(_ownedPages, _freeSpacePages, - _addRowBufferH); - - if(dataPage == null) { - // No data pages exist (with free space). Create a new one. - return newDataPage(); - } - - // found a page, see if it will work - pageNumber = _addRowBufferH.getPageNumber(); - // since we just loaded this page, it is not yet modified - modifiedPage = false; - } - - if(!rowFitsOnDataPage(rowSize, dataPage, getFormat())) { - - // Last data page is full. Write old one and create a new one. - if(modifiedPage) { - writeDataPage(dataPage, pageNumber); - } - _freeSpacePages.removePageNumber(pageNumber, true); - - dataPage = newDataPage(); - } - - return dataPage; - } - - static ByteBuffer findFreeRowSpace( - UsageMap ownedPages, UsageMap freeSpacePages, - TempPageHolder rowBufferH) - throws IOException - { - // find last data page (Not bothering to check other pages for free - // space.) - UsageMap.PageCursor revPageCursor = ownedPages.cursor(); - revPageCursor.afterLast(); - while(true) { - int tmpPageNumber = revPageCursor.getPreviousPage(); - if(tmpPageNumber < 0) { - break; - } - ByteBuffer dataPage = rowBufferH.setPage(ownedPages.getPageChannel(), - tmpPageNumber); - if(dataPage.get() == PageTypes.DATA) { - // found last data page, only use if actually listed in free space - // pages - if(freeSpacePages.containsPageNumber(tmpPageNumber)) { - return dataPage; - } - } - } - - return null; - } - /** - * Updates the table definition after rows are modified. + * Update the given row. Provided Row must have previously been returned + * from this Table. + * @return the given row, updated with the current row values + * @throws IllegalStateException if the given row is not valid, or deleted. */ - private void updateTableDefinition(int rowCountInc) throws IOException - { - // load table definition - ByteBuffer tdefPage = _tableDefBufferH.setPage(getPageChannel(), - _tableDefPageNumber); - - // make sure rowcount and autonumber are up-to-date - _rowCount += rowCountInc; - tdefPage.putInt(getFormat().OFFSET_NUM_ROWS, _rowCount); - tdefPage.putInt(getFormat().OFFSET_NEXT_AUTO_NUMBER, _lastLongAutoNumber); - int ctypeOff = getFormat().OFFSET_NEXT_COMPLEX_AUTO_NUMBER; - if(ctypeOff >= 0) { - tdefPage.putInt(ctypeOff, _lastComplexTypeAutoNumber); - } + public Row updateRow(Row row) throws IOException; - // write any index changes - for (IndexData indexData : _indexDatas) { - // write the unique entry count for the index to the table definition - // page - tdefPage.putInt(indexData.getUniqueEntryCountOffset(), - indexData.getUniqueEntryCount()); - // write the entry page for the index - indexData.update(); - } - - // write modified table definition - getPageChannel().writePage(tdefPage, _tableDefPageNumber); - } - /** - * Create a new data page - * @return Page number of the new page + * Delete the given row. Provided Row must have previously been returned + * from this Table. + * @return the given row + * @throws IllegalStateException if the given row is not valid */ - private ByteBuffer newDataPage() throws IOException { - if (LOG.isDebugEnabled()) { - LOG.debug("Creating new data page"); - } - ByteBuffer dataPage = _addRowBufferH.setNewPage(getPageChannel()); - dataPage.put(PageTypes.DATA); //Page type - dataPage.put((byte) 1); //Unknown - dataPage.putShort((short)getFormat().DATA_PAGE_INITIAL_FREE_SPACE); //Free space in this page - dataPage.putInt(_tableDefPageNumber); //Page pointer to table definition - dataPage.putInt(0); //Unknown - dataPage.putShort((short)0); //Number of rows on this page - int pageNumber = _addRowBufferH.getPageNumber(); - getPageChannel().writePage(dataPage, pageNumber); - _ownedPages.addPageNumber(pageNumber); - _freeSpacePages.addPageNumber(pageNumber); - return dataPage; - } - - ByteBuffer createRow(Object[] rowArray, ByteBuffer buffer) - throws IOException - { - return createRow(rowArray, buffer, 0, Collections.emptyMap()); - } - - /** - * Serialize a row of Objects into a byte buffer. - * - * @param rowArray row data, expected to be correct length for this table - * @param buffer buffer to which to write the row data - * @param minRowSize min size for result row - * @param rawVarValues optional, pre-written values for var length columns - * (enables re-use of previously written values). - * @return the given buffer, filled with the row data - */ - private ByteBuffer createRow(Object[] rowArray, ByteBuffer buffer, - int minRowSize, Map rawVarValues) - throws IOException - { - buffer.putShort(_maxColumnCount); - NullMask nullMask = new NullMask(_maxColumnCount); - - //Fixed length column data comes first - int fixedDataStart = buffer.position(); - int fixedDataEnd = fixedDataStart; - for (Column col : _columns) { - - if(col.isVariableLength()) { - continue; - } - - Object rowValue = col.getRowValue(rowArray); - - if (col.getType() == DataType.BOOLEAN) { - - if(Column.toBooleanValue(rowValue)) { - //Booleans are stored in the null mask - nullMask.markNotNull(col); - } - rowValue = null; - } - - if(rowValue != null) { - - // we have a value to write - nullMask.markNotNull(col); - - // remainingRowLength is ignored when writing fixed length data - buffer.position(fixedDataStart + col.getFixedDataOffset()); - buffer.put(col.write(rowValue, 0)); - } - - // always insert space for the entire fixed data column length - // (including null values), access expects the row to always be at least - // big enough to hold all fixed values - buffer.position(fixedDataStart + col.getFixedDataOffset() + - col.getLength()); - - // keep track of the end of fixed data - if(buffer.position() > fixedDataEnd) { - fixedDataEnd = buffer.position(); - } - - } - - // reposition at end of fixed data - buffer.position(fixedDataEnd); - - // only need this info if this table contains any var length data - if(_maxVarColumnCount > 0) { - - int maxRowSize = getFormat().MAX_ROW_SIZE; - - // figure out how much space remains for var length data. first, - // account for already written space - maxRowSize -= buffer.position(); - // now, account for trailer space - int trailerSize = (nullMask.byteSize() + 4 + (_maxVarColumnCount * 2)); - maxRowSize -= trailerSize; - - // for each non-null long value column we need to reserve a small - // amount of space so that we don't end up running out of row space - // later by being too greedy - for (Column varCol : _varColumns) { - if((varCol.getType().isLongValue()) && - (varCol.getRowValue(rowArray) != null)) { - maxRowSize -= getFormat().SIZE_LONG_VALUE_DEF; - } - } - - //Now write out variable length column data - short[] varColumnOffsets = new short[_maxVarColumnCount]; - int varColumnOffsetsIndex = 0; - for (Column varCol : _varColumns) { - short offset = (short) buffer.position(); - Object rowValue = varCol.getRowValue(rowArray); - if (rowValue != null) { - // we have a value - nullMask.markNotNull(varCol); - - byte[] rawValue = null; - ByteBuffer varDataBuf = null; - if(((rawValue = rawVarValues.get(varCol)) != null) && - (rawValue.length <= maxRowSize)) { - // save time and potentially db space, re-use raw value - varDataBuf = ByteBuffer.wrap(rawValue); - } else { - // write column value - varDataBuf = varCol.write(rowValue, maxRowSize); - } - - maxRowSize -= varDataBuf.remaining(); - if(varCol.getType().isLongValue()) { - // we already accounted for some amount of the long value data - // above. add that space back so we don't double count - maxRowSize += getFormat().SIZE_LONG_VALUE_DEF; - } - buffer.put(varDataBuf); - } - - // we do a loop here so that we fill in offsets for deleted columns - while(varColumnOffsetsIndex <= varCol.getVarLenTableIndex()) { - varColumnOffsets[varColumnOffsetsIndex++] = offset; - } - } - - // fill in offsets for any remaining deleted columns - while(varColumnOffsetsIndex < varColumnOffsets.length) { - varColumnOffsets[varColumnOffsetsIndex++] = (short) buffer.position(); - } - - // record where we stopped writing - int eod = buffer.position(); - - // insert padding if necessary - padRowBuffer(buffer, minRowSize, trailerSize); - - buffer.putShort((short) eod); //EOD marker - - //Now write out variable length offsets - //Offsets are stored in reverse order - for (int i = _maxVarColumnCount - 1; i >= 0; i--) { - buffer.putShort(varColumnOffsets[i]); - } - buffer.putShort(_maxVarColumnCount); //Number of var length columns - - } else { - - // insert padding for row w/ no var cols - padRowBuffer(buffer, minRowSize, nullMask.byteSize()); - } - - nullMask.write(buffer); //Null mask - buffer.flip(); - if (LOG.isDebugEnabled()) { - LOG.debug("Creating new data block:\n" + ByteUtil.toHexString(buffer, buffer.limit())); - } - return buffer; - } - - /** - * Fill in all autonumber column values. - */ - private void handleAutoNumbersForAdd(Object[] row) - throws IOException - { - if(_autoNumColumns.isEmpty()) { - return; - } - - Object complexAutoNumber = null; - for(Column col : _autoNumColumns) { - // ignore given row value, use next autonumber - Column.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); - } - } - - private static void padRowBuffer(ByteBuffer buffer, int minRowSize, - int trailerSize) - { - int pos = buffer.position(); - if((pos + trailerSize) < minRowSize) { - // pad the row to get to the min byte size - int padSize = minRowSize - (pos + trailerSize); - ByteUtil.clearRange(buffer, pos, pos + padSize); - ByteUtil.forward(buffer, padSize); - } - } + public Row deleteRow(Row row) throws IOException; /** + * Calls {@link #reset} on this table and returns a modifiable + * Iterator which will iterate through all the rows of this table. Use of + * the Iterator follows the same restrictions as a call to + * {@link #getNextRow}. + *

+ * For more advanced iteration, use the {@link #getDefaultCursor default + * cursor} directly. + * @throws RuntimeIOException if an IOException is thrown by one of the + * operations, the actual exception will be contained within * @usage _general_method_ */ - public int getRowCount() { - return _rowCount; - } - - int getNextLongAutoNumber() { - // note, the saved value is the last one handed out, so pre-increment - return ++_lastLongAutoNumber; - } - - int getLastLongAutoNumber() { - // gets the last used auto number (does not modify) - return _lastLongAutoNumber; - } - - int getNextComplexTypeAutoNumber() { - // note, the saved value is the last one handed out, so pre-increment - return ++_lastComplexTypeAutoNumber; - } + public Iterator iterator(); - int getLastComplexTypeAutoNumber() { - // gets the last used auto number (does not modify) - return _lastComplexTypeAutoNumber; - } - - @Override - public String toString() { - StringBuilder rtn = new StringBuilder(); - rtn.append("Type: " + _tableType + - ((_tableType == TYPE_USER) ? " (USER)" : " (SYSTEM)")); - rtn.append("\nName: " + _name); - rtn.append("\nRow count: " + _rowCount); - rtn.append("\nColumn count: " + _columns.size()); - rtn.append("\nIndex (data) count: " + _indexCount); - rtn.append("\nLogical Index count: " + _logicalIndexCount); - rtn.append("\nColumns:\n"); - for(Column col : _columns) { - rtn.append(col); - } - rtn.append("\nIndexes:\n"); - for(Index index : _indexes) { - rtn.append(index); - } - rtn.append("\nOwned pages: " + _ownedPages + "\n"); - return rtn.toString(); - } - - /** - * @return A simple String representation of the entire table in - * tab-delimited format - * @usage _general_method_ - */ - public String display() throws IOException { - return display(Long.MAX_VALUE); - } - /** - * @param limit Maximum number of rows to display - * @return A simple String representation of the entire table in - * tab-delimited format + * After calling this method, {@link #getNextRow} will return the first row + * in the table, see {@link Cursor#reset} (uses the {@link #getDefaultCursor + * default cursor}). * @usage _general_method_ */ - public String display(long limit) throws IOException { - reset(); - StringBuilder rtn = new StringBuilder(); - for(Iterator iter = _columns.iterator(); iter.hasNext(); ) { - Column col = iter.next(); - rtn.append(col.getName()); - if (iter.hasNext()) { - rtn.append("\t"); - } - } - rtn.append("\n"); - Map row; - int rowCount = 0; - while ((rowCount++ < limit) && (row = getNextRow()) != null) { - for(Iterator iter = row.values().iterator(); iter.hasNext(); ) { - Object obj = iter.next(); - if (obj instanceof byte[]) { - byte[] b = (byte[]) obj; - rtn.append(ByteUtil.toHexString(b)); - //This block can be used to easily dump a binary column to a file - /*java.io.File f = java.io.File.createTempFile("ole", ".bin"); - java.io.FileOutputStream out = new java.io.FileOutputStream(f); - out.write(b); - out.flush(); - out.close();*/ - } else { - rtn.append(String.valueOf(obj)); - } - if (iter.hasNext()) { - rtn.append("\t"); - } - } - rtn.append("\n"); - } - return rtn.toString(); - } - - /** - * Updates free space and row info for a new row of the given size in the - * given data page. Positions the page for writing the row data. - * @return the row number of the new row - * @usage _advanced_method_ - */ - public static int addDataPageRow(ByteBuffer dataPage, - int rowSize, - JetFormat format, - int rowFlags) - { - int rowSpaceUsage = getRowSpaceUsage(rowSize, format); - - // Decrease free space record. - short freeSpaceInPage = dataPage.getShort(format.OFFSET_FREE_SPACE); - dataPage.putShort(format.OFFSET_FREE_SPACE, (short) (freeSpaceInPage - - rowSpaceUsage)); - - // Increment row count record. - short rowCount = dataPage.getShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE); - dataPage.putShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE, - (short) (rowCount + 1)); - - // determine row position - short rowLocation = findRowEnd(dataPage, rowCount, format); - rowLocation -= rowSize; - - // write row position - dataPage.putShort(getRowStartOffset(rowCount, format), - (short)(rowLocation | rowFlags)); - - // set position for row data - dataPage.position(rowLocation); - - return rowCount; - } - - /** - * Returns the row count for the current page. If the page is invalid - * ({@code null}) or the page is not a DATA page, 0 is returned. - */ - static int getRowsOnDataPage(ByteBuffer rowBuffer, JetFormat format) - throws IOException - { - int rowsOnPage = 0; - if((rowBuffer != null) && (rowBuffer.get(0) == PageTypes.DATA)) { - rowsOnPage = rowBuffer.getShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE); - } - return rowsOnPage; - } - - /** - * @throws IllegalStateException if the given rowId is invalid - */ - private static void requireValidRowId(RowId rowId) { - if(!rowId.isValid()) { - throw new IllegalArgumentException("Given rowId is invalid: " + rowId); - } - } - - /** - * @throws IllegalStateException if the given row is invalid or deleted - */ - private static void requireNonDeletedRow(RowState rowState, RowId rowId) { - if(!rowState.isValid()) { - throw new IllegalArgumentException( - "Given rowId is invalid for this table: " + rowId); - } - if(rowState.isDeleted()) { - throw new IllegalStateException("Row is deleted: " + rowId); - } - } - - /** - * @usage _advanced_method_ - */ - public static boolean isDeletedRow(short rowStart) { - return ((rowStart & DELETED_ROW_MASK) != 0); - } - - /** - * @usage _advanced_method_ - */ - public static boolean isOverflowRow(short rowStart) { - return ((rowStart & OVERFLOW_ROW_MASK) != 0); - } - - /** - * @usage _advanced_method_ - */ - public static short cleanRowStart(short rowStart) { - return (short)(rowStart & OFFSET_MASK); - } - - /** - * @usage _advanced_method_ - */ - public static short findRowStart(ByteBuffer buffer, int rowNum, - JetFormat format) - { - return cleanRowStart( - buffer.getShort(getRowStartOffset(rowNum, format))); - } - - /** - * @usage _advanced_method_ - */ - public static int getRowStartOffset(int rowNum, JetFormat format) - { - return format.OFFSET_ROW_START + (format.SIZE_ROW_LOCATION * rowNum); - } - - /** - * @usage _advanced_method_ - */ - public static short findRowEnd(ByteBuffer buffer, int rowNum, - JetFormat format) - { - return (short)((rowNum == 0) ? - format.PAGE_SIZE : - cleanRowStart( - buffer.getShort(getRowEndOffset(rowNum, format)))); - } - - /** - * @usage _advanced_method_ - */ - public static int getRowEndOffset(int rowNum, JetFormat format) - { - return format.OFFSET_ROW_START + (format.SIZE_ROW_LOCATION * (rowNum - 1)); - } - - /** - * @usage _advanced_method_ - */ - public static int getRowSpaceUsage(int rowSize, JetFormat format) - { - return rowSize + format.SIZE_ROW_LOCATION; - } - - /** - * @return the "AutoNumber" columns in the given collection of columns. - * @usage _advanced_method_ - */ - public static List getAutoNumberColumns(Collection columns) { - List autoCols = new ArrayList(1); - for(Column c : columns) { - if(c.isAutoNumber()) { - autoCols.add(c); - } - } - return (!autoCols.isEmpty() ? autoCols : Collections.emptyList()); - } + public void reset(); /** - * Returns {@code true} if a row of the given size will fit on the given - * data page, {@code false} otherwise. - * @usage _advanced_method_ + * @return The next row in this table (Column name -> Column value) (uses + * the {@link #getDefaultCursor default cursor}) + * @usage _general_method_ */ - public static boolean rowFitsOnDataPage( - int rowLength, ByteBuffer dataPage, JetFormat format) - throws IOException - { - int rowSpaceUsage = getRowSpaceUsage(rowLength, format); - short freeSpaceInPage = dataPage.getShort(format.OFFSET_FREE_SPACE); - int rowsOnPage = getRowsOnDataPage(dataPage, format); - return ((rowSpaceUsage <= freeSpaceInPage) && - (rowsOnPage < format.MAX_NUM_ROWS_ON_DATA_PAGE)); - } + public Row getNextRow() throws IOException; /** - * Duplicates and returns a row of data, optionally with a longer length - * filled with {@code null}. + * @return a simple Cursor, initialized on demand and held by this table. + * This cursor backs the row traversal methods available on the + * Table interface. For advanced Table traversal and manipulation, + * use the Cursor directly. */ - static Object[] dupeRow(Object[] row, int newRowLength) { - Object[] copy = new Object[newRowLength]; - System.arraycopy(row, 0, copy, 0, Math.min(row.length, newRowLength)); - return copy; - } - - /** various statuses for the row data */ - private enum RowStatus { - INIT, INVALID_PAGE, INVALID_ROW, VALID, DELETED, NORMAL, OVERFLOW; - } - - /** the phases the RowState moves through as the data is parsed */ - private enum RowStateStatus { - INIT, AT_HEADER, AT_FINAL; - } + public Cursor getDefaultCursor(); /** - * Maintains the state of reading a row of data. - * @usage _advanced_class_ + * Convenience method for constructing a new CursorBuilder for this Table. */ - public final class RowState - { - /** Buffer used for reading the header row data pages */ - private final TempPageHolder _headerRowBufferH; - /** the header rowId */ - private RowId _headerRowId = RowId.FIRST_ROW_ID; - /** the number of rows on the header page */ - private int _rowsOnHeaderPage; - /** the rowState status */ - private RowStateStatus _status = RowStateStatus.INIT; - /** the row status */ - private RowStatus _rowStatus = RowStatus.INIT; - /** buffer used for reading overflow pages */ - private final TempPageHolder _overflowRowBufferH = - TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); - /** the row buffer which contains the final data (after following any - overflow pointers) */ - private ByteBuffer _finalRowBuffer; - /** the rowId which contains the final data (after following any overflow - pointers) */ - private RowId _finalRowId = null; - /** true if the row values array has data */ - private boolean _haveRowValues; - /** values read from the last row */ - private final Object[] _rowValues; - /** null mask for the last row */ - private NullMask _nullMask; - /** last modification count seen on the table we track this so that the - 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; - /** cached variable column offsets for jump-table based rows */ - private short[] _varColOffsets; - - private RowState(TempBufferHolder.Type headerType) { - _headerRowBufferH = TempPageHolder.newHolder(headerType); - _rowValues = new Object[Table.this.getColumnCount()]; - _lastModCount = Table.this._modCount; - } - - 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; - _finalRowBuffer = null; - _rowsOnHeaderPage = 0; - _status = RowStateStatus.INIT; - _rowStatus = RowStatus.INIT; - _varColOffsets = null; - _nullMask = null; - if(_haveRowValues) { - Arrays.fill(_rowValues, null); - _haveRowValues = false; - } - } - - public boolean isUpToDate() { - return(Table.this._modCount == _lastModCount); - } - - private void checkForModification() { - if(!isUpToDate()) { - reset(); - _headerRowBufferH.invalidate(); - _overflowRowBufferH.invalidate(); - _lastModCount = Table.this._modCount; - } - } - - private ByteBuffer getFinalPage() - throws IOException - { - if(_finalRowBuffer == null) { - // (re)load current page - _finalRowBuffer = getHeaderPage(); - } - return _finalRowBuffer; - } - - public RowId getFinalRowId() { - if(_finalRowId == null) { - _finalRowId = getHeaderRowId(); - } - return _finalRowId; - } - - private void setRowStatus(RowStatus rowStatus) { - _rowStatus = rowStatus; - } - - public boolean isValid() { - return(_rowStatus.ordinal() >= RowStatus.VALID.ordinal()); - } - - public boolean isDeleted() { - return(_rowStatus == RowStatus.DELETED); - } - - public boolean isOverflow() { - return(_rowStatus == RowStatus.OVERFLOW); - } - - public boolean isHeaderPageNumberValid() { - return(_rowStatus.ordinal() > RowStatus.INVALID_PAGE.ordinal()); - } - - public boolean isHeaderRowNumberValid() { - return(_rowStatus.ordinal() > RowStatus.INVALID_ROW.ordinal()); - } - - private void setStatus(RowStateStatus status) { - _status = status; - } - - public boolean isAtHeaderRow() { - return(_status.ordinal() >= RowStateStatus.AT_HEADER.ordinal()); - } - - public boolean isAtFinalRow() { - return(_status.ordinal() >= RowStateStatus.AT_FINAL.ordinal()); - } - - private Object setRowValue(int idx, Object value) { - _haveRowValues = true; - _rowValues[idx] = value; - return value; - } - - public Object[] getRowValues() { - return dupeRow(_rowValues, _rowValues.length); - } - - public NullMask getNullMask(ByteBuffer rowBuffer) throws IOException { - if(_nullMask == null) { - _nullMask = getRowNullMask(rowBuffer); - } - return _nullMask; - } - - private short[] getVarColOffsets() { - return _varColOffsets; - } - - private void setVarColOffsets(short[] varColOffsets) { - _varColOffsets = varColOffsets; - } - - public RowId getHeaderRowId() { - return _headerRowId; - } - - public int getRowsOnHeaderPage() { - return _rowsOnHeaderPage; - } - - private ByteBuffer getHeaderPage() - throws IOException - { - checkForModification(); - return _headerRowBufferH.getPage(getPageChannel()); - } - - private ByteBuffer setHeaderRow(RowId rowId) - throws IOException - { - checkForModification(); - - // don't do any work if we are already positioned correctly - if(isAtHeaderRow() && (getHeaderRowId().equals(rowId))) { - return(isValid() ? getHeaderPage() : null); - } - - // rejigger everything - reset(); - _headerRowId = rowId; - _finalRowId = rowId; - - int pageNumber = rowId.getPageNumber(); - int rowNumber = rowId.getRowNumber(); - if((pageNumber < 0) || !_ownedPages.containsPageNumber(pageNumber)) { - setRowStatus(RowStatus.INVALID_PAGE); - return null; - } - - _finalRowBuffer = _headerRowBufferH.setPage(getPageChannel(), - pageNumber); - _rowsOnHeaderPage = getRowsOnDataPage(_finalRowBuffer, getFormat()); - - if((rowNumber < 0) || (rowNumber >= _rowsOnHeaderPage)) { - setRowStatus(RowStatus.INVALID_ROW); - return null; - } - - setRowStatus(RowStatus.VALID); - return _finalRowBuffer; - } - - private ByteBuffer setOverflowRow(RowId rowId) - throws IOException - { - // this should never see modifications because it only happens within - // the positionAtRowData method - if(!isUpToDate()) { - throw new IllegalStateException("Table modified while searching?"); - } - if(_rowStatus != RowStatus.OVERFLOW) { - throw new IllegalStateException("Row is not an overflow row?"); - } - _finalRowId = rowId; - _finalRowBuffer = _overflowRowBufferH.setPage(getPageChannel(), - rowId.getPageNumber()); - 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; - } - } - + public CursorBuilder newCursor(); } diff --git a/src/java/com/healthmarketscience/jackcess/TableBuilder.java b/src/java/com/healthmarketscience/jackcess/TableBuilder.java index 51e8697..9530f51 100644 --- a/src/java/com/healthmarketscience/jackcess/TableBuilder.java +++ b/src/java/com/healthmarketscience/jackcess/TableBuilder.java @@ -29,7 +29,13 @@ package com.healthmarketscience.jackcess; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; + +import com.healthmarketscience.jackcess.impl.DatabaseImpl; /** * Builder style class for constructing a Column. @@ -38,10 +44,54 @@ import java.util.List; */ public class TableBuilder { + /** Prefix for column or table names that are reserved words */ + private static final String ESCAPE_PREFIX = "x"; + + /* nested class for lazy loading */ + private static final class ReservedWords { + /** + * All of the reserved words in Access that should be escaped when creating + * table or column names + */ + private static final Set VALUES = + new HashSet(Arrays.asList( + "add", "all", "alphanumeric", "alter", "and", "any", "application", "as", + "asc", "assistant", "autoincrement", "avg", "between", "binary", "bit", + "boolean", "by", "byte", "char", "character", "column", "compactdatabase", + "constraint", "container", "count", "counter", "create", "createdatabase", + "createfield", "creategroup", "createindex", "createobject", "createproperty", + "createrelation", "createtabledef", "createuser", "createworkspace", + "currency", "currentuser", "database", "date", "datetime", "delete", + "desc", "description", "disallow", "distinct", "distinctrow", "document", + "double", "drop", "echo", "else", "end", "eqv", "error", "exists", "exit", + "false", "field", "fields", "fillcache", "float", "float4", "float8", + "foreign", "form", "forms", "from", "full", "function", "general", + "getobject", "getoption", "gotopage", "group", "group by", "guid", "having", + "idle", "ieeedouble", "ieeesingle", "if", "ignore", "imp", "in", "index", + "indexes", "inner", "insert", "inserttext", "int", "integer", "integer1", + "integer2", "integer4", "into", "is", "join", "key", "lastmodified", "left", + "level", "like", "logical", "logical1", "long", "longbinary", "longtext", + "macro", "match", "max", "min", "mod", "memo", "module", "money", "move", + "name", "newpassword", "no", "not", "null", "number", "numeric", "object", + "oleobject", "off", "on", "openrecordset", "option", "or", "order", "outer", + "owneraccess", "parameter", "parameters", "partial", "percent", "pivot", + "primary", "procedure", "property", "queries", "query", "quit", "real", + "recalc", "recordset", "references", "refresh", "refreshlink", + "registerdatabase", "relation", "repaint", "repairdatabase", "report", + "reports", "requery", "right", "screen", "section", "select", "set", + "setfocus", "setoption", "short", "single", "smallint", "some", "sql", + "stdev", "stdevp", "string", "sum", "table", "tabledef", "tabledefs", + "tableid", "text", "time", "timestamp", "top", "transform", "true", "type", + "union", "unique", "update", "user", "value", "values", "var", "varp", + "varbinary", "varchar", "where", "with", "workspace", "xor", "year", "yes", + "yesno")); + } + + /** name of the new table */ private String _name; /** columns for the new table */ - private List _columns = new ArrayList(); + private List _columns = new ArrayList(); /** indexes for the new table */ private List _indexes = new ArrayList(); /** whether or not table/column/index names are automatically escaped */ @@ -56,7 +106,7 @@ public class TableBuilder { _name = name; _escapeIdentifiers = escapeIdentifiers; if(_escapeIdentifiers) { - _name = Database.escapeIdentifier(_name); + _name = escapeIdentifier(_name); } } @@ -64,19 +114,24 @@ public class TableBuilder { /** * Adds a Column to the new table. */ - public TableBuilder addColumn(Column column) { + public TableBuilder addColumn(ColumnBuilder column) { if(_escapeIdentifiers) { - column.setName(Database.escapeIdentifier(column.getName())); + column.escapeName(); } _columns.add(column); return this; } /** - * Adds a Column to the new table. + * Adds the Columns to the new table. */ - public TableBuilder addColumn(ColumnBuilder columnBuilder) { - return addColumn(columnBuilder.toColumn()); + public TableBuilder addColumns(Collection columns) { + if(columns != null) { + for(ColumnBuilder col : columns) { + addColumn(col); + } + } + return this; } /** @@ -84,9 +139,9 @@ public class TableBuilder { */ public TableBuilder addIndex(IndexBuilder index) { if(_escapeIdentifiers) { - index.setName(Database.escapeIdentifier(index.getName())); + index.setName(escapeIdentifier(index.getName())); for(IndexBuilder.Column col : index.getColumns()) { - col.setName(Database.escapeIdentifier(col.getName())); + col.setName(escapeIdentifier(col.getName())); } } _indexes.add(index); @@ -113,11 +168,10 @@ public class TableBuilder { } /** - * Escapes the new table's name using {@link Database#escapeIdentifier}. + * Escapes the new table's name using {@link TableBuilder#escapeIdentifier}. */ - public TableBuilder escapeName() - { - _name = Database.escapeIdentifier(_name); + public TableBuilder escapeName() { + _name = escapeIdentifier(_name); return this; } @@ -128,8 +182,29 @@ public class TableBuilder { public Table toTable(Database db) throws IOException { - db.createTable(_name, _columns, _indexes); + ((DatabaseImpl)db).createTable(_name, _columns, _indexes); return db.getTable(_name); } + + /** + * @return A table or column name escaped for Access + * @usage _general_method_ + */ + public static String escapeIdentifier(String s) { + if (isReservedWord(s)) { + return ESCAPE_PREFIX + s; + } + return s; + } + + /** + * @return {@code true} if the given string is a reserved word, + * {@code false} otherwise + * @usage _general_method_ + */ + public static boolean isReservedWord(String s) { + return ReservedWords.VALUES.contains(s.toLowerCase()); + } + } diff --git a/src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java index ec16d07..f2f605a 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java +++ b/src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2011 James Ahlborn +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -19,501 +19,12 @@ USA package com.healthmarketscience.jackcess.complex; -import java.io.ByteArrayInputStream; -import java.io.DataInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.zip.Deflater; -import java.util.zip.DeflaterOutputStream; -import java.util.zip.InflaterInputStream; - -import com.healthmarketscience.jackcess.ByteUtil; -import com.healthmarketscience.jackcess.Column; -import com.healthmarketscience.jackcess.JetFormat; -import com.healthmarketscience.jackcess.PageChannel; -import com.healthmarketscience.jackcess.Table; - - /** * Complex column info for a column holding 0 or more attachments per row. * * @author James Ahlborn */ -public class AttachmentColumnInfo extends ComplexColumnInfo +public interface AttachmentColumnInfo extends ComplexColumnInfo { - /** some file formats which may not be worth re-compressing */ - private static final Set COMPRESSED_FORMATS = new HashSet( - Arrays.asList("jpg", "zip", "gz", "bz2", "z", "7z", "cab", "rar", - "mp3", "mpg")); - - private static final String FILE_NAME_COL_NAME = "FileName"; - private static final String FILE_TYPE_COL_NAME = "FileType"; - - private static final int DATA_TYPE_RAW = 0; - private static final int DATA_TYPE_COMPRESSED = 1; - - private static final int UNKNOWN_HEADER_VAL = 1; - private static final int WRAPPER_HEADER_SIZE = 8; - private static final int CONTENT_HEADER_SIZE = 12; - - private final Column _fileUrlCol; - private final Column _fileNameCol; - private final Column _fileTypeCol; - private final Column _fileDataCol; - private final Column _fileTimeStampCol; - private final Column _fileFlagsCol; - - public AttachmentColumnInfo(Column column, int complexId, - Table typeObjTable, Table flatTable) - throws IOException - { - super(column, complexId, typeObjTable, flatTable); - - Column fileUrlCol = null; - Column fileNameCol = null; - Column fileTypeCol = null; - Column fileDataCol = null; - Column fileTimeStampCol = null; - Column fileFlagsCol = null; - - for(Column col : getTypeColumns()) { - switch(col.getType()) { - case TEXT: - if(FILE_NAME_COL_NAME.equalsIgnoreCase(col.getName())) { - fileNameCol = col; - } else if(FILE_TYPE_COL_NAME.equalsIgnoreCase(col.getName())) { - fileTypeCol = col; - } else { - // if names don't match, assign in order: name, type - if(fileNameCol == null) { - fileNameCol = col; - } else if(fileTypeCol == null) { - fileTypeCol = col; - } - } - break; - case LONG: - fileFlagsCol = col; - break; - case SHORT_DATE_TIME: - fileTimeStampCol = col; - break; - case OLE: - fileDataCol = col; - break; - case MEMO: - fileUrlCol = col; - break; - default: - // ignore - } - } - - _fileUrlCol = fileUrlCol; - _fileNameCol = fileNameCol; - _fileTypeCol = fileTypeCol; - _fileDataCol = fileDataCol; - _fileTimeStampCol = fileTimeStampCol; - _fileFlagsCol = fileFlagsCol; - } - - public Column getFileUrlColumn() { - return _fileUrlCol; - } - - public Column getFileNameColumn() { - return _fileNameCol; - } - - public Column getFileTypeColumn() { - return _fileTypeCol; - } - - public Column getFileDataColumn() { - return _fileDataCol; - } - - public Column getFileTimeStampColumn() { - return _fileTimeStampCol; - } - - public Column getFileFlagsColumn() { - return _fileFlagsCol; - } - - @Override - public ComplexDataType getType() - { - return ComplexDataType.ATTACHMENT; - } - - @Override - protected AttachmentImpl toValue(ComplexValueForeignKey complexValueFk, - Map rawValue) { - int id = (Integer)getPrimaryKeyColumn().getRowValue(rawValue); - String url = (String)getFileUrlColumn().getRowValue(rawValue); - String name = (String)getFileNameColumn().getRowValue(rawValue); - String type = (String)getFileTypeColumn().getRowValue(rawValue); - Integer flags = (Integer)getFileFlagsColumn().getRowValue(rawValue); - Date ts = (Date)getFileTimeStampColumn().getRowValue(rawValue); - byte[] data = (byte[])getFileDataColumn().getRowValue(rawValue); - - return new AttachmentImpl(id, complexValueFk, url, name, type, null, - ts, flags, data); - } - - @Override - protected Object[] asRow(Object[] row, Attachment attachment) - throws IOException - { - super.asRow(row, attachment); - getFileUrlColumn().setRowValue(row, attachment.getFileUrl()); - getFileNameColumn().setRowValue(row, attachment.getFileName()); - getFileTypeColumn().setRowValue(row, attachment.getFileType()); - getFileFlagsColumn().setRowValue(row, attachment.getFileFlags()); - getFileTimeStampColumn().setRowValue(row, attachment.getFileTimeStamp()); - getFileDataColumn().setRowValue(row, attachment.getEncodedFileData()); - return row; - } - - public static Attachment newAttachment(byte[] data) { - return newAttachment(INVALID_COMPLEX_VALUE_ID, data); - } - - public static Attachment newAttachment(ComplexValueForeignKey complexValueFk, - byte[] data) { - return newAttachment(complexValueFk, null, null, null, data, null, null); - } - - public static Attachment newAttachment( - String url, String name, String type, byte[] data, - Date timeStamp, Integer flags) - { - return newAttachment(INVALID_COMPLEX_VALUE_ID, url, name, type, data, - timeStamp, flags); - } - - public static Attachment newAttachment( - ComplexValueForeignKey complexValueFk, String url, String name, - String type, byte[] data, Date timeStamp, Integer flags) - { - return new AttachmentImpl(INVALID_ID, complexValueFk, url, name, type, - data, timeStamp, flags, null); - } - - public static Attachment newEncodedAttachment(byte[] encodedData) { - return newEncodedAttachment(INVALID_COMPLEX_VALUE_ID, encodedData); - } - - public static Attachment newEncodedAttachment( - ComplexValueForeignKey complexValueFk, byte[] encodedData) { - return newEncodedAttachment(complexValueFk, null, null, null, encodedData, - null, null); - } - - public static Attachment newEncodedAttachment( - String url, String name, String type, byte[] encodedData, - Date timeStamp, Integer flags) - { - return newEncodedAttachment(INVALID_COMPLEX_VALUE_ID, url, name, type, - encodedData, timeStamp, flags); - } - - public static Attachment newEncodedAttachment( - ComplexValueForeignKey complexValueFk, String url, String name, - String type, byte[] encodedData, Date timeStamp, Integer flags) - { - return new AttachmentImpl(INVALID_ID, complexValueFk, url, name, type, - null, timeStamp, flags, encodedData); - } - - - public static boolean isAttachmentColumn(Table typeObjTable) { - // attachment data has these columns FileURL(MEMO), FileName(TEXT), - // FileType(TEXT), FileData(OLE), FileTimeStamp(SHORT_DATE_TIME), - // FileFlags(LONG) - List typeCols = typeObjTable.getColumns(); - if(typeCols.size() < 6) { - return false; - } - - int numMemo = 0; - int numText = 0; - int numDate = 0; - int numOle= 0; - int numLong = 0; - - for(Column col : typeCols) { - switch(col.getType()) { - case TEXT: - ++numText; - break; - case LONG: - ++numLong; - break; - case SHORT_DATE_TIME: - ++numDate; - break; - case OLE: - ++numOle; - break; - case MEMO: - ++numMemo; - break; - default: - // ignore - } - } - - // be flexible, allow for extra columns... - return((numMemo >= 1) && (numText >= 2) && (numOle >= 1) && - (numDate >= 1) && (numLong >= 1)); - } - - - private static class AttachmentImpl extends ComplexValueImpl - implements Attachment - { - private String _url; - private String _name; - private String _type; - private byte[] _data; - private Date _timeStamp; - private Integer _flags; - private byte[] _encodedData; - - private AttachmentImpl(int id, ComplexValueForeignKey complexValueFk, - String url, String name, String type, byte[] data, - Date timeStamp, Integer flags, byte[] encodedData) - { - super(id, complexValueFk); - _url = url; - _name = name; - _type = type; - _data = data; - _timeStamp = timeStamp; - _flags = flags; - _encodedData = encodedData; - } - - public byte[] getFileData() throws IOException { - if((_data == null) && (_encodedData != null)) { - _data = decodeData(); - } - return _data; - } - - public void setFileData(byte[] data) { - _data = data; - _encodedData = null; - } - - public byte[] getEncodedFileData() throws IOException { - if((_encodedData == null) && (_data != null)) { - _encodedData = encodeData(); - } - return _encodedData; - } - - public void setEncodedFileData(byte[] data) { - _encodedData = data; - _data = null; - } - - public String getFileName() { - return _name; - } - - public void setFileName(String fileName) { - _name = fileName; - } - - public String getFileUrl() { - return _url; - } - - public void setFileUrl(String fileUrl) { - _url = fileUrl; - } - - public String getFileType() { - return _type; - } - - public void setFileType(String fileType) { - _type = fileType; - } - - public Date getFileTimeStamp() { - return _timeStamp; - } - - public void setFileTimeStamp(Date fileTimeStamp) { - _timeStamp = fileTimeStamp; - } - - public Integer getFileFlags() { - return _flags; - } - - public void setFileFlags(Integer fileFlags) { - _flags = fileFlags; - } - - public void update() throws IOException { - getComplexValueForeignKey().updateAttachment(this); - } - - public void delete() throws IOException { - getComplexValueForeignKey().deleteAttachment(this); - } - - @Override - public String toString() { - - String dataStr = null; - try { - dataStr = ByteUtil.toHexString(getFileData()); - } catch(IOException e) { - dataStr = e.toString(); - } - - return "Attachment(" + getComplexValueForeignKey() + "," + getId() + - ") " + getFileUrl() + ", " + getFileName() + ", " + getFileType() - + ", " + getFileTimeStamp() + ", " + getFileFlags() + ", " + - dataStr; - } - - /** - * Decodes the raw attachment file data to get the _actual_ content. - */ - private byte[] decodeData() throws IOException { - - if(_encodedData.length < WRAPPER_HEADER_SIZE) { - // nothing we can do - throw new IOException("Unknown encoded attachment data format"); - } - - // read initial header info - ByteBuffer bb = PageChannel.wrap(_encodedData); - int typeFlag = bb.getInt(); - int dataLen = bb.getInt(); - - DataInputStream contentStream = null; - try { - InputStream bin = new ByteArrayInputStream( - _encodedData, WRAPPER_HEADER_SIZE, - _encodedData.length - WRAPPER_HEADER_SIZE); - - if(typeFlag == DATA_TYPE_RAW) { - // nothing else to do - } else if(typeFlag == DATA_TYPE_COMPRESSED) { - // actual content is deflate compressed - bin = new InflaterInputStream(bin); - } else { - throw new IOException( - "Unknown encoded attachment data type " + typeFlag); - } - - contentStream = new DataInputStream(bin); - - // header is an unknown flag followed by the "file extension" of the - // data (no clue why we need that again since it's already a separate - // field in the attachment table). just skip all of it - byte[] tmpBytes = new byte[4]; - contentStream.readFully(tmpBytes); - int headerLen = PageChannel.wrap(tmpBytes).getInt(); - contentStream.skipBytes(headerLen - 4); - - // calculate actual data length and read it (note, header length - // includes the bytes for the length) - tmpBytes = new byte[dataLen - headerLen]; - contentStream.readFully(tmpBytes); - - return tmpBytes; - - } finally { - if(contentStream != null) { - try { - contentStream.close(); - } catch(IOException e) { - // ignored - } - } - } - } - - /** - * Encodes the actual attachment file data to get the raw, stored format. - */ - private byte[] encodeData() throws IOException { - - // possibly compress data based on file type - String type = ((_type != null) ? _type.toLowerCase() : ""); - boolean shouldCompress = !COMPRESSED_FORMATS.contains(type); - - // encode extension, which ends w/ a null byte - type += '\0'; - ByteBuffer typeBytes = Column.encodeUncompressedText( - type, JetFormat.VERSION_12.CHARSET); - int headerLen = typeBytes.remaining() + CONTENT_HEADER_SIZE; - - int dataLen = _data.length; - ByteUtil.ByteStream dataStream = new ByteUtil.ByteStream( - WRAPPER_HEADER_SIZE + headerLen + dataLen); - - // write the wrapper header info - ByteBuffer bb = PageChannel.wrap(dataStream.getBytes()); - bb.putInt(shouldCompress ? DATA_TYPE_COMPRESSED : DATA_TYPE_RAW); - bb.putInt(dataLen + headerLen); - dataStream.skip(WRAPPER_HEADER_SIZE); - - OutputStream contentStream = dataStream; - Deflater deflater = null; - try { - - if(shouldCompress) { - contentStream = new DeflaterOutputStream( - contentStream, deflater = new Deflater(3)); - } - - // write the header w/ the file extension - byte[] tmpBytes = new byte[CONTENT_HEADER_SIZE]; - PageChannel.wrap(tmpBytes) - .putInt(headerLen) - .putInt(UNKNOWN_HEADER_VAL) - .putInt(type.length()); - contentStream.write(tmpBytes); - contentStream.write(typeBytes.array(), 0, typeBytes.remaining()); - - // write the _actual_ contents - contentStream.write(_data); - contentStream.close(); - contentStream = null; - - return dataStream.toByteArray(); - - } finally { - if(contentStream != null) { - try { - contentStream.close(); - } catch(IOException e) { - // ignored - } - } - if(deflater != null) { - deflater.end(); - } - } - } - } } diff --git a/src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java index 3dac47c..14851f6 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java +++ b/src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2011 James Ahlborn +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -20,436 +20,55 @@ USA package com.healthmarketscience.jackcess.complex; import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; import java.util.List; import java.util.Map; -import com.healthmarketscience.jackcess.Column; -import com.healthmarketscience.jackcess.CursorBuilder; -import com.healthmarketscience.jackcess.DataType; -import com.healthmarketscience.jackcess.Database; -import com.healthmarketscience.jackcess.IndexCursor; -import com.healthmarketscience.jackcess.JetFormat; -import com.healthmarketscience.jackcess.PageChannel; -import com.healthmarketscience.jackcess.Table; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import com.healthmarketscience.jackcess.Row; /** * Base class for the additional information tracked for complex columns. * * @author James Ahlborn */ -public abstract class ComplexColumnInfo +public interface ComplexColumnInfo { - private static final Log LOG = LogFactory.getLog(Column.class); + public ComplexDataType getType(); - public static final int INVALID_ID = -1; - public static final ComplexValueForeignKey INVALID_COMPLEX_VALUE_ID = - new ComplexValueForeignKey(null, INVALID_ID); - - private static final String COL_COMPLEX_TYPE_OBJECT_ID = "ComplexTypeObjectID"; - private static final String COL_TABLE_ID = "ConceptualTableID"; - private static final String COL_FLAT_TABLE_ID = "FlatTableID"; + public int countValues(int complexValueFk) throws IOException; - private final Column _column; - private final int _complexTypeId; - private final Table _flatTable; - private final List _typeCols; - private final Column _pkCol; - private final Column _complexValFkCol; - private IndexCursor _pkCursor; - private IndexCursor _complexValIdCursor; - - protected ComplexColumnInfo(Column column, int complexTypeId, - Table typeObjTable, Table flatTable) - throws IOException - { - _column = column; - _complexTypeId = complexTypeId; - _flatTable = flatTable; - - // the flat table has all the "value" columns and 2 extra columns, a - // primary key for each row, and a LONG value which is essentially a - // foreign key to the main table. - List typeCols = new ArrayList(); - List otherCols = new ArrayList(); - diffFlatColumns(typeObjTable, flatTable, typeCols, otherCols); - - _typeCols = Collections.unmodifiableList(typeCols); - - Column pkCol = null; - Column complexValFkCol = null; - for(Column col : otherCols) { - if(col.isAutoNumber()) { - pkCol = col; - } else if(col.getType() == DataType.LONG) { - complexValFkCol = col; - } - } - - if((pkCol == null) || (complexValFkCol == null)) { - throw new IOException("Could not find expected columns in flat table " + - flatTable.getName() + " for complex column with id " - + complexTypeId); - } - _pkCol = pkCol; - _complexValFkCol = complexValFkCol; - } - - public static ComplexColumnInfo create( - Column column, ByteBuffer buffer, int offset) - throws IOException - { - int complexTypeId = buffer.getInt( - offset + column.getFormat().OFFSET_COLUMN_COMPLEX_ID); - - Database db = column.getDatabase(); - Table complexColumns = db.getSystemComplexColumns(); - IndexCursor cursor = IndexCursor.createCursor( - complexColumns, complexColumns.getPrimaryKeyIndex()); - if(!cursor.findFirstRowByEntry(complexTypeId)) { - throw new IOException( - "Could not find complex column info for complex column with id " + - complexTypeId); - } - Map cColRow = cursor.getCurrentRow(); - int tableId = (Integer)cColRow.get(COL_TABLE_ID); - if(tableId != column.getTable().getTableDefPageNumber()) { - throw new IOException( - "Found complex column for table " + tableId + " but expected table " + - column.getTable().getTableDefPageNumber()); - } - int flatTableId = (Integer)cColRow.get(COL_FLAT_TABLE_ID); - int typeObjId = (Integer)cColRow.get(COL_COMPLEX_TYPE_OBJECT_ID); - - Table typeObjTable = db.getTable(typeObjId); - Table flatTable = db.getTable(flatTableId); - - if((typeObjTable == null) || (flatTable == null)) { - throw new IOException( - "Could not find supporting tables (" + typeObjId + ", " + flatTableId - + ") for complex column with id " + complexTypeId); - } - - // we inspect the structore of the "type table" to determine what kind of - // complex info we are dealing with - if(MultiValueColumnInfo.isMultiValueColumn(typeObjTable)) { - return new MultiValueColumnInfo(column, complexTypeId, typeObjTable, - flatTable); - } else if(AttachmentColumnInfo.isAttachmentColumn(typeObjTable)) { - return new AttachmentColumnInfo(column, complexTypeId, typeObjTable, - flatTable); - } else if(VersionHistoryColumnInfo.isVersionHistoryColumn(typeObjTable)) { - return new VersionHistoryColumnInfo(column, complexTypeId, typeObjTable, - flatTable); - } - - LOG.warn("Unsupported complex column type " + typeObjTable.getName()); - return new UnsupportedColumnInfo(column, complexTypeId, typeObjTable, - flatTable); - } - - public void postTableLoadInit() throws IOException { - // nothing to do in base class - } - - public Column getColumn() { - return _column; - } - - public Database getDatabase() { - return getColumn().getDatabase(); - } - - public JetFormat getFormat() { - return getDatabase().getFormat(); - } - - public PageChannel getPageChannel() { - return getDatabase().getPageChannel(); - } - - public Column getPrimaryKeyColumn() { - return _pkCol; - } - - public Column getComplexValueForeignKeyColumn() { - return _complexValFkCol; - } - - protected List getTypeColumns() { - return _typeCols; - } - - public int countValues(int complexValueFk) throws IOException { - return getRawValues(complexValueFk, - Collections.singleton(_complexValFkCol.getName())) - .size(); - } - - public List> getRawValues(int complexValueFk) - throws IOException - { - return getRawValues(complexValueFk, null); - } - - private Iterator> getComplexValFkIter( - int complexValueFk, Collection columnNames) - throws IOException - { - if(_complexValIdCursor == null) { - _complexValIdCursor = new CursorBuilder(_flatTable) - .setIndexByColumns(_complexValFkCol) - .toIndexCursor(); - } - - return _complexValIdCursor.entryIterator(columnNames, complexValueFk); - } - - public List> getRawValues(int complexValueFk, - Collection columnNames) - throws IOException - { - Iterator> entryIter = - getComplexValFkIter(complexValueFk, columnNames); - if(!entryIter.hasNext()) { - return Collections.emptyList(); - } + public List getRawValues(int complexValueFk) + throws IOException; - List> values = new ArrayList>(); - while(entryIter.hasNext()) { - values.add(entryIter.next()); - } - - return values; - } + public List getRawValues(int complexValueFk, + Collection columnNames) + throws IOException; public List getValues(ComplexValueForeignKey complexValueFk) - throws IOException - { - List> rawValues = getRawValues(complexValueFk.get()); - if(rawValues.isEmpty()) { - return Collections.emptyList(); - } + throws IOException; - return toValues(complexValueFk, rawValues); - } - - protected List toValues(ComplexValueForeignKey complexValueFk, - List> rawValues) - throws IOException - { - List values = new ArrayList(); - for(Map rawValue : rawValues) { - values.add(toValue(complexValueFk, rawValue)); - } + public ComplexValue.Id addRawValue(Map rawValue) + throws IOException; - return values; - } + public ComplexValue.Id addValue(V value) throws IOException; - public int addRawValue(Map rawValue) throws IOException { - Object[] row = _flatTable.asRow(rawValue); - _flatTable.addRow(row); - return (Integer)_pkCol.getRowValue(row); - } + public void addValues(Collection values) throws IOException; - public int addValue(V value) throws IOException { - Object[] row = asRow(newRowArray(), value); - _flatTable.addRow(row); - int id = (Integer)_pkCol.getRowValue(row); - value.setId(id); - return id; - } + public ComplexValue.Id updateRawValue(Row rawValue) throws IOException; - public void addValues(Collection values) throws IOException { - for(V value : values) { - addValue(value); - } - } + public ComplexValue.Id updateValue(V value) throws IOException; - public int updateRawValue(Map rawValue) throws IOException { - Integer id = (Integer)_pkCol.getRowValue(rawValue); - updateRow(id, _flatTable.asUpdateRow(rawValue)); - return id; - } - - public int updateValue(V value) throws IOException { - int id = value.getId(); - updateRow(id, asRow(newRowArray(), value)); - return id; - } + public void updateValues(Collection values) throws IOException; - public void updateValues(Collection values) throws IOException { - for(V value : values) { - updateValue(value); - } - } + public void deleteRawValue(Row rawValue) throws IOException; - public void deleteRawValue(Map rawValue) throws IOException { - deleteRow((Integer)_pkCol.getRowValue(rawValue)); - } - - public void deleteValue(V value) throws IOException { - deleteRow(value.getId()); - } + public void deleteValue(V value) throws IOException; - public void deleteValues(Collection values) throws IOException { - for(V value : values) { - deleteValue(value); - } - } + public void deleteValues(Collection values) throws IOException; - public void deleteAllValues(int complexValueFk) throws IOException { - Iterator> entryIter = - getComplexValFkIter(complexValueFk, Collections.emptySet()); - try { - while(entryIter.hasNext()) { - entryIter.next(); - entryIter.remove(); - } - } catch(RuntimeException e) { - if(e.getCause() instanceof IOException) { - throw (IOException)e.getCause(); - } - throw e; - } - } + public void deleteAllValues(int complexValueFk) throws IOException; public void deleteAllValues(ComplexValueForeignKey complexValueFk) - throws IOException - { - deleteAllValues(complexValueFk.get()); - } - - private void moveToRow(Integer id) throws IOException { - if(_pkCursor == null) { - _pkCursor = new CursorBuilder(_flatTable) - .setIndexByColumns(_pkCol) - .toIndexCursor(); - } - - if(!_pkCursor.findFirstRowByEntry(id)) { - throw new IllegalArgumentException("Row with id " + id + - " does not exist"); - } - } - - private void updateRow(Integer id, Object[] row) throws IOException { - moveToRow(id); - _pkCursor.updateCurrentRow(row); - } - - private void deleteRow(Integer id) throws IOException { - moveToRow(id); - _pkCursor.deleteCurrentRow(); - } - - protected Object[] asRow(Object[] row, V value) - throws IOException - { - int id = value.getId(); - _pkCol.setRowValue(row, ((id != INVALID_ID) ? id : Column.AUTO_NUMBER)); - int cId = value.getComplexValueForeignKey().get(); - _complexValFkCol.setRowValue( - row, ((cId != INVALID_ID) ? cId : Column.AUTO_NUMBER)); - return row; - } - - private Object[] newRowArray() { - return new Object[_flatTable.getColumnCount()]; - } - - @Override - public String toString() { - StringBuilder rtn = new StringBuilder(); - rtn.append("\n\t\tComplexType: " + getType()); - rtn.append("\n\t\tComplexTypeId: " + _complexTypeId); - return rtn.toString(); - } - - protected static void diffFlatColumns(Table typeObjTable, Table flatTable, - List typeCols, - List otherCols) - { - // each "flat"" table has the columns from the "type" table, plus some - // others. separate the "flat" columns into these 2 buckets - for(Column col : flatTable.getColumns()) { - boolean found = false; - try { - typeObjTable.getColumn(col.getName()); - found = true; - } catch(IllegalArgumentException e) { - // FIXME better way to test this? - } - if(found) { - typeCols.add(col); - } else { - otherCols.add(col); - } - } - } - - public abstract ComplexDataType getType(); - - protected abstract V toValue( - ComplexValueForeignKey complexValueFk, - Map rawValues) throws IOException; - - protected static abstract class ComplexValueImpl implements ComplexValue - { - private int _id; - private ComplexValueForeignKey _complexValueFk; - - protected ComplexValueImpl(int id, ComplexValueForeignKey complexValueFk) { - _id = id; - _complexValueFk = complexValueFk; - } - - public int getId() { - return _id; - } - - public void setId(int id) { - if(_id != INVALID_ID) { - throw new IllegalStateException("id may not be reset"); - } - _id = id; - } - - public ComplexValueForeignKey getComplexValueForeignKey() { - return _complexValueFk; - } - - public void setComplexValueForeignKey(ComplexValueForeignKey complexValueFk) - { - if(_complexValueFk != INVALID_COMPLEX_VALUE_ID) { - throw new IllegalStateException("complexValueFk may not be reset"); - } - _complexValueFk = complexValueFk; - } - - public Column getColumn() { - return _complexValueFk.getColumn(); - } - - @Override - public int hashCode() { - return ((_id * 37) ^ _complexValueFk.hashCode()); - } - @Override - public boolean equals(Object o) { - return ((this == o) || - ((o != null) && (getClass() == o.getClass()) && - (_id == ((ComplexValueImpl)o)._id) && - _complexValueFk.equals(((ComplexValueImpl)o)._complexValueFk))); - } - } - } diff --git a/src/java/com/healthmarketscience/jackcess/complex/ComplexValue.java b/src/java/com/healthmarketscience/jackcess/complex/ComplexValue.java index 4e6f19a..29b62d3 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/ComplexValue.java +++ b/src/java/com/healthmarketscience/jackcess/complex/ComplexValue.java @@ -20,11 +20,14 @@ USA package com.healthmarketscience.jackcess.complex; import java.io.IOException; +import java.io.ObjectStreamException; import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.RowId; +import com.healthmarketscience.jackcess.impl.complex.ComplexColumnInfoImpl; /** - * Base class for a value in a complex column (where there may be multiple + * Base interface for a value in a complex column (where there may be multiple * values for a single row in the main table). * * @author James Ahlborn @@ -35,18 +38,22 @@ public interface ComplexValue * Returns the unique identifier of this complex value (this value is unique * among all values in all rows of the main table). * - * @return the current id or {@link ComplexColumnInfo#INVALID_ID} for a new, + * @return the current id or {@link ComplexColumnInfoImpl#INVALID_ID} for a new, * unsaved value. */ - public int getId(); + public Id getId(); - public void setId(int newId); + /** + * Called once when a new ComplexValue is saved to set the new unique + * identifier. + */ + public void setId(Id newId); /** * Returns the foreign key identifier for this complex value (this value is * the same for all values in the same row of the main table). * - * @return the current id or {@link ComplexColumnInfo#INVALID_COMPLEX_VALUE_ID} + * @return the current id or {@link ComplexColumnInfoImpl#INVALID_FK} * for a new, unsaved value. */ public ComplexValueForeignKey getComplexValueForeignKey(); @@ -69,4 +76,78 @@ public interface ComplexValue */ public void delete() throws IOException; + + /** + * Identifier for a ComplexValue. Only valid for comparing complex values + * for the same column. + */ + public abstract class Id extends Number + { + private static final long serialVersionUID = 20130318L; + + @Override + public byte byteValue() { + return (byte)get(); + } + + @Override + public short shortValue() { + return (short)get(); + } + + @Override + public int intValue() { + return get(); + } + + @Override + public long longValue() { + return get(); + } + + @Override + public float floatValue() { + return get(); + } + + @Override + public double doubleValue() { + return get(); + } + + @Override + public int hashCode() { + return get(); + } + + @Override + public boolean equals(Object o) { + return ((this == o) || + ((o != null) && (getClass() == o.getClass()) && + (get() == ((Id)o).get()))); + } + + @Override + public String toString() { + return String.valueOf(get()); + } + + protected final Object writeReplace() throws ObjectStreamException { + // if we are going to serialize this ComplexValue.Id, convert it back to + // a normal Integer (in case it is restored outside of the context of + // jackcess) + return Integer.valueOf(get()); + } + + /** + * Returns the unique identifier of this complex value (this value is unique + * among all values in all rows of the main table for the complex column). + */ + public abstract int get(); + + /** + * Returns the rowId of this ComplexValue within the secondary table. + */ + public abstract RowId getRowId(); + } } diff --git a/src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java b/src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java index 2e4b376..aeff8c9 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java +++ b/src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2011 James Ahlborn +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -24,9 +24,9 @@ import java.io.ObjectStreamException; import java.util.Date; import java.util.List; import java.util.Map; - import com.healthmarketscience.jackcess.Column; + /** * Value which is returned for a complex column. This value corresponds to a * foreign key in a secondary table which contains the actual complex data for @@ -41,27 +41,10 @@ import com.healthmarketscience.jackcess.Column; * * @author James Ahlborn */ -public class ComplexValueForeignKey extends Number +public abstract class ComplexValueForeignKey extends Number { - private static final long serialVersionUID = 20110805L; - - private transient final Column _column; - private final int _value; - private transient List _values; - - public ComplexValueForeignKey(Column column, int value) { - _column = column; - _value = value; - } - - public int get() { - return _value; - } + private static final long serialVersionUID = 20130319L; - public Column getColumn() { - return _column; - } - @Override public byte byteValue() { return (byte)get(); @@ -92,242 +75,100 @@ public class ComplexValueForeignKey extends Number return get(); } - public ComplexDataType getComplexType() { - return getComplexInfo().getType(); - } - - protected ComplexColumnInfo getComplexInfo() { - return _column.getComplexInfo(); - } - - protected VersionHistoryColumnInfo getVersionInfo() { - return (VersionHistoryColumnInfo)getComplexInfo(); - } - - protected AttachmentColumnInfo getAttachmentInfo() { - return (AttachmentColumnInfo)getComplexInfo(); - } - - protected MultiValueColumnInfo getMultiValueInfo() { - return (MultiValueColumnInfo)getComplexInfo(); - } - - protected UnsupportedColumnInfo getUnsupportedInfo() { - return (UnsupportedColumnInfo)getComplexInfo(); - } - - public int countValues() - throws IOException - { - return getComplexInfo().countValues(get()); - } - - public List> getRawValues() - throws IOException - { - return getComplexInfo().getRawValues(get()); - } - - public List getValues() - throws IOException - { - if(_values == null) { - _values = getComplexInfo().getValues(this); - } - return _values; - } - - @SuppressWarnings("unchecked") - public List getVersions() - throws IOException - { - if(getComplexType() != ComplexDataType.VERSION_HISTORY) { - throw new UnsupportedOperationException(); - } - return (List)getValues(); - } - - @SuppressWarnings("unchecked") - public List getAttachments() - throws IOException - { - if(getComplexType() != ComplexDataType.ATTACHMENT) { - throw new UnsupportedOperationException(); - } - return (List)getValues(); - } - - @SuppressWarnings("unchecked") - public List getMultiValues() - throws IOException - { - if(getComplexType() != ComplexDataType.MULTI_VALUE) { - throw new UnsupportedOperationException(); - } - return (List)getValues(); - } - - @SuppressWarnings("unchecked") - public List getUnsupportedValues() - throws IOException - { - if(getComplexType() != ComplexDataType.UNSUPPORTED) { - throw new UnsupportedOperationException(); - } - return (List)getValues(); - } - - public void reset() { - // discard any cached values - _values = null; - } - - public Version addVersion(String value) - throws IOException - { - return addVersion(value, new Date()); - } - - public Version addVersion(String value, Date modifiedDate) - throws IOException - { - reset(); - Version v = VersionHistoryColumnInfo.newVersion(this, value, modifiedDate); - getVersionInfo().addValue(v); - return v; - } - - public Attachment addAttachment(byte[] data) - throws IOException - { - return addAttachment(null, null, null, data, null, null); - } - - public Attachment addAttachment( - String url, String name, String type, byte[] data, - Date timeStamp, Integer flags) - throws IOException - { - reset(); - Attachment a = AttachmentColumnInfo.newAttachment( - this, url, name, type, data, timeStamp, flags); - getAttachmentInfo().addValue(a); - return a; - } - - public Attachment addEncodedAttachment(byte[] encodedData) - throws IOException - { - return addEncodedAttachment(null, null, null, encodedData, null, null); - } - - public Attachment addEncodedAttachment( - String url, String name, String type, byte[] encodedData, - Date timeStamp, Integer flags) - throws IOException - { - reset(); - Attachment a = AttachmentColumnInfo.newEncodedAttachment( - this, url, name, type, encodedData, timeStamp, flags); - getAttachmentInfo().addValue(a); - return a; - } - - public Attachment updateAttachment(Attachment attachment) - throws IOException - { - reset(); - getAttachmentInfo().updateValue(attachment); - return attachment; - } - - public Attachment deleteAttachment(Attachment attachment) - throws IOException - { - reset(); - getAttachmentInfo().deleteValue(attachment); - return attachment; - } - - public SingleValue addMultiValue(Object value) - throws IOException - { - reset(); - SingleValue v = MultiValueColumnInfo.newSingleValue(this, value); - getMultiValueInfo().addValue(v); - return v; - } - - public SingleValue updateMultiValue(SingleValue value) - throws IOException - { - reset(); - getMultiValueInfo().updateValue(value); - return value; - } - - public SingleValue deleteMultiValue(SingleValue value) - throws IOException - { - reset(); - getMultiValueInfo().deleteValue(value); - return value; - } - - public UnsupportedValue addUnsupportedValue(Map values) - throws IOException - { - reset(); - UnsupportedValue v = UnsupportedColumnInfo.newValue(this, values); - getUnsupportedInfo().addValue(v); - return v; - } - - public UnsupportedValue updateUnsupportedValue(UnsupportedValue value) - throws IOException - { - reset(); - getUnsupportedInfo().updateValue(value); - return value; - } - - public UnsupportedValue deleteUnsupportedValue(UnsupportedValue value) - throws IOException - { - reset(); - getUnsupportedInfo().deleteValue(value); - return value; - } - - public void deleteAllValues() - throws IOException - { - reset(); - getComplexInfo().deleteAllValues(this); - } - - private Object writeReplace() throws ObjectStreamException { + protected final Object writeReplace() throws ObjectStreamException { // if we are going to serialize this ComplexValueForeignKey, convert it // back to a normal Integer (in case it is restored outside of the context // of jackcess) - return Integer.valueOf(_value); + return Integer.valueOf(get()); } @Override public int hashCode() { - return _value; + return get(); } @Override public boolean equals(Object o) { return ((this == o) || ((o != null) && (getClass() == o.getClass()) && - (_value == ((ComplexValueForeignKey)o)._value) && - (_column == ((ComplexValueForeignKey)o)._column))); + (get() == ((ComplexValueForeignKey)o).get()))); } @Override public String toString() { - return String.valueOf(_value); + return String.valueOf(get()); } + + public abstract int get(); + + public abstract Column getColumn(); + + public abstract ComplexDataType getComplexType(); + + public abstract int countValues() throws IOException; + + public abstract List getValues() throws IOException; + + public abstract List getVersions() throws IOException; + + public abstract List getAttachments() + throws IOException; + + public abstract List getMultiValues() + throws IOException; + + public abstract List getUnsupportedValues() + throws IOException; + + public abstract void reset(); + + public abstract Version addVersion(String value) + throws IOException; + + public abstract Version addVersion(String value, Date modifiedDate) + throws IOException; + + public abstract Attachment addAttachment(byte[] data) + throws IOException; + + public abstract Attachment addAttachment( + String url, String name, String type, byte[] data, + Date timeStamp, Integer flags) + throws IOException; + + public abstract Attachment addEncodedAttachment(byte[] encodedData) + throws IOException; + + public abstract Attachment addEncodedAttachment( + String url, String name, String type, byte[] encodedData, + Date timeStamp, Integer flags) + throws IOException; + + public abstract Attachment updateAttachment(Attachment attachment) + throws IOException; + + public abstract Attachment deleteAttachment(Attachment attachment) + throws IOException; + + public abstract SingleValue addMultiValue(Object value) + throws IOException; + + public abstract SingleValue updateMultiValue(SingleValue value) + throws IOException; + + public abstract SingleValue deleteMultiValue(SingleValue value) + throws IOException; + + public abstract UnsupportedValue addUnsupportedValue(Map values) + throws IOException; + + public abstract UnsupportedValue updateUnsupportedValue(UnsupportedValue value) + throws IOException; + + public abstract UnsupportedValue deleteUnsupportedValue(UnsupportedValue value) + throws IOException; + + public abstract void deleteAllValues() + throws IOException; + } diff --git a/src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java index efbd8b0..406908e 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java +++ b/src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2011 James Ahlborn +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -19,116 +19,12 @@ USA package com.healthmarketscience.jackcess.complex; -import java.io.IOException; -import java.util.EnumSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import com.healthmarketscience.jackcess.Column; -import com.healthmarketscience.jackcess.DataType; -import com.healthmarketscience.jackcess.Table; - /** - * Complex column info for a column holding multiple values per row. + * Complex column info for a column holding multiple simple values per row. * * @author James Ahlborn */ -public class MultiValueColumnInfo extends ComplexColumnInfo +public interface MultiValueColumnInfo extends ComplexColumnInfo { - private static final Set VALUE_TYPES = EnumSet.of( - DataType.BYTE, DataType.INT, DataType.LONG, DataType.FLOAT, - DataType.DOUBLE, DataType.GUID, DataType.NUMERIC, DataType.TEXT); - - private final Column _valueCol; - - public MultiValueColumnInfo(Column column, int complexId, - Table typeObjTable, Table flatTable) - throws IOException - { - super(column, complexId, typeObjTable, flatTable); - - _valueCol = getTypeColumns().get(0); - } - - @Override - public ComplexDataType getType() - { - return ComplexDataType.MULTI_VALUE; - } - - public Column getValueColumn() { - return _valueCol; - } - - @Override - protected SingleValueImpl toValue( - ComplexValueForeignKey complexValueFk, - Map rawValue) - { - int id = (Integer)getPrimaryKeyColumn().getRowValue(rawValue); - Object value = getValueColumn().getRowValue(rawValue); - - return new SingleValueImpl(id, complexValueFk, value); - } - - @Override - protected Object[] asRow(Object[] row, SingleValue value) throws IOException { - super.asRow(row, value); - getValueColumn().setRowValue(row, value.get()); - return row; - } - - public static SingleValue newSingleValue(Object value) { - return newSingleValue(INVALID_COMPLEX_VALUE_ID, value); - } - - public static SingleValue newSingleValue( - ComplexValueForeignKey complexValueFk, Object value) { - return new SingleValueImpl(INVALID_ID, complexValueFk, value); - } - - public static boolean isMultiValueColumn(Table typeObjTable) { - // if we found a single value of a "simple" type, then we are dealing with - // a multi-value column - List typeCols = typeObjTable.getColumns(); - return ((typeCols.size() == 1) && - VALUE_TYPES.contains(typeCols.get(0).getType())); - } - - private static class SingleValueImpl extends ComplexValueImpl - implements SingleValue - { - private Object _value; - - private SingleValueImpl(int id, ComplexValueForeignKey complexValueFk, - Object value) - { - super(id, complexValueFk); - _value = value; - } - - public Object get() { - return _value; - } - - public void set(Object value) { - _value = value; - } - public void update() throws IOException { - getComplexValueForeignKey().updateMultiValue(this); - } - - public void delete() throws IOException { - getComplexValueForeignKey().deleteMultiValue(this); - } - - @Override - public String toString() - { - return "SingleValue(" + getComplexValueForeignKey() + "," + getId() + - ") " + get(); - } - } } diff --git a/src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java index 0eda7f7..646ecfc 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java +++ b/src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2011 James Ahlborn +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -19,115 +19,12 @@ USA package com.healthmarketscience.jackcess.complex; -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import com.healthmarketscience.jackcess.Column; -import com.healthmarketscience.jackcess.Table; - /** * Complex column info for an unsupported complex type. * * @author James Ahlborn */ -public class UnsupportedColumnInfo extends ComplexColumnInfo +public interface UnsupportedColumnInfo extends ComplexColumnInfo { - public UnsupportedColumnInfo(Column column, int complexId, Table typeObjTable, - Table flatTable) - throws IOException - { - super(column, complexId, typeObjTable, flatTable); - } - - public List getValueColumns() { - return getTypeColumns(); - } - - @Override - public ComplexDataType getType() - { - return ComplexDataType.UNSUPPORTED; - } - - @Override - protected UnsupportedValueImpl toValue( - ComplexValueForeignKey complexValueFk, - Map rawValue) - { - int id = (Integer)getPrimaryKeyColumn().getRowValue(rawValue); - - Map values = new LinkedHashMap(); - for(Column col : getValueColumns()) { - col.setRowValue(values, col.getRowValue(rawValue)); - } - - return new UnsupportedValueImpl(id, complexValueFk, values); - } - - @Override - protected Object[] asRow(Object[] row, UnsupportedValue value) - throws IOException - { - super.asRow(row, value); - - Map values = value.getValues(); - for(Column col : getValueColumns()) { - col.setRowValue(row, col.getRowValue(values)); - } - - return row; - } - - public static UnsupportedValue newValue(Map values) { - return newValue(INVALID_COMPLEX_VALUE_ID, values); - } - - public static UnsupportedValue newValue( - ComplexValueForeignKey complexValueFk, Map values) { - return new UnsupportedValueImpl(INVALID_ID, complexValueFk, - new LinkedHashMap(values)); - } - - private static class UnsupportedValueImpl extends ComplexValueImpl - implements UnsupportedValue - { - private Map _values; - - private UnsupportedValueImpl(int id, ComplexValueForeignKey complexValueFk, - Map values) - { - super(id, complexValueFk); - _values = values; - } - - public Map getValues() { - return _values; - } - - public Object get(String columnName) { - return getValues().get(columnName); - } - - public void set(String columnName, Object value) { - getValues().put(columnName, value); - } - - public void update() throws IOException { - getComplexValueForeignKey().updateUnsupportedValue(this); - } - - public void delete() throws IOException { - getComplexValueForeignKey().deleteUnsupportedValue(this); - } - - @Override - public String toString() - { - return "UnsupportedValue(" + getComplexValueForeignKey() + "," + getId() + - ") " + getValues(); - } - } } diff --git a/src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java index c8df424..db1f1cf 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java +++ b/src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2011 James Ahlborn +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -19,15 +19,6 @@ USA package com.healthmarketscience.jackcess.complex; -import java.io.IOException; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Map; - -import com.healthmarketscience.jackcess.Column; -import com.healthmarketscience.jackcess.Table; - /** * Complex column info for a column which tracking the version history of an * "append only" memo column. @@ -39,206 +30,7 @@ import com.healthmarketscience.jackcess.Table; * * @author James Ahlborn */ -public class VersionHistoryColumnInfo extends ComplexColumnInfo +public interface VersionHistoryColumnInfo extends ComplexColumnInfo { - private final Column _valueCol; - private final Column _modifiedCol; - - public VersionHistoryColumnInfo(Column column, int complexId, - Table typeObjTable, Table flatTable) - throws IOException - { - super(column, complexId, typeObjTable, flatTable); - - Column valueCol = null; - Column modifiedCol = null; - for(Column col : getTypeColumns()) { - switch(col.getType()) { - case SHORT_DATE_TIME: - modifiedCol = col; - break; - case MEMO: - valueCol = col; - break; - default: - // ignore - } - } - - _valueCol = valueCol; - _modifiedCol = modifiedCol; - } - - @Override - public void postTableLoadInit() throws IOException { - super.postTableLoadInit(); - - // link up with the actual versioned column. it should have the same name - // as the "value" column in the type table. - Column versionedCol = getColumn().getTable().getColumn( - getValueColumn().getName()); - versionedCol.setVersionHistoryColumn(getColumn()); - } - - public Column getValueColumn() { - return _valueCol; - } - - public Column getModifiedDateColumn() { - return _modifiedCol; - } - - @Override - public ComplexDataType getType() { - return ComplexDataType.VERSION_HISTORY; - } - - @Override - public int updateValue(Version value) throws IOException { - throw new UnsupportedOperationException( - "This column does not support value updates"); - } - - @Override - public void deleteValue(Version value) throws IOException { - throw new UnsupportedOperationException( - "This column does not support value deletes"); - } - - @Override - public void deleteAllValues(int complexValueFk) throws IOException { - throw new UnsupportedOperationException( - "This column does not support value deletes"); - } - - @Override - protected List toValues(ComplexValueForeignKey complexValueFk, - List> rawValues) - throws IOException - { - List versions = super.toValues(complexValueFk, rawValues); - - // order versions newest to oldest - Collections.sort(versions); - - return versions; - } - - @Override - protected VersionImpl toValue(ComplexValueForeignKey complexValueFk, - Map rawValue) { - int id = (Integer)getPrimaryKeyColumn().getRowValue(rawValue); - String value = (String)getValueColumn().getRowValue(rawValue); - Date modifiedDate = (Date)getModifiedDateColumn().getRowValue(rawValue); - - return new VersionImpl(id, complexValueFk, value, modifiedDate); - } - - @Override - protected Object[] asRow(Object[] row, Version version) throws IOException { - super.asRow(row, version); - getValueColumn().setRowValue(row, version.getValue()); - getModifiedDateColumn().setRowValue(row, version.getModifiedDate()); - return row; - } - - public static Version newVersion(String value, Date modifiedDate) { - return newVersion(INVALID_COMPLEX_VALUE_ID, value, modifiedDate); - } - - public static Version newVersion(ComplexValueForeignKey complexValueFk, - String value, Date modifiedDate) { - return new VersionImpl(INVALID_ID, complexValueFk, value, modifiedDate); - } - - public static boolean isVersionHistoryColumn(Table typeObjTable) { - // version history data has these columns (MEMO), - // (SHORT_DATE_TIME) - List typeCols = typeObjTable.getColumns(); - if(typeCols.size() < 2) { - return false; - } - - int numMemo = 0; - int numDate = 0; - - for(Column col : typeCols) { - switch(col.getType()) { - case SHORT_DATE_TIME: - ++numDate; - break; - case MEMO: - ++numMemo; - break; - default: - // ignore - } - } - - // be flexible, allow for extra columns... - return((numMemo >= 1) && (numDate >= 1)); - } - - private static class VersionImpl extends ComplexValueImpl implements Version - { - private final String _value; - private final Date _modifiedDate; - - private VersionImpl(int id, ComplexValueForeignKey complexValueFk, - String value, Date modifiedDate) - { - super(id, complexValueFk); - _value = value; - _modifiedDate = modifiedDate; - } - - public String getValue() { - return _value; - } - - public Date getModifiedDate() { - return _modifiedDate; - } - - public int compareTo(Version o) { - Date d1 = getModifiedDate(); - Date d2 = o.getModifiedDate(); - - // sort by descending date (newest/greatest first) - int cmp = d2.compareTo(d1); - if(cmp != 0) { - return cmp; - } - - // use id, then complexValueFk to break ties (although we really - // shouldn't be comparing across different columns) - int id1 = getId(); - int id2 = o.getId(); - if(id1 != id2) { - return ((id1 > id2) ? -1 : 1); - } - id1 = getComplexValueForeignKey().get(); - id2 = o.getComplexValueForeignKey().get(); - return ((id1 > id2) ? -1 : - ((id1 < id2) ? 1 : 0)); - } - - public void update() throws IOException { - throw new UnsupportedOperationException( - "This column does not support value updates"); - } - - public void delete() throws IOException { - throw new UnsupportedOperationException( - "This column does not support value deletes"); - } - @Override - public String toString() - { - return "Version(" + getComplexValueForeignKey() + "," + getId() + ") " + - getModifiedDate() + ", " + getValue(); - } - } - } diff --git a/src/java/com/healthmarketscience/jackcess/ByteUtil.java b/src/java/com/healthmarketscience/jackcess/impl/ByteUtil.java similarity index 99% rename from src/java/com/healthmarketscience/jackcess/ByteUtil.java rename to src/java/com/healthmarketscience/jackcess/impl/ByteUtil.java index b46a44b..857c631 100644 --- a/src/java/com/healthmarketscience/jackcess/ByteUtil.java +++ b/src/java/com/healthmarketscience/jackcess/impl/ByteUtil.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.io.FileWriter; import java.io.IOException; @@ -487,7 +487,7 @@ public final class ByteUtil { * Convert the given number of bytes from the given database page to a * hexidecimal string for display. */ - public static String toHexString(Database db, int pageNumber, int size) + public static String toHexString(DatabaseImpl db, int pageNumber, int size) throws IOException { ByteBuffer buffer = db.getPageChannel().createPageBuffer(); diff --git a/src/java/com/healthmarketscience/jackcess/CodecHandler.java b/src/java/com/healthmarketscience/jackcess/impl/CodecHandler.java similarity index 76% rename from src/java/com/healthmarketscience/jackcess/CodecHandler.java rename to src/java/com/healthmarketscience/jackcess/impl/CodecHandler.java index c448668..944ac08 100644 --- a/src/java/com/healthmarketscience/jackcess/CodecHandler.java +++ b/src/java/com/healthmarketscience/jackcess/impl/CodecHandler.java @@ -17,7 +17,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.io.IOException; import java.nio.ByteBuffer; @@ -39,14 +39,24 @@ public interface CodecHandler public boolean canEncodePartialPage(); /** - * Decodes the given page buffer inline. + * Returns {@code true} if this handler can decode a page inline, + * {@code false} otherwise. If this method returns {@code false}, the + * {@link #decodePage} method will always be called with separate buffers. + */ + public boolean canDecodeInline(); + + /** + * Decodes the given page buffer. * - * @param page the page to be decoded + * @param inPage the page to be decoded + * @param outPage the decoded page. if {@link #canDecodeInline} is {@code + * true}, this will be the same buffer as inPage. * @param pageNumber the page number of the given page * * @throws IOException if an exception occurs during decoding */ - public void decodePage(ByteBuffer page, int pageNumber) throws IOException; + public void decodePage(ByteBuffer inPage, ByteBuffer outPage, int pageNumber) + throws IOException; /** * Encodes the given page buffer into a new page buffer and returns it. The diff --git a/src/java/com/healthmarketscience/jackcess/CodecProvider.java b/src/java/com/healthmarketscience/jackcess/impl/CodecProvider.java similarity index 97% rename from src/java/com/healthmarketscience/jackcess/CodecProvider.java rename to src/java/com/healthmarketscience/jackcess/impl/CodecProvider.java index bb891cd..22f7404 100644 --- a/src/java/com/healthmarketscience/jackcess/CodecProvider.java +++ b/src/java/com/healthmarketscience/jackcess/impl/CodecProvider.java @@ -17,7 +17,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.io.IOException; import java.nio.charset.Charset; diff --git a/src/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java new file mode 100644 index 0000000..13eb370 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -0,0 +1,2280 @@ +/* +Copyright (c) 2005 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.impl; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamException; +import java.io.Reader; +import java.io.Serializable; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.SQLException; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.ColumnBuilder; +import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.PropertyMap; +import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.complex.ComplexColumnInfo; +import com.healthmarketscience.jackcess.complex.ComplexValue; +import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; +import com.healthmarketscience.jackcess.impl.complex.ComplexColumnInfoImpl; +import com.healthmarketscience.jackcess.impl.complex.ComplexValueForeignKeyImpl; +import com.healthmarketscience.jackcess.impl.scsu.Compress; +import com.healthmarketscience.jackcess.impl.scsu.EndOfInputException; +import com.healthmarketscience.jackcess.impl.scsu.Expand; +import com.healthmarketscience.jackcess.impl.scsu.IllegalInputException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Access database column definition + * @author Tim McCune + * @usage _general_class_ + */ +public class ColumnImpl implements Column, Comparable { + + private static final Log LOG = LogFactory.getLog(ColumnImpl.class); + + /** + * Placeholder object for adding rows which indicates that the caller wants + * the RowId of the new row. Must be added as an extra value at the end of + * the row values array. + * @see TableImpl#asRowWithRowId + * @usage _intermediate_field_ + */ + public static final Object RETURN_ROW_ID = ""; + + /** + * Access stores numeric dates in days. Java stores them in milliseconds. + */ + private static final double MILLISECONDS_PER_DAY = + (24L * 60L * 60L * 1000L); + + /** + * Access starts counting dates at Jan 1, 1900. Java starts counting + * at Jan 1, 1970. This is the # of millis between them for conversion. + */ + private static final long MILLIS_BETWEEN_EPOCH_AND_1900 = + 25569L * (long)MILLISECONDS_PER_DAY; + + /** + * Long value (LVAL) type that indicates that the value is stored on the + * same page + */ + private static final byte LONG_VALUE_TYPE_THIS_PAGE = (byte) 0x80; + /** + * Long value (LVAL) type that indicates that the value is stored on another + * page + */ + private static final byte LONG_VALUE_TYPE_OTHER_PAGE = (byte) 0x40; + /** + * Long value (LVAL) type that indicates that the value is stored on + * multiple other pages + */ + private static final byte LONG_VALUE_TYPE_OTHER_PAGES = (byte) 0x00; + /** + * Mask to apply the long length in order to get the flag bits (only the + * first 2 bits are type flags). + */ + private static final int LONG_VALUE_TYPE_MASK = 0xC0000000; + + /** + * mask for the fixed len bit + * @usage _advanced_field_ + */ + public static final byte FIXED_LEN_FLAG_MASK = (byte)0x01; + + /** + * mask for the auto number bit + * @usage _advanced_field_ + */ + public static final byte AUTO_NUMBER_FLAG_MASK = (byte)0x04; + + /** + * mask for the auto number guid bit + * @usage _advanced_field_ + */ + public static final byte AUTO_NUMBER_GUID_FLAG_MASK = (byte)0x40; + + /** + * mask for the hyperlink bit (on memo types) + * @usage _advanced_field_ + */ + public static final byte HYPERLINK_FLAG_MASK = (byte)0x80; + + /** + * mask for the unknown bit (possible "can be null"?) + * @usage _advanced_field_ + */ + public static final byte UNKNOWN_FLAG_MASK = (byte)0x02; + + // some other flags? + // 0x10: replication related field (or hidden?) + // 0x80: hyperlink (some memo based thing) + + /** the value for the "general" sort order */ + private static final short GENERAL_SORT_ORDER_VALUE = 1033; + + /** + * the "general" text sort order, legacy version (access 2000-2007) + * @usage _intermediate_field_ + */ + public static final SortOrder GENERAL_LEGACY_SORT_ORDER = + new SortOrder(GENERAL_SORT_ORDER_VALUE, (byte)0); + + /** + * the "general" text sort order, latest version (access 2010+) + * @usage _intermediate_field_ + */ + public static final SortOrder GENERAL_SORT_ORDER = + new SortOrder(GENERAL_SORT_ORDER_VALUE, (byte)1); + + /** pattern matching textual guid strings (allows for optional surrounding + '{' and '}') */ + private static final Pattern GUID_PATTERN = Pattern.compile("\\s*[{]?([\\p{XDigit}]{8})-([\\p{XDigit}]{4})-([\\p{XDigit}]{4})-([\\p{XDigit}]{4})-([\\p{XDigit}]{12})[}]?\\s*"); + + /** header used to indicate unicode text compression */ + private static final byte[] TEXT_COMPRESSION_HEADER = + { (byte)0xFF, (byte)0XFE }; + + /** placeholder for column which is not numeric */ + private static final NumericInfo DEFAULT_NUMERIC_INFO = new NumericInfo(); + + /** placeholder for column which is not textual */ + private static final TextInfo DEFAULT_TEXT_INFO = new TextInfo(); + + + /** owning table */ + private final TableImpl _table; + /** Whether or not the column is of variable length */ + private final boolean _variableLength; + /** Whether or not the column is an autonumber column */ + private final boolean _autoNumber; + /** Data type */ + private final DataType _type; + /** Maximum column length */ + private final short _columnLength; + /** 0-based column number */ + private final short _columnNumber; + /** index of the data for this column within a list of row data */ + private int _columnIndex; + /** display index of the data for this column */ + private final int _displayIndex; + /** Column name */ + private String _name; + /** the offset of the fixed data in the row */ + private final int _fixedDataOffset; + /** the index of the variable length data in the var len offset table */ + private final int _varLenTableIndex; + /** information specific to numeric columns */ + private NumericInfo _numericInfo = DEFAULT_NUMERIC_INFO; + /** information specific to text columns */ + private TextInfo _textInfo = DEFAULT_TEXT_INFO; + /** the auto number generator for this column (if autonumber column) */ + private final AutoNumberGenerator _autoNumberGenerator; + /** additional information specific to complex columns */ + private final ComplexColumnInfo _complexInfo; + /** properties for this column, if any */ + private PropertyMap _props; + /** Holds additional info for writing long values */ + private LongValueBufferHolder _lvalBufferH; + + /** + * @usage _advanced_method_ + */ + protected ColumnImpl(TableImpl table, DataType type, int colNumber, + int fixedOffset, int varLenIndex) { + _table = table; + _type = type; + + if(!_type.isVariableLength()) { + _columnLength = (short)type.getFixedSize(); + } else { + _columnLength = (short)type.getMaxSize(); + } + _variableLength = type.isVariableLength(); + if(type.getHasScalePrecision()) { + modifyNumericInfo(); + _numericInfo._scale = (byte)type.getDefaultScale(); + _numericInfo._precision =(byte)type.getDefaultPrecision(); + } + _autoNumber = false; + _autoNumberGenerator = null; + _columnNumber = (short)colNumber; + _columnIndex = colNumber; + _displayIndex = colNumber; + _fixedDataOffset = fixedOffset; + _varLenTableIndex = varLenIndex; + _complexInfo = null; + } + + /** + * Read a column definition in from a buffer + * @param table owning table + * @param buffer Buffer containing column definition + * @param offset Offset in the buffer at which the column definition starts + * @usage _advanced_method_ + */ + public ColumnImpl(TableImpl table, ByteBuffer buffer, int offset, + int displayIndex) + throws IOException + { + _table = table; + _displayIndex = displayIndex; + + byte colType = buffer.get(offset + getFormat().OFFSET_COLUMN_TYPE); + _columnNumber = buffer.getShort(offset + getFormat().OFFSET_COLUMN_NUMBER); + _columnLength = buffer.getShort(offset + getFormat().OFFSET_COLUMN_LENGTH); + + byte flags = buffer.get(offset + getFormat().OFFSET_COLUMN_FLAGS); + _variableLength = ((flags & FIXED_LEN_FLAG_MASK) == 0); + _autoNumber = ((flags & (AUTO_NUMBER_FLAG_MASK | AUTO_NUMBER_GUID_FLAG_MASK)) + != 0); + + DataType type = null; + try { + type = DataType.fromByte(colType); + } catch(IOException e) { + LOG.warn("Unsupported column type " + colType); + type = (_variableLength ? DataType.UNSUPPORTED_VARLEN : + DataType.UNSUPPORTED_FIXEDLEN); + setUnknownDataType(colType); + } + _type = type; + + if (_type.getHasScalePrecision()) { + modifyNumericInfo(); + _numericInfo._precision = buffer.get(offset + + getFormat().OFFSET_COLUMN_PRECISION); + _numericInfo._scale = buffer.get(offset + getFormat().OFFSET_COLUMN_SCALE); + } else if(_type.isTextual()) { + modifyTextInfo(); + + // co-located w/ precision/scale + _textInfo._sortOrder = readSortOrder( + buffer, offset + getFormat().OFFSET_COLUMN_SORT_ORDER, getFormat()); + int cpOffset = getFormat().OFFSET_COLUMN_CODE_PAGE; + if(cpOffset >= 0) { + _textInfo._codePage = buffer.getShort(offset + cpOffset); + } + + _textInfo._compressedUnicode = ((buffer.get(offset + + getFormat().OFFSET_COLUMN_COMPRESSED_UNICODE) & 1) == 1); + + if(_type == DataType.MEMO) { + // only memo fields can be hyperlinks + _textInfo._hyperlink = ((flags & HYPERLINK_FLAG_MASK) != 0); + } + } + + _autoNumberGenerator = createAutoNumberGenerator(); + + if(_variableLength) { + _varLenTableIndex = buffer.getShort(offset + getFormat().OFFSET_COLUMN_VARIABLE_TABLE_INDEX); + _fixedDataOffset = 0; + } else { + _fixedDataOffset = buffer.getShort(offset + getFormat().OFFSET_COLUMN_FIXED_DATA_OFFSET); + _varLenTableIndex = 0; + } + + // load complex info + if(_type == DataType.COMPLEX_TYPE) { + _complexInfo = ComplexColumnSupport.create(this, buffer, offset); + } else { + _complexInfo = null; + } + } + + /** + * Sets the usage maps for this column. + */ + void setUsageMaps(UsageMap ownedPages, UsageMap freeSpacePages) { + _lvalBufferH = new UmapLongValueBufferHolder(ownedPages, freeSpacePages); + } + + /** + * Secondary column initialization after the table is fully loaded. + */ + void postTableLoadInit() throws IOException { + if(getType().isLongValue() && (_lvalBufferH == null)) { + _lvalBufferH = new LegacyLongValueBufferHolder(); + } + if(_complexInfo != null) { + ((ComplexColumnInfoImpl)_complexInfo) + .postTableLoadInit(); + } + } + + public TableImpl getTable() { + return _table; + } + + public DatabaseImpl getDatabase() { + return getTable().getDatabase(); + } + + /** + * @usage _advanced_method_ + */ + public JetFormat getFormat() { + return getDatabase().getFormat(); + } + + /** + * @usage _advanced_method_ + */ + public PageChannel getPageChannel() { + return getDatabase().getPageChannel(); + } + + public String getName() { + return _name; + } + + /** + * @usage _advanced_method_ + */ + public void setName(String name) { + _name = name; + } + + public boolean isVariableLength() { + return _variableLength; + } + + public boolean isAutoNumber() { + return _autoNumber; + } + + /** + * @usage _advanced_method_ + */ + public short getColumnNumber() { + return _columnNumber; + } + + public int getColumnIndex() { + return _columnIndex; + } + + /** + * @usage _advanced_method_ + */ + public void setColumnIndex(int newColumnIndex) { + _columnIndex = newColumnIndex; + } + + /** + * @usage _advanced_method_ + */ + public int getDisplayIndex() { + return _displayIndex; + } + + public DataType getType() { + return _type; + } + + public int getSQLType() throws SQLException { + return _type.getSQLType(); + } + + public boolean isCompressedUnicode() { + return _textInfo._compressedUnicode; + } + + public byte getPrecision() { + return _numericInfo._precision; + } + + public byte getScale() { + return _numericInfo._scale; + } + + /** + * @usage _intermediate_method_ + */ + public SortOrder getTextSortOrder() { + return _textInfo._sortOrder; + } + + /** + * @usage _intermediate_method_ + */ + public short getTextCodePage() { + return _textInfo._codePage; + } + + public short getLength() { + return _columnLength; + } + + public short getLengthInUnits() { + return (short)getType().toUnitSize(getLength()); + } + + /** + * @usage _advanced_method_ + */ + public int getVarLenTableIndex() { + return _varLenTableIndex; + } + + /** + * @usage _advanced_method_ + */ + public int getFixedDataOffset() { + return _fixedDataOffset; + } + + protected Charset getCharset() { + return getDatabase().getCharset(); + } + + protected Calendar getCalendar() { + return getDatabase().getCalendar(); + } + + public boolean isAppendOnly() { + return (getVersionHistoryColumn() != null); + } + + public ColumnImpl getVersionHistoryColumn() { + return _textInfo._versionHistoryCol; + } + + /** + * Returns the number of database pages owned by this column. + * @usage _intermediate_method_ + */ + public int getOwnedPageCount() { + return ((_lvalBufferH == null) ? 0 : _lvalBufferH.getOwnedPageCount()); + } + + /** + * @usage _advanced_method_ + */ + public void setVersionHistoryColumn(ColumnImpl versionHistoryCol) { + modifyTextInfo(); + _textInfo._versionHistoryCol = versionHistoryCol; + } + + public boolean isHyperlink() { + return _textInfo._hyperlink; + } + + public ComplexColumnInfo getComplexInfo() { + return _complexInfo; + } + + private void setUnknownDataType(byte type) { + // slight hack, stash the original type in the _scale + modifyNumericInfo(); + _numericInfo._scale = type; + } + + private byte getUnknownDataType() { + // slight hack, we stashed the real type in the _scale + return _numericInfo._scale; + } + + private AutoNumberGenerator createAutoNumberGenerator() { + if(!_autoNumber || (_type == null)) { + return null; + } + + switch(_type) { + case LONG: + return new LongAutoNumberGenerator(); + case GUID: + return new GuidAutoNumberGenerator(); + case COMPLEX_TYPE: + return new ComplexTypeAutoNumberGenerator(); + default: + LOG.warn("Unknown auto number column type " + _type); + return new UnsupportedAutoNumberGenerator(_type); + } + } + + /** + * Returns the AutoNumberGenerator for this column if this is an autonumber + * column, {@code null} otherwise. + * @usage _advanced_method_ + */ + public AutoNumberGenerator getAutoNumberGenerator() { + return _autoNumberGenerator; + } + + public PropertyMap getProperties() throws IOException { + if(_props == null) { + _props = getTable().getPropertyMaps().get(getName()); + } + return _props; + } + + private void modifyNumericInfo() { + if(_numericInfo == DEFAULT_NUMERIC_INFO) { + _numericInfo = new NumericInfo(); + } + } + + private void modifyTextInfo() { + if(_textInfo == DEFAULT_TEXT_INFO) { + _textInfo = new TextInfo(); + } + } + + public Object setRowValue(Object[] rowArray, Object value) { + rowArray[_columnIndex] = value; + return value; + } + + public Object setRowValue(Map rowMap, Object value) { + rowMap.put(_name, value); + return value; + } + + public Object getRowValue(Object[] rowArray) { + return rowArray[_columnIndex]; + } + + public Object getRowValue(Map rowMap) { + return rowMap.get(_name); + } + + /** + * Deserialize a raw byte value for this column into an Object + * @param data The raw byte value + * @return The deserialized Object + * @usage _advanced_method_ + */ + public Object read(byte[] data) throws IOException { + return read(data, PageChannel.DEFAULT_BYTE_ORDER); + } + + /** + * Deserialize a raw byte value for this column into an Object + * @param data The raw byte value + * @param order Byte order in which the raw value is stored + * @return The deserialized Object + * @usage _advanced_method_ + */ + public Object read(byte[] data, ByteOrder order) throws IOException { + ByteBuffer buffer = ByteBuffer.wrap(data); + buffer.order(order); + if (_type == DataType.BOOLEAN) { + throw new IOException("Tried to read a boolean from data instead of null mask."); + } else if (_type == DataType.BYTE) { + return Byte.valueOf(buffer.get()); + } else if (_type == DataType.INT) { + return Short.valueOf(buffer.getShort()); + } else if (_type == DataType.LONG) { + return Integer.valueOf(buffer.getInt()); + } else if (_type == DataType.DOUBLE) { + return Double.valueOf(buffer.getDouble()); + } else if (_type == DataType.FLOAT) { + return Float.valueOf(buffer.getFloat()); + } else if (_type == DataType.SHORT_DATE_TIME) { + return readDateValue(buffer); + } else if (_type == DataType.BINARY) { + return data; + } else if (_type == DataType.TEXT) { + return decodeTextValue(data); + } else if (_type == DataType.MONEY) { + return readCurrencyValue(buffer); + } else if (_type == DataType.OLE) { + if (data.length > 0) { + return readLongValue(data); + } + return null; + } else if (_type == DataType.MEMO) { + if (data.length > 0) { + return readLongStringValue(data); + } + return null; + } else if (_type == DataType.NUMERIC) { + return readNumericValue(buffer); + } else if (_type == DataType.GUID) { + return readGUIDValue(buffer, order); + } else if ((_type == DataType.UNKNOWN_0D) || + (_type == DataType.UNKNOWN_11)) { + // treat like "binary" data + return data; + } else if (_type == DataType.COMPLEX_TYPE) { + return new ComplexValueForeignKeyImpl(this, buffer.getInt()); + } else if(_type.isUnsupported()) { + return rawDataWrapper(data); + } else { + throw new IOException("Unrecognized data type: " + _type); + } + } + + /** + * @param lvalDefinition Column value that points to an LVAL record + * @return The LVAL data + */ + private byte[] readLongValue(byte[] lvalDefinition) + throws IOException + { + ByteBuffer def = PageChannel.wrap(lvalDefinition); + int lengthWithFlags = def.getInt(); + int length = lengthWithFlags & (~LONG_VALUE_TYPE_MASK); + + byte[] rtn = new byte[length]; + byte type = (byte)((lengthWithFlags & LONG_VALUE_TYPE_MASK) >>> 24); + + if(type == LONG_VALUE_TYPE_THIS_PAGE) { + + // inline long value + def.getInt(); //Skip over lval_dp + def.getInt(); //Skip over unknown + def.get(rtn); + + } else { + + // long value on other page(s) + if (lvalDefinition.length != getFormat().SIZE_LONG_VALUE_DEF) { + throw new IOException("Expected " + getFormat().SIZE_LONG_VALUE_DEF + + " bytes in long value definition, but found " + + lvalDefinition.length); + } + + int rowNum = ByteUtil.getUnsignedByte(def); + int pageNum = ByteUtil.get3ByteInt(def, def.position()); + ByteBuffer lvalPage = getPageChannel().createPageBuffer(); + + switch (type) { + case LONG_VALUE_TYPE_OTHER_PAGE: + { + getPageChannel().readPage(lvalPage, pageNum); + + short rowStart = TableImpl.findRowStart(lvalPage, rowNum, getFormat()); + short rowEnd = TableImpl.findRowEnd(lvalPage, rowNum, getFormat()); + + if((rowEnd - rowStart) != length) { + throw new IOException("Unexpected lval row length"); + } + + lvalPage.position(rowStart); + lvalPage.get(rtn); + } + break; + + case LONG_VALUE_TYPE_OTHER_PAGES: + + ByteBuffer rtnBuf = ByteBuffer.wrap(rtn); + int remainingLen = length; + while(remainingLen > 0) { + lvalPage.clear(); + getPageChannel().readPage(lvalPage, pageNum); + + short rowStart = TableImpl.findRowStart(lvalPage, rowNum, getFormat()); + short rowEnd = TableImpl.findRowEnd(lvalPage, rowNum, getFormat()); + + // read next page information + lvalPage.position(rowStart); + rowNum = ByteUtil.getUnsignedByte(lvalPage); + pageNum = ByteUtil.get3ByteInt(lvalPage); + + // update rowEnd and remainingLen based on chunkLength + int chunkLength = (rowEnd - rowStart) - 4; + if(chunkLength > remainingLen) { + rowEnd = (short)(rowEnd - (chunkLength - remainingLen)); + chunkLength = remainingLen; + } + remainingLen -= chunkLength; + + lvalPage.limit(rowEnd); + rtnBuf.put(lvalPage); + } + + break; + + default: + throw new IOException("Unrecognized long value type: " + type); + } + } + + return rtn; + } + + /** + * @param lvalDefinition Column value that points to an LVAL record + * @return The LVAL data + */ + private String readLongStringValue(byte[] lvalDefinition) + throws IOException + { + byte[] binData = readLongValue(lvalDefinition); + if(binData == null) { + return null; + } + return decodeTextValue(binData); + } + + /** + * Decodes "Currency" values. + * + * @param buffer Column value that points to currency data + * @return BigDecimal representing the monetary value + * @throws IOException if the value cannot be parsed + */ + private static BigDecimal readCurrencyValue(ByteBuffer buffer) + throws IOException + { + if(buffer.remaining() != 8) { + throw new IOException("Invalid money value."); + } + + return new BigDecimal(BigInteger.valueOf(buffer.getLong(0)), 4); + } + + /** + * Writes "Currency" values. + */ + private static void writeCurrencyValue(ByteBuffer buffer, Object value) + throws IOException + { + Object inValue = value; + try { + BigDecimal decVal = toBigDecimal(value); + inValue = decVal; + + // adjust scale (will cause the an ArithmeticException if number has too + // many decimal places) + decVal = decVal.setScale(4); + + // now, remove scale and convert to long (this will throw if the value is + // too big) + buffer.putLong(decVal.movePointRight(4).longValueExact()); + } catch(ArithmeticException e) { + throw (IOException) + new IOException("Currency value '" + inValue + "' out of range") + .initCause(e); + } + } + + /** + * Decodes a NUMERIC field. + */ + private BigDecimal readNumericValue(ByteBuffer buffer) + { + boolean negate = (buffer.get() != 0); + + byte[] tmpArr = ByteUtil.getBytes(buffer, 16); + + if(buffer.order() != ByteOrder.BIG_ENDIAN) { + fixNumericByteOrder(tmpArr); + } + + BigInteger intVal = new BigInteger(tmpArr); + if(negate) { + intVal = intVal.negate(); + } + return new BigDecimal(intVal, getScale()); + } + + /** + * Writes a numeric value. + */ + private void writeNumericValue(ByteBuffer buffer, Object value) + throws IOException + { + Object inValue = value; + try { + BigDecimal decVal = toBigDecimal(value); + inValue = decVal; + + boolean negative = (decVal.compareTo(BigDecimal.ZERO) < 0); + if(negative) { + decVal = decVal.negate(); + } + + // write sign byte + buffer.put(negative ? (byte)0x80 : (byte)0); + + // adjust scale according to this column type (will cause the an + // ArithmeticException if number has too many decimal places) + decVal = decVal.setScale(getScale()); + + // check precision + if(decVal.precision() > getPrecision()) { + throw new IOException( + "Numeric value is too big for specified precision " + + getPrecision() + ": " + decVal); + } + + // convert to unscaled BigInteger, big-endian bytes + byte[] intValBytes = decVal.unscaledValue().toByteArray(); + int maxByteLen = getType().getFixedSize() - 1; + if(intValBytes.length > maxByteLen) { + throw new IOException("Too many bytes for valid BigInteger?"); + } + if(intValBytes.length < maxByteLen) { + byte[] tmpBytes = new byte[maxByteLen]; + System.arraycopy(intValBytes, 0, tmpBytes, + (maxByteLen - intValBytes.length), + intValBytes.length); + intValBytes = tmpBytes; + } + if(buffer.order() != ByteOrder.BIG_ENDIAN) { + fixNumericByteOrder(intValBytes); + } + buffer.put(intValBytes); + } catch(ArithmeticException e) { + throw (IOException) + new IOException("Numeric value '" + inValue + "' out of range") + .initCause(e); + } + } + + /** + * Decodes a date value. + */ + private Date readDateValue(ByteBuffer buffer) + { + // seems access stores dates in the local timezone. guess you just hope + // you read it in the same timezone in which it was written! + long dateBits = buffer.getLong(); + long time = fromDateDouble(Double.longBitsToDouble(dateBits)); + return new DateExt(time, dateBits); + } + + /** + * Returns a java long time value converted from an access date double. + * @usage _advanced_method_ + */ + public long fromDateDouble(double value) + { + long time = Math.round(value * MILLISECONDS_PER_DAY); + time -= MILLIS_BETWEEN_EPOCH_AND_1900; + time -= getFromLocalTimeZoneOffset(time); + return time; + } + + /** + * Writes a date value. + */ + private void writeDateValue(ByteBuffer buffer, Object value) + { + if(value == null) { + buffer.putDouble(0d); + } else if(value instanceof DateExt) { + + // this is a Date value previously read from readDateValue(). use the + // original bits to store the value so we don't lose any precision + buffer.putLong(((DateExt)value).getDateBits()); + + } else { + + buffer.putDouble(toDateDouble(value)); + } + } + + /** + * Returns an access date double converted from a java Date/Calendar/Number + * time value. + * @usage _advanced_method_ + */ + public double toDateDouble(Object value) + { + // seems access stores dates in the local timezone. guess you just + // hope you read it in the same timezone in which it was written! + long time = ((value instanceof Date) ? + ((Date)value).getTime() : + ((value instanceof Calendar) ? + ((Calendar)value).getTimeInMillis() : + ((Number)value).longValue())); + time += getToLocalTimeZoneOffset(time); + time += MILLIS_BETWEEN_EPOCH_AND_1900; + return time / MILLISECONDS_PER_DAY; + } + + /** + * Gets the timezone offset from UTC to local time for the given time + * (including DST). + */ + private long getToLocalTimeZoneOffset(long time) + { + Calendar c = getCalendar(); + c.setTimeInMillis(time); + return ((long)c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET)); + } + + /** + * Gets the timezone offset from local time to UTC for the given time + * (including DST). + */ + private long getFromLocalTimeZoneOffset(long time) + { + // getting from local time back to UTC is a little wonky (and not + // guaranteed to get you back to where you started) + Calendar c = getCalendar(); + c.setTimeInMillis(time); + // apply the zone offset first to get us closer to the original time + c.setTimeInMillis(time - c.get(Calendar.ZONE_OFFSET)); + return ((long)c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET)); + } + + /** + * Decodes a GUID value. + */ + private static String readGUIDValue(ByteBuffer buffer, ByteOrder order) + { + if(order != ByteOrder.BIG_ENDIAN) { + byte[] tmpArr = ByteUtil.getBytes(buffer, 16); + + // the first 3 guid components are integer components which need to + // respect endianness, so swap 4-byte int, 2-byte int, 2-byte int + ByteUtil.swap4Bytes(tmpArr, 0); + ByteUtil.swap2Bytes(tmpArr, 4); + ByteUtil.swap2Bytes(tmpArr, 6); + buffer = ByteBuffer.wrap(tmpArr); + } + + StringBuilder sb = new StringBuilder(22); + sb.append("{"); + sb.append(ByteUtil.toHexString(buffer, 0, 4, + false)); + sb.append("-"); + sb.append(ByteUtil.toHexString(buffer, 4, 2, + false)); + sb.append("-"); + sb.append(ByteUtil.toHexString(buffer, 6, 2, + false)); + sb.append("-"); + sb.append(ByteUtil.toHexString(buffer, 8, 2, + false)); + sb.append("-"); + sb.append(ByteUtil.toHexString(buffer, 10, 6, + false)); + sb.append("}"); + return (sb.toString()); + } + + /** + * Writes a GUID value. + */ + private static void writeGUIDValue(ByteBuffer buffer, Object value, + ByteOrder order) + throws IOException + { + Matcher m = GUID_PATTERN.matcher(toCharSequence(value)); + if(m.matches()) { + ByteBuffer origBuffer = null; + byte[] tmpBuf = null; + if(order != ByteOrder.BIG_ENDIAN) { + // write to a temp buf so we can do some swapping below + origBuffer = buffer; + tmpBuf = new byte[16]; + buffer = ByteBuffer.wrap(tmpBuf); + } + + ByteUtil.writeHexString(buffer, m.group(1)); + ByteUtil.writeHexString(buffer, m.group(2)); + ByteUtil.writeHexString(buffer, m.group(3)); + ByteUtil.writeHexString(buffer, m.group(4)); + ByteUtil.writeHexString(buffer, m.group(5)); + + if(tmpBuf != null) { + // the first 3 guid components are integer components which need to + // respect endianness, so swap 4-byte int, 2-byte int, 2-byte int + ByteUtil.swap4Bytes(tmpBuf, 0); + ByteUtil.swap2Bytes(tmpBuf, 4); + ByteUtil.swap2Bytes(tmpBuf, 6); + origBuffer.put(tmpBuf); + } + + } else { + throw new IOException("Invalid GUID: " + value); + } + } + + /** + * Write an LVAL column into a ByteBuffer inline if it fits, otherwise in + * other data page(s). + * @param value Value of the LVAL column + * @return A buffer containing the LVAL definition and (possibly) the column + * value (unless written to other pages) + * @usage _advanced_method_ + */ + public ByteBuffer writeLongValue(byte[] value, + int remainingRowLength) throws IOException + { + if(value.length > getType().getMaxSize()) { + throw new IOException("value too big for column, max " + + getType().getMaxSize() + ", got " + + value.length); + } + + // determine which type to write + byte type = 0; + int lvalDefLen = getFormat().SIZE_LONG_VALUE_DEF; + if(((getFormat().SIZE_LONG_VALUE_DEF + value.length) <= remainingRowLength) + && (value.length <= getFormat().MAX_INLINE_LONG_VALUE_SIZE)) { + type = LONG_VALUE_TYPE_THIS_PAGE; + lvalDefLen += value.length; + } else if(value.length <= getFormat().MAX_LONG_VALUE_ROW_SIZE) { + type = LONG_VALUE_TYPE_OTHER_PAGE; + } else { + type = LONG_VALUE_TYPE_OTHER_PAGES; + } + + ByteBuffer def = getPageChannel().createBuffer(lvalDefLen); + // take length and apply type to first byte + int lengthWithFlags = value.length | (type << 24); + def.putInt(lengthWithFlags); + + if(type == LONG_VALUE_TYPE_THIS_PAGE) { + // write long value inline + def.putInt(0); + def.putInt(0); //Unknown + def.put(value); + } else { + + ByteBuffer lvalPage = null; + int firstLvalPageNum = PageChannel.INVALID_PAGE_NUMBER; + byte firstLvalRow = 0; + + // write other page(s) + switch(type) { + case LONG_VALUE_TYPE_OTHER_PAGE: + lvalPage = _lvalBufferH.getLongValuePage(value.length); + firstLvalPageNum = _lvalBufferH.getPageNumber(); + firstLvalRow = (byte)TableImpl.addDataPageRow(lvalPage, value.length, + getFormat(), 0); + lvalPage.put(value); + getPageChannel().writePage(lvalPage, firstLvalPageNum); + break; + + case LONG_VALUE_TYPE_OTHER_PAGES: + + ByteBuffer buffer = ByteBuffer.wrap(value); + int remainingLen = buffer.remaining(); + buffer.limit(0); + lvalPage = _lvalBufferH.getLongValuePage(remainingLen); + firstLvalPageNum = _lvalBufferH.getPageNumber(); + firstLvalRow = (byte)TableImpl.getRowsOnDataPage(lvalPage, getFormat()); + int lvalPageNum = firstLvalPageNum; + ByteBuffer nextLvalPage = null; + int nextLvalPageNum = 0; + int nextLvalRowNum = 0; + while(remainingLen > 0) { + lvalPage.clear(); + + // figure out how much we will put in this page (we need 4 bytes for + // the next page pointer) + int chunkLength = Math.min(getFormat().MAX_LONG_VALUE_ROW_SIZE - 4, + remainingLen); + + // figure out if we will need another page, and if so, allocate it + if(chunkLength < remainingLen) { + // force a new page to be allocated for the chunk after this + _lvalBufferH.clear(); + nextLvalPage = _lvalBufferH.getLongValuePage( + (remainingLen - chunkLength) + 4); + nextLvalPageNum = _lvalBufferH.getPageNumber(); + nextLvalRowNum = TableImpl.getRowsOnDataPage(nextLvalPage, + getFormat()); + } else { + nextLvalPage = null; + nextLvalPageNum = 0; + nextLvalRowNum = 0; + } + + // add row to this page + byte lvalRow = (byte)TableImpl.addDataPageRow(lvalPage, chunkLength + 4, + getFormat(), 0); + + // write next page info + lvalPage.put((byte)nextLvalRowNum); // row number + ByteUtil.put3ByteInt(lvalPage, nextLvalPageNum); // page number + + // write this page's chunk of data + buffer.limit(buffer.limit() + chunkLength); + lvalPage.put(buffer); + remainingLen -= chunkLength; + + // write new page to database + getPageChannel().writePage(lvalPage, lvalPageNum); + + // move to next page + lvalPage = nextLvalPage; + lvalPageNum = nextLvalPageNum; + } + break; + + default: + throw new IOException("Unrecognized long value type: " + type); + } + + // update def + def.put(firstLvalRow); + ByteUtil.put3ByteInt(def, firstLvalPageNum); + def.putInt(0); //Unknown + + } + + def.flip(); + return def; + } + + /** + * Writes the header info for a long value page. + */ + private void writeLongValueHeader(ByteBuffer lvalPage) + { + lvalPage.put(PageTypes.DATA); //Page type + lvalPage.put((byte) 1); //Unknown + lvalPage.putShort((short)getFormat().DATA_PAGE_INITIAL_FREE_SPACE); //Free space + lvalPage.put((byte) 'L'); + lvalPage.put((byte) 'V'); + lvalPage.put((byte) 'A'); + lvalPage.put((byte) 'L'); + lvalPage.putInt(0); //unknown + lvalPage.putShort((short)0); // num rows in page + } + + /** + * Serialize an Object into a raw byte value for this column in little + * endian order + * @param obj Object to serialize + * @return A buffer containing the bytes + * @usage _advanced_method_ + */ + public ByteBuffer write(Object obj, int remainingRowLength) + throws IOException + { + return write(obj, remainingRowLength, PageChannel.DEFAULT_BYTE_ORDER); + } + + /** + * Serialize an Object into a raw byte value for this column + * @param obj Object to serialize + * @param order Order in which to serialize + * @return A buffer containing the bytes + * @usage _advanced_method_ + */ + public ByteBuffer write(Object obj, int remainingRowLength, ByteOrder order) + throws IOException + { + if(isRawData(obj)) { + // just slap it right in (not for the faint of heart!) + return ByteBuffer.wrap(((RawData)obj).getBytes()); + } + + if(!isVariableLength() || !getType().isVariableLength()) { + return writeFixedLengthField(obj, order); + } + + // var length column + if(!getType().isLongValue()) { + + // this is an "inline" var length field + switch(getType()) { + case NUMERIC: + // don't ask me why numerics are "var length" columns... + ByteBuffer buffer = getPageChannel().createBuffer( + getType().getFixedSize(), order); + writeNumericValue(buffer, obj); + buffer.flip(); + return buffer; + + case TEXT: + byte[] encodedData = encodeTextValue( + obj, 0, getLengthInUnits(), false).array(); + obj = encodedData; + break; + + case BINARY: + case UNKNOWN_0D: + case UNSUPPORTED_VARLEN: + // should already be "encoded" + break; + default: + throw new RuntimeException("unexpected inline var length type: " + + getType()); + } + + ByteBuffer buffer = ByteBuffer.wrap(toByteArray(obj)); + buffer.order(order); + return buffer; + } + + // var length, long value column + switch(getType()) { + case OLE: + // should already be "encoded" + break; + case MEMO: + int maxMemoChars = DataType.MEMO.toUnitSize(DataType.MEMO.getMaxSize()); + obj = encodeTextValue(obj, 0, maxMemoChars, false).array(); + break; + default: + throw new RuntimeException("unexpected var length, long value type: " + + getType()); + } + + // create long value buffer + return writeLongValue(toByteArray(obj), remainingRowLength); + } + + /** + * Serialize an Object into a raw byte value for this column + * @param obj Object to serialize + * @param order Order in which to serialize + * @return A buffer containing the bytes + * @usage _advanced_method_ + */ + public ByteBuffer writeFixedLengthField(Object obj, ByteOrder order) + throws IOException + { + int size = getType().getFixedSize(_columnLength); + + // create buffer for data + ByteBuffer buffer = getPageChannel().createBuffer(size, order); + + // since booleans are not written by this method, it's safe to convert any + // incoming boolean into an integer. + obj = booleanToInteger(obj); + + switch(getType()) { + case BOOLEAN: + //Do nothing + break; + case BYTE: + buffer.put(toNumber(obj).byteValue()); + break; + case INT: + buffer.putShort(toNumber(obj).shortValue()); + break; + case LONG: + buffer.putInt(toNumber(obj).intValue()); + break; + case MONEY: + writeCurrencyValue(buffer, obj); + break; + case FLOAT: + buffer.putFloat(toNumber(obj).floatValue()); + break; + case DOUBLE: + buffer.putDouble(toNumber(obj).doubleValue()); + break; + case SHORT_DATE_TIME: + writeDateValue(buffer, obj); + break; + case TEXT: + // apparently text numeric values are also occasionally written as fixed + // length... + int numChars = getLengthInUnits(); + // force uncompressed encoding for fixed length text + buffer.put(encodeTextValue(obj, numChars, numChars, true)); + break; + case GUID: + writeGUIDValue(buffer, obj, order); + break; + case NUMERIC: + // yes, that's right, occasionally numeric values are written as fixed + // length... + writeNumericValue(buffer, obj); + break; + case BINARY: + case UNKNOWN_0D: + case UNKNOWN_11: + case COMPLEX_TYPE: + buffer.putInt(toNumber(obj).intValue()); + break; + case UNSUPPORTED_FIXEDLEN: + byte[] bytes = toByteArray(obj); + if(bytes.length != getLength()) { + throw new IOException("Invalid fixed size binary data, size " + + getLength() + ", got " + bytes.length); + } + buffer.put(bytes); + break; + default: + throw new IOException("Unsupported data type: " + getType()); + } + buffer.flip(); + return buffer; + } + + /** + * Decodes a compressed or uncompressed text value. + */ + private String decodeTextValue(byte[] data) + throws IOException + { + try { + + // see if data is compressed. the 0xFF, 0xFE sequence indicates that + // compression is used (sort of, see algorithm below) + boolean isCompressed = ((data.length > 1) && + (data[0] == TEXT_COMPRESSION_HEADER[0]) && + (data[1] == TEXT_COMPRESSION_HEADER[1])); + + if(isCompressed) { + + Expand expander = new Expand(); + + // this is a whacky compression combo that switches back and forth + // between compressed/uncompressed using a 0x00 byte (starting in + // compressed mode) + StringBuilder textBuf = new StringBuilder(data.length); + // start after two bytes indicating compression use + int dataStart = TEXT_COMPRESSION_HEADER.length; + int dataEnd = dataStart; + boolean inCompressedMode = true; + while(dataEnd < data.length) { + if(data[dataEnd] == (byte)0x00) { + + // handle current segment + decodeTextSegment(data, dataStart, dataEnd, inCompressedMode, + expander, textBuf); + inCompressedMode = !inCompressedMode; + ++dataEnd; + dataStart = dataEnd; + + } else { + ++dataEnd; + } + } + // handle last segment + decodeTextSegment(data, dataStart, dataEnd, inCompressedMode, + expander, textBuf); + + return textBuf.toString(); + + } + + return decodeUncompressedText(data, getCharset()); + + } catch (IllegalInputException e) { + throw (IOException) + new IOException("Can't expand text column").initCause(e); + } catch (EndOfInputException e) { + throw (IOException) + new IOException("Can't expand text column").initCause(e); + } + } + + /** + * Decodes a segnment of a text value into the given buffer according to the + * given status of the segment (compressed/uncompressed). + */ + private void decodeTextSegment(byte[] data, int dataStart, int dataEnd, + boolean inCompressedMode, Expand expander, + StringBuilder textBuf) + throws IllegalInputException, EndOfInputException + { + if(dataEnd <= dataStart) { + // no data + return; + } + int dataLength = dataEnd - dataStart; + if(inCompressedMode) { + // handle compressed data + byte[] tmpData = ByteUtil.copyOf(data, dataStart, dataLength); + expander.reset(); + textBuf.append(expander.expand(tmpData)); + } else { + // handle uncompressed data + textBuf.append(decodeUncompressedText(data, dataStart, dataLength, + getCharset())); + } + } + + /** + * @param textBytes bytes of text to decode + * @return the decoded string + */ + private static CharBuffer decodeUncompressedText( + byte[] textBytes, int startPos, int length, Charset charset) + { + return charset.decode(ByteBuffer.wrap(textBytes, startPos, length)); + } + + /** + * Encodes a text value, possibly compressing. + */ + private ByteBuffer encodeTextValue(Object obj, int minChars, int maxChars, + boolean forceUncompressed) + throws IOException + { + CharSequence text = toCharSequence(obj); + if((text.length() > maxChars) || (text.length() < minChars)) { + throw new IOException("Text is wrong length for " + getType() + + " column, max " + maxChars + + ", min " + minChars + ", got " + text.length()); + } + + // may only compress if column type allows it + if(!forceUncompressed && isCompressedUnicode() && + (text.length() <= getFormat().MAX_COMPRESSED_UNICODE_SIZE)) { + + // for now, only do very simple compression (only compress text which is + // all ascii text) + if(isAsciiCompressible(text)) { + + byte[] encodedChars = new byte[TEXT_COMPRESSION_HEADER.length + + text.length()]; + encodedChars[0] = TEXT_COMPRESSION_HEADER[0]; + encodedChars[1] = TEXT_COMPRESSION_HEADER[1]; + for(int i = 0; i < text.length(); ++i) { + encodedChars[i + TEXT_COMPRESSION_HEADER.length] = + (byte)text.charAt(i); + } + return ByteBuffer.wrap(encodedChars); + } + } + + return encodeUncompressedText(text, getCharset()); + } + + /** + * Returns {@code true} if the given text can be compressed using simple + * ASCII encoding, {@code false} otherwise. + */ + private static boolean isAsciiCompressible(CharSequence text) { + // only attempt to compress > 2 chars (compressing less than 3 chars would + // not result in a space savings due to the 2 byte compression header) + if(text.length() <= TEXT_COMPRESSION_HEADER.length) { + return false; + } + // now, see if it is all printable ASCII + for(int i = 0; i < text.length(); ++i) { + char c = text.charAt(i); + if(!Compress.isAsciiCrLfOrTab(c)) { + return false; + } + } + return true; + } + + /** + * Constructs a byte containing the flags for this column. + */ + private static byte getColumnBitFlags(ColumnBuilder col) { + byte flags = UNKNOWN_FLAG_MASK; + if(!col.getType().isVariableLength()) { + flags |= FIXED_LEN_FLAG_MASK; + } + if(col.isAutoNumber()) { + byte autoNumFlags = 0; + switch(col.getType()) { + case LONG: + case COMPLEX_TYPE: + autoNumFlags = AUTO_NUMBER_FLAG_MASK; + break; + case GUID: + autoNumFlags = AUTO_NUMBER_GUID_FLAG_MASK; + break; + default: + // unknown autonum type + } + flags |= autoNumFlags; + } + if(col.isHyperlink()) { + flags |= HYPERLINK_FLAG_MASK; + } + return flags; + } + + @Override + public String toString() { + StringBuilder rtn = new StringBuilder(); + rtn.append("\tName: (" + _table.getName() + ") " + _name); + byte typeValue = _type.getValue(); + if(_type.isUnsupported()) { + typeValue = getUnknownDataType(); + } + rtn.append("\n\tType: 0x" + Integer.toHexString(typeValue) + + " (" + _type + ")"); + rtn.append("\n\tNumber: " + _columnNumber); + rtn.append("\n\tLength: " + _columnLength); + rtn.append("\n\tVariable length: " + _variableLength); + if(_type.isTextual()) { + rtn.append("\n\tCompressed Unicode: " + _textInfo._compressedUnicode); + rtn.append("\n\tText Sort order: " + _textInfo._sortOrder); + if(_textInfo._codePage > 0) { + rtn.append("\n\tText Code Page: " + _textInfo._codePage); + } + if(isAppendOnly()) { + rtn.append("\n\tAppend only: " + isAppendOnly()); + } + if(isHyperlink()) { + rtn.append("\n\tHyperlink: " + isHyperlink()); + } + } + if(_autoNumber) { + rtn.append("\n\tLast AutoNumber: " + _autoNumberGenerator.getLast()); + } + if(_complexInfo != null) { + rtn.append("\n\tComplexInfo: " + _complexInfo); + } + rtn.append("\n\n"); + return rtn.toString(); + } + + /** + * @param textBytes bytes of text to decode + * @param charset relevant charset + * @return the decoded string + * @usage _advanced_method_ + */ + public static String decodeUncompressedText(byte[] textBytes, + Charset charset) + { + return decodeUncompressedText(textBytes, 0, textBytes.length, charset) + .toString(); + } + + /** + * @param text Text to encode + * @param charset database charset + * @return A buffer with the text encoded + * @usage _advanced_method_ + */ + public static ByteBuffer encodeUncompressedText(CharSequence text, + Charset charset) + { + CharBuffer cb = ((text instanceof CharBuffer) ? + (CharBuffer)text : CharBuffer.wrap(text)); + return charset.encode(cb); + } + + + /** + * Orders Columns by column number. + * @usage _general_method_ + */ + public int compareTo(ColumnImpl other) { + if (_columnNumber > other.getColumnNumber()) { + return 1; + } else if (_columnNumber < other.getColumnNumber()) { + return -1; + } else { + return 0; + } + } + + /** + * @param columns A list of columns in a table definition + * @return The number of variable length columns found in the list + * @usage _advanced_method_ + */ + public static short countVariableLength(List columns) { + short rtn = 0; + for (ColumnBuilder col : columns) { + if (col.getType().isVariableLength()) { + rtn++; + } + } + return rtn; + } + + /** + * @param columns A list of columns in a table definition + * @return The number of variable length columns which are not long values + * found in the list + * @usage _advanced_method_ + */ + public static short countNonLongVariableLength(List columns) { + short rtn = 0; + for (ColumnBuilder col : columns) { + if (col.getType().isVariableLength() && !col.getType().isLongValue()) { + rtn++; + } + } + return rtn; + } + + /** + * @return an appropriate BigDecimal representation of the given object. + * null is returned as 0 and Numbers are converted + * using their double representation. + */ + private static BigDecimal toBigDecimal(Object value) + { + if(value == null) { + return BigDecimal.ZERO; + } else if(value instanceof BigDecimal) { + return (BigDecimal)value; + } else if(value instanceof BigInteger) { + return new BigDecimal((BigInteger)value); + } else if(value instanceof Number) { + return new BigDecimal(((Number)value).doubleValue()); + } + return new BigDecimal(value.toString()); + } + + /** + * @return an appropriate Number representation of the given object. + * null is returned as 0 and Strings are parsed as + * Doubles. + */ + private static Number toNumber(Object value) + { + if(value == null) { + return BigDecimal.ZERO; + } if(value instanceof Number) { + return (Number)value; + } + return Double.valueOf(value.toString()); + } + + /** + * @return an appropriate CharSequence representation of the given object. + * @usage _advanced_method_ + */ + public static CharSequence toCharSequence(Object value) + throws IOException + { + if(value == null) { + return null; + } else if(value instanceof CharSequence) { + return (CharSequence)value; + } else if(value instanceof Clob) { + try { + Clob c = (Clob)value; + // note, start pos is 1-based + return c.getSubString(1L, (int)c.length()); + } catch(SQLException e) { + throw (IOException)(new IOException(e.getMessage())).initCause(e); + } + } else if(value instanceof Reader) { + char[] buf = new char[8 * 1024]; + StringBuilder sout = new StringBuilder(); + Reader in = (Reader)value; + int read = 0; + while((read = in.read(buf)) != -1) { + sout.append(buf, 0, read); + } + return sout; + } + + return value.toString(); + } + + /** + * @return an appropriate byte[] representation of the given object. + * @usage _advanced_method_ + */ + public static byte[] toByteArray(Object value) + throws IOException + { + if(value == null) { + return null; + } else if(value instanceof byte[]) { + return (byte[])value; + } else if(value instanceof Blob) { + try { + Blob b = (Blob)value; + // note, start pos is 1-based + return b.getBytes(1L, (int)b.length()); + } catch(SQLException e) { + throw (IOException)(new IOException(e.getMessage())).initCause(e); + } + } + + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + + if(value instanceof InputStream) { + byte[] buf = new byte[8 * 1024]; + InputStream in = (InputStream)value; + int read = 0; + while((read = in.read(buf)) != -1) { + bout.write(buf, 0, read); + } + } else { + // if all else fails, serialize it + ObjectOutputStream oos = new ObjectOutputStream(bout); + oos.writeObject(value); + oos.close(); + } + + return bout.toByteArray(); + } + + /** + * Interpret a boolean value (null == false) + * @usage _advanced_method_ + */ + public static boolean toBooleanValue(Object obj) { + return ((obj != null) && ((Boolean)obj).booleanValue()); + } + + /** + * Swaps the bytes of the given numeric in place. + */ + private static void fixNumericByteOrder(byte[] bytes) + { + // fix endianness of each 4 byte segment + for(int i = 0; i < 4; ++i) { + ByteUtil.swap4Bytes(bytes, i * 4); + } + } + + /** + * Treat booleans as integers (C-style). + */ + protected static Object booleanToInteger(Object obj) { + if (obj instanceof Boolean) { + obj = ((Boolean) obj) ? 1 : 0; + } + return obj; + } + + /** + * Returns a wrapper for raw column data that can be written without + * understanding the data. Useful for wrapping unparseable data for + * re-writing. + */ + static RawData rawDataWrapper(byte[] bytes) { + return new RawData(bytes); + } + + /** + * Returs {@code true} if the given value is "raw" column data, + * {@code false} otherwise. + * @usage _advanced_method_ + */ + public static boolean isRawData(Object value) { + return(value instanceof RawData); + } + + /** + * Writes the column definitions into a table definition buffer. + * @param buffer Buffer to write to + * @param columns List of Columns to write definitions for + */ + protected static void writeDefinitions(TableCreator creator, ByteBuffer buffer) + throws IOException + { + List columns = creator.getColumns(); + short fixedOffset = (short) 0; + short variableOffset = (short) 0; + // we specifically put the "long variable" values after the normal + // variable length values so that we have a better chance of fitting it + // all (because "long variable" values can go in separate pages) + short longVariableOffset = countNonLongVariableLength(columns); + for (ColumnBuilder col : columns) { + + buffer.put(col.getType().getValue()); + buffer.putInt(TableImpl.MAGIC_TABLE_NUMBER); //constant magic number + buffer.putShort(col.getColumnNumber()); //Column Number + if (col.getType().isVariableLength()) { + if(!col.getType().isLongValue()) { + buffer.putShort(variableOffset++); + } else { + buffer.putShort(longVariableOffset++); + } + } else { + buffer.putShort((short) 0); + } + buffer.putShort(col.getColumnNumber()); //Column Number again + if(col.getType().isTextual()) { + // this will write 4 bytes (note we don't support writing dbs which + // use the text code page) + writeSortOrder(buffer, col.getTextSortOrder(), creator.getFormat()); + } else { + if(col.getType().getHasScalePrecision()) { + buffer.put(col.getPrecision()); // numeric precision + buffer.put(col.getScale()); // numeric scale + } else { + buffer.put((byte) 0x00); //unused + buffer.put((byte) 0x00); //unused + } + buffer.putShort((short) 0); //Unknown + } + buffer.put(getColumnBitFlags(col)); // misc col flags + if (col.isCompressedUnicode()) { //Compressed + buffer.put((byte) 1); + } else { + buffer.put((byte) 0); + } + buffer.putInt(0); //Unknown, but always 0. + //Offset for fixed length columns + if (col.getType().isVariableLength()) { + buffer.putShort((short) 0); + } else { + buffer.putShort(fixedOffset); + fixedOffset += col.getType().getFixedSize(col.getLength()); + } + if(!col.getType().isLongValue()) { + buffer.putShort(col.getLength()); //Column length + } else { + buffer.putShort((short)0x0000); // unused + } + } + for (ColumnBuilder col : columns) { + TableImpl.writeName(buffer, col.getName(), creator.getCharset()); + } + } + + /** + * Reads the sort order info from the given buffer from the given position. + */ + static SortOrder readSortOrder(ByteBuffer buffer, int position, + JetFormat format) + { + short value = buffer.getShort(position); + byte version = 0; + if(format.SIZE_SORT_ORDER == 4) { + version = buffer.get(position + 3); + } + + if(value == 0) { + // probably a file we wrote, before handling sort order + return format.DEFAULT_SORT_ORDER; + } + + if(value == GENERAL_SORT_ORDER_VALUE) { + if(version == GENERAL_LEGACY_SORT_ORDER.getVersion()) { + return GENERAL_LEGACY_SORT_ORDER; + } + if(version == GENERAL_SORT_ORDER.getVersion()) { + return GENERAL_SORT_ORDER; + } + } + return new SortOrder(value, version); + } + + /** + * Writes the sort order info to the given buffer at the current position. + */ + private static void writeSortOrder(ByteBuffer buffer, SortOrder sortOrder, + JetFormat format) { + if(sortOrder == null) { + sortOrder = format.DEFAULT_SORT_ORDER; + } + buffer.putShort(sortOrder.getValue()); + if(format.SIZE_SORT_ORDER == 4) { + buffer.put((byte)0x00); // unknown + buffer.put(sortOrder.getVersion()); + } + } + + /** + * Date subclass which stashes the original date bits, in case we attempt to + * re-write the value (will not lose precision). + */ + private static final class DateExt extends Date + { + private static final long serialVersionUID = 0L; + + /** cached bits of the original date value */ + private transient final long _dateBits; + + private DateExt(long time, long dateBits) { + super(time); + _dateBits = dateBits; + } + + public long getDateBits() { + return _dateBits; + } + + private Object writeReplace() throws ObjectStreamException { + // if we are going to serialize this Date, convert it back to a normal + // Date (in case it is restored outside of the context of jackcess) + return new Date(super.getTime()); + } + } + + /** + * Wrapper for raw column data which can be re-written. + */ + private static class RawData implements Serializable + { + private static final long serialVersionUID = 0L; + + private final byte[] _bytes; + + private RawData(byte[] bytes) { + _bytes = bytes; + } + + private byte[] getBytes() { + return _bytes; + } + + @Override + public String toString() { + return "RawData: " + ByteUtil.toHexString(getBytes()); + } + + private Object writeReplace() throws ObjectStreamException { + // if we are going to serialize this, convert it back to a normal + // byte[] (in case it is restored outside of the context of jackcess) + return getBytes(); + } + } + + /** + * Base class for the supported autonumber types. + * @usage _advanced_class_ + */ + public abstract class AutoNumberGenerator + { + protected AutoNumberGenerator() {} + + /** + * Returns the last autonumber generated by this generator. Only valid + * after a call to {@link Table#addRow}, otherwise undefined. + */ + public abstract Object getLast(); + + /** + * Returns the next autonumber for this generator. + *

+ * Warning, calling this externally will result in this value being + * "lost" for the table. + */ + public abstract Object getNext(Object prevRowValue); + + /** + * Returns the type of values generated by this generator. + */ + public abstract DataType getType(); + } + + private final class LongAutoNumberGenerator extends AutoNumberGenerator + { + private LongAutoNumberGenerator() {} + + @Override + public Object getLast() { + // the table stores the last long autonumber used + return getTable().getLastLongAutoNumber(); + } + + @Override + public Object getNext(Object prevRowValue) { + // the table stores the last long autonumber used + return getTable().getNextLongAutoNumber(); + } + + @Override + public DataType getType() { + return DataType.LONG; + } + } + + private final class GuidAutoNumberGenerator extends AutoNumberGenerator + { + private Object _lastAutoNumber; + + private GuidAutoNumberGenerator() {} + + @Override + public Object getLast() { + return _lastAutoNumber; + } + + @Override + public Object getNext(Object prevRowValue) { + // format guids consistently w/ Column.readGUIDValue() + _lastAutoNumber = "{" + UUID.randomUUID() + "}"; + return _lastAutoNumber; + } + + @Override + public DataType getType() { + return DataType.GUID; + } + } + + private final class ComplexTypeAutoNumberGenerator extends AutoNumberGenerator + { + private ComplexTypeAutoNumberGenerator() {} + + @Override + public Object getLast() { + // the table stores the last ComplexType autonumber used + return getTable().getLastComplexTypeAutoNumber(); + } + + @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); + } + + @Override + public DataType getType() { + return DataType.COMPLEX_TYPE; + } + } + + private final class UnsupportedAutoNumberGenerator extends AutoNumberGenerator + { + private final DataType _genType; + + private UnsupportedAutoNumberGenerator(DataType genType) { + _genType = genType; + } + + @Override + public Object getLast() { + return null; + } + + @Override + public Object getNext(Object prevRowValue) { + throw new UnsupportedOperationException(); + } + + @Override + public DataType getType() { + return _genType; + } + } + + + /** + * Information about the sort order (collation) for a textual column. + * @usage _intermediate_class_ + */ + public static final class SortOrder + { + private final short _value; + private final byte _version; + + public SortOrder(short value, byte version) { + _value = value; + _version = version; + } + + public short getValue() { + return _value; + } + + public byte getVersion() { + return _version; + } + + @Override + public int hashCode() { + return _value; + } + + @Override + public boolean equals(Object o) { + return ((this == o) || + ((o != null) && (getClass() == o.getClass()) && + (_value == ((SortOrder)o)._value) && + (_version == ((SortOrder)o)._version))); + } + + @Override + public String toString() { + return _value + "(" + _version + ")"; + } + } + + /** + * Information specific to numeric types. + */ + private static final class NumericInfo + { + /** Numeric precision */ + private byte _precision; + /** Numeric scale */ + private byte _scale; + } + + /** + * Information specific to textual types. + */ + private static final class TextInfo + { + /** whether or not they are compressed */ + private boolean _compressedUnicode; + /** the collating sort order for a text field */ + private SortOrder _sortOrder; + /** the code page for a text field (for certain db versions) */ + private short _codePage; + /** complex column which tracks the version history for this "append only" + column */ + private ColumnImpl _versionHistoryCol; + /** whether or not this is a hyperlink column (only possible for columns + of type MEMO) */ + private boolean _hyperlink; + } + + /** + * Manages secondary page buffers for long value writing. + */ + private abstract class LongValueBufferHolder + { + /** + * Returns a long value data page with space for data of the given length. + */ + public ByteBuffer getLongValuePage(int dataLength) throws IOException { + + TempPageHolder lvalBufferH = getBufferHolder(); + dataLength = Math.min(dataLength, getFormat().MAX_LONG_VALUE_ROW_SIZE); + + ByteBuffer lvalPage = null; + if(lvalBufferH.getPageNumber() != PageChannel.INVALID_PAGE_NUMBER) { + lvalPage = lvalBufferH.getPage(getPageChannel()); + if(TableImpl.rowFitsOnDataPage(dataLength, lvalPage, getFormat())) { + // the current page has space + return lvalPage; +} + } + + // need new page + return findNewPage(dataLength); + } + + protected ByteBuffer findNewPage(int dataLength) throws IOException { + ByteBuffer lvalPage = getBufferHolder().setNewPage(getPageChannel()); + writeLongValueHeader(lvalPage); + return lvalPage; + } + + public int getOwnedPageCount() { + return 0; + } + + /** + * Returns the page number of the current long value data page. + */ + public int getPageNumber() { + return getBufferHolder().getPageNumber(); + } + + /** + * Discards the current the current long value data page. + */ + public void clear() throws IOException { + getBufferHolder().clear(); + } + + protected abstract TempPageHolder getBufferHolder(); + } + + /** + * Manages a common, shared extra page for long values. This is legacy + * behavior from before it was understood that there were additional usage + * maps for each columns. + */ + private final class LegacyLongValueBufferHolder extends LongValueBufferHolder + { + @Override + protected TempPageHolder getBufferHolder() { + return getTable().getLongValueBuffer(); + } + } + + /** + * Manages the column usage maps for long values. + */ + private final class UmapLongValueBufferHolder extends LongValueBufferHolder + { + /** Usage map of pages that this column owns */ + private final UsageMap _ownedPages; + /** Usage map of pages that this column owns with free space on them */ + private final UsageMap _freeSpacePages; + /** page buffer used to write "long value" data */ + private final TempPageHolder _longValueBufferH = + TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); + + private UmapLongValueBufferHolder(UsageMap ownedPages, + UsageMap freeSpacePages) { + _ownedPages = ownedPages; + _freeSpacePages = freeSpacePages; + } + + @Override + protected TempPageHolder getBufferHolder() { + return _longValueBufferH; + } + + @Override + public int getOwnedPageCount() { + return _ownedPages.getPageCount(); + } + + @Override + protected ByteBuffer findNewPage(int dataLength) throws IOException { + + // grab last owned page and check for free space. + ByteBuffer newPage = TableImpl.findFreeRowSpace( + _ownedPages, _freeSpacePages, _longValueBufferH); + + if(newPage != null) { + if(TableImpl.rowFitsOnDataPage(dataLength, newPage, getFormat())) { + return newPage; + } + // discard this page and allocate a new one + clear(); + } + + // nothing found on current pages, need new page + newPage = super.findNewPage(dataLength); + int pageNumber = getPageNumber(); + _ownedPages.addPageNumber(pageNumber); + _freeSpacePages.addPageNumber(pageNumber); + return newPage; + } + + @Override + public void clear() throws IOException { + int pageNumber = getPageNumber(); + if(pageNumber != PageChannel.INVALID_PAGE_NUMBER) { + _freeSpacePages.removePageNumber(pageNumber, true); + } + super.clear(); + } + } +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/ComplexColumnSupport.java b/src/java/com/healthmarketscience/jackcess/impl/ComplexColumnSupport.java new file mode 100644 index 0000000..9cf9b68 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/ComplexColumnSupport.java @@ -0,0 +1,201 @@ +/* +Copyright (c) 2013 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.impl; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.CursorBuilder; +import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.IndexCursor; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.complex.AttachmentColumnInfo; +import com.healthmarketscience.jackcess.complex.ComplexColumnInfo; +import com.healthmarketscience.jackcess.complex.ComplexValue; +import com.healthmarketscience.jackcess.complex.MultiValueColumnInfo; +import com.healthmarketscience.jackcess.complex.UnsupportedColumnInfo; +import com.healthmarketscience.jackcess.complex.VersionHistoryColumnInfo; +import com.healthmarketscience.jackcess.impl.complex.AttachmentColumnInfoImpl; +import com.healthmarketscience.jackcess.impl.complex.MultiValueColumnInfoImpl; +import com.healthmarketscience.jackcess.impl.complex.UnsupportedColumnInfoImpl; +import com.healthmarketscience.jackcess.impl.complex.VersionHistoryColumnInfoImpl; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +/** + * Utility code for loading complex columns. + * + * @author James Ahlborn + */ +public class ComplexColumnSupport +{ + private static final Log LOG = LogFactory.getLog(ComplexColumnSupport.class); + + private static final String COL_COMPLEX_TYPE_OBJECT_ID = "ComplexTypeObjectID"; + private static final String COL_TABLE_ID = "ConceptualTableID"; + private static final String COL_FLAT_TABLE_ID = "FlatTableID"; + + private static final Set MULTI_VALUE_TYPES = EnumSet.of( + DataType.BYTE, DataType.INT, DataType.LONG, DataType.FLOAT, + DataType.DOUBLE, DataType.GUID, DataType.NUMERIC, DataType.TEXT); + + + /** + * Creates a ComplexColumnInfo for a complex column. + */ + public static ComplexColumnInfo create( + ColumnImpl column, ByteBuffer buffer, int offset) + throws IOException + { + int complexTypeId = buffer.getInt( + offset + column.getFormat().OFFSET_COLUMN_COMPLEX_ID); + + DatabaseImpl db = column.getDatabase(); + TableImpl complexColumns = db.getSystemComplexColumns(); + IndexCursor cursor = CursorBuilder.createCursor( + complexColumns, complexColumns.getPrimaryKeyIndex()); + if(!cursor.findFirstRowByEntry(complexTypeId)) { + throw new IOException( + "Could not find complex column info for complex column with id " + + complexTypeId); + } + Row cColRow = cursor.getCurrentRow(); + int tableId = (Integer)cColRow.get(COL_TABLE_ID); + if(tableId != column.getTable().getTableDefPageNumber()) { + throw new IOException( + "Found complex column for table " + tableId + " but expected table " + + column.getTable().getTableDefPageNumber()); + } + int flatTableId = (Integer)cColRow.get(COL_FLAT_TABLE_ID); + int typeObjId = (Integer)cColRow.get(COL_COMPLEX_TYPE_OBJECT_ID); + + TableImpl typeObjTable = db.getTable(typeObjId); + TableImpl flatTable = db.getTable(flatTableId); + + if((typeObjTable == null) || (flatTable == null)) { + throw new IOException( + "Could not find supporting tables (" + typeObjId + ", " + flatTableId + + ") for complex column with id " + complexTypeId); + } + + // we inspect the structore of the "type table" to determine what kind of + // complex info we are dealing with + if(isMultiValueColumn(typeObjTable)) { + return new MultiValueColumnInfoImpl(column, complexTypeId, typeObjTable, + flatTable); + } else if(isAttachmentColumn(typeObjTable)) { + return new AttachmentColumnInfoImpl(column, complexTypeId, typeObjTable, + flatTable); + } else if(isVersionHistoryColumn(typeObjTable)) { + return new VersionHistoryColumnInfoImpl(column, complexTypeId, typeObjTable, + flatTable); + } + + LOG.warn("Unsupported complex column type " + typeObjTable.getName()); + return new UnsupportedColumnInfoImpl(column, complexTypeId, typeObjTable, + flatTable); + } + + + public static boolean isMultiValueColumn(Table typeObjTable) { + // if we found a single value of a "simple" type, then we are dealing with + // a multi-value column + List typeCols = typeObjTable.getColumns(); + return ((typeCols.size() == 1) && + MULTI_VALUE_TYPES.contains(typeCols.get(0).getType())); + } + + public static boolean isAttachmentColumn(Table typeObjTable) { + // attachment data has these columns FileURL(MEMO), FileName(TEXT), + // FileType(TEXT), FileData(OLE), FileTimeStamp(SHORT_DATE_TIME), + // FileFlags(LONG) + List typeCols = typeObjTable.getColumns(); + if(typeCols.size() < 6) { + return false; + } + + int numMemo = 0; + int numText = 0; + int numDate = 0; + int numOle= 0; + int numLong = 0; + + for(Column col : typeCols) { + switch(col.getType()) { + case TEXT: + ++numText; + break; + case LONG: + ++numLong; + break; + case SHORT_DATE_TIME: + ++numDate; + break; + case OLE: + ++numOle; + break; + case MEMO: + ++numMemo; + break; + default: + // ignore + } + } + + // be flexible, allow for extra columns... + return((numMemo >= 1) && (numText >= 2) && (numOle >= 1) && + (numDate >= 1) && (numLong >= 1)); + } + + public static boolean isVersionHistoryColumn(Table typeObjTable) { + // version history data has these columns (MEMO), + // (SHORT_DATE_TIME) + List typeCols = typeObjTable.getColumns(); + if(typeCols.size() < 2) { + return false; + } + + int numMemo = 0; + int numDate = 0; + + for(Column col : typeCols) { + switch(col.getType()) { + case SHORT_DATE_TIME: + ++numDate; + break; + case MEMO: + ++numMemo; + break; + default: + // ignore + } + } + + // be flexible, allow for extra columns... + return((numMemo >= 1) && (numDate >= 1)); + } +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/CursorImpl.java b/src/java/com/healthmarketscience/jackcess/impl/CursorImpl.java new file mode 100644 index 0000000..06cda47 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/CursorImpl.java @@ -0,0 +1,961 @@ +/* +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.impl; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; + +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.Cursor; +import com.healthmarketscience.jackcess.CursorBuilder; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.RuntimeIOException; +import com.healthmarketscience.jackcess.impl.TableImpl.RowState; +import com.healthmarketscience.jackcess.util.ColumnMatcher; +import com.healthmarketscience.jackcess.util.ErrorHandler; +import com.healthmarketscience.jackcess.util.IterableBuilder; +import com.healthmarketscience.jackcess.util.SimpleColumnMatcher; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Manages iteration for a Table. Different cursors provide different methods + * of traversing a table. Cursors should be fairly robust in the face of + * table modification during traversal (although depending on how the table is + * traversed, row updates may or may not be seen). Multiple cursors may + * traverse the same table simultaneously. + *

+ * The Cursor provides a variety of static utility methods to construct + * cursors with given characteristics or easily search for specific values. + * For even friendlier and more flexible construction, see + * {@link CursorBuilder}. + *

+ * Is not thread-safe. + * + * @author James Ahlborn + */ +public abstract class CursorImpl implements Cursor +{ + private static final Log LOG = LogFactory.getLog(CursorImpl.class); + + /** boolean value indicating forward movement */ + public static final boolean MOVE_FORWARD = true; + /** boolean value indicating reverse movement */ + public static final boolean MOVE_REVERSE = false; + + /** identifier for this cursor */ + private final IdImpl _id; + /** owning table */ + private final TableImpl _table; + /** State used for reading the table rows */ + private final RowState _rowState; + /** the first (exclusive) row id for this cursor */ + private final PositionImpl _firstPos; + /** the last (exclusive) row id for this cursor */ + private final PositionImpl _lastPos; + /** the previous row */ + protected PositionImpl _prevPos; + /** the current row */ + protected PositionImpl _curPos; + /** ColumnMatcher to be used when matching column values */ + protected ColumnMatcher _columnMatcher = SimpleColumnMatcher.INSTANCE; + + protected CursorImpl(IdImpl id, TableImpl table, PositionImpl firstPos, + PositionImpl lastPos) { + _id = id; + _table = table; + _rowState = _table.createRowState(); + _firstPos = firstPos; + _lastPos = lastPos; + _curPos = firstPos; + _prevPos = firstPos; + } + + /** + * Creates a normal, un-indexed cursor for the given table. + * @param table the table over which this cursor will traverse + */ + public static CursorImpl createCursor(TableImpl table) { + return new TableScanCursor(table); + } + + public RowState getRowState() { + return _rowState; + } + + public IdImpl getId() { + return _id; + } + + public TableImpl getTable() { + return _table; + } + + public JetFormat getFormat() { + return getTable().getFormat(); + } + + public PageChannel getPageChannel() { + return getTable().getPageChannel(); + } + + public ErrorHandler getErrorHandler() { + return _rowState.getErrorHandler(); + } + + public void setErrorHandler(ErrorHandler newErrorHandler) { + _rowState.setErrorHandler(newErrorHandler); + } + + public ColumnMatcher getColumnMatcher() { + return _columnMatcher; + } + + public void setColumnMatcher(ColumnMatcher columnMatcher) { + if(columnMatcher == null) { + columnMatcher = getDefaultColumnMatcher(); + } + _columnMatcher = columnMatcher; + } + + /** + * Returns the default ColumnMatcher for this Cursor. + */ + protected ColumnMatcher getDefaultColumnMatcher() { + return SimpleColumnMatcher.INSTANCE; + } + + public SavepointImpl getSavepoint() { + return new SavepointImpl(_id, _curPos, _prevPos); + } + + public void restoreSavepoint(Savepoint savepoint) + throws IOException + { + restoreSavepoint((SavepointImpl)savepoint); + } + + public void restoreSavepoint(SavepointImpl savepoint) + throws IOException + { + if(!_id.equals(savepoint.getCursorId())) { + throw new IllegalArgumentException( + "Savepoint " + savepoint + " is not valid for this cursor with id " + + _id); + } + restorePosition(savepoint.getCurrentPosition(), + savepoint.getPreviousPosition()); + } + + /** + * Returns the first row id (exclusive) as defined by this cursor. + */ + protected PositionImpl getFirstPosition() { + return _firstPos; + } + + /** + * Returns the last row id (exclusive) as defined by this cursor. + */ + protected PositionImpl getLastPosition() { + return _lastPos; + } + + public void reset() { + beforeFirst(); + } + + public void beforeFirst() { + reset(MOVE_FORWARD); + } + + public void afterLast() { + reset(MOVE_REVERSE); + } + + public boolean isBeforeFirst() throws IOException { + return isAtBeginning(MOVE_FORWARD); + } + + public boolean isAfterLast() throws IOException { + return isAtBeginning(MOVE_REVERSE); + } + + protected boolean isAtBeginning(boolean moveForward) throws IOException { + if(getDirHandler(moveForward).getBeginningPosition().equals(_curPos)) { + return !recheckPosition(!moveForward); + } + return false; + } + + public boolean isCurrentRowDeleted() throws IOException + { + // we need to ensure that the "deleted" flag has been read for this row + // (or re-read if the table has been recently modified) + TableImpl.positionAtRowData(_rowState, _curPos.getRowId()); + return _rowState.isDeleted(); + } + + /** + * Resets this cursor for traversing the given direction. + */ + protected void reset(boolean moveForward) { + _curPos = getDirHandler(moveForward).getBeginningPosition(); + _prevPos = _curPos; + _rowState.reset(); + } + + public Iterator iterator() { + return new RowIterator(null, true, MOVE_FORWARD); + } + + public IterableBuilder newIterable() { + return new IterableBuilder(this); + } + + public Iterator iterator(IterableBuilder iterBuilder) { + + switch(iterBuilder.getType()) { + case SIMPLE: + return new RowIterator(iterBuilder.getColumnNames(), + iterBuilder.isReset(), iterBuilder.isForward()); + case COLUMN_MATCH: { + @SuppressWarnings("unchecked") + Map.Entry matchPattern = (Map.Entry) + iterBuilder.getMatchPattern(); + return new ColumnMatchIterator( + iterBuilder.getColumnNames(), (ColumnImpl)matchPattern.getKey(), + matchPattern.getValue(), iterBuilder.isReset(), + iterBuilder.isForward(), iterBuilder.getColumnMatcher()); + } + case ROW_MATCH: { + @SuppressWarnings("unchecked") + Map matchPattern = (Map) + iterBuilder.getMatchPattern(); + return new RowMatchIterator( + iterBuilder.getColumnNames(), matchPattern,iterBuilder.isReset(), + iterBuilder.isForward(), iterBuilder.getColumnMatcher()); + } + default: + throw new RuntimeException("unknown match type " + iterBuilder.getType()); + } + } + + public void deleteCurrentRow() throws IOException { + _table.deleteRow(_rowState, _curPos.getRowId()); + } + + public Object[] updateCurrentRow(Object... row) throws IOException { + return _table.updateRow(_rowState, _curPos.getRowId(), row); + } + + public > M updateCurrentRowFromMap(M row) + throws IOException + { + return _table.updateRowFromMap(_rowState, _curPos.getRowId(), row); + } + + public Row getNextRow() throws IOException { + return getNextRow(null); + } + + public Row getNextRow(Collection columnNames) + throws IOException + { + return getAnotherRow(columnNames, MOVE_FORWARD); + } + + public Row getPreviousRow() throws IOException { + return getPreviousRow(null); + } + + public Row getPreviousRow(Collection columnNames) + throws IOException + { + return getAnotherRow(columnNames, MOVE_REVERSE); + } + + + /** + * Moves to another row in the table based on the given direction and + * returns it. + * @param columnNames Only column names in this collection will be returned + * @return another row in this table (Column name -> Column value), where + * "next" may be backwards if moveForward is {@code false}, or + * {@code null} if there is not another row in the given direction. + */ + private Row getAnotherRow(Collection columnNames, + boolean moveForward) + throws IOException + { + if(moveToAnotherRow(moveForward)) { + return getCurrentRow(columnNames); + } + return null; + } + + public boolean moveToNextRow() throws IOException + { + return moveToAnotherRow(MOVE_FORWARD); + } + + public boolean moveToPreviousRow() throws IOException + { + return moveToAnotherRow(MOVE_REVERSE); + } + + /** + * Moves to another row in the given direction as defined by this cursor. + * @return {@code true} if another valid row was found in the given + * direction, {@code false} otherwise + */ + protected boolean moveToAnotherRow(boolean moveForward) + throws IOException + { + if(_curPos.equals(getDirHandler(moveForward).getEndPosition())) { + // already at end, make sure nothing has changed + return recheckPosition(moveForward); + } + + return moveToAnotherRowImpl(moveForward); + } + + /** + * Restores a current position for the cursor (current position becomes + * previous position). + */ + protected void restorePosition(PositionImpl curPos) + throws IOException + { + restorePosition(curPos, _curPos); + } + + /** + * Restores a current and previous position for the cursor if the given + * positions are different from the current positions. + */ + protected final void restorePosition(PositionImpl curPos, + PositionImpl prevPos) + throws IOException + { + if(!curPos.equals(_curPos) || !prevPos.equals(_prevPos)) { + restorePositionImpl(curPos, prevPos); + } + } + + /** + * Restores a current and previous position for the cursor. + */ + protected void restorePositionImpl(PositionImpl curPos, PositionImpl prevPos) + throws IOException + { + // make the current position previous, and the new position current + _prevPos = _curPos; + _curPos = curPos; + _rowState.reset(); + } + + /** + * Rechecks the current position if the underlying data structures have been + * modified. + * @return {@code true} if the cursor ended up in a new position, + * {@code false} otherwise. + */ + private boolean recheckPosition(boolean moveForward) + throws IOException + { + if(isUpToDate()) { + // nothing has changed + return false; + } + + // move the cursor back to the previous position + restorePosition(_prevPos); + return moveToAnotherRowImpl(moveForward); + } + + /** + * Does the grunt work of moving the cursor to another position in the given + * direction. + */ + private boolean moveToAnotherRowImpl(boolean moveForward) + throws IOException + { + _rowState.reset(); + _prevPos = _curPos; + _curPos = findAnotherPosition(_rowState, _curPos, moveForward); + TableImpl.positionAtRowHeader(_rowState, _curPos.getRowId()); + return(!_curPos.equals(getDirHandler(moveForward).getEndPosition())); + } + + public boolean findFirstRow(Column columnPattern, Object valuePattern) + throws IOException + { + return findFirstRow((ColumnImpl)columnPattern, valuePattern); + } + + public boolean findFirstRow(ColumnImpl columnPattern, Object valuePattern) + throws IOException + { + return findAnotherRow(columnPattern, valuePattern, true, MOVE_FORWARD, + _columnMatcher); + } + + public boolean findNextRow(Column columnPattern, Object valuePattern) + throws IOException + { + return findNextRow((ColumnImpl)columnPattern, valuePattern); + } + + public boolean findNextRow(ColumnImpl columnPattern, Object valuePattern) + throws IOException + { + return findAnotherRow(columnPattern, valuePattern, false, MOVE_FORWARD, + _columnMatcher); + } + + protected boolean findAnotherRow(ColumnImpl columnPattern, Object valuePattern, + boolean reset, boolean moveForward, + ColumnMatcher columnMatcher) + throws IOException + { + PositionImpl curPos = _curPos; + PositionImpl prevPos = _prevPos; + boolean found = false; + try { + if(reset) { + reset(moveForward); + } + found = findAnotherRowImpl(columnPattern, valuePattern, moveForward, + columnMatcher); + return found; + } finally { + if(!found) { + try { + restorePosition(curPos, prevPos); + } catch(IOException e) { + LOG.error("Failed restoring position", e); + } + } + } + } + + public boolean findFirstRow(Map rowPattern) throws IOException + { + return findAnotherRow(rowPattern, true, MOVE_FORWARD, _columnMatcher); + } + + public boolean findNextRow(Map rowPattern) + throws IOException + { + return findAnotherRow(rowPattern, false, MOVE_FORWARD, _columnMatcher); + } + + protected boolean findAnotherRow(Map rowPattern, boolean reset, + boolean moveForward, + ColumnMatcher columnMatcher) + throws IOException + { + PositionImpl curPos = _curPos; + PositionImpl prevPos = _prevPos; + boolean found = false; + try { + if(reset) { + reset(moveForward); + } + found = findAnotherRowImpl(rowPattern, moveForward, columnMatcher); + return found; + } finally { + if(!found) { + try { + restorePosition(curPos, prevPos); + } catch(IOException e) { + LOG.error("Failed restoring position", e); + } + } + } + } + + public boolean currentRowMatches(Column columnPattern, Object valuePattern) + throws IOException + { + return currentRowMatches((ColumnImpl)columnPattern, valuePattern); + } + + public boolean currentRowMatches(ColumnImpl columnPattern, Object valuePattern) + throws IOException + { + return currentRowMatchesImpl(columnPattern, valuePattern, _columnMatcher); + } + + protected boolean currentRowMatchesImpl(ColumnImpl columnPattern, + Object valuePattern, + ColumnMatcher columnMatcher) + throws IOException + { + return columnMatcher.matches(getTable(), columnPattern.getName(), + valuePattern, + getCurrentRowValue(columnPattern)); + } + + public boolean currentRowMatches(Map rowPattern) + throws IOException + { + return currentRowMatchesImpl(rowPattern, _columnMatcher); + } + + protected boolean currentRowMatchesImpl(Map rowPattern, + ColumnMatcher columnMatcher) + throws IOException + { + Row row = getCurrentRow(rowPattern.keySet()); + + if(rowPattern.size() != row.size()) { + return false; + } + + for(Map.Entry e : row.entrySet()) { + String columnName = e.getKey(); + if(!columnMatcher.matches(getTable(), columnName, + rowPattern.get(columnName), e.getValue())) { + return false; + } + } + + return true; + } + + /** + * Moves to the next row (as defined by the cursor) where the given column + * has the given value. Caller manages save/restore on failure. + *

+ * Default implementation scans the table from beginning to end. + * + * @param columnPattern column from the table for this cursor which is being + * matched by the valuePattern + * @param valuePattern value which is equal to the corresponding value in + * the matched row + * @return {@code true} if a valid row was found with the given value, + * {@code false} if no row was found + */ + protected boolean findAnotherRowImpl( + ColumnImpl columnPattern, Object valuePattern, boolean moveForward, + ColumnMatcher columnMatcher) + throws IOException + { + while(moveToAnotherRow(moveForward)) { + if(currentRowMatchesImpl(columnPattern, valuePattern, columnMatcher)) { + return true; + } + } + return false; + } + + /** + * Moves to the next row (as defined by the cursor) where the given columns + * have the given values. Caller manages save/restore on failure. + *

+ * Default implementation scans the table from beginning to end. + * + * @param rowPattern column names and values which must be equal to the + * corresponding values in the matched row + * @return {@code true} if a valid row was found with the given values, + * {@code false} if no row was found + */ + protected boolean findAnotherRowImpl(Map rowPattern, + boolean moveForward, + ColumnMatcher columnMatcher) + throws IOException + { + while(moveToAnotherRow(moveForward)) { + if(currentRowMatchesImpl(rowPattern, columnMatcher)) { + return true; + } + } + return false; + } + + public int moveNextRows(int numRows) throws IOException + { + return moveSomeRows(numRows, MOVE_FORWARD); + } + + public int movePreviousRows(int numRows) throws IOException + { + return moveSomeRows(numRows, MOVE_REVERSE); + } + + /** + * Moves as many rows as possible in the given direction up to the given + * number of rows. + * @return the number of rows moved. + */ + private int moveSomeRows(int numRows, boolean moveForward) + throws IOException + { + int numMovedRows = 0; + while((numMovedRows < numRows) && moveToAnotherRow(moveForward)) { + ++numMovedRows; + } + return numMovedRows; + } + + public Row getCurrentRow() throws IOException + { + return getCurrentRow(null); + } + + public Row getCurrentRow(Collection columnNames) + throws IOException + { + return _table.getRow(_rowState, _curPos.getRowId(), columnNames); + } + + public Object getCurrentRowValue(Column column) + throws IOException + { + return getCurrentRowValue((ColumnImpl)column); + } + + public Object getCurrentRowValue(ColumnImpl column) + throws IOException + { + return _table.getRowValue(_rowState, _curPos.getRowId(), column); + } + + public void setCurrentRowValue(Column column, Object value) + throws IOException + { + setCurrentRowValue((ColumnImpl)column, value); + } + + public void setCurrentRowValue(ColumnImpl column, Object value) + throws IOException + { + Object[] row = new Object[_table.getColumnCount()]; + Arrays.fill(row, Column.KEEP_VALUE); + column.setRowValue(row, value); + _table.updateRow(_rowState, _curPos.getRowId(), row); + } + + /** + * Returns {@code true} if this cursor is up-to-date with respect to the + * relevant table and related table objects, {@code false} otherwise. + */ + protected boolean isUpToDate() { + return _rowState.isUpToDate(); + } + + /** + * Returns {@code true} of the current row is valid, {@code false} otherwise. + */ + protected boolean isCurrentRowValid() throws IOException { + return(_curPos.getRowId().isValid() && !isCurrentRowDeleted() && + !isBeforeFirst() && !isAfterLast()); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " CurPosition " + _curPos + + ", PrevPosition " + _prevPos; + } + + /** + * Finds the next non-deleted row after the given row (as defined by this + * cursor) and returns the id of the row, where "next" may be backwards if + * moveForward is {@code false}. If there are no more rows, the returned + * rowId should equal the value returned by {@link #getLastPosition} if + * moving forward and {@link #getFirstPosition} if moving backward. + */ + protected abstract PositionImpl findAnotherPosition(RowState rowState, + PositionImpl curPos, + boolean moveForward) + throws IOException; + + /** + * Returns the DirHandler for the given movement direction. + */ + protected abstract DirHandler getDirHandler(boolean moveForward); + + + /** + * Base implementation of iterator for this cursor, modifiable. + */ + protected abstract class BaseIterator implements Iterator + { + protected final Collection _columnNames; + protected final boolean _moveForward; + protected final ColumnMatcher _colMatcher; + protected Boolean _hasNext; + protected boolean _validRow; + + protected BaseIterator(Collection columnNames, + boolean reset, boolean moveForward, + ColumnMatcher columnMatcher) + { + _columnNames = columnNames; + _moveForward = moveForward; + _colMatcher = ((columnMatcher != null) ? columnMatcher : _columnMatcher); + try { + if(reset) { + reset(_moveForward); + } else if(isCurrentRowValid()) { + _hasNext = _validRow = true; + } + } catch(IOException e) { + throw new RuntimeIOException(e); + } + } + + public boolean hasNext() { + if(_hasNext == null) { + try { + _hasNext = findNext(); + _validRow = _hasNext; + } catch(IOException e) { + throw new RuntimeIOException(e); + } + } + return _hasNext; + } + + public Row next() { + if(!hasNext()) { + throw new NoSuchElementException(); + } + try { + Row rtn = getCurrentRow(_columnNames); + _hasNext = null; + return rtn; + } catch(IOException e) { + throw new RuntimeIOException(e); + } + } + + public void remove() { + if(_validRow) { + try { + deleteCurrentRow(); + _validRow = false; + } catch(IOException e) { + throw new RuntimeIOException(e); + } + } else { + throw new IllegalStateException("Not at valid row"); + } + } + + protected abstract boolean findNext() throws IOException; + } + + + /** + * Row iterator for this cursor, modifiable. + */ + private final class RowIterator extends BaseIterator + { + private RowIterator(Collection columnNames, boolean reset, + boolean moveForward) + { + super(columnNames, reset, moveForward, null); + } + + @Override + protected boolean findNext() throws IOException { + return moveToAnotherRow(_moveForward); + } + } + + + /** + * Row iterator for this cursor, modifiable. + */ + private final class ColumnMatchIterator extends BaseIterator + { + private final ColumnImpl _columnPattern; + private final Object _valuePattern; + + private ColumnMatchIterator(Collection columnNames, + ColumnImpl columnPattern, Object valuePattern, + boolean reset, boolean moveForward, + ColumnMatcher columnMatcher) + { + super(columnNames, reset, moveForward, columnMatcher); + _columnPattern = columnPattern; + _valuePattern = valuePattern; + } + + @Override + protected boolean findNext() throws IOException { + return findAnotherRow(_columnPattern, _valuePattern, false, _moveForward, + _colMatcher); + } + } + + + /** + * Row iterator for this cursor, modifiable. + */ + private final class RowMatchIterator extends BaseIterator + { + private final Map _rowPattern; + + private RowMatchIterator(Collection columnNames, + Map rowPattern, + boolean reset, boolean moveForward, + ColumnMatcher columnMatcher) + { + super(columnNames, reset, moveForward, columnMatcher); + _rowPattern = rowPattern; + } + + @Override + protected boolean findNext() throws IOException { + return findAnotherRow(_rowPattern, false, _moveForward, _colMatcher); + } + } + + + /** + * Handles moving the cursor in a given direction. Separates cursor + * logic from value storage. + */ + protected abstract class DirHandler + { + public abstract PositionImpl getBeginningPosition(); + public abstract PositionImpl getEndPosition(); + } + + + /** + * Identifier for a cursor. Will be equal to any other cursor of the same + * type for the same table. Primarily used to check the validity of a + * Savepoint. + */ + protected static final class IdImpl implements Id + { + private final int _tablePageNumber; + private final int _indexNumber; + + protected IdImpl(TableImpl table, IndexImpl index) { + _tablePageNumber = table.getTableDefPageNumber(); + _indexNumber = ((index != null) ? index.getIndexNumber() : -1); + } + + @Override + public int hashCode() { + return _tablePageNumber; + } + + @Override + public boolean equals(Object o) { + return((this == o) || + ((o != null) && (getClass() == o.getClass()) && + (_tablePageNumber == ((IdImpl)o)._tablePageNumber) && + (_indexNumber == ((IdImpl)o)._indexNumber))); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " " + _tablePageNumber + ":" + _indexNumber; + } + } + + /** + * Value object which maintains the current position of the cursor. + */ + protected static abstract class PositionImpl implements Position + { + protected PositionImpl() { + } + + @Override + public final int hashCode() { + return getRowId().hashCode(); + } + + @Override + public final boolean equals(Object o) { + return((this == o) || + ((o != null) && (getClass() == o.getClass()) && equalsImpl(o))); + } + + /** + * Returns the unique RowId of the position of the cursor. + */ + public abstract RowIdImpl getRowId(); + + /** + * Returns {@code true} if the subclass specific info in a Position is + * equal, {@code false} otherwise. + * @param o object being tested for equality, guaranteed to be the same + * class as this object + */ + protected abstract boolean equalsImpl(Object o); + } + + /** + * Value object which represents a complete save state of the cursor. + */ + protected static final class SavepointImpl implements Savepoint + { + private final IdImpl _cursorId; + private final PositionImpl _curPos; + private final PositionImpl _prevPos; + + private SavepointImpl(IdImpl cursorId, PositionImpl curPos, + PositionImpl prevPos) { + _cursorId = cursorId; + _curPos = curPos; + _prevPos = prevPos; + } + + public IdImpl getCursorId() { + return _cursorId; + } + + public PositionImpl getCurrentPosition() { + return _curPos; + } + + private PositionImpl getPreviousPosition() { + return _prevPos; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " " + _cursorId + " CurPosition " + + _curPos + ", PrevPosition " + _prevPos; + } + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java b/src/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java new file mode 100644 index 0000000..87f7ef1 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java @@ -0,0 +1,2114 @@ +/* +Copyright (c) 2005 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.impl; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.TimeZone; +import java.util.TreeSet; + +import com.healthmarketscience.jackcess.ColumnBuilder; +import com.healthmarketscience.jackcess.Cursor; +import com.healthmarketscience.jackcess.CursorBuilder; +import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.Database; +import com.healthmarketscience.jackcess.IndexBuilder; +import com.healthmarketscience.jackcess.IndexCursor; +import com.healthmarketscience.jackcess.PropertyMap; +import com.healthmarketscience.jackcess.Relationship; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.RuntimeIOException; +import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.impl.query.QueryImpl; +import com.healthmarketscience.jackcess.query.Query; +import com.healthmarketscience.jackcess.util.CaseInsensitiveColumnMatcher; +import com.healthmarketscience.jackcess.util.ErrorHandler; +import com.healthmarketscience.jackcess.util.LinkResolver; +import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +/** + * + * @author Tim McCune + * @usage _general_class_ + */ +public class DatabaseImpl implements Database +{ + private static final Log LOG = LogFactory.getLog(DatabaseImpl.class); + + /** this is the default "userId" used if we cannot find existing info. this + seems to be some standard "Admin" userId for access files */ + private static final byte[] SYS_DEFAULT_SID = new byte[2]; + static { + SYS_DEFAULT_SID[0] = (byte) 0xA6; + SYS_DEFAULT_SID[1] = (byte) 0x33; + } + + /** the default value for the resource path used to load classpath + * resources. + */ + public static final String DEFAULT_RESOURCE_PATH = + "com/healthmarketscience/jackcess/"; + + /** the resource path to be used when loading classpath resources */ + static final String RESOURCE_PATH = + System.getProperty(RESOURCE_PATH_PROPERTY, DEFAULT_RESOURCE_PATH); + + /** whether or not this jvm has "broken" nio support */ + static final boolean BROKEN_NIO = Boolean.TRUE.toString().equalsIgnoreCase( + System.getProperty(BROKEN_NIO_PROPERTY)); + + /** additional internal details about each FileFormat */ + private static final Map FILE_FORMAT_DETAILS = + new EnumMap(Database.FileFormat.class); + + static { + addFileFormatDetails(FileFormat.V1997, null, JetFormat.VERSION_3); + addFileFormatDetails(FileFormat.V2000, "empty", JetFormat.VERSION_4); + addFileFormatDetails(FileFormat.V2003, "empty2003", JetFormat.VERSION_4); + addFileFormatDetails(FileFormat.V2007, "empty2007", JetFormat.VERSION_12); + addFileFormatDetails(FileFormat.V2010, "empty2010", JetFormat.VERSION_14); + addFileFormatDetails(FileFormat.MSISAM, null, JetFormat.VERSION_MSISAM); + } + + /** System catalog always lives on page 2 */ + private static final int PAGE_SYSTEM_CATALOG = 2; + /** Name of the system catalog */ + private static final String TABLE_SYSTEM_CATALOG = "MSysObjects"; + + /** this is the access control bit field for created tables. the value used + is equivalent to full access (Visual Basic DAO PermissionEnum constant: + dbSecFullAccess) */ + private static final Integer SYS_FULL_ACCESS_ACM = 1048575; + + /** ACE table column name of the actual access control entry */ + private static final String ACE_COL_ACM = "ACM"; + /** ACE table column name of the inheritable attributes flag */ + private static final String ACE_COL_F_INHERITABLE = "FInheritable"; + /** ACE table column name of the relevant objectId */ + private static final String ACE_COL_OBJECT_ID = "ObjectId"; + /** ACE table column name of the relevant userId */ + private static final String ACE_COL_SID = "SID"; + + /** Relationship table column name of the column count */ + private static final String REL_COL_COLUMN_COUNT = "ccolumn"; + /** Relationship table column name of the flags */ + private static final String REL_COL_FLAGS = "grbit"; + /** Relationship table column name of the index of the columns */ + private static final String REL_COL_COLUMN_INDEX = "icolumn"; + /** Relationship table column name of the "to" column name */ + private static final String REL_COL_TO_COLUMN = "szColumn"; + /** Relationship table column name of the "to" table name */ + private static final String REL_COL_TO_TABLE = "szObject"; + /** Relationship table column name of the "from" column name */ + private static final String REL_COL_FROM_COLUMN = "szReferencedColumn"; + /** Relationship table column name of the "from" table name */ + private static final String REL_COL_FROM_TABLE = "szReferencedObject"; + /** Relationship table column name of the relationship */ + private static final String REL_COL_NAME = "szRelationship"; + + /** System catalog column name of the page on which system object definitions + are stored */ + private static final String CAT_COL_ID = "Id"; + /** System catalog column name of the name of a system object */ + private static final String CAT_COL_NAME = "Name"; + private static final String CAT_COL_OWNER = "Owner"; + /** System catalog column name of a system object's parent's id */ + private static final String CAT_COL_PARENT_ID = "ParentId"; + /** System catalog column name of the type of a system object */ + private static final String CAT_COL_TYPE = "Type"; + /** System catalog column name of the date a system object was created */ + private static final String CAT_COL_DATE_CREATE = "DateCreate"; + /** System catalog column name of the date a system object was updated */ + private static final String CAT_COL_DATE_UPDATE = "DateUpdate"; + /** System catalog column name of the flags column */ + private static final String CAT_COL_FLAGS = "Flags"; + /** System catalog column name of the properties column */ + private static final String CAT_COL_PROPS = "LvProp"; + /** System catalog column name of the remote database */ + private static final String CAT_COL_DATABASE = "Database"; + /** System catalog column name of the remote table name */ + private static final String CAT_COL_FOREIGN_NAME = "ForeignName"; + + /** top-level parentid for a database */ + private static final int DB_PARENT_ID = 0xF000000; + + /** the maximum size of any of the included "empty db" resources */ + private static final long MAX_EMPTYDB_SIZE = 350000L; + + /** this object is a "system" object */ + static final int SYSTEM_OBJECT_FLAG = 0x80000000; + /** this object is another type of "system" object */ + static final int ALT_SYSTEM_OBJECT_FLAG = 0x02; + /** this object is hidden */ + static final int HIDDEN_OBJECT_FLAG = 0x08; + /** all flags which seem to indicate some type of system object */ + static final int SYSTEM_OBJECT_FLAGS = + SYSTEM_OBJECT_FLAG | ALT_SYSTEM_OBJECT_FLAG; + + /** read-only channel access mode */ + public static final String RO_CHANNEL_MODE = "r"; + /** read/write channel access mode */ + public static final String RW_CHANNEL_MODE = "rw"; + + /** Name of the system object that is the parent of all tables */ + private static final String SYSTEM_OBJECT_NAME_TABLES = "Tables"; + /** Name of the system object that is the parent of all databases */ + private static final String SYSTEM_OBJECT_NAME_DATABASES = "Databases"; + /** Name of the system object that is the parent of all relationships */ + private static final String SYSTEM_OBJECT_NAME_RELATIONSHIPS = + "Relationships"; + /** Name of the table that contains system access control entries */ + private static final String TABLE_SYSTEM_ACES = "MSysACEs"; + /** Name of the table that contains table relationships */ + private static final String TABLE_SYSTEM_RELATIONSHIPS = "MSysRelationships"; + /** Name of the table that contains queries */ + private static final String TABLE_SYSTEM_QUERIES = "MSysQueries"; + /** Name of the table that contains complex type information */ + private static final String TABLE_SYSTEM_COMPLEX_COLS = "MSysComplexColumns"; + /** Name of the main database properties object */ + private static final String OBJECT_NAME_DB_PROPS = "MSysDb"; + /** Name of the summary properties object */ + private static final String OBJECT_NAME_SUMMARY_PROPS = "SummaryInfo"; + /** Name of the user-defined properties object */ + private static final String OBJECT_NAME_USERDEF_PROPS = "UserDefined"; + /** System object type for table definitions */ + static final Short TYPE_TABLE = 1; + /** System object type for query definitions */ + private static final Short TYPE_QUERY = 5; + /** System object type for linked table definitions */ + private static final Short TYPE_LINKED_TABLE = 6; + + /** max number of table lookups to cache */ + private static final int MAX_CACHED_LOOKUP_TABLES = 50; + + /** the columns to read when reading system catalog normally */ + private static Collection SYSTEM_CATALOG_COLUMNS = + new HashSet(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID, + CAT_COL_FLAGS, CAT_COL_DATABASE, + CAT_COL_FOREIGN_NAME)); + /** the columns to read when finding table names */ + private static Collection SYSTEM_CATALOG_TABLE_NAME_COLUMNS = + new HashSet(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID, + CAT_COL_FLAGS, CAT_COL_PARENT_ID)); + /** the columns to read when getting object propertyes */ + private static Collection SYSTEM_CATALOG_PROPS_COLUMNS = + new HashSet(Arrays.asList(CAT_COL_ID, CAT_COL_PROPS)); + + + /** the File of the database */ + private final File _file; + /** Buffer to hold database pages */ + private ByteBuffer _buffer; + /** ID of the Tables system object */ + private Integer _tableParentId; + /** Format that the containing database is in */ + private final JetFormat _format; + /** + * Cache map of UPPERCASE table names to page numbers containing their + * definition and their stored table name (max size + * MAX_CACHED_LOOKUP_TABLES). + */ + private final Map _tableLookup = + new LinkedHashMap() { + private static final long serialVersionUID = 0L; + @Override + protected boolean removeEldestEntry(Map.Entry e) { + return(size() > MAX_CACHED_LOOKUP_TABLES); + } + }; + /** set of table names as stored in the mdb file, created on demand */ + private Set _tableNames; + /** Reads and writes database pages */ + private final PageChannel _pageChannel; + /** System catalog table */ + private TableImpl _systemCatalog; + /** utility table finder */ + private TableFinder _tableFinder; + /** System access control entries table (initialized on first use) */ + private TableImpl _accessControlEntries; + /** System relationships table (initialized on first use) */ + private TableImpl _relationships; + /** System queries table (initialized on first use) */ + private TableImpl _queries; + /** System complex columns table (initialized on first use) */ + private TableImpl _complexCols; + /** SIDs to use for the ACEs added for new tables */ + private final List _newTableSIDs = new ArrayList(); + /** optional error handler to use when row errors are encountered */ + private ErrorHandler _dbErrorHandler; + /** the file format of the database */ + private FileFormat _fileFormat; + /** charset to use when handling text */ + private Charset _charset; + /** timezone to use when handling dates */ + private TimeZone _timeZone; + /** language sort order to be used for textual columns */ + private ColumnImpl.SortOrder _defaultSortOrder; + /** default code page to be used for textual columns (in some dbs) */ + private Short _defaultCodePage; + /** the ordering used for table columns */ + private Table.ColumnOrder _columnOrder; + /** whether or not enforcement of foreign-keys is enabled */ + private boolean _enforceForeignKeys; + /** cache of in-use tables */ + private final TableCache _tableCache = new TableCache(); + /** handler for reading/writing properteies */ + private PropertyMaps.Handler _propsHandler; + /** ID of the Databases system object */ + private Integer _dbParentId; + /** core database properties */ + private PropertyMaps _dbPropMaps; + /** summary properties */ + private PropertyMaps _summaryPropMaps; + /** user-defined properties */ + private PropertyMaps _userDefPropMaps; + /** linked table resolver */ + private LinkResolver _linkResolver; + /** any linked databases which have been opened */ + private Map _linkedDbs; + /** shared state used when enforcing foreign keys */ + private final FKEnforcer.SharedState _fkEnforcerSharedState = + FKEnforcer.initSharedState(); + /** Calendar for use interpreting dates/times in Columns */ + private Calendar _calendar; + + /** + * Open an existing Database. If the existing file is not writeable or the + * readOnly flag is {@code true}, the file will be opened read-only. + * @param mdbFile File containing the database + * @param readOnly iff {@code true}, force opening file in read-only + * mode + * @param channel pre-opened FileChannel. if provided explicitly, it will + * not be closed by this Database instance + * @param autoSync whether or not to enable auto-syncing on write. if + * {@code true}, writes will be immediately flushed to disk. + * This leaves the database in a (fairly) consistent state + * on each write, but can be very inefficient for many + * updates. if {@code false}, flushing to disk happens at + * the jvm's leisure, which can be much faster, but may + * leave the database in an inconsistent state if failures + * are encountered during writing. Writes may be flushed at + * any time using {@link #flush}. + * @param charset Charset to use, if {@code null}, uses default + * @param timeZone TimeZone to use, if {@code null}, uses default + * @param provider CodecProvider for handling page encoding/decoding, may be + * {@code null} if no special encoding is necessary + * @usage _advanced_method_ + */ + public static DatabaseImpl open( + File mdbFile, boolean readOnly, FileChannel channel, + boolean autoSync, Charset charset, TimeZone timeZone, + CodecProvider provider) + throws IOException + { + boolean closeChannel = false; + if(channel == null) { + if(!mdbFile.exists() || !mdbFile.canRead()) { + throw new FileNotFoundException("given file does not exist: " + + mdbFile); + } + + // force read-only for non-writable files + readOnly |= !mdbFile.canWrite(); + + // open file channel + channel = openChannel(mdbFile, readOnly); + closeChannel = true; + } + + boolean success = false; + try { + + if(!readOnly) { + + // verify that format supports writing + JetFormat jetFormat = JetFormat.getFormat(channel); + + if(jetFormat.READ_ONLY) { + throw new IOException("jet format '" + jetFormat + + "' does not support writing"); + } + } + + DatabaseImpl db = new DatabaseImpl(mdbFile, channel, closeChannel, autoSync, + null, charset, timeZone, provider); + success = true; + return db; + + } finally { + if(!success && closeChannel) { + // something blew up, shutdown the channel (quietly) + try { + channel.close(); + } catch(Exception ignored) { + // we don't care + } + } + } + } + + /** + * Create a new Database for the given fileFormat + * @param fileFormat version of new database. + * @param mdbFile Location to write the new database to. If this file + * already exists, it will be overwritten. + * @param channel pre-opened FileChannel. if provided explicitly, it will + * not be closed by this Database instance + * @param autoSync whether or not to enable auto-syncing on write. if + * {@code true}, writes will be immediately flushed to disk. + * This leaves the database in a (fairly) consistent state + * on each write, but can be very inefficient for many + * updates. if {@code false}, flushing to disk happens at + * the jvm's leisure, which can be much faster, but may + * leave the database in an inconsistent state if failures + * are encountered during writing. Writes may be flushed at + * any time using {@link #flush}. + * @param charset Charset to use, if {@code null}, uses default + * @param timeZone TimeZone to use, if {@code null}, uses default + * @usage _advanced_method_ + */ + public static DatabaseImpl create(FileFormat fileFormat, File mdbFile, + FileChannel channel, boolean autoSync, + Charset charset, TimeZone timeZone) + throws IOException + { + FileFormatDetails details = getFileFormatDetails(fileFormat); + if (details.getFormat().READ_ONLY) { + throw new IOException("file format " + fileFormat + + " does not support writing"); + } + + boolean closeChannel = false; + if(channel == null) { + channel = openChannel(mdbFile, false); + closeChannel = true; + } + + boolean success = false; + try { + channel.truncate(0); + transferFrom(channel, getResourceAsStream(details.getEmptyFilePath())); + channel.force(true); + DatabaseImpl db = new DatabaseImpl(mdbFile, channel, closeChannel, autoSync, + fileFormat, charset, timeZone, null); + success = true; + return db; + } finally { + if(!success && closeChannel) { + // something blew up, shutdown the channel (quietly) + try { + channel.close(); + } catch(Exception ignored) { + // we don't care + } + } + } + } + + /** + * Package visible only to support unit tests via DatabaseTest.openChannel(). + * @param mdbFile file to open + * @param readOnly true if read-only + * @return a FileChannel on the given file. + * @exception FileNotFoundException + * if the mode is "r" but the given file object does + * not denote an existing regular file, or if the mode begins + * with "rw" but the given file object does not denote + * an existing, writable regular file and a new regular file of + * that name cannot be created, or if some other error occurs + * while opening or creating the file + */ + static FileChannel openChannel(final File mdbFile, final boolean readOnly) + throws FileNotFoundException + { + final String mode = (readOnly ? RO_CHANNEL_MODE : RW_CHANNEL_MODE); + return new RandomAccessFile(mdbFile, mode).getChannel(); + } + + /** + * Create a new database by reading it in from a FileChannel. + * @param file the File to which the channel is connected + * @param channel File channel of the database. This needs to be a + * FileChannel instead of a ReadableByteChannel because we need to + * randomly jump around to various points in the file. + * @param autoSync whether or not to enable auto-syncing on write. if + * {@code true}, writes will be immediately flushed to disk. + * This leaves the database in a (fairly) consistent state + * on each write, but can be very inefficient for many + * updates. if {@code false}, flushing to disk happens at + * the jvm's leisure, which can be much faster, but may + * leave the database in an inconsistent state if failures + * are encountered during writing. Writes may be flushed at + * any time using {@link #flush}. + * @param fileFormat version of new database (if known) + * @param charset Charset to use, if {@code null}, uses default + * @param timeZone TimeZone to use, if {@code null}, uses default + */ + protected DatabaseImpl(File file, FileChannel channel, boolean closeChannel, + boolean autoSync, FileFormat fileFormat, Charset charset, + TimeZone timeZone, CodecProvider provider) + throws IOException + { + _file = file; + _format = JetFormat.getFormat(channel); + _charset = ((charset == null) ? getDefaultCharset(_format) : charset); + _columnOrder = getDefaultColumnOrder(); + _enforceForeignKeys = getDefaultEnforceForeignKeys(); + _fileFormat = fileFormat; + _pageChannel = new PageChannel(channel, closeChannel, _format, autoSync); + _timeZone = ((timeZone == null) ? getDefaultTimeZone() : timeZone); + if(provider == null) { + provider = DefaultCodecProvider.INSTANCE; + } + // note, it's slighly sketchy to pass ourselves along partially + // constructed, but only our _format and _pageChannel refs should be + // needed + _pageChannel.initialize(this, provider); + _buffer = _pageChannel.createPageBuffer(); + readSystemCatalog(); + } + + public File getFile() { + return _file; + } + + /** + * @usage _advanced_method_ + */ + public PageChannel getPageChannel() { + return _pageChannel; + } + + /** + * @usage _advanced_method_ + */ + public JetFormat getFormat() { + return _format; + } + + /** + * @return The system catalog table + * @usage _advanced_method_ + */ + public TableImpl getSystemCatalog() { + return _systemCatalog; + } + + /** + * @return The system Access Control Entries table (loaded on demand) + * @usage _advanced_method_ + */ + public TableImpl getAccessControlEntries() throws IOException { + if(_accessControlEntries == null) { + _accessControlEntries = getSystemTable(TABLE_SYSTEM_ACES); + if(_accessControlEntries == null) { + throw new IOException("Could not find system table " + + TABLE_SYSTEM_ACES); + } + + } + return _accessControlEntries; + } + + /** + * @return the complex column system table (loaded on demand) + * @usage _advanced_method_ + */ + public TableImpl getSystemComplexColumns() throws IOException { + if(_complexCols == null) { + _complexCols = getSystemTable(TABLE_SYSTEM_COMPLEX_COLS); + if(_complexCols == null) { + throw new IOException("Could not find system table " + + TABLE_SYSTEM_COMPLEX_COLS); + } + } + return _complexCols; + } + + public ErrorHandler getErrorHandler() { + return((_dbErrorHandler != null) ? _dbErrorHandler : ErrorHandler.DEFAULT); + } + + public void setErrorHandler(ErrorHandler newErrorHandler) { + _dbErrorHandler = newErrorHandler; + } + + public LinkResolver getLinkResolver() { + return((_linkResolver != null) ? _linkResolver : LinkResolver.DEFAULT); + } + + public void setLinkResolver(LinkResolver newLinkResolver) { + _linkResolver = newLinkResolver; + } + + public Map getLinkedDatabases() { + return ((_linkedDbs == null) ? Collections.emptyMap() : + Collections.unmodifiableMap(_linkedDbs)); + } + + public TimeZone getTimeZone() { + return _timeZone; + } + + public void setTimeZone(TimeZone newTimeZone) { + if(newTimeZone == null) { + newTimeZone = getDefaultTimeZone(); + } + _timeZone = newTimeZone; + // clear cached calendar when timezone is changed + _calendar = null; + } + + public Charset getCharset() + { + return _charset; + } + + public void setCharset(Charset newCharset) { + if(newCharset == null) { + newCharset = getDefaultCharset(getFormat()); + } + _charset = newCharset; + } + + public Table.ColumnOrder getColumnOrder() { + return _columnOrder; + } + + public void setColumnOrder(Table.ColumnOrder newColumnOrder) { + if(newColumnOrder == null) { + newColumnOrder = getDefaultColumnOrder(); + } + _columnOrder = newColumnOrder; + } + + public boolean isEnforceForeignKeys() { + return _enforceForeignKeys; + } + + public void setEnforceForeignKeys(Boolean newEnforceForeignKeys) { + if(newEnforceForeignKeys == null) { + newEnforceForeignKeys = getDefaultEnforceForeignKeys(); + } + _enforceForeignKeys = newEnforceForeignKeys; + } + + /** + * @usage _advanced_method_ + */ + FKEnforcer.SharedState getFKEnforcerSharedState() { + return _fkEnforcerSharedState; + } + + /** + * @usage _advanced_method_ + */ + Calendar getCalendar() + { + if(_calendar == null) { + _calendar = Calendar.getInstance(_timeZone); + } + return _calendar; + } + + /** + * @returns the current handler for reading/writing properties, creating if + * necessary + */ + private PropertyMaps.Handler getPropsHandler() { + if(_propsHandler == null) { + _propsHandler = new PropertyMaps.Handler(this); + } + return _propsHandler; + } + + public FileFormat getFileFormat() throws IOException { + + if(_fileFormat == null) { + + Map possibleFileFormats = + getFormat().getPossibleFileFormats(); + + if(possibleFileFormats.size() == 1) { + + // single possible format (null key), easy enough + _fileFormat = possibleFileFormats.get(null); + + } else { + + // need to check the "AccessVersion" property + String accessVersion = (String)getDatabaseProperties().getValue( + PropertyMap.ACCESS_VERSION_PROP); + + _fileFormat = possibleFileFormats.get(accessVersion); + + if(_fileFormat == null) { + throw new IllegalStateException("Could not determine FileFormat"); + } + } + } + return _fileFormat; + } + + /** + * @return a (possibly cached) page ByteBuffer for internal use. the + * returned buffer should be released using + * {@link #releaseSharedBuffer} when no longer in use + */ + private ByteBuffer takeSharedBuffer() { + // we try to re-use a single shared _buffer, but occassionally, it may be + // needed by multiple operations at the same time (e.g. loading a + // secondary table while loading a primary table). this method ensures + // that we don't corrupt the _buffer, but instead force the second caller + // to use a new buffer. + if(_buffer != null) { + ByteBuffer curBuffer = _buffer; + _buffer = null; + return curBuffer; + } + return _pageChannel.createPageBuffer(); + } + + /** + * Relinquishes use of a page ByteBuffer returned by + * {@link #takeSharedBuffer}. + */ + private void releaseSharedBuffer(ByteBuffer buffer) { + // we always stuff the returned buffer back into _buffer. it doesn't + // really matter if multiple values over-write, at the end of the day, we + // just need one shared buffer + _buffer = buffer; + } + + /** + * @return the currently configured database default language sort order for + * textual columns + * @usage _intermediate_method_ + */ + public ColumnImpl.SortOrder getDefaultSortOrder() throws IOException { + + if(_defaultSortOrder == null) { + initRootPageInfo(); + } + return _defaultSortOrder; + } + + /** + * @return the currently configured database default code page for textual + * data (may not be relevant to all database versions) + * @usage _intermediate_method_ + */ + public short getDefaultCodePage() throws IOException { + + if(_defaultCodePage == null) { + initRootPageInfo(); + } + return _defaultCodePage; + } + + /** + * Reads various config info from the db page 0. + */ + private void initRootPageInfo() throws IOException { + ByteBuffer buffer = takeSharedBuffer(); + try { + _pageChannel.readPage(buffer, 0); + _defaultSortOrder = ColumnImpl.readSortOrder( + buffer, _format.OFFSET_SORT_ORDER, _format); + _defaultCodePage = buffer.getShort(_format.OFFSET_CODE_PAGE); + } finally { + releaseSharedBuffer(buffer); + } + } + + /** + * @return a PropertyMaps instance decoded from the given bytes (always + * returns non-{@code null} result). + * @usage _intermediate_method_ + */ + public PropertyMaps readProperties(byte[] propsBytes, int objectId) + throws IOException + { + return getPropsHandler().read(propsBytes, objectId); + } + + /** + * Read the system catalog + */ + private void readSystemCatalog() throws IOException { + _systemCatalog = readTable(TABLE_SYSTEM_CATALOG, PAGE_SYSTEM_CATALOG, + SYSTEM_OBJECT_FLAGS); + + try { + _tableFinder = new DefaultTableFinder( + _systemCatalog.newCursor() + .setIndexByColumnNames(CAT_COL_PARENT_ID, CAT_COL_NAME) + .setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE) + .toIndexCursor()); + } catch(IllegalArgumentException e) { + LOG.info("Could not find expected index on table " + + _systemCatalog.getName()); + // use table scan instead + _tableFinder = new FallbackTableFinder( + _systemCatalog.newCursor() + .setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE) + .toCursor()); + } + + _tableParentId = _tableFinder.findObjectId(DB_PARENT_ID, + SYSTEM_OBJECT_NAME_TABLES); + + if(_tableParentId == null) { + throw new IOException("Did not find required parent table id"); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Finished reading system catalog. Tables: " + + getTableNames()); + } + } + + public Set getTableNames() throws IOException { + if(_tableNames == null) { + Set tableNames = + new TreeSet(String.CASE_INSENSITIVE_ORDER); + _tableFinder.getTableNames(tableNames, false); + _tableNames = tableNames; + } + return _tableNames; + } + + public Set getSystemTableNames() throws IOException { + Set sysTableNames = + new TreeSet(String.CASE_INSENSITIVE_ORDER); + _tableFinder.getTableNames(sysTableNames, true); + return sysTableNames; + } + + public Iterator

iterator() { + return new TableIterator(); + } + + public TableImpl getTable(String name) throws IOException { + return getTable(name, false); + } + + /** + * @param tableDefPageNumber the page number of a table definition + * @return The table, or null if it doesn't exist + * @usage _advanced_method_ + */ + public TableImpl getTable(int tableDefPageNumber) throws IOException { + + // first, check for existing table + TableImpl table = _tableCache.get(tableDefPageNumber); + if(table != null) { + return table; + } + + // lookup table info from system catalog + Row objectRow = _tableFinder.getObjectRow( + tableDefPageNumber, SYSTEM_CATALOG_COLUMNS); + if(objectRow == null) { + return null; + } + + String name = (String)objectRow.get(CAT_COL_NAME); + int flags = (Integer)objectRow.get(CAT_COL_FLAGS); + + return readTable(name, tableDefPageNumber, flags); + } + + /** + * @param name Table name + * @param includeSystemTables whether to consider returning a system table + * @return The table, or null if it doesn't exist + */ + private TableImpl getTable(String name, boolean includeSystemTables) + throws IOException + { + TableInfo tableInfo = lookupTable(name); + + if ((tableInfo == null) || (tableInfo.pageNumber == null)) { + return null; + } + if(!includeSystemTables && isSystemObject(tableInfo.flags)) { + return null; + } + + if(tableInfo.isLinked()) { + + if(_linkedDbs == null) { + _linkedDbs = new HashMap(); + } + + String linkedDbName = ((LinkedTableInfo)tableInfo).linkedDbName; + String linkedTableName = ((LinkedTableInfo)tableInfo).linkedTableName; + Database linkedDb = _linkedDbs.get(linkedDbName); + if(linkedDb == null) { + linkedDb = getLinkResolver().resolveLinkedDatabase(this, linkedDbName); + _linkedDbs.put(linkedDbName, linkedDb); + } + + return ((DatabaseImpl)linkedDb).getTable(linkedTableName, + includeSystemTables); + } + + return readTable(tableInfo.tableName, tableInfo.pageNumber, + tableInfo.flags); + } + + /** + * Create a new table in this database + * @param name Name of the table to create + * @param columns List of Columns in the table + * @usage _general_method_ + */ + public void createTable(String name, List columns) + throws IOException + { + createTable(name, columns, null); + } + + /** + * Create a new table in this database + * @param name Name of the table to create + * @param columns List of Columns in the table + * @param indexes List of IndexBuilders describing indexes for the table + * @usage _general_method_ + */ + public void createTable(String name, List columns, + List indexes) + throws IOException + { + if(lookupTable(name) != null) { + throw new IllegalArgumentException( + "Cannot create table with name of existing table"); + } + + new TableCreator(this, name, columns, indexes).createTable(); + } + + public void createLinkedTable(String name, String linkedDbName, + String linkedTableName) + throws IOException + { + if(lookupTable(name) != null) { + throw new IllegalArgumentException( + "Cannot create linked table with name of existing table"); + } + + validateIdentifierName(name, getFormat().MAX_TABLE_NAME_LENGTH, "table"); + validateIdentifierName(linkedDbName, DataType.MEMO.getMaxSize(), + "linked database"); + validateIdentifierName(linkedTableName, getFormat().MAX_TABLE_NAME_LENGTH, + "linked table"); + + getPageChannel().startWrite(); + try { + + int linkedTableId = _tableFinder.getNextFreeSyntheticId(); + + addNewTable(name, linkedTableId, TYPE_LINKED_TABLE, linkedDbName, + linkedTableName); + + } finally { + getPageChannel().finishWrite(); + } + } + + /** + * Adds a newly created table to the relevant internal database structures. + */ + void addNewTable(String name, int tdefPageNumber, Short type, + String linkedDbName, String linkedTableName) + throws IOException + { + //Add this table to our internal list. + addTable(name, Integer.valueOf(tdefPageNumber), type, linkedDbName, + linkedTableName); + + //Add this table to system tables + addToSystemCatalog(name, tdefPageNumber, type, linkedDbName, + linkedTableName); + addToAccessControlEntries(tdefPageNumber); + } + + public List getRelationships(Table table1, Table table2) + throws IOException + { + return getRelationships((TableImpl)table1, (TableImpl)table2); + } + + public List getRelationships( + TableImpl table1, TableImpl table2) + throws IOException + { + int nameCmp = table1.getName().compareTo(table2.getName()); + if(nameCmp == 0) { + throw new IllegalArgumentException("Must provide two different tables"); + } + if(nameCmp > 0) { + // we "order" the two tables given so that we will return a collection + // of relationships in the same order regardless of whether we are given + // (TableFoo, TableBar) or (TableBar, TableFoo). + TableImpl tmp = table1; + table1 = table2; + table2 = tmp; + } + + return getRelationshipsImpl(table1, table2, true); + } + + public List getRelationships(Table table) + throws IOException + { + if(table == null) { + throw new IllegalArgumentException("Must provide a table"); + } + // since we are getting relationships specific to certain table include + // all tables + return getRelationshipsImpl((TableImpl)table, null, true); + } + + public List getRelationships() + throws IOException + { + return getRelationshipsImpl(null, null, false); + } + + public List getSystemRelationships() + throws IOException + { + return getRelationshipsImpl(null, null, true); + } + + private List getRelationshipsImpl( + TableImpl table1, TableImpl table2, boolean includeSystemTables) + throws IOException + { + // the relationships table does not get loaded until first accessed + if(_relationships == null) { + _relationships = getSystemTable(TABLE_SYSTEM_RELATIONSHIPS); + if(_relationships == null) { + throw new IOException("Could not find system relationships table"); + } + } + + List relationships = new ArrayList(); + + if(table1 != null) { + Cursor cursor = createCursorWithOptionalIndex( + _relationships, REL_COL_FROM_TABLE, table1.getName()); + collectRelationships(cursor, table1, table2, relationships, + includeSystemTables); + cursor = createCursorWithOptionalIndex( + _relationships, REL_COL_TO_TABLE, table1.getName()); + collectRelationships(cursor, table2, table1, relationships, + includeSystemTables); + } else { + collectRelationships(new CursorBuilder(_relationships).toCursor(), + null, null, relationships, includeSystemTables); + } + + return relationships; + } + + public List getQueries() throws IOException + { + // the queries table does not get loaded until first accessed + if(_queries == null) { + _queries = getSystemTable(TABLE_SYSTEM_QUERIES); + if(_queries == null) { + throw new IOException("Could not find system queries table"); + } + } + + // find all the queries from the system catalog + List queryInfo = new ArrayList(); + Map> queryRowMap = + new HashMap>(); + for(Row row : + CursorImpl.createCursor(_systemCatalog).newIterable().setColumnNames( + SYSTEM_CATALOG_COLUMNS)) + { + String name = (String) row.get(CAT_COL_NAME); + if (name != null && TYPE_QUERY.equals(row.get(CAT_COL_TYPE))) { + queryInfo.add(row); + Integer id = (Integer)row.get(CAT_COL_ID); + queryRowMap.put(id, new ArrayList()); + } + } + + // find all the query rows + for(Row row : CursorImpl.createCursor(_queries)) { + QueryImpl.Row queryRow = new QueryImpl.Row(row); + List queryRows = queryRowMap.get(queryRow.objectId); + if(queryRows == null) { + LOG.warn("Found rows for query with id " + queryRow.objectId + + " missing from system catalog"); + continue; + } + queryRows.add(queryRow); + } + + // lastly, generate all the queries + List queries = new ArrayList(); + for(Row row : queryInfo) { + String name = (String) row.get(CAT_COL_NAME); + Integer id = (Integer)row.get(CAT_COL_ID); + int flags = (Integer)row.get(CAT_COL_FLAGS); + List queryRows = queryRowMap.get(id); + queries.add(QueryImpl.create(flags, name, queryRows, id)); + } + + return queries; + } + + public TableImpl getSystemTable(String tableName) throws IOException + { + return getTable(tableName, true); + } + + public PropertyMap getDatabaseProperties() throws IOException { + if(_dbPropMaps == null) { + _dbPropMaps = getPropertiesForDbObject(OBJECT_NAME_DB_PROPS); + } + return _dbPropMaps.getDefault(); + } + + public PropertyMap getSummaryProperties() throws IOException { + if(_summaryPropMaps == null) { + _summaryPropMaps = getPropertiesForDbObject(OBJECT_NAME_SUMMARY_PROPS); + } + return _summaryPropMaps.getDefault(); + } + + public PropertyMap getUserDefinedProperties() throws IOException { + if(_userDefPropMaps == null) { + _userDefPropMaps = getPropertiesForDbObject(OBJECT_NAME_USERDEF_PROPS); + } + return _userDefPropMaps.getDefault(); + } + + /** + * @return the PropertyMaps for the object with the given id + * @usage _advanced_method_ + */ + public PropertyMaps getPropertiesForObject(int objectId) + throws IOException + { + Row objectRow = _tableFinder.getObjectRow( + objectId, SYSTEM_CATALOG_PROPS_COLUMNS); + byte[] propsBytes = null; + if(objectRow != null) { + propsBytes = (byte[])objectRow.get(CAT_COL_PROPS); + } + return readProperties(propsBytes, objectId); + } + + /** + * @return property group for the given "database" object + */ + private PropertyMaps getPropertiesForDbObject(String dbName) + throws IOException + { + if(_dbParentId == null) { + // need the parent if of the databases objects + _dbParentId = _tableFinder.findObjectId(DB_PARENT_ID, + SYSTEM_OBJECT_NAME_DATABASES); + if(_dbParentId == null) { + throw new IOException("Did not find required parent db id"); + } + } + + Row objectRow = _tableFinder.getObjectRow( + _dbParentId, dbName, SYSTEM_CATALOG_PROPS_COLUMNS); + byte[] propsBytes = null; + int objectId = -1; + if(objectRow != null) { + propsBytes = (byte[])objectRow.get(CAT_COL_PROPS); + objectId = (Integer)objectRow.get(CAT_COL_ID); + } + return readProperties(propsBytes, objectId); + } + + public String getDatabasePassword() throws IOException + { + ByteBuffer buffer = takeSharedBuffer(); + try { + _pageChannel.readPage(buffer, 0); + + byte[] pwdBytes = new byte[_format.SIZE_PASSWORD]; + buffer.position(_format.OFFSET_PASSWORD); + buffer.get(pwdBytes); + + // de-mask password using extra password mask if necessary (the extra + // password mask is generated from the database creation date stored in + // the header) + byte[] pwdMask = getPasswordMask(buffer, _format); + if(pwdMask != null) { + for(int i = 0; i < pwdBytes.length; ++i) { + pwdBytes[i] ^= pwdMask[i % pwdMask.length]; + } + } + + boolean hasPassword = false; + for(int i = 0; i < pwdBytes.length; ++i) { + if(pwdBytes[i] != 0) { + hasPassword = true; + break; + } + } + + if(!hasPassword) { + return null; + } + + String pwd = ColumnImpl.decodeUncompressedText(pwdBytes, getCharset()); + + // remove any trailing null chars + int idx = pwd.indexOf('\0'); + if(idx >= 0) { + pwd = pwd.substring(0, idx); + } + + return pwd; + } finally { + releaseSharedBuffer(buffer); + } + } + + /** + * Finds the relationships matching the given from and to tables from the + * given cursor and adds them to the given list. + */ + private void collectRelationships( + Cursor cursor, TableImpl fromTable, TableImpl toTable, + List relationships, boolean includeSystemTables) + throws IOException + { + String fromTableName = ((fromTable != null) ? fromTable.getName() : null); + String toTableName = ((toTable != null) ? toTable.getName() : null); + + for(Row row : cursor) { + String fromName = (String)row.get(REL_COL_FROM_TABLE); + String toName = (String)row.get(REL_COL_TO_TABLE); + + if(((fromTableName == null) || + fromTableName.equalsIgnoreCase(fromName)) && + ((toTableName == null) || + toTableName.equalsIgnoreCase(toName))) { + + String relName = (String)row.get(REL_COL_NAME); + + // found more info for a relationship. see if we already have some + // info for this relationship + Relationship rel = null; + for(Relationship tmp : relationships) { + if(tmp.getName().equalsIgnoreCase(relName)) { + rel = tmp; + break; + } + } + + TableImpl relFromTable = fromTable; + if(relFromTable == null) { + relFromTable = getTable(fromName, includeSystemTables); + if(relFromTable == null) { + // invalid table or ignoring system tables, just ignore + continue; + } + } + TableImpl relToTable = toTable; + if(relToTable == null) { + relToTable = getTable(toName, includeSystemTables); + if(relToTable == null) { + // invalid table or ignoring system tables, just ignore + continue; + } + } + + if(rel == null) { + // new relationship + int numCols = (Integer)row.get(REL_COL_COLUMN_COUNT); + int flags = (Integer)row.get(REL_COL_FLAGS); + rel = new RelationshipImpl(relName, relFromTable, relToTable, + flags, numCols); + relationships.add(rel); + } + + // add column info + int colIdx = (Integer)row.get(REL_COL_COLUMN_INDEX); + ColumnImpl fromCol = relFromTable.getColumn( + (String)row.get(REL_COL_FROM_COLUMN)); + ColumnImpl toCol = relToTable.getColumn( + (String)row.get(REL_COL_TO_COLUMN)); + + rel.getFromColumns().set(colIdx, fromCol); + rel.getToColumns().set(colIdx, toCol); + } + } + } + + /** + * Add a new table to the system catalog + * @param name Table name + * @param pageNumber Page number that contains the table definition + */ + private void addToSystemCatalog(String name, int pageNumber, Short type, + String linkedDbName, String linkedTableName) + throws IOException + { + Object[] catalogRow = new Object[_systemCatalog.getColumnCount()]; + int idx = 0; + Date creationTime = new Date(); + for (Iterator iter = _systemCatalog.getColumns().iterator(); + iter.hasNext(); idx++) + { + ColumnImpl col = iter.next(); + if (CAT_COL_ID.equals(col.getName())) { + catalogRow[idx] = Integer.valueOf(pageNumber); + } else if (CAT_COL_NAME.equals(col.getName())) { + catalogRow[idx] = name; + } else if (CAT_COL_TYPE.equals(col.getName())) { + catalogRow[idx] = type; + } else if (CAT_COL_DATE_CREATE.equals(col.getName()) || + CAT_COL_DATE_UPDATE.equals(col.getName())) { + catalogRow[idx] = creationTime; + } else if (CAT_COL_PARENT_ID.equals(col.getName())) { + catalogRow[idx] = _tableParentId; + } else if (CAT_COL_FLAGS.equals(col.getName())) { + catalogRow[idx] = Integer.valueOf(0); + } else if (CAT_COL_OWNER.equals(col.getName())) { + byte[] owner = new byte[2]; + catalogRow[idx] = owner; + owner[0] = (byte) 0xcf; + owner[1] = (byte) 0x5f; + } else if (CAT_COL_DATABASE.equals(col.getName())) { + catalogRow[idx] = linkedDbName; + } else if (CAT_COL_FOREIGN_NAME.equals(col.getName())) { + catalogRow[idx] = linkedTableName; + } + } + _systemCatalog.addRow(catalogRow); + } + + /** + * Add a new table to the system's access control entries + * @param pageNumber Page number that contains the table definition + */ + private void addToAccessControlEntries(int pageNumber) throws IOException { + + if(_newTableSIDs.isEmpty()) { + initNewTableSIDs(); + } + + TableImpl acEntries = getAccessControlEntries(); + ColumnImpl acmCol = acEntries.getColumn(ACE_COL_ACM); + ColumnImpl inheritCol = acEntries.getColumn(ACE_COL_F_INHERITABLE); + ColumnImpl objIdCol = acEntries.getColumn(ACE_COL_OBJECT_ID); + ColumnImpl sidCol = acEntries.getColumn(ACE_COL_SID); + + // construct a collection of ACE entries mimicing those of our parent, the + // "Tables" system object + List aceRows = new ArrayList(_newTableSIDs.size()); + for(byte[] sid : _newTableSIDs) { + Object[] aceRow = new Object[acEntries.getColumnCount()]; + acmCol.setRowValue(aceRow, SYS_FULL_ACCESS_ACM); + inheritCol.setRowValue(aceRow, Boolean.FALSE); + objIdCol.setRowValue(aceRow, Integer.valueOf(pageNumber)); + sidCol.setRowValue(aceRow, sid); + aceRows.add(aceRow); + } + acEntries.addRows(aceRows); + } + + /** + * Determines the collection of SIDs which need to be added to new tables. + */ + private void initNewTableSIDs() throws IOException + { + // search for ACEs matching the tableParentId. use the index on the + // objectId column if found (should be there) + Cursor cursor = createCursorWithOptionalIndex( + getAccessControlEntries(), ACE_COL_OBJECT_ID, _tableParentId); + + for(Row row : cursor) { + Integer objId = (Integer)row.get(ACE_COL_OBJECT_ID); + if(_tableParentId.equals(objId)) { + _newTableSIDs.add((byte[])row.get(ACE_COL_SID)); + } + } + + if(_newTableSIDs.isEmpty()) { + // if all else fails, use the hard-coded default + _newTableSIDs.add(SYS_DEFAULT_SID); + } + } + + /** + * Reads a table with the given name from the given pageNumber. + */ + private TableImpl readTable(String name, int pageNumber, int flags) + throws IOException + { + // first, check for existing table + TableImpl table = _tableCache.get(pageNumber); + if(table != null) { + return table; + } + + ByteBuffer buffer = takeSharedBuffer(); + try { + // need to load table from db + _pageChannel.readPage(buffer, pageNumber); + byte pageType = buffer.get(0); + if (pageType != PageTypes.TABLE_DEF) { + throw new IOException( + "Looking for " + name + " at page " + pageNumber + + ", but page type is " + pageType); + } + return _tableCache.put( + new TableImpl(this, buffer, pageNumber, name, flags)); + } finally { + releaseSharedBuffer(buffer); + } + } + + /** + * Creates a Cursor restricted to the given column value if possible (using + * an existing index), otherwise a simple table cursor. + */ + private static Cursor createCursorWithOptionalIndex( + TableImpl table, String colName, Object colValue) + throws IOException + { + try { + return table.newCursor() + .setIndexByColumns(table.getColumn(colName)) + .setSpecificEntry(colValue) + .toCursor(); + } catch(IllegalArgumentException e) { + LOG.info("Could not find expected index on table " + table.getName()); + } + // use table scan instead + return CursorImpl.createCursor(table); + } + + public void flush() throws IOException { + if(_linkedDbs != null) { + for(Database linkedDb : _linkedDbs.values()) { + linkedDb.flush(); + } + } + _pageChannel.flush(); + } + + public void close() throws IOException { + if(_linkedDbs != null) { + for(Database linkedDb : _linkedDbs.values()) { + linkedDb.close(); + } + } + _pageChannel.close(); + } + + /** + * Validates an identifier name. + * @usage _advanced_method_ + */ + public static void validateIdentifierName(String name, + int maxLength, + String identifierType) + { + if((name == null) || (name.trim().length() == 0)) { + throw new IllegalArgumentException( + identifierType + " must have non-empty name"); + } + if(name.length() > maxLength) { + throw new IllegalArgumentException( + identifierType + " name is longer than max length of " + maxLength + + ": " + name); + } + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } + + /** + * Adds a table to the _tableLookup and resets the _tableNames set + */ + private void addTable(String tableName, Integer pageNumber, Short type, + String linkedDbName, String linkedTableName) + { + _tableLookup.put(toLookupName(tableName), + createTableInfo(tableName, pageNumber, 0, type, + linkedDbName, linkedTableName)); + // clear this, will be created next time needed + _tableNames = null; + } + + /** + * Creates a TableInfo instance appropriate for the given table data. + */ + private static TableInfo createTableInfo( + String tableName, Integer pageNumber, int flags, Short type, + String linkedDbName, String linkedTableName) + { + if(TYPE_LINKED_TABLE.equals(type)) { + return new LinkedTableInfo(pageNumber, tableName, flags, linkedDbName, + linkedTableName); + } + return new TableInfo(pageNumber, tableName, flags); + } + + /** + * @return the tableInfo of the given table, if any + */ + private TableInfo lookupTable(String tableName) throws IOException { + + String lookupTableName = toLookupName(tableName); + TableInfo tableInfo = _tableLookup.get(lookupTableName); + if(tableInfo != null) { + return tableInfo; + } + + tableInfo = _tableFinder.lookupTable(tableName); + + if(tableInfo != null) { + // cache for later + _tableLookup.put(lookupTableName, tableInfo); + } + + return tableInfo; + } + + /** + * @return a string usable in the _tableLookup map. + */ + public static String toLookupName(String name) { + return ((name != null) ? name.toUpperCase() : null); + } + + /** + * @return {@code true} if the given flags indicate that an object is some + * sort of system object, {@code false} otherwise. + */ + private static boolean isSystemObject(int flags) { + return ((flags & SYSTEM_OBJECT_FLAGS) != 0); + } + + /** + * Returns the default TimeZone. This is normally the platform default + * TimeZone as returned by {@link TimeZone#getDefault}, but can be + * overridden using the system property + * {@value com.healthmarketscience.jackcess.Database#TIMEZONE_PROPERTY}. + * @usage _advanced_method_ + */ + public static TimeZone getDefaultTimeZone() + { + String tzProp = System.getProperty(TIMEZONE_PROPERTY); + if(tzProp != null) { + tzProp = tzProp.trim(); + if(tzProp.length() > 0) { + return TimeZone.getTimeZone(tzProp); + } + } + + // use system default + return TimeZone.getDefault(); + } + + /** + * Returns the default Charset for the given JetFormat. This may or may not + * be platform specific, depending on the format, but can be overridden + * using a system property composed of the prefix + * {@value com.healthmarketscience.jackcess.Database#CHARSET_PROPERTY_PREFIX} + * followed by the JetFormat version to which the charset should apply, + * e.g. {@code "com.healthmarketscience.jackcess.charset.VERSION_3"}. + * @usage _advanced_method_ + */ + public static Charset getDefaultCharset(JetFormat format) + { + String csProp = System.getProperty(CHARSET_PROPERTY_PREFIX + format); + if(csProp != null) { + csProp = csProp.trim(); + if(csProp.length() > 0) { + return Charset.forName(csProp); + } + } + + // use format default + return format.CHARSET; + } + + /** + * Returns the default Table.ColumnOrder. This defaults to + * {@link Database#DEFAULT_COLUMN_ORDER}, but can be overridden using the system + * property {@value com.healthmarketscience.jackcess.Database#COLUMN_ORDER_PROPERTY}. + * @usage _advanced_method_ + */ + public static Table.ColumnOrder getDefaultColumnOrder() + { + String coProp = System.getProperty(COLUMN_ORDER_PROPERTY); + if(coProp != null) { + coProp = coProp.trim(); + if(coProp.length() > 0) { + return Table.ColumnOrder.valueOf(coProp); + } + } + + // use default order + return DEFAULT_COLUMN_ORDER; + } + + /** + * Returns the default enforce foreign-keys policy. This defaults to + * {@code true}, but can be overridden using the system + * property {@value com.healthmarketscience.jackcess.Database#FK_ENFORCE_PROPERTY}. + * @usage _advanced_method_ + */ + public static boolean getDefaultEnforceForeignKeys() + { + String prop = System.getProperty(FK_ENFORCE_PROPERTY); + if(prop != null) { + return Boolean.TRUE.toString().equalsIgnoreCase(prop); + } + return true; + } + + /** + * Copies the given InputStream to the given channel using the most + * efficient means possible. + */ + private static void transferFrom(FileChannel channel, InputStream in) + throws IOException + { + ReadableByteChannel readChannel = Channels.newChannel(in); + if(!BROKEN_NIO) { + // sane implementation + channel.transferFrom(readChannel, 0, MAX_EMPTYDB_SIZE); + } else { + // do things the hard way for broken vms + ByteBuffer bb = ByteBuffer.allocate(8096); + while(readChannel.read(bb) >= 0) { + bb.flip(); + channel.write(bb); + bb.clear(); + } + } + } + + /** + * Returns the password mask retrieved from the given header page and + * format, or {@code null} if this format does not use a password mask. + */ + static byte[] getPasswordMask(ByteBuffer buffer, JetFormat format) + { + // get extra password mask if necessary (the extra password mask is + // generated from the database creation date stored in the header) + int pwdMaskPos = format.OFFSET_HEADER_DATE; + if(pwdMaskPos < 0) { + return null; + } + + buffer.position(pwdMaskPos); + double dateVal = Double.longBitsToDouble(buffer.getLong()); + + byte[] pwdMask = new byte[4]; + PageChannel.wrap(pwdMask).putInt((int)dateVal); + + return pwdMask; + } + + static InputStream getResourceAsStream(String resourceName) + throws IOException + { + InputStream stream = DatabaseImpl.class.getClassLoader() + .getResourceAsStream(resourceName); + + if(stream == null) { + + stream = Thread.currentThread().getContextClassLoader() + .getResourceAsStream(resourceName); + + if(stream == null) { + throw new IOException("Could not load jackcess resource " + + resourceName); + } + } + + return stream; + } + + private static boolean isTableType(Short objType) { + return(TYPE_TABLE.equals(objType) || TYPE_LINKED_TABLE.equals(objType)); + } + + public static FileFormatDetails getFileFormatDetails(FileFormat fileFormat) { + return FILE_FORMAT_DETAILS.get(fileFormat); + } + + private static void addFileFormatDetails( + FileFormat fileFormat, String emptyFileName, JetFormat format) + { + String emptyFile = + ((emptyFileName != null) ? + RESOURCE_PATH + emptyFileName + fileFormat.getFileExtension() : null); + FILE_FORMAT_DETAILS.put(fileFormat, new FileFormatDetails(emptyFile, format)); + } + + /** + * Utility class for storing table page number and actual name. + */ + private static class TableInfo + { + public final Integer pageNumber; + public final String tableName; + public final int flags; + + private TableInfo(Integer newPageNumber, String newTableName, int newFlags) { + pageNumber = newPageNumber; + tableName = newTableName; + flags = newFlags; + } + + public boolean isLinked() { + return false; + } + } + + /** + * Utility class for storing linked table info + */ + private static class LinkedTableInfo extends TableInfo + { + private final String linkedDbName; + private final String linkedTableName; + + private LinkedTableInfo(Integer newPageNumber, String newTableName, + int newFlags, String newLinkedDbName, + String newLinkedTableName) { + super(newPageNumber, newTableName, newFlags); + linkedDbName = newLinkedDbName; + linkedTableName = newLinkedTableName; + } + + @Override + public boolean isLinked() { + return true; + } + } + + /** + * Table iterator for this database, unmodifiable. + */ + private class TableIterator implements Iterator
+ { + private Iterator _tableNameIter; + + private TableIterator() { + try { + _tableNameIter = getTableNames().iterator(); + } catch(IOException e) { + throw new RuntimeIOException(e); + } + } + + public boolean hasNext() { + return _tableNameIter.hasNext(); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + public Table next() { + if(!hasNext()) { + throw new NoSuchElementException(); + } + try { + return getTable(_tableNameIter.next()); + } catch(IOException e) { + throw new RuntimeIOException(e); + } + } + } + + /** + * Utility class for handling table lookups. + */ + private abstract class TableFinder + { + public Integer findObjectId(Integer parentId, String name) + throws IOException + { + Cursor cur = findRow(parentId, name); + if(cur == null) { + return null; + } + ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID); + return (Integer)cur.getCurrentRowValue(idCol); + } + + public Row getObjectRow(Integer parentId, String name, + Collection columns) + throws IOException + { + Cursor cur = findRow(parentId, name); + return ((cur != null) ? cur.getCurrentRow(columns) : null); + } + + public Row getObjectRow( + Integer objectId, Collection columns) + throws IOException + { + Cursor cur = findRow(objectId); + return ((cur != null) ? cur.getCurrentRow(columns) : null); + } + + public void getTableNames(Set tableNames, + boolean systemTables) + throws IOException + { + for(Row row : getTableNamesCursor().newIterable().setColumnNames( + SYSTEM_CATALOG_TABLE_NAME_COLUMNS)) { + + String tableName = (String)row.get(CAT_COL_NAME); + int flags = (Integer)row.get(CAT_COL_FLAGS); + Short type = (Short)row.get(CAT_COL_TYPE); + int parentId = (Integer)row.get(CAT_COL_PARENT_ID); + + if((parentId == _tableParentId) && isTableType(type) && + (isSystemObject(flags) == systemTables)) { + tableNames.add(tableName); + } + } + } + + protected abstract Cursor findRow(Integer parentId, String name) + throws IOException; + + protected abstract Cursor findRow(Integer objectId) + throws IOException; + + protected abstract Cursor getTableNamesCursor() throws IOException; + + public abstract TableInfo lookupTable(String tableName) + throws IOException; + + protected abstract int findMaxSyntheticId() throws IOException; + + public int getNextFreeSyntheticId() throws IOException + { + int maxSynthId = findMaxSyntheticId(); + if(maxSynthId >= -1) { + // bummer, no more ids available + throw new IllegalStateException("Too many database objects!"); + } + return maxSynthId + 1; + } + } + + /** + * Normal table lookup handler, using catalog table index. + */ + private final class DefaultTableFinder extends TableFinder + { + private final IndexCursor _systemCatalogCursor; + private IndexCursor _systemCatalogIdCursor; + + private DefaultTableFinder(IndexCursor systemCatalogCursor) { + _systemCatalogCursor = systemCatalogCursor; + } + + private void initIdCursor() throws IOException { + if(_systemCatalogIdCursor == null) { + _systemCatalogIdCursor = _systemCatalog.newCursor() + .setIndexByColumnNames(CAT_COL_ID) + .toIndexCursor(); + } + } + + @Override + protected Cursor findRow(Integer parentId, String name) + throws IOException + { + return (_systemCatalogCursor.findFirstRowByEntry(parentId, name) ? + _systemCatalogCursor : null); + } + + @Override + protected Cursor findRow(Integer objectId) throws IOException + { + initIdCursor(); + return (_systemCatalogIdCursor.findFirstRowByEntry(objectId) ? + _systemCatalogIdCursor : null); + } + + @Override + public TableInfo lookupTable(String tableName) throws IOException { + + if(findRow(_tableParentId, tableName) == null) { + return null; + } + + Row row = _systemCatalogCursor.getCurrentRow( + SYSTEM_CATALOG_COLUMNS); + Integer pageNumber = (Integer)row.get(CAT_COL_ID); + String realName = (String)row.get(CAT_COL_NAME); + int flags = (Integer)row.get(CAT_COL_FLAGS); + Short type = (Short)row.get(CAT_COL_TYPE); + + if(!isTableType(type)) { + return null; + } + + String linkedDbName = (String)row.get(CAT_COL_DATABASE); + String linkedTableName = (String)row.get(CAT_COL_FOREIGN_NAME); + + return createTableInfo(realName, pageNumber, flags, type, linkedDbName, + linkedTableName); + } + + @Override + protected Cursor getTableNamesCursor() throws IOException { + return _systemCatalog.newCursor() + .setIndex(_systemCatalogCursor.getIndex()) + .setStartEntry(_tableParentId, IndexData.MIN_VALUE) + .setEndEntry(_tableParentId, IndexData.MAX_VALUE) + .toIndexCursor(); + } + + @Override + protected int findMaxSyntheticId() throws IOException { + initIdCursor(); + _systemCatalogIdCursor.reset(); + + // synthetic ids count up from min integer. so the current, highest, + // in-use synthetic id is the max id < 0. + _systemCatalogIdCursor.findClosestRowByEntry(0); + if(!_systemCatalogIdCursor.moveToPreviousRow()) { + return Integer.MIN_VALUE; + } + ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID); + return (Integer)_systemCatalogIdCursor.getCurrentRowValue(idCol); + } + } + + /** + * Fallback table lookup handler, using catalog table scans. + */ + private final class FallbackTableFinder extends TableFinder + { + private final Cursor _systemCatalogCursor; + + private FallbackTableFinder(Cursor systemCatalogCursor) { + _systemCatalogCursor = systemCatalogCursor; + } + + @Override + protected Cursor findRow(Integer parentId, String name) + throws IOException + { + Map rowPat = new HashMap(); + rowPat.put(CAT_COL_PARENT_ID, parentId); + rowPat.put(CAT_COL_NAME, name); + return (_systemCatalogCursor.findFirstRow(rowPat) ? + _systemCatalogCursor : null); + } + + @Override + protected Cursor findRow(Integer objectId) throws IOException + { + ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID); + return (_systemCatalogCursor.findFirstRow(idCol, objectId) ? + _systemCatalogCursor : null); + } + + @Override + public TableInfo lookupTable(String tableName) throws IOException { + + for(Row row : _systemCatalogCursor.newIterable().setColumnNames( + SYSTEM_CATALOG_TABLE_NAME_COLUMNS)) { + + Short type = (Short)row.get(CAT_COL_TYPE); + if(!isTableType(type)) { + continue; + } + + int parentId = (Integer)row.get(CAT_COL_PARENT_ID); + if(parentId != _tableParentId) { + continue; + } + + String realName = (String)row.get(CAT_COL_NAME); + if(!tableName.equalsIgnoreCase(realName)) { + continue; + } + + Integer pageNumber = (Integer)row.get(CAT_COL_ID); + int flags = (Integer)row.get(CAT_COL_FLAGS); + String linkedDbName = (String)row.get(CAT_COL_DATABASE); + String linkedTableName = (String)row.get(CAT_COL_FOREIGN_NAME); + + return createTableInfo(realName, pageNumber, flags, type, linkedDbName, + linkedTableName); + } + + return null; + } + + @Override + protected Cursor getTableNamesCursor() throws IOException { + return _systemCatalogCursor; + } + + @Override + protected int findMaxSyntheticId() throws IOException { + // find max id < 0 + ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID); + _systemCatalogCursor.reset(); + int curMaxSynthId = Integer.MIN_VALUE; + while(_systemCatalogCursor.moveToNextRow()) { + int id = (Integer)_systemCatalogCursor.getCurrentRowValue(idCol); + if((id > curMaxSynthId) && (id < 0)) { + curMaxSynthId = id; + } + } + return curMaxSynthId; + } + } + + /** + * WeakReference for a Table which holds the table pageNumber (for later + * cache purging). + */ + private static final class WeakTableReference extends WeakReference + { + private final Integer _pageNumber; + + private WeakTableReference(Integer pageNumber, TableImpl table, + ReferenceQueue queue) { + super(table, queue); + _pageNumber = pageNumber; + } + + public Integer getPageNumber() { + return _pageNumber; + } + } + + /** + * Cache of currently in-use tables, allows re-use of existing tables. + */ + private static final class TableCache + { + private final Map _tables = + new HashMap(); + private final ReferenceQueue _queue = + new ReferenceQueue(); + + public TableImpl get(Integer pageNumber) { + WeakTableReference ref = _tables.get(pageNumber); + return ((ref != null) ? ref.get() : null); + } + + public TableImpl put(TableImpl table) { + purgeOldRefs(); + + Integer pageNumber = table.getTableDefPageNumber(); + WeakTableReference ref = new WeakTableReference( + pageNumber, table, _queue); + _tables.put(pageNumber, ref); + + return table; + } + + private void purgeOldRefs() { + WeakTableReference oldRef = null; + while((oldRef = (WeakTableReference)_queue.poll()) != null) { + _tables.remove(oldRef.getPageNumber()); + } + } + } + + /** + * Internal details for each FileForrmat + * @usage _advanced_class_ + */ + public static final class FileFormatDetails + { + private final String _emptyFile; + private final JetFormat _format; + + private FileFormatDetails(String emptyFile, JetFormat format) { + _emptyFile = emptyFile; + _format = format; + } + + public String getEmptyFilePath() { + return _emptyFile; + } + + public JetFormat getFormat() { + return _format; + } + } +} diff --git a/src/java/com/healthmarketscience/jackcess/DefaultCodecProvider.java b/src/java/com/healthmarketscience/jackcess/impl/DefaultCodecProvider.java similarity index 88% rename from src/java/com/healthmarketscience/jackcess/DefaultCodecProvider.java rename to src/java/com/healthmarketscience/jackcess/impl/DefaultCodecProvider.java index 7694617..0e1de8f 100644 --- a/src/java/com/healthmarketscience/jackcess/DefaultCodecProvider.java +++ b/src/java/com/healthmarketscience/jackcess/impl/DefaultCodecProvider.java @@ -17,7 +17,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.io.IOException; import java.nio.ByteBuffer; @@ -87,7 +87,14 @@ public class DefaultCodecProvider implements CodecProvider return true; } - public void decodePage(ByteBuffer page, int pageNumber) throws IOException { + public boolean canDecodeInline() { + return true; + } + + public void decodePage(ByteBuffer inPage, ByteBuffer outPage, + int pageNumber) + throws IOException + { // does nothing } @@ -111,7 +118,14 @@ public class DefaultCodecProvider implements CodecProvider return true; } - public void decodePage(ByteBuffer page, int pageNumber) throws IOException { + public boolean canDecodeInline() { + return true; + } + + public void decodePage(ByteBuffer inPage, ByteBuffer outPage, + int pageNumber) + throws IOException + { throw new UnsupportedCodecException("Decoding not supported. Please choose a CodecProvider which supports reading the current database encoding."); } diff --git a/src/java/com/healthmarketscience/jackcess/FKEnforcer.java b/src/java/com/healthmarketscience/jackcess/impl/FKEnforcer.java similarity index 87% rename from src/java/com/healthmarketscience/jackcess/FKEnforcer.java rename to src/java/com/healthmarketscience/jackcess/impl/FKEnforcer.java index b5ce3ec..e0efe15 100644 --- a/src/java/com/healthmarketscience/jackcess/FKEnforcer.java +++ b/src/java/com/healthmarketscience/jackcess/impl/FKEnforcer.java @@ -17,7 +17,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.io.IOException; import java.util.ArrayList; @@ -29,6 +29,14 @@ import java.util.Map; import java.util.Set; import java.util.TreeSet; +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.Index; +import com.healthmarketscience.jackcess.IndexCursor; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.util.CaseInsensitiveColumnMatcher; +import com.healthmarketscience.jackcess.util.ColumnMatcher; +import com.healthmarketscience.jackcess.util.Joiner; /** * Utility class used by Table to enforce foreign-key relationships (if @@ -44,21 +52,21 @@ final class FKEnforcer private static final ColumnMatcher MATCHER = CaseInsensitiveColumnMatcher.INSTANCE; - private final Table _table; - private final List _cols; + private final TableImpl _table; + private final List _cols; private List _primaryJoinersChkUp; private List _primaryJoinersChkDel; private List _primaryJoinersDoUp; private List _primaryJoinersDoDel; private List _secondaryJoiners; - FKEnforcer(Table table) { + FKEnforcer(TableImpl table) { _table = table; // at this point, only init the index columns - Set cols = new TreeSet(); - for(Index idx : _table.getIndexes()) { - Index.ForeignKeyReference ref = idx.getReference(); + Set cols = new TreeSet(); + for(IndexImpl idx : _table.getIndexes()) { + IndexImpl.ForeignKeyReference ref = idx.getReference(); if(ref != null) { // compile an ordered list of all columns in this table which are // involved in foreign key relationships with other tables @@ -68,8 +76,8 @@ final class FKEnforcer } } _cols = !cols.isEmpty() ? - Collections.unmodifiableList(new ArrayList(cols)) : - Collections.emptyList(); + Collections.unmodifiableList(new ArrayList(cols)) : + Collections.emptyList(); } /** @@ -88,8 +96,8 @@ final class FKEnforcer _primaryJoinersDoDel = new ArrayList(1); _secondaryJoiners = new ArrayList(1); - for(Index idx : _table.getIndexes()) { - Index.ForeignKeyReference ref = idx.getReference(); + for(IndexImpl idx : _table.getIndexes()) { + IndexImpl.ForeignKeyReference ref = idx.getReference(); if(ref != null) { Joiner joiner = Joiner.create(idx); @@ -237,11 +245,11 @@ final class FKEnforcer throws IOException { IndexCursor toCursor = joiner.getToCursor(); - List fromCols = joiner.getColumns(); - List toCols = joiner.getToIndex().getColumns(); + List fromCols = joiner.getColumns(); + List toCols = joiner.getToIndex().getColumns(); Object[] toRow = new Object[joiner.getToTable().getColumnCount()]; - for(Iterator> iter = joiner.findRows( + for(Iterator iter = joiner.findRows( oldFromRow, Collections.emptySet()); iter.hasNext(); ) { iter.next(); @@ -257,7 +265,7 @@ final class FKEnforcer } private boolean anyUpdates(Object[] oldRow, Object[] newRow) { - for(Column col : _cols) { + for(ColumnImpl col : _cols) { if(!MATCHER.matches(_table, col.getName(), col.getRowValue(oldRow), col.getRowValue(newRow))) { return true; @@ -270,7 +278,7 @@ final class FKEnforcer Object[] newRow) { Table fromTable = joiner.getFromTable(); - for(IndexData.ColumnDescriptor iCol : joiner.getColumns()) { + for(Index.Column iCol : joiner.getColumns()) { Column col = iCol.getColumn(); if(!MATCHER.matches(fromTable, col.getName(), col.getRowValue(oldRow), col.getRowValue(newRow))) { diff --git a/src/java/com/healthmarketscience/jackcess/GeneralIndexCodes.java b/src/java/com/healthmarketscience/jackcess/impl/GeneralIndexCodes.java similarity index 92% rename from src/java/com/healthmarketscience/jackcess/GeneralIndexCodes.java rename to src/java/com/healthmarketscience/jackcess/impl/GeneralIndexCodes.java index 6e11c60..35fca17 100644 --- a/src/java/com/healthmarketscience/jackcess/GeneralIndexCodes.java +++ b/src/java/com/healthmarketscience/jackcess/impl/GeneralIndexCodes.java @@ -17,7 +17,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; @@ -31,9 +31,9 @@ public class GeneralIndexCodes extends GeneralLegacyIndexCodes { // stash the codes in some resource files private static final String CODES_FILE = - Database.RESOURCE_PATH + "index_codes_gen.txt"; + DatabaseImpl.RESOURCE_PATH + "index_codes_gen.txt"; private static final String EXT_CODES_FILE = - Database.RESOURCE_PATH + "index_codes_ext_gen.txt"; + DatabaseImpl.RESOURCE_PATH + "index_codes_ext_gen.txt"; private static final class Codes { diff --git a/src/java/com/healthmarketscience/jackcess/GeneralLegacyIndexCodes.java b/src/java/com/healthmarketscience/jackcess/impl/GeneralLegacyIndexCodes.java similarity index 98% rename from src/java/com/healthmarketscience/jackcess/GeneralLegacyIndexCodes.java rename to src/java/com/healthmarketscience/jackcess/impl/GeneralLegacyIndexCodes.java index e6d204c..4bdfeeb 100644 --- a/src/java/com/healthmarketscience/jackcess/GeneralLegacyIndexCodes.java +++ b/src/java/com/healthmarketscience/jackcess/impl/GeneralLegacyIndexCodes.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.io.BufferedReader; import java.io.IOException; @@ -34,7 +34,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; -import static com.healthmarketscience.jackcess.ByteUtil.ByteStream; +import static com.healthmarketscience.jackcess.impl.ByteUtil.ByteStream; /** * Various constants used for creating "general legacy" (access 2000-2007) @@ -75,9 +75,9 @@ public class GeneralLegacyIndexCodes { // stash the codes in some resource files private static final String CODES_FILE = - Database.RESOURCE_PATH + "index_codes_genleg.txt"; + DatabaseImpl.RESOURCE_PATH + "index_codes_genleg.txt"; private static final String EXT_CODES_FILE = - Database.RESOURCE_PATH + "index_codes_ext_genleg.txt"; + DatabaseImpl.RESOURCE_PATH + "index_codes_ext_genleg.txt"; /** * Enum which classifies the types of char encoding strategies used when @@ -326,7 +326,7 @@ public class GeneralLegacyIndexCodes { reader = new BufferedReader( new InputStreamReader( - Database.getResourceAsStream(codesFilePath), "US-ASCII")); + DatabaseImpl.getResourceAsStream(codesFilePath), "US-ASCII")); int start = asUnsignedChar(firstChar); int end = asUnsignedChar(lastChar); @@ -490,7 +490,7 @@ public class GeneralLegacyIndexCodes { throws IOException { // first, convert to string - String str = Column.toCharSequence(value).toString(); + String str = ColumnImpl.toCharSequence(value).toString(); // all text columns (including memos) are only indexed up to the max // number of chars in a VARCHAR column diff --git a/src/java/com/healthmarketscience/jackcess/IndexCodes.java b/src/java/com/healthmarketscience/jackcess/impl/IndexCodes.java similarity index 97% rename from src/java/com/healthmarketscience/jackcess/IndexCodes.java rename to src/java/com/healthmarketscience/jackcess/impl/IndexCodes.java index 753c919..a605883 100644 --- a/src/java/com/healthmarketscience/jackcess/IndexCodes.java +++ b/src/java/com/healthmarketscience/jackcess/impl/IndexCodes.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; /** diff --git a/src/java/com/healthmarketscience/jackcess/impl/IndexCursorImpl.java b/src/java/com/healthmarketscience/jackcess/impl/IndexCursorImpl.java new file mode 100644 index 0000000..365cd2e --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/IndexCursorImpl.java @@ -0,0 +1,510 @@ +/* +Copyright (c) 2011 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.impl; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import com.healthmarketscience.jackcess.Index; +import com.healthmarketscience.jackcess.IndexCursor; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.RuntimeIOException; +import com.healthmarketscience.jackcess.impl.TableImpl.RowState; +import com.healthmarketscience.jackcess.util.CaseInsensitiveColumnMatcher; +import com.healthmarketscience.jackcess.util.ColumnMatcher; +import com.healthmarketscience.jackcess.util.EntryIterableBuilder; +import com.healthmarketscience.jackcess.util.SimpleColumnMatcher; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Cursor backed by an index with extended traversal options. + * + * @author James Ahlborn + */ +public class IndexCursorImpl extends CursorImpl implements IndexCursor +{ + private static final Log LOG = LogFactory.getLog(IndexCursorImpl.class); + + /** IndexDirHandler for forward traversal */ + private final IndexDirHandler _forwardDirHandler = + new ForwardIndexDirHandler(); + /** IndexDirHandler for backward traversal */ + private final IndexDirHandler _reverseDirHandler = + new ReverseIndexDirHandler(); + /** logical index which this cursor is using */ + private final IndexImpl _index; + /** Cursor over the entries of the relevant index */ + private final IndexData.EntryCursor _entryCursor; + /** column names for the index entry columns */ + private Set _indexEntryPattern; + + private IndexCursorImpl(TableImpl table, IndexImpl index, + IndexData.EntryCursor entryCursor) + throws IOException + { + super(new IdImpl(table, index), table, + new IndexPosition(entryCursor.getFirstEntry()), + new IndexPosition(entryCursor.getLastEntry())); + _index = index; + _index.initialize(); + _entryCursor = entryCursor; + } + + /** + * Creates an indexed cursor for the given table, narrowed to the given + * range. + *

+ * Note, index based table traversal may not include all rows, as certain + * types of indexes do not include all entries (namely, some indexes ignore + * null entries, see {@link Index#shouldIgnoreNulls}). + * + * @param table the table over which this cursor will traverse + * @param index index for the table which will define traversal order as + * well as enhance certain lookups + * @param startRow the first row of data for the cursor, or {@code null} for + * the first entry + * @param startInclusive whether or not startRow is inclusive or exclusive + * @param endRow the last row of data for the cursor, or {@code null} for + * the last entry + * @param endInclusive whether or not endRow is inclusive or exclusive + */ + public static IndexCursorImpl createCursor(TableImpl table, IndexImpl index, + Object[] startRow, + boolean startInclusive, + Object[] endRow, + boolean endInclusive) + throws IOException + { + if(table != index.getTable()) { + throw new IllegalArgumentException( + "Given index is not for given table: " + index + ", " + table); + } + if(!table.getFormat().INDEXES_SUPPORTED) { + throw new IllegalArgumentException( + "JetFormat " + table.getFormat() + + " does not currently support index lookups"); + } + if(index.getIndexData().getUnsupportedReason() != null) { + throw new IllegalArgumentException( + "Given index " + index + + " is not usable for indexed lookups due to " + + index.getIndexData().getUnsupportedReason()); + } + IndexCursorImpl cursor = new IndexCursorImpl( + table, index, index.cursor(startRow, startInclusive, + endRow, endInclusive)); + // init the column matcher appropriately for the index type + cursor.setColumnMatcher(null); + return cursor; + } + + public IndexImpl getIndex() { + return _index; + } + + public boolean findFirstRowByEntry(Object... entryValues) + throws IOException + { + PositionImpl curPos = _curPos; + PositionImpl prevPos = _prevPos; + boolean found = false; + try { + found = findFirstRowByEntryImpl(toRowValues(entryValues), true, + _columnMatcher); + return found; + } finally { + if(!found) { + try { + restorePosition(curPos, prevPos); + } catch(IOException e) { + LOG.error("Failed restoring position", e); + } + } + } + } + + public void findClosestRowByEntry(Object... entryValues) + throws IOException + { + PositionImpl curPos = _curPos; + PositionImpl prevPos = _prevPos; + boolean found = false; + try { + findFirstRowByEntryImpl(toRowValues(entryValues), false, + _columnMatcher); + found = true; + } finally { + if(!found) { + try { + restorePosition(curPos, prevPos); + } catch(IOException e) { + LOG.error("Failed restoring position", e); + } + } + } + } + + public boolean currentRowMatchesEntry(Object... entryValues) + throws IOException + { + return currentRowMatchesEntryImpl(toRowValues(entryValues), _columnMatcher); + } + + public EntryIterableBuilder newEntryIterable(Object... entryValues) { + return new EntryIterableBuilder(this, entryValues); + } + + public Iterator entryIterator(EntryIterableBuilder iterBuilder) { + return new EntryIterator(iterBuilder.getColumnNames(), + toRowValues(iterBuilder.getEntryValues()), + iterBuilder.getColumnMatcher()); + } + + @Override + protected IndexDirHandler getDirHandler(boolean moveForward) { + return (moveForward ? _forwardDirHandler : _reverseDirHandler); + } + + @Override + protected boolean isUpToDate() { + return(super.isUpToDate() && _entryCursor.isUpToDate()); + } + + @Override + protected void reset(boolean moveForward) { + _entryCursor.reset(moveForward); + super.reset(moveForward); + } + + @Override + protected void restorePositionImpl(PositionImpl curPos, PositionImpl prevPos) + throws IOException + { + if(!(curPos instanceof IndexPosition) || + !(prevPos instanceof IndexPosition)) { + throw new IllegalArgumentException( + "Restored positions must be index positions"); + } + _entryCursor.restorePosition(((IndexPosition)curPos).getEntry(), + ((IndexPosition)prevPos).getEntry()); + super.restorePositionImpl(curPos, prevPos); + } + + @Override + protected boolean findAnotherRowImpl( + ColumnImpl columnPattern, Object valuePattern, boolean moveForward, + ColumnMatcher columnMatcher) + throws IOException + { + if(!isAtBeginning(moveForward)) { + // use the default table scan for finding rows mid-cursor + return super.findAnotherRowImpl(columnPattern, valuePattern, moveForward, + columnMatcher); + } + + // searching for the first match + Object[] rowValues = _entryCursor.getIndexData().constructIndexRow( + columnPattern.getName(), valuePattern); + + if(rowValues == null) { + // bummer, use the default table scan + return super.findAnotherRowImpl(columnPattern, valuePattern, moveForward, + columnMatcher); + } + + // sweet, we can use our index + if(!findPotentialRow(rowValues, true)) { + return false; + } + + // either we found a row with the given value, or none exist in the + // table + return currentRowMatchesImpl(columnPattern, valuePattern, columnMatcher); + } + + /** + * Moves to the first row (as defined by the cursor) where the index entries + * match the given values. Caller manages save/restore on failure. + * + * @param rowValues the column values built from the index column values + * @param requireMatch whether or not an exact match is found + * @return {@code true} if a valid row was found with the given values, + * {@code false} if no row was found + */ + protected boolean findFirstRowByEntryImpl(Object[] rowValues, + boolean requireMatch, + ColumnMatcher columnMatcher) + throws IOException + { + if(!findPotentialRow(rowValues, requireMatch)) { + return false; + } else if(!requireMatch) { + // nothing more to do, we have moved to the closest row + return true; + } + + return currentRowMatchesEntryImpl(rowValues, columnMatcher); + } + + @Override + protected boolean findAnotherRowImpl(Map rowPattern, + boolean moveForward, + ColumnMatcher columnMatcher) + throws IOException + { + if(!isAtBeginning(moveForward)) { + // use the default table scan for finding rows mid-cursor + return super.findAnotherRowImpl(rowPattern, moveForward, columnMatcher); + } + + // searching for the first match + IndexData indexData = _entryCursor.getIndexData(); + Object[] rowValues = indexData.constructIndexRow(rowPattern); + + if(rowValues == null) { + // bummer, use the default table scan + return super.findAnotherRowImpl(rowPattern, moveForward, columnMatcher); + } + + // sweet, we can use our index + if(!findPotentialRow(rowValues, true)) { + // at end of index, no potential matches + return false; + } + + // find actual matching row + Map indexRowPattern = null; + if(rowPattern.size() == indexData.getColumns().size()) { + // the rowPattern matches our index columns exactly, so we can + // streamline our testing below + indexRowPattern = rowPattern; + } else { + // the rowPattern has more columns than just the index, so we need to + // do more work when testing below + Map tmpRowPattern = new LinkedHashMap(); + indexRowPattern = tmpRowPattern; + for(IndexData.ColumnDescriptor idxCol : indexData.getColumns()) { + tmpRowPattern.put(idxCol.getName(), rowValues[idxCol.getColumnIndex()]); + } + } + + // there may be multiple columns which fit the pattern subset used by + // the index, so we need to keep checking until our index values no + // longer match + do { + + if(!currentRowMatchesImpl(indexRowPattern, columnMatcher)) { + // there are no more rows which could possibly match + break; + } + + // note, if rowPattern == indexRowPattern, no need to do an extra + // comparison with the current row + if((rowPattern == indexRowPattern) || + currentRowMatchesImpl(rowPattern, columnMatcher)) { + // found it! + return true; + } + + } while(moveToAnotherRow(moveForward)); + + // none of the potential rows matched + return false; + } + + private boolean currentRowMatchesEntryImpl(Object[] rowValues, + ColumnMatcher columnMatcher) + throws IOException + { + if(_indexEntryPattern == null) { + // init our set of index column names + _indexEntryPattern = new HashSet(); + for(IndexData.ColumnDescriptor col : getIndex().getColumns()) { + _indexEntryPattern.add(col.getName()); + } + } + + // check the next row to see if it actually matches + Row row = getCurrentRow(_indexEntryPattern); + + for(IndexData.ColumnDescriptor col : getIndex().getColumns()) { + String columnName = col.getName(); + Object patValue = rowValues[col.getColumnIndex()]; + Object rowValue = row.get(columnName); + if(!columnMatcher.matches(getTable(), columnName, patValue, rowValue)) { + return false; + } + } + + return true; + } + + private boolean findPotentialRow(Object[] rowValues, boolean requireMatch) + throws IOException + { + _entryCursor.beforeEntry(rowValues); + IndexData.Entry startEntry = _entryCursor.getNextEntry(); + if(requireMatch && !startEntry.getRowId().isValid()) { + // at end of index, no potential matches + return false; + } + // move to position and check it out + restorePosition(new IndexPosition(startEntry)); + return true; + } + + private Object[] toRowValues(Object[] entryValues) + { + return _entryCursor.getIndexData().constructIndexRowFromEntry(entryValues); + } + + @Override + protected PositionImpl findAnotherPosition( + RowState rowState, PositionImpl curPos, boolean moveForward) + throws IOException + { + IndexDirHandler handler = getDirHandler(moveForward); + IndexPosition endPos = (IndexPosition)handler.getEndPosition(); + IndexData.Entry entry = handler.getAnotherEntry(); + return ((!entry.equals(endPos.getEntry())) ? + new IndexPosition(entry) : endPos); + } + + @Override + protected ColumnMatcher getDefaultColumnMatcher() { + if(getIndex().isUnique()) { + // text indexes are case-insensitive, therefore we should always use a + // case-insensitive matcher for unique indexes. + return CaseInsensitiveColumnMatcher.INSTANCE; + } + return SimpleColumnMatcher.INSTANCE; + } + + /** + * Handles moving the table index cursor in a given direction. Separates + * cursor logic from value storage. + */ + private abstract class IndexDirHandler extends DirHandler { + public abstract IndexData.Entry getAnotherEntry() + throws IOException; + } + + /** + * Handles moving the table index cursor forward. + */ + private final class ForwardIndexDirHandler extends IndexDirHandler { + @Override + public PositionImpl getBeginningPosition() { + return getFirstPosition(); + } + @Override + public PositionImpl getEndPosition() { + return getLastPosition(); + } + @Override + public IndexData.Entry getAnotherEntry() throws IOException { + return _entryCursor.getNextEntry(); + } + } + + /** + * Handles moving the table index cursor backward. + */ + private final class ReverseIndexDirHandler extends IndexDirHandler { + @Override + public PositionImpl getBeginningPosition() { + return getLastPosition(); + } + @Override + public PositionImpl getEndPosition() { + return getFirstPosition(); + } + @Override + public IndexData.Entry getAnotherEntry() throws IOException { + return _entryCursor.getPreviousEntry(); + } + } + + /** + * Value object which maintains the current position of an IndexCursor. + */ + private static final class IndexPosition extends PositionImpl + { + private final IndexData.Entry _entry; + + private IndexPosition(IndexData.Entry entry) { + _entry = entry; + } + + @Override + public RowIdImpl getRowId() { + return getEntry().getRowId(); + } + + public IndexData.Entry getEntry() { + return _entry; + } + + @Override + protected boolean equalsImpl(Object o) { + return getEntry().equals(((IndexPosition)o).getEntry()); + } + + @Override + public String toString() { + return "Entry = " + getEntry(); + } + } + + /** + * Row iterator (by matching entry) for this cursor, modifiable. + */ + private final class EntryIterator extends BaseIterator + { + private final Object[] _rowValues; + + private EntryIterator(Collection columnNames, Object[] rowValues, + ColumnMatcher columnMatcher) + { + super(columnNames, false, MOVE_FORWARD, columnMatcher); + _rowValues = rowValues; + try { + _hasNext = findFirstRowByEntryImpl(rowValues, true, _columnMatcher); + _validRow = _hasNext; + } catch(IOException e) { + throw new RuntimeIOException(e); + } + } + + @Override + protected boolean findNext() throws IOException { + return (moveToNextRow() && + currentRowMatchesEntryImpl(_rowValues, _colMatcher)); + } + } + + +} diff --git a/src/java/com/healthmarketscience/jackcess/IndexData.java b/src/java/com/healthmarketscience/jackcess/impl/IndexData.java similarity index 90% rename from src/java/com/healthmarketscience/jackcess/IndexData.java rename to src/java/com/healthmarketscience/jackcess/impl/IndexData.java index d807693..a1e945b 100644 --- a/src/java/com/healthmarketscience/jackcess/IndexData.java +++ b/src/java/com/healthmarketscience/jackcess/impl/IndexData.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.io.IOException; import java.nio.ByteBuffer; @@ -37,13 +37,14 @@ import java.util.Comparator; import java.util.List; import java.util.Map; +import com.healthmarketscience.jackcess.ColumnBuilder; +import com.healthmarketscience.jackcess.Index; +import com.healthmarketscience.jackcess.IndexBuilder; +import static com.healthmarketscience.jackcess.impl.ByteUtil.ByteStream; +import static com.healthmarketscience.jackcess.impl.IndexCodes.*; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import static com.healthmarketscience.jackcess.IndexCodes.*; -import static com.healthmarketscience.jackcess.ByteUtil.ByteStream; - - /** * Access table index data. This is the actual data which backs a logical * Index, where one or more logical indexes can be backed by the same index @@ -51,17 +52,17 @@ import static com.healthmarketscience.jackcess.ByteUtil.ByteStream; * * @author Tim McCune */ -public abstract class IndexData { +public class IndexData { protected static final Log LOG = LogFactory.getLog(Index.class); /** special entry which is less than any other entry */ public static final Entry FIRST_ENTRY = - createSpecialEntry(RowId.FIRST_ROW_ID); + createSpecialEntry(RowIdImpl.FIRST_ROW_ID); /** special entry which is greater than any other entry */ public static final Entry LAST_ENTRY = - createSpecialEntry(RowId.LAST_ROW_ID); + createSpecialEntry(RowIdImpl.LAST_ROW_ID); /** special object which will always be greater than any other value, when searching for an index entry range in a multi-value index */ @@ -70,22 +71,24 @@ public abstract class IndexData { /** special object which will always be greater than any other value, when searching for an index entry range in a multi-value index */ public static final Object MIN_VALUE = new Object(); + + private static final DataPage NEW_ROOT_DATA_PAGE = new RootDataPage(); protected static final int INVALID_INDEX_PAGE_NUMBER = 0; /** Max number of columns in an index */ - static final int MAX_COLUMNS = 10; + public static final int MAX_COLUMNS = 10; protected static final byte[] EMPTY_PREFIX = new byte[0]; static final short COLUMN_UNUSED = -1; - static final byte ASCENDING_COLUMN_FLAG = (byte)0x01; + public static final byte ASCENDING_COLUMN_FLAG = (byte)0x01; - static final byte UNIQUE_INDEX_FLAG = (byte)0x01; - static final byte IGNORE_NULLS_INDEX_FLAG = (byte)0x02; - static final byte SPECIAL_INDEX_FLAG = (byte)0x08; // set on MSysACEs and MSysAccessObjects indexes, purpose unknown - static final byte UNKNOWN_INDEX_FLAG = (byte)0x80; // always seems to be set on indexes in access 2000+ + public static final byte UNIQUE_INDEX_FLAG = (byte)0x01; + public static final byte IGNORE_NULLS_INDEX_FLAG = (byte)0x02; + public static final byte SPECIAL_INDEX_FLAG = (byte)0x08; // set on MSysACEs and MSysAccessObjects indexes, purpose unknown + public static final byte UNKNOWN_INDEX_FLAG = (byte)0x80; // always seems to be set on indexes in access 2000+ private static final int MAGIC_INDEX_NUMBER = 1923; @@ -110,7 +113,7 @@ public abstract class IndexData { ALWAYS_LAST; } - static final Comparator BYTE_CODE_COMPARATOR = + public static final Comparator BYTE_CODE_COMPARATOR = new Comparator() { public int compare(byte[] left, byte[] right) { if(left == right) { @@ -139,7 +142,7 @@ public abstract class IndexData { /** owning table */ - private final Table _table; + private final TableImpl _table; /** 0-based index data number */ private final int _number; /** Page number of the root index data */ @@ -174,10 +177,12 @@ public abstract class IndexData { private final int _maxPageEntrySize; /** whether or not this index data is backing a primary key logical index */ private boolean _primaryKey; - /** FIXME, for SimpleIndex, we can't write multi-page indexes or indexes using the entry compression scheme */ - private boolean _readOnly; + /** if non-null, the reason why we cannot create entries for this index */ + private String _unsupportedReason; + /** Cache which manages the index pages */ + private final IndexPageCache _pageCache; - protected IndexData(Table table, int number, int uniqueEntryCount, + protected IndexData(TableImpl table, int number, int uniqueEntryCount, int uniqueEntryCountOffset) { _table = table; @@ -185,13 +190,14 @@ public abstract class IndexData { _uniqueEntryCount = uniqueEntryCount; _uniqueEntryCountOffset = uniqueEntryCountOffset; _maxPageEntrySize = calcMaxPageEntrySize(_table.getFormat()); + _pageCache = new IndexPageCache(this); } /** * Creates an IndexData appropriate for the given table, using information * from the given table definition buffer. */ - public static IndexData create(Table table, ByteBuffer tableBuffer, + public static IndexData create(TableImpl table, ByteBuffer tableBuffer, int number, JetFormat format) throws IOException { @@ -200,14 +206,10 @@ public abstract class IndexData { (number * format.SIZE_INDEX_DEFINITION) + 4); int uniqueEntryCount = tableBuffer.getInt(uniqueEntryCountOffset); - return(table.doUseBigIndex() ? - new BigIndexData(table, number, uniqueEntryCount, - uniqueEntryCountOffset) : - new SimpleIndexData(table, number, uniqueEntryCount, - uniqueEntryCountOffset)); + return new IndexData(table, number, uniqueEntryCount, uniqueEntryCountOffset); } - public Table getTable() { + public TableImpl getTable() { return _table; } @@ -318,12 +320,13 @@ public abstract class IndexData { return _rootPageNumber; } - protected void setReadOnly() { - _readOnly = true; + private void setUnsupportedReason(String reason) { + _unsupportedReason = reason; + LOG.warn(reason + ", making read-only"); } - protected boolean isReadOnly() { - return _readOnly; + String getUnsupportedReason() { + return _unsupportedReason; } protected int getMaxPageEntrySize() { @@ -342,13 +345,22 @@ public abstract class IndexData { _ownedPages.addPageNumber(pageNumber); } + /** + * Used by unit tests to validate the internal status of the index. + * @usage _advanced_method_ + */ + public void validate() throws IOException { + _pageCache.validate(); + } + /** * Returns the number of index entries in the index. Only called by unit * tests. *

* Forces index initialization. + * @usage _advanced_method_ */ - protected int getEntryCount() + public int getEntryCount() throws IOException { initialize(); @@ -368,7 +380,7 @@ public abstract class IndexData { */ public void initialize() throws IOException { if(!_initialized) { - readIndexEntries(); + _pageCache.setRootPageNumber(getRootPageNumber()); _initialized = true; } } @@ -383,11 +395,11 @@ public abstract class IndexData { // make sure we've parsed the entries initialize(); - if(_readOnly) { + if(_unsupportedReason != null) { throw new UnsupportedOperationException( - "FIXME cannot write indexes of this type yet, see Database javadoc for info on enabling large index support"); + "Cannot write indexes of this type due to " + _unsupportedReason); } - updateImpl(); + _pageCache.write(); } /** @@ -395,7 +407,7 @@ public abstract class IndexData { * @param tableBuffer table definition buffer to read from initial info * @param availableColumns Columns that this index may use */ - public void read(ByteBuffer tableBuffer, List availableColumns) + public void read(ByteBuffer tableBuffer, List availableColumns) throws IOException { ByteUtil.forward(tableBuffer, getFormat().SKIP_BEFORE_INDEX); //Forward past Unknown @@ -406,8 +418,8 @@ public abstract class IndexData { if (columnNumber != COLUMN_UNUSED) { // find the desired column by column number (which is not necessarily // the same as the column index) - Column idxCol = null; - for(Column col : availableColumns) { + ColumnImpl idxCol = null; + for(ColumnImpl col : availableColumns) { if(col.getColumnNumber() == columnNumber) { idxCol = col; break; @@ -453,7 +465,7 @@ public abstract class IndexData { throws IOException { ByteBuffer rootPageBuffer = creator.getPageChannel().createPageBuffer(); - writeDataPage(rootPageBuffer, SimpleIndexData.NEW_ROOT_DATA_PAGE, + writeDataPage(rootPageBuffer, NEW_ROOT_DATA_PAGE, creator.getTdefPageNumber(), creator.getFormat()); for(IndexBuilder idx : creator.getIndexes()) { @@ -473,7 +485,7 @@ public abstract class IndexData { flags = idxCol.getFlags(); // find actual table column number - for(Column col : creator.getColumns()) { + for(ColumnBuilder col : creator.getColumns()) { if(col.getName().equalsIgnoreCase(idxCol.getName())) { columnNumber = col.getColumnNumber(); break; @@ -514,7 +526,7 @@ public abstract class IndexData { * @param row Row to add * @param rowId rowId of the row to be added */ - public void addRow(Object[] row, RowId rowId) + public void addRow(Object[] row, RowIdImpl rowId) throws IOException { int nullCount = countNullValues(row); @@ -588,7 +600,7 @@ public abstract class IndexData { * @param row Row to remove * @param rowId rowId of the row to be removed */ - public void deleteRow(Object[] row, RowId rowId) + public void deleteRow(Object[] row, RowIdImpl rowId) throws IOException { int nullCount = countNullValues(row); @@ -628,7 +640,7 @@ public abstract class IndexData { Position tmpPos = null; Position endPos = cursor._lastPos; while(!endPos.equals( - tmpPos = cursor.getAnotherPosition(Cursor.MOVE_FORWARD))) { + tmpPos = cursor.getAnotherPosition(CursorImpl.MOVE_FORWARD))) { if(tmpPos.getEntry().getRowId().equals(oldEntry.getRowId())) { dataPage = tmpPos.getDataPage(); idx = tmpPos.getIndex(); @@ -685,7 +697,7 @@ public abstract class IndexData { startEntryBytes = createEntryBytes(startRow); startEntry = new Entry(startEntryBytes, (startInclusive ? - RowId.FIRST_ROW_ID : RowId.LAST_ROW_ID)); + RowIdImpl.FIRST_ROW_ID : RowIdImpl.LAST_ROW_ID)); } Entry endEntry = LAST_ENTRY; if(endRow != null) { @@ -696,7 +708,7 @@ public abstract class IndexData { createEntryBytes(endRow)); endEntry = new Entry(endEntryBytes, (endInclusive ? - RowId.LAST_ROW_ID : RowId.FIRST_ROW_ID)); + RowIdImpl.LAST_ROW_ID : RowIdImpl.FIRST_ROW_ID)); } return new EntryCursor(findEntryPosition(startEntry), findEntryPosition(endEntry)); @@ -853,6 +865,7 @@ public abstract class IndexData { throw new RuntimeException(e); } } + rtn.append("\n").append(_pageCache.toString()); return rtn.toString(); } @@ -863,10 +876,6 @@ public abstract class IndexData { throws IOException { if(dataPage.getCompressedEntrySize() > _maxPageEntrySize) { - if(this instanceof SimpleIndexData) { - throw new UnsupportedOperationException( - "FIXME cannot write large index yet, see Database javadoc for info on enabling large index support"); - } throw new IllegalStateException("data page is too large"); } @@ -1092,7 +1101,7 @@ public abstract class IndexData { for(ColumnDescriptor col : _columns) { Object value = values[col.getColumnIndex()]; - if(Column.isRawData(value)) { + if(ColumnImpl.isRawData(value)) { // ignore it, we could not parse it continue; } @@ -1113,30 +1122,24 @@ public abstract class IndexData { return _entryBuffer.toByteArray(); } - - /** - * Writes the current index state to the database. Index has already been - * initialized. - */ - protected abstract void updateImpl() throws IOException; - - /** - * Reads the actual index entries. - */ - protected abstract void readIndexEntries() - throws IOException; /** * Finds the data page for the given entry. */ - protected abstract DataPage findDataPage(Entry entry) - throws IOException; + protected DataPage findDataPage(Entry entry) + throws IOException + { + return _pageCache.findCacheDataPage(entry); + } /** * Gets the data page for the pageNumber. */ - protected abstract DataPage getDataPage(int pageNumber) - throws IOException; + protected DataPage getDataPage(int pageNumber) + throws IOException + { + return _pageCache.getCacheDataPage(pageNumber); + } /** * Flips the first bit in the byte at the given index. @@ -1168,7 +1171,7 @@ public abstract class IndexData { /** * Writes the value of the given column type to a byte array and returns it. */ - private static byte[] encodeNumberColumnValue(Object value, Column column) + private static byte[] encodeNumberColumnValue(Object value, ColumnImpl column) throws IOException { // always write in big endian order @@ -1178,30 +1181,29 @@ public abstract class IndexData { /** * Creates one of the special index entries. */ - private static Entry createSpecialEntry(RowId rowId) { + private static Entry createSpecialEntry(RowIdImpl rowId) { return new Entry((byte[])null, rowId); } /** * Constructs a ColumnDescriptor of the relevant type for the given Column. */ - private ColumnDescriptor newColumnDescriptor(Column col, byte flags) + private ColumnDescriptor newColumnDescriptor(ColumnImpl col, byte flags) throws IOException { switch(col.getType()) { case TEXT: case MEMO: - Column.SortOrder sortOrder = col.getTextSortOrder(); - if(Column.GENERAL_LEGACY_SORT_ORDER.equals(sortOrder)) { + ColumnImpl.SortOrder sortOrder = col.getTextSortOrder(); + if(ColumnImpl.GENERAL_LEGACY_SORT_ORDER.equals(sortOrder)) { return new GenLegTextColumnDescriptor(col, flags); } - if(Column.GENERAL_SORT_ORDER.equals(sortOrder)) { + if(ColumnImpl.GENERAL_SORT_ORDER.equals(sortOrder)) { return new GenTextColumnDescriptor(col, flags); } // unsupported sort order - LOG.warn("Unsupported collating sort order " + sortOrder + - " for text index, making read-only"); - setReadOnly(); + setUnsupportedReason("unsupported collating sort order " + sortOrder + + " for text index"); return new ReadOnlyColumnDescriptor(col, flags); case INT: case LONG: @@ -1224,10 +1226,9 @@ public abstract class IndexData { return new GuidColumnDescriptor(col, flags); default: - // FIXME we can't modify this index at this point in time - LOG.warn("Unsupported data type " + col.getType() + - " for index, making read-only"); - setReadOnly(); + // we can't modify this index at this point in time + setUnsupportedReason("unsupported data type " + col.getType() + + " for index"); return new ReadOnlyColumnDescriptor(col, flags); } } @@ -1235,16 +1236,17 @@ public abstract class IndexData { /** * Returns the EntryType based on the given entry info. */ - private static EntryType determineEntryType(byte[] entryBytes, RowId rowId) + private static EntryType determineEntryType(byte[] entryBytes, + RowIdImpl rowId) { if(entryBytes != null) { - return ((rowId.getType() == RowId.Type.NORMAL) ? + return ((rowId.getType() == RowIdImpl.Type.NORMAL) ? EntryType.NORMAL : - ((rowId.getType() == RowId.Type.ALWAYS_FIRST) ? + ((rowId.getType() == RowIdImpl.Type.ALWAYS_FIRST) ? EntryType.FIRST_VALID : EntryType.LAST_VALID)); } else if(!rowId.isValid()) { // this is a "special" entry (first/last) - return ((rowId.getType() == RowId.Type.ALWAYS_FIRST) ? + return ((rowId.getType() == RowIdImpl.Type.ALWAYS_FIRST) ? EntryType.ALWAYS_FIRST : EntryType.ALWAYS_LAST); } throw new IllegalArgumentException("Values was null for valid entry"); @@ -1269,19 +1271,19 @@ public abstract class IndexData { * Information about the columns in an index. Also encodes new index * values. */ - public static abstract class ColumnDescriptor + public static abstract class ColumnDescriptor implements Index.Column { - private final Column _column; + private final ColumnImpl _column; private final byte _flags; - private ColumnDescriptor(Column column, byte flags) + private ColumnDescriptor(ColumnImpl column, byte flags) throws IOException { _column = column; _flags = flags; } - public Column getColumn() { + public ColumnImpl getColumn() { return _column; } @@ -1335,7 +1337,7 @@ public abstract class IndexData { */ private static final class IntegerColumnDescriptor extends ColumnDescriptor { - private IntegerColumnDescriptor(Column column, byte flags) + private IntegerColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); @@ -1367,7 +1369,7 @@ public abstract class IndexData { private static final class FloatingPointColumnDescriptor extends ColumnDescriptor { - private FloatingPointColumnDescriptor(Column column, byte flags) + private FloatingPointColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); @@ -1407,7 +1409,7 @@ public abstract class IndexData { private static class LegacyFixedPointColumnDescriptor extends ColumnDescriptor { - private LegacyFixedPointColumnDescriptor(Column column, byte flags) + private LegacyFixedPointColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); @@ -1458,7 +1460,7 @@ public abstract class IndexData { private static final class FixedPointColumnDescriptor extends LegacyFixedPointColumnDescriptor { - private FixedPointColumnDescriptor(Column column, byte flags) + private FixedPointColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); @@ -1484,7 +1486,7 @@ public abstract class IndexData { */ private static final class ByteColumnDescriptor extends ColumnDescriptor { - private ByteColumnDescriptor(Column column, byte flags) + private ByteColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); @@ -1513,7 +1515,7 @@ public abstract class IndexData { */ private static final class BooleanColumnDescriptor extends ColumnDescriptor { - private BooleanColumnDescriptor(Column column, byte flags) + private BooleanColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); @@ -1530,7 +1532,7 @@ public abstract class IndexData { throws IOException { bout.write( - Column.toBooleanValue(value) ? + ColumnImpl.toBooleanValue(value) ? (isAscending() ? ASC_BOOLEAN_TRUE : DESC_BOOLEAN_TRUE) : (isAscending() ? ASC_BOOLEAN_FALSE : DESC_BOOLEAN_FALSE)); } @@ -1542,7 +1544,7 @@ public abstract class IndexData { private static final class GenLegTextColumnDescriptor extends ColumnDescriptor { - private GenLegTextColumnDescriptor(Column column, byte flags) + private GenLegTextColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); @@ -1563,7 +1565,7 @@ public abstract class IndexData { */ private static final class GenTextColumnDescriptor extends ColumnDescriptor { - private GenTextColumnDescriptor(Column column, byte flags) + private GenTextColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); @@ -1584,7 +1586,7 @@ public abstract class IndexData { */ private static final class GuidColumnDescriptor extends ColumnDescriptor { - private GuidColumnDescriptor(Column column, byte flags) + private GuidColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); @@ -1617,9 +1619,9 @@ public abstract class IndexData { /** * ColumnDescriptor for columns which we cannot currently write. */ - private static final class ReadOnlyColumnDescriptor extends ColumnDescriptor + private final class ReadOnlyColumnDescriptor extends ColumnDescriptor { - private ReadOnlyColumnDescriptor(Column column, byte flags) + private ReadOnlyColumnDescriptor(ColumnImpl column, byte flags) throws IOException { super(column, flags); @@ -1629,7 +1631,8 @@ public abstract class IndexData { protected void writeNonNullValue(Object value, ByteStream bout) throws IOException { - throw new UnsupportedOperationException("should not be called"); + throw new UnsupportedOperationException( + "Cannot write indexes of this type due to " + _unsupportedReason); } } @@ -1639,7 +1642,7 @@ public abstract class IndexData { public static class Entry implements Comparable { /** page/row on which this row is stored */ - private final RowId _rowId; + private final RowIdImpl _rowId; /** the entry value */ private final byte[] _entryBytes; /** comparable type for the entry */ @@ -1651,7 +1654,7 @@ public abstract class IndexData { * @param rowId rowId in which the row is stored * @param type the type of the entry */ - private Entry(byte[] entryBytes, RowId rowId, EntryType type) { + private Entry(byte[] entryBytes, RowIdImpl rowId, EntryType type) { _rowId = rowId; _entryBytes = entryBytes; _type = type; @@ -1662,7 +1665,7 @@ public abstract class IndexData { * @param entryBytes encoded bytes for this index entry * @param rowId rowId in which the row is stored */ - private Entry(byte[] entryBytes, RowId rowId) + private Entry(byte[] entryBytes, RowIdImpl rowId) { this(entryBytes, rowId, determineEntryType(entryBytes, rowId)); } @@ -1693,11 +1696,11 @@ public abstract class IndexData { int page = ByteUtil.get3ByteInt(buffer, ENTRY_BYTE_ORDER); int row = ByteUtil.getUnsignedByte(buffer); - _rowId = new RowId(page, row); + _rowId = new RowIdImpl(page, row); _type = EntryType.NORMAL; } - public RowId getRowId() { + public RowIdImpl getRowId() { return _rowId; } @@ -1851,7 +1854,7 @@ public abstract class IndexData { * @param type the type of the entry * @param subPageNumber the sub-page to which this node entry refers */ - private NodeEntry(byte[] entryBytes, RowId rowId, EntryType type, + private NodeEntry(byte[] entryBytes, RowIdImpl rowId, EntryType type, Integer subPageNumber) { super(entryBytes, rowId, type); _subPageNumber = subPageNumber; @@ -1980,11 +1983,11 @@ public abstract class IndexData { } public void beforeFirst() { - reset(Cursor.MOVE_FORWARD); + reset(CursorImpl.MOVE_FORWARD); } public void afterLast() { - reset(Cursor.MOVE_REVERSE); + reset(CursorImpl.MOVE_REVERSE); } protected void reset(boolean moveForward) @@ -2000,8 +2003,8 @@ public abstract class IndexData { public void beforeEntry(Object[] row) throws IOException { - restorePosition( - new Entry(IndexData.this.createEntryBytes(row), RowId.FIRST_ROW_ID)); + restorePosition(new Entry(IndexData.this.createEntryBytes(row), + RowIdImpl.FIRST_ROW_ID)); } /** @@ -2011,8 +2014,8 @@ public abstract class IndexData { public void afterEntry(Object[] row) throws IOException { - restorePosition( - new Entry(IndexData.this.createEntryBytes(row), RowId.LAST_ROW_ID)); + restorePosition(new Entry(IndexData.this.createEntryBytes(row), + RowIdImpl.LAST_ROW_ID)); } /** @@ -2020,7 +2023,7 @@ public abstract class IndexData { * {@code #getLastEntry} otherwise */ public Entry getNextEntry() throws IOException { - return getAnotherPosition(Cursor.MOVE_FORWARD).getEntry(); + return getAnotherPosition(CursorImpl.MOVE_FORWARD).getEntry(); } /** @@ -2028,7 +2031,7 @@ public abstract class IndexData { * {@code #getFirstEntry} otherwise */ public Entry getPreviousEntry() throws IOException { - return getAnotherPosition(Cursor.MOVE_REVERSE).getEntry(); + return getAnotherPosition(CursorImpl.MOVE_REVERSE).getEntry(); } /** @@ -2373,5 +2376,52 @@ public abstract class IndexData { } } + /** + * Simple implementation of a DataPage + */ + private static final class RootDataPage extends DataPage { + + @Override + public int getPageNumber() { return 0; } + + @Override + public boolean isLeaf() { return true; } + @Override + public void setLeaf(boolean isLeaf) { } + + @Override + public int getPrevPageNumber() { return 0; } + @Override + public void setPrevPageNumber(int pageNumber) { } + + @Override + public int getNextPageNumber() { return 0; } + @Override + public void setNextPageNumber(int pageNumber) { } + + @Override + public int getChildTailPageNumber() { return 0; } + @Override + public void setChildTailPageNumber(int pageNumber) { } + + @Override + public int getTotalEntrySize() { return 0; } + @Override + public void setTotalEntrySize(int totalSize) { } + + @Override + public byte[] getEntryPrefix() { return EMPTY_PREFIX; } + @Override + public void setEntryPrefix(byte[] entryPrefix) { } + + @Override + public List getEntries() { return Collections.emptyList(); } + @Override + public void setEntries(List entries) { } + @Override + public void addEntry(int idx, Entry entry) { } + @Override + public void removeEntry(int idx) { } + } } diff --git a/src/java/com/healthmarketscience/jackcess/impl/IndexImpl.java b/src/java/com/healthmarketscience/jackcess/impl/IndexImpl.java new file mode 100644 index 0000000..1fd560b --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/IndexImpl.java @@ -0,0 +1,458 @@ +/* +Copyright (c) 2005 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.impl; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import com.healthmarketscience.jackcess.Index; +import com.healthmarketscience.jackcess.RowId; +import com.healthmarketscience.jackcess.IndexBuilder; + +/** + * Access table (logical) index. Logical indexes are backed for IndexData, + * where one or more logical indexes could be backed by the same data. + * + * @author Tim McCune + */ +public class IndexImpl implements Index, Comparable +{ + protected static final Log LOG = LogFactory.getLog(IndexImpl.class); + + /** index type for primary key indexes */ + public static final byte PRIMARY_KEY_INDEX_TYPE = (byte)1; + + /** index type for foreign key indexes */ + public static final byte FOREIGN_KEY_INDEX_TYPE = (byte)2; + + /** flag for indicating that updates should cascade in a foreign key index */ + private static final byte CASCADE_UPDATES_FLAG = (byte)1; + /** flag for indicating that deletes should cascade in a foreign key index */ + private static final byte CASCADE_DELETES_FLAG = (byte)1; + + /** index table type for the "primary" table in a foreign key index */ + private static final byte PRIMARY_TABLE_TYPE = (byte)1; + + /** indicate an invalid index number for foreign key field */ + private static final int INVALID_INDEX_NUMBER = -1; + + /** the actual data backing this index (more than one index may be backed by + the same data */ + private final IndexData _data; + /** 0-based index number */ + private final int _indexNumber; + /** the type of the index */ + private final byte _indexType; + /** Index name */ + private String _name; + /** foreign key reference info, if any */ + private final ForeignKeyReference _reference; + + protected IndexImpl(ByteBuffer tableBuffer, List indexDatas, + JetFormat format) + throws IOException + { + + ByteUtil.forward(tableBuffer, format.SKIP_BEFORE_INDEX_SLOT); //Forward past Unknown + _indexNumber = tableBuffer.getInt(); + int indexDataNumber = tableBuffer.getInt(); + + // read foreign key reference info + byte relIndexType = tableBuffer.get(); + int relIndexNumber = tableBuffer.getInt(); + int relTablePageNumber = tableBuffer.getInt(); + byte cascadeUpdatesFlag = tableBuffer.get(); + byte cascadeDeletesFlag = tableBuffer.get(); + + _indexType = tableBuffer.get(); + + if((_indexType == FOREIGN_KEY_INDEX_TYPE) && + (relIndexNumber != INVALID_INDEX_NUMBER)) { + _reference = new ForeignKeyReference( + relIndexType, relIndexNumber, relTablePageNumber, + (cascadeUpdatesFlag == CASCADE_UPDATES_FLAG), + (cascadeDeletesFlag == CASCADE_DELETES_FLAG)); + } else { + _reference = null; + } + + ByteUtil.forward(tableBuffer, format.SKIP_AFTER_INDEX_SLOT); //Skip past Unknown + + _data = indexDatas.get(indexDataNumber); + + _data.addIndex(this); + } + + public IndexData getIndexData() { + return _data; + } + + public TableImpl getTable() { + return getIndexData().getTable(); + } + + public JetFormat getFormat() { + return getTable().getFormat(); + } + + public PageChannel getPageChannel() { + return getTable().getPageChannel(); + } + + public int getIndexNumber() { + return _indexNumber; + } + + public byte getIndexFlags() { + return getIndexData().getIndexFlags(); + } + + public int getUniqueEntryCount() { + return getIndexData().getUniqueEntryCount(); + } + + public int getUniqueEntryCountOffset() { + return getIndexData().getUniqueEntryCountOffset(); + } + + public String getName() { + return _name; + } + + void setName(String name) { + _name = name; + } + + public boolean isPrimaryKey() { + return _indexType == PRIMARY_KEY_INDEX_TYPE; + } + + public boolean isForeignKey() { + return _indexType == FOREIGN_KEY_INDEX_TYPE; + } + + public ForeignKeyReference getReference() { + return _reference; + } + + public IndexImpl getReferencedIndex() throws IOException { + + if(_reference == null) { + return null; + } + + TableImpl refTable = getTable().getDatabase().getTable( + _reference.getOtherTablePageNumber()); + + if(refTable == null) { + throw new IOException("Reference to missing table " + + _reference.getOtherTablePageNumber()); + } + + IndexImpl refIndex = null; + int idxNumber = _reference.getOtherIndexNumber(); + for(IndexImpl idx : refTable.getIndexes()) { + if(idx.getIndexNumber() == idxNumber) { + refIndex = idx; + break; + } + } + + if(refIndex == null) { + throw new IOException("Reference to missing index " + idxNumber + + " on table " + refTable.getName()); + } + + // finally verify that we found the expected index (should reference this + // index) + ForeignKeyReference otherRef = refIndex.getReference(); + if((otherRef == null) || + (otherRef.getOtherTablePageNumber() != + getTable().getTableDefPageNumber()) || + (otherRef.getOtherIndexNumber() != _indexNumber)) { + throw new IOException("Found unexpected index " + refIndex.getName() + + " on table " + refTable.getName() + + " with reference " + otherRef); + } + + return refIndex; + } + + public boolean shouldIgnoreNulls() { + return getIndexData().shouldIgnoreNulls(); + } + + public boolean isUnique() { + return getIndexData().isUnique(); + } + + public List getColumns() { + return getIndexData().getColumns(); + } + + /** + * Whether or not the complete index state has been read. + */ + public boolean isInitialized() { + return getIndexData().isInitialized(); + } + + /** + * Forces initialization of this index (actual parsing of index pages). + * normally, the index will not be initialized until the entries are + * actually needed. + */ + public void initialize() throws IOException { + getIndexData().initialize(); + } + + /** + * Writes the current index state to the database. + *

+ * Forces index initialization. + */ + public void update() throws IOException { + getIndexData().update(); + } + + /** + * Adds a row to this index + *

+ * Forces index initialization. + * + * @param row Row to add + * @param rowId rowId of the row to be added + */ + public void addRow(Object[] row, RowIdImpl rowId) + throws IOException + { + getIndexData().addRow(row, rowId); + } + + /** + * Removes a row from this index + *

+ * Forces index initialization. + * + * @param row Row to remove + * @param rowId rowId of the row to be removed + */ + public void deleteRow(Object[] row, RowIdImpl rowId) + throws IOException + { + getIndexData().deleteRow(row, rowId); + } + + /** + * Gets a new cursor for this index. + *

+ * Forces index initialization. + */ + public IndexData.EntryCursor cursor() + throws IOException + { + return cursor(null, true, null, true); + } + + /** + * Gets a new cursor for this index, narrowed to the range defined by the + * given startRow and endRow. + *

+ * Forces index initialization. + * + * @param startRow the first row of data for the cursor, or {@code null} for + * the first entry + * @param startInclusive whether or not startRow is inclusive or exclusive + * @param endRow the last row of data for the cursor, or {@code null} for + * the last entry + * @param endInclusive whether or not endRow is inclusive or exclusive + */ + public IndexData.EntryCursor cursor(Object[] startRow, + boolean startInclusive, + Object[] endRow, + boolean endInclusive) + throws IOException + { + return getIndexData().cursor(startRow, startInclusive, endRow, + endInclusive); + } + + /** + * Constructs an array of values appropriate for this index from the given + * column values, expected to match the columns for this index. + * @return the appropriate sparse array of data + * @throws IllegalArgumentException if the wrong number of values are + * provided + */ + public Object[] constructIndexRowFromEntry(Object... values) + { + return getIndexData().constructIndexRowFromEntry(values); + } + + /** + * Constructs an array of values appropriate for this index from the given + * column value. + * @return the appropriate sparse array of data or {@code null} if not all + * columns for this index were provided + */ + public Object[] constructIndexRow(String colName, Object value) + { + return constructIndexRow(Collections.singletonMap(colName, value)); + } + + /** + * Constructs an array of values appropriate for this index from the given + * column values. + * @return the appropriate sparse array of data or {@code null} if not all + * columns for this index were provided + */ + public Object[] constructIndexRow(Map row) + { + return getIndexData().constructIndexRow(row); + } + + @Override + public String toString() { + StringBuilder rtn = new StringBuilder(); + rtn.append("\tName: (").append(getTable().getName()).append(") ") + .append(_name); + rtn.append("\n\tNumber: ").append(_indexNumber); + rtn.append("\n\tIs Primary Key: ").append(isPrimaryKey()); + rtn.append("\n\tIs Foreign Key: ").append(isForeignKey()); + if(_reference != null) { + rtn.append("\n\tForeignKeyReference: ").append(_reference); + } + rtn.append(_data.toString()); + rtn.append("\n\n"); + return rtn.toString(); + } + + public int compareTo(IndexImpl other) { + if (_indexNumber > other.getIndexNumber()) { + return 1; + } else if (_indexNumber < other.getIndexNumber()) { + return -1; + } else { + return 0; + } + } + + /** + * Writes the logical index definitions into a table definition buffer. + * @param buffer Buffer to write to + * @param indexes List of IndexBuilders to write definitions for + */ + protected static void writeDefinitions( + TableCreator creator, ByteBuffer buffer) + throws IOException + { + // write logical index information + for(IndexBuilder idx : creator.getIndexes()) { + TableCreator.IndexState idxState = creator.getIndexState(idx); + buffer.putInt(TableImpl.MAGIC_TABLE_NUMBER); // seemingly constant magic value which matches the table def + buffer.putInt(idxState.getIndexNumber()); // index num + buffer.putInt(idxState.getIndexDataNumber()); // index data num + buffer.put((byte)0); // related table type + buffer.putInt(INVALID_INDEX_NUMBER); // related index num + buffer.putInt(0); // related table definition page number + buffer.put((byte)0); // cascade updates flag + buffer.put((byte)0); // cascade deletes flag + buffer.put(idx.getType()); // index type flags + buffer.putInt(0); // unknown + } + + // write index names + for(IndexBuilder idx : creator.getIndexes()) { + TableImpl.writeName(buffer, idx.getName(), creator.getCharset()); + } + } + + /** + * Information about a foreign key reference defined in an index (when + * referential integrity should be enforced). + */ + public static class ForeignKeyReference + { + private final byte _tableType; + private final int _otherIndexNumber; + private final int _otherTablePageNumber; + private final boolean _cascadeUpdates; + private final boolean _cascadeDeletes; + + public ForeignKeyReference( + byte tableType, int otherIndexNumber, int otherTablePageNumber, + boolean cascadeUpdates, boolean cascadeDeletes) + { + _tableType = tableType; + _otherIndexNumber = otherIndexNumber; + _otherTablePageNumber = otherTablePageNumber; + _cascadeUpdates = cascadeUpdates; + _cascadeDeletes = cascadeDeletes; + } + + public byte getTableType() { + return _tableType; + } + + public boolean isPrimaryTable() { + return(getTableType() == PRIMARY_TABLE_TYPE); + } + + public int getOtherIndexNumber() { + return _otherIndexNumber; + } + + public int getOtherTablePageNumber() { + return _otherTablePageNumber; + } + + public boolean isCascadeUpdates() { + return _cascadeUpdates; + } + + public boolean isCascadeDeletes() { + return _cascadeDeletes; + } + + @Override + public String toString() { + return new StringBuilder() + .append("\n\t\tOther Index Number: ").append(_otherIndexNumber) + .append("\n\t\tOther Table Page Num: ").append(_otherTablePageNumber) + .append("\n\t\tIs Primary Table: ").append(isPrimaryTable()) + .append("\n\t\tIs Cascade Updates: ").append(isCascadeUpdates()) + .append("\n\t\tIs Cascade Deletes: ").append(isCascadeDeletes()) + .toString(); + } + } +} diff --git a/src/java/com/healthmarketscience/jackcess/IndexPageCache.java b/src/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java similarity index 95% rename from src/java/com/healthmarketscience/jackcess/IndexPageCache.java rename to src/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java index 56cb44a..325e178 100644 --- a/src/java/com/healthmarketscience/jackcess/IndexPageCache.java +++ b/src/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.io.IOException; import java.lang.ref.Reference; @@ -33,17 +33,16 @@ import java.lang.ref.SoftReference; import java.util.AbstractList; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.RandomAccess; - -import static com.healthmarketscience.jackcess.IndexData.*; +import static com.healthmarketscience.jackcess.impl.IndexData.*; /** - * Manager of the index pages for a BigIndex. + * Manager of the index pages for a IndexData. * @author James Ahlborn */ public class IndexPageCache @@ -52,29 +51,45 @@ public class IndexPageCache ADD, REMOVE, REPLACE; } + /** max number of pages to cache (unless a write operation is in + progress) */ + private static final int MAX_CACHE_SIZE = 25; + /** the index whose pages this cache is managing */ - private final BigIndexData _indexData; + private final IndexData _indexData; /** the root page for the index */ private DataPageMain _rootPage; /** the currently loaded pages for this index, pageNumber -> page */ private final Map _dataPages = - new HashMap(); + new LinkedHashMap(16, 0.75f, true) { + private static final long serialVersionUID = 0L; + @Override + protected boolean removeEldestEntry(Map.Entry e) { + // only purge when the size is too big and a logical write operation is + // not in progress (while an update is happening, the pages can be in + // flux and removing pages from the cache can cause problems) + if((size() > MAX_CACHE_SIZE) && !getPageChannel().isWriting()) { + purgeOldPages(); + } + return false; + } + }; /** the currently modified index pages */ private final List _modifiedPages = new ArrayList(); - public IndexPageCache(BigIndexData indexData) { + public IndexPageCache(IndexData indexData) { _indexData = indexData; } - public BigIndexData getIndexData() { + public IndexData getIndexData() { return _indexData; } public PageChannel getPageChannel() { return getIndexData().getPageChannel(); } - + /** * Sets the root page for this index, must be called before normal usage. * @@ -98,6 +113,10 @@ public class IndexPageCache preparePagesForWriting(); // finally, write all the modified pages (which are not being deleted) writeDataPages(); + // after we write everything, we can purge our cache if necessary + if(_dataPages.size() > MAX_CACHE_SIZE) { + purgeOldPages(); + } } /** @@ -575,7 +594,7 @@ public class IndexPageCache * @throws IllegalStateException if the entry type does not match the page * type */ - private void validateEntryForPage(DataPageMain dpMain, Entry entry) { + private static void validateEntryForPage(DataPageMain dpMain, Entry entry) { if(dpMain._leaf != entry.isLeafEntry()) { throw new IllegalStateException( "Trying to update page with wrong entry type; pageLeaf " + @@ -645,7 +664,7 @@ public class IndexPageCache // insert this new page between the old page and any previous page addToPeersBefore(newDataPage, origDataPage); - + if(!newMain._leaf) { // reparent the children pages of the new page reparentChildren(newDataPage); @@ -965,7 +984,8 @@ public class IndexPageCache * Used by unit tests to validate the internal status of the index. */ void validate() throws IOException { - for(DataPageMain dpMain : _dataPages.values()) { + // copy the values as the validation methods might trigger map updates + for(DataPageMain dpMain : new ArrayList(_dataPages.values())) { DataPageExtra dpExtra = dpMain.getExtra(); validateEntries(dpExtra); validateChildren(dpMain, dpExtra); @@ -978,7 +998,7 @@ public class IndexPageCache * * @param dpExtra the entries to validate */ - private void validateEntries(DataPageExtra dpExtra) throws IOException { + private static void validateEntries(DataPageExtra dpExtra) throws IOException { int entrySize = 0; Entry prevEntry = IndexData.FIRST_ENTRY; for(Entry e : dpExtra._entries) { @@ -1019,7 +1039,7 @@ public class IndexPageCache DataPageMain childMain = _dataPages.get(subPageNumber); if(childMain != null) { if(childMain._parentPageNumber != null) { - if((int)childMain._parentPageNumber != dpMain._pageNumber) { + if(childMain._parentPageNumber != dpMain._pageNumber) { throw new IllegalStateException("Child's parent is incorrect " + childMain); } @@ -1069,7 +1089,7 @@ public class IndexPageCache * @param dpMain the index page * @param peerMain the peer index page */ - private void validatePeerStatus(DataPageMain dpMain, DataPageMain peerMain) + private static void validatePeerStatus(DataPageMain dpMain, DataPageMain peerMain) throws IOException { if(dpMain._leaf != peerMain._leaf) { @@ -1107,6 +1127,24 @@ public class IndexPageCache rtn.append("Page[" + dpMain._pageNumber + "]: " + e); } } + + /** + * Trims the size of the _dataPages cache appropriately (assuming caller has + * already verified that the cache needs trimming). + */ + private void purgeOldPages() { + Iterator iter = _dataPages.values().iterator(); + while(iter.hasNext()) { + DataPageMain dpMain = iter.next(); + // note, we never purge the root page + if(dpMain != _rootPage) { + iter.remove(); + if(_dataPages.size() <= MAX_CACHE_SIZE) { + break; + } + } + } + } @Override public String toString() { @@ -1224,7 +1262,7 @@ public class IndexPageCache return extra; } - + public void setExtra(DataPageExtra extra) throws IOException { extra.setEntryView(this); @@ -1289,7 +1327,7 @@ public class IndexPageCache /** * IndexPageCache implementation of an Index {@link DataPage}. */ - public static final class CacheDataPage + private static final class CacheDataPage extends IndexData.DataPage { public final DataPageMain _main; @@ -1460,10 +1498,6 @@ public class IndexPageCache _childTailEntry = newEntry; return old; } - - public Entry getChildTailEntry() { - return _childTailEntry; - } private boolean hasChildTail() { return(_childTailEntry != null); diff --git a/src/java/com/healthmarketscience/jackcess/JetFormat.java b/src/java/com/healthmarketscience/jackcess/impl/JetFormat.java similarity index 97% rename from src/java/com/healthmarketscience/jackcess/JetFormat.java rename to src/java/com/healthmarketscience/jackcess/impl/JetFormat.java index e3a8af8..70f5fd9 100644 --- a/src/java/com/healthmarketscience/jackcess/JetFormat.java +++ b/src/java/com/healthmarketscience/jackcess/impl/JetFormat.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.io.IOException; import java.nio.ByteBuffer; @@ -35,6 +35,9 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import com.healthmarketscience.jackcess.Database; +import com.healthmarketscience.jackcess.DataType; + /** * Encapsulates constants describing a specific version of the Access Jet format * @author Tim McCune @@ -259,7 +262,7 @@ public abstract class JetFormat { public final boolean LEGACY_NUMERIC_INDEXES; public final Charset CHARSET; - public final Column.SortOrder DEFAULT_SORT_ORDER; + public final ColumnImpl.SortOrder DEFAULT_SORT_ORDER; /** * @param channel the database file. @@ -493,13 +496,13 @@ public abstract class JetFormat { protected abstract int defineMaxIndexNameLength(); protected abstract Charset defineCharset(); - protected abstract Column.SortOrder defineDefaultSortOrder(); + protected abstract ColumnImpl.SortOrder defineDefaultSortOrder(); protected abstract boolean defineLegacyNumericIndexes(); protected abstract Map getPossibleFileFormats(); - protected abstract boolean isSupportedDataType(DataType type); + public abstract boolean isSupportedDataType(DataType type); @Override public String toString() { @@ -708,8 +711,8 @@ public abstract class JetFormat { protected Charset defineCharset() { return Charset.defaultCharset(); } @Override - protected Column.SortOrder defineDefaultSortOrder() { - return Column.GENERAL_LEGACY_SORT_ORDER; + protected ColumnImpl.SortOrder defineDefaultSortOrder() { + return ColumnImpl.GENERAL_LEGACY_SORT_ORDER; } @Override @@ -719,7 +722,7 @@ public abstract class JetFormat { } @Override - protected boolean isSupportedDataType(DataType type) { + public boolean isSupportedDataType(DataType type) { return (type != DataType.COMPLEX_TYPE); } } @@ -928,8 +931,8 @@ public abstract class JetFormat { protected Charset defineCharset() { return Charset.forName("UTF-16LE"); } @Override - protected Column.SortOrder defineDefaultSortOrder() { - return Column.GENERAL_LEGACY_SORT_ORDER; + protected ColumnImpl.SortOrder defineDefaultSortOrder() { + return ColumnImpl.GENERAL_LEGACY_SORT_ORDER; } @Override @@ -939,7 +942,7 @@ public abstract class JetFormat { } @Override - protected boolean isSupportedDataType(DataType type) { + public boolean isSupportedDataType(DataType type) { return (type != DataType.COMPLEX_TYPE); } } @@ -991,7 +994,7 @@ public abstract class JetFormat { protected int defineOffsetColumnComplexId() { return 11; } @Override - protected boolean isSupportedDataType(DataType type) { + public boolean isSupportedDataType(DataType type) { return true; } } @@ -1002,8 +1005,8 @@ public abstract class JetFormat { } @Override - protected Column.SortOrder defineDefaultSortOrder() { - return Column.GENERAL_SORT_ORDER; + protected ColumnImpl.SortOrder defineDefaultSortOrder() { + return ColumnImpl.GENERAL_SORT_ORDER; } @Override diff --git a/src/java/com/healthmarketscience/jackcess/NullMask.java b/src/java/com/healthmarketscience/jackcess/impl/NullMask.java similarity index 95% rename from src/java/com/healthmarketscience/jackcess/NullMask.java rename to src/java/com/healthmarketscience/jackcess/impl/NullMask.java index 5be5218..e342155 100644 --- a/src/java/com/healthmarketscience/jackcess/NullMask.java +++ b/src/java/com/healthmarketscience/jackcess/impl/NullMask.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.nio.ByteBuffer; @@ -73,7 +73,7 @@ public class NullMask { * columns, returns the actual value of the column (where * non-{@code null} == {@code true}) */ - public boolean isNull(Column column) { + public boolean isNull(ColumnImpl column) { int columnNumber = column.getColumnNumber(); // if new columns were added to the table, old null masks may not include // them (meaning the field is null) @@ -89,7 +89,7 @@ public class NullMask { * boolean value is {@code true}). * @param column column to be marked non-{@code null} */ - public void markNotNull(Column column) { + public void markNotNull(ColumnImpl column) { int columnNumber = column.getColumnNumber(); int maskIndex = byteIndex(columnNumber); _mask[maskIndex] = (byte) (_mask[maskIndex] | bitMask(columnNumber)); diff --git a/src/java/com/healthmarketscience/jackcess/PageChannel.java b/src/java/com/healthmarketscience/jackcess/impl/PageChannel.java similarity index 85% rename from src/java/com/healthmarketscience/jackcess/PageChannel.java rename to src/java/com/healthmarketscience/jackcess/impl/PageChannel.java index 27cb0ab..89e952d 100644 --- a/src/java/com/healthmarketscience/jackcess/PageChannel.java +++ b/src/java/com/healthmarketscience/jackcess/impl/PageChannel.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.io.Flushable; import java.io.IOException; @@ -79,9 +79,23 @@ public class PageChannel implements Channel, Flushable { /** handler for the current database encoding type */ private CodecHandler _codecHandler = DefaultCodecProvider.DUMMY_HANDLER; /** temp page buffer used when pages cannot be partially encoded */ - private final TempPageHolder _fullPageEncodeBufferH = - TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); + private TempPageHolder _fullPageEncodeBufferH; + private TempBufferHolder _tempDecodeBufferH; + private int _writeCount; + /** + * Only used by unit tests + */ + protected PageChannel(boolean testing) { + if(!testing) { + throw new IllegalArgumentException(); + } + _channel = null; + _closeChannel = false; + _format = JetFormat.VERSION_4; + _autoSync = false; + } + /** * @param channel Channel containing the database * @param format Format of the database in the channel @@ -99,11 +113,19 @@ public class PageChannel implements Channel, Flushable { /** * Does second-stage initialization, must be called after construction. */ - public void initialize(Database database, CodecProvider codecProvider) + public void initialize(DatabaseImpl database, CodecProvider codecProvider) throws IOException { // initialize page en/decoding support _codecHandler = codecProvider.createHandler(this, database.getCharset()); + if(!_codecHandler.canEncodePartialPage()) { + _fullPageEncodeBufferH = + TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); + } + if(!_codecHandler.canDecodeInline()) { + _tempDecodeBufferH = TempBufferHolder.newHolder( + TempBufferHolder.Type.SOFT, true); + } // note the global usage map is a special map where any page outside of // the current range is assumed to be "on" @@ -111,19 +133,6 @@ public class PageChannel implements Channel, Flushable { ROW_GLOBAL_USAGE_MAP, true); } - /** - * Only used by unit tests - */ - PageChannel(boolean testing) { - if(!testing) { - throw new IllegalArgumentException(); - } - _channel = null; - _closeChannel = false; - _format = JetFormat.VERSION_4; - _autoSync = false; - } - public JetFormat getFormat() { return _format; } @@ -132,6 +141,45 @@ public class PageChannel implements Channel, Flushable { return _autoSync; } + /** + * Begins a "logical" write operation. See {@link #finishWrite} for more + * details. + */ + public void startWrite() { + ++_writeCount; + } + + /** + * Completes a "logical" write operation. This method should be called in + * finally block which wraps a logical write operation (which is preceded by + * a {@link #startWrite} call). Logical write operations may be nested. If + * the database is configured for "auto-sync", the channel will be flushed + * when the outermost operation is complete, + */ + public void finishWrite() throws IOException { + assertWriting(); + if((--_writeCount == 0) && _autoSync) { + flush(); + } + } + + /** + * Returns {@code true} if a logical write operation is in progress, {@code + * false} otherwise. + */ + public boolean isWriting() { + return(_writeCount > 0); + } + + /** + * Asserts that a write operation is in progress. + */ + private void assertWriting() { + if(!isWriting()) { + throw new IllegalStateException("No write operation in progress"); + } + } + /** * Returns the next page number based on the given file size. */ @@ -166,13 +214,18 @@ public class PageChannel implements Channel, Flushable { throws IOException { validatePageNumber(pageNumber); - if (LOG.isDebugEnabled()) { - LOG.debug("Reading in page " + Integer.toHexString(pageNumber)); + + ByteBuffer inPage = buffer; + ByteBuffer outPage = buffer; + if((pageNumber != 0) && !_codecHandler.canDecodeInline()) { + inPage = _tempDecodeBufferH.getPageBuffer(this); + outPage.clear(); } - buffer.clear(); + + inPage.clear(); int bytesRead = _channel.read( - buffer, (long) pageNumber * (long) getFormat().PAGE_SIZE); - buffer.flip(); + inPage, (long) pageNumber * (long) getFormat().PAGE_SIZE); + inPage.flip(); if(bytesRead != getFormat().PAGE_SIZE) { throw new IOException("Failed attempting to read " + getFormat().PAGE_SIZE + " bytes from page " + @@ -183,7 +236,7 @@ public class PageChannel implements Channel, Flushable { // de-mask header (note, page 0 never has additional encoding) applyHeaderMask(buffer); } else { - _codecHandler.decodePage(buffer, pageNumber); + _codecHandler.decodePage(inPage, outPage, pageNumber); } } @@ -206,6 +259,7 @@ public class PageChannel implements Channel, Flushable { public void writePage(ByteBuffer page, int pageNumber, int pageOffset) throws IOException { + assertWriting(); validatePageNumber(pageNumber); page.rewind().position(pageOffset); @@ -256,9 +310,6 @@ public class PageChannel implements Channel, Flushable { try { _channel.write(encodedPage, (getPageOffset(pageNumber) + pageOffset)); - if(_autoSync) { - flush(); - } } finally { if(pageNumber == 0) { // de-mask header @@ -272,6 +323,8 @@ public class PageChannel implements Channel, Flushable { * until it is written in a call to {@link #writePage(ByteBuffer,int)}. */ public int allocateNewPage() throws IOException { + assertWriting(); + // this will force the file to be extended with mostly undefined bytes long size = _channel.size(); if(size >= getFormat().MAX_DATABASE_SIZE) { @@ -306,6 +359,8 @@ public class PageChannel implements Channel, Flushable { * Deallocate a previously used page in the database. */ public void deallocatePage(int pageNumber) throws IOException { + assertWriting(); + validatePageNumber(pageNumber); // don't write the whole page, just wipe out the header (which should be @@ -365,7 +420,7 @@ public class PageChannel implements Channel, Flushable { buffer.put(pos, b); } } - + /** * @return a duplicate of the current buffer narrowed to the given position * and limit. mark will be set at the current position. @@ -387,5 +442,5 @@ public class PageChannel implements Channel, Flushable { */ public static ByteBuffer wrap(byte[] bytes) { return ByteBuffer.wrap(bytes).order(DEFAULT_BYTE_ORDER); - } +} } diff --git a/src/java/com/healthmarketscience/jackcess/PageTypes.java b/src/java/com/healthmarketscience/jackcess/impl/PageTypes.java similarity index 97% rename from src/java/com/healthmarketscience/jackcess/PageTypes.java rename to src/java/com/healthmarketscience/jackcess/impl/PageTypes.java index 91eab9d..0f7a084 100644 --- a/src/java/com/healthmarketscience/jackcess/PageTypes.java +++ b/src/java/com/healthmarketscience/jackcess/impl/PageTypes.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; /** * Codes for page types diff --git a/src/java/com/healthmarketscience/jackcess/impl/PropertyMapImpl.java b/src/java/com/healthmarketscience/jackcess/impl/PropertyMapImpl.java new file mode 100644 index 0000000..e267c9b --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/PropertyMapImpl.java @@ -0,0 +1,146 @@ +/* +Copyright (c) 2011 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.impl; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.PropertyMap; + +/** + * Map of properties for a database object. + * + * @author James Ahlborn + */ +public class PropertyMapImpl implements PropertyMap +{ + private final String _mapName; + private final short _mapType; + private final Map _props = + new LinkedHashMap(); + + public PropertyMapImpl(String name, short type) { + _mapName = name; + _mapType = type; + } + + public String getName() { + return _mapName; + } + + public short getType() { + return _mapType; + } + + public int getSize() { + return _props.size(); + } + + public boolean isEmpty() { + return _props.isEmpty(); + } + + public Property get(String name) { + return _props.get(DatabaseImpl.toLookupName(name)); + } + + public Object getValue(String name) { + return getValue(name, null); + } + + public Object getValue(String name, Object defaultValue) { + Property prop = get(name); + Object value = defaultValue; + if((prop != null) && (prop.getValue() != null)) { + value = prop.getValue(); + } + return value; + } + + /** + * Puts a property into this map with the given information. + */ + public void put(String name, DataType type, byte flag, Object value) { + _props.put(DatabaseImpl.toLookupName(name), + new PropertyImpl(name, type, flag, value)); + } + + public Iterator iterator() { + return _props.values().iterator(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(PropertyMaps.DEFAULT_NAME.equals(getName()) ? + "" : getName()) + .append(" {"); + for(Iterator iter = iterator(); iter.hasNext(); ) { + sb.append(iter.next()); + if(iter.hasNext()) { + sb.append(","); + } + } + sb.append("}"); + return sb.toString(); + } + + /** + * Info about a property defined in a PropertyMap. + */ + private static final class PropertyImpl implements PropertyMap.Property + { + private final String _name; + private final DataType _type; + private final byte _flag; + private final Object _value; + + private PropertyImpl(String name, DataType type, byte flag, Object value) { + _name = name; + _type = type; + _flag = flag; + _value = value; + } + + public String getName() { + return _name; + } + + public DataType getType() { + return _type; + } + + public Object getValue() { + return _value; + } + + @Override + public String toString() { + Object val = getValue(); + if(val instanceof byte[]) { + val = ByteUtil.toHexString((byte[])val); + } + return getName() + "[" + getType() + ":" + _flag + "]=" + val; + } + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/PropertyMaps.java b/src/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java similarity index 84% rename from src/java/com/healthmarketscience/jackcess/PropertyMaps.java rename to src/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java index 51853ee..41468aa 100644 --- a/src/java/com/healthmarketscience/jackcess/PropertyMaps.java +++ b/src/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java @@ -17,7 +17,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.io.IOException; import java.nio.ByteBuffer; @@ -28,12 +28,15 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import com.healthmarketscience.jackcess.PropertyMap; +import com.healthmarketscience.jackcess.DataType; + /** * Collection of PropertyMap instances read from a single property data block. * * @author James Ahlborn */ -public class PropertyMaps implements Iterable +public class PropertyMaps implements Iterable { /** the name of the "default" properties for a PropertyMaps instance */ public static final String DEFAULT_NAME = ""; @@ -44,8 +47,8 @@ public class PropertyMaps implements Iterable /** maps the PropertyMap name (case-insensitive) to the PropertyMap instance */ - private final Map _maps = - new LinkedHashMap(); + private final Map _maps = + new LinkedHashMap(); private final int _objectId; public PropertyMaps(int objectId) { @@ -68,7 +71,7 @@ public class PropertyMaps implements Iterable * @return the unnamed "default" PropertyMap in this group, creating if * necessary. */ - public PropertyMap getDefault() { + public PropertyMapImpl getDefault() { return get(DEFAULT_NAME, DEFAULT_PROPERTY_VALUE_LIST); } @@ -76,7 +79,7 @@ public class PropertyMaps implements Iterable * @return the PropertyMap with the given name in this group, creating if * necessary */ - public PropertyMap get(String name) { + public PropertyMapImpl get(String name) { return get(name, COLUMN_PROPERTY_VALUE_LIST); } @@ -84,11 +87,11 @@ public class PropertyMaps implements Iterable * @return the PropertyMap with the given name and type in this group, * creating if necessary */ - private PropertyMap get(String name, short type) { - String lookupName = Database.toLookupName(name); - PropertyMap map = _maps.get(lookupName); + private PropertyMapImpl get(String name, short type) { + String lookupName = DatabaseImpl.toLookupName(name); + PropertyMapImpl map = _maps.get(lookupName); if(map == null) { - map = new PropertyMap(name, type); + map = new PropertyMapImpl(name, type); _maps.put(lookupName, map); } return map; @@ -97,18 +100,18 @@ public class PropertyMaps implements Iterable /** * Adds the given PropertyMap to this group. */ - public void put(PropertyMap map) { - _maps.put(Database.toLookupName(map.getName()), map); + public void put(PropertyMapImpl map) { + _maps.put(DatabaseImpl.toLookupName(map.getName()), map); } - public Iterator iterator() { + public Iterator iterator() { return _maps.values().iterator(); } @Override public String toString() { StringBuilder sb = new StringBuilder(); - for(Iterator iter = iterator(); iter.hasNext(); ) { + for(Iterator iter = iterator(); iter.hasNext(); ) { sb.append(iter.next()); if(iter.hasNext()) { sb.append("\n"); @@ -123,12 +126,12 @@ public class PropertyMaps implements Iterable static final class Handler { /** the current database */ - private final Database _database; + private final DatabaseImpl _database; /** cache of PropColumns used to read/write property values */ private final Map _columns = new HashMap(); - Handler(Database database) { + Handler(DatabaseImpl database) { _database = database; } @@ -204,7 +207,7 @@ public class PropertyMaps implements Iterable * @return the PropertyMap created from the values parsed from the given * data chunk combined with the given property names */ - private PropertyMap readPropertyValues( + private PropertyMapImpl readPropertyValues( ByteBuffer bbBlock, List propNames, short blockType) throws IOException { @@ -221,7 +224,7 @@ public class PropertyMaps implements Iterable bbBlock.position(endPos); } - PropertyMap map = new PropertyMap(mapName, blockType); + PropertyMapImpl map = new PropertyMapImpl(mapName, blockType); // read the values while(bbBlock.hasRemaining()) { @@ -253,7 +256,7 @@ public class PropertyMaps implements Iterable private String readPropName(ByteBuffer buffer) { int nameLength = buffer.getShort(); byte[] nameBytes = ByteUtil.getBytes(buffer, nameLength); - return Column.decodeUncompressedText(nameBytes, _database.getCharset()); + return ColumnImpl.decodeUncompressedText(nameBytes, _database.getCharset()); } /** @@ -281,18 +284,14 @@ public class PropertyMaps implements Iterable // create column with ability to read/write the given data type col = ((colType == DataType.BOOLEAN) ? - new BooleanPropColumn() : new PropColumn()); - col.setType(colType); - if(col.isVariableLength()) { - col.setLength((short)colType.getMaxSize()); - } + new BooleanPropColumn() : new PropColumn(colType)); } return col; } - private boolean isPseudoGuidColumn(DataType dataType, String propName, - int dataSize) { + private static boolean isPseudoGuidColumn( + DataType dataType, String propName, int dataSize) { // guids seem to be marked as "binary" fields return((dataType == DataType.BINARY) && (dataSize == DataType.GUID.getFixedSize()) && @@ -302,10 +301,14 @@ public class PropertyMaps implements Iterable /** * Column adapted to work w/out a Table. */ - private class PropColumn extends Column + private class PropColumn extends ColumnImpl { + private PropColumn(DataType type) { + super(null, type, 0, 0, 0); + } + @Override - public Database getDatabase() { + public DatabaseImpl getDatabase() { return _database; } } @@ -316,6 +319,10 @@ public class PropertyMaps implements Iterable */ private final class BooleanPropColumn extends PropColumn { + private BooleanPropColumn() { + super(DataType.BOOLEAN); + } + @Override public Object read(byte[] data) throws IOException { return ((data[0] != 0) ? Boolean.TRUE : Boolean.FALSE); diff --git a/src/java/com/healthmarketscience/jackcess/impl/RelationshipImpl.java b/src/java/com/healthmarketscience/jackcess/impl/RelationshipImpl.java new file mode 100644 index 0000000..8424610 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/RelationshipImpl.java @@ -0,0 +1,152 @@ +/* +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.impl; + +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; + +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.Relationship; +import com.healthmarketscience.jackcess.Table; + +/** + * Information about a relationship between two tables in the database. + * + * @author James Ahlborn + */ +public class RelationshipImpl implements Relationship +{ + + /** flag indicating one-to-one relationship */ + private static final int ONE_TO_ONE_FLAG = 0x00000001; + /** flag indicating no referential integrity */ + private static final int NO_REFERENTIAL_INTEGRITY_FLAG = 0x00000002; + /** flag indicating cascading updates (requires referential integrity) */ + private static final int CASCADE_UPDATES_FLAG = 0x00000100; + /** flag indicating cascading deletes (requires referential integrity) */ + private static final int CASCADE_DELETES_FLAG = 0x00001000; + /** flag indicating left outer join */ + private static final int LEFT_OUTER_JOIN_FLAG = 0x01000000; + /** flag indicating right outer join */ + private static final int RIGHT_OUTER_JOIN_FLAG = 0x02000000; + + /** the name of this relationship */ + private final String _name; + /** the "from" table in this relationship */ + private final Table _fromTable; + /** the "to" table in this relationship */ + private final Table _toTable; + /** the columns in the "from" table in this relationship (aligned w/ + toColumns list) */ + private final List _toColumns; + /** the columns in the "to" table in this relationship (aligned w/ + toColumns list) */ + private final List _fromColumns; + /** the various flags describing this relationship */ + private final int _flags; + + public RelationshipImpl(String name, Table fromTable, Table toTable, int flags, + int numCols) + { + _name = name; + _fromTable = fromTable; + _fromColumns = new ArrayList( + Collections.nCopies(numCols, (Column)null)); + _toTable = toTable; + _toColumns = new ArrayList( + Collections.nCopies(numCols, (Column)null)); + _flags = flags; + } + + public String getName() { + return _name; + } + + public Table getFromTable() { + return _fromTable; + } + + public List getFromColumns() { + return _fromColumns; + } + + public Table getToTable() { + return _toTable; + } + + public List getToColumns() { + return _toColumns; + } + + public int getFlags() { + return _flags; + } + + public boolean isOneToOne() { + return hasFlag(ONE_TO_ONE_FLAG); + } + + public boolean hasReferentialIntegrity() { + return !hasFlag(NO_REFERENTIAL_INTEGRITY_FLAG); + } + + public boolean cascadeUpdates() { + return hasFlag(CASCADE_UPDATES_FLAG); + } + + public boolean cascadeDeletes() { + return hasFlag(CASCADE_DELETES_FLAG); + } + + public boolean isLeftOuterJoin() { + return hasFlag(LEFT_OUTER_JOIN_FLAG); + } + + public boolean isRightOuterJoin() { + return hasFlag(RIGHT_OUTER_JOIN_FLAG); + } + + private boolean hasFlag(int flagMask) { + return((getFlags() & flagMask) != 0); + } + + @Override + public String toString() { + StringBuilder rtn = new StringBuilder(); + rtn.append("\tName: " + _name); + rtn.append("\n\tFromTable: " + _fromTable.getName()); + rtn.append("\n\tFromColumns: " + _fromColumns); + rtn.append("\n\tToTable: " + _toTable.getName()); + rtn.append("\n\tToColumns: " + _toColumns); + rtn.append("\n\tFlags: " + Integer.toHexString(_flags)); + rtn.append("\n\n"); + return rtn.toString(); + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/RowIdImpl.java b/src/java/com/healthmarketscience/jackcess/impl/RowIdImpl.java new file mode 100644 index 0000000..7524f1c --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/RowIdImpl.java @@ -0,0 +1,138 @@ +/* +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.impl; + +import org.apache.commons.lang.builder.CompareToBuilder; +import com.healthmarketscience.jackcess.RowId; + + +/** + * Uniquely identifies a row of data within the access database. + * + * @author James Ahlborn + */ +public class RowIdImpl implements RowId +{ + /** special page number which will sort before any other valid page + number */ + public static final int FIRST_PAGE_NUMBER = -1; + /** special page number which will sort after any other valid page + number */ + public static final int LAST_PAGE_NUMBER = -2; + + /** special row number representing an invalid row number */ + public static final int INVALID_ROW_NUMBER = -1; + + /** type attributes for RowIds which simplify comparisons */ + public enum Type { + /** comparable type indicating this RowId should always compare less than + normal RowIds */ + ALWAYS_FIRST, + /** comparable type indicating this RowId should always compare + normally */ + NORMAL, + /** comparable type indicating this RowId should always compare greater + than normal RowIds */ + ALWAYS_LAST; + } + + /** special rowId which will sort before any other valid rowId */ + public static final RowIdImpl FIRST_ROW_ID = new RowIdImpl( + FIRST_PAGE_NUMBER, INVALID_ROW_NUMBER); + + /** special rowId which will sort after any other valid rowId */ + public static final RowIdImpl LAST_ROW_ID = new RowIdImpl( + LAST_PAGE_NUMBER, INVALID_ROW_NUMBER); + + private final int _pageNumber; + private final int _rowNumber; + private final Type _type; + + /** + * Creates a new RowId instance. + * + */ + public RowIdImpl(int pageNumber,int rowNumber) { + _pageNumber = pageNumber; + _rowNumber = rowNumber; + _type = ((_pageNumber == FIRST_PAGE_NUMBER) ? Type.ALWAYS_FIRST : + ((_pageNumber == LAST_PAGE_NUMBER) ? Type.ALWAYS_LAST : + Type.NORMAL)); + } + + public int getPageNumber() { + return _pageNumber; + } + + public int getRowNumber() { + return _rowNumber; + } + + /** + * Returns {@code true} if this rowId potentially represents an actual row + * of data, {@code false} otherwise. + */ + public boolean isValid() { + return((getRowNumber() >= 0) && (getPageNumber() >= 0)); + } + + public Type getType() { + return _type; + } + + public int compareTo(RowId other) { + return compareTo((RowIdImpl)other); + } + + public int compareTo(RowIdImpl other) { + return new CompareToBuilder() + .append(getType(), other.getType()) + .append(getPageNumber(), other.getPageNumber()) + .append(getRowNumber(), other.getRowNumber()) + .toComparison(); + } + + @Override + public int hashCode() { + return getPageNumber() ^ getRowNumber(); + } + + @Override + public boolean equals(Object o) { + return ((this == o) || + ((o != null) && (getClass() == o.getClass()) && + (getPageNumber() == ((RowIdImpl)o).getPageNumber()) && + (getRowNumber() == ((RowIdImpl)o).getRowNumber()))); + } + + @Override + public String toString() { + return getPageNumber() + ":" + getRowNumber(); + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/RowImpl.java b/src/java/com/healthmarketscience/jackcess/impl/RowImpl.java new file mode 100644 index 0000000..898508a --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/RowImpl.java @@ -0,0 +1,65 @@ +/* +Copyright (c) 2013 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.impl; + +import java.util.LinkedHashMap; + +import com.healthmarketscience.jackcess.Row; + +/** + * A row of data as column->value pairs. + *

+ * Note that the {@link #equals} and {@link #hashCode} methods work on the row + * contents only (i.e. they ignore the id). + * + * @author James Ahlborn + */ +public class RowImpl extends LinkedHashMap implements Row +{ + private static final long serialVersionUID = 20130314L; + + private final RowIdImpl _id; + + public RowImpl(RowIdImpl id) + { + _id = id; + } + + public RowImpl(RowIdImpl id, int expectedSize) + { + super(expectedSize); + _id = id; + } + + public RowImpl(Row row) + { + super(row); + _id = (RowIdImpl)row.getId(); + } + + public RowIdImpl getId() { + return _id; + } + + @Override + public String toString() { + return "Row[" + _id + "] " + super.toString(); + } +} diff --git a/src/java/com/healthmarketscience/jackcess/TableCreator.java b/src/java/com/healthmarketscience/jackcess/impl/TableCreator.java similarity index 82% rename from src/java/com/healthmarketscience/jackcess/TableCreator.java rename to src/java/com/healthmarketscience/jackcess/impl/TableCreator.java index 9f7911d..8828ac2 100644 --- a/src/java/com/healthmarketscience/jackcess/TableCreator.java +++ b/src/java/com/healthmarketscience/jackcess/impl/TableCreator.java @@ -17,7 +17,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.io.IOException; import java.nio.charset.Charset; @@ -30,6 +30,10 @@ import java.util.List; import java.util.Map; import java.util.Set; +import com.healthmarketscience.jackcess.ColumnBuilder; +import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.IndexBuilder; + /** * Helper class used to maintain state during table creation. * @@ -38,21 +42,21 @@ import java.util.Set; */ class TableCreator { - private final Database _database; + private final DatabaseImpl _database; private final String _name; - private final List _columns; + private final List _columns; private final List _indexes; private final Map _indexStates = new IdentityHashMap(); - private final Map _columnStates = - new IdentityHashMap(); - private final List _lvalCols = new ArrayList(); + private final Map _columnStates = + new IdentityHashMap(); + private final List _lvalCols = new ArrayList(); private int _tdefPageNumber = PageChannel.INVALID_PAGE_NUMBER; private int _umapPageNumber = PageChannel.INVALID_PAGE_NUMBER; private int _indexCount; private int _logicalIndexCount; - public TableCreator(Database database, String name, List columns, + public TableCreator(DatabaseImpl database, String name, List columns, List indexes) { _database = database; _name = name; @@ -81,7 +85,7 @@ class TableCreator return _umapPageNumber; } - public List getColumns() { + public List getColumns() { return _columns; } @@ -109,11 +113,11 @@ class TableCreator return getPageChannel().allocateNewPage(); } - public ColumnState getColumnState(Column col) { + public ColumnState getColumnState(ColumnBuilder col) { return _columnStates.get(col); } - public List getLongValueColumns() { + public List getLongValueColumns() { return _lvalCols; } @@ -127,7 +131,7 @@ class TableCreator // assign column numbers and do some assorted column bookkeeping short columnNumber = (short) 0; - for(Column col : _columns) { + for(ColumnBuilder col : _columns) { col.setColumnNumber(columnNumber++); if(col.getType().isLongValue()) { _lvalCols.add(col); @@ -147,15 +151,22 @@ class TableCreator } } - // reserve some pages - _tdefPageNumber = reservePageNumber(); - _umapPageNumber = reservePageNumber(); + getPageChannel().startWrite(); + try { + + // reserve some pages + _tdefPageNumber = reservePageNumber(); + _umapPageNumber = reservePageNumber(); - //Write the tdef page to disk. - Table.writeTableDefinition(this); + //Write the tdef page to disk. + TableImpl.writeTableDefinition(this); - // update the database with the new table info - _database.addNewTable(_name, _tdefPageNumber, Database.TYPE_TABLE, null, null); + // update the database with the new table info + _database.addNewTable(_name, _tdefPageNumber, DatabaseImpl.TYPE_TABLE, null, null); + + } finally { + getPageChannel().finishWrite(); + } } /** @@ -163,7 +174,7 @@ class TableCreator */ private void validate() { - Database.validateIdentifierName( + DatabaseImpl.validateIdentifierName( _name, getFormat().MAX_TABLE_NAME_LENGTH, "table"); if((_columns == null) || _columns.isEmpty()) { @@ -176,7 +187,7 @@ class TableCreator getFormat().MAX_COLUMNS_PER_TABLE + " columns"); } - Column.SortOrder dbSortOrder = null; + ColumnImpl.SortOrder dbSortOrder = null; try { dbSortOrder = _database.getDefaultSortOrder(); } catch(IOException e) { @@ -185,7 +196,7 @@ class TableCreator Set colNames = new HashSet(); // next, validate the column definitions - for(Column column : _columns) { + for(ColumnBuilder column : _columns) { // FIXME for now, we can't create complex columns if(column.getType() == DataType.COMPLEX_TYPE) { @@ -205,11 +216,11 @@ class TableCreator } } - List autoCols = Table.getAutoNumberColumns(_columns); + List autoCols = getAutoNumberColumns(); if(autoCols.size() > 1) { // for most autonumber types, we can only have one of each type Set autoTypes = EnumSet.noneOf(DataType.class); - for(Column c : autoCols) { + for(ColumnBuilder c : autoCols) { if(!c.getType().isMultipleAutoNumberAllowed() && !autoTypes.add(c.getType())) { throw new IllegalArgumentException( @@ -240,6 +251,17 @@ class TableCreator } } + private List getAutoNumberColumns() + { + List autoCols = new ArrayList(1); + for(ColumnBuilder c : _columns) { + if(c.isAutoNumber()) { + autoCols.add(c); + } + } + return autoCols; + } + /** * Maintains additional state used during index creation. * @usage _advanced_class_ @@ -290,9 +312,9 @@ class TableCreator public void setRootPageNumber(int newRootPageNumber) { _rootPageNumber = newRootPageNumber; - } + } } - + /** * Maintains additional state used during column creation. * @usage _advanced_class_ @@ -306,11 +328,11 @@ class TableCreator public byte getUmapOwnedRowNumber() { return _umapOwnedRowNumber; - } + } public void setUmapOwnedRowNumber(byte newUmapOwnedRowNumber) { _umapOwnedRowNumber = newUmapOwnedRowNumber; - } +} public byte getUmapFreeRowNumber() { return _umapFreeRowNumber; diff --git a/src/java/com/healthmarketscience/jackcess/impl/TableImpl.java b/src/java/com/healthmarketscience/jackcess/impl/TableImpl.java new file mode 100644 index 0000000..a42b7c2 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/TableImpl.java @@ -0,0 +1,2589 @@ +/* +Copyright (c) 2005 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.impl; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.ColumnBuilder; +import com.healthmarketscience.jackcess.CursorBuilder; +import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.Index; +import com.healthmarketscience.jackcess.IndexBuilder; +import com.healthmarketscience.jackcess.PropertyMap; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.RowId; +import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.util.ErrorHandler; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A single database table + *

+ * Is not thread-safe. + * + * @author Tim McCune + * @usage _general_class_ + */ +public class TableImpl implements Table +{ + private static final Log LOG = LogFactory.getLog(TableImpl.class); + + private static final short OFFSET_MASK = (short)0x1FFF; + + private static final short DELETED_ROW_MASK = (short)0x8000; + + private static final short OVERFLOW_ROW_MASK = (short)0x4000; + + static final int MAGIC_TABLE_NUMBER = 1625; + + private static final int MAX_BYTE = 256; + + /** + * Table type code for system tables + * @usage _intermediate_class_ + */ + public static final byte TYPE_SYSTEM = 0x53; + /** + * Table type code for user tables + * @usage _intermediate_class_ + */ + public static final byte TYPE_USER = 0x4e; + + /** comparator which sorts variable length columns based on their index into + the variable length offset table */ + private static final Comparator VAR_LEN_COLUMN_COMPARATOR = + new Comparator() { + public int compare(ColumnImpl c1, ColumnImpl c2) { + return ((c1.getVarLenTableIndex() < c2.getVarLenTableIndex()) ? -1 : + ((c1.getVarLenTableIndex() > c2.getVarLenTableIndex()) ? 1 : + 0)); + } + }; + + /** comparator which sorts columns based on their display index */ + private static final Comparator DISPLAY_ORDER_COMPARATOR = + new Comparator() { + public int compare(ColumnImpl c1, ColumnImpl c2) { + return ((c1.getDisplayIndex() < c2.getDisplayIndex()) ? -1 : + ((c1.getDisplayIndex() > c2.getDisplayIndex()) ? 1 : + 0)); + } + }; + + /** owning database */ + private final DatabaseImpl _database; + /** additional table flags from the catalog entry */ + private final int _flags; + /** Type of the table (either TYPE_SYSTEM or TYPE_USER) */ + private final byte _tableType; + /** Number of actual indexes on the table */ + private final int _indexCount; + /** Number of logical indexes for the table */ + private final int _logicalIndexCount; + /** page number of the definition of this table */ + private final int _tableDefPageNumber; + /** max Number of columns in the table (includes previous deletions) */ + private final short _maxColumnCount; + /** max Number of variable columns in the table */ + private final short _maxVarColumnCount; + /** List of columns in this table, ordered by column number */ + private final List _columns = new ArrayList(); + /** List of variable length columns in this table, ordered by offset */ + private final List _varColumns = new ArrayList(); + /** List of autonumber columns in this table, ordered by column number */ + private final List _autoNumColumns = new ArrayList(1); + /** List of indexes on this table (multiple logical indexes may be backed by + the same index data) */ + private final List _indexes = new ArrayList(); + /** List of index datas on this table (the actual backing data for an + index) */ + private final List _indexDatas = new ArrayList(); + /** List of columns in this table which are in one or more indexes */ + private final Set _indexColumns = new LinkedHashSet(); + /** Table name as stored in Database */ + private final String _name; + /** Usage map of pages that this table owns */ + private final UsageMap _ownedPages; + /** Usage map of pages that this table owns with free space on them */ + private final UsageMap _freeSpacePages; + /** Number of rows in the table */ + private int _rowCount; + /** last long auto number for the table */ + private int _lastLongAutoNumber; + /** last complex type auto number for the table */ + private int _lastComplexTypeAutoNumber; + /** modification count for the table, keeps row-states up-to-date */ + private int _modCount; + /** page buffer used to update data pages when adding rows */ + private final TempPageHolder _addRowBufferH = + TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); + /** page buffer used to update the table def page */ + private final TempPageHolder _tableDefBufferH = + TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); + /** buffer used to writing single rows of data */ + private final TempBufferHolder _singleRowBufferH = + TempBufferHolder.newHolder(TempBufferHolder.Type.SOFT, true); + /** "buffer" used to writing multi rows of data (will create new buffer on + every call) */ + private final TempBufferHolder _multiRowBufferH = + TempBufferHolder.newHolder(TempBufferHolder.Type.NONE, true); + /** page buffer used to write out-of-row "long value" data */ + private final TempPageHolder _longValueBufferH = + TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); + /** optional error handler to use when row errors are encountered */ + private ErrorHandler _tableErrorHandler; + /** properties for this table */ + private PropertyMap _props; + /** properties group for this table (and columns) */ + private PropertyMaps _propertyMaps; + /** foreign-key enforcer for this table */ + private final FKEnforcer _fkEnforcer; + + /** default cursor for iterating through the table, kept here for basic + table traversal */ + private CursorImpl _defaultCursor; + + /** + * Only used by unit tests + * @usage _advanced_method_ + */ + protected TableImpl(boolean testing, List columns) + throws IOException + { + if(!testing) { + throw new IllegalArgumentException(); + } + _database = null; + _tableDefPageNumber = PageChannel.INVALID_PAGE_NUMBER; + _name = null; + + _columns.addAll(columns); + for(ColumnImpl col : _columns) { + if(col.getType().isVariableLength()) { + _varColumns.add(col); + } + } + _maxColumnCount = (short)_columns.size(); + _maxVarColumnCount = (short)_varColumns.size(); + getAutoNumberColumns(); + + _fkEnforcer = null; + _flags = 0; + _tableType = TYPE_USER; + _indexCount = 0; + _logicalIndexCount = 0; + _ownedPages = null; + _freeSpacePages = null; + } + + /** + * @param database database which owns this table + * @param tableBuffer Buffer to read the table with + * @param pageNumber Page number of the table definition + * @param name Table name + */ + protected TableImpl(DatabaseImpl database, ByteBuffer tableBuffer, + int pageNumber, String name, int flags) + throws IOException + { + _database = database; + _tableDefPageNumber = pageNumber; + _name = name; + _flags = flags; + + // read table definition + tableBuffer = loadCompleteTableDefinitionBuffer(tableBuffer); + _rowCount = tableBuffer.getInt(getFormat().OFFSET_NUM_ROWS); + _lastLongAutoNumber = tableBuffer.getInt(getFormat().OFFSET_NEXT_AUTO_NUMBER); + if(getFormat().OFFSET_NEXT_COMPLEX_AUTO_NUMBER >= 0) { + _lastComplexTypeAutoNumber = tableBuffer.getInt( + getFormat().OFFSET_NEXT_COMPLEX_AUTO_NUMBER); + } + _tableType = tableBuffer.get(getFormat().OFFSET_TABLE_TYPE); + _maxColumnCount = tableBuffer.getShort(getFormat().OFFSET_MAX_COLS); + _maxVarColumnCount = tableBuffer.getShort(getFormat().OFFSET_NUM_VAR_COLS); + short columnCount = tableBuffer.getShort(getFormat().OFFSET_NUM_COLS); + _logicalIndexCount = tableBuffer.getInt(getFormat().OFFSET_NUM_INDEX_SLOTS); + _indexCount = tableBuffer.getInt(getFormat().OFFSET_NUM_INDEXES); + + tableBuffer.position(getFormat().OFFSET_OWNED_PAGES); + _ownedPages = UsageMap.read(getDatabase(), tableBuffer, false); + tableBuffer.position(getFormat().OFFSET_FREE_SPACE_PAGES); + _freeSpacePages = UsageMap.read(getDatabase(), tableBuffer, false); + + for (int i = 0; i < _indexCount; i++) { + _indexDatas.add(IndexData.create(this, tableBuffer, i, getFormat())); + } + + readColumnDefinitions(tableBuffer, columnCount); + + readIndexDefinitions(tableBuffer); + + // read column usage map info + while(tableBuffer.remaining() >= 2) { + + short umapColNum = tableBuffer.getShort(); + if(umapColNum == IndexData.COLUMN_UNUSED) { + break; + } + + UsageMap colOwnedPages = UsageMap.read( + getDatabase(), tableBuffer, false); + UsageMap colFreeSpacePages = UsageMap.read( + getDatabase(), tableBuffer, false); + + for(ColumnImpl col : _columns) { + if(col.getColumnNumber() == umapColNum) { + col.setUsageMaps(colOwnedPages, colFreeSpacePages); + break; + } + } + } + + // re-sort columns if necessary + if(getDatabase().getColumnOrder() != ColumnOrder.DATA) { + Collections.sort(_columns, DISPLAY_ORDER_COMPARATOR); + } + + for(ColumnImpl col : _columns) { + // some columns need to do extra work after the table is completely + // loaded + col.postTableLoadInit(); + } + + _fkEnforcer = new FKEnforcer(this); + } + + public String getName() { + return _name; + } + + public boolean isHidden() { + return((_flags & DatabaseImpl.HIDDEN_OBJECT_FLAG) != 0); + } + + /** + * @usage _advanced_method_ + */ + public int getMaxColumnCount() { + return _maxColumnCount; + } + + public int getColumnCount() { + return _columns.size(); + } + + public DatabaseImpl getDatabase() { + return _database; + } + + /** + * @usage _advanced_method_ + */ + public JetFormat getFormat() { + return getDatabase().getFormat(); + } + + /** + * @usage _advanced_method_ + */ + public PageChannel getPageChannel() { + return getDatabase().getPageChannel(); + } + + public ErrorHandler getErrorHandler() { + return((_tableErrorHandler != null) ? _tableErrorHandler : + getDatabase().getErrorHandler()); + } + + public void setErrorHandler(ErrorHandler newErrorHandler) { + _tableErrorHandler = newErrorHandler; + } + + public int getTableDefPageNumber() { + return _tableDefPageNumber; + } + + /** + * @usage _advanced_method_ + */ + public RowState createRowState() { + return new RowState(TempBufferHolder.Type.HARD); + } + + /** + * @usage _advanced_method_ + */ + public UsageMap.PageCursor getOwnedPagesCursor() { + return _ownedPages.cursor(); + } + + /** + * Returns the approximate number of database pages owned by this + * table and all related indexes (this number does not take into + * account pages used for large OLE/MEMO fields). + *

+ * To calculate the approximate number of bytes owned by a table: + * + * int approxTableBytes = (table.getApproximateOwnedPageCount() * + * table.getFormat().PAGE_SIZE); + * + * @usage _intermediate_method_ + */ + public int getApproximateOwnedPageCount() { + + // add a page for the table def (although that might actually be more than + // one page) + int count = _ownedPages.getPageCount() + 1; + + for(ColumnImpl col : _columns) { + count += col.getOwnedPageCount(); + } + + // note, we count owned pages from _physical_ indexes, not logical indexes + // (otherwise we could double count pages) + for(IndexData indexData : _indexDatas) { + count += indexData.getOwnedPageCount(); + } + + return count; + } + + protected TempPageHolder getLongValueBuffer() { + return _longValueBufferH; + } + + public List getColumns() { + return Collections.unmodifiableList(_columns); + } + + public ColumnImpl getColumn(String name) { + for(ColumnImpl column : _columns) { + if(column.getName().equalsIgnoreCase(name)) { + return column; + } + } + throw new IllegalArgumentException("Column with name " + name + + " does not exist in this table"); + } + + public boolean hasColumn(String name) { + for(ColumnImpl column : _columns) { + if(column.getName().equalsIgnoreCase(name)) { + return true; + } + } + return false; + } + + public PropertyMap getProperties() throws IOException { + if(_props == null) { + _props = getPropertyMaps().getDefault(); + } + return _props; + } + + /** + * @return all PropertyMaps for this table (and columns) + * @usage _advanced_method_ + */ + public PropertyMaps getPropertyMaps() throws IOException { + if(_propertyMaps == null) { + _propertyMaps = getDatabase().getPropertiesForObject( + _tableDefPageNumber); + } + return _propertyMaps; + } + + public List getIndexes() { + return Collections.unmodifiableList(_indexes); + } + + public IndexImpl getIndex(String name) { + for(IndexImpl index : _indexes) { + if(index.getName().equalsIgnoreCase(name)) { + return index; + } + } + throw new IllegalArgumentException("Index with name " + name + + " does not exist on this table"); + } + + public IndexImpl getPrimaryKeyIndex() { + for(IndexImpl index : _indexes) { + if(index.isPrimaryKey()) { + return index; + } + } + throw new IllegalArgumentException("Table " + getName() + + " does not have a primary key index"); + } + + public IndexImpl getForeignKeyIndex(Table otherTable) { + for(IndexImpl index : _indexes) { + if(index.isForeignKey() && (index.getReference() != null) && + (index.getReference().getOtherTablePageNumber() == + ((TableImpl)otherTable).getTableDefPageNumber())) { + return index; + } + } + throw new IllegalArgumentException( + "Table " + getName() + " does not have a foreign key reference to " + + otherTable.getName()); + } + + /** + * @return All of the IndexData on this table (unmodifiable List) + * @usage _advanced_method_ + */ + public List getIndexDatas() { + return Collections.unmodifiableList(_indexDatas); + } + + /** + * Only called by unit tests + * @usage _advanced_method_ + */ + public int getLogicalIndexCount() { + return _logicalIndexCount; + } + + public CursorImpl getDefaultCursor() { + if(_defaultCursor == null) { + _defaultCursor = CursorImpl.createCursor(this); + } + return _defaultCursor; + } + + public CursorBuilder newCursor() { + return new CursorBuilder(this); + } + + public void reset() { + getDefaultCursor().reset(); + } + + public Row deleteRow(Row row) throws IOException { + deleteRow(row.getId()); + return row; + } + + /** + * Delete the row with the given id. Provided RowId must have previously + * been returned from this Table. + * @return the given rowId + * @throws IllegalStateException if the given row is not valid + * @usage _intermediate_method_ + */ + public RowId deleteRow(RowId rowId) throws IOException { + deleteRow(getDefaultCursor().getRowState(), (RowIdImpl)rowId); + return rowId; + } + + /** + * Delete the row for the given rowId. + * @usage _advanced_method_ + */ + public void deleteRow(RowState rowState, RowIdImpl rowId) + throws IOException + { + requireValidRowId(rowId); + + getPageChannel().startWrite(); + try { + + // ensure that the relevant row state is up-to-date + ByteBuffer rowBuffer = positionAtRowHeader(rowState, rowId); + + if(rowState.isDeleted()) { + // don't care about duplicate deletion + return; + } + requireNonDeletedRow(rowState, rowId); + + // delete flag always gets set in the "header" row (even if data is on + // overflow row) + int pageNumber = rowState.getHeaderRowId().getPageNumber(); + int rowNumber = rowState.getHeaderRowId().getRowNumber(); + + // attempt to fill in index column values + Object[] rowValues = null; + if(!_indexDatas.isEmpty()) { + + // move to row data to get index values + rowBuffer = positionAtRowData(rowState, rowId); + + for(ColumnImpl idxCol : _indexColumns) { + getRowColumn(getFormat(), rowBuffer, idxCol, rowState, null); + } + + // use any read rowValues to help update the indexes + rowValues = rowState.getRowValues(); + + // check foreign keys before proceeding w/ deletion + _fkEnforcer.deleteRow(rowValues); + + // move back to the header + rowBuffer = positionAtRowHeader(rowState, rowId); + } + + // finally, pull the trigger + int rowIndex = getRowStartOffset(rowNumber, getFormat()); + rowBuffer.putShort(rowIndex, (short)(rowBuffer.getShort(rowIndex) + | DELETED_ROW_MASK | OVERFLOW_ROW_MASK)); + writeDataPage(rowBuffer, pageNumber); + + // update the indexes + for(IndexData indexData : _indexDatas) { + indexData.deleteRow(rowValues, rowId); + } + + // make sure table def gets updated + updateTableDefinition(-1); + + } finally { + getPageChannel().finishWrite(); + } + } + + public Row getNextRow() throws IOException { + return getDefaultCursor().getNextRow(); + } + + /** + * Reads a single column from the given row. + * @usage _advanced_method_ + */ + public Object getRowValue(RowState rowState, RowIdImpl rowId, + ColumnImpl column) + throws IOException + { + if(this != column.getTable()) { + throw new IllegalArgumentException( + "Given column " + column + " is not from this table"); + } + requireValidRowId(rowId); + + // position at correct row + ByteBuffer rowBuffer = positionAtRowData(rowState, rowId); + requireNonDeletedRow(rowState, rowId); + + return getRowColumn(getFormat(), rowBuffer, column, rowState, null); + } + + /** + * Reads some columns from the given row. + * @param columnNames Only column names in this collection will be returned + * @usage _advanced_method_ + */ + public RowImpl getRow( + RowState rowState, RowIdImpl rowId, Collection columnNames) + throws IOException + { + requireValidRowId(rowId); + + // position at correct row + ByteBuffer rowBuffer = positionAtRowData(rowState, rowId); + requireNonDeletedRow(rowState, rowId); + + return getRow(getFormat(), rowState, rowBuffer, _columns, columnNames); + } + + /** + * Reads the row data from the given row buffer. Leaves limit unchanged. + * Saves parsed row values to the given rowState. + */ + private static RowImpl getRow( + JetFormat format, + RowState rowState, + ByteBuffer rowBuffer, + Collection columns, + Collection columnNames) + throws IOException + { + RowImpl rtn = new RowImpl(rowState.getHeaderRowId(), columns.size()); + for(ColumnImpl column : columns) { + + if((columnNames == null) || (columnNames.contains(column.getName()))) { + // Add the value to the row data + column.setRowValue( + rtn, getRowColumn(format, rowBuffer, column, rowState, null)); + } + } + return rtn; + } + + /** + * Reads the column data from the given row buffer. Leaves limit unchanged. + * Caches the returned value in the rowState. + */ + private static Object getRowColumn(JetFormat format, + ByteBuffer rowBuffer, + ColumnImpl column, + RowState rowState, + Map rawVarValues) + throws IOException + { + byte[] columnData = null; + try { + + NullMask nullMask = rowState.getNullMask(rowBuffer); + boolean isNull = nullMask.isNull(column); + 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)); + } else if(isNull) { + // well, that's easy! (no need to update cache w/ null) + return null; + } + + // 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 + format.OFFSET_COLUMN_FIXED_DATA_ROW_OFFSET; + colDataPos = dataStart + column.getFixedDataOffset(); + colDataLen = column.getType().getFixedSize(column.getLength()); + + } else { + int varDataStart; + int varDataEnd; + + if(format.SIZE_ROW_VAR_COL_OFFSET == 2) { + + // read simple var length value + int varColumnOffsetPos = + (rowBuffer.limit() - nullMask.byteSize() - 4) - + (column.getVarLenTableIndex() * 2); + + varDataStart = rowBuffer.getShort(varColumnOffsetPos); + varDataEnd = rowBuffer.getShort(varColumnOffsetPos - 2); + + } else { + + // read jump-table based var length values + short[] varColumnOffsets = readJumpTableVarColOffsets( + rowState, rowBuffer, rowStart, nullMask); + + varDataStart = varColumnOffsets[column.getVarLenTableIndex()]; + varDataEnd = varColumnOffsets[column.getVarLenTableIndex() + 1]; + } + + colDataPos = rowStart + varDataStart; + colDataLen = varDataEnd - varDataStart; + } + + // grab the column data + rowBuffer.position(colDataPos); + columnData = ByteUtil.getBytes(rowBuffer, colDataLen); + + if((rawVarValues != null) && column.isVariableLength()) { + // caller wants raw value as well + rawVarValues.put(column, columnData); + } + + // parse the column data. we 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 for binary data (returned as byte[]), + // but binary data shouldn't be indexed anyway. + return rowState.setRowValue(column.getColumnIndex(), + column.read(columnData)); + + } catch(Exception e) { + + // cache "raw" row value. see note about caching above + rowState.setRowValue(column.getColumnIndex(), + ColumnImpl.rawDataWrapper(columnData)); + + return rowState.handleRowError(column, columnData, e); + } + } + + private static short[] readJumpTableVarColOffsets( + RowState rowState, ByteBuffer rowBuffer, int rowStart, + NullMask nullMask) + { + short[] varColOffsets = rowState.getVarColOffsets(); + if(varColOffsets != null) { + return varColOffsets; + } + + // calculate offsets using jump-table info + int nullMaskSize = nullMask.byteSize(); + int rowEnd = rowStart + rowBuffer.remaining() - 1; + int numVarCols = ByteUtil.getUnsignedByte(rowBuffer, + rowEnd - nullMaskSize); + varColOffsets = new short[numVarCols + 1]; + + int rowLen = rowEnd - rowStart + 1; + int numJumps = (rowLen - 1) / MAX_BYTE; + int colOffset = rowEnd - nullMaskSize - numJumps - 1; + + // If last jump is a dummy value, ignore it + if(((colOffset - rowStart - numVarCols) / MAX_BYTE) < numJumps) { + numJumps--; + } + + int jumpsUsed = 0; + for(int i = 0; i < numVarCols + 1; i++) { + + while((jumpsUsed < numJumps) && + (i == ByteUtil.getUnsignedByte( + rowBuffer, rowEnd - nullMaskSize-jumpsUsed - 1))) { + jumpsUsed++; + } + + varColOffsets[i] = (short) + (ByteUtil.getUnsignedByte(rowBuffer, colOffset - i) + + (jumpsUsed * MAX_BYTE)); + } + + rowState.setVarColOffsets(varColOffsets); + return varColOffsets; + } + + /** + * Reads the null mask from the given row buffer. Leaves limit unchanged. + */ + private NullMask getRowNullMask(ByteBuffer rowBuffer) + throws IOException + { + // reset position to row start + rowBuffer.reset(); + + // Number of columns in this row + int columnCount = ByteUtil.getUnsignedVarInt( + rowBuffer, getFormat().SIZE_ROW_COLUMN_COUNT); + + // read null mask + NullMask nullMask = new NullMask(columnCount); + rowBuffer.position(rowBuffer.limit() - nullMask.byteSize()); //Null mask at end + nullMask.read(rowBuffer); + + return nullMask; + } + + /** + * Sets a new buffer to the correct row header page using the given rowState + * according to the given rowId. Deleted state is + * determined, but overflow row pointers are not followed. + * + * @return a ByteBuffer of the relevant page, or null if row was invalid + * @usage _advanced_method_ + */ + public static ByteBuffer positionAtRowHeader(RowState rowState, + RowIdImpl rowId) + throws IOException + { + ByteBuffer rowBuffer = rowState.setHeaderRow(rowId); + + if(rowState.isAtHeaderRow()) { + // this task has already been accomplished + return rowBuffer; + } + + if(!rowState.isValid()) { + // this was an invalid page/row + rowState.setStatus(RowStateStatus.AT_HEADER); + return null; + } + + // note, we don't use findRowStart here cause we need the unmasked value + short rowStart = rowBuffer.getShort( + getRowStartOffset(rowId.getRowNumber(), + rowState.getTable().getFormat())); + + // check the deleted, overflow flags for the row (the "real" flags are + // always set on the header row) + RowStatus rowStatus = RowStatus.NORMAL; + if(isDeletedRow(rowStart)) { + rowStatus = RowStatus.DELETED; + } else if(isOverflowRow(rowStart)) { + rowStatus = RowStatus.OVERFLOW; + } + + rowState.setRowStatus(rowStatus); + rowState.setStatus(RowStateStatus.AT_HEADER); + return rowBuffer; + } + + /** + * Sets the position and limit in a new buffer using the given rowState + * according to the given row number and row end, following overflow row + * pointers as necessary. + * + * @return a ByteBuffer narrowed to the actual row data, or null if row was + * invalid or deleted + * @usage _advanced_method_ + */ + public static ByteBuffer positionAtRowData(RowState rowState, + RowIdImpl rowId) + throws IOException + { + positionAtRowHeader(rowState, rowId); + if(!rowState.isValid() || rowState.isDeleted()) { + // row is invalid or deleted + rowState.setStatus(RowStateStatus.AT_FINAL); + return null; + } + + ByteBuffer rowBuffer = rowState.getFinalPage(); + int rowNum = rowState.getFinalRowId().getRowNumber(); + JetFormat format = rowState.getTable().getFormat(); + + if(rowState.isAtFinalRow()) { + // we've already found the final row data + return PageChannel.narrowBuffer( + rowBuffer, + findRowStart(rowBuffer, rowNum, format), + findRowEnd(rowBuffer, rowNum, format)); + } + + while(true) { + + // note, we don't use findRowStart here cause we need the unmasked value + short rowStart = rowBuffer.getShort(getRowStartOffset(rowNum, format)); + short rowEnd = findRowEnd(rowBuffer, rowNum, format); + + // note, at this point we know the row is not deleted, so ignore any + // subsequent deleted flags (as overflow rows are always marked deleted + // anyway) + boolean overflowRow = isOverflowRow(rowStart); + + // now, strip flags from rowStart offset + rowStart = (short)(rowStart & OFFSET_MASK); + + if (overflowRow) { + + if((rowEnd - rowStart) < 4) { + throw new IOException("invalid overflow row info"); + } + + // Overflow page. the "row" data in the current page points to + // another page/row + int overflowRowNum = ByteUtil.getUnsignedByte(rowBuffer, rowStart); + int overflowPageNum = ByteUtil.get3ByteInt(rowBuffer, rowStart + 1); + rowBuffer = rowState.setOverflowRow( + new RowIdImpl(overflowPageNum, overflowRowNum)); + rowNum = overflowRowNum; + + } else { + + rowState.setStatus(RowStateStatus.AT_FINAL); + return PageChannel.narrowBuffer(rowBuffer, rowStart, rowEnd); + } + } + } + + public Iterator iterator() { + return getDefaultCursor().iterator(); + } + + /** + * Writes a new table defined by the given TableCreator to the database. + * @usage _advanced_method_ + */ + protected static void writeTableDefinition(TableCreator creator) + throws IOException + { + // first, create the usage map page + createUsageMapDefinitionBuffer(creator); + + // next, determine how big the table def will be (in case it will be more + // than one page) + JetFormat format = creator.getFormat(); + int idxDataLen = (creator.getIndexCount() * + (format.SIZE_INDEX_DEFINITION + + format.SIZE_INDEX_COLUMN_BLOCK)) + + (creator.getLogicalIndexCount() * format.SIZE_INDEX_INFO_BLOCK); + int colUmapLen = creator.getLongValueColumns().size() * 10; + int totalTableDefSize = format.SIZE_TDEF_HEADER + + (format.SIZE_COLUMN_DEF_BLOCK * creator.getColumns().size()) + + idxDataLen + colUmapLen + format.SIZE_TDEF_TRAILER; + + // total up the amount of space used by the column and index names (2 + // bytes per char + 2 bytes for the length) + for(ColumnBuilder col : creator.getColumns()) { + int nameByteLen = (col.getName().length() * + JetFormat.TEXT_FIELD_UNIT_SIZE); + totalTableDefSize += nameByteLen + 2; + } + + for(IndexBuilder idx : creator.getIndexes()) { + int nameByteLen = (idx.getName().length() * + JetFormat.TEXT_FIELD_UNIT_SIZE); + totalTableDefSize += nameByteLen + 2; + } + + + // now, create the table definition + PageChannel pageChannel = creator.getPageChannel(); + ByteBuffer buffer = pageChannel .createBuffer(Math.max(totalTableDefSize, + format.PAGE_SIZE)); + writeTableDefinitionHeader(creator, buffer, totalTableDefSize); + + if(creator.hasIndexes()) { + // index row counts + IndexData.writeRowCountDefinitions(creator, buffer); + } + + // column definitions + ColumnImpl.writeDefinitions(creator, buffer); + + if(creator.hasIndexes()) { + // index and index data definitions + IndexData.writeDefinitions(creator, buffer); + IndexImpl.writeDefinitions(creator, buffer); + } + + // write long value column usage map references + for(ColumnBuilder lvalCol : creator.getLongValueColumns()) { + buffer.putShort(lvalCol.getColumnNumber()); + TableCreator.ColumnState colState = + creator.getColumnState(lvalCol); + + // owned pages umap (both are on same page) + buffer.put(colState.getUmapOwnedRowNumber()); + ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber()); + // free space pages umap + buffer.put(colState.getUmapFreeRowNumber()); + ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber()); + } + + //End of tabledef + buffer.put((byte) 0xff); + buffer.put((byte) 0xff); + + // write table buffer to database + if(totalTableDefSize <= format.PAGE_SIZE) { + + // easy case, fits on one page + buffer.putShort(format.OFFSET_FREE_SPACE, + (short)(buffer.remaining() - 8)); // overwrite page free space + // Write the tdef page to disk. + pageChannel.writePage(buffer, creator.getTdefPageNumber()); + + } else { + + // need to split across multiple pages + ByteBuffer partialTdef = pageChannel.createPageBuffer(); + buffer.rewind(); + int nextTdefPageNumber = PageChannel.INVALID_PAGE_NUMBER; + while(buffer.hasRemaining()) { + + // reset for next write + partialTdef.clear(); + + if(nextTdefPageNumber == PageChannel.INVALID_PAGE_NUMBER) { + + // this is the first page. note, the first page already has the + // page header, so no need to write it here + nextTdefPageNumber = creator.getTdefPageNumber(); + + } else { + + // write page header + writeTablePageHeader(partialTdef); + } + + // copy the next page of tdef bytes + int curTdefPageNumber = nextTdefPageNumber; + int writeLen = Math.min(partialTdef.remaining(), buffer.remaining()); + partialTdef.put(buffer.array(), buffer.position(), writeLen); + ByteUtil.forward(buffer, writeLen); + + if(buffer.hasRemaining()) { + // need a next page + nextTdefPageNumber = pageChannel.allocateNewPage(); + partialTdef.putInt(format.OFFSET_NEXT_TABLE_DEF_PAGE, + nextTdefPageNumber); + } + + // update page free space + partialTdef.putShort(format.OFFSET_FREE_SPACE, + (short)(partialTdef.remaining() - 8)); // overwrite page free space + + // write partial page to disk + pageChannel.writePage(partialTdef, curTdefPageNumber); + } + + } + } + + /** + * @param buffer Buffer to write to + * @param columns List of Columns in the table + */ + private static void writeTableDefinitionHeader( + TableCreator creator, ByteBuffer buffer, int totalTableDefSize) + throws IOException + { + List columns = creator.getColumns(); + + //Start writing the tdef + writeTablePageHeader(buffer); + buffer.putInt(totalTableDefSize); //Length of table def + buffer.putInt(MAGIC_TABLE_NUMBER); // seemingly constant magic value + buffer.putInt(0); //Number of rows + buffer.putInt(0); //Last Autonumber + buffer.put((byte) 1); // this makes autonumbering work in access + for (int i = 0; i < 15; i++) { //Unknown + buffer.put((byte) 0); + } + buffer.put(TYPE_USER); //Table type + buffer.putShort((short) columns.size()); //Max columns a row will have + buffer.putShort(ColumnImpl.countVariableLength(columns)); //Number of variable columns in table + buffer.putShort((short) columns.size()); //Number of columns in table + buffer.putInt(creator.getLogicalIndexCount()); //Number of logical indexes in table + buffer.putInt(creator.getIndexCount()); //Number of indexes in table + buffer.put((byte) 0); //Usage map row number + ByteUtil.put3ByteInt(buffer, creator.getUmapPageNumber()); //Usage map page number + buffer.put((byte) 1); //Free map row number + ByteUtil.put3ByteInt(buffer, creator.getUmapPageNumber()); //Free map page number + } + + /** + * Writes the page header for a table definition page + * @param buffer Buffer to write to + */ + private static void writeTablePageHeader(ByteBuffer buffer) + { + buffer.put(PageTypes.TABLE_DEF); //Page type + buffer.put((byte) 0x01); //Unknown + buffer.put((byte) 0); //Unknown + buffer.put((byte) 0); //Unknown + buffer.putInt(0); //Next TDEF page pointer + } + + /** + * Writes the given name into the given buffer in the format as expected by + * {@link #readName}. + */ + static void writeName(ByteBuffer buffer, String name, Charset charset) + { + ByteBuffer encName = ColumnImpl.encodeUncompressedText(name, charset); + buffer.putShort((short) encName.remaining()); + buffer.put(encName); + } + + /** + * Create the usage map definition page buffer. The "used pages" map is in + * row 0, the "pages with free space" map is in row 1. Index usage maps are + * in subsequent rows. + */ + private static void createUsageMapDefinitionBuffer(TableCreator creator) + throws IOException + { + List lvalCols = creator.getLongValueColumns(); + + // 2 table usage maps plus 1 for each index and 2 for each lval col + int indexUmapEnd = 2 + creator.getIndexCount(); + int umapNum = indexUmapEnd + (lvalCols.size() * 2); + + JetFormat format = creator.getFormat(); + int umapRowLength = format.OFFSET_USAGE_MAP_START + + format.USAGE_MAP_TABLE_BYTE_LENGTH; + int umapSpaceUsage = getRowSpaceUsage(umapRowLength, format); + PageChannel pageChannel = creator.getPageChannel(); + int umapPageNumber = PageChannel.INVALID_PAGE_NUMBER; + ByteBuffer umapBuf = null; + int freeSpace = 0; + int rowStart = 0; + int umapRowNum = 0; + + for(int i = 0; i < umapNum; ++i) { + + if(umapBuf == null) { + + // need new page for usage maps + if(umapPageNumber == PageChannel.INVALID_PAGE_NUMBER) { + // first umap page has already been reserved + umapPageNumber = creator.getUmapPageNumber(); + } else { + // need another umap page + umapPageNumber = creator.reservePageNumber(); + } + + freeSpace = format.DATA_PAGE_INITIAL_FREE_SPACE; + + umapBuf = pageChannel.createPageBuffer(); + umapBuf.put(PageTypes.DATA); + umapBuf.put((byte) 0x1); //Unknown + umapBuf.putShort((short)freeSpace); //Free space in page + umapBuf.putInt(0); //Table definition + umapBuf.putInt(0); //Unknown + umapBuf.putShort((short)0); //Number of records on this page + + rowStart = findRowEnd(umapBuf, 0, format) - umapRowLength; + umapRowNum = 0; + } + + umapBuf.putShort(getRowStartOffset(umapRowNum, format), (short)rowStart); + + if(i == 0) { + + // table "owned pages" map definition + umapBuf.put(rowStart, UsageMap.MAP_TYPE_REFERENCE); + + } else if(i == 1) { + + // table "free space pages" map definition + umapBuf.put(rowStart, UsageMap.MAP_TYPE_INLINE); + + } else if(i < indexUmapEnd) { + + // index umap + int indexIdx = i - 2; + IndexBuilder idx = creator.getIndexes().get(indexIdx); + + // allocate root page for the index + int rootPageNumber = pageChannel.allocateNewPage(); + + // stash info for later use + TableCreator.IndexState idxState = creator.getIndexState(idx); + idxState.setRootPageNumber(rootPageNumber); + idxState.setUmapRowNumber((byte)umapRowNum); + idxState.setUmapPageNumber(umapPageNumber); + + // index map definition, including initial root page + umapBuf.put(rowStart, UsageMap.MAP_TYPE_INLINE); + umapBuf.putInt(rowStart + 1, rootPageNumber); + umapBuf.put(rowStart + 5, (byte)1); + + } else { + + // long value column umaps + int lvalColIdx = i - indexUmapEnd; + int umapType = lvalColIdx % 2; + lvalColIdx /= 2; + + ColumnBuilder lvalCol = lvalCols.get(lvalColIdx); + TableCreator.ColumnState colState = + creator.getColumnState(lvalCol); + + umapBuf.put(rowStart, UsageMap.MAP_TYPE_INLINE); + + if((umapType == 1) && + (umapPageNumber != colState.getUmapPageNumber())) { + // we want to force both usage maps for a column to be on the same + // data page, so just discard the previous one we wrote + --i; + umapType = 0; + } + + if(umapType == 0) { + // lval column "owned pages" usage map + colState.setUmapOwnedRowNumber((byte)umapRowNum); + colState.setUmapPageNumber(umapPageNumber); + } else { + // lval column "free space pages" usage map (always on same page) + colState.setUmapFreeRowNumber((byte)umapRowNum); + } + } + + rowStart -= umapRowLength; + freeSpace -= umapSpaceUsage; + ++umapRowNum; + + if((freeSpace <= umapSpaceUsage) || (i == (umapNum - 1))) { + // finish current page + umapBuf.putShort(format.OFFSET_FREE_SPACE, (short)freeSpace); + umapBuf.putShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE, + (short)umapRowNum); + pageChannel.writePage(umapBuf, umapPageNumber); + umapBuf = null; + } + } + } + + /** + * Returns a single ByteBuffer which contains the entire table definition + * (which may span multiple database pages). + */ + private ByteBuffer loadCompleteTableDefinitionBuffer(ByteBuffer tableBuffer) + throws IOException + { + int nextPage = tableBuffer.getInt(getFormat().OFFSET_NEXT_TABLE_DEF_PAGE); + ByteBuffer nextPageBuffer = null; + while (nextPage != 0) { + if (nextPageBuffer == null) { + nextPageBuffer = getPageChannel().createPageBuffer(); + } + getPageChannel().readPage(nextPageBuffer, nextPage); + nextPage = nextPageBuffer.getInt(getFormat().OFFSET_NEXT_TABLE_DEF_PAGE); + ByteBuffer newBuffer = getPageChannel().createBuffer( + tableBuffer.capacity() + getFormat().PAGE_SIZE - 8); + newBuffer.put(tableBuffer); + newBuffer.put(nextPageBuffer.array(), 8, getFormat().PAGE_SIZE - 8); + tableBuffer = newBuffer; + tableBuffer.flip(); + } + return tableBuffer; + } + + private void readColumnDefinitions(ByteBuffer tableBuffer, short columnCount) + throws IOException + { + int colOffset = getFormat().OFFSET_INDEX_DEF_BLOCK + + _indexCount * getFormat().SIZE_INDEX_DEFINITION; + int dispIndex = 0; + for (int i = 0; i < columnCount; i++) { + ColumnImpl column = new ColumnImpl(this, tableBuffer, + colOffset + (i * getFormat().SIZE_COLUMN_HEADER), dispIndex++); + _columns.add(column); + if(column.isVariableLength()) { + // also shove it in the variable columns list, which is ordered + // differently from the _columns list + _varColumns.add(column); + } + } + tableBuffer.position(colOffset + + (columnCount * getFormat().SIZE_COLUMN_HEADER)); + for (int i = 0; i < columnCount; i++) { + ColumnImpl column = _columns.get(i); + column.setName(readName(tableBuffer)); + } + Collections.sort(_columns); + getAutoNumberColumns(); + + // setup the data index for the columns + int colIdx = 0; + for(ColumnImpl col : _columns) { + col.setColumnIndex(colIdx++); + } + + // sort variable length columns based on their index into the variable + // length offset table, because we will write the columns in this order + Collections.sort(_varColumns, VAR_LEN_COLUMN_COMPARATOR); + } + + private void readIndexDefinitions(ByteBuffer tableBuffer) throws IOException + { + // read index column information + for (int i = 0; i < _indexCount; i++) { + IndexData idxData = _indexDatas.get(i); + idxData.read(tableBuffer, _columns); + // keep track of all columns involved in indexes + for(IndexData.ColumnDescriptor iCol : idxData.getColumns()) { + _indexColumns.add(iCol.getColumn()); + } + } + + // read logical index info (may be more logical indexes than index datas) + for (int i = 0; i < _logicalIndexCount; i++) { + _indexes.add(new IndexImpl(tableBuffer, _indexDatas, getFormat())); + } + + // read logical index names + for (int i = 0; i < _logicalIndexCount; i++) { + _indexes.get(i).setName(readName(tableBuffer)); + } + + Collections.sort(_indexes); + } + + /** + * Writes the given page data to the given page number, clears any other + * relevant buffers. + */ + private void writeDataPage(ByteBuffer pageBuffer, int pageNumber) + throws IOException + { + // write the page data + getPageChannel().writePage(pageBuffer, pageNumber); + + // possibly invalidate the add row buffer if a different data buffer is + // being written (e.g. this happens during deleteRow) + _addRowBufferH.possiblyInvalidate(pageNumber, pageBuffer); + + // update modification count so any active RowStates can keep themselves + // up-to-date + ++_modCount; + } + + /** + * Returns a name read from the buffer at the current position. The + * expected name format is the name length followed by the name + * encoded using the {@link JetFormat#CHARSET} + */ + private String readName(ByteBuffer buffer) { + int nameLength = readNameLength(buffer); + byte[] nameBytes = ByteUtil.getBytes(buffer, nameLength); + return ColumnImpl.decodeUncompressedText(nameBytes, + getDatabase().getCharset()); + } + + /** + * Returns a name length read from the buffer at the current position. + */ + private int readNameLength(ByteBuffer buffer) { + return ByteUtil.getUnsignedVarInt(buffer, getFormat().SIZE_NAME_LENGTH); + } + + public Object[] asRow(Map rowMap) { + return asRow(rowMap, null, false); + } + + /** + * Converts a map of columnName -> columnValue to an array of row values + * appropriate for a call to {@link #addRow(Object...)}, where the generated + * RowId will be an extra value at the end of the array. + * @see ColumnImpl#RETURN_ROW_ID + * @usage _intermediate_method_ + */ + public Object[] asRowWithRowId(Map rowMap) { + return asRow(rowMap, null, true); + } + + public Object[] asUpdateRow(Map rowMap) { + return asRow(rowMap, Column.KEEP_VALUE, false); + } + + /** + * @return the generated RowId added to a row of values created via {@link + * #asRowWithRowId} + * @usage _intermediate_method_ + */ + public RowId getRowId(Object[] row) { + return (RowId)row[_columns.size()]; + } + + /** + * Converts a map of columnName -> columnValue to an array of row values. + */ + private Object[] asRow(Map rowMap, Object defaultValue, + boolean returnRowId) + { + int len = _columns.size(); + if(returnRowId) { + ++len; + } + Object[] row = new Object[len]; + if(defaultValue != null) { + Arrays.fill(row, defaultValue); + } + if(returnRowId) { + row[len - 1] = ColumnImpl.RETURN_ROW_ID; + } + if(rowMap == null) { + return row; + } + for(ColumnImpl col : _columns) { + if(rowMap.containsKey(col.getName())) { + col.setRowValue(row, col.getRowValue(rowMap)); + } + } + return row; + } + + public Object[] addRow(Object... row) throws IOException { + return addRows(Collections.singletonList(row), _singleRowBufferH).get(0); + } + + public > M addRowFromMap(M row) + throws IOException + { + Object[] rowValues = asRow(row); + + addRow(rowValues); + + returnRowValues(row, rowValues, _autoNumColumns); + return row; + } + + public List addRows(List rows) + throws IOException + { + return addRows(rows, _multiRowBufferH); + } + + public > List addRowsFromMaps(List rows) + throws IOException + { + List rowValuesList = new ArrayList(rows.size()); + for(Map row : rows) { + rowValuesList.add(asRow(row)); + } + + addRows(rowValuesList); + + if(!_autoNumColumns.isEmpty()) { + for(int i = 0; i < rowValuesList.size(); ++i) { + Map row = rows.get(i); + Object[] rowValues = rowValuesList.get(i); + returnRowValues(row, rowValues, _autoNumColumns); + } + } + return rows; + } + + private static void returnRowValues(Map row, Object[] rowValues, + List cols) + { + for(ColumnImpl col : cols) { + col.setRowValue(row, col.getRowValue(rowValues)); + } + } + + /** + * 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 writeRowBufferH TempBufferHolder used to generate buffers for + * writing the row data + */ + private List addRows(List rows, + TempBufferHolder writeRowBufferH) + throws IOException + { + if(rows.isEmpty()) { + return rows; + } + + getPageChannel().startWrite(); + try { + + List dupeRows = null; + ByteBuffer[] rowData = new ByteBuffer[rows.size()]; + int numCols = _columns.size(); + for (int i = 0; i < rows.size(); i++) { + + // we need to make sure the row is the right length and is an Object[] + // (fill with null if too short). note, if the row is copied the caller + // will not be able to access any generated auto-number value, but if + // they need that info they should use a row array of the right + // size/type! + Object[] row = rows.get(i); + if((row.length < numCols) || (row.getClass() != Object[].class)) { + row = dupeRow(row, numCols); + // copy the input rows to a modifiable list so we can update the + // elements + if(dupeRows == null) { + dupeRows = new ArrayList(rows); + rows = dupeRows; + } + // we copied the row, so put the copy back into the rows list + dupeRows.set(i, row); + } + + // fill in autonumbers + handleAutoNumbersForAdd(row); + + // write the row of data to a temporary buffer + rowData[i] = createRow(row, + writeRowBufferH.getPageBuffer(getPageChannel())); + + if (rowData[i].limit() > getFormat().MAX_ROW_SIZE) { + throw new IOException("Row size " + rowData[i].limit() + + " is too large"); + } + } + + ByteBuffer dataPage = null; + int pageNumber = PageChannel.INVALID_PAGE_NUMBER; + + for (int i = 0; i < rowData.length; i++) { + int rowSize = rowData[i].remaining(); + Object[] row = rows.get(i); + + // handle foreign keys before adding to table + _fkEnforcer.addRow(row); + + // get page with space + dataPage = findFreeRowSpace(rowSize, dataPage, pageNumber); + pageNumber = _addRowBufferH.getPageNumber(); + + // write out the row data + int rowNum = addDataPageRow(dataPage, rowSize, getFormat(), 0); + dataPage.put(rowData[i]); + + // update the indexes + RowIdImpl rowId = new RowIdImpl(pageNumber, rowNum); + for(IndexData indexData : _indexDatas) { + indexData.addRow(row, rowId); + } + + // return rowTd if desired + if((row.length > numCols) && (row[numCols] == ColumnImpl.RETURN_ROW_ID)) { + row[numCols] = rowId; + } + } + + writeDataPage(dataPage, pageNumber); + + // Update tdef page + updateTableDefinition(rows.size()); + + } finally { + getPageChannel().finishWrite(); + } + + return rows; + } + + public Row updateRow(Row row) throws IOException { + return updateRowFromMap( + getDefaultCursor().getRowState(), (RowIdImpl)row.getId(), row); + } + + /** + * Update the row with the given id. Provided RowId must have previously + * been returned from this Table. + * @return the given row, updated with the current row values + * @throws IllegalStateException if the given row is not valid, or deleted. + * @usage _intermediate_method_ + */ + public Object[] updateRow(RowId rowId, Object... row) throws IOException { + return updateRow( + getDefaultCursor().getRowState(), (RowIdImpl)rowId, row); + } + + public > M updateRowFromMap( + RowState rowState, RowIdImpl rowId, M row) + throws IOException + { + Object[] rowValues = updateRow(rowState, rowId, asUpdateRow(row)); + returnRowValues(row, rowValues, _columns); + return row; + } + + /** + * Update the row for the given rowId. + * @usage _advanced_method_ + */ + public Object[] updateRow(RowState rowState, RowIdImpl rowId, Object... row) + throws IOException + { + requireValidRowId(rowId); + + getPageChannel().startWrite(); + try { + + // ensure that the relevant row state is up-to-date + ByteBuffer rowBuffer = positionAtRowData(rowState, rowId); + int oldRowSize = rowBuffer.remaining(); + + requireNonDeletedRow(rowState, rowId); + + // we need to make sure the row is the right length & type (fill with + // null if too short). + if((row.length < _columns.size()) || (row.getClass() != Object[].class)) { + row = dupeRow(row, _columns.size()); + } + + // hang on to the raw values of var length columns we are "keeping". this + // will allow us to re-use pre-written var length data, which can save + // space for things like long value columns. + Map keepRawVarValues = + (!_varColumns.isEmpty() ? new HashMap() : null); + + for(ColumnImpl column : _columns) { + if(_autoNumColumns.contains(column)) { + // fill in any auto-numbers (we don't allow autonumber values to be + // modified) + column.setRowValue(row, getRowColumn(getFormat(), rowBuffer, column, + rowState, null)); + } else if(column.getRowValue(row) == Column.KEEP_VALUE) { + // fill in any "keep value" fields + column.setRowValue(row, getRowColumn(getFormat(), rowBuffer, column, + rowState, keepRawVarValues)); + } else if(_indexColumns.contains(column)) { + // read row value to help update indexes + getRowColumn(getFormat(), rowBuffer, column, rowState, null); + } + } + + // generate new row bytes + ByteBuffer newRowData = createRow( + row, _singleRowBufferH.getPageBuffer(getPageChannel()), oldRowSize, + keepRawVarValues); + + if (newRowData.limit() > getFormat().MAX_ROW_SIZE) { + throw new IOException("Row size " + newRowData.limit() + + " is too large"); + } + + if(!_indexDatas.isEmpty()) { + + Object[] oldRowValues = rowState.getRowValues(); + + // check foreign keys before actually updating + _fkEnforcer.updateRow(oldRowValues, row); + + // delete old values from indexes + for(IndexData indexData : _indexDatas) { + indexData.deleteRow(oldRowValues, rowId); + } + } + + // see if we can squeeze the new row data into the existing row + rowBuffer.reset(); + int rowSize = newRowData.remaining(); + + ByteBuffer dataPage = null; + int pageNumber = PageChannel.INVALID_PAGE_NUMBER; + + if(oldRowSize >= rowSize) { + + // awesome, slap it in! + rowBuffer.put(newRowData); + + // grab the page we just updated + dataPage = rowState.getFinalPage(); + pageNumber = rowState.getFinalRowId().getPageNumber(); + + } else { + + // bummer, need to find a new page for the data + dataPage = findFreeRowSpace(rowSize, null, + PageChannel.INVALID_PAGE_NUMBER); + pageNumber = _addRowBufferH.getPageNumber(); + + RowIdImpl headerRowId = rowState.getHeaderRowId(); + ByteBuffer headerPage = rowState.getHeaderPage(); + if(pageNumber == headerRowId.getPageNumber()) { + // new row is on the same page as header row, share page + dataPage = headerPage; + } + + // write out the new row data (set the deleted flag on the new data row + // so that it is ignored during normal table traversal) + int rowNum = addDataPageRow(dataPage, rowSize, getFormat(), + DELETED_ROW_MASK); + dataPage.put(newRowData); + + // write the overflow info into the header row and clear out the + // remaining header data + rowBuffer = PageChannel.narrowBuffer( + headerPage, + findRowStart(headerPage, headerRowId.getRowNumber(), getFormat()), + findRowEnd(headerPage, headerRowId.getRowNumber(), getFormat())); + rowBuffer.put((byte)rowNum); + ByteUtil.put3ByteInt(rowBuffer, pageNumber); + ByteUtil.clearRemaining(rowBuffer); + + // set the overflow flag on the header row + int headerRowIndex = getRowStartOffset(headerRowId.getRowNumber(), + getFormat()); + headerPage.putShort(headerRowIndex, + (short)(headerPage.getShort(headerRowIndex) + | OVERFLOW_ROW_MASK)); + if(pageNumber != headerRowId.getPageNumber()) { + writeDataPage(headerPage, headerRowId.getPageNumber()); + } + } + + // update the indexes + for(IndexData indexData : _indexDatas) { + indexData.addRow(row, rowId); + } + + writeDataPage(dataPage, pageNumber); + + updateTableDefinition(0); + + } finally { + getPageChannel().finishWrite(); + } + + return row; + } + + private ByteBuffer findFreeRowSpace(int rowSize, ByteBuffer dataPage, + int pageNumber) + throws IOException + { + // assume incoming page is modified + boolean modifiedPage = true; + + if(dataPage == null) { + + // find owned page w/ free space + dataPage = findFreeRowSpace(_ownedPages, _freeSpacePages, + _addRowBufferH); + + if(dataPage == null) { + // No data pages exist (with free space). Create a new one. + return newDataPage(); + } + + // found a page, see if it will work + pageNumber = _addRowBufferH.getPageNumber(); + // since we just loaded this page, it is not yet modified + modifiedPage = false; + } + + if(!rowFitsOnDataPage(rowSize, dataPage, getFormat())) { + + // Last data page is full. Write old one and create a new one. + if(modifiedPage) { + writeDataPage(dataPage, pageNumber); + } + _freeSpacePages.removePageNumber(pageNumber, true); + + dataPage = newDataPage(); + } + + return dataPage; + } + + static ByteBuffer findFreeRowSpace( + UsageMap ownedPages, UsageMap freeSpacePages, + TempPageHolder rowBufferH) + throws IOException + { + // find last data page (Not bothering to check other pages for free + // space.) + UsageMap.PageCursor revPageCursor = ownedPages.cursor(); + revPageCursor.afterLast(); + while(true) { + int tmpPageNumber = revPageCursor.getPreviousPage(); + if(tmpPageNumber < 0) { + break; + } + ByteBuffer dataPage = rowBufferH.setPage(ownedPages.getPageChannel(), + tmpPageNumber); + if(dataPage.get() == PageTypes.DATA) { + // found last data page, only use if actually listed in free space + // pages + if(freeSpacePages.containsPageNumber(tmpPageNumber)) { + return dataPage; + } + } + } + + return null; + } + + /** + * Updates the table definition after rows are modified. + */ + private void updateTableDefinition(int rowCountInc) throws IOException + { + // load table definition + ByteBuffer tdefPage = _tableDefBufferH.setPage(getPageChannel(), + _tableDefPageNumber); + + // make sure rowcount and autonumber are up-to-date + _rowCount += rowCountInc; + tdefPage.putInt(getFormat().OFFSET_NUM_ROWS, _rowCount); + tdefPage.putInt(getFormat().OFFSET_NEXT_AUTO_NUMBER, _lastLongAutoNumber); + int ctypeOff = getFormat().OFFSET_NEXT_COMPLEX_AUTO_NUMBER; + if(ctypeOff >= 0) { + tdefPage.putInt(ctypeOff, _lastComplexTypeAutoNumber); + } + + // write any index changes + for (IndexData indexData : _indexDatas) { + // write the unique entry count for the index to the table definition + // page + tdefPage.putInt(indexData.getUniqueEntryCountOffset(), + indexData.getUniqueEntryCount()); + // write the entry page for the index + indexData.update(); + } + + // write modified table definition + getPageChannel().writePage(tdefPage, _tableDefPageNumber); + } + + /** + * Create a new data page + * @return Page number of the new page + */ + private ByteBuffer newDataPage() throws IOException { + ByteBuffer dataPage = _addRowBufferH.setNewPage(getPageChannel()); + dataPage.put(PageTypes.DATA); //Page type + dataPage.put((byte) 1); //Unknown + dataPage.putShort((short)getFormat().DATA_PAGE_INITIAL_FREE_SPACE); //Free space in this page + dataPage.putInt(_tableDefPageNumber); //Page pointer to table definition + dataPage.putInt(0); //Unknown + dataPage.putShort((short)0); //Number of rows on this page + int pageNumber = _addRowBufferH.getPageNumber(); + getPageChannel().writePage(dataPage, pageNumber); + _ownedPages.addPageNumber(pageNumber); + _freeSpacePages.addPageNumber(pageNumber); + return dataPage; + } + + /** + * @usage _advanced_method_ + */ + public ByteBuffer createRow(Object[] rowArray, ByteBuffer buffer) + throws IOException + { + return createRow(rowArray, buffer, 0, + Collections.emptyMap()); + } + + /** + * Serialize a row of Objects into a byte buffer. + * + * @param rowArray row data, expected to be correct length for this table + * @param buffer buffer to which to write the row data + * @param minRowSize min size for result row + * @param rawVarValues optional, pre-written values for var length columns + * (enables re-use of previously written values). + * @return the given buffer, filled with the row data + */ + private ByteBuffer createRow(Object[] rowArray, ByteBuffer buffer, + int minRowSize, + Map rawVarValues) + throws IOException + { + buffer.putShort(_maxColumnCount); + NullMask nullMask = new NullMask(_maxColumnCount); + + //Fixed length column data comes first + int fixedDataStart = buffer.position(); + int fixedDataEnd = fixedDataStart; + for (ColumnImpl col : _columns) { + + if(col.isVariableLength()) { + continue; + } + + Object rowValue = col.getRowValue(rowArray); + + if (col.getType() == DataType.BOOLEAN) { + + if(ColumnImpl.toBooleanValue(rowValue)) { + //Booleans are stored in the null mask + nullMask.markNotNull(col); + } + rowValue = null; + } + + if(rowValue != null) { + + // we have a value to write + nullMask.markNotNull(col); + + // remainingRowLength is ignored when writing fixed length data + buffer.position(fixedDataStart + col.getFixedDataOffset()); + buffer.put(col.write(rowValue, 0)); + } + + // always insert space for the entire fixed data column length + // (including null values), access expects the row to always be at least + // big enough to hold all fixed values + buffer.position(fixedDataStart + col.getFixedDataOffset() + + col.getLength()); + + // keep track of the end of fixed data + if(buffer.position() > fixedDataEnd) { + fixedDataEnd = buffer.position(); + } + + } + + // reposition at end of fixed data + buffer.position(fixedDataEnd); + + // only need this info if this table contains any var length data + if(_maxVarColumnCount > 0) { + + int maxRowSize = getFormat().MAX_ROW_SIZE; + + // figure out how much space remains for var length data. first, + // account for already written space + maxRowSize -= buffer.position(); + // now, account for trailer space + int trailerSize = (nullMask.byteSize() + 4 + (_maxVarColumnCount * 2)); + maxRowSize -= trailerSize; + + // for each non-null long value column we need to reserve a small + // amount of space so that we don't end up running out of row space + // later by being too greedy + for (ColumnImpl varCol : _varColumns) { + if((varCol.getType().isLongValue()) && + (varCol.getRowValue(rowArray) != null)) { + maxRowSize -= getFormat().SIZE_LONG_VALUE_DEF; + } + } + + //Now write out variable length column data + short[] varColumnOffsets = new short[_maxVarColumnCount]; + int varColumnOffsetsIndex = 0; + for (ColumnImpl varCol : _varColumns) { + short offset = (short) buffer.position(); + Object rowValue = varCol.getRowValue(rowArray); + if (rowValue != null) { + // we have a value + nullMask.markNotNull(varCol); + + byte[] rawValue = null; + ByteBuffer varDataBuf = null; + if(((rawValue = rawVarValues.get(varCol)) != null) && + (rawValue.length <= maxRowSize)) { + // save time and potentially db space, re-use raw value + varDataBuf = ByteBuffer.wrap(rawValue); + } else { + // write column value + varDataBuf = varCol.write(rowValue, maxRowSize); + } + + maxRowSize -= varDataBuf.remaining(); + if(varCol.getType().isLongValue()) { + // we already accounted for some amount of the long value data + // above. add that space back so we don't double count + maxRowSize += getFormat().SIZE_LONG_VALUE_DEF; + } + buffer.put(varDataBuf); + } + + // we do a loop here so that we fill in offsets for deleted columns + while(varColumnOffsetsIndex <= varCol.getVarLenTableIndex()) { + varColumnOffsets[varColumnOffsetsIndex++] = offset; + } + } + + // fill in offsets for any remaining deleted columns + while(varColumnOffsetsIndex < varColumnOffsets.length) { + varColumnOffsets[varColumnOffsetsIndex++] = (short) buffer.position(); + } + + // record where we stopped writing + int eod = buffer.position(); + + // insert padding if necessary + padRowBuffer(buffer, minRowSize, trailerSize); + + buffer.putShort((short) eod); //EOD marker + + //Now write out variable length offsets + //Offsets are stored in reverse order + for (int i = _maxVarColumnCount - 1; i >= 0; i--) { + buffer.putShort(varColumnOffsets[i]); + } + buffer.putShort(_maxVarColumnCount); //Number of var length columns + + } else { + + // insert padding for row w/ no var cols + padRowBuffer(buffer, minRowSize, nullMask.byteSize()); + } + + nullMask.write(buffer); //Null mask + buffer.flip(); + 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); + } + } + + private static void padRowBuffer(ByteBuffer buffer, int minRowSize, + int trailerSize) + { + int pos = buffer.position(); + if((pos + trailerSize) < minRowSize) { + // pad the row to get to the min byte size + int padSize = minRowSize - (pos + trailerSize); + ByteUtil.clearRange(buffer, pos, pos + padSize); + ByteUtil.forward(buffer, padSize); + } + } + + public int getRowCount() { + return _rowCount; + } + + int getNextLongAutoNumber() { + // note, the saved value is the last one handed out, so pre-increment + return ++_lastLongAutoNumber; + } + + int getLastLongAutoNumber() { + // gets the last used auto number (does not modify) + return _lastLongAutoNumber; + } + + int getNextComplexTypeAutoNumber() { + // note, the saved value is the last one handed out, so pre-increment + return ++_lastComplexTypeAutoNumber; + } + + int getLastComplexTypeAutoNumber() { + // gets the last used auto number (does not modify) + return _lastComplexTypeAutoNumber; + } + + @Override + public String toString() { + StringBuilder rtn = new StringBuilder(); + rtn.append("Type: " + _tableType + + ((_tableType == TYPE_USER) ? " (USER)" : " (SYSTEM)")); + rtn.append("\nName: " + _name); + rtn.append("\nRow count: " + _rowCount); + rtn.append("\nColumn count: " + _columns.size()); + rtn.append("\nIndex (data) count: " + _indexCount); + rtn.append("\nLogical Index count: " + _logicalIndexCount); + rtn.append("\nColumns:\n"); + for(ColumnImpl col : _columns) { + rtn.append(col); + } + rtn.append("\nIndexes:\n"); + for(Index index : _indexes) { + rtn.append(index); + } + rtn.append("\nOwned pages: " + _ownedPages + "\n"); + return rtn.toString(); + } + + /** + * @return A simple String representation of the entire table in + * tab-delimited format + * @usage _general_method_ + */ + public String display() throws IOException { + return display(Long.MAX_VALUE); + } + + /** + * @param limit Maximum number of rows to display + * @return A simple String representation of the entire table in + * tab-delimited format + * @usage _general_method_ + */ + public String display(long limit) throws IOException { + reset(); + StringBuilder rtn = new StringBuilder(); + for(Iterator iter = _columns.iterator(); iter.hasNext(); ) { + ColumnImpl col = iter.next(); + rtn.append(col.getName()); + if (iter.hasNext()) { + rtn.append("\t"); + } + } + rtn.append("\n"); + Row row; + int rowCount = 0; + while ((rowCount++ < limit) && (row = getNextRow()) != null) { + for(Iterator iter = row.values().iterator(); iter.hasNext(); ) { + Object obj = iter.next(); + if (obj instanceof byte[]) { + byte[] b = (byte[]) obj; + rtn.append(ByteUtil.toHexString(b)); + //This block can be used to easily dump a binary column to a file + /*java.io.File f = java.io.File.createTempFile("ole", ".bin"); + java.io.FileOutputStream out = new java.io.FileOutputStream(f); + out.write(b); + out.flush(); + out.close();*/ + } else { + rtn.append(String.valueOf(obj)); + } + if (iter.hasNext()) { + rtn.append("\t"); + } + } + rtn.append("\n"); + } + return rtn.toString(); + } + + /** + * Updates free space and row info for a new row of the given size in the + * given data page. Positions the page for writing the row data. + * @return the row number of the new row + * @usage _advanced_method_ + */ + public static int addDataPageRow(ByteBuffer dataPage, + int rowSize, + JetFormat format, + int rowFlags) + { + int rowSpaceUsage = getRowSpaceUsage(rowSize, format); + + // Decrease free space record. + short freeSpaceInPage = dataPage.getShort(format.OFFSET_FREE_SPACE); + dataPage.putShort(format.OFFSET_FREE_SPACE, (short) (freeSpaceInPage - + rowSpaceUsage)); + + // Increment row count record. + short rowCount = dataPage.getShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE); + dataPage.putShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE, + (short) (rowCount + 1)); + + // determine row position + short rowLocation = findRowEnd(dataPage, rowCount, format); + rowLocation -= rowSize; + + // write row position + dataPage.putShort(getRowStartOffset(rowCount, format), + (short)(rowLocation | rowFlags)); + + // set position for row data + dataPage.position(rowLocation); + + return rowCount; + } + + /** + * Returns the row count for the current page. If the page is invalid + * ({@code null}) or the page is not a DATA page, 0 is returned. + */ + static int getRowsOnDataPage(ByteBuffer rowBuffer, JetFormat format) + throws IOException + { + int rowsOnPage = 0; + if((rowBuffer != null) && (rowBuffer.get(0) == PageTypes.DATA)) { + rowsOnPage = rowBuffer.getShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE); + } + return rowsOnPage; + } + + /** + * @throws IllegalStateException if the given rowId is invalid + */ + private static void requireValidRowId(RowIdImpl rowId) { + if(!rowId.isValid()) { + throw new IllegalArgumentException("Given rowId is invalid: " + rowId); + } + } + + /** + * @throws IllegalStateException if the given row is invalid or deleted + */ + private static void requireNonDeletedRow(RowState rowState, RowIdImpl rowId) + { + if(!rowState.isValid()) { + throw new IllegalArgumentException( + "Given rowId is invalid for this table: " + rowId); + } + if(rowState.isDeleted()) { + throw new IllegalStateException("Row is deleted: " + rowId); + } + } + + /** + * @usage _advanced_method_ + */ + public static boolean isDeletedRow(short rowStart) { + return ((rowStart & DELETED_ROW_MASK) != 0); + } + + /** + * @usage _advanced_method_ + */ + public static boolean isOverflowRow(short rowStart) { + return ((rowStart & OVERFLOW_ROW_MASK) != 0); + } + + /** + * @usage _advanced_method_ + */ + public static short cleanRowStart(short rowStart) { + return (short)(rowStart & OFFSET_MASK); + } + + /** + * @usage _advanced_method_ + */ + public static short findRowStart(ByteBuffer buffer, int rowNum, + JetFormat format) + { + return cleanRowStart( + buffer.getShort(getRowStartOffset(rowNum, format))); + } + + /** + * @usage _advanced_method_ + */ + public static int getRowStartOffset(int rowNum, JetFormat format) + { + return format.OFFSET_ROW_START + (format.SIZE_ROW_LOCATION * rowNum); + } + + /** + * @usage _advanced_method_ + */ + public static short findRowEnd(ByteBuffer buffer, int rowNum, + JetFormat format) + { + return (short)((rowNum == 0) ? + format.PAGE_SIZE : + cleanRowStart( + buffer.getShort(getRowEndOffset(rowNum, format)))); + } + + /** + * @usage _advanced_method_ + */ + public static int getRowEndOffset(int rowNum, JetFormat format) + { + return format.OFFSET_ROW_START + (format.SIZE_ROW_LOCATION * (rowNum - 1)); + } + + /** + * @usage _advanced_method_ + */ + public static int getRowSpaceUsage(int rowSize, JetFormat format) + { + return rowSize + format.SIZE_ROW_LOCATION; + } + + private void getAutoNumberColumns() { + for(ColumnImpl c : _columns) { + if(c.isAutoNumber()) { + _autoNumColumns.add(c); + } + } + } + + /** + * Returns {@code true} if a row of the given size will fit on the given + * data page, {@code false} otherwise. + * @usage _advanced_method_ + */ + public static boolean rowFitsOnDataPage( + int rowLength, ByteBuffer dataPage, JetFormat format) + throws IOException + { + int rowSpaceUsage = getRowSpaceUsage(rowLength, format); + short freeSpaceInPage = dataPage.getShort(format.OFFSET_FREE_SPACE); + int rowsOnPage = getRowsOnDataPage(dataPage, format); + return ((rowSpaceUsage <= freeSpaceInPage) && + (rowsOnPage < format.MAX_NUM_ROWS_ON_DATA_PAGE)); + } + + /** + * Duplicates and returns a row of data, optionally with a longer length + * filled with {@code null}. + */ + static Object[] dupeRow(Object[] row, int newRowLength) { + Object[] copy = new Object[newRowLength]; + System.arraycopy(row, 0, copy, 0, Math.min(row.length, newRowLength)); + return copy; + } + + /** various statuses for the row data */ + private enum RowStatus { + INIT, INVALID_PAGE, INVALID_ROW, VALID, DELETED, NORMAL, OVERFLOW; + } + + /** the phases the RowState moves through as the data is parsed */ + private enum RowStateStatus { + INIT, AT_HEADER, AT_FINAL; + } + + /** + * Maintains the state of reading a row of data. + * @usage _advanced_class_ + */ + public final class RowState implements ErrorHandler.Location + { + /** Buffer used for reading the header row data pages */ + private final TempPageHolder _headerRowBufferH; + /** the header rowId */ + private RowIdImpl _headerRowId = RowIdImpl.FIRST_ROW_ID; + /** the number of rows on the header page */ + private int _rowsOnHeaderPage; + /** the rowState status */ + private RowStateStatus _status = RowStateStatus.INIT; + /** the row status */ + private RowStatus _rowStatus = RowStatus.INIT; + /** buffer used for reading overflow pages */ + private final TempPageHolder _overflowRowBufferH = + TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); + /** the row buffer which contains the final data (after following any + overflow pointers) */ + private ByteBuffer _finalRowBuffer; + /** the rowId which contains the final data (after following any overflow + pointers) */ + private RowIdImpl _finalRowId = null; + /** true if the row values array has data */ + private boolean _haveRowValues; + /** values read from the last row */ + private final Object[] _rowValues; + /** null mask for the last row */ + private NullMask _nullMask; + /** last modification count seen on the table we track this so that the + 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; + /** cached variable column offsets for jump-table based rows */ + private short[] _varColOffsets; + + private RowState(TempBufferHolder.Type headerType) { + _headerRowBufferH = TempPageHolder.newHolder(headerType); + _rowValues = new Object[TableImpl.this.getColumnCount()]; + _lastModCount = TableImpl.this._modCount; + } + + public TableImpl getTable() { + return TableImpl.this; + } + + public ErrorHandler getErrorHandler() { + return((_errorHandler != null) ? _errorHandler : + getTable().getErrorHandler()); + } + + public void setErrorHandler(ErrorHandler newErrorHandler) { + _errorHandler = newErrorHandler; + } + + public void reset() { + _finalRowId = null; + _finalRowBuffer = null; + _rowsOnHeaderPage = 0; + _status = RowStateStatus.INIT; + _rowStatus = RowStatus.INIT; + _varColOffsets = null; + _nullMask = null; + if(_haveRowValues) { + Arrays.fill(_rowValues, null); + _haveRowValues = false; + } + } + + public boolean isUpToDate() { + return(TableImpl.this._modCount == _lastModCount); + } + + private void checkForModification() { + if(!isUpToDate()) { + reset(); + _headerRowBufferH.invalidate(); + _overflowRowBufferH.invalidate(); + _lastModCount = TableImpl.this._modCount; + } + } + + private ByteBuffer getFinalPage() + throws IOException + { + if(_finalRowBuffer == null) { + // (re)load current page + _finalRowBuffer = getHeaderPage(); + } + return _finalRowBuffer; + } + + public RowIdImpl getFinalRowId() { + if(_finalRowId == null) { + _finalRowId = getHeaderRowId(); + } + return _finalRowId; + } + + private void setRowStatus(RowStatus rowStatus) { + _rowStatus = rowStatus; + } + + public boolean isValid() { + return(_rowStatus.ordinal() >= RowStatus.VALID.ordinal()); + } + + public boolean isDeleted() { + return(_rowStatus == RowStatus.DELETED); + } + + public boolean isOverflow() { + return(_rowStatus == RowStatus.OVERFLOW); + } + + public boolean isHeaderPageNumberValid() { + return(_rowStatus.ordinal() > RowStatus.INVALID_PAGE.ordinal()); + } + + public boolean isHeaderRowNumberValid() { + return(_rowStatus.ordinal() > RowStatus.INVALID_ROW.ordinal()); + } + + private void setStatus(RowStateStatus status) { + _status = status; + } + + public boolean isAtHeaderRow() { + return(_status.ordinal() >= RowStateStatus.AT_HEADER.ordinal()); + } + + public boolean isAtFinalRow() { + return(_status.ordinal() >= RowStateStatus.AT_FINAL.ordinal()); + } + + private Object setRowValue(int idx, Object value) { + _haveRowValues = true; + _rowValues[idx] = value; + return value; + } + + public Object[] getRowValues() { + return dupeRow(_rowValues, _rowValues.length); + } + + public NullMask getNullMask(ByteBuffer rowBuffer) throws IOException { + if(_nullMask == null) { + _nullMask = getRowNullMask(rowBuffer); + } + return _nullMask; + } + + private short[] getVarColOffsets() { + return _varColOffsets; + } + + private void setVarColOffsets(short[] varColOffsets) { + _varColOffsets = varColOffsets; + } + + public RowIdImpl getHeaderRowId() { + return _headerRowId; + } + + public int getRowsOnHeaderPage() { + return _rowsOnHeaderPage; + } + + private ByteBuffer getHeaderPage() + throws IOException + { + checkForModification(); + return _headerRowBufferH.getPage(getPageChannel()); + } + + private ByteBuffer setHeaderRow(RowIdImpl rowId) + throws IOException + { + checkForModification(); + + // don't do any work if we are already positioned correctly + if(isAtHeaderRow() && (getHeaderRowId().equals(rowId))) { + return(isValid() ? getHeaderPage() : null); + } + + // rejigger everything + reset(); + _headerRowId = rowId; + _finalRowId = rowId; + + int pageNumber = rowId.getPageNumber(); + int rowNumber = rowId.getRowNumber(); + if((pageNumber < 0) || !_ownedPages.containsPageNumber(pageNumber)) { + setRowStatus(RowStatus.INVALID_PAGE); + return null; + } + + _finalRowBuffer = _headerRowBufferH.setPage(getPageChannel(), + pageNumber); + _rowsOnHeaderPage = getRowsOnDataPage(_finalRowBuffer, getFormat()); + + if((rowNumber < 0) || (rowNumber >= _rowsOnHeaderPage)) { + setRowStatus(RowStatus.INVALID_ROW); + return null; + } + + setRowStatus(RowStatus.VALID); + return _finalRowBuffer; + } + + private ByteBuffer setOverflowRow(RowIdImpl rowId) + throws IOException + { + // this should never see modifications because it only happens within + // the positionAtRowData method + if(!isUpToDate()) { + throw new IllegalStateException("Table modified while searching?"); + } + if(_rowStatus != RowStatus.OVERFLOW) { + throw new IllegalStateException("Row is not an overflow row?"); + } + _finalRowId = rowId; + _finalRowBuffer = _overflowRowBufferH.setPage(getPageChannel(), + rowId.getPageNumber()); + return _finalRowBuffer; + } + + private Object handleRowError(ColumnImpl column, byte[] columnData, + Exception error) + throws IOException + { + return getErrorHandler().handleRowError(column, columnData, + this, error); + } + + @Override + public String toString() + { + return "RowState: headerRowId = " + _headerRowId + ", finalRowId = " + + _finalRowId; + } + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/TableScanCursor.java b/src/java/com/healthmarketscience/jackcess/impl/TableScanCursor.java new file mode 100644 index 0000000..9fe8dc4 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/TableScanCursor.java @@ -0,0 +1,220 @@ +/* +Copyright (c) 2013 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.impl; + +import java.io.IOException; + +import com.healthmarketscience.jackcess.impl.TableImpl.RowState; + + +/** + * Simple un-indexed cursor. + * + * @author James Ahlborn + */ +public class TableScanCursor extends CursorImpl +{ + /** first position for the TableScanCursor */ + private static final ScanPosition FIRST_SCAN_POSITION = + new ScanPosition(RowIdImpl.FIRST_ROW_ID); + /** last position for the TableScanCursor */ + private static final ScanPosition LAST_SCAN_POSITION = + new ScanPosition(RowIdImpl.LAST_ROW_ID); + + + /** ScanDirHandler for forward traversal */ + private final ScanDirHandler _forwardDirHandler = + new ForwardScanDirHandler(); + /** ScanDirHandler for backward traversal */ + private final ScanDirHandler _reverseDirHandler = + new ReverseScanDirHandler(); + /** Cursor over the pages that this table owns */ + private final UsageMap.PageCursor _ownedPagesCursor; + + public TableScanCursor(TableImpl table) { + super(new IdImpl(table, null), table, + FIRST_SCAN_POSITION, LAST_SCAN_POSITION); + _ownedPagesCursor = table.getOwnedPagesCursor(); + } + + @Override + protected ScanDirHandler getDirHandler(boolean moveForward) { + return (moveForward ? _forwardDirHandler : _reverseDirHandler); + } + + @Override + protected boolean isUpToDate() { + return(super.isUpToDate() && _ownedPagesCursor.isUpToDate()); + } + + @Override + protected void reset(boolean moveForward) { + _ownedPagesCursor.reset(moveForward); + super.reset(moveForward); + } + + @Override + protected void restorePositionImpl(PositionImpl curPos, PositionImpl prevPos) + throws IOException + { + if(!(curPos instanceof ScanPosition) || + !(prevPos instanceof ScanPosition)) { + throw new IllegalArgumentException( + "Restored positions must be scan positions"); + } + _ownedPagesCursor.restorePosition(curPos.getRowId().getPageNumber(), + prevPos.getRowId().getPageNumber()); + super.restorePositionImpl(curPos, prevPos); + } + + @Override + protected PositionImpl findAnotherPosition( + RowState rowState, PositionImpl curPos, boolean moveForward) + throws IOException + { + ScanDirHandler handler = getDirHandler(moveForward); + + // figure out how many rows are left on this page so we can find the + // next row + RowIdImpl curRowId = curPos.getRowId(); + TableImpl.positionAtRowHeader(rowState, curRowId); + int currentRowNumber = curRowId.getRowNumber(); + + // loop until we find the next valid row or run out of pages + while(true) { + + currentRowNumber = handler.getAnotherRowNumber(currentRowNumber); + curRowId = new RowIdImpl(curRowId.getPageNumber(), currentRowNumber); + TableImpl.positionAtRowHeader(rowState, curRowId); + + if(!rowState.isValid()) { + + // load next page + curRowId = new RowIdImpl(handler.getAnotherPageNumber(), + RowIdImpl.INVALID_ROW_NUMBER); + TableImpl.positionAtRowHeader(rowState, curRowId); + + if(!rowState.isHeaderPageNumberValid()) { + //No more owned pages. No more rows. + return handler.getEndPosition(); + } + + // update row count and initial row number + currentRowNumber = handler.getInitialRowNumber( + rowState.getRowsOnHeaderPage()); + + } else if(!rowState.isDeleted()) { + + // we found a valid, non-deleted row, return it + return new ScanPosition(curRowId); + } + + } + } + + /** + * Handles moving the table scan cursor in a given direction. Separates + * cursor logic from value storage. + */ + private abstract class ScanDirHandler extends DirHandler { + public abstract int getAnotherRowNumber(int curRowNumber); + public abstract int getAnotherPageNumber(); + public abstract int getInitialRowNumber(int rowsOnPage); + } + + /** + * Handles moving the table scan cursor forward. + */ + private final class ForwardScanDirHandler extends ScanDirHandler { + @Override + public PositionImpl getBeginningPosition() { + return getFirstPosition(); + } + @Override + public PositionImpl getEndPosition() { + return getLastPosition(); + } + @Override + public int getAnotherRowNumber(int curRowNumber) { + return curRowNumber + 1; + } + @Override + public int getAnotherPageNumber() { + return _ownedPagesCursor.getNextPage(); + } + @Override + public int getInitialRowNumber(int rowsOnPage) { + return -1; + } + } + + /** + * Handles moving the table scan cursor backward. + */ + private final class ReverseScanDirHandler extends ScanDirHandler { + @Override + public PositionImpl getBeginningPosition() { + return getLastPosition(); + } + @Override + public PositionImpl getEndPosition() { + return getFirstPosition(); + } + @Override + public int getAnotherRowNumber(int curRowNumber) { + return curRowNumber - 1; + } + @Override + public int getAnotherPageNumber() { + return _ownedPagesCursor.getPreviousPage(); + } + @Override + public int getInitialRowNumber(int rowsOnPage) { + return rowsOnPage; + } + } + + /** + * Value object which maintains the current position of a TableScanCursor. + */ + private static final class ScanPosition extends PositionImpl + { + private final RowIdImpl _rowId; + + private ScanPosition(RowIdImpl rowId) { + _rowId = rowId; + } + + @Override + public RowIdImpl getRowId() { + return _rowId; + } + + @Override + protected boolean equalsImpl(Object o) { + return getRowId().equals(((ScanPosition)o).getRowId()); + } + + @Override + public String toString() { + return "RowId = " + getRowId(); + } + } +} diff --git a/src/java/com/healthmarketscience/jackcess/TempBufferHolder.java b/src/java/com/healthmarketscience/jackcess/impl/TempBufferHolder.java similarity index 99% rename from src/java/com/healthmarketscience/jackcess/TempBufferHolder.java rename to src/java/com/healthmarketscience/jackcess/impl/TempBufferHolder.java index 83a193b..4e2b6f6 100644 --- a/src/java/com/healthmarketscience/jackcess/TempBufferHolder.java +++ b/src/java/com/healthmarketscience/jackcess/impl/TempBufferHolder.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.lang.ref.Reference; import java.lang.ref.SoftReference; diff --git a/src/java/com/healthmarketscience/jackcess/TempPageHolder.java b/src/java/com/healthmarketscience/jackcess/impl/TempPageHolder.java similarity index 99% rename from src/java/com/healthmarketscience/jackcess/TempPageHolder.java rename to src/java/com/healthmarketscience/jackcess/impl/TempPageHolder.java index d310a30..dfe5765 100644 --- a/src/java/com/healthmarketscience/jackcess/TempPageHolder.java +++ b/src/java/com/healthmarketscience/jackcess/impl/TempPageHolder.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.io.IOException; import java.nio.ByteBuffer; diff --git a/src/java/com/healthmarketscience/jackcess/UnsupportedCodecException.java b/src/java/com/healthmarketscience/jackcess/impl/UnsupportedCodecException.java similarity index 90% rename from src/java/com/healthmarketscience/jackcess/UnsupportedCodecException.java rename to src/java/com/healthmarketscience/jackcess/impl/UnsupportedCodecException.java index 51b772c..2fee4d1 100644 --- a/src/java/com/healthmarketscience/jackcess/UnsupportedCodecException.java +++ b/src/java/com/healthmarketscience/jackcess/impl/UnsupportedCodecException.java @@ -17,7 +17,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; /** * Exception thrown by a CodecHandler to indicate that the current encoding is @@ -26,8 +26,7 @@ package com.healthmarketscience.jackcess; * * @author James Ahlborn */ -public class UnsupportedCodecException - extends UnsupportedOperationException +public class UnsupportedCodecException extends UnsupportedOperationException { private static final long serialVersionUID = 20120313L; diff --git a/src/java/com/healthmarketscience/jackcess/UsageMap.java b/src/java/com/healthmarketscience/jackcess/impl/UsageMap.java similarity index 95% rename from src/java/com/healthmarketscience/jackcess/UsageMap.java rename to src/java/com/healthmarketscience/jackcess/impl/UsageMap.java index fa5d694..6a80e04 100644 --- a/src/java/com/healthmarketscience/jackcess/UsageMap.java +++ b/src/java/com/healthmarketscience/jackcess/impl/UsageMap.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.io.IOException; import java.nio.ByteBuffer; @@ -33,6 +33,7 @@ import java.util.BitSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import com.healthmarketscience.jackcess.RowId; /** * Describes which database pages a particular table uses @@ -51,7 +52,7 @@ public class UsageMap private static final int INVALID_BIT_INDEX = -1; /** owning database */ - private final Database _database; + private final DatabaseImpl _database; /** Page number of the map table declaration */ private final int _tablePageNum; /** Offset of the data page at which the usage map data starts */ @@ -82,7 +83,7 @@ public class UsageMap * @param pageNum Page number that this usage map is contained in * @param rowStart Offset at which the declaration starts in the buffer */ - private UsageMap(Database database, ByteBuffer tableBuffer, + private UsageMap(DatabaseImpl database, ByteBuffer tableBuffer, int pageNum, short rowStart) throws IOException { @@ -92,13 +93,9 @@ public class UsageMap _rowStart = rowStart; _tableBuffer.position(_rowStart + getFormat().OFFSET_USAGE_MAP_START); _startOffset = _tableBuffer.position(); - if (LOG.isDebugEnabled()) { - LOG.debug("Usage map block:\n" + ByteUtil.toHexString(_tableBuffer, _rowStart, - tableBuffer.limit() - _rowStart)); - } } - public Database getDatabase() { + public DatabaseImpl getDatabase() { return _database; } @@ -109,14 +106,14 @@ public class UsageMap public PageChannel getPageChannel() { return getDatabase().getPageChannel(); } - + /** * @param database database that contains this usage map * @param buf buffer which contains the usage map row info * @return Either an InlineUsageMap or a ReferenceUsageMap, depending on * which type of map is found */ - public static UsageMap read(Database database, ByteBuffer buf, + public static UsageMap read(DatabaseImpl database, ByteBuffer buf, boolean assumeOutOfRangeBitsOn) throws IOException { @@ -132,7 +129,7 @@ public class UsageMap * @return Either an InlineUsageMap or a ReferenceUsageMap, depending on * which type of map is found */ - public static UsageMap read(Database database, int pageNum, + public static UsageMap read(DatabaseImpl database, int pageNum, int rowNum, boolean assumeOutOfRangeBitsOn) throws IOException { @@ -140,8 +137,8 @@ public class UsageMap PageChannel pageChannel = database.getPageChannel(); ByteBuffer tableBuffer = pageChannel.createPageBuffer(); pageChannel.readPage(tableBuffer, pageNum); - short rowStart = Table.findRowStart(tableBuffer, rowNum, format); - int rowEnd = Table.findRowEnd(tableBuffer, rowNum, format); + short rowStart = TableImpl.findRowStart(tableBuffer, rowNum, format); + int rowEnd = TableImpl.findRowEnd(tableBuffer, rowNum, format); tableBuffer.limit(rowEnd); byte mapType = tableBuffer.get(rowStart); UsageMap rtn = new UsageMap(database, tableBuffer, pageNum, rowStart); @@ -216,13 +213,14 @@ public class UsageMap } protected int getFirstPageNumber() { - return bitIndexToPageNumber(getNextBitIndex(-1), RowId.LAST_PAGE_NUMBER); + return bitIndexToPageNumber(getNextBitIndex(-1), + RowIdImpl.LAST_PAGE_NUMBER); } protected int getNextPageNumber(int curPage) { return bitIndexToPageNumber( getNextBitIndex(pageNumberToBitIndex(curPage)), - RowId.LAST_PAGE_NUMBER); + RowIdImpl.LAST_PAGE_NUMBER); } protected int getNextBitIndex(int curIndex) { @@ -231,13 +229,13 @@ public class UsageMap protected int getLastPageNumber() { return bitIndexToPageNumber(getPrevBitIndex(_pageNumbers.length()), - RowId.FIRST_PAGE_NUMBER); + RowIdImpl.FIRST_PAGE_NUMBER); } protected int getPrevPageNumber(int curPage) { return bitIndexToPageNumber( getPrevBitIndex(pageNumberToBitIndex(curPage)), - RowId.FIRST_PAGE_NUMBER); + RowIdImpl.FIRST_PAGE_NUMBER); } protected int getPrevBitIndex(int curIndex) { @@ -823,18 +821,18 @@ public class UsageMap /** * @return valid page number if there was another page to read, - * {@link RowId#LAST_PAGE_NUMBER} otherwise + * {@link RowIdImpl#LAST_PAGE_NUMBER} otherwise */ public int getNextPage() { - return getAnotherPage(Cursor.MOVE_FORWARD); + return getAnotherPage(CursorImpl.MOVE_FORWARD); } /** * @return valid page number if there was another page to read, - * {@link RowId#FIRST_PAGE_NUMBER} otherwise + * {@link RowIdImpl#FIRST_PAGE_NUMBER} otherwise */ public int getPreviousPage() { - return getAnotherPage(Cursor.MOVE_REVERSE); + return getAnotherPage(CursorImpl.MOVE_REVERSE); } /** @@ -872,7 +870,7 @@ public class UsageMap * page in the map */ public void beforeFirst() { - reset(Cursor.MOVE_FORWARD); + reset(CursorImpl.MOVE_FORWARD); } /** @@ -880,7 +878,7 @@ public class UsageMap * last page in the map */ public void afterLast() { - reset(Cursor.MOVE_REVERSE); + reset(CursorImpl.MOVE_REVERSE); } /** @@ -930,9 +928,9 @@ public class UsageMap private int updatePosition(int pageNumber) { if(pageNumber < UsageMap.this.getFirstPageNumber()) { - pageNumber = RowId.FIRST_PAGE_NUMBER; + pageNumber = RowIdImpl.FIRST_PAGE_NUMBER; } else if(pageNumber > UsageMap.this.getLastPageNumber()) { - pageNumber = RowId.LAST_PAGE_NUMBER; + pageNumber = RowIdImpl.LAST_PAGE_NUMBER; } return pageNumber; } @@ -967,11 +965,11 @@ public class UsageMap } @Override public int getBeginningPageNumber() { - return RowId.FIRST_PAGE_NUMBER; + return RowIdImpl.FIRST_PAGE_NUMBER; } @Override public int getEndPageNumber() { - return RowId.LAST_PAGE_NUMBER; + return RowIdImpl.LAST_PAGE_NUMBER; } } @@ -988,11 +986,11 @@ public class UsageMap } @Override public int getBeginningPageNumber() { - return RowId.LAST_PAGE_NUMBER; + return RowIdImpl.LAST_PAGE_NUMBER; } @Override public int getEndPageNumber() { - return RowId.FIRST_PAGE_NUMBER; + return RowIdImpl.FIRST_PAGE_NUMBER; } } diff --git a/src/java/com/healthmarketscience/jackcess/impl/complex/AttachmentColumnInfoImpl.java b/src/java/com/healthmarketscience/jackcess/impl/complex/AttachmentColumnInfoImpl.java new file mode 100644 index 0000000..69c43df --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/complex/AttachmentColumnInfoImpl.java @@ -0,0 +1,482 @@ +/* +Copyright (c) 2011 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.impl.complex; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.InflaterInputStream; + +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.complex.Attachment; +import com.healthmarketscience.jackcess.complex.AttachmentColumnInfo; +import com.healthmarketscience.jackcess.complex.ComplexDataType; +import com.healthmarketscience.jackcess.complex.ComplexValue; +import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; +import com.healthmarketscience.jackcess.impl.ByteUtil; +import com.healthmarketscience.jackcess.impl.ColumnImpl; +import com.healthmarketscience.jackcess.impl.JetFormat; +import com.healthmarketscience.jackcess.impl.PageChannel; + + +/** + * Complex column info for a column holding 0 or more attachments per row. + * + * @author James Ahlborn + */ +public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl + implements AttachmentColumnInfo +{ + /** some file formats which may not be worth re-compressing */ + private static final Set COMPRESSED_FORMATS = new HashSet( + Arrays.asList("jpg", "zip", "gz", "bz2", "z", "7z", "cab", "rar", + "mp3", "mpg")); + + private static final String FILE_NAME_COL_NAME = "FileName"; + private static final String FILE_TYPE_COL_NAME = "FileType"; + + private static final int DATA_TYPE_RAW = 0; + private static final int DATA_TYPE_COMPRESSED = 1; + + private static final int UNKNOWN_HEADER_VAL = 1; + private static final int WRAPPER_HEADER_SIZE = 8; + private static final int CONTENT_HEADER_SIZE = 12; + + private final Column _fileUrlCol; + private final Column _fileNameCol; + private final Column _fileTypeCol; + private final Column _fileDataCol; + private final Column _fileTimeStampCol; + private final Column _fileFlagsCol; + + public AttachmentColumnInfoImpl(Column column, int complexId, + Table typeObjTable, Table flatTable) + throws IOException + { + super(column, complexId, typeObjTable, flatTable); + + Column fileUrlCol = null; + Column fileNameCol = null; + Column fileTypeCol = null; + Column fileDataCol = null; + Column fileTimeStampCol = null; + Column fileFlagsCol = null; + + for(Column col : getTypeColumns()) { + switch(col.getType()) { + case TEXT: + if(FILE_NAME_COL_NAME.equalsIgnoreCase(col.getName())) { + fileNameCol = col; + } else if(FILE_TYPE_COL_NAME.equalsIgnoreCase(col.getName())) { + fileTypeCol = col; + } else { + // if names don't match, assign in order: name, type + if(fileNameCol == null) { + fileNameCol = col; + } else if(fileTypeCol == null) { + fileTypeCol = col; + } + } + break; + case LONG: + fileFlagsCol = col; + break; + case SHORT_DATE_TIME: + fileTimeStampCol = col; + break; + case OLE: + fileDataCol = col; + break; + case MEMO: + fileUrlCol = col; + break; + default: + // ignore + } + } + + _fileUrlCol = fileUrlCol; + _fileNameCol = fileNameCol; + _fileTypeCol = fileTypeCol; + _fileDataCol = fileDataCol; + _fileTimeStampCol = fileTimeStampCol; + _fileFlagsCol = fileFlagsCol; + } + + public Column getFileUrlColumn() { + return _fileUrlCol; + } + + public Column getFileNameColumn() { + return _fileNameCol; + } + + public Column getFileTypeColumn() { + return _fileTypeCol; + } + + public Column getFileDataColumn() { + return _fileDataCol; + } + + public Column getFileTimeStampColumn() { + return _fileTimeStampCol; + } + + public Column getFileFlagsColumn() { + return _fileFlagsCol; + } + + @Override + public ComplexDataType getType() + { + return ComplexDataType.ATTACHMENT; + } + + @Override + protected AttachmentImpl toValue(ComplexValueForeignKey complexValueFk, + Row rawValue) { + ComplexValue.Id id = getValueId(rawValue); + String url = (String)getFileUrlColumn().getRowValue(rawValue); + String name = (String)getFileNameColumn().getRowValue(rawValue); + String type = (String)getFileTypeColumn().getRowValue(rawValue); + Integer flags = (Integer)getFileFlagsColumn().getRowValue(rawValue); + Date ts = (Date)getFileTimeStampColumn().getRowValue(rawValue); + byte[] data = (byte[])getFileDataColumn().getRowValue(rawValue); + + return new AttachmentImpl(id, complexValueFk, url, name, type, null, + ts, flags, data); + } + + @Override + protected Object[] asRow(Object[] row, Attachment attachment) + throws IOException + { + super.asRow(row, attachment); + getFileUrlColumn().setRowValue(row, attachment.getFileUrl()); + getFileNameColumn().setRowValue(row, attachment.getFileName()); + getFileTypeColumn().setRowValue(row, attachment.getFileType()); + getFileFlagsColumn().setRowValue(row, attachment.getFileFlags()); + getFileTimeStampColumn().setRowValue(row, attachment.getFileTimeStamp()); + getFileDataColumn().setRowValue(row, attachment.getEncodedFileData()); + return row; + } + + public static Attachment newAttachment(byte[] data) { + return newAttachment(INVALID_FK, data); + } + + public static Attachment newAttachment(ComplexValueForeignKey complexValueFk, + byte[] data) { + return newAttachment(complexValueFk, null, null, null, data, null, null); + } + + public static Attachment newAttachment( + String url, String name, String type, byte[] data, + Date timeStamp, Integer flags) + { + return newAttachment(INVALID_FK, url, name, type, data, + timeStamp, flags); + } + + public static Attachment newAttachment( + ComplexValueForeignKey complexValueFk, String url, String name, + String type, byte[] data, Date timeStamp, Integer flags) + { + return new AttachmentImpl(INVALID_ID, complexValueFk, url, name, type, + data, timeStamp, flags, null); + } + + public static Attachment newEncodedAttachment(byte[] encodedData) { + return newEncodedAttachment(INVALID_FK, encodedData); + } + + public static Attachment newEncodedAttachment( + ComplexValueForeignKey complexValueFk, byte[] encodedData) { + return newEncodedAttachment(complexValueFk, null, null, null, encodedData, + null, null); + } + + public static Attachment newEncodedAttachment( + String url, String name, String type, byte[] encodedData, + Date timeStamp, Integer flags) + { + return newEncodedAttachment(INVALID_FK, url, name, type, + encodedData, timeStamp, flags); + } + + public static Attachment newEncodedAttachment( + ComplexValueForeignKey complexValueFk, String url, String name, + String type, byte[] encodedData, Date timeStamp, Integer flags) + { + return new AttachmentImpl(INVALID_ID, complexValueFk, url, name, type, + null, timeStamp, flags, encodedData); + } + + + private static class AttachmentImpl extends ComplexValueImpl + implements Attachment + { + private String _url; + private String _name; + private String _type; + private byte[] _data; + private Date _timeStamp; + private Integer _flags; + private byte[] _encodedData; + + private AttachmentImpl(Id id, ComplexValueForeignKey complexValueFk, + String url, String name, String type, byte[] data, + Date timeStamp, Integer flags, byte[] encodedData) + { + super(id, complexValueFk); + _url = url; + _name = name; + _type = type; + _data = data; + _timeStamp = timeStamp; + _flags = flags; + _encodedData = encodedData; + } + + public byte[] getFileData() throws IOException { + if((_data == null) && (_encodedData != null)) { + _data = decodeData(); + } + return _data; + } + + public void setFileData(byte[] data) { + _data = data; + _encodedData = null; + } + + public byte[] getEncodedFileData() throws IOException { + if((_encodedData == null) && (_data != null)) { + _encodedData = encodeData(); + } + return _encodedData; + } + + public void setEncodedFileData(byte[] data) { + _encodedData = data; + _data = null; + } + + public String getFileName() { + return _name; + } + + public void setFileName(String fileName) { + _name = fileName; + } + + public String getFileUrl() { + return _url; + } + + public void setFileUrl(String fileUrl) { + _url = fileUrl; + } + + public String getFileType() { + return _type; + } + + public void setFileType(String fileType) { + _type = fileType; + } + + public Date getFileTimeStamp() { + return _timeStamp; + } + + public void setFileTimeStamp(Date fileTimeStamp) { + _timeStamp = fileTimeStamp; + } + + public Integer getFileFlags() { + return _flags; + } + + public void setFileFlags(Integer fileFlags) { + _flags = fileFlags; + } + + public void update() throws IOException { + getComplexValueForeignKey().updateAttachment(this); + } + + public void delete() throws IOException { + getComplexValueForeignKey().deleteAttachment(this); + } + + @Override + public String toString() { + + String dataStr = null; + try { + dataStr = ByteUtil.toHexString(getFileData()); + } catch(IOException e) { + dataStr = e.toString(); + } + + return "Attachment(" + getComplexValueForeignKey() + "," + getId() + + ") " + getFileUrl() + ", " + getFileName() + ", " + getFileType() + + ", " + getFileTimeStamp() + ", " + getFileFlags() + ", " + + dataStr; + } + + /** + * Decodes the raw attachment file data to get the _actual_ content. + */ + private byte[] decodeData() throws IOException { + + if(_encodedData.length < WRAPPER_HEADER_SIZE) { + // nothing we can do + throw new IOException("Unknown encoded attachment data format"); + } + + // read initial header info + ByteBuffer bb = PageChannel.wrap(_encodedData); + int typeFlag = bb.getInt(); + int dataLen = bb.getInt(); + + DataInputStream contentStream = null; + try { + InputStream bin = new ByteArrayInputStream( + _encodedData, WRAPPER_HEADER_SIZE, + _encodedData.length - WRAPPER_HEADER_SIZE); + + if(typeFlag == DATA_TYPE_RAW) { + // nothing else to do + } else if(typeFlag == DATA_TYPE_COMPRESSED) { + // actual content is deflate compressed + bin = new InflaterInputStream(bin); + } else { + throw new IOException( + "Unknown encoded attachment data type " + typeFlag); +} + + contentStream = new DataInputStream(bin); + + // header is an unknown flag followed by the "file extension" of the + // data (no clue why we need that again since it's already a separate + // field in the attachment table). just skip all of it + byte[] tmpBytes = new byte[4]; + contentStream.readFully(tmpBytes); + int headerLen = PageChannel.wrap(tmpBytes).getInt(); + contentStream.skipBytes(headerLen - 4); + + // calculate actual data length and read it (note, header length + // includes the bytes for the length) + tmpBytes = new byte[dataLen - headerLen]; + contentStream.readFully(tmpBytes); + + return tmpBytes; + + } finally { + if(contentStream != null) { + try { + contentStream.close(); + } catch(IOException e) { + // ignored + } + } + } + } + + /** + * Encodes the actual attachment file data to get the raw, stored format. + */ + private byte[] encodeData() throws IOException { + + // possibly compress data based on file type + String type = ((_type != null) ? _type.toLowerCase() : ""); + boolean shouldCompress = !COMPRESSED_FORMATS.contains(type); + + // encode extension, which ends w/ a null byte + type += '\0'; + ByteBuffer typeBytes = ColumnImpl.encodeUncompressedText( + type, JetFormat.VERSION_12.CHARSET); + int headerLen = typeBytes.remaining() + CONTENT_HEADER_SIZE; + + int dataLen = _data.length; + ByteUtil.ByteStream dataStream = new ByteUtil.ByteStream( + WRAPPER_HEADER_SIZE + headerLen + dataLen); + + // write the wrapper header info + ByteBuffer bb = PageChannel.wrap(dataStream.getBytes()); + bb.putInt(shouldCompress ? DATA_TYPE_COMPRESSED : DATA_TYPE_RAW); + bb.putInt(dataLen + headerLen); + dataStream.skip(WRAPPER_HEADER_SIZE); + + OutputStream contentStream = dataStream; + Deflater deflater = null; + try { + + if(shouldCompress) { + contentStream = new DeflaterOutputStream( + contentStream, deflater = new Deflater(3)); + } + + // write the header w/ the file extension + byte[] tmpBytes = new byte[CONTENT_HEADER_SIZE]; + PageChannel.wrap(tmpBytes) + .putInt(headerLen) + .putInt(UNKNOWN_HEADER_VAL) + .putInt(type.length()); + contentStream.write(tmpBytes); + contentStream.write(typeBytes.array(), 0, typeBytes.remaining()); + + // write the _actual_ contents + contentStream.write(_data); + contentStream.close(); + contentStream = null; + + return dataStream.toByteArray(); + + } finally { + if(contentStream != null) { + try { + contentStream.close(); + } catch(IOException e) { + // ignored + } + } + if(deflater != null) { + deflater.end(); + } + } + } + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/complex/ComplexColumnInfoImpl.java b/src/java/com/healthmarketscience/jackcess/impl/complex/ComplexColumnInfoImpl.java new file mode 100644 index 0000000..83e86a2 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/complex/ComplexColumnInfoImpl.java @@ -0,0 +1,419 @@ +/* +Copyright (c) 2011 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.impl.complex; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.Database; +import com.healthmarketscience.jackcess.IndexCursor; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.RowId; +import com.healthmarketscience.jackcess.RuntimeIOException; +import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.complex.ComplexColumnInfo; +import com.healthmarketscience.jackcess.complex.ComplexDataType; +import com.healthmarketscience.jackcess.complex.ComplexValue; +import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; +import com.healthmarketscience.jackcess.impl.ColumnImpl; +import com.healthmarketscience.jackcess.impl.TableImpl; + +/** + * Base class for the additional information tracked for complex columns. + * + * @author James Ahlborn + */ +public abstract class ComplexColumnInfoImpl + implements ComplexColumnInfo +{ + private static final int INVALID_ID_VALUE = -1; + public static final ComplexValue.Id INVALID_ID = new ComplexValueIdImpl( + INVALID_ID_VALUE, null); + public static final ComplexValueForeignKey INVALID_FK = + new ComplexValueForeignKeyImpl(null, INVALID_ID_VALUE); + + private final Column _column; + private final int _complexTypeId; + private final Table _flatTable; + private final List _typeCols; + private final Column _pkCol; + private final Column _complexValFkCol; + private IndexCursor _complexValIdCursor; + + protected ComplexColumnInfoImpl(Column column, int complexTypeId, + Table typeObjTable, Table flatTable) + throws IOException + { + _column = column; + _complexTypeId = complexTypeId; + _flatTable = flatTable; + + // the flat table has all the "value" columns and 2 extra columns, a + // primary key for each row, and a LONG value which is essentially a + // foreign key to the main table. + List typeCols = new ArrayList(); + List otherCols = new ArrayList(); + diffFlatColumns(typeObjTable, flatTable, typeCols, otherCols); + + _typeCols = Collections.unmodifiableList(typeCols); + + Column pkCol = null; + Column complexValFkCol = null; + for(Column col : otherCols) { + if(col.isAutoNumber()) { + pkCol = col; + } else if(col.getType() == DataType.LONG) { + complexValFkCol = col; + } + } + + if((pkCol == null) || (complexValFkCol == null)) { + throw new IOException("Could not find expected columns in flat table " + + flatTable.getName() + " for complex column with id " + + complexTypeId); + } + _pkCol = pkCol; + _complexValFkCol = complexValFkCol; + } + + public void postTableLoadInit() throws IOException { + // nothing to do in base class + } + + public Column getColumn() { + return _column; + } + + public Database getDatabase() { + return getColumn().getDatabase(); + } + + public Column getPrimaryKeyColumn() { + return _pkCol; + } + + public Column getComplexValueForeignKeyColumn() { + return _complexValFkCol; + } + + protected List getTypeColumns() { + return _typeCols; + } + + public int countValues(int complexValueFk) throws IOException { + return getRawValues(complexValueFk, + Collections.singleton(_complexValFkCol.getName())) + .size(); + } + + public List getRawValues(int complexValueFk) + throws IOException + { + return getRawValues(complexValueFk, null); + } + + private Iterator getComplexValFkIter( + int complexValueFk, Collection columnNames) + throws IOException + { + if(_complexValIdCursor == null) { + _complexValIdCursor = _flatTable.newCursor() + .setIndexByColumns(_complexValFkCol) + .toIndexCursor(); + } + + return _complexValIdCursor.newEntryIterable(complexValueFk) + .setColumnNames(columnNames).iterator(); + } + + public List getRawValues(int complexValueFk, + Collection columnNames) + throws IOException + { + Iterator entryIter = + getComplexValFkIter(complexValueFk, columnNames); + if(!entryIter.hasNext()) { + return Collections.emptyList(); + } + + List values = new ArrayList(); + while(entryIter.hasNext()) { + values.add(entryIter.next()); + } + + return values; + } + + public List getValues(ComplexValueForeignKey complexValueFk) + throws IOException + { + List rawValues = getRawValues(complexValueFk.get()); + if(rawValues.isEmpty()) { + return Collections.emptyList(); + } + + return toValues(complexValueFk, rawValues); + } + + protected List toValues(ComplexValueForeignKey complexValueFk, + List rawValues) + throws IOException + { + List values = new ArrayList(); + for(Row rawValue : rawValues) { + values.add(toValue(complexValueFk, rawValue)); + } + + return values; + } + + public ComplexValue.Id addRawValue(Map rawValue) + throws IOException + { + Object[] row = ((TableImpl)_flatTable).asRowWithRowId(rawValue); + _flatTable.addRow(row); + return getValueId(row); + } + + public ComplexValue.Id addValue(V value) throws IOException { + Object[] row = asRow(newRowArray(), value); + _flatTable.addRow(row); + ComplexValue.Id id = getValueId(row); + value.setId(id); + return id; + } + + public void addValues(Collection values) throws IOException { + for(V value : values) { + addValue(value); + } + } + + public ComplexValue.Id updateRawValue(Row rawValue) throws IOException { + _flatTable.updateRow(rawValue); + return getValueId(rawValue); + } + + public ComplexValue.Id updateValue(V value) throws IOException { + ComplexValue.Id id = value.getId(); + updateRow(id, asRow(newRowArray(), value)); + return id; + } + + public void updateValues(Collection values) throws IOException { + for(V value : values) { + updateValue(value); + } + } + + public void deleteRawValue(Row rawValue) throws IOException { + deleteRow(rawValue.getId()); + } + + public void deleteValue(V value) throws IOException { + deleteRow(value.getId().getRowId()); + } + + public void deleteValues(Collection values) throws IOException { + for(V value : values) { + deleteValue(value); + } + } + + public void deleteAllValues(int complexValueFk) throws IOException { + Iterator entryIter = + getComplexValFkIter(complexValueFk, Collections.emptySet()); + try { + while(entryIter.hasNext()) { + entryIter.next(); + entryIter.remove(); + } + } catch(RuntimeIOException e) { + throw (IOException)e.getCause(); + } + } + + public void deleteAllValues(ComplexValueForeignKey complexValueFk) + throws IOException + { + deleteAllValues(complexValueFk.get()); + } + + private void updateRow(ComplexValue.Id id, Object[] row) throws IOException { + ((TableImpl)_flatTable).updateRow(id.getRowId(), row); + } + + private void deleteRow(RowId rowId) throws IOException { + ((TableImpl)_flatTable).deleteRow(rowId); + } + + protected ComplexValueIdImpl getValueId(Row row) { + int idVal = (Integer)getPrimaryKeyColumn().getRowValue(row); + return new ComplexValueIdImpl(idVal, row.getId()); + } + + protected ComplexValueIdImpl getValueId(Object[] row) { + int idVal = (Integer)getPrimaryKeyColumn().getRowValue(row); + return new ComplexValueIdImpl(idVal, + ((TableImpl)_flatTable).getRowId(row)); + } + + protected Object[] asRow(Object[] row, V value) + throws IOException + { + ComplexValue.Id id = value.getId(); + _pkCol.setRowValue( + row, ((id != INVALID_ID) ? id : Column.AUTO_NUMBER)); + ComplexValueForeignKey cFk = value.getComplexValueForeignKey(); + _complexValFkCol.setRowValue( + row, ((cFk != INVALID_FK) ? cFk : Column.AUTO_NUMBER)); + return row; + } + + private Object[] newRowArray() { + Object[] row = new Object[_flatTable.getColumnCount() + 1]; + row[row.length - 1] = ColumnImpl.RETURN_ROW_ID; + return row; + } + + @Override + public String toString() { + StringBuilder rtn = new StringBuilder(); + rtn.append("\n\t\tComplexType: " + getType()); + rtn.append("\n\t\tComplexTypeId: " + _complexTypeId); + return rtn.toString(); + } + + protected static void diffFlatColumns(Table typeObjTable, + Table flatTable, + List typeCols, + List otherCols) + { + // each "flat"" table has the columns from the "type" table, plus some + // others. separate the "flat" columns into these 2 buckets + for(Column col : flatTable.getColumns()) { + if(((TableImpl)typeObjTable).hasColumn(col.getName())) { + typeCols.add(col); + } else { + otherCols.add(col); + } + } + } + + public abstract ComplexDataType getType(); + + protected abstract V toValue( + ComplexValueForeignKey complexValueFk, + Row rawValues) + throws IOException; + + protected static abstract class ComplexValueImpl implements ComplexValue + { + private Id _id; + private ComplexValueForeignKey _complexValueFk; + + protected ComplexValueImpl(Id id, ComplexValueForeignKey complexValueFk) { + _id = id; + _complexValueFk = complexValueFk; + } + + public Id getId() { + return _id; + } + + public void setId(Id id) { + if(_id == id) { + // harmless, ignore + return; + } + if(_id != INVALID_ID) { + throw new IllegalStateException("id may not be reset"); + } + _id = id; + } + + public ComplexValueForeignKey getComplexValueForeignKey() { + return _complexValueFk; + } + + public void setComplexValueForeignKey(ComplexValueForeignKey complexValueFk) + { + if(_complexValueFk == complexValueFk) { + // harmless, ignore + return; + } + if(_complexValueFk != INVALID_FK) { + throw new IllegalStateException("complexValueFk may not be reset"); + } + _complexValueFk = complexValueFk; + } + + public Column getColumn() { + return _complexValueFk.getColumn(); + } + + @Override + public int hashCode() { + return ((_id.get() * 37) ^ _complexValueFk.hashCode()); + } + + @Override + public boolean equals(Object o) { + return ((this == o) || + ((o != null) && (getClass() == o.getClass()) && + (_id == ((ComplexValueImpl)o)._id) && + _complexValueFk.equals(((ComplexValueImpl)o)._complexValueFk))); + } + } + + /** + * Implementation of ComplexValue.Id. + */ + private static final class ComplexValueIdImpl extends ComplexValue.Id + { + private static final long serialVersionUID = 20130318L; + + private final int _value; + private final RowId _rowId; + + protected ComplexValueIdImpl(int value, RowId rowId) { + _value = value; + _rowId = rowId; + } + + @Override + public int get() { + return _value; + } + + @Override + public RowId getRowId() { + return _rowId; + } + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/complex/ComplexValueForeignKeyImpl.java b/src/java/com/healthmarketscience/jackcess/impl/complex/ComplexValueForeignKeyImpl.java new file mode 100644 index 0000000..4e0cc0c --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/complex/ComplexValueForeignKeyImpl.java @@ -0,0 +1,289 @@ +/* +Copyright (c) 2011 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.impl.complex; + +import java.io.IOException; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.complex.Attachment; +import com.healthmarketscience.jackcess.complex.AttachmentColumnInfo; +import com.healthmarketscience.jackcess.complex.ComplexColumnInfo; +import com.healthmarketscience.jackcess.complex.ComplexDataType; +import com.healthmarketscience.jackcess.complex.ComplexValue; +import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; +import com.healthmarketscience.jackcess.complex.MultiValueColumnInfo; +import com.healthmarketscience.jackcess.complex.SingleValue; +import com.healthmarketscience.jackcess.complex.UnsupportedColumnInfo; +import com.healthmarketscience.jackcess.complex.UnsupportedValue; +import com.healthmarketscience.jackcess.complex.Version; +import com.healthmarketscience.jackcess.complex.VersionHistoryColumnInfo; + +/** + * Value which is returned for a complex column. This value corresponds to a + * foreign key in a secondary table which contains the actual complex data for + * this row (which could be 0 or more complex values for a given row). This + * class contains various convenience methods for interacting with the actual + * complex values. + *

+ * This class will cache the associated complex values returned from one of + * the lookup methods. The various modification methods will clear this cache + * automatically. The {@link #reset} method may be called manually to clear + * this internal cache. + * + * @author James Ahlborn + */ +public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey +{ + private static final long serialVersionUID = 20110805L; + + private transient final Column _column; + private final int _value; + private transient List _values; + + public ComplexValueForeignKeyImpl(Column column, int value) { + _column = column; + _value = value; + } + + @Override + public int get() { + return _value; + } + + @Override + public Column getColumn() { + return _column; + } + + @Override + public ComplexDataType getComplexType() { + return getComplexInfo().getType(); + } + + protected ComplexColumnInfo getComplexInfo() { + return _column.getComplexInfo(); + } + + protected VersionHistoryColumnInfo getVersionInfo() { + return (VersionHistoryColumnInfo)getComplexInfo(); + } + + protected AttachmentColumnInfo getAttachmentInfo() { + return (AttachmentColumnInfo)getComplexInfo(); + } + + protected MultiValueColumnInfo getMultiValueInfo() { + return (MultiValueColumnInfo)getComplexInfo(); + } + + protected UnsupportedColumnInfo getUnsupportedInfo() { + return (UnsupportedColumnInfo)getComplexInfo(); + } + + @Override + public int countValues() throws IOException { + return getComplexInfo().countValues(get()); + } + + public List getRawValues() throws IOException { + return getComplexInfo().getRawValues(get()); + } + + @Override + public List getValues() throws IOException { + if(_values == null) { + _values = getComplexInfo().getValues(this); + } + return _values; + } + + @Override + @SuppressWarnings("unchecked") + public List getVersions() throws IOException { + if(getComplexType() != ComplexDataType.VERSION_HISTORY) { + throw new UnsupportedOperationException(); + } + return (List)getValues(); + } + + @Override + @SuppressWarnings("unchecked") + public List getAttachments() throws IOException { + if(getComplexType() != ComplexDataType.ATTACHMENT) { + throw new UnsupportedOperationException(); + } + return (List)getValues(); + } + + @Override + @SuppressWarnings("unchecked") + public List getMultiValues() throws IOException { + if(getComplexType() != ComplexDataType.MULTI_VALUE) { + throw new UnsupportedOperationException(); + } + return (List)getValues(); + } + + @Override + @SuppressWarnings("unchecked") + public List getUnsupportedValues() throws IOException { + if(getComplexType() != ComplexDataType.UNSUPPORTED) { + throw new UnsupportedOperationException(); + } + return (List)getValues(); + } + + @Override + public void reset() { + // discard any cached values + _values = null; + } + + @Override + public Version addVersion(String value) throws IOException { + return addVersion(value, new Date()); + } + + @Override + public Version addVersion(String value, Date modifiedDate) throws IOException { + reset(); + Version v = VersionHistoryColumnInfoImpl.newVersion(this, value, modifiedDate); + getVersionInfo().addValue(v); + return v; + } + + @Override + public Attachment addAttachment(byte[] data) throws IOException { + return addAttachment(null, null, null, data, null, null); + } + + @Override + public Attachment addAttachment( + String url, String name, String type, byte[] data, + Date timeStamp, Integer flags) + throws IOException + { + reset(); + Attachment a = AttachmentColumnInfoImpl.newAttachment( + this, url, name, type, data, timeStamp, flags); + getAttachmentInfo().addValue(a); + return a; + } + + @Override + public Attachment addEncodedAttachment(byte[] encodedData) + throws IOException + { + return addEncodedAttachment(null, null, null, encodedData, null, null); + } + + @Override + public Attachment addEncodedAttachment( + String url, String name, String type, byte[] encodedData, + Date timeStamp, Integer flags) + throws IOException + { + reset(); + Attachment a = AttachmentColumnInfoImpl.newEncodedAttachment( + this, url, name, type, encodedData, timeStamp, flags); + getAttachmentInfo().addValue(a); + return a; + } + + @Override + public Attachment updateAttachment(Attachment attachment) throws IOException { + reset(); + getAttachmentInfo().updateValue(attachment); + return attachment; + } + + @Override + public Attachment deleteAttachment(Attachment attachment) throws IOException { + reset(); + getAttachmentInfo().deleteValue(attachment); + return attachment; + } + + @Override + public SingleValue addMultiValue(Object value) throws IOException { + reset(); + SingleValue v = MultiValueColumnInfoImpl.newSingleValue(this, value); + getMultiValueInfo().addValue(v); + return v; + } + + @Override + public SingleValue updateMultiValue(SingleValue value) throws IOException { + reset(); + getMultiValueInfo().updateValue(value); + return value; + } + + @Override + public SingleValue deleteMultiValue(SingleValue value) throws IOException { + reset(); + getMultiValueInfo().deleteValue(value); + return value; + } + + @Override + public UnsupportedValue addUnsupportedValue(Map values) + throws IOException + { + reset(); + UnsupportedValue v = UnsupportedColumnInfoImpl.newValue(this, values); + getUnsupportedInfo().addValue(v); + return v; + } + + @Override + public UnsupportedValue updateUnsupportedValue(UnsupportedValue value) + throws IOException + { + reset(); + getUnsupportedInfo().updateValue(value); + return value; + } + + @Override + public UnsupportedValue deleteUnsupportedValue(UnsupportedValue value) + throws IOException + { + reset(); + getUnsupportedInfo().deleteValue(value); + return value; + } + + @Override + public void deleteAllValues() throws IOException { + reset(); + getComplexInfo().deleteAllValues(this); + } + + @Override + public boolean equals(Object o) { + return(super.equals(o) && + (_column == ((ComplexValueForeignKeyImpl)o)._column)); + } +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/complex/MultiValueColumnInfoImpl.java b/src/java/com/healthmarketscience/jackcess/impl/complex/MultiValueColumnInfoImpl.java new file mode 100644 index 0000000..5f33688 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/complex/MultiValueColumnInfoImpl.java @@ -0,0 +1,125 @@ +/* +Copyright (c) 2011 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.impl.complex; + +import java.io.IOException; + +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.complex.ComplexDataType; +import com.healthmarketscience.jackcess.complex.ComplexValue; +import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; +import com.healthmarketscience.jackcess.complex.MultiValueColumnInfo; +import com.healthmarketscience.jackcess.complex.SingleValue; + +/** + * Complex column info for a column holding multiple simple values per row. + * + * @author James Ahlborn + */ +public class MultiValueColumnInfoImpl extends ComplexColumnInfoImpl + implements MultiValueColumnInfo +{ + private final Column _valueCol; + + public MultiValueColumnInfoImpl(Column column, int complexId, + Table typeObjTable, Table flatTable) + throws IOException + { + super(column, complexId, typeObjTable, flatTable); + + _valueCol = getTypeColumns().get(0); + } + + @Override + public ComplexDataType getType() + { + return ComplexDataType.MULTI_VALUE; + } + + public Column getValueColumn() { + return _valueCol; + } + + @Override + protected SingleValueImpl toValue( + ComplexValueForeignKey complexValueFk, + Row rawValue) + { + ComplexValue.Id id = getValueId(rawValue); + Object value = getValueColumn().getRowValue(rawValue); + + return new SingleValueImpl(id, complexValueFk, value); + } + + @Override + protected Object[] asRow(Object[] row, SingleValue value) throws IOException { + super.asRow(row, value); + getValueColumn().setRowValue(row, value.get()); + return row; + } + + public static SingleValue newSingleValue(Object value) { + return newSingleValue(INVALID_FK, value); + } + + public static SingleValue newSingleValue( + ComplexValueForeignKey complexValueFk, Object value) { + return new SingleValueImpl(INVALID_ID, complexValueFk, value); + } + + + private static class SingleValueImpl extends ComplexValueImpl + implements SingleValue + { + private Object _value; + + private SingleValueImpl(Id id, ComplexValueForeignKey complexValueFk, + Object value) + { + super(id, complexValueFk); + _value = value; + } + + public Object get() { + return _value; + } + + public void set(Object value) { + _value = value; + } + + public void update() throws IOException { + getComplexValueForeignKey().updateMultiValue(this); + } + + public void delete() throws IOException { + getComplexValueForeignKey().deleteMultiValue(this); + } + + @Override + public String toString() + { + return "SingleValue(" + getComplexValueForeignKey() + "," + getId() + + ") " + get(); + } + } +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/complex/UnsupportedColumnInfoImpl.java b/src/java/com/healthmarketscience/jackcess/impl/complex/UnsupportedColumnInfoImpl.java new file mode 100644 index 0000000..d84f050 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/complex/UnsupportedColumnInfoImpl.java @@ -0,0 +1,141 @@ +/* +Copyright (c) 2011 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.impl.complex; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.complex.ComplexDataType; +import com.healthmarketscience.jackcess.complex.ComplexValue; +import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; +import com.healthmarketscience.jackcess.complex.UnsupportedColumnInfo; +import com.healthmarketscience.jackcess.complex.UnsupportedValue; + +/** + * Complex column info for an unsupported complex type. + * + * @author James Ahlborn + */ +public class UnsupportedColumnInfoImpl + extends ComplexColumnInfoImpl + implements UnsupportedColumnInfo +{ + + public UnsupportedColumnInfoImpl(Column column, int complexId, + Table typeObjTable, Table flatTable) + throws IOException + { + super(column, complexId, typeObjTable, flatTable); + } + + public List getValueColumns() { + return getTypeColumns(); + } + + @Override + public ComplexDataType getType() + { + return ComplexDataType.UNSUPPORTED; + } + + @Override + protected UnsupportedValueImpl toValue( + ComplexValueForeignKey complexValueFk, + Row rawValue) + { + ComplexValue.Id id = getValueId(rawValue); + + Map values = new LinkedHashMap(); + for(Column col : getValueColumns()) { + col.setRowValue(values, col.getRowValue(rawValue)); + } + + return new UnsupportedValueImpl(id, complexValueFk, values); + } + + @Override + protected Object[] asRow(Object[] row, UnsupportedValue value) + throws IOException + { + super.asRow(row, value); + + Map values = value.getValues(); + for(Column col : getValueColumns()) { + col.setRowValue(row, col.getRowValue(values)); + } + + return row; + } + + public static UnsupportedValue newValue(Map values) { + return newValue(INVALID_FK, values); + } + + public static UnsupportedValue newValue( + ComplexValueForeignKey complexValueFk, Map values) { + return new UnsupportedValueImpl(INVALID_ID, complexValueFk, + new LinkedHashMap(values)); + } + + private static class UnsupportedValueImpl extends ComplexValueImpl + implements UnsupportedValue + { + private Map _values; + + private UnsupportedValueImpl(Id id, ComplexValueForeignKey complexValueFk, + Map values) + { + super(id, complexValueFk); + _values = values; + } + + public Map getValues() { + return _values; + } + + public Object get(String columnName) { + return getValues().get(columnName); + } + + public void set(String columnName, Object value) { + getValues().put(columnName, value); + } + + public void update() throws IOException { + getComplexValueForeignKey().updateUnsupportedValue(this); + } + + public void delete() throws IOException { + getComplexValueForeignKey().deleteUnsupportedValue(this); + } + + @Override + public String toString() + { + return "UnsupportedValue(" + getComplexValueForeignKey() + "," + getId() + + ") " + getValues(); + } + } +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/complex/VersionHistoryColumnInfoImpl.java b/src/java/com/healthmarketscience/jackcess/impl/complex/VersionHistoryColumnInfoImpl.java new file mode 100644 index 0000000..c08d1f1 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/complex/VersionHistoryColumnInfoImpl.java @@ -0,0 +1,224 @@ +/* +Copyright (c) 2011 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.impl.complex; + +import java.io.IOException; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.complex.ComplexDataType; +import com.healthmarketscience.jackcess.complex.ComplexValue; +import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; +import com.healthmarketscience.jackcess.complex.Version; +import com.healthmarketscience.jackcess.complex.VersionHistoryColumnInfo; +import com.healthmarketscience.jackcess.impl.ColumnImpl; + +/** + * Complex column info for a column which tracking the version history of an + * "append only" memo column. + *

+ * Note, the strongly typed update/delete methods are not supported for + * version history columns (the data is supposed to be immutable). That said, + * the "raw" update/delete methods are supported for those that really + * want to muck with the version history data. + * + * @author James Ahlborn + */ +public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl + implements VersionHistoryColumnInfo +{ + private final Column _valueCol; + private final Column _modifiedCol; + + public VersionHistoryColumnInfoImpl(Column column, int complexId, + Table typeObjTable, Table flatTable) + throws IOException + { + super(column, complexId, typeObjTable, flatTable); + + Column valueCol = null; + Column modifiedCol = null; + for(Column col : getTypeColumns()) { + switch(col.getType()) { + case SHORT_DATE_TIME: + modifiedCol = col; + break; + case MEMO: + valueCol = col; + break; + default: + // ignore + } + } + + _valueCol = valueCol; + _modifiedCol = modifiedCol; + } + + @Override + public void postTableLoadInit() throws IOException { + super.postTableLoadInit(); + + // link up with the actual versioned column. it should have the same name + // as the "value" column in the type table. + Column versionedCol = getColumn().getTable().getColumn( + getValueColumn().getName()); + ((ColumnImpl)versionedCol).setVersionHistoryColumn((ColumnImpl)getColumn()); + } + + public Column getValueColumn() { + return _valueCol; + } + + public Column getModifiedDateColumn() { + return _modifiedCol; + } + + @Override + public ComplexDataType getType() { + return ComplexDataType.VERSION_HISTORY; + } + + @Override + public ComplexValue.Id updateValue(Version value) throws IOException { + throw new UnsupportedOperationException( + "This column does not support value updates"); + } + + @Override + public void deleteValue(Version value) throws IOException { + throw new UnsupportedOperationException( + "This column does not support value deletes"); + } + + @Override + public void deleteAllValues(int complexValueFk) throws IOException { + throw new UnsupportedOperationException( + "This column does not support value deletes"); + } + + @Override + protected List toValues(ComplexValueForeignKey complexValueFk, + List rawValues) + throws IOException + { + List versions = super.toValues(complexValueFk, rawValues); + + // order versions newest to oldest + Collections.sort(versions); + + return versions; + } + + @Override + protected VersionImpl toValue(ComplexValueForeignKey complexValueFk, + Row rawValue) { + ComplexValue.Id id = getValueId(rawValue); + String value = (String)getValueColumn().getRowValue(rawValue); + Date modifiedDate = (Date)getModifiedDateColumn().getRowValue(rawValue); + + return new VersionImpl(id, complexValueFk, value, modifiedDate); + } + + @Override + protected Object[] asRow(Object[] row, Version version) throws IOException { + super.asRow(row, version); + getValueColumn().setRowValue(row, version.getValue()); + getModifiedDateColumn().setRowValue(row, version.getModifiedDate()); + return row; + } + + public static Version newVersion(String value, Date modifiedDate) { + return newVersion(INVALID_FK, value, modifiedDate); + } + + public static Version newVersion(ComplexValueForeignKey complexValueFk, + String value, Date modifiedDate) { + return new VersionImpl(INVALID_ID, complexValueFk, value, modifiedDate); + } + + + private static class VersionImpl extends ComplexValueImpl implements Version + { + private final String _value; + private final Date _modifiedDate; + + private VersionImpl(Id id, ComplexValueForeignKey complexValueFk, + String value, Date modifiedDate) + { + super(id, complexValueFk); + _value = value; + _modifiedDate = modifiedDate; + } + + public String getValue() { + return _value; + } + + public Date getModifiedDate() { + return _modifiedDate; + } + + public int compareTo(Version o) { + Date d1 = getModifiedDate(); + Date d2 = o.getModifiedDate(); + + // sort by descending date (newest/greatest first) + int cmp = d2.compareTo(d1); + if(cmp != 0) { + return cmp; + } + + // use id, then complexValueFk to break ties (although we really + // shouldn't be comparing across different columns) + int id1 = getId().get(); + int id2 = o.getId().get(); + if(id1 != id2) { + return ((id1 > id2) ? -1 : 1); + } + id1 = getComplexValueForeignKey().get(); + id2 = o.getComplexValueForeignKey().get(); + return ((id1 > id2) ? -1 : + ((id1 < id2) ? 1 : 0)); + } + + public void update() throws IOException { + throw new UnsupportedOperationException( + "This column does not support value updates"); + } + + public void delete() throws IOException { + throw new UnsupportedOperationException( + "This column does not support value deletes"); + } + + @Override + public String toString() + { + return "Version(" + getComplexValueForeignKey() + "," + getId() + ") " + + getModifiedDate() + ", " + getValue(); + } + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/query/AppendQueryImpl.java b/src/java/com/healthmarketscience/jackcess/impl/query/AppendQueryImpl.java new file mode 100644 index 0000000..1177d6e --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/query/AppendQueryImpl.java @@ -0,0 +1,92 @@ +/* +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.impl.query; + +import java.util.List; + +import static com.healthmarketscience.jackcess.impl.query.QueryFormat.*; +import com.healthmarketscience.jackcess.query.AppendQuery; + + +/** + * Concrete Query subclass which represents an append query, e.g.: + * {@code INSERT INTO

VALUES ()} + * + * @author James Ahlborn + */ +public class AppendQueryImpl extends BaseSelectQueryImpl implements AppendQuery +{ + + public AppendQueryImpl(String name, List rows, int objectId) { + super(name, rows, objectId, Type.APPEND); + } + + public String getTargetTable() { + return getTypeRow().name1; + } + + public String getRemoteDbPath() { + return getTypeRow().name2; + } + + public String getRemoteDbType() { + return getTypeRow().expression; + } + + protected List getValueRows() { + return filterRowsByFlag(super.getColumnRows(), APPEND_VALUE_FLAG); + } + + @Override + protected List getColumnRows() { + return filterRowsByNotFlag(super.getColumnRows(), APPEND_VALUE_FLAG); + } + + public List getValues() { + return new RowFormatter(getValueRows()) { + @Override protected void format(StringBuilder builder, Row row) { + builder.append(row.expression); + } + }.format(); + } + + @Override + protected void toSQLString(StringBuilder builder) + { + builder.append("INSERT INTO ").append(getTargetTable()); + toRemoteDb(builder, getRemoteDbPath(), getRemoteDbType()); + builder.append(NEWLINE); + List values = getValues(); + if(!values.isEmpty()) { + builder.append("VALUES (").append(values).append(')'); + } else { + toSQLSelectString(builder, true); + } + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/query/BaseSelectQueryImpl.java b/src/java/com/healthmarketscience/jackcess/impl/query/BaseSelectQueryImpl.java new file mode 100644 index 0000000..0fddb59 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/query/BaseSelectQueryImpl.java @@ -0,0 +1,177 @@ +/* +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.impl.query; + +import java.util.List; + +import static com.healthmarketscience.jackcess.impl.query.QueryFormat.*; +import com.healthmarketscience.jackcess.query.BaseSelectQuery; + + +/** + * Base class for queries which represent some form of SELECT statement. + * + * @author James Ahlborn + */ +public abstract class BaseSelectQueryImpl extends QueryImpl + implements BaseSelectQuery +{ + + protected BaseSelectQueryImpl(String name, List rows, int objectId, + Type type) { + super(name, rows, objectId, type); + } + + protected void toSQLSelectString(StringBuilder builder, + boolean useSelectPrefix) + { + if(useSelectPrefix) { + builder.append("SELECT "); + String selectType = getSelectType(); + if(!DEFAULT_TYPE.equals(selectType)) { + builder.append(selectType).append(' '); + } + } + + builder.append(getSelectColumns()); + toSelectInto(builder); + + List fromTables = getFromTables(); + if(!fromTables.isEmpty()) { + builder.append(NEWLINE).append("FROM ").append(fromTables); + toRemoteDb(builder, getFromRemoteDbPath(), getFromRemoteDbType()); + } + + String whereExpr = getWhereExpression(); + if(whereExpr != null) { + builder.append(NEWLINE).append("WHERE ").append(whereExpr); + } + + List groupings = getGroupings(); + if(!groupings.isEmpty()) { + builder.append(NEWLINE).append("GROUP BY ").append(groupings); + } + + String havingExpr = getHavingExpression(); + if(havingExpr != null) { + builder.append(NEWLINE).append("HAVING ").append(havingExpr); + } + + List orderings = getOrderings(); + if(!orderings.isEmpty()) { + builder.append(NEWLINE).append("ORDER BY ").append(orderings); + } + } + + public String getSelectType() + { + if(hasFlag(DISTINCT_SELECT_TYPE)) { + return "DISTINCT"; + } + + if(hasFlag(DISTINCT_ROW_SELECT_TYPE)) { + return "DISTINCTROW"; + } + + if(hasFlag(TOP_SELECT_TYPE)) { + StringBuilder builder = new StringBuilder(); + builder.append("TOP ").append(getFlagRow().name1); + if(hasFlag(PERCENT_SELECT_TYPE)) { + builder.append(" PERCENT"); + } + return builder.toString(); + } + + return DEFAULT_TYPE; + } + + public List getSelectColumns() + { + List result = (new RowFormatter(getColumnRows()) { + @Override protected void format(StringBuilder builder, Row row) { + // note column expression are always quoted appropriately + builder.append(row.expression); + toAlias(builder, row.name1); + } + }).format(); + if(hasFlag(SELECT_STAR_SELECT_TYPE)) { + result.add("*"); + } + return result; + } + + protected void toSelectInto(StringBuilder builder) + { + // base does nothing + } + + @Override + public List getFromTables() + { + return super.getFromTables(); + } + + @Override + public String getFromRemoteDbPath() + { + return super.getFromRemoteDbPath(); + } + + @Override + public String getFromRemoteDbType() + { + return super.getFromRemoteDbType(); + } + + @Override + public String getWhereExpression() + { + return super.getWhereExpression(); + } + + public List getGroupings() + { + return (new RowFormatter(getGroupByRows()) { + @Override protected void format(StringBuilder builder, Row row) { + builder.append(row.expression); + } + }).format(); + } + + public String getHavingExpression() + { + return getHavingRow().expression; + } + + @Override + public List getOrderings() + { + return super.getOrderings(); + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/query/CrossTabQueryImpl.java b/src/java/com/healthmarketscience/jackcess/impl/query/CrossTabQueryImpl.java new file mode 100644 index 0000000..d4b7e28 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/query/CrossTabQueryImpl.java @@ -0,0 +1,100 @@ +/* +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.impl.query; + +import java.util.List; + +import static com.healthmarketscience.jackcess.impl.query.QueryFormat.*; +import com.healthmarketscience.jackcess.query.CrossTabQuery; + + +/** + * Concrete Query subclass which represents a crosstab/pivot query, e.g.: + * {@code TRANSFORM SELECT PIVOT } + * + * @author James Ahlborn + */ +public class CrossTabQueryImpl extends BaseSelectQueryImpl + implements CrossTabQuery +{ + + public CrossTabQueryImpl(String name, List rows, int objectId) { + super(name, rows, objectId, Type.CROSS_TAB); + } + + protected Row getTransformRow() { + return getUniqueRow( + filterRowsByNotFlag(super.getColumnRows(), + (short)(CROSSTAB_PIVOT_FLAG | + CROSSTAB_NORMAL_FLAG))); + } + + @Override + protected List getColumnRows() { + return filterRowsByFlag(super.getColumnRows(), CROSSTAB_NORMAL_FLAG); + } + + @Override + protected List getGroupByRows() { + return filterRowsByFlag(super.getGroupByRows(), CROSSTAB_NORMAL_FLAG); + } + + protected Row getPivotRow() { + return getUniqueRow(filterRowsByFlag(super.getColumnRows(), + CROSSTAB_PIVOT_FLAG)); + } + + public String getTransformExpression() { + Row row = getTransformRow(); + if(row.expression == null) { + return null; + } + // note column expression are always quoted appropriately + StringBuilder builder = new StringBuilder(row.expression); + return toAlias(builder, row.name1).toString(); + } + + public String getPivotExpression() { + return getPivotRow().expression; + } + + @Override + protected void toSQLString(StringBuilder builder) + { + String transformExpr = getTransformExpression(); + if(transformExpr != null) { + builder.append("TRANSFORM ").append(transformExpr).append(NEWLINE); + } + + toSQLSelectString(builder, true); + + builder.append(NEWLINE).append("PIVOT ") + .append(getPivotExpression()); + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/query/DataDefinitionQueryImpl.java b/src/java/com/healthmarketscience/jackcess/impl/query/DataDefinitionQueryImpl.java new file mode 100644 index 0000000..27ee5ab --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/query/DataDefinitionQueryImpl.java @@ -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.impl.query; + +import java.util.List; +import com.healthmarketscience.jackcess.query.DataDefinitionQuery; + + +/** + * Concrete Query subclass which represents a DDL query. + * + * @author James Ahlborn + */ +public class DataDefinitionQueryImpl extends QueryImpl + implements DataDefinitionQuery +{ + + public DataDefinitionQueryImpl(String name, List rows, int objectId) { + super(name, rows, objectId, Type.DATA_DEFINITION); + } + + public String getDDLString() { + return getTypeRow().expression; + } + + @Override + protected boolean supportsStandardClauses() { + return false; + } + + @Override + protected void toSQLString(StringBuilder builder) + { + String ddl = getDDLString(); + if(ddl != null) { + builder.append(ddl); + } + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/query/DeleteQueryImpl.java b/src/java/com/healthmarketscience/jackcess/impl/query/DeleteQueryImpl.java new file mode 100644 index 0000000..8c96b6d --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/query/DeleteQueryImpl.java @@ -0,0 +1,54 @@ +/* +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.impl.query; + +import java.util.List; +import com.healthmarketscience.jackcess.query.DeleteQuery; + + +/** + * Concrete Query subclass which represents a delete query, e.g.: + * {@code DELETE * FROM
WHERE } + * + * @author James Ahlborn + */ +public class DeleteQueryImpl extends BaseSelectQueryImpl implements DeleteQuery +{ + + public DeleteQueryImpl(String name, List rows, int objectId) { + super(name, rows, objectId, Type.DELETE); + } + + @Override + protected void toSQLString(StringBuilder builder) + { + builder.append("DELETE "); + toSQLSelectString(builder, false); + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/query/MakeTableQueryImpl.java b/src/java/com/healthmarketscience/jackcess/impl/query/MakeTableQueryImpl.java new file mode 100644 index 0000000..29e402b --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/query/MakeTableQueryImpl.java @@ -0,0 +1,73 @@ +/* +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.impl.query; + +import java.util.List; +import com.healthmarketscience.jackcess.query.MakeTableQuery; + + +/** + * Concrete Query subclass which represents an table creation query, e.g.: + * {@code SELECT INTO } + * + * @author James Ahlborn + */ +public class MakeTableQueryImpl extends BaseSelectQueryImpl + implements MakeTableQuery +{ + + public MakeTableQueryImpl(String name, List rows, int objectId) { + super(name, rows, objectId, Type.MAKE_TABLE); + } + + public String getTargetTable() { + return getTypeRow().name1; + } + + public String getRemoteDbPath() { + return getTypeRow().name2; + } + + public String getRemoteDbType() { + return getTypeRow().expression; + } + + @Override + protected void toSelectInto(StringBuilder builder) + { + builder.append(" INTO ").append(getTargetTable()); + toRemoteDb(builder, getRemoteDbPath(), getRemoteDbType()); + } + + @Override + protected void toSQLString(StringBuilder builder) + { + toSQLSelectString(builder, true); + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/query/PassthroughQueryImpl.java b/src/java/com/healthmarketscience/jackcess/impl/query/PassthroughQueryImpl.java new file mode 100644 index 0000000..af67e2c --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/query/PassthroughQueryImpl.java @@ -0,0 +1,69 @@ +/* +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.impl.query; + +import java.util.List; +import com.healthmarketscience.jackcess.query.PassthroughQuery; + + +/** + * Concrete Query subclass which represents a query which will be executed via + * ODBC. + * + * @author James Ahlborn + */ +public class PassthroughQueryImpl extends QueryImpl implements PassthroughQuery +{ + + public PassthroughQueryImpl(String name, List rows, int objectId) { + super(name, rows, objectId, Type.PASSTHROUGH); + } + + public String getConnectionString() { + return getTypeRow().name1; + } + + public String getPassthroughString() { + return getTypeRow().expression; + } + + @Override + protected boolean supportsStandardClauses() { + return false; + } + + @Override + protected void toSQLString(StringBuilder builder) + { + String pt = getPassthroughString(); + if(pt != null) { + builder.append(pt); + } + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/query/QueryFormat.java b/src/java/com/healthmarketscience/jackcess/impl/query/QueryFormat.java similarity index 99% rename from src/java/com/healthmarketscience/jackcess/query/QueryFormat.java rename to src/java/com/healthmarketscience/jackcess/impl/query/QueryFormat.java index 89d586e..83064c0 100644 --- a/src/java/com/healthmarketscience/jackcess/query/QueryFormat.java +++ b/src/java/com/healthmarketscience/jackcess/impl/query/QueryFormat.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess.query; +package com.healthmarketscience.jackcess.impl.query; import java.util.HashMap; import java.util.Map; diff --git a/src/java/com/healthmarketscience/jackcess/impl/query/QueryImpl.java b/src/java/com/healthmarketscience/jackcess/impl/query/QueryImpl.java new file mode 100644 index 0000000..114933a --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/query/QueryImpl.java @@ -0,0 +1,721 @@ +/* +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.impl.query; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.healthmarketscience.jackcess.RowId; +import com.healthmarketscience.jackcess.query.Query; +import com.healthmarketscience.jackcess.impl.RowIdImpl; +import com.healthmarketscience.jackcess.impl.RowImpl; +import static com.healthmarketscience.jackcess.impl.query.QueryFormat.*; +import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +/** + * Base class for classes which encapsulate information about an Access query. + * The {@link #toSQLString()} method can be used to convert this object into + * the actual SQL string which this query data represents. + * + * @author James Ahlborn + */ +public abstract class QueryImpl implements Query +{ + protected static final Log LOG = LogFactory.getLog(QueryImpl.class); + + private static final Row EMPTY_ROW = new Row(); + + private final String _name; + private final List _rows; + private final int _objectId; + private final Type _type; + + protected QueryImpl(String name, List rows, int objectId, Type type) { + _name = name; + _rows = rows; + _objectId = objectId; + _type = type; + + if(type != Type.UNKNOWN) { + short foundType = getShortValue(getQueryType(rows), + _type.getValue()); + if(foundType != _type.getValue()) { + throw new IllegalStateException("Unexpected query type " + foundType); + } + } + } + + /** + * Returns the name of the query. + */ + public String getName() { + return _name; + } + + /** + * Returns the type of the query. + */ + public Type getType() { + return _type; + } + + /** + * Returns the unique object id of the query. + */ + public int getObjectId() { + return _objectId; + } + + public int getObjectFlag() { + return getType().getObjectFlag(); + } + + /** + * Returns the rows from the system query table from which the query + * information was derived. + */ + public List getRows() { + return _rows; + } + + protected List getRowsByAttribute(Byte attribute) { + return getRowsByAttribute(getRows(), attribute); + } + + protected Row getRowByAttribute(Byte attribute) { + return getUniqueRow(getRowsByAttribute(getRows(), attribute)); + } + + public Row getTypeRow() { + return getRowByAttribute(TYPE_ATTRIBUTE); + } + + protected List getParameterRows() { + return getRowsByAttribute(PARAMETER_ATTRIBUTE); + } + + protected Row getFlagRow() { + return getRowByAttribute(FLAG_ATTRIBUTE); + } + + protected Row getRemoteDatabaseRow() { + return getRowByAttribute(REMOTEDB_ATTRIBUTE); + } + + protected List getTableRows() { + return getRowsByAttribute(TABLE_ATTRIBUTE); + } + + protected List getColumnRows() { + return getRowsByAttribute(COLUMN_ATTRIBUTE); + } + + protected List getJoinRows() { + return getRowsByAttribute(JOIN_ATTRIBUTE); + } + + protected Row getWhereRow() { + return getRowByAttribute(WHERE_ATTRIBUTE); + } + + protected List getGroupByRows() { + return getRowsByAttribute(GROUPBY_ATTRIBUTE); + } + + protected Row getHavingRow() { + return getRowByAttribute(HAVING_ATTRIBUTE); + } + + protected List getOrderByRows() { + return getRowsByAttribute(ORDERBY_ATTRIBUTE); + } + + protected abstract void toSQLString(StringBuilder builder); + + protected void toSQLParameterString(StringBuilder builder) { + // handle any parameters + List params = getParameters(); + if(!params.isEmpty()) { + builder.append("PARAMETERS ").append(params) + .append(';').append(NEWLINE); + } + } + + public List getParameters() + { + return (new RowFormatter(getParameterRows()) { + @Override protected void format(StringBuilder builder, Row row) { + String typeName = PARAM_TYPE_MAP.get(row.flag); + if(typeName == null) { + throw new IllegalStateException("Unknown param type " + row.flag); + } + + builder.append(row.name1).append(' ').append(typeName); + if((TEXT_FLAG.equals(row.flag)) && (getIntValue(row.extra, 0) > 0)) { + builder.append('(').append(row.extra).append(')'); + } + } + }).format(); + } + + protected List getFromTables() + { + List joinExprs = new ArrayList(); + for(Row table : getTableRows()) { + StringBuilder builder = new StringBuilder(); + + if(table.expression != null) { + toQuotedExpr(builder, table.expression).append(IDENTIFIER_SEP_CHAR); + } + if(table.name1 != null) { + toOptionalQuotedExpr(builder, table.name1, true); + } + toAlias(builder, table.name2); + + String key = ((table.name2 != null) ? table.name2 : table.name1); + joinExprs.add(new Join(key, builder.toString())); + } + + + List joins = getJoinRows(); + if(!joins.isEmpty()) { + + // combine any multi-column joins + Collection> comboJoins = combineJoins(joins); + + for(List comboJoin : comboJoins) { + + Row join = comboJoin.get(0); + String joinExpr = join.expression; + + if(comboJoin.size() > 1) { + + // combine all the join expressions with "AND" + AppendableList comboExprs = new AppendableList() { + private static final long serialVersionUID = 0L; + @Override + protected String getSeparator() { + return ") AND ("; + } + }; + for(Row tmpJoin : comboJoin) { + comboExprs.add(tmpJoin.expression); + } + + joinExpr = new StringBuilder().append("(") + .append(comboExprs).append(")").toString(); + } + + String fromTable = join.name1; + String toTable = join.name2; + + Join fromExpr = getJoinExpr(fromTable, joinExprs); + Join toExpr = getJoinExpr(toTable, joinExprs); + String joinType = JOIN_TYPE_MAP.get(join.flag); + if(joinType == null) { + throw new IllegalStateException("Unknown join type " + join.flag); + } + + String expr = new StringBuilder().append(fromExpr) + .append(joinType).append(toExpr).append(" ON ") + .append(joinExpr).toString(); + + fromExpr.join(toExpr, expr); + joinExprs.add(fromExpr); + } + } + + List result = new AppendableList(); + for(Join joinExpr : joinExprs) { + result.add(joinExpr.expression); + } + + return result; + } + + private static Join getJoinExpr(String table, List joinExprs) + { + for(Iterator iter = joinExprs.iterator(); iter.hasNext(); ) { + Join joinExpr = iter.next(); + if(joinExpr.tables.contains(table)) { + iter.remove(); + return joinExpr; + } + } + throw new IllegalStateException("Cannot find join table " + table); + } + + private static Collection> combineJoins(List joins) + { + // combine joins with the same to/from tables + Map,List> comboJoinMap = + new LinkedHashMap,List>(); + for(Row join : joins) { + List key = Arrays.asList(join.name1, join.name2); + List comboJoins = comboJoinMap.get(key); + if(comboJoins == null) { + comboJoins = new ArrayList(); + comboJoinMap.put(key, comboJoins); + } else { + if(comboJoins.get(0).flag != join.flag) { + throw new IllegalStateException( + "Mismatched join flags for combo joins"); + } + } + comboJoins.add(join); + } + return comboJoinMap.values(); + } + + protected String getFromRemoteDbPath() + { + return getRemoteDatabaseRow().name1; + } + + protected String getFromRemoteDbType() + { + return getRemoteDatabaseRow().expression; + } + + protected String getWhereExpression() + { + return getWhereRow().expression; + } + + protected List getOrderings() + { + return (new RowFormatter(getOrderByRows()) { + @Override protected void format(StringBuilder builder, Row row) { + builder.append(row.expression); + if(DESCENDING_FLAG.equalsIgnoreCase(row.name1)) { + builder.append(" DESC"); + } + } + }).format(); + } + + public String getOwnerAccessType() { + return(hasFlag(OWNER_ACCESS_SELECT_TYPE) ? + "WITH OWNERACCESS OPTION" : DEFAULT_TYPE); + } + + protected boolean hasFlag(int flagMask) + { + return hasFlag(getFlagRow(), flagMask); + } + + protected boolean supportsStandardClauses() { + return true; + } + + /** + * Returns the actual SQL string which this query data represents. + */ + public String toSQLString() + { + StringBuilder builder = new StringBuilder(); + if(supportsStandardClauses()) { + toSQLParameterString(builder); + } + + toSQLString(builder); + + if(supportsStandardClauses()) { + + String accessType = getOwnerAccessType(); + if(!DEFAULT_TYPE.equals(accessType)) { + builder.append(NEWLINE).append(accessType); + } + + builder.append(';'); + } + return builder.toString(); + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } + + /** + * Creates a concrete Query instance from the given query data. + * + * @param objectFlag the flag indicating the type of the query + * @param name the name of the query + * @param rows the rows from the system query table containing the data + * describing this query + * @param objectId the unique object id of this query + * + * @return a Query instance for the given query data + */ + public static QueryImpl create(int objectFlag, String name, List rows, + int objectId) + { + try { + switch(objectFlag) { + case SELECT_QUERY_OBJECT_FLAG: + return new SelectQueryImpl(name, rows, objectId); + case MAKE_TABLE_QUERY_OBJECT_FLAG: + return new MakeTableQueryImpl(name, rows, objectId); + case APPEND_QUERY_OBJECT_FLAG: + return new AppendQueryImpl(name, rows, objectId); + case UPDATE_QUERY_OBJECT_FLAG: + return new UpdateQueryImpl(name, rows, objectId); + case DELETE_QUERY_OBJECT_FLAG: + return new DeleteQueryImpl(name, rows, objectId); + case CROSS_TAB_QUERY_OBJECT_FLAG: + return new CrossTabQueryImpl(name, rows, objectId); + case DATA_DEF_QUERY_OBJECT_FLAG: + return new DataDefinitionQueryImpl(name, rows, objectId); + case PASSTHROUGH_QUERY_OBJECT_FLAG: + return new PassthroughQueryImpl(name, rows, objectId); + case UNION_QUERY_OBJECT_FLAG: + return new UnionQueryImpl(name, rows, objectId); + default: + // unknown querytype + throw new IllegalStateException( + "unknown query object flag " + objectFlag); + } + } catch(IllegalStateException e) { + LOG.warn("Failed parsing query", e); + } + + // return unknown query + return new UnknownQueryImpl(name, rows, objectId, objectFlag); + } + + private static Short getQueryType(List rows) + { + return getUniqueRow(getRowsByAttribute(rows, TYPE_ATTRIBUTE)).flag; + } + + private static List getRowsByAttribute(List rows, Byte attribute) { + List result = new ArrayList(); + for(Row row : rows) { + if(attribute.equals(row.attribute)) { + result.add(row); + } + } + return result; + } + + protected static Row getUniqueRow(List rows) { + if(rows.size() == 1) { + return rows.get(0); + } + if(rows.isEmpty()) { + return EMPTY_ROW; + } + throw new IllegalStateException("Unexpected number of rows for" + rows); + } + + protected static List filterRowsByFlag( + List rows, final short flag) + { + return new RowFilter() { + @Override protected boolean keep(Row row) { + return hasFlag(row, flag); + } + }.filter(rows); + } + + protected static List filterRowsByNotFlag( + List rows, final short flag) + { + return new RowFilter() { + @Override protected boolean keep(Row row) { + return !hasFlag(row, flag); + } + }.filter(rows); + } + + protected static boolean hasFlag(Row row, int flagMask) + { + return((getShortValue(row.flag, 0) & flagMask) != 0); + } + + protected static short getShortValue(Short s, int def) { + return ((s != null) ? (short)s : (short)def); + } + + protected static int getIntValue(Integer i, int def) { + return ((i != null) ? (int)i : def); + } + + protected static StringBuilder toOptionalQuotedExpr(StringBuilder builder, + String fullExpr, + boolean isIdentifier) + { + String[] exprs = (isIdentifier ? + IDENTIFIER_SEP_PAT.split(fullExpr) : + new String[]{fullExpr}); + for(int i = 0; i < exprs.length; ++i) { + String expr = exprs[i]; + if(QUOTABLE_CHAR_PAT.matcher(expr).find()) { + toQuotedExpr(builder, expr); + } else { + builder.append(expr); + } + if(i < (exprs.length - 1)) { + builder.append(IDENTIFIER_SEP_CHAR); + } + } + return builder; + } + + protected static StringBuilder toQuotedExpr(StringBuilder builder, + String expr) + { + return builder.append('[').append(expr).append(']'); + } + + protected static StringBuilder toRemoteDb(StringBuilder builder, + String remoteDbPath, + String remoteDbType) { + if((remoteDbPath != null) || (remoteDbType != null)) { + // note, always include path string, even if empty + builder.append(" IN '"); + if(remoteDbPath != null) { + builder.append(remoteDbPath); + } + builder.append('\''); + if(remoteDbType != null) { + builder.append(" [").append(remoteDbType).append(']'); + } + } + return builder; + } + + protected static StringBuilder toAlias(StringBuilder builder, + String alias) { + if(alias != null) { + toOptionalQuotedExpr(builder.append(" AS "), alias, false); + } + return builder; + } + + private static final class UnknownQueryImpl extends QueryImpl + { + private final int _objectFlag; + + private UnknownQueryImpl(String name, List rows, int objectId, + int objectFlag) + { + super(name, rows, objectId, Type.UNKNOWN); + _objectFlag = objectFlag; + } + + @Override + public int getObjectFlag() { + return _objectFlag; + } + + @Override + protected void toSQLString(StringBuilder builder) { + throw new UnsupportedOperationException(); + } + } + + /** + * Struct containing the information from a single row of the system query + * table. + */ + public static final class Row + { + private final RowId _id; + public final Byte attribute; + public final String expression; + public final Short flag; + public final Integer extra; + public final String name1; + public final String name2; + public final Integer objectId; + public final byte[] order; + + private Row() { + this._id = null; + this.attribute = null; + this.expression = null; + this.flag = null; + this.extra = null; + this.name1 = null; + this.name2= null; + this.objectId = null; + this.order = null; + } + + public Row(com.healthmarketscience.jackcess.Row tableRow) { + this(tableRow.getId(), + (Byte)tableRow.get(COL_ATTRIBUTE), + (String)tableRow.get(COL_EXPRESSION), + (Short)tableRow.get(COL_FLAG), + (Integer)tableRow.get(COL_EXTRA), + (String)tableRow.get(COL_NAME1), + (String)tableRow.get(COL_NAME2), + (Integer)tableRow.get(COL_OBJECTID), + (byte[])tableRow.get(COL_ORDER)); + } + + public Row(RowId id, Byte attribute, String expression, Short flag, + Integer extra, String name1, String name2, + Integer objectId, byte[] order) + { + this._id = id; + this.attribute = attribute; + this.expression = expression; + this.flag = flag; + this.extra = extra; + this.name1 = name1; + this.name2= name2; + this.objectId = objectId; + this.order = order; + } + + public com.healthmarketscience.jackcess.Row toTableRow() + { + com.healthmarketscience.jackcess.Row tableRow = new RowImpl((RowIdImpl)_id); + + tableRow.put(COL_ATTRIBUTE, attribute); + tableRow.put(COL_EXPRESSION, expression); + tableRow.put(COL_FLAG, flag); + tableRow.put(COL_EXTRA, extra); + tableRow.put(COL_NAME1, name1); + tableRow.put(COL_NAME2, name2); + tableRow.put(COL_OBJECTID, objectId); + tableRow.put(COL_ORDER, order); + + return tableRow; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } + } + + protected static abstract class RowFormatter + { + private final List _list; + + protected RowFormatter(List list) { + _list = list; + } + + public List format() { + return format(new AppendableList()); + } + + public List format(List strs) { + for(Row row : _list) { + StringBuilder builder = new StringBuilder(); + format(builder, row); + strs.add(builder.toString()); + } + return strs; + } + + protected abstract void format(StringBuilder builder, Row row); + } + + protected static abstract class RowFilter + { + protected RowFilter() { + } + + public List filter(List list) { + for(Iterator iter = list.iterator(); iter.hasNext(); ) { + if(!keep(iter.next())) { + iter.remove(); + } + } + return list; + } + + protected abstract boolean keep(Row row); + } + + protected static class AppendableList extends ArrayList + { + private static final long serialVersionUID = 0L; + + protected AppendableList() { + } + + protected AppendableList(Collection c) { + super(c); + } + + protected String getSeparator() { + return ", "; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + for(Iterator iter = iterator(); iter.hasNext(); ) { + builder.append(iter.next().toString()); + if(iter.hasNext()) { + builder.append(getSeparator()); + } + } + return builder.toString(); + } + } + + private static final class Join + { + public final List tables = new ArrayList(); + public boolean isJoin; + public String expression; + + private Join(String table, String expr) { + tables.add(table); + expression = expr; + } + + public void join(Join other, String newExpr) { + tables.addAll(other.tables); + isJoin = true; + expression = newExpr; + } + + @Override + public String toString() { + return (isJoin ? ("(" + expression + ")") : expression); + } + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/query/SelectQueryImpl.java b/src/java/com/healthmarketscience/jackcess/impl/query/SelectQueryImpl.java new file mode 100644 index 0000000..dfe326a --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/query/SelectQueryImpl.java @@ -0,0 +1,53 @@ +/* +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.impl.query; + +import java.util.List; +import com.healthmarketscience.jackcess.query.SelectQuery; + + +/** + * Concrete Query subclass which represents a select query, e.g.: + * {@code SELECT FROM WHERE } + * + * @author James Ahlborn + */ +public class SelectQueryImpl extends BaseSelectQueryImpl implements SelectQuery +{ + + public SelectQueryImpl(String name, List rows, int objectId) { + super(name, rows, objectId, Type.SELECT); + } + + @Override + protected void toSQLString(StringBuilder builder) + { + toSQLSelectString(builder, true); + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/query/UnionQueryImpl.java b/src/java/com/healthmarketscience/jackcess/impl/query/UnionQueryImpl.java new file mode 100644 index 0000000..d94efc1 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/query/UnionQueryImpl.java @@ -0,0 +1,96 @@ +/* +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.impl.query; + +import java.util.List; + +import static com.healthmarketscience.jackcess.impl.query.QueryFormat.*; +import com.healthmarketscience.jackcess.query.UnionQuery; + + +/** + * Concrete Query subclass which represents a UNION query, e.g.: + * {@code SELECT UNION SELECT } + * + * @author James Ahlborn + */ +public class UnionQueryImpl extends QueryImpl implements UnionQuery +{ + public UnionQueryImpl(String name, List rows, int objectId) { + super(name, rows, objectId, Type.UNION); + } + + public String getUnionType() { + return(hasFlag(UNION_FLAG) ? DEFAULT_TYPE : "ALL"); + } + + public String getUnionString1() { + return getUnionString(UNION_PART1); + } + + public String getUnionString2() { + return getUnionString(UNION_PART2); + } + + @Override + public List getOrderings() { + return super.getOrderings(); + } + + private String getUnionString(String id) { + for(Row row : getTableRows()) { + if(id.equals(row.name2)) { + return cleanUnionString(row.expression); + } + } + throw new IllegalStateException( + "Could not find union query with id " + id); + } + + @Override + protected void toSQLString(StringBuilder builder) + { + builder.append(getUnionString1()).append(NEWLINE) + .append("UNION "); + String unionType = getUnionType(); + if(!DEFAULT_TYPE.equals(unionType)) { + builder.append(unionType).append(' '); + } + builder.append(getUnionString2()); + List orderings = getOrderings(); + if(!orderings.isEmpty()) { + builder.append(NEWLINE).append("ORDER BY ").append(orderings); + } + } + + private static String cleanUnionString(String str) + { + return str.trim().replaceAll("[\r\n]+", NEWLINE); + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/impl/query/UpdateQueryImpl.java b/src/java/com/healthmarketscience/jackcess/impl/query/UpdateQueryImpl.java new file mode 100644 index 0000000..093f3ec --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/impl/query/UpdateQueryImpl.java @@ -0,0 +1,94 @@ +/* +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.impl.query; + +import java.util.List; + +import static com.healthmarketscience.jackcess.impl.query.QueryFormat.*; +import com.healthmarketscience.jackcess.query.UpdateQuery; + + +/** + * Concrete Query subclass which represents a row update query, e.g.: + * {@code UPDATE
SET } + * + * @author James Ahlborn + */ +public class UpdateQueryImpl extends QueryImpl implements UpdateQuery +{ + + public UpdateQueryImpl(String name, List rows, int objectId) { + super(name, rows, objectId, Type.UPDATE); + } + + public List getTargetTables() + { + return super.getFromTables(); + } + + public String getRemoteDbPath() + { + return super.getFromRemoteDbPath(); + } + + public String getRemoteDbType() + { + return super.getFromRemoteDbType(); + } + + public List getNewValues() + { + return (new RowFormatter(getColumnRows()) { + @Override protected void format(StringBuilder builder, Row row) { + toOptionalQuotedExpr(builder, row.name2, true) + .append(" = ").append(row.expression); + } + }).format(); + } + + @Override + public String getWhereExpression() + { + return super.getWhereExpression(); + } + + @Override + protected void toSQLString(StringBuilder builder) + { + builder.append("UPDATE ").append(getTargetTables()); + toRemoteDb(builder, getRemoteDbPath(), getRemoteDbType()); + + builder.append(NEWLINE).append("SET ").append(getNewValues()); + + String whereExpr = getWhereExpression(); + if(whereExpr != null) { + builder.append(NEWLINE).append("WHERE ").append(whereExpr); + } + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/scsu/Compress.java b/src/java/com/healthmarketscience/jackcess/impl/scsu/Compress.java similarity index 99% rename from src/java/com/healthmarketscience/jackcess/scsu/Compress.java rename to src/java/com/healthmarketscience/jackcess/impl/scsu/Compress.java index c5f7360..9428075 100644 --- a/src/java/com/healthmarketscience/jackcess/scsu/Compress.java +++ b/src/java/com/healthmarketscience/jackcess/impl/scsu/Compress.java @@ -1,4 +1,4 @@ -package com.healthmarketscience.jackcess.scsu; +package com.healthmarketscience.jackcess.impl.scsu; /** * This sample software accompanies Unicode Technical Report #6 and diff --git a/src/java/com/healthmarketscience/jackcess/scsu/Debug.java b/src/java/com/healthmarketscience/jackcess/impl/scsu/Debug.java similarity index 98% rename from src/java/com/healthmarketscience/jackcess/scsu/Debug.java rename to src/java/com/healthmarketscience/jackcess/impl/scsu/Debug.java index 10485ea..c973765 100644 --- a/src/java/com/healthmarketscience/jackcess/scsu/Debug.java +++ b/src/java/com/healthmarketscience/jackcess/impl/scsu/Debug.java @@ -1,4 +1,4 @@ -package com.healthmarketscience.jackcess.scsu; +package com.healthmarketscience.jackcess.impl.scsu; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; diff --git a/src/java/com/healthmarketscience/jackcess/scsu/EndOfInputException.java b/src/java/com/healthmarketscience/jackcess/impl/scsu/EndOfInputException.java similarity index 96% rename from src/java/com/healthmarketscience/jackcess/scsu/EndOfInputException.java rename to src/java/com/healthmarketscience/jackcess/impl/scsu/EndOfInputException.java index 4ac8973..b3148a7 100644 --- a/src/java/com/healthmarketscience/jackcess/scsu/EndOfInputException.java +++ b/src/java/com/healthmarketscience/jackcess/impl/scsu/EndOfInputException.java @@ -1,4 +1,4 @@ -package com.healthmarketscience.jackcess.scsu; +package com.healthmarketscience.jackcess.impl.scsu; /** * This sample software accompanies Unicode Technical Report #6 and diff --git a/src/java/com/healthmarketscience/jackcess/scsu/EndOfOutputException.java b/src/java/com/healthmarketscience/jackcess/impl/scsu/EndOfOutputException.java similarity index 96% rename from src/java/com/healthmarketscience/jackcess/scsu/EndOfOutputException.java rename to src/java/com/healthmarketscience/jackcess/impl/scsu/EndOfOutputException.java index 501d195..94f5be6 100644 --- a/src/java/com/healthmarketscience/jackcess/scsu/EndOfOutputException.java +++ b/src/java/com/healthmarketscience/jackcess/impl/scsu/EndOfOutputException.java @@ -1,4 +1,4 @@ -package com.healthmarketscience.jackcess.scsu; +package com.healthmarketscience.jackcess.impl.scsu; /** * This sample software accompanies Unicode Technical Report #6 and diff --git a/src/java/com/healthmarketscience/jackcess/scsu/Expand.java b/src/java/com/healthmarketscience/jackcess/impl/scsu/Expand.java similarity index 99% rename from src/java/com/healthmarketscience/jackcess/scsu/Expand.java rename to src/java/com/healthmarketscience/jackcess/impl/scsu/Expand.java index 4858044..378ca2f 100644 --- a/src/java/com/healthmarketscience/jackcess/scsu/Expand.java +++ b/src/java/com/healthmarketscience/jackcess/impl/scsu/Expand.java @@ -1,4 +1,4 @@ -package com.healthmarketscience.jackcess.scsu; +package com.healthmarketscience.jackcess.impl.scsu; /* * This sample software accompanies Unicode Technical Report #6 and diff --git a/src/java/com/healthmarketscience/jackcess/scsu/IllegalInputException.java b/src/java/com/healthmarketscience/jackcess/impl/scsu/IllegalInputException.java similarity index 96% rename from src/java/com/healthmarketscience/jackcess/scsu/IllegalInputException.java rename to src/java/com/healthmarketscience/jackcess/impl/scsu/IllegalInputException.java index 1600d03..b191f56 100644 --- a/src/java/com/healthmarketscience/jackcess/scsu/IllegalInputException.java +++ b/src/java/com/healthmarketscience/jackcess/impl/scsu/IllegalInputException.java @@ -1,4 +1,4 @@ -package com.healthmarketscience.jackcess.scsu; +package com.healthmarketscience.jackcess.impl.scsu; /** * This sample software accompanies Unicode Technical Report #6 and diff --git a/src/java/com/healthmarketscience/jackcess/scsu/SCSU.java b/src/java/com/healthmarketscience/jackcess/impl/scsu/SCSU.java similarity index 99% rename from src/java/com/healthmarketscience/jackcess/scsu/SCSU.java rename to src/java/com/healthmarketscience/jackcess/impl/scsu/SCSU.java index 887062b..7859780 100644 --- a/src/java/com/healthmarketscience/jackcess/scsu/SCSU.java +++ b/src/java/com/healthmarketscience/jackcess/impl/scsu/SCSU.java @@ -1,4 +1,4 @@ -package com.healthmarketscience.jackcess.scsu; +package com.healthmarketscience.jackcess.impl.scsu; /* * This sample software accompanies Unicode Technical Report #6 and diff --git a/src/java/com/healthmarketscience/jackcess/query/AppendQuery.java b/src/java/com/healthmarketscience/jackcess/query/AppendQuery.java index 3a216e8..96ae5ad 100644 --- a/src/java/com/healthmarketscience/jackcess/query/AppendQuery.java +++ b/src/java/com/healthmarketscience/jackcess/query/AppendQuery.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2008 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,77 +15,27 @@ 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.query; import java.util.List; -import static com.healthmarketscience.jackcess.query.QueryFormat.*; - /** - * Concrete Query subclass which represents an append query, e.g.: + * Query interface which represents an append query, e.g.: * {@code INSERT INTO
VALUES ()} * * @author James Ahlborn */ -public class AppendQuery extends BaseSelectQuery +public interface AppendQuery extends BaseSelectQuery { - public AppendQuery(String name, List rows, int objectId) { - super(name, rows, objectId, Type.APPEND); - } - - public String getTargetTable() { - return getTypeRow().name1; - } - - public String getRemoteDbPath() { - return getTypeRow().name2; - } - - public String getRemoteDbType() { - return getTypeRow().expression; - } - - protected List getValueRows() { - return filterRowsByFlag(super.getColumnRows(), APPEND_VALUE_FLAG); - } + public String getTargetTable(); - @Override - protected List getColumnRows() { - return filterRowsByNotFlag(super.getColumnRows(), APPEND_VALUE_FLAG); - } + public String getRemoteDbPath(); - public List getValues() { - return new RowFormatter(getValueRows()) { - @Override protected void format(StringBuilder builder, Row row) { - builder.append(row.expression); - } - }.format(); - } + public String getRemoteDbType(); - @Override - protected void toSQLString(StringBuilder builder) - { - builder.append("INSERT INTO ").append(getTargetTable()); - toRemoteDb(builder, getRemoteDbPath(), getRemoteDbType()); - builder.append(NEWLINE); - List values = getValues(); - if(!values.isEmpty()) { - builder.append("VALUES (").append(values).append(')'); - } else { - toSQLSelectString(builder, true); - } - } - + public List getValues(); } diff --git a/src/java/com/healthmarketscience/jackcess/query/BaseSelectQuery.java b/src/java/com/healthmarketscience/jackcess/query/BaseSelectQuery.java index 272baca..107dbe9 100644 --- a/src/java/com/healthmarketscience/jackcess/query/BaseSelectQuery.java +++ b/src/java/com/healthmarketscience/jackcess/query/BaseSelectQuery.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2008 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,161 +15,36 @@ 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.query; import java.util.List; -import static com.healthmarketscience.jackcess.query.QueryFormat.*; - /** - * Base class for queries which represent some form of SELECT statement. + * Base interface for queries which represent some form of SELECT statement. * * @author James Ahlborn */ -public abstract class BaseSelectQuery extends Query +public interface BaseSelectQuery extends Query { - protected BaseSelectQuery(String name, List rows, int objectId, - Type type) { - super(name, rows, objectId, type); - } - - protected void toSQLSelectString(StringBuilder builder, - boolean useSelectPrefix) - { - if(useSelectPrefix) { - builder.append("SELECT "); - String selectType = getSelectType(); - if(!DEFAULT_TYPE.equals(selectType)) { - builder.append(selectType).append(' '); - } - } - - builder.append(getSelectColumns()); - toSelectInto(builder); - - List fromTables = getFromTables(); - if(!fromTables.isEmpty()) { - builder.append(NEWLINE).append("FROM ").append(fromTables); - toRemoteDb(builder, getFromRemoteDbPath(), getFromRemoteDbType()); - } - - String whereExpr = getWhereExpression(); - if(whereExpr != null) { - builder.append(NEWLINE).append("WHERE ").append(whereExpr); - } - - List groupings = getGroupings(); - if(!groupings.isEmpty()) { - builder.append(NEWLINE).append("GROUP BY ").append(groupings); - } - - String havingExpr = getHavingExpression(); - if(havingExpr != null) { - builder.append(NEWLINE).append("HAVING ").append(havingExpr); - } - - List orderings = getOrderings(); - if(!orderings.isEmpty()) { - builder.append(NEWLINE).append("ORDER BY ").append(orderings); - } - } - - public String getSelectType() - { - if(hasFlag(DISTINCT_SELECT_TYPE)) { - return "DISTINCT"; - } - - if(hasFlag(DISTINCT_ROW_SELECT_TYPE)) { - return "DISTINCTROW"; - } - - if(hasFlag(TOP_SELECT_TYPE)) { - StringBuilder builder = new StringBuilder(); - builder.append("TOP ").append(getFlagRow().name1); - if(hasFlag(PERCENT_SELECT_TYPE)) { - builder.append(" PERCENT"); - } - return builder.toString(); - } - - return DEFAULT_TYPE; - } - - public List getSelectColumns() - { - List result = (new RowFormatter(getColumnRows()) { - @Override protected void format(StringBuilder builder, Row row) { - // note column expression are always quoted appropriately - builder.append(row.expression); - toAlias(builder, row.name1); - } - }).format(); - if(hasFlag(SELECT_STAR_SELECT_TYPE)) { - result.add("*"); - } - return result; - } - - protected void toSelectInto(StringBuilder builder) - { - // base does nothing - } - - @Override - public List getFromTables() - { - return super.getFromTables(); - } - - @Override - public String getFromRemoteDbPath() - { - return super.getFromRemoteDbPath(); - } - - @Override - public String getFromRemoteDbType() - { - return super.getFromRemoteDbType(); - } - - @Override - public String getWhereExpression() - { - return super.getWhereExpression(); - } - - public List getGroupings() - { - return (new RowFormatter(getGroupByRows()) { - @Override protected void format(StringBuilder builder, Row row) { - builder.append(row.expression); - } - }).format(); - } - - public String getHavingExpression() - { - return getHavingRow().expression; - } - - @Override - public List getOrderings() - { - return super.getOrderings(); - } - + public String getSelectType(); + + public List getSelectColumns(); + + public List getFromTables(); + + public String getFromRemoteDbPath(); + + public String getFromRemoteDbType(); + + public String getWhereExpression(); + + public List getGroupings(); + + public String getHavingExpression(); + + public List getOrderings(); } diff --git a/src/java/com/healthmarketscience/jackcess/query/CrossTabQuery.java b/src/java/com/healthmarketscience/jackcess/query/CrossTabQuery.java index 3fd6bf6..474c979 100644 --- a/src/java/com/healthmarketscience/jackcess/query/CrossTabQuery.java +++ b/src/java/com/healthmarketscience/jackcess/query/CrossTabQuery.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2008 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,84 +15,23 @@ 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.query; import java.util.List; -import static com.healthmarketscience.jackcess.query.QueryFormat.*; - /** - * Concrete Query subclass which represents a crosstab/pivot query, e.g.: + * Query interface which represents a crosstab/pivot query, e.g.: * {@code TRANSFORM SELECT PIVOT } * * @author James Ahlborn */ -public class CrossTabQuery extends BaseSelectQuery +public interface CrossTabQuery extends BaseSelectQuery { - public CrossTabQuery(String name, List rows, int objectId) { - super(name, rows, objectId, Type.CROSS_TAB); - } - - protected Row getTransformRow() { - return getUniqueRow( - filterRowsByNotFlag(super.getColumnRows(), - (short)(CROSSTAB_PIVOT_FLAG | - CROSSTAB_NORMAL_FLAG))); - } - - @Override - protected List getColumnRows() { - return filterRowsByFlag(super.getColumnRows(), CROSSTAB_NORMAL_FLAG); - } - - @Override - protected List getGroupByRows() { - return filterRowsByFlag(super.getGroupByRows(), CROSSTAB_NORMAL_FLAG); - } - - protected Row getPivotRow() { - return getUniqueRow(filterRowsByFlag(super.getColumnRows(), - CROSSTAB_PIVOT_FLAG)); - } - - public String getTransformExpression() { - Row row = getTransformRow(); - if(row.expression == null) { - return null; - } - // note column expression are always quoted appropriately - StringBuilder builder = new StringBuilder(row.expression); - return toAlias(builder, row.name1).toString(); - } - - public String getPivotExpression() { - return getPivotRow().expression; - } - - @Override - protected void toSQLString(StringBuilder builder) - { - String transformExpr = getTransformExpression(); - if(transformExpr != null) { - builder.append("TRANSFORM ").append(transformExpr).append(NEWLINE); - } - - toSQLSelectString(builder, true); - - builder.append(NEWLINE).append("PIVOT ") - .append(getPivotExpression()); - } + public String getTransformExpression(); + public String getPivotExpression(); } diff --git a/src/java/com/healthmarketscience/jackcess/query/DataDefinitionQuery.java b/src/java/com/healthmarketscience/jackcess/query/DataDefinitionQuery.java index 16937b3..9b6b6fe 100644 --- a/src/java/com/healthmarketscience/jackcess/query/DataDefinitionQuery.java +++ b/src/java/com/healthmarketscience/jackcess/query/DataDefinitionQuery.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2008 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,14 +15,6 @@ 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.query; @@ -31,33 +23,11 @@ import java.util.List; /** - * Concrete Query subclass which represents a DDL query. + * Query interface which represents a DDL query. * * @author James Ahlborn */ -public class DataDefinitionQuery extends Query +public interface DataDefinitionQuery extends Query { - - public DataDefinitionQuery(String name, List rows, int objectId) { - super(name, rows, objectId, Type.DATA_DEFINITION); - } - - public String getDDLString() { - return getTypeRow().expression; - } - - @Override - protected boolean supportsStandardClauses() { - return false; - } - - @Override - protected void toSQLString(StringBuilder builder) - { - String ddl = getDDLString(); - if(ddl != null) { - builder.append(ddl); - } - } - + public String getDDLString(); } diff --git a/src/java/com/healthmarketscience/jackcess/query/DeleteQuery.java b/src/java/com/healthmarketscience/jackcess/query/DeleteQuery.java index 4f42b82..b598c8b 100644 --- a/src/java/com/healthmarketscience/jackcess/query/DeleteQuery.java +++ b/src/java/com/healthmarketscience/jackcess/query/DeleteQuery.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2008 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,39 +15,19 @@ 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.query; -import java.util.List; /** - * Concrete Query subclass which represents a delete query, e.g.: + * Query interface which represents a delete query, e.g.: * {@code DELETE * FROM
WHERE } * * @author James Ahlborn */ -public class DeleteQuery extends BaseSelectQuery +public interface DeleteQuery extends BaseSelectQuery { - public DeleteQuery(String name, List rows, int objectId) { - super(name, rows, objectId, Type.DELETE); - } - - @Override - protected void toSQLString(StringBuilder builder) - { - builder.append("DELETE "); - toSQLSelectString(builder, false); - } - } diff --git a/src/java/com/healthmarketscience/jackcess/query/MakeTableQuery.java b/src/java/com/healthmarketscience/jackcess/query/MakeTableQuery.java index 2144197..f7798e0 100644 --- a/src/java/com/healthmarketscience/jackcess/query/MakeTableQuery.java +++ b/src/java/com/healthmarketscience/jackcess/query/MakeTableQuery.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2008 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,57 +15,24 @@ 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.query; -import java.util.List; /** - * Concrete Query subclass which represents an table creation query, e.g.: + * Query interface which represents an table creation query, e.g.: * {@code SELECT INTO } * * @author James Ahlborn */ -public class MakeTableQuery extends BaseSelectQuery +public interface MakeTableQuery extends BaseSelectQuery { - public MakeTableQuery(String name, List rows, int objectId) { - super(name, rows, objectId, Type.MAKE_TABLE); - } - - public String getTargetTable() { - return getTypeRow().name1; - } - - public String getRemoteDbPath() { - return getTypeRow().name2; - } - - public String getRemoteDbType() { - return getTypeRow().expression; - } - - @Override - protected void toSelectInto(StringBuilder builder) - { - builder.append(" INTO ").append(getTargetTable()); - toRemoteDb(builder, getRemoteDbPath(), getRemoteDbType()); - } + public String getTargetTable(); - @Override - protected void toSQLString(StringBuilder builder) - { - toSQLSelectString(builder, true); - } + public String getRemoteDbPath(); + public String getRemoteDbType(); } diff --git a/src/java/com/healthmarketscience/jackcess/query/PassthroughQuery.java b/src/java/com/healthmarketscience/jackcess/query/PassthroughQuery.java index cb18090..7004cee 100644 --- a/src/java/com/healthmarketscience/jackcess/query/PassthroughQuery.java +++ b/src/java/com/healthmarketscience/jackcess/query/PassthroughQuery.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2008 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,54 +15,20 @@ 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.query; -import java.util.List; /** - * Concrete Query subclass which represents a query which will be executed via - * ODBC. + * Query interface which represents a query which will be executed via ODBC. * * @author James Ahlborn */ -public class PassthroughQuery extends Query +public interface PassthroughQuery extends Query { + public String getConnectionString(); - public PassthroughQuery(String name, List rows, int objectId) { - super(name, rows, objectId, Type.PASSTHROUGH); - } - - public String getConnectionString() { - return getTypeRow().name1; - } - - public String getPassthroughString() { - return getTypeRow().expression; - } - - @Override - protected boolean supportsStandardClauses() { - return false; - } - - @Override - protected void toSQLString(StringBuilder builder) - { - String pt = getPassthroughString(); - if(pt != null) { - builder.append(pt); - } - } - + public String getPassthroughString(); } diff --git a/src/java/com/healthmarketscience/jackcess/query/Query.java b/src/java/com/healthmarketscience/jackcess/query/Query.java index 15189b3..f6d6cc3 100644 --- a/src/java/com/healthmarketscience/jackcess/query/Query.java +++ b/src/java/com/healthmarketscience/jackcess/query/Query.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2008 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,47 +15,24 @@ 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.query; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; -import org.apache.commons.lang.builder.ToStringBuilder; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import static com.healthmarketscience.jackcess.query.QueryFormat.*; +import static com.healthmarketscience.jackcess.impl.query.QueryFormat.*; /** - * Base class for classes which encapsulate information about an Access query. - * The {@link #toSQLString()} method can be used to convert this object into - * the actual SQL string which this query data represents. + * Base interface for classes which encapsulate information about an Access + * query. The {@link #toSQLString()} method can be used to convert this + * object into the actual SQL string which this query data represents. * * @author James Ahlborn */ -public abstract class Query +public interface Query { - protected static final Log LOG = LogFactory.getLog(Query.class); - - private static final Row EMPTY_ROW = - new Row(Collections.emptyMap()); public enum Type { @@ -87,649 +64,35 @@ public abstract class Query } } - private final String _name; - private final List _rows; - private final int _objectId; - private final Type _type; - - protected Query(String name, List rows, int objectId, Type type) { - _name = name; - _rows = rows; - _objectId = objectId; - _type = type; - - if(type != Type.UNKNOWN) { - short foundType = getShortValue(getQueryType(rows), - _type.getValue()); - if(foundType != _type.getValue()) { - throw new IllegalStateException("Unexpected query type " + foundType); - } - } - } - /** * Returns the name of the query. */ - public String getName() { - return _name; - } + public String getName(); /** * Returns the type of the query. */ - public Type getType() { - return _type; - } + public Type getType(); /** * Returns the unique object id of the query. */ - public int getObjectId() { - return _objectId; - } + public int getObjectId(); - public int getObjectFlag() { - return getType().getObjectFlag(); - } + public int getObjectFlag(); /** * Returns the rows from the system query table from which the query * information was derived. */ - public List getRows() { - return _rows; - } - - protected List getRowsByAttribute(Byte attribute) { - return getRowsByAttribute(getRows(), attribute); - } - - protected Row getRowByAttribute(Byte attribute) { - return getUniqueRow(getRowsByAttribute(getRows(), attribute)); - } - - protected Row getTypeRow() { - return getRowByAttribute(TYPE_ATTRIBUTE); - } - - protected List getParameterRows() { - return getRowsByAttribute(PARAMETER_ATTRIBUTE); - } - - protected Row getFlagRow() { - return getRowByAttribute(FLAG_ATTRIBUTE); - } - - protected Row getRemoteDatabaseRow() { - return getRowByAttribute(REMOTEDB_ATTRIBUTE); - } - - protected List getTableRows() { - return getRowsByAttribute(TABLE_ATTRIBUTE); - } - - protected List getColumnRows() { - return getRowsByAttribute(COLUMN_ATTRIBUTE); - } - - protected List getJoinRows() { - return getRowsByAttribute(JOIN_ATTRIBUTE); - } - - protected Row getWhereRow() { - return getRowByAttribute(WHERE_ATTRIBUTE); - } - - protected List getGroupByRows() { - return getRowsByAttribute(GROUPBY_ATTRIBUTE); - } - - protected Row getHavingRow() { - return getRowByAttribute(HAVING_ATTRIBUTE); - } - - protected List getOrderByRows() { - return getRowsByAttribute(ORDERBY_ATTRIBUTE); - } - - protected abstract void toSQLString(StringBuilder builder); - - protected void toSQLParameterString(StringBuilder builder) { - // handle any parameters - List params = getParameters(); - if(!params.isEmpty()) { - builder.append("PARAMETERS ").append(params) - .append(';').append(NEWLINE); - } - } - - public List getParameters() - { - return (new RowFormatter(getParameterRows()) { - @Override protected void format(StringBuilder builder, Row row) { - String typeName = PARAM_TYPE_MAP.get(row.flag); - if(typeName == null) { - throw new IllegalStateException("Unknown param type " + row.flag); - } - - builder.append(row.name1).append(' ').append(typeName); - if((TEXT_FLAG.equals(row.flag)) && (getIntValue(row.extra, 0) > 0)) { - builder.append('(').append(row.extra).append(')'); - } - } - }).format(); - } - - protected List getFromTables() - { - List joinExprs = new ArrayList(); - for(Row table : getTableRows()) { - StringBuilder builder = new StringBuilder(); - - if(table.expression != null) { - toQuotedExpr(builder, table.expression).append(IDENTIFIER_SEP_CHAR); - } - if(table.name1 != null) { - toOptionalQuotedExpr(builder, table.name1, true); - } - toAlias(builder, table.name2); - - String key = ((table.name2 != null) ? table.name2 : table.name1); - joinExprs.add(new Join(key, builder.toString())); - } - + // public List getRows(); - List joins = getJoinRows(); - if(!joins.isEmpty()) { + public List getParameters(); - // combine any multi-column joins - Collection> comboJoins = combineJoins(joins); - - for(List comboJoin : comboJoins) { - - Row join = comboJoin.get(0); - String joinExpr = join.expression; - - if(comboJoin.size() > 1) { - - // combine all the join expressions with "AND" - AppendableList comboExprs = new AppendableList() { - private static final long serialVersionUID = 0L; - @Override - protected String getSeparator() { - return ") AND ("; - } - }; - for(Row tmpJoin : comboJoin) { - comboExprs.add(tmpJoin.expression); - } - - joinExpr = new StringBuilder().append("(") - .append(comboExprs).append(")").toString(); - } - - String fromTable = join.name1; - String toTable = join.name2; - - Join fromExpr = getJoinExpr(fromTable, joinExprs); - Join toExpr = getJoinExpr(toTable, joinExprs); - String joinType = JOIN_TYPE_MAP.get(join.flag); - if(joinType == null) { - throw new IllegalStateException("Unknown join type " + join.flag); - } - - String expr = new StringBuilder().append(fromExpr) - .append(joinType).append(toExpr).append(" ON ") - .append(joinExpr).toString(); - - fromExpr.join(toExpr, expr); - joinExprs.add(fromExpr); - } - } - - List result = new AppendableList(); - for(Join joinExpr : joinExprs) { - result.add(joinExpr.expression); - } - - return result; - } - - private Join getJoinExpr(String table, List joinExprs) - { - for(Iterator iter = joinExprs.iterator(); iter.hasNext(); ) { - Join joinExpr = iter.next(); - if(joinExpr.tables.contains(table)) { - iter.remove(); - return joinExpr; - } - } - throw new IllegalStateException("Cannot find join table " + table); - } - - private Collection> combineJoins(List joins) - { - // combine joins with the same to/from tables - Map,List> comboJoinMap = - new LinkedHashMap,List>(); - for(Row join : joins) { - List key = Arrays.asList(join.name1, join.name2); - List comboJoins = comboJoinMap.get(key); - if(comboJoins == null) { - comboJoins = new ArrayList(); - comboJoinMap.put(key, comboJoins); - } else { - if((short)comboJoins.get(0).flag != (short)join.flag) { - throw new IllegalStateException( - "Mismatched join flags for combo joins"); - } - } - comboJoins.add(join); - } - return comboJoinMap.values(); - } - - protected String getFromRemoteDbPath() - { - return getRemoteDatabaseRow().name1; - } - - protected String getFromRemoteDbType() - { - return getRemoteDatabaseRow().expression; - } - - protected String getWhereExpression() - { - return getWhereRow().expression; - } - - protected List getOrderings() - { - return (new RowFormatter(getOrderByRows()) { - @Override protected void format(StringBuilder builder, Row row) { - builder.append(row.expression); - if(DESCENDING_FLAG.equalsIgnoreCase(row.name1)) { - builder.append(" DESC"); - } - } - }).format(); - } - - public String getOwnerAccessType() { - return(hasFlag(OWNER_ACCESS_SELECT_TYPE) ? - "WITH OWNERACCESS OPTION" : DEFAULT_TYPE); - } - - protected boolean hasFlag(int flagMask) - { - return hasFlag(getFlagRow(), flagMask); - } - - protected boolean supportsStandardClauses() { - return true; - } + public String getOwnerAccessType(); /** * Returns the actual SQL string which this query data represents. */ - public String toSQLString() - { - StringBuilder builder = new StringBuilder(); - if(supportsStandardClauses()) { - toSQLParameterString(builder); - } - - toSQLString(builder); - - if(supportsStandardClauses()) { - - String accessType = getOwnerAccessType(); - if(!DEFAULT_TYPE.equals(accessType)) { - builder.append(NEWLINE).append(accessType); - } - - builder.append(';'); - } - return builder.toString(); - } - - @Override - public String toString() { - return ToStringBuilder.reflectionToString(this); - } - - /** - * Creates a concrete Query instance from the given query data. - * - * @param objectFlag the flag indicating the type of the query - * @param name the name of the query - * @param rows the rows from the system query table containing the data - * describing this query - * @param objectId the unique object id of this query - * - * @return a Query instance for the given query data - */ - public static Query create(int objectFlag, String name, List rows, - int objectId) - { - try { - switch(objectFlag) { - case SELECT_QUERY_OBJECT_FLAG: - return new SelectQuery(name, rows, objectId); - case MAKE_TABLE_QUERY_OBJECT_FLAG: - return new MakeTableQuery(name, rows, objectId); - case APPEND_QUERY_OBJECT_FLAG: - return new AppendQuery(name, rows, objectId); - case UPDATE_QUERY_OBJECT_FLAG: - return new UpdateQuery(name, rows, objectId); - case DELETE_QUERY_OBJECT_FLAG: - return new DeleteQuery(name, rows, objectId); - case CROSS_TAB_QUERY_OBJECT_FLAG: - return new CrossTabQuery(name, rows, objectId); - case DATA_DEF_QUERY_OBJECT_FLAG: - return new DataDefinitionQuery(name, rows, objectId); - case PASSTHROUGH_QUERY_OBJECT_FLAG: - return new PassthroughQuery(name, rows, objectId); - case UNION_QUERY_OBJECT_FLAG: - return new UnionQuery(name, rows, objectId); - default: - // unknown querytype - throw new IllegalStateException( - "unknown query object flag " + objectFlag); - } - } catch(IllegalStateException e) { - LOG.warn("Failed parsing query", e); - } - - // return unknown query - return new UnknownQuery(name, rows, objectId, objectFlag); - } - - private static Short getQueryType(List rows) - { - return getUniqueRow(getRowsByAttribute(rows, TYPE_ATTRIBUTE)).flag; - } - - private static List getRowsByAttribute(List rows, Byte attribute) { - List result = new ArrayList(); - for(Row row : rows) { - if(attribute.equals(row.attribute)) { - result.add(row); - } - } - return result; - } - - protected static Row getUniqueRow(List rows) { - if(rows.size() == 1) { - return rows.get(0); - } - if(rows.isEmpty()) { - return EMPTY_ROW; - } - throw new IllegalStateException("Unexpected number of rows for" + rows); - } - - protected static List filterRowsByFlag( - List rows, final short flag) - { - return new RowFilter() { - @Override protected boolean keep(Row row) { - return hasFlag(row, flag); - } - }.filter(rows); - } - - protected static List filterRowsByNotFlag( - List rows, final short flag) - { - return new RowFilter() { - @Override protected boolean keep(Row row) { - return !hasFlag(row, flag); - } - }.filter(rows); - } - - protected static boolean hasFlag(Row row, int flagMask) - { - return((getShortValue(row.flag, 0) & flagMask) != 0); - } - - protected static short getShortValue(Short s, int def) { - return ((s != null) ? (short)s : (short)def); - } - - protected static int getIntValue(Integer i, int def) { - return ((i != null) ? (int)i : def); - } - - protected static StringBuilder toOptionalQuotedExpr(StringBuilder builder, - String fullExpr, - boolean isIdentifier) - { - String[] exprs = (isIdentifier ? - IDENTIFIER_SEP_PAT.split(fullExpr) : - new String[]{fullExpr}); - for(int i = 0; i < exprs.length; ++i) { - String expr = exprs[i]; - if(QUOTABLE_CHAR_PAT.matcher(expr).find()) { - toQuotedExpr(builder, expr); - } else { - builder.append(expr); - } - if(i < (exprs.length - 1)) { - builder.append(IDENTIFIER_SEP_CHAR); - } - } - return builder; - } - - protected static StringBuilder toQuotedExpr(StringBuilder builder, - String expr) - { - return builder.append('[').append(expr).append(']'); - } - - protected static StringBuilder toRemoteDb(StringBuilder builder, - String remoteDbPath, - String remoteDbType) { - if((remoteDbPath != null) || (remoteDbType != null)) { - // note, always include path string, even if empty - builder.append(" IN '"); - if(remoteDbPath != null) { - builder.append(remoteDbPath); - } - builder.append('\''); - if(remoteDbType != null) { - builder.append(" [").append(remoteDbType).append(']'); - } - } - return builder; - } - - protected static StringBuilder toAlias(StringBuilder builder, - String alias) { - if(alias != null) { - toOptionalQuotedExpr(builder.append(" AS "), alias, false); - } - return builder; - } - - private static final class UnknownQuery extends Query - { - private final int _objectFlag; - - private UnknownQuery(String name, List rows, int objectId, - int objectFlag) - { - super(name, rows, objectId, Type.UNKNOWN); - _objectFlag = objectFlag; - } - - @Override - public int getObjectFlag() { - return _objectFlag; - } - - @Override - protected void toSQLString(StringBuilder builder) { - throw new UnsupportedOperationException(); - } - } - - /** - * Struct containing the information from a single row of the system query - * table. - */ - public static final class Row - { - public final Byte attribute; - public final String expression; - public final Short flag; - public final Integer extra; - public final String name1; - public final String name2; - public final Integer objectId; - public final byte[] order; - - public Row(Map tableRow) { - this((Byte)tableRow.get(COL_ATTRIBUTE), - (String)tableRow.get(COL_EXPRESSION), - (Short)tableRow.get(COL_FLAG), - (Integer)tableRow.get(COL_EXTRA), - (String)tableRow.get(COL_NAME1), - (String)tableRow.get(COL_NAME2), - (Integer)tableRow.get(COL_OBJECTID), - (byte[])tableRow.get(COL_ORDER)); - } - - public Row(Byte attribute, String expression, Short flag, - Integer extra, String name1, String name2, - Integer objectId, byte[] order) - { - this.attribute = attribute; - this.expression = expression; - this.flag = flag; - this.extra = extra; - this.name1 = name1; - this.name2= name2; - this.objectId = objectId; - this.order = order; - } - - public Map toTableRow() - { - Map tableRow = new LinkedHashMap(); - - tableRow.put(COL_ATTRIBUTE, attribute); - tableRow.put(COL_EXPRESSION, expression); - tableRow.put(COL_FLAG, flag); - tableRow.put(COL_EXTRA, extra); - tableRow.put(COL_NAME1, name1); - tableRow.put(COL_NAME2, name2); - tableRow.put(COL_OBJECTID, objectId); - tableRow.put(COL_ORDER, order); - - return tableRow; - } - - @Override - public String toString() { - return ToStringBuilder.reflectionToString(this); - } - } - - protected static abstract class RowFormatter - { - private final List _list; - - protected RowFormatter(List list) { - _list = list; - } - - public List format() { - return format(new AppendableList()); - } - - public List format(List strs) { - for(Row row : _list) { - StringBuilder builder = new StringBuilder(); - format(builder, row); - strs.add(builder.toString()); - } - return strs; - } - - protected abstract void format(StringBuilder builder, Row row); - } - - protected static abstract class RowFilter - { - protected RowFilter() { - } - - public List filter(List list) { - for(Iterator iter = list.iterator(); iter.hasNext(); ) { - if(!keep(iter.next())) { - iter.remove(); - } - } - return list; - } - - protected abstract boolean keep(Row row); - } - - protected static class AppendableList extends ArrayList - { - private static final long serialVersionUID = 0L; - - protected AppendableList() { - } - - protected AppendableList(Collection c) { - super(c); - } - - protected String getSeparator() { - return ", "; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - for(Iterator iter = iterator(); iter.hasNext(); ) { - builder.append(iter.next().toString()); - if(iter.hasNext()) { - builder.append(getSeparator()); - } - } - return builder.toString(); - } - } - - private static final class Join - { - public final List tables = new ArrayList(); - public boolean isJoin; - public String expression; - - private Join(String table, String expr) { - tables.add(table); - expression = expr; - } - - public void join(Join other, String newExpr) { - tables.addAll(other.tables); - isJoin = true; - expression = newExpr; - } - - @Override - public String toString() { - return (isJoin ? ("(" + expression + ")") : expression); - } - } - + public String toSQLString(); } diff --git a/src/java/com/healthmarketscience/jackcess/query/SelectQuery.java b/src/java/com/healthmarketscience/jackcess/query/SelectQuery.java index 3efd029..7ada9b2 100644 --- a/src/java/com/healthmarketscience/jackcess/query/SelectQuery.java +++ b/src/java/com/healthmarketscience/jackcess/query/SelectQuery.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2008 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,38 +15,18 @@ 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.query; -import java.util.List; /** - * Concrete Query subclass which represents a select query, e.g.: + * Query interface which represents a select query, e.g.: * {@code SELECT FROM WHERE } * * @author James Ahlborn */ -public class SelectQuery extends BaseSelectQuery +public interface SelectQuery extends BaseSelectQuery { - - public SelectQuery(String name, List rows, int objectId) { - super(name, rows, objectId, Type.SELECT); - } - - @Override - protected void toSQLString(StringBuilder builder) - { - toSQLSelectString(builder, true); - } - } diff --git a/src/java/com/healthmarketscience/jackcess/query/UnionQuery.java b/src/java/com/healthmarketscience/jackcess/query/UnionQuery.java index cd75906..6b8a1fb 100644 --- a/src/java/com/healthmarketscience/jackcess/query/UnionQuery.java +++ b/src/java/com/healthmarketscience/jackcess/query/UnionQuery.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2008 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,81 +15,26 @@ 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.query; import java.util.List; -import static com.healthmarketscience.jackcess.query.QueryFormat.*; - /** - * Concrete Query subclass which represents a UNION query, e.g.: + * Query interface which represents a UNION query, e.g.: * {@code SELECT UNION SELECT } * * @author James Ahlborn */ -public class UnionQuery extends Query +public interface UnionQuery extends Query { - public UnionQuery(String name, List rows, int objectId) { - super(name, rows, objectId, Type.UNION); - } - - public String getUnionType() { - return(hasFlag(UNION_FLAG) ? DEFAULT_TYPE : "ALL"); - } - - public String getUnionString1() { - return getUnionString(UNION_PART1); - } - - public String getUnionString2() { - return getUnionString(UNION_PART2); - } - - @Override - public List getOrderings() { - return super.getOrderings(); - } - - private String getUnionString(String id) { - for(Row row : getTableRows()) { - if(id.equals(row.name2)) { - return cleanUnionString(row.expression); - } - } - throw new IllegalStateException( - "Could not find union query with id " + id); - } + public String getUnionType(); - @Override - protected void toSQLString(StringBuilder builder) - { - builder.append(getUnionString1()).append(NEWLINE) - .append("UNION "); - String unionType = getUnionType(); - if(!DEFAULT_TYPE.equals(unionType)) { - builder.append(unionType).append(' '); - } - builder.append(getUnionString2()); - List orderings = getOrderings(); - if(!orderings.isEmpty()) { - builder.append(NEWLINE).append("ORDER BY ").append(orderings); - } - } + public String getUnionString1(); - private static String cleanUnionString(String str) - { - return str.trim().replaceAll("[\r\n]+", NEWLINE); - } + public String getUnionString2(); + public List getOrderings(); } diff --git a/src/java/com/healthmarketscience/jackcess/query/UpdateQuery.java b/src/java/com/healthmarketscience/jackcess/query/UpdateQuery.java index 747a9b3..f2990a1 100644 --- a/src/java/com/healthmarketscience/jackcess/query/UpdateQuery.java +++ b/src/java/com/healthmarketscience/jackcess/query/UpdateQuery.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2008 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,79 +15,29 @@ 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.query; import java.util.List; -import static com.healthmarketscience.jackcess.query.QueryFormat.*; - /** - * Concrete Query subclass which represents a row update query, e.g.: + * Query interface which represents a row update query, e.g.: * {@code UPDATE
SET } * * @author James Ahlborn */ -public class UpdateQuery extends Query +public interface UpdateQuery extends Query { - public UpdateQuery(String name, List rows, int objectId) { - super(name, rows, objectId, Type.UPDATE); - } - - public List getTargetTables() - { - return super.getFromTables(); - } - - public String getRemoteDbPath() - { - return super.getFromRemoteDbPath(); - } - - public String getRemoteDbType() - { - return super.getFromRemoteDbType(); - } - - public List getNewValues() - { - return (new RowFormatter(getColumnRows()) { - @Override protected void format(StringBuilder builder, Row row) { - toOptionalQuotedExpr(builder, row.name2, true) - .append(" = ").append(row.expression); - } - }).format(); - } - - @Override - public String getWhereExpression() - { - return super.getWhereExpression(); - } + public List getTargetTables(); - @Override - protected void toSQLString(StringBuilder builder) - { - builder.append("UPDATE ").append(getTargetTables()); - toRemoteDb(builder, getRemoteDbPath(), getRemoteDbType()); + public String getRemoteDbPath(); - builder.append(NEWLINE).append("SET ").append(getNewValues()); + public String getRemoteDbType(); - String whereExpr = getWhereExpression(); - if(whereExpr != null) { - builder.append(NEWLINE).append("WHERE ").append(whereExpr); - } - } + public List getNewValues(); + public String getWhereExpression(); } diff --git a/src/java/com/healthmarketscience/jackcess/CaseInsensitiveColumnMatcher.java b/src/java/com/healthmarketscience/jackcess/util/CaseInsensitiveColumnMatcher.java similarity index 78% rename from src/java/com/healthmarketscience/jackcess/CaseInsensitiveColumnMatcher.java rename to src/java/com/healthmarketscience/jackcess/util/CaseInsensitiveColumnMatcher.java index a88d0d2..63e4608 100644 --- a/src/java/com/healthmarketscience/jackcess/CaseInsensitiveColumnMatcher.java +++ b/src/java/com/healthmarketscience/jackcess/util/CaseInsensitiveColumnMatcher.java @@ -18,10 +18,15 @@ USA */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; import java.io.IOException; +import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.RuntimeIOException; +import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.impl.ColumnImpl; + /** * Concrete implementation of ColumnMatcher which tests textual columns * case-insensitively ({@link DataType#TEXT} and {@link DataType#MEMO}), and @@ -49,15 +54,15 @@ public class CaseInsensitiveColumnMatcher implements ColumnMatcher { // convert both values to Strings and compare case-insensitively try { - CharSequence cs1 = Column.toCharSequence(value1); - CharSequence cs2 = Column.toCharSequence(value2); + CharSequence cs1 = ColumnImpl.toCharSequence(value1); + CharSequence cs2 = ColumnImpl.toCharSequence(value2); return((cs1 == cs2) || ((cs1 != null) && (cs2 != null) && cs1.toString().equalsIgnoreCase(cs2.toString()))); } catch(IOException e) { - throw new IllegalStateException("Could not read column " + columnName - + " value", e); + throw new RuntimeIOException("Could not read column " + columnName + + " value", e); } } diff --git a/src/java/com/healthmarketscience/jackcess/ColumnMatcher.java b/src/java/com/healthmarketscience/jackcess/util/ColumnMatcher.java similarity index 93% rename from src/java/com/healthmarketscience/jackcess/ColumnMatcher.java rename to src/java/com/healthmarketscience/jackcess/util/ColumnMatcher.java index 5532e7a..664dbd1 100644 --- a/src/java/com/healthmarketscience/jackcess/ColumnMatcher.java +++ b/src/java/com/healthmarketscience/jackcess/util/ColumnMatcher.java @@ -18,7 +18,9 @@ USA */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; + +import com.healthmarketscience.jackcess.Table; /** * Interface for handling comparisons between column values. diff --git a/src/java/com/healthmarketscience/jackcess/DebugErrorHandler.java b/src/java/com/healthmarketscience/jackcess/util/DebugErrorHandler.java similarity index 81% rename from src/java/com/healthmarketscience/jackcess/DebugErrorHandler.java rename to src/java/com/healthmarketscience/jackcess/util/DebugErrorHandler.java index 2fbd478..36b3941 100644 --- a/src/java/com/healthmarketscience/jackcess/DebugErrorHandler.java +++ b/src/java/com/healthmarketscience/jackcess/util/DebugErrorHandler.java @@ -25,10 +25,14 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; import java.io.IOException; +import javax.xml.stream.Location; +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.impl.ByteUtil; +import com.healthmarketscience.jackcess.util.ReplacementErrorHandler; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -60,21 +64,19 @@ public class DebugErrorHandler extends ReplacementErrorHandler } @Override - public Object handleRowError(Column column, - byte[] columnData, - Table.RowState rowState, - Exception error) + public Object handleRowError(Column column, byte[] columnData, + Location location, Exception error) throws IOException { if(LOG.isDebugEnabled()) { LOG.debug("Failed reading column " + column + ", row " + - rowState + ", bytes " + + location + ", bytes " + ((columnData != null) ? ByteUtil.toHexString(columnData) : "null"), error); } - return super.handleRowError(column, columnData, rowState, error); + return super.handleRowError(column, columnData, location, error); } } diff --git a/src/java/com/healthmarketscience/jackcess/util/EntryIterableBuilder.java b/src/java/com/healthmarketscience/jackcess/util/EntryIterableBuilder.java new file mode 100644 index 0000000..54b88a8 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/util/EntryIterableBuilder.java @@ -0,0 +1,114 @@ +/* +Copyright (c) 2013 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.Collection; +import java.util.HashSet; +import java.util.Iterator; + +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.IndexCursor; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.impl.IndexCursorImpl; + +/** + * Builder style class for constructing an IndexCursor entry + * Iterable/Iterator. + * + * @author James Ahlborn + */ +public class EntryIterableBuilder implements Iterable +{ + private final IndexCursor _cursor; + + private Collection _columnNames; + private Object[] _entryValues; + private ColumnMatcher _columnMatcher; + + public EntryIterableBuilder(IndexCursor cursor, Object... entryValues) { + _cursor = cursor; + _entryValues = entryValues; + } + + public Collection getColumnNames() { + return _columnNames; + } + + public ColumnMatcher getColumnMatcher() { + return _columnMatcher; + } + + public Object[] getEntryValues() { + return _entryValues; + } + + public EntryIterableBuilder setColumnNames(Collection columnNames) { + _columnNames = columnNames; + return this; + } + + public EntryIterableBuilder addColumnNames(Iterable columnNames) { + if(columnNames != null) { + for(String name : columnNames) { + addColumnName(name); + } + } + return this; + } + + public EntryIterableBuilder addColumns(Iterable cols) { + if(cols != null) { + for(Column col : cols) { + addColumnName(col.getName()); + } + } + return this; + } + + public EntryIterableBuilder addColumnNames(String... columnNames) { + if(columnNames != null) { + for(String name : columnNames) { + addColumnName(name); + } + } + return this; + } + + private void addColumnName(String columnName) { + if(_columnNames == null) { + _columnNames = new HashSet(); + } + _columnNames.add(columnName); + } + + public EntryIterableBuilder setEntryValues(Object... entryValues) { + _entryValues = entryValues; + return this; + } + + public EntryIterableBuilder setColumnMatcher(ColumnMatcher columnMatcher) { + _columnMatcher = columnMatcher; + return this; + } + + public Iterator iterator() { + return ((IndexCursorImpl)_cursor).entryIterator(this); + } +} diff --git a/src/java/com/healthmarketscience/jackcess/ErrorHandler.java b/src/java/com/healthmarketscience/jackcess/util/ErrorHandler.java similarity index 63% rename from src/java/com/healthmarketscience/jackcess/ErrorHandler.java rename to src/java/com/healthmarketscience/jackcess/util/ErrorHandler.java index 25c4d9d..368b247 100644 --- a/src/java/com/healthmarketscience/jackcess/ErrorHandler.java +++ b/src/java/com/healthmarketscience/jackcess/util/ErrorHandler.java @@ -25,21 +25,40 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; import java.io.IOException; +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.Table; + /** * 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. + * instance used is {@link #DEFAULT}, which just rethrows any exceptions + * encountered. * * @author James Ahlborn */ public interface ErrorHandler { + /** + * default error handler used if none provided (just rethrows exception) + * @usage _general_field_ + */ + public static final ErrorHandler DEFAULT = new ErrorHandler() { + public Object handleRowError(Column column, byte[] columnData, + Location location, Exception error) + throws IOException + { + // really can only be RuntimeException or IOException + if(error instanceof IOException) { + throw (IOException)error; + } + throw (RuntimeException)error; + } + }; /** * Handles an error encountered while reading a column of data from a Table @@ -51,15 +70,30 @@ public interface ErrorHandler * @param columnData the actual column data for the column being read (which * may be {@code null} depending on when the exception * was thrown during the reading process) - * @param rowState the current row state for the caller + * @param location the current location of the error * @param error the error that was encountered * * @return replacement for this row's column */ public Object handleRowError(Column column, byte[] columnData, - Table.RowState rowState, + Location location, Exception error) throws IOException; + /** + * Provides location information for an error. + */ + public interface Location + { + /** + * @return the table in which the error occurred + */ + public Table getTable(); + + /** + * Contains details about the errored row, useful for debugging. + */ + public String toString(); + } } diff --git a/src/java/com/healthmarketscience/jackcess/ExportFilter.java b/src/java/com/healthmarketscience/jackcess/util/ExportFilter.java similarity index 91% rename from src/java/com/healthmarketscience/jackcess/ExportFilter.java rename to src/java/com/healthmarketscience/jackcess/util/ExportFilter.java index f145fd5..b9b8607 100644 --- a/src/java/com/healthmarketscience/jackcess/ExportFilter.java +++ b/src/java/com/healthmarketscience/jackcess/util/ExportFilter.java @@ -25,10 +25,11 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; import java.io.IOException; import java.util.List; +import com.healthmarketscience.jackcess.Column; /** * Interface which allows customization of the behavior of the @@ -46,7 +47,8 @@ public interface ExportFilter { * modified and returned * @return the columns to use when creating the export file */ - public List filterColumns(List columns) throws IOException; + public List filterColumns(List columns) + throws IOException; /** * The desired values for the row. diff --git a/src/java/com/healthmarketscience/jackcess/ExportUtil.java b/src/java/com/healthmarketscience/jackcess/util/ExportUtil.java similarity index 96% rename from src/java/com/healthmarketscience/jackcess/ExportUtil.java rename to src/java/com/healthmarketscience/jackcess/util/ExportUtil.java index ad8d502..059347d 100644 --- a/src/java/com/healthmarketscience/jackcess/ExportUtil.java +++ b/src/java/com/healthmarketscience/jackcess/util/ExportUtil.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; import java.io.BufferedWriter; import java.io.File; @@ -36,9 +36,14 @@ import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.regex.Pattern; +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.Cursor; +import com.healthmarketscience.jackcess.CursorBuilder; +import com.healthmarketscience.jackcess.Database; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.impl.ByteUtil; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -268,7 +273,7 @@ public class ExportUtil { char quote, ExportFilter filter) throws IOException { - exportWriter(Cursor.createCursor(db.getTable(tableName)), out, header, + exportWriter(CursorBuilder.createCursor(db.getTable(tableName)), out, header, delim, quote, filter); } @@ -303,7 +308,7 @@ public class ExportUtil { "(?:" + Pattern.quote(delimiter) + ")|(?:" + Pattern.quote("" + quote) + ")|(?:[\n\r])"); - List origCols = cursor.getTable().getColumns(); + List origCols = cursor.getTable().getColumns(); List columns = new ArrayList(origCols); columns = filter.filterColumns(columns); @@ -331,8 +336,8 @@ public class ExportUtil { } // print the data rows - Map row; Object[] unfilteredRowData = new Object[columns.size()]; + Row row; while ((row = cursor.getNextRow(columnNames)) != null) { // fill raw row data in array diff --git a/src/java/com/healthmarketscience/jackcess/ImportFilter.java b/src/java/com/healthmarketscience/jackcess/util/ImportFilter.java similarity index 88% rename from src/java/com/healthmarketscience/jackcess/ImportFilter.java rename to src/java/com/healthmarketscience/jackcess/util/ImportFilter.java index 144b481..a7131b7 100644 --- a/src/java/com/healthmarketscience/jackcess/ImportFilter.java +++ b/src/java/com/healthmarketscience/jackcess/util/ImportFilter.java @@ -25,12 +25,13 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; import java.io.IOException; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.List; +import com.healthmarketscience.jackcess.ColumnBuilder; /** * Interface which allows customization of the behavior of the @@ -48,8 +49,8 @@ public interface ImportFilter { * JDBC source * @return the columns to use when creating the import table */ - public List filterColumns(List destColumns, - ResultSetMetaData srcColumns) + public List filterColumns(List destColumns, + ResultSetMetaData srcColumns) throws SQLException, IOException; /** diff --git a/src/java/com/healthmarketscience/jackcess/ImportUtil.java b/src/java/com/healthmarketscience/jackcess/util/ImportUtil.java similarity index 95% rename from src/java/com/healthmarketscience/jackcess/ImportUtil.java rename to src/java/com/healthmarketscience/jackcess/util/ImportUtil.java index 0fc1802..65ee700 100644 --- a/src/java/com/healthmarketscience/jackcess/ImportUtil.java +++ b/src/java/com/healthmarketscience/jackcess/util/ImportUtil.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; import java.io.BufferedReader; import java.io.EOFException; @@ -41,6 +41,11 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.healthmarketscience.jackcess.ColumnBuilder; +import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.Database; +import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.TableBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -68,13 +73,13 @@ public class ImportUtil * * @return a List of Columns */ - public static List toColumns(ResultSetMetaData md) + public static List toColumns(ResultSetMetaData md) throws SQLException { - List columns = new LinkedList(); + List columns = new LinkedList(); for (int i = 1; i <= md.getColumnCount(); i++) { - Column column = new Column(); - column.setName(Database.escapeIdentifier(md.getColumnName(i))); + ColumnBuilder column = new ColumnBuilder(md.getColumnName(i)) + .escapeName(); int lengthInUnits = md.getColumnDisplaySize(i); column.setSQLType(md.getColumnType(i), lengthInUnits); DataType type = column.getType(); @@ -164,10 +169,10 @@ public class ImportUtil { ResultSetMetaData md = source.getMetaData(); - name = Database.escapeIdentifier(name); + name = TableBuilder.escapeIdentifier(name); Table table = null; if(!useExistingTable || ((table = db.getTable(name)) == null)) { - List columns = toColumns(md); + List columns = toColumns(md); table = createUniqueTable(db, name, columns, md, filter); } @@ -453,11 +458,11 @@ public class ImportUtil Pattern delimPat = Pattern.compile(delim); try { - name = Database.escapeIdentifier(name); + name = TableBuilder.escapeIdentifier(name); Table table = null; if(!useExistingTable || ((table = db.getTable(name)) == null)) { - List columns = new LinkedList(); + List columns = new LinkedList(); Object[] columnNames = splitLine(line, delimPat, quote, in, 0); for (int i = 0; i < columnNames.length; i++) { @@ -591,7 +596,7 @@ public class ImportUtil * Returns a new table with a unique name and the given table definition. */ private static Table createUniqueTable(Database db, String name, - List columns, + List columns, ResultSetMetaData md, ImportFilter filter) throws IOException, SQLException @@ -603,9 +608,9 @@ public class ImportUtil name = baseName + (counter++); } - db.createTable(name, filter.filterColumns(columns, md)); - - return db.getTable(name); + return new TableBuilder(name) + .addColumns(filter.filterColumns(columns, md)) + .toTable(db); } /** diff --git a/src/java/com/healthmarketscience/jackcess/util/IterableBuilder.java b/src/java/com/healthmarketscience/jackcess/util/IterableBuilder.java new file mode 100644 index 0000000..089d8da --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/util/IterableBuilder.java @@ -0,0 +1,186 @@ +/* +Copyright (c) 2013 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.AbstractMap; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; + +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.Cursor; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.impl.CursorImpl; + +/** + * Builder style class for constructing a Cursor Iterable/Iterator. + * + * @author James Ahlborn + */ +public class IterableBuilder implements Iterable +{ + public enum Type { + SIMPLE, COLUMN_MATCH, ROW_MATCH; + } + + private final Cursor _cursor; + private Type _type = Type.SIMPLE; + private boolean _forward = true; + private boolean _reset = true; + private Collection _columnNames; + private ColumnMatcher _columnMatcher; + private Object _matchPattern; + + public IterableBuilder(Cursor cursor) { + _cursor = cursor; + } + + public Collection getColumnNames() { + return _columnNames; + } + + public ColumnMatcher getColumnMatcher() { + return _columnMatcher; + } + + public boolean isForward() { + return _forward; + } + + public boolean isReset() { + return _reset; + } + + /** + * @usage _advanced_method_ + */ + public Object getMatchPattern() { + return _matchPattern; + } + + /** + * @usage _advanced_method_ + */ + public Type getType() { + return _type; + } + + public IterableBuilder forward() { + return setForward(true); + } + + public IterableBuilder reverse() { + return setForward(false); + } + + public IterableBuilder setForward(boolean forward) { + _forward = forward; + return this; + } + + public IterableBuilder reset(boolean reset) { + _reset = reset; + return this; + } + + public IterableBuilder setColumnNames(Collection columnNames) { + _columnNames = columnNames; + return this; + } + + public IterableBuilder addColumnNames(Iterable columnNames) { + if(columnNames != null) { + for(String name : columnNames) { + addColumnName(name); + } + } + return this; + } + + public IterableBuilder addColumns(Iterable cols) { + if(cols != null) { + for(Column col : cols) { + addColumnName(col.getName()); + } + } + return this; + } + + public IterableBuilder addColumnNames(String... columnNames) { + if(columnNames != null) { + for(String name : columnNames) { + addColumnName(name); + } + } + return this; + } + + private void addColumnName(String columnName) { + if(_columnNames == null) { + _columnNames = new HashSet(); + } + _columnNames.add(columnName); + } + + public IterableBuilder setMatchPattern(Column columnPattern, + Object valuePattern) { + _type = Type.COLUMN_MATCH; + _matchPattern = new AbstractMap.SimpleImmutableEntry( + columnPattern, valuePattern); + return this; + } + + public IterableBuilder setMatchPattern(String columnNamePattern, + Object valuePattern) { + return setMatchPattern(_cursor.getTable().getColumn(columnNamePattern), + valuePattern); + } + + public IterableBuilder setMatchPattern(Map rowPattern) { + _type = Type.ROW_MATCH; + _matchPattern = rowPattern; + return this; + } + + public IterableBuilder addMatchPattern(String columnNamePattern, + Object valuePattern) + { + _type = Type.ROW_MATCH; + @SuppressWarnings("unchecked") + Map matchPattern = ((Map)_matchPattern); + if(matchPattern == null) { + matchPattern = new HashMap(); + _matchPattern = matchPattern; + } + matchPattern.put(columnNamePattern, valuePattern); + return this; + } + + public IterableBuilder setColumnMatcher(ColumnMatcher columnMatcher) { + _columnMatcher = columnMatcher; + return this; + } + + public Iterator iterator() { + return ((CursorImpl)_cursor).iterator(this); + } +} diff --git a/src/java/com/healthmarketscience/jackcess/Joiner.java b/src/java/com/healthmarketscience/jackcess/util/Joiner.java similarity index 83% rename from src/java/com/healthmarketscience/jackcess/Joiner.java rename to src/java/com/healthmarketscience/jackcess/util/Joiner.java index dc3f4ba..02aa051 100644 --- a/src/java/com/healthmarketscience/jackcess/Joiner.java +++ b/src/java/com/healthmarketscience/jackcess/util/Joiner.java @@ -17,7 +17,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; import java.io.IOException; import java.util.Collection; @@ -26,6 +26,14 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import com.healthmarketscience.jackcess.CursorBuilder; +import com.healthmarketscience.jackcess.Index; +import com.healthmarketscience.jackcess.IndexCursor; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.RuntimeIOException; +import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.impl.IndexImpl; + /** * Utility for finding rows based on pre-defined, foreign-key table * relationships. @@ -35,7 +43,7 @@ import java.util.Map; public class Joiner { private final Index _fromIndex; - private final List _fromCols; + private final List _fromCols; private final IndexCursor _toCursor; private final Object[] _entryValues; @@ -73,7 +81,7 @@ public class Joiner throws IOException { Index toIndex = fromIndex.getReferencedIndex(); - IndexCursor toCursor = IndexCursor.createCursor( + IndexCursor toCursor = CursorBuilder.createCursor( toIndex.getTable(), toIndex); // text lookups are always case-insensitive toCursor.setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE); @@ -110,7 +118,7 @@ public class Joiner return _toCursor; } - public List getColumns() { + public List getColumns() { // note, this list is already unmodifiable, no need to re-wrap return _fromCols; } @@ -127,8 +135,9 @@ public class Joiner /** * Returns {@code true} if the "to" table has any rows based on the given * columns in the "from" table, {@code false} otherwise. + * @usage _intermediate_method_ */ - boolean hasRows(Object[] fromRow) throws IOException { + public boolean hasRows(Object[] fromRow) throws IOException { toEntryValues(fromRow); return _toCursor.findFirstRowByEntry(_entryValues); } @@ -140,7 +149,7 @@ public class Joiner * @param fromRow row from the "from" table (which must include the relevant * columns for this join relationship) */ - public Map findFirstRow(Map fromRow) + public Row findFirstRow(Map fromRow) throws IOException { return findFirstRow(fromRow, null); @@ -155,7 +164,7 @@ public class Joiner * columns for this join relationship) * @param columnNames desired columns in the from table row */ - public Map findFirstRow(Map fromRow, + public Row findFirstRow(Map fromRow, Collection columnNames) throws IOException { @@ -169,7 +178,7 @@ public class Joiner * @param fromRow row from the "from" table (which must include the relevant * columns for this join relationship) */ - public Iterator> findRows(Map fromRow) + public Iterator findRows(Map fromRow) { return findRows(fromRow, null); } @@ -182,11 +191,12 @@ public class Joiner * columns for this join relationship) * @param columnNames desired columns in the from table row */ - public Iterator> findRows(Map fromRow, - Collection columnNames) + public Iterator findRows(Map fromRow, + Collection columnNames) { toEntryValues(fromRow); - return _toCursor.entryIterator(columnNames, _entryValues); + return _toCursor.newEntryIterable(_entryValues) + .setColumnNames(columnNames).iterator(); } /** @@ -196,12 +206,14 @@ public class Joiner * @param fromRow row from the "from" table (which must include the relevant * columns for this join relationship) * @param columnNames desired columns in the from table row + * @usage _intermediate_method_ */ - Iterator> findRows(Object[] fromRow, - Collection columnNames) + public Iterator findRows(Object[] fromRow, + Collection columnNames) { toEntryValues(fromRow); - return _toCursor.entryIterator(columnNames, _entryValues); + return _toCursor.newEntryIterable(_entryValues) + .setColumnNames(columnNames).iterator(); } /** @@ -210,10 +222,10 @@ public class Joiner * * @param fromRow row from the "from" table (which must include the relevant * columns for this join relationship) - * @throws IllegalStateException if an IOException is thrown by one of the + * @throws RuntimeIOException if an IOException is thrown by one of the * operations, the actual exception will be contained within */ - public Iterable> findRowsIterable(Map fromRow) + public Iterable findRowsIterable(Map fromRow) { return findRowsIterable(fromRow, null); } @@ -225,14 +237,14 @@ public class Joiner * @param fromRow row from the "from" table (which must include the relevant * columns for this join relationship) * @param columnNames desired columns in the from table row - * @throws IllegalStateException if an IOException is thrown by one of the + * @throws RuntimeIOException if an IOException is thrown by one of the * operations, the actual exception will be contained within */ - public Iterable> findRowsIterable( + public Iterable findRowsIterable( final Map fromRow, final Collection columnNames) { - return new Iterable>() { - public Iterator> iterator() { + return new Iterable() { + public Iterator iterator() { return findRows(fromRow, columnNames); } }; @@ -259,8 +271,9 @@ public class Joiner * columns for this join relationship) * @return {@code true} if any "to" rows were deleted, {@code false} * otherwise + * @usage _intermediate_method_ */ - boolean deleteRows(Object[] fromRow) throws IOException { + public boolean deleteRows(Object[] fromRow) throws IOException { return deleteRowsImpl(findRows(fromRow, Collections.emptySet())); } @@ -268,7 +281,7 @@ public class Joiner * Deletes all the rows and returns whether or not any "to"" rows were * deleted. */ - private static boolean deleteRowsImpl(Iterator> iter) + private static boolean deleteRowsImpl(Iterator iter) throws IOException { boolean removed = false; @@ -310,7 +323,7 @@ public class Joiner String fromType = "] (primary)"; String toType = "] (secondary)"; - if(!_fromIndex.getReference().isPrimaryTable()) { + if(!((IndexImpl)_fromIndex).getReference().isPrimaryTable()) { fromType = "] (secondary)"; toType = "] (primary)"; } @@ -324,7 +337,7 @@ public class Joiner sb.append(fromType); sb.append(" to ").append(getToTable().getName()).append("["); - List toCols = _toCursor.getIndex().getColumns(); + List toCols = _toCursor.getIndex().getColumns(); sb.append(toCols.get(0).getName()); for(int i = 1; i < toCols.size(); ++i) { sb.append(",").append(toCols.get(i).getName()); diff --git a/src/java/com/healthmarketscience/jackcess/util/LinkResolver.java b/src/java/com/healthmarketscience/jackcess/util/LinkResolver.java new file mode 100644 index 0000000..512069e --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/util/LinkResolver.java @@ -0,0 +1,54 @@ +/* +Copyright (c) 2011 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.io.File; +import java.io.IOException; + +import com.healthmarketscience.jackcess.Database; +import com.healthmarketscience.jackcess.DatabaseBuilder; + +/** + * Resolver for linked databases. + * + * @author James Ahlborn + */ +public interface LinkResolver +{ + /** + * default link resolver used if none provided + * @usage _general_field_ + */ + public static final LinkResolver DEFAULT = new LinkResolver() { + public Database resolveLinkedDatabase(Database linkerDb, + String linkeeFileName) + throws IOException + { + return DatabaseBuilder.open(new File(linkeeFileName)); + } + }; + + /** + * Returns the appropriate Database instance for the linkeeFileName from the + * given linkerDb. + */ + public Database resolveLinkedDatabase(Database linkerDb, String linkeeFileName) + throws IOException; +} diff --git a/src/java/com/healthmarketscience/jackcess/MemFileChannel.java b/src/java/com/healthmarketscience/jackcess/util/MemFileChannel.java similarity index 96% rename from src/java/com/healthmarketscience/jackcess/MemFileChannel.java rename to src/java/com/healthmarketscience/jackcess/util/MemFileChannel.java index 719a793..3a583e5 100644 --- a/src/java/com/healthmarketscience/jackcess/MemFileChannel.java +++ b/src/java/com/healthmarketscience/jackcess/util/MemFileChannel.java @@ -17,7 +17,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; import java.io.File; import java.io.IOException; @@ -33,6 +33,10 @@ import java.nio.channels.NonWritableChannelException; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; +import com.healthmarketscience.jackcess.Database; +import com.healthmarketscience.jackcess.DatabaseBuilder; +import com.healthmarketscience.jackcess.impl.DatabaseImpl; + /** * FileChannel implementation which maintains the entire "file" in memory. * This enables working with a Database entirely in memory (for situations @@ -93,7 +97,7 @@ public class MemFileChannel extends FileChannel * affect the original File source. */ public static MemFileChannel newChannel(File file) throws IOException { - return newChannel(file, Database.RW_CHANNEL_MODE); + return newChannel(file, DatabaseImpl.RW_CHANNEL_MODE); } /** @@ -109,7 +113,7 @@ public class MemFileChannel extends FileChannel FileChannel in = null; try { return newChannel(in = new RandomAccessFile( - file, Database.RO_CHANNEL_MODE).getChannel(), + file, DatabaseImpl.RO_CHANNEL_MODE).getChannel(), mode); } finally { if(in != null) { @@ -127,7 +131,7 @@ public class MemFileChannel extends FileChannel * given InputStream. */ public static MemFileChannel newChannel(InputStream in) throws IOException { - return newChannel(in, Database.RW_CHANNEL_MODE); + return newChannel(in, DatabaseImpl.RW_CHANNEL_MODE); } /** @@ -148,7 +152,7 @@ public class MemFileChannel extends FileChannel public static MemFileChannel newChannel(ReadableByteChannel in) throws IOException { - return newChannel(in, Database.RW_CHANNEL_MODE); + return newChannel(in, DatabaseImpl.RW_CHANNEL_MODE); } /** diff --git a/src/java/com/healthmarketscience/jackcess/ReplacementErrorHandler.java b/src/java/com/healthmarketscience/jackcess/util/ReplacementErrorHandler.java similarity index 86% rename from src/java/com/healthmarketscience/jackcess/ReplacementErrorHandler.java rename to src/java/com/healthmarketscience/jackcess/util/ReplacementErrorHandler.java index bdb003c..0658447 100644 --- a/src/java/com/healthmarketscience/jackcess/ReplacementErrorHandler.java +++ b/src/java/com/healthmarketscience/jackcess/util/ReplacementErrorHandler.java @@ -25,9 +25,11 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; import java.io.IOException; +import javax.xml.stream.Location; +import com.healthmarketscience.jackcess.Column; /** * Simple implementation of an ErrorHandler which always returns the @@ -56,10 +58,8 @@ public class ReplacementErrorHandler implements ErrorHandler _replacement = replacement; } - public Object handleRowError(Column column, - byte[] columnData, - Table.RowState rowState, - Exception error) + public Object handleRowError(Column column, byte[] columnData, + Location location, Exception error) throws IOException { return _replacement; diff --git a/src/java/com/healthmarketscience/jackcess/RowFilter.java b/src/java/com/healthmarketscience/jackcess/util/RowFilter.java similarity index 79% rename from src/java/com/healthmarketscience/jackcess/RowFilter.java rename to src/java/com/healthmarketscience/jackcess/util/RowFilter.java index 3a537af..fd13c13 100644 --- a/src/java/com/healthmarketscience/jackcess/RowFilter.java +++ b/src/java/com/healthmarketscience/jackcess/util/RowFilter.java @@ -25,12 +25,14 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; import java.util.Iterator; import java.util.Map; +import com.healthmarketscience.jackcess.Column; import org.apache.commons.lang.ObjectUtils; +import com.healthmarketscience.jackcess.Row; /** @@ -48,7 +50,7 @@ public abstract class RowFilter * {@code false} otherwise. * @param row current row to test for inclusion in the filter */ - public abstract boolean matches(Map row); + public abstract boolean matches(Row row); /** * Returns an iterable which filters the given iterable based on this @@ -58,8 +60,7 @@ public abstract class RowFilter * * @return a filtering iterable */ - public Iterable> apply( - Iterable> iterable) + public Iterable apply(Iterable iterable) { return new FilterIterable(iterable); } @@ -75,13 +76,13 @@ public abstract class RowFilter * @return a filter which matches table rows which match the values in the * row pattern */ - public static RowFilter matchPattern(final Map rowPattern) + public static RowFilter matchPattern(final Map rowPattern) { return new RowFilter() { @Override - public boolean matches(Map row) + public boolean matches(Row row) { - for(Map.Entry e : rowPattern.entrySet()) { + for(Map.Entry e : rowPattern.entrySet()) { if(!ObjectUtils.equals(e.getValue(), row.get(e.getKey()))) { return false; } @@ -101,11 +102,12 @@ public abstract class RowFilter * @return a filter which matches table rows which match the value in the * row pattern */ - public static RowFilter matchPattern(final Column columnPattern, final Object valuePattern) + public static RowFilter matchPattern(final Column columnPattern, + final Object valuePattern) { return new RowFilter() { @Override - public boolean matches(Map row) + public boolean matches(Row row) { return ObjectUtils.equals(valuePattern, columnPattern.getRowValue(row)); } @@ -125,7 +127,7 @@ public abstract class RowFilter { return new RowFilter() { @Override - public boolean matches(Map row) + public boolean matches(Row row) { return !filter.matches(row); } @@ -143,22 +145,23 @@ public abstract class RowFilter * @return a filtering iterable (or the given iterable if a {@code null} * filter was given) */ - public static Iterable> apply( - RowFilter rowFilter, - Iterable> iterable) + @SuppressWarnings("unchecked") + public static Iterable apply(RowFilter rowFilter, + Iterable iterable) { - return((rowFilter != null) ? rowFilter.apply(iterable) : iterable); + return((rowFilter != null) ? rowFilter.apply(iterable) : + (Iterable)iterable); } /** * Iterable which creates a filtered view of a another row iterable. */ - private class FilterIterable implements Iterable> + private class FilterIterable implements Iterable { - private final Iterable> _iterable; + private final Iterable _iterable; - private FilterIterable(Iterable> iterable) + private FilterIterable(Iterable iterable) { _iterable = iterable; } @@ -169,12 +172,11 @@ public abstract class RowFilter * iterable, returning only rows for which the {@link RowFilter#matches} * method returns {@code true} */ - public Iterator> iterator() + public Iterator iterator() { - return new Iterator>() { - private final Iterator> _iter = - _iterable.iterator(); - private Map _next; + return new Iterator() { + private final Iterator _iter = _iterable.iterator(); + private Row _next; public boolean hasNext() { while(_iter.hasNext()) { @@ -187,7 +189,7 @@ public abstract class RowFilter return false; } - public Map next() { + public Row next() { return _next; } diff --git a/src/java/com/healthmarketscience/jackcess/SimpleColumnMatcher.java b/src/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java similarity index 89% rename from src/java/com/healthmarketscience/jackcess/SimpleColumnMatcher.java rename to src/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java index ff65317..2f069e0 100644 --- a/src/java/com/healthmarketscience/jackcess/SimpleColumnMatcher.java +++ b/src/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java @@ -18,8 +18,10 @@ USA */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; +import com.healthmarketscience.jackcess.util.ColumnMatcher; +import com.healthmarketscience.jackcess.Table; import org.apache.commons.lang.ObjectUtils; /** diff --git a/src/java/com/healthmarketscience/jackcess/SimpleExportFilter.java b/src/java/com/healthmarketscience/jackcess/util/SimpleExportFilter.java similarity index 93% rename from src/java/com/healthmarketscience/jackcess/SimpleExportFilter.java rename to src/java/com/healthmarketscience/jackcess/util/SimpleExportFilter.java index 3669a94..5e61d6d 100644 --- a/src/java/com/healthmarketscience/jackcess/SimpleExportFilter.java +++ b/src/java/com/healthmarketscience/jackcess/util/SimpleExportFilter.java @@ -25,10 +25,11 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; import java.io.IOException; import java.util.List; +import com.healthmarketscience.jackcess.Column; /** * Simple concrete implementation of ImportFilter which just returns the given diff --git a/src/java/com/healthmarketscience/jackcess/SimpleImportFilter.java b/src/java/com/healthmarketscience/jackcess/util/SimpleImportFilter.java similarity index 83% rename from src/java/com/healthmarketscience/jackcess/SimpleImportFilter.java rename to src/java/com/healthmarketscience/jackcess/util/SimpleImportFilter.java index ba7eabb..40b27ef 100644 --- a/src/java/com/healthmarketscience/jackcess/SimpleImportFilter.java +++ b/src/java/com/healthmarketscience/jackcess/util/SimpleImportFilter.java @@ -25,12 +25,14 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; import java.io.IOException; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.List; +import com.healthmarketscience.jackcess.ColumnBuilder; +import com.healthmarketscience.jackcess.util.ImportFilter; /** * Simple concrete implementation of ImportFilter which just returns the given @@ -44,9 +46,9 @@ public class SimpleImportFilter implements ImportFilter { public SimpleImportFilter() { } - - public List filterColumns(List destColumns, - ResultSetMetaData srcColumns) + + public List filterColumns(List destColumns, + ResultSetMetaData srcColumns) throws SQLException, IOException { return destColumns; diff --git a/test/src/java/com/healthmarketscience/jackcess/BigIndexTest.java b/test/src/java/com/healthmarketscience/jackcess/BigIndexTest.java index f6d1c0a..70d63d8 100644 --- a/test/src/java/com/healthmarketscience/jackcess/BigIndexTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/BigIndexTest.java @@ -35,43 +35,26 @@ import java.util.Random; import junit.framework.TestCase; import static com.healthmarketscience.jackcess.DatabaseTest.*; -import static com.healthmarketscience.jackcess.JetFormatTest.*; - +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; +import com.healthmarketscience.jackcess.impl.TableImpl; +import com.healthmarketscience.jackcess.impl.IndexImpl; /** * @author james */ public class BigIndexTest extends TestCase { - private String _oldBigIndexValue = null; - public BigIndexTest(String name) { super(name); } - - @Override - protected void setUp() { - _oldBigIndexValue = System.getProperty(Database.USE_BIG_INDEX_PROPERTY); - System.setProperty(Database.USE_BIG_INDEX_PROPERTY, - Boolean.TRUE.toString()); - } - - @Override - protected void tearDown() { - if (_oldBigIndexValue != null) { - System.setProperty(Database.USE_BIG_INDEX_PROPERTY, _oldBigIndexValue); - } else { - System.clearProperty(Database.USE_BIG_INDEX_PROPERTY); - } - } public void testComplexIndex() throws Exception { for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.COMP_INDEX, true)) { // this file has an index with "compressed" entries and node pages Database db = open(testDB); - Table t = db.getTable("Table1"); - Index index = t.getIndex("CD_AGENTE"); + TableImpl t = (TableImpl)db.getTable("Table1"); + IndexImpl index = t.getIndex("CD_AGENTE"); assertFalse(index.isInitialized()); assertEquals(512, countRows(t)); assertEquals(512, index.getIndexData().getEntryCount()); @@ -84,8 +67,8 @@ public class BigIndexTest extends TestCase { for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.BIG_INDEX)) { // this file has an index with "compressed" entries and node pages Database db = open(testDB); - Table t = db.getTable("Table1"); - Index index = t.getIndex("col1"); + TableImpl t = (TableImpl)db.getTable("Table1"); + IndexImpl index = t.getIndex("col1"); assertFalse(index.isInitialized()); assertEquals(0, countRows(t)); assertEquals(0, index.getIndexData().getEntryCount()); @@ -98,12 +81,9 @@ public class BigIndexTest extends TestCase { // copy to temp file and attempt to edit db = openCopy(testDB); - t = db.getTable("Table1"); + t = (TableImpl)db.getTable("Table1"); index = t.getIndex("col1"); - System.out.println("BigIndexTest: Index type: " + - index.getIndexData().getClass()); - // add 2,000 (pseudo) random entries to the table Random rand = new Random(13L); for(int i = 0; i < 2000; ++i) { @@ -131,10 +111,13 @@ public class BigIndexTest extends TestCase { } } - ((BigIndexData)index.getIndexData()).validate(); + index.getIndexData().validate(); db.flush(); - t = db.getTable("Table1"); + t = null; + System.gc(); + + t = (TableImpl)db.getTable("Table1"); index = t.getIndex("col1"); // make sure all entries are there and correctly ordered @@ -142,7 +125,7 @@ public class BigIndexTest extends TestCase { String prevValue = firstValue; int rowCount = 0; List firstTwo = new ArrayList(); - for(Map row : Cursor.createIndexCursor(t, index)) { + for(Map row : CursorBuilder.createCursor(t, index)) { String origVal = (String)row.get("col1"); String val = origVal; if(val == null) { @@ -159,10 +142,10 @@ public class BigIndexTest extends TestCase { assertEquals(2000, rowCount); - ((BigIndexData)index.getIndexData()).validate(); + index.getIndexData().validate(); // delete an entry in the middle - Cursor cursor = Cursor.createIndexCursor(t, index); + Cursor cursor = CursorBuilder.createCursor(t, index); for(int i = 0; i < (rowCount / 2); ++i) { assertTrue(cursor.moveToNextRow()); } @@ -176,17 +159,17 @@ public class BigIndexTest extends TestCase { cursor.deleteCurrentRow(); } - ((BigIndexData)index.getIndexData()).validate(); + index.getIndexData().validate(); List found = new ArrayList(); - for(Map row : Cursor.createIndexCursor(t, index)) { + for(Map row : CursorBuilder.createCursor(t, index)) { found.add((String)row.get("col1")); } assertEquals(firstTwo, found); // remove remaining entries - cursor = Cursor.createCursor(t); + cursor = CursorBuilder.createCursor(t); for(int i = 0; i < 2; ++i) { assertTrue(cursor.moveToNextRow()); cursor.deleteCurrentRow(); @@ -195,7 +178,7 @@ public class BigIndexTest extends TestCase { assertFalse(cursor.moveToNextRow()); assertFalse(cursor.moveToPreviousRow()); - ((BigIndexData)index.getIndexData()).validate(); + index.getIndexData().validate(); // add 50 (pseudo) random entries to the table rand = new Random(42L); @@ -208,14 +191,14 @@ public class BigIndexTest extends TestCase { t.addRow(nextVal, "this is some row data " + nextInt); } - ((BigIndexData)index.getIndexData()).validate(); + index.getIndexData().validate(); - cursor = Cursor.createIndexCursor(t, index); + cursor = CursorBuilder.createCursor(t, index); while(cursor.moveToNextRow()) { cursor.deleteCurrentRow(); } - ((BigIndexData)index.getIndexData()).validate(); + index.getIndexData().validate(); db.close(); diff --git a/test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java b/test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java index 8fea667..173a53c 100644 --- a/test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java @@ -26,13 +26,16 @@ import java.util.List; import java.util.Map; import static com.healthmarketscience.jackcess.DatabaseTest.*; -import static com.healthmarketscience.jackcess.JetFormatTest.*; import com.healthmarketscience.jackcess.complex.Attachment; import com.healthmarketscience.jackcess.complex.ComplexDataType; import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; import com.healthmarketscience.jackcess.complex.SingleValue; import com.healthmarketscience.jackcess.complex.UnsupportedValue; import com.healthmarketscience.jackcess.complex.Version; +import com.healthmarketscience.jackcess.impl.ByteUtil; +import com.healthmarketscience.jackcess.impl.ColumnImpl; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; +import com.healthmarketscience.jackcess.impl.PageChannel; import junit.framework.TestCase; @@ -97,7 +100,7 @@ public class ComplexColumnTest extends TestCase checkVersions(row8ValFk.get(), row8ValFk, "row8-memo", "row8-memo", upTime); - Cursor cursor = Cursor.createCursor(t1); + Cursor cursor = CursorBuilder.createCursor(t1); assertTrue(cursor.findFirstRow(t1.getColumn("id"), "row3")); ComplexValueForeignKey row3ValFk = (ComplexValueForeignKey) cursor.getCurrentRowValue(verCol); @@ -196,7 +199,7 @@ public class ComplexColumnTest extends TestCase checkAttachments(row8ValFk.get(), row8ValFk, "test_data.txt", "test_data2.txt"); - Cursor cursor = Cursor.createCursor(t1); + Cursor cursor = CursorBuilder.createCursor(t1); assertTrue(cursor.findFirstRow(t1.getColumn("id"), "row4")); ComplexValueForeignKey row4ValFk = (ComplexValueForeignKey) cursor.getCurrentRowValue(col); @@ -277,7 +280,7 @@ public class ComplexColumnTest extends TestCase row8ValFk.addMultiValue("value2"); checkMultiValues(row8ValFk.get(), row8ValFk, "value1", "value2"); - Cursor cursor = Cursor.createCursor(t1); + Cursor cursor = CursorBuilder.createCursor(t1); assertTrue(cursor.findFirstRow(t1.getColumn("id"), "row2")); ComplexValueForeignKey row2ValFk = (ComplexValueForeignKey) cursor.getCurrentRowValue(col); @@ -425,7 +428,7 @@ public class ComplexColumnTest extends TestCase UnsupportedValue v = values.get(i); assertEquals(1, v.getValues().size()); Object rv = v.get("Value"); - assertTrue(Column.isRawData(rv)); + assertTrue(ColumnImpl.isRawData(rv)); assertEquals(value, rv.toString()); } } diff --git a/test/src/java/com/healthmarketscience/jackcess/CursorBuilderTest.java b/test/src/java/com/healthmarketscience/jackcess/CursorBuilderTest.java index c1872fa..26d22e7 100644 --- a/test/src/java/com/healthmarketscience/jackcess/CursorBuilderTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/CursorBuilderTest.java @@ -29,7 +29,8 @@ package com.healthmarketscience.jackcess; import junit.framework.TestCase; -import static com.healthmarketscience.jackcess.JetFormatTest.*; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; +import com.healthmarketscience.jackcess.impl.IndexImpl; /** * @author James Ahlborn @@ -59,20 +60,20 @@ public class CursorBuilderTest extends TestCase { Database db = CursorTest.createTestIndexTable(indexCursorDB); Table table = db.getTable("test"); - Index idx = table.getIndexes().get(0); + IndexImpl idx = (IndexImpl)table.getIndexes().get(0); - Cursor expected = Cursor.createCursor(table); + Cursor expected = CursorBuilder.createCursor(table); Cursor found = new CursorBuilder(table).toCursor(); assertCursor(expected, found); - expected = Cursor.createIndexCursor(table, idx); + expected = CursorBuilder.createCursor(table, idx); found = new CursorBuilder(table) .setIndex(idx) .toCursor(); assertCursor(expected, found); - expected = Cursor.createIndexCursor(table, idx); + expected = CursorBuilder.createCursor(table, idx); found = new CursorBuilder(table) .setIndexByName("id") .toCursor(); @@ -86,7 +87,7 @@ public class CursorBuilderTest extends TestCase { // success } - expected = Cursor.createIndexCursor(table, idx); + expected = CursorBuilder.createCursor(table, idx); found = new CursorBuilder(table) .setIndexByColumns(table.getColumn("id")) .toCursor(); @@ -108,21 +109,21 @@ public class CursorBuilderTest extends TestCase { // success } - expected = Cursor.createCursor(table); + expected = CursorBuilder.createCursor(table); expected.beforeFirst(); found = new CursorBuilder(table) .beforeFirst() .toCursor(); assertCursor(expected, found); - expected = Cursor.createCursor(table); + expected = CursorBuilder.createCursor(table); expected.afterLast(); found = new CursorBuilder(table) .afterLast() .toCursor(); assertCursor(expected, found); - expected = Cursor.createCursor(table); + expected = CursorBuilder.createCursor(table); expected.moveNextRows(2); Cursor.Savepoint sp = expected.getSavepoint(); found = new CursorBuilder(table) @@ -131,7 +132,7 @@ public class CursorBuilderTest extends TestCase { .toCursor(); assertCursor(expected, found); - expected = Cursor.createIndexCursor(table, idx); + expected = CursorBuilder.createCursor(table, idx); expected.moveNextRows(2); sp = expected.getSavepoint(); found = new CursorBuilder(table) @@ -141,7 +142,7 @@ public class CursorBuilderTest extends TestCase { .toCursor(); assertCursor(expected, found); - expected = Cursor.createIndexCursor(table, idx, + expected = CursorBuilder.createCursor(table, idx, idx.constructIndexRowFromEntry(3), null); found = new CursorBuilder(table) @@ -150,7 +151,7 @@ public class CursorBuilderTest extends TestCase { .toCursor(); assertCursor(expected, found); - expected = Cursor.createIndexCursor(table, idx, + expected = CursorBuilder.createCursor(table, idx, idx.constructIndexRowFromEntry(3), false, idx.constructIndexRowFromEntry(7), diff --git a/test/src/java/com/healthmarketscience/jackcess/CursorTest.java b/test/src/java/com/healthmarketscience/jackcess/CursorTest.java index 897cf53..59de129 100644 --- a/test/src/java/com/healthmarketscience/jackcess/CursorTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/CursorTest.java @@ -38,7 +38,13 @@ import java.util.TreeSet; import static com.healthmarketscience.jackcess.Database.*; import static com.healthmarketscience.jackcess.DatabaseTest.*; -import static com.healthmarketscience.jackcess.JetFormatTest.*; +import com.healthmarketscience.jackcess.impl.JetFormatTest; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; +import com.healthmarketscience.jackcess.impl.RowIdImpl; +import com.healthmarketscience.jackcess.util.CaseInsensitiveColumnMatcher; +import com.healthmarketscience.jackcess.util.ColumnMatcher; +import com.healthmarketscience.jackcess.util.RowFilterTest; +import com.healthmarketscience.jackcess.util.SimpleColumnMatcher; import junit.framework.TestCase; /** @@ -170,7 +176,7 @@ public class CursorTest extends TestCase { int type) throws Exception { - return new CursorBuilder(table) + return table.newCursor() .setIndex(idx) .setStartEntry(3 - type) .setStartRowInclusive(type == 0) @@ -181,16 +187,17 @@ public class CursorTest extends TestCase { public void testRowId() throws Exception { // test special cases - RowId rowId1 = new RowId(1, 2); - RowId rowId2 = new RowId(1, 3); - RowId rowId3 = new RowId(2, 1); + RowIdImpl rowId1 = new RowIdImpl(1, 2); + RowIdImpl rowId2 = new RowIdImpl(1, 3); + RowIdImpl rowId3 = new RowIdImpl(2, 1); - List sortedRowIds = new ArrayList(new TreeSet( - Arrays.asList(rowId1, rowId2, rowId3, RowId.FIRST_ROW_ID, - RowId.LAST_ROW_ID))); + List sortedRowIds = + new ArrayList(new TreeSet( + Arrays.asList(rowId1, rowId2, rowId3, RowIdImpl.FIRST_ROW_ID, + RowIdImpl.LAST_ROW_ID))); - assertEquals(Arrays.asList(RowId.FIRST_ROW_ID, rowId1, rowId2, rowId3, - RowId.LAST_ROW_ID), + assertEquals(Arrays.asList(RowIdImpl.FIRST_ROW_ID, rowId1, rowId2, rowId3, + RowIdImpl.LAST_ROW_ID), sortedRowIds); } @@ -199,7 +206,7 @@ public class CursorTest extends TestCase { Database db = createTestTable(fileFormat); Table table = db.getTable("test"); - Cursor cursor = Cursor.createCursor(table); + Cursor cursor = CursorBuilder.createCursor(table); doTestSimple(cursor, null); db.close(); } @@ -226,7 +233,7 @@ public class CursorTest extends TestCase { Database db = createTestTable(fileFormat); Table table = db.getTable("test"); - Cursor cursor = Cursor.createCursor(table); + Cursor cursor = CursorBuilder.createCursor(table); doTestMove(cursor, null); db.close(); @@ -280,12 +287,55 @@ public class CursorTest extends TestCase { assertEquals(expectedRow, cursor.getCurrentRow()); } + public void testMoveNoReset() throws Exception { + for (final FileFormat fileFormat : JetFormatTest.SUPPORTED_FILEFORMATS) { + Database db = createTestTable(fileFormat); + + Table table = db.getTable("test"); + Cursor cursor = CursorBuilder.createCursor(table); + doTestMoveNoReset(cursor); + + db.close(); + } + } + + private static void doTestMoveNoReset(Cursor cursor) + throws Exception + { + List> expectedRows = createTestTableData(); + List> foundRows = new ArrayList>(); + + Iterator iter = cursor.newIterable().iterator(); + + for(int i = 0; i < 6; ++i) { + foundRows.add(iter.next()); + } + + iter = cursor.newIterable().reset(false).reverse().iterator(); + iter.next(); + Map row = iter.next(); + assertEquals(expectedRows.get(4), row); + + iter = cursor.newIterable().reset(false).iterator(); + iter.next(); + row = iter.next(); + assertEquals(expectedRows.get(5), row); + iter.next(); + + iter = cursor.newIterable().reset(false).iterator(); + for(int i = 6; i < 10; ++i) { + foundRows.add(iter.next()); + } + + assertEquals(expectedRows, foundRows); + } + public void testSearch() throws Exception { for (final FileFormat fileFormat : JetFormatTest.SUPPORTED_FILEFORMATS) { Database db = createTestTable(fileFormat); Table table = db.getTable("test"); - Cursor cursor = Cursor.createCursor(table); + Cursor cursor = CursorBuilder.createCursor(table); doTestSearch(table, cursor, null, 42, -13); db.close(); @@ -338,28 +388,28 @@ public class CursorTest extends TestCase { } assertEquals("data" + 5, - Cursor.findValue(table, + CursorBuilder.findValue(table, table.getColumn("value"), table.getColumn("id"), 5)); assertEquals(createExpectedRow("id", 5, "value", "data" + 5), - Cursor.findRow(table, + CursorBuilder.findRow(table, createExpectedRow("id", 5))); if(index != null) { assertEquals("data" + 5, - Cursor.findValue(table, index, + CursorBuilder.findValue(table, index, table.getColumn("value"), table.getColumn("id"), 5)); assertEquals(createExpectedRow("id", 5, "value", "data" + 5), - Cursor.findRow(table, index, + CursorBuilder.findRow(table, index, createExpectedRow("id", 5))); - assertNull(Cursor.findValue(table, index, + assertNull(CursorBuilder.findValue(table, index, table.getColumn("value"), table.getColumn("id"), -17)); - assertNull(Cursor.findRow(table, index, + assertNull(CursorBuilder.findRow(table, index, createExpectedRow("id", 13))); } } @@ -369,7 +419,7 @@ public class CursorTest extends TestCase { Database db = createTestTable(fileFormat); Table table = db.getTable("test"); - Cursor cursor = Cursor.createCursor(table); + Cursor cursor = CursorBuilder.createCursor(table); doTestReverse(cursor, null); db.close(); @@ -387,7 +437,7 @@ public class CursorTest extends TestCase { List> foundRows = new ArrayList>(); - for(Map row : cursor.reverseIterable()) { + for(Map row : cursor.newIterable().reverse()) { foundRows.add(row); } assertEquals(expectedRows, foundRows); @@ -399,8 +449,8 @@ public class CursorTest extends TestCase { Table table = db.getTable("test"); - Cursor cursor1 = Cursor.createCursor(table); - Cursor cursor2 = Cursor.createCursor(table); + Cursor cursor1 = CursorBuilder.createCursor(table); + Cursor cursor2 = CursorBuilder.createCursor(table); doTestLiveAddition(table, cursor1, cursor2, 11); db.close(); @@ -440,10 +490,10 @@ public class CursorTest extends TestCase { Table table = db.getTable("test"); - Cursor cursor1 = Cursor.createCursor(table); - Cursor cursor2 = Cursor.createCursor(table); - Cursor cursor3 = Cursor.createCursor(table); - Cursor cursor4 = Cursor.createCursor(table); + Cursor cursor1 = CursorBuilder.createCursor(table); + Cursor cursor2 = CursorBuilder.createCursor(table); + Cursor cursor3 = CursorBuilder.createCursor(table); + Cursor cursor4 = CursorBuilder.createCursor(table); doTestLiveDeletion(cursor1, cursor2, cursor3, cursor4, 1); db.close(); @@ -536,7 +586,7 @@ public class CursorTest extends TestCase { assertTable(createUnorderedTestTableData(), table); - Cursor cursor = Cursor.createIndexCursor(table, idx); + Cursor cursor = CursorBuilder.createCursor(table, idx); doTestSimple(cursor, null); db.close(); @@ -549,7 +599,7 @@ public class CursorTest extends TestCase { Table table = db.getTable("test"); Index idx = table.getIndexes().get(0); - Cursor cursor = Cursor.createIndexCursor(table, idx); + Cursor cursor = CursorBuilder.createCursor(table, idx); doTestMove(cursor, null); db.close(); @@ -562,7 +612,7 @@ public class CursorTest extends TestCase { Table table = db.getTable("test"); Index idx = table.getIndexes().get(0); - Cursor cursor = Cursor.createIndexCursor(table, idx); + Cursor cursor = CursorBuilder.createCursor(table, idx); doTestReverse(cursor, null); db.close(); @@ -575,7 +625,7 @@ public class CursorTest extends TestCase { Table table = db.getTable("test"); Index idx = table.getIndexes().get(0); - Cursor cursor = Cursor.createIndexCursor(table, idx); + Cursor cursor = CursorBuilder.createCursor(table, idx); doTestSearch(table, cursor, idx, 42, -13); db.close(); @@ -589,8 +639,8 @@ public class CursorTest extends TestCase { Table table = db.getTable("test"); Index idx = table.getIndexes().get(0); - Cursor cursor1 = Cursor.createIndexCursor(table, idx); - Cursor cursor2 = Cursor.createIndexCursor(table, idx); + Cursor cursor1 = CursorBuilder.createCursor(table, idx); + Cursor cursor2 = CursorBuilder.createCursor(table, idx); doTestLiveAddition(table, cursor1, cursor2, 11); db.close(); @@ -604,10 +654,10 @@ public class CursorTest extends TestCase { Table table = db.getTable("test"); Index idx = table.getIndexes().get(0); - Cursor cursor1 = Cursor.createIndexCursor(table, idx); - Cursor cursor2 = Cursor.createIndexCursor(table, idx); - Cursor cursor3 = Cursor.createIndexCursor(table, idx); - Cursor cursor4 = Cursor.createIndexCursor(table, idx); + Cursor cursor1 = CursorBuilder.createCursor(table, idx); + Cursor cursor2 = CursorBuilder.createCursor(table, idx); + Cursor cursor3 = CursorBuilder.createCursor(table, idx); + Cursor cursor4 = CursorBuilder.createCursor(table, idx); doTestLiveDeletion(cursor1, cursor2, cursor3, cursor4, 1); db.close(); @@ -734,7 +784,7 @@ public class CursorTest extends TestCase { Database db = createDupeTestTable(fileFormat); Table table = db.getTable("test"); - Cursor cursor = Cursor.createCursor(table); + Cursor cursor = CursorBuilder.createCursor(table); doTestFindAll(table, cursor, null); @@ -748,7 +798,7 @@ public class CursorTest extends TestCase { Table table = db.getTable("test"); Index idx = table.getIndexes().get(0); - Cursor cursor = Cursor.createIndexCursor(table, idx); + Cursor cursor = CursorBuilder.createCursor(table, idx); doTestFindAll(table, cursor, idx); @@ -759,11 +809,10 @@ public class CursorTest extends TestCase { private static void doTestFindAll(Table table, Cursor cursor, Index index) throws Exception { - Column valCol = table.getColumn("value"); - List> rows = RowFilterTest.toList( - cursor.columnMatchIterable(valCol, "data2")); + List> rows = RowFilterTest.toList( + cursor.newIterable().setMatchPattern("value", "data2")); - List> expectedRows = null; + List> expectedRows = null; if(index == null) { expectedRows = @@ -794,8 +843,9 @@ public class CursorTest extends TestCase { } assertEquals(expectedRows, rows); + Column valCol = table.getColumn("value"); rows = RowFilterTest.toList( - cursor.columnMatchIterable(valCol, "data4")); + cursor.newIterable().setMatchPattern(valCol, "data4")); if(index == null) { expectedRows = @@ -815,12 +865,13 @@ public class CursorTest extends TestCase { assertEquals(expectedRows, rows); rows = RowFilterTest.toList( - cursor.columnMatchIterable(valCol, "data9")); + cursor.newIterable().setMatchPattern(valCol, "data9")); assertTrue(rows.isEmpty()); rows = RowFilterTest.toList( - cursor.rowMatchIterable(Collections.singletonMap("id", 8))); + cursor.newIterable().setMatchPattern( + Collections.singletonMap("id", 8))); expectedRows = createExpectedTable( @@ -832,22 +883,23 @@ public class CursorTest extends TestCase { for(Map row : table) { - expectedRows = new ArrayList>(); + List> tmpRows = new ArrayList>(); for(Map tmpRow : cursor) { if(row.equals(tmpRow)) { - expectedRows.add(tmpRow); + tmpRows.add(tmpRow); } } + expectedRows = tmpRows; assertFalse(expectedRows.isEmpty()); - rows = RowFilterTest.toList(cursor.rowMatchIterable(row)); + rows = RowFilterTest.toList(cursor.newIterable().setMatchPattern(row)); assertEquals(expectedRows, rows); } rows = RowFilterTest.toList( - cursor.rowMatchIterable(createExpectedRow( - "id", 8, "value", "data13"))); + cursor.newIterable().addMatchPattern("id", 8) + .addMatchPattern("value", "data13")); assertTrue(rows.isEmpty()); } @@ -859,8 +911,8 @@ public class CursorTest extends TestCase { Table table = db.getTable("test"); Index idx = table.getIndexes().get(0); - Cursor tCursor = Cursor.createCursor(table); - Cursor iCursor = Cursor.createIndexCursor(table, idx); + Cursor tCursor = CursorBuilder.createCursor(table); + Cursor iCursor = CursorBuilder.createCursor(table, idx); Cursor.Savepoint tSave = tCursor.getSavepoint(); Cursor.Savepoint iSave = iCursor.getSavepoint(); @@ -882,8 +934,8 @@ public class CursorTest extends TestCase { // success } - Cursor tCursor2 = Cursor.createCursor(table); - Cursor iCursor2 = Cursor.createIndexCursor(table, idx); + Cursor tCursor2 = CursorBuilder.createCursor(table); + Cursor iCursor2 = CursorBuilder.createCursor(table, idx); tCursor2.restoreSavepoint(tSave); iCursor2.restoreSavepoint(iSave); @@ -892,7 +944,7 @@ public class CursorTest extends TestCase { } } - public void testColmnMatcher() throws Exception { + public void testColumnMatcher() throws Exception { for (final FileFormat fileFormat : JetFormatTest.SUPPORTED_FILEFORMATS) { @@ -903,7 +955,7 @@ public class CursorTest extends TestCase { doTestMatchers(table, SimpleColumnMatcher.INSTANCE, false); doTestMatchers(table, CaseInsensitiveColumnMatcher.INSTANCE, true); - Cursor cursor = Cursor.createCursor(table); + Cursor cursor = CursorBuilder.createCursor(table); doTestMatcher(table, cursor, SimpleColumnMatcher.INSTANCE, false); doTestMatcher(table, cursor, CaseInsensitiveColumnMatcher.INSTANCE, true); @@ -989,6 +1041,28 @@ public class CursorTest extends TestCase { "value", "data" + 4), cursor.getCurrentRow()); } + + assertEquals(Arrays.asList(createExpectedRow("id", 4, + "value", "data" + 4)), + RowFilterTest.toList( + cursor.newIterable() + .setMatchPattern("value", "data4") + .setColumnMatcher(SimpleColumnMatcher.INSTANCE))); + + assertEquals(Arrays.asList(createExpectedRow("id", 3, + "value", "data" + 3)), + RowFilterTest.toList( + cursor.newIterable() + .setMatchPattern("value", "DaTa3") + .setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE))); + + assertEquals(Arrays.asList(createExpectedRow("id", 2, + "value", "data" + 2)), + RowFilterTest.toList( + cursor.newIterable() + .addMatchPattern("value", "DaTa2") + .addMatchPattern("id", 2) + .setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE))); } public void testIndexCursor() throws Exception @@ -998,7 +1072,7 @@ public class CursorTest extends TestCase { Database db = open(testDB); Table t1 = db.getTable("Table1"); Index idx = t1.getIndex(IndexBuilder.PRIMARY_KEY_NAME); - IndexCursor cursor = IndexCursor.createCursor(t1, idx); + IndexCursor cursor = CursorBuilder.createCursor(t1, idx); assertFalse(cursor.findFirstRowByEntry(-1)); cursor.findClosestRowByEntry(-1); @@ -1025,18 +1099,19 @@ public class CursorTest extends TestCase { Database db = openCopy(testDB); Table t1 = db.getTable("Table1"); Index idx = t1.getIndex("Table2Table1"); - IndexCursor cursor = IndexCursor.createCursor(t1, idx); + IndexCursor cursor = CursorBuilder.createCursor(t1, idx); List expectedData = new ArrayList(); - for(Map row : cursor.entryIterable( - Arrays.asList("data"), 1)) { + for(Map row : cursor.newEntryIterable(1) + .addColumnNames("data")) { expectedData.add((String)row.get("data")); } assertEquals(Arrays.asList("baz11", "baz11-2"), expectedData); expectedData = new ArrayList(); - for(Iterator> iter = cursor.entryIterator(1); + for(Iterator> iter = + cursor.newEntryIterable(1).iterator(); iter.hasNext(); ) { expectedData.add((String)iter.next().get("data")); iter.remove(); @@ -1060,8 +1135,8 @@ public class CursorTest extends TestCase { assertEquals(Arrays.asList("baz11", "baz11-2"), expectedData); expectedData = new ArrayList(); - for(Map row : cursor.entryIterable( - Arrays.asList("data"), 1)) { + for(Map row : cursor.newEntryIterable(1) + .addColumnNames("data")) { expectedData.add((String)row.get("data")); } @@ -1077,10 +1152,10 @@ public class CursorTest extends TestCase { Database db = openCopy(testDB); Table t1 = db.getTable("Table1"); - Cursor cursor = Cursor.createCursor(t1); + Cursor cursor = CursorBuilder.createCursor(t1); List expectedData = new ArrayList(); - for(Map row : cursor.iterable( + for(Map row : cursor.newIterable().setColumnNames( Arrays.asList("otherfk1", "data"))) { if(row.get("otherfk1").equals(1)) { expectedData.add((String)row.get("data")); @@ -1090,7 +1165,7 @@ public class CursorTest extends TestCase { assertEquals(Arrays.asList("baz11", "baz11-2"), expectedData); expectedData = new ArrayList(); - for(Iterator> iter = cursor.iterator(); + for(Iterator> iter = cursor.iterator(); iter.hasNext(); ) { Map row = iter.next(); if(row.get("otherfk1").equals(1)) { @@ -1117,7 +1192,7 @@ public class CursorTest extends TestCase { assertEquals(Arrays.asList("baz11", "baz11-2"), expectedData); expectedData = new ArrayList(); - for(Map row : cursor.iterable( + for(Map row : cursor.newIterable().setColumnNames( Arrays.asList("otherfk1", "data"))) { if(row.get("otherfk1").equals(1)) { expectedData.add((String)row.get("data")); diff --git a/test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java b/test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java index e73661f..eef4ecb 100644 --- a/test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java @@ -57,10 +57,23 @@ import java.util.TreeSet; import java.util.UUID; import static com.healthmarketscience.jackcess.Database.*; -import static com.healthmarketscience.jackcess.JetFormatTest.*; import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; +import com.healthmarketscience.jackcess.impl.ByteUtil; +import com.healthmarketscience.jackcess.impl.ColumnImpl; +import com.healthmarketscience.jackcess.impl.DatabaseImpl; +import com.healthmarketscience.jackcess.impl.IndexData; +import com.healthmarketscience.jackcess.impl.IndexImpl; +import com.healthmarketscience.jackcess.impl.JetFormat; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; +import com.healthmarketscience.jackcess.impl.RowIdImpl; +import com.healthmarketscience.jackcess.impl.RowImpl; +import com.healthmarketscience.jackcess.impl.TableImpl; +import com.healthmarketscience.jackcess.util.LinkResolver; +import com.healthmarketscience.jackcess.util.MemFileChannel; +import com.healthmarketscience.jackcess.util.RowFilterTest; import junit.framework.TestCase; + /** * @author Tim McCune */ @@ -90,8 +103,9 @@ public class DatabaseTest extends TestCase { : null); final Database db = new DatabaseBuilder(file).setReadOnly(true) .setAutoSync(_autoSync).setChannel(channel).open(); - assertEquals("Wrong JetFormat.", fileFormat.getJetFormat(), - db.getFormat()); + assertEquals("Wrong JetFormat.", + DatabaseImpl.getFileFormatDetails(fileFormat).getFormat(), + ((DatabaseImpl)db).getFormat()); assertEquals("Wrong FileFormat.", fileFormat, db.getFileFormat()); return db; } @@ -151,8 +165,9 @@ public class DatabaseTest extends TestCase { File tmp = createTempFile(keep); copyFile(file, tmp); Database db = new DatabaseBuilder(tmp).setAutoSync(_autoSync).open(); - assertEquals("Wrong JetFormat.", fileFormat.getJetFormat(), - db.getFormat()); + assertEquals("Wrong JetFormat.", + DatabaseImpl.getFileFormatDetails(fileFormat).getFormat(), + ((DatabaseImpl)db).getFormat()); assertEquals("Wrong FileFormat.", fileFormat, db.getFileFormat()); return db; } @@ -163,7 +178,7 @@ public class DatabaseTest extends TestCase { Database db = create(fileFormat); try { - db.createTable("test", Collections.emptyList()); + ((DatabaseImpl)db).createTable("test", Collections.emptyList()); fail("created table with no columns?"); } catch(IllegalArgumentException e) { // success @@ -231,7 +246,7 @@ public class DatabaseTest extends TestCase { public void testGetColumns() throws Exception { for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) { - List columns = open(testDB).getTable("Table1").getColumns(); + List columns = open(testDB).getTable("Table1").getColumns(); assertEquals(9, columns.size()); checkColumn(columns, 0, "A", DataType.TEXT); checkColumn(columns, 1, "B", DataType.TEXT); @@ -245,8 +260,8 @@ public class DatabaseTest extends TestCase { } } - static void checkColumn(List columns, int columnNumber, String name, - DataType dataType) + static void checkColumn(List columns, int columnNumber, + String name, DataType dataType) throws Exception { Column column = columns.get(columnNumber); @@ -396,17 +411,19 @@ public class DatabaseTest extends TestCase { for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { Database db = create(fileFormat); createTestTable(db); - Object[] row1 = createTestRow("Tim1"); - Object[] row2 = createTestRow("Tim2"); - Object[] row3 = createTestRow("Tim3"); + Map row1 = createTestRowMap("Tim1"); + Map row2 = createTestRowMap("Tim2"); + Map row3 = createTestRowMap("Tim3"); Table table = db.getTable("Test"); - table.addRows(Arrays.asList(row1, row2, row3)); + @SuppressWarnings("unchecked") + List> rows = Arrays.asList(row1, row2, row3); + table.addRowsFromMaps(rows); assertRowCount(3, table); table.reset(); table.getNextRow(); table.getNextRow(); - table.deleteCurrentRow(); + table.getDefaultCursor().deleteCurrentRow(); table.reset(); @@ -429,17 +446,17 @@ public class DatabaseTest extends TestCase { assertRowCount(10, table); table.reset(); table.getNextRow(); - table.deleteCurrentRow(); + table.getDefaultCursor().deleteCurrentRow(); assertRowCount(9, table); table.reset(); table.getNextRow(); - table.deleteCurrentRow(); + table.getDefaultCursor().deleteCurrentRow(); assertRowCount(8, table); table.reset(); for (int i = 0; i < 8; i++) { table.getNextRow(); } - table.deleteCurrentRow(); + table.getDefaultCursor().deleteCurrentRow(); assertRowCount(7, table); table.addRow(row); assertRowCount(8, table); @@ -447,7 +464,7 @@ public class DatabaseTest extends TestCase { for (int i = 0; i < 3; i++) { table.getNextRow(); } - table.deleteCurrentRow(); + table.getDefaultCursor().deleteCurrentRow(); assertRowCount(7, table); table.reset(); assertEquals(2, table.getNextRow().get("D")); @@ -456,6 +473,38 @@ public class DatabaseTest extends TestCase { } } + public void testDeleteRow() throws Exception { + + // make sure correct row is deleted + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = create(fileFormat); + createTestTable(db); + Table table = db.getTable("Test"); + for(int i = 0; i < 10; ++i) { + table.addRowFromMap(createTestRowMap("Tim" + i)); + } + assertRowCount(10, table); + + table.reset(); + + List rows = RowFilterTest.toList(table); + + Row r1 = rows.remove(7); + Row r2 = rows.remove(3); + assertEquals(8, rows.size()); + + assertSame(r2, table.deleteRow(r2)); + assertSame(r1, table.deleteRow(r1)); + + assertTable(rows, table); + + table.deleteRow(r2); + table.deleteRow(r1); + + assertTable(rows, table); + } + } + public void testReadLongValue() throws Exception { for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.TEST2, true)) { @@ -711,9 +760,9 @@ public class DatabaseTest extends TestCase { for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { Database db = create(fileFormat); - Column col = new ColumnBuilder("A", DataType.NUMERIC) + ColumnBuilder col = new ColumnBuilder("A", DataType.NUMERIC) .setScale(4).setPrecision(8).toColumn(); - assertTrue(col.isVariableLength()); + assertTrue(col.getType().isVariableLength()); Table table = new TableBuilder("test") .addColumn(col) @@ -809,7 +858,7 @@ public class DatabaseTest extends TestCase { public void testMultiPageTableDef() throws Exception { for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) { - List columns = open(testDB).getTable("Table2").getColumns(); + List columns = open(testDB).getTable("Table2").getColumns(); assertEquals(89, columns.size()); } } @@ -875,7 +924,7 @@ public class DatabaseTest extends TestCase { Database db = openCopy(testDB); Table t = db.getTable("jobDB1"); - assertTrue(t.getOwnedPagesCursor().getUsageMap().toString() + assertTrue(((TableImpl)t).getOwnedPagesCursor().getUsageMap().toString() .startsWith("(InlineHandler)")); String lval = createNonAsciiString(255); // "--255 chars long text--"; @@ -890,7 +939,7 @@ public class DatabaseTest extends TestCase { } assertEquals(1000, ids.size()); - assertTrue(t.getOwnedPagesCursor().getUsageMap().toString() + assertTrue(((TableImpl)t).getOwnedPagesCursor().getUsageMap().toString() .startsWith("(ReferenceHandler)")); db.close(); @@ -904,7 +953,7 @@ public class DatabaseTest extends TestCase { final int numColumns = 90; - List columns = new ArrayList(); + List columns = new ArrayList(); List colNames = new ArrayList(); for(int i = 0; i < numColumns; ++i) { String colName = "MyColumnName" + i; @@ -912,7 +961,7 @@ public class DatabaseTest extends TestCase { columns.add(new ColumnBuilder(colName, DataType.TEXT).toColumn()); } - db.createTable("test", columns); + ((DatabaseImpl)db).createTable("test", columns); Table t = db.getTable("test"); @@ -963,18 +1012,29 @@ public class DatabaseTest extends TestCase { private void doTestAutoNumber(Table table) throws Exception { - table.addRow(null, "row1"); - table.addRow(13, "row2"); - table.addRow("flubber", "row3"); + Object[] row = {null, "row1"}; + assertSame(row, table.addRow(row)); + assertEquals(1, ((Integer)row[0]).intValue()); + row = table.addRow(13, "row2"); + assertEquals(2, ((Integer)row[0]).intValue()); + row = table.addRow("flubber", "row3"); + assertEquals(3, ((Integer)row[0]).intValue()); table.reset(); - table.addRow(Column.AUTO_NUMBER, "row4"); - table.addRow(Column.AUTO_NUMBER, "row5"); + row = table.addRow(Column.AUTO_NUMBER, "row4"); + assertEquals(4, ((Integer)row[0]).intValue()); + row = table.addRow(Column.AUTO_NUMBER, "row5"); + assertEquals(5, ((Integer)row[0]).intValue()); + + Object[] smallRow = {Column.AUTO_NUMBER}; + row = table.addRow(smallRow); + assertNotSame(row, smallRow); + assertEquals(6, ((Integer)row[0]).intValue()); table.reset(); - List> expectedRows = + List> expectedRows = createExpectedTable( createExpectedRow( "a", 1, @@ -990,7 +1050,10 @@ public class DatabaseTest extends TestCase { "b", "row4"), createExpectedRow( "a", 5, - "b", "row5")); + "b", "row5"), + createExpectedRow( + "a", 6, + "b", null)); assertTable(expectedRows, table); } @@ -1119,7 +1182,7 @@ public class DatabaseTest extends TestCase { t.addRow("row" + i, Column.AUTO_NUMBER, "initial data"); } - Cursor c = Cursor.createCursor(t); + Cursor c = CursorBuilder.createCursor(t); c.reset(); c.moveNextRows(2); Map row = c.getCurrentRow(); @@ -1129,7 +1192,15 @@ public class DatabaseTest extends TestCase { "data", "initial data"), row); - c.updateCurrentRow(Column.KEEP_VALUE, Column.AUTO_NUMBER, "new data"); + Map newRow = createExpectedRow( + "name", Column.KEEP_VALUE, + "id", Column.AUTO_NUMBER, + "data", "new data"); + assertSame(newRow, c.updateCurrentRowFromMap(newRow)); + assertEquals(createExpectedRow("name", "row1", + "id", 2, + "data", "new data"), + newRow); c.moveNextRows(3); row = c.getCurrentRow(); @@ -1187,6 +1258,23 @@ public class DatabaseTest extends TestCase { "data", newText), row); + List rows = RowFilterTest.toList(t); + assertEquals(50, rows.size()); + + for(Row r : rows) { + r.put("data", "final data " + r.get("id")); + } + + for(Row r : rows) { + assertSame(r, t.updateRow(r)); + } + + t.reset(); + + for(Row r : t) { + assertEquals("final data " + r.get("id"), r.get("data")); + } + db.close(); } } @@ -1222,8 +1310,8 @@ public class DatabaseTest extends TestCase { for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) { Database db = open(testDB); - assertEquals(db.getFormat().DEFAULT_SORT_ORDER, - db.getDefaultSortOrder()); + assertEquals(((DatabaseImpl)db).getFormat().DEFAULT_SORT_ORDER, + ((DatabaseImpl)db).getDefaultSortOrder()); db.close(); } } @@ -1275,7 +1363,7 @@ public class DatabaseTest extends TestCase { public Database resolveLinkedDatabase(Database linkerdb, String dbName) throws IOException { assertEquals(linkeeDbName, dbName); - return Database.open(linkeeFile); + return DatabaseBuilder.open(linkeeFile); } }); @@ -1286,7 +1374,7 @@ public class DatabaseTest extends TestCase { assertNotNull(linkeeDb); assertEquals(linkeeFile, linkeeDb.getFile()); - List> expectedRows = + List> expectedRows = createExpectedTable( createExpectedRow( "ID", 1, @@ -1323,9 +1411,9 @@ public class DatabaseTest extends TestCase { private static void doTestTimeZone(final TimeZone tz) throws Exception { - Column col = new Column(true, null) { + ColumnImpl col = new ColumnImpl(null, DataType.SHORT_DATE_TIME, 0, 0, 0) { @Override - Calendar getCalendar() { return Calendar.getInstance(tz); } + protected Calendar getCalendar() { return Calendar.getInstance(tz); } }; SimpleDateFormat df = new SimpleDateFormat("yyyy.MM.dd"); @@ -1353,7 +1441,7 @@ public class DatabaseTest extends TestCase { private void checkRawValue(String expected, Object val) { if(expected != null) { - assertTrue(Column.isRawData(val)); + assertTrue(ColumnImpl.isRawData(val)); assertEquals(expected, val.toString()); } else { assertNull(val); @@ -1369,6 +1457,12 @@ public class DatabaseTest extends TestCase { return createTestRow("Tim"); } + static Map createTestRowMap(String col1Val) { + return createExpectedRow("A", col1Val, "B", "R", "C", "McCune", + "D", 1234, "E", (byte) 0xad, "F", 555.66d, + "G", 777.88f, "H", (short) 999, "I", new Date()); + } + static void createTestTable(Database db) throws Exception { new TableBuilder("test") .addColumn(new ColumnBuilder("A", DataType.TEXT)) @@ -1383,7 +1477,7 @@ public class DatabaseTest extends TestCase { .toTable(db); } - static String createString(int len) { + public static String createString(int len) { return createString(len, 'a'); } @@ -1406,21 +1500,25 @@ public class DatabaseTest extends TestCase { assertEquals(expectedRowCount, table.getRowCount()); } - static int countRows(Table table) throws Exception { + public static int countRows(Table table) throws Exception { int rtn = 0; - for(Map row : Cursor.createCursor(table)) { + for(Map row : CursorBuilder.createCursor(table)) { rtn++; } return rtn; } - static void assertTable(List> expectedTable, Table table) + public static void assertTable( + List> expectedTable, + Table table) + throws IOException { - assertCursor(expectedTable, Cursor.createCursor(table)); + assertCursor(expectedTable, CursorBuilder.createCursor(table)); } - static void assertCursor(List> expectedTable, - Cursor cursor) + public static void assertCursor( + List> expectedTable, + Cursor cursor) { List> foundTable = new ArrayList>(); @@ -1430,8 +1528,8 @@ public class DatabaseTest extends TestCase { assertEquals(expectedTable, foundTable); } - static Map createExpectedRow(Object... rowElements) { - Map row = new LinkedHashMap(); + public static RowImpl createExpectedRow(Object... rowElements) { + RowImpl row = new RowImpl((RowIdImpl)null); for(int i = 0; i < rowElements.length; i += 2) { row.put((String)rowElements[i], rowElements[i + 1]); @@ -1440,8 +1538,8 @@ public class DatabaseTest extends TestCase { } @SuppressWarnings("unchecked") - static List> createExpectedTable(Map... rows) { - return Arrays.>asList(rows); + public static List createExpectedTable(Row... rows) { + return Arrays.asList(rows); } static void dumpDatabase(Database mdb) throws Exception { @@ -1475,7 +1573,7 @@ public class DatabaseTest extends TestCase { static void dumpTable(Table table, PrintWriter writer) throws Exception { // make sure all indexes are read for(Index index : table.getIndexes()) { - index.initialize(); + ((IndexImpl)index).initialize(); } writer.println("TABLE: " + table.getName()); @@ -1484,7 +1582,7 @@ public class DatabaseTest extends TestCase { colNames.add(col.getName()); } writer.println("COLUMNS: " + colNames); - for(Map row : Cursor.createCursor(table)) { + for(Map row : CursorBuilder.createCursor(table)) { writer.println(massageRow(row)); } } @@ -1515,7 +1613,7 @@ public class DatabaseTest extends TestCase { static void dumpIndex(Index index, PrintWriter writer) throws Exception { writer.println("INDEX: " + index); - IndexData.EntryCursor ec = index.cursor(); + IndexData.EntryCursor ec = ((IndexImpl)index).cursor(); IndexData.Entry lastE = ec.getLastEntry(); IndexData.Entry e = null; while((e = ec.getNextEntry()) != lastE) { @@ -1570,7 +1668,7 @@ public class DatabaseTest extends TestCase { return tmp; } - static byte[] toByteArray(File file) + public static byte[] toByteArray(File file) throws IOException { // FIXME should really be using commons io IOUtils here, but don't want diff --git a/test/src/java/com/healthmarketscience/jackcess/IndexTest.java b/test/src/java/com/healthmarketscience/jackcess/IndexTest.java index 8f078f6..8c6284a 100644 --- a/test/src/java/com/healthmarketscience/jackcess/IndexTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/IndexTest.java @@ -36,11 +36,16 @@ import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; -import junit.framework.TestCase; - import static com.healthmarketscience.jackcess.Database.*; import static com.healthmarketscience.jackcess.DatabaseTest.*; -import static com.healthmarketscience.jackcess.JetFormatTest.*; +import com.healthmarketscience.jackcess.impl.ByteUtil; +import com.healthmarketscience.jackcess.impl.IndexCodesTest; +import com.healthmarketscience.jackcess.impl.IndexData; +import com.healthmarketscience.jackcess.impl.IndexImpl; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; +import com.healthmarketscience.jackcess.impl.RowIdImpl; +import com.healthmarketscience.jackcess.impl.TableImpl; +import junit.framework.TestCase; /** * @author James Ahlborn @@ -110,8 +115,8 @@ public class IndexTest extends TestCase { for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.INDEX, true)) { Database mdb = open(testDB); - Table table = mdb.getTable("Table1"); - for(Index idx : table.getIndexes()) { + TableImpl table = (TableImpl)mdb.getTable("Table1"); + for(IndexImpl idx : table.getIndexes()) { idx.initialize(); } assertEquals(4, table.getIndexes().size()); @@ -122,8 +127,8 @@ public class IndexTest extends TestCase { "Table2Table1", "otherfk1", "Table3Table1", "otherfk2"); - table = mdb.getTable("Table2"); - for(Index idx : table.getIndexes()) { + table = (TableImpl)mdb.getTable("Table2"); + for(IndexImpl idx : table.getIndexes()) { idx.initialize(); } assertEquals(3, table.getIndexes().size()); @@ -134,8 +139,8 @@ public class IndexTest extends TestCase { "PrimaryKey", "id", ".rC", "id"); - Index pkIdx = table.getIndex("PrimaryKey"); - Index fkIdx = table.getIndex(".rC"); + IndexImpl pkIdx = table.getIndex("PrimaryKey"); + IndexImpl fkIdx = table.getIndex(".rC"); assertNotSame(pkIdx, fkIdx); assertTrue(fkIdx.isForeignKey()); assertSame(pkIdx.getIndexData(), fkIdx.getIndexData()); @@ -143,8 +148,8 @@ public class IndexTest extends TestCase { assertEquals(Arrays.asList(pkIdx, fkIdx), indexData.getIndexes()); assertSame(pkIdx, indexData.getPrimaryIndex()); - table = mdb.getTable("Table3"); - for(Index idx : table.getIndexes()) { + table = (TableImpl)mdb.getTable("Table3"); + for(IndexImpl idx : table.getIndexes()) { idx.initialize(); } assertEquals(3, table.getIndexes().size()); @@ -171,8 +176,8 @@ public class IndexTest extends TestCase { for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.COMP_INDEX)) { // this file has an index with "compressed" entries and node pages Database db = open(testDB); - Table t = db.getTable("Table1"); - Index index = t.getIndexes().get(0); + TableImpl t = (TableImpl)db.getTable("Table1"); + IndexImpl index = t.getIndexes().get(0); assertFalse(index.isInitialized()); assertEquals(512, countRows(t)); assertEquals(512, index.getIndexData().getEntryCount()); @@ -180,23 +185,12 @@ public class IndexTest extends TestCase { // copy to temp file and attempt to edit db = openCopy(testDB); - t = db.getTable("Table1"); + t = (TableImpl)db.getTable("Table1"); index = t.getIndexes().get(0); System.out.println("IndexTest: Index type: " + index.getIndexData().getClass()); - try { - t.addRow(99, "abc", "def"); - if(index.getIndexData() instanceof SimpleIndexData) { - // SimpleIndex doesn't support writing these indexes - fail("Should have thrown UnsupportedOperationException"); - } - } catch(UnsupportedOperationException e) { - // success - if(index.getIndexData() instanceof BigIndexData) { - throw e; - } - } + t.addRow(99, "abc", "def"); } } @@ -212,28 +206,28 @@ public class IndexTest extends TestCase { assertRowCount(12, table); for(Index index : table.getIndexes()) { - assertEquals(12, index.getIndexData().getEntryCount()); + assertEquals(12, ((IndexImpl)index).getIndexData().getEntryCount()); } table.reset(); table.getNextRow(); table.getNextRow(); - table.deleteCurrentRow(); + table.getDefaultCursor().deleteCurrentRow(); table.getNextRow(); - table.deleteCurrentRow(); + table.getDefaultCursor().deleteCurrentRow(); table.getNextRow(); table.getNextRow(); - table.deleteCurrentRow(); + table.getDefaultCursor().deleteCurrentRow(); table.getNextRow(); table.getNextRow(); table.getNextRow(); - table.deleteCurrentRow(); + table.getDefaultCursor().deleteCurrentRow(); table.reset(); assertRowCount(8, table); for(Index index : table.getIndexes()) { - assertEquals(8, index.getIndexData().getEntryCount()); + assertEquals(8, ((IndexImpl)index).getIndexData().getEntryCount()); } } } @@ -254,9 +248,9 @@ public class IndexTest extends TestCase { throws Exception { Table orig = db.getTable(tableName); - Index origI = orig.getIndex("DataIndex"); + IndexImpl origI = (IndexImpl)orig.getIndex("DataIndex"); Table temp = db.getTable(tableName + "_temp"); - Index tempI = temp.getIndex("DataIndex"); + IndexImpl tempI = (IndexImpl)temp.getIndex("DataIndex"); // copy from orig table to temp table for(Map row : orig) { @@ -266,8 +260,8 @@ public class IndexTest extends TestCase { assertEquals(origI.getIndexData().getEntryCount(), tempI.getIndexData().getEntryCount()); - Cursor origC = Cursor.createIndexCursor(orig, origI); - Cursor tempC = Cursor.createIndexCursor(temp, tempI); + Cursor origC = CursorBuilder.createCursor(orig, origI); + Cursor tempC = CursorBuilder.createCursor(temp, tempI); while(true) { boolean origHasNext = origC.moveToNextRow(); @@ -340,7 +334,7 @@ public class IndexTest extends TestCase { IOException failure = null; try { - index.addRow(row, new RowId(400 + i, 0)); + ((IndexImpl)index).addRow(row, new RowIdImpl(400 + i, 0)); } catch(IOException e) { failure = e; } @@ -357,8 +351,8 @@ public class IndexTest extends TestCase { for (final TestDB testDB : SUPPORTED_DBS_TEST) { Database db = openCopy(testDB); Table table = db.getTable("Table1"); - Index indA = table.getIndex("PrimaryKey"); - Index indB = table.getIndex("B"); + IndexImpl indA = (IndexImpl)table.getIndex("PrimaryKey"); + IndexImpl indB = (IndexImpl)table.getIndex("B"); assertEquals(2, indA.getUniqueEntryCount()); assertEquals(2, indB.getUniqueEntryCount()); @@ -382,8 +376,8 @@ public class IndexTest extends TestCase { indB = null; table = db.getTable("Table1"); - indA = table.getIndex("PrimaryKey"); - indB = table.getIndex("B"); + indA = (IndexImpl)table.getIndex("PrimaryKey"); + indB = (IndexImpl)table.getIndex("B"); assertEquals(12, indA.getIndexData().getEntryCount()); assertEquals(12, indB.getIndexData().getEntryCount()); @@ -391,7 +385,7 @@ public class IndexTest extends TestCase { assertEquals(12, indA.getUniqueEntryCount()); assertEquals(8, indB.getUniqueEntryCount()); - Cursor c = Cursor.createCursor(table); + Cursor c = CursorBuilder.createCursor(table); assertTrue(c.moveToNextRow()); final Map row = c.getCurrentRow(); @@ -443,7 +437,7 @@ public class IndexTest extends TestCase { .toTable(db); assertEquals(1, t.getIndexes().size()); - Index idx = t.getIndexes().get(0); + IndexImpl idx = (IndexImpl)t.getIndexes().get(0); assertEquals(IndexBuilder.PRIMARY_KEY_NAME, idx.getName()); assertEquals(1, idx.getColumns().size()); @@ -458,7 +452,7 @@ public class IndexTest extends TestCase { t.addRow(1, "row1"); t.addRow(3, "row3"); - Cursor c = new CursorBuilder(t) + Cursor c = t.newCursor() .setIndexByName(IndexBuilder.PRIMARY_KEY_NAME).toCursor(); for(int i = 1; i <= 3; ++i) { @@ -478,8 +472,8 @@ public class IndexTest extends TestCase { Table t2 = db.getTable("Table2"); Table t3 = db.getTable("Table3"); - Index t2t1 = t1.getIndex("Table2Table1"); - Index t3t1 = t1.getIndex("Table3Table1"); + IndexImpl t2t1 = (IndexImpl)t1.getIndex("Table2Table1"); + IndexImpl t3t1 = (IndexImpl)t1.getIndex("Table3Table1"); assertTrue(t2t1.isForeignKey()); @@ -498,7 +492,7 @@ public class IndexTest extends TestCase { Index t1pk = t1.getIndex(IndexBuilder.PRIMARY_KEY_NAME); assertNotNull(t1pk); - assertNull(t1pk.getReference()); + assertNull(((IndexImpl)t1pk).getReference()); assertNull(t1pk.getReferencedIndex()); } } @@ -506,7 +500,7 @@ public class IndexTest extends TestCase { private void doCheckForeignKeyIndex(Table ta, Index ia, Table tb) throws Exception { - Index ib = ia.getReferencedIndex(); + IndexImpl ib = (IndexImpl)ia.getReferencedIndex(); assertNotNull(ib); assertSame(tb, ib.getTable()); diff --git a/test/src/java/com/healthmarketscience/jackcess/PropertiesTest.java b/test/src/java/com/healthmarketscience/jackcess/PropertiesTest.java index fccbc67..8cd5a55 100644 --- a/test/src/java/com/healthmarketscience/jackcess/PropertiesTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/PropertiesTest.java @@ -26,10 +26,13 @@ import java.util.List; import java.util.Map; import junit.framework.TestCase; - +import com.healthmarketscience.jackcess.impl.PropertyMapImpl; +import com.healthmarketscience.jackcess.impl.PropertyMaps; import static com.healthmarketscience.jackcess.Database.*; import static com.healthmarketscience.jackcess.DatabaseTest.*; -import static com.healthmarketscience.jackcess.JetFormatTest.*; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; +import com.healthmarketscience.jackcess.impl.TableImpl; +import com.healthmarketscience.jackcess.impl.DatabaseImpl; /** * @author James Ahlborn @@ -49,12 +52,12 @@ public class PropertiesTest extends TestCase assertFalse(maps.iterator().hasNext()); assertEquals(10, maps.getObjectId()); - PropertyMap defMap = maps.getDefault(); + PropertyMapImpl defMap = maps.getDefault(); assertTrue(defMap.isEmpty()); assertEquals(0, defMap.getSize()); assertFalse(defMap.iterator().hasNext()); - PropertyMap colMap = maps.get("testcol"); + PropertyMapImpl colMap = maps.get("testcol"); assertTrue(colMap.isEmpty()); assertEquals(0, colMap.getSize()); assertFalse(colMap.iterator().hasNext()); @@ -105,7 +108,7 @@ public class PropertiesTest extends TestCase for(TestDB testDb : SUPPORTED_DBS_TEST_FOR_READ) { Database db = open(testDb); - Table t = db.getTable("Table1"); + TableImpl t = (TableImpl)db.getTable("Table1"); assertEquals(t.getTableDefPageNumber(), t.getPropertyMaps().getObjectId()); PropertyMap tProps = t.getProperties(); @@ -186,10 +189,10 @@ public class PropertiesTest extends TestCase assertTrue(((String)dbProps.getValue(PropertyMap.ACCESS_VERSION_PROP)) .matches("[0-9]{2}[.][0-9]{2}")); - for(Map row : db.getSystemCatalog()) { + for(Map row : ((DatabaseImpl)db).getSystemCatalog()) { int id = (Integer)row.get("Id"); byte[] propBytes = (byte[])row.get("LvProp"); - PropertyMaps propMaps = db.getPropertiesForObject(id); + PropertyMaps propMaps = ((DatabaseImpl)db).getPropertiesForObject(id); int byteLen = ((propBytes != null) ? propBytes.length : 0); if(byteLen == 0) { assertTrue(propMaps.isEmpty()); diff --git a/test/src/java/com/healthmarketscience/jackcess/RelationshipTest.java b/test/src/java/com/healthmarketscience/jackcess/RelationshipTest.java index e49b9bb..e2162c3 100644 --- a/test/src/java/com/healthmarketscience/jackcess/RelationshipTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/RelationshipTest.java @@ -34,7 +34,8 @@ import java.util.Comparator; import java.util.List; import static com.healthmarketscience.jackcess.DatabaseTest.*; -import static com.healthmarketscience.jackcess.JetFormatTest.*; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; +import com.healthmarketscience.jackcess.impl.RelationshipImpl; import junit.framework.TestCase; /** @@ -47,7 +48,7 @@ public class RelationshipTest extends TestCase { return String.CASE_INSENSITIVE_ORDER.compare(r1.getName(), r2.getName()); } }; - + public RelationshipTest(String name) throws Exception { super(name); } @@ -70,7 +71,7 @@ public class RelationshipTest extends TestCase { assertEquals(Arrays.asList(t1.getColumn("otherfk1")), rel.getToColumns()); assertTrue(rel.hasReferentialIntegrity()); - assertEquals(4096, rel.getFlags()); + assertEquals(4096, ((RelationshipImpl)rel).getFlags()); assertTrue(rel.cascadeDeletes()); assertSameRelationships(rels, db.getRelationships(t2, t1), true); @@ -89,7 +90,7 @@ public class RelationshipTest extends TestCase { assertEquals(Arrays.asList(t1.getColumn("otherfk2")), rel.getToColumns()); assertTrue(rel.hasReferentialIntegrity()); - assertEquals(256, rel.getFlags()); + assertEquals(256, ((RelationshipImpl)rel).getFlags()); assertTrue(rel.cascadeUpdates()); assertSameRelationships(rels, db.getRelationships(t3, t1), true); diff --git a/test/src/java/com/healthmarketscience/jackcess/TableTest.java b/test/src/java/com/healthmarketscience/jackcess/TableTest.java index 146d42d..b70c045 100644 --- a/test/src/java/com/healthmarketscience/jackcess/TableTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/TableTest.java @@ -36,6 +36,10 @@ import java.util.Calendar; import java.util.List; import java.util.TimeZone; +import com.healthmarketscience.jackcess.impl.ColumnImpl; +import com.healthmarketscience.jackcess.impl.JetFormat; +import com.healthmarketscience.jackcess.impl.PageChannel; +import com.healthmarketscience.jackcess.impl.TableImpl; import junit.framework.TestCase; /** @@ -43,24 +47,29 @@ import junit.framework.TestCase; */ public class TableTest extends TestCase { - private final PageChannel _pageChannel = new PageChannel(true); - private List _columns = new ArrayList(); - private Table _testTable; + private final PageChannel _pageChannel = new PageChannel(true) {}; + private List _columns = new ArrayList(); + private TableImpl _testTable; + private int _varLenIdx; + private int _fixedOffset; + public TableTest(String name) { super(name); } + + private void reset() { + _testTable = null; + _columns = new ArrayList(); + _varLenIdx = 0; + _fixedOffset = 0; + } public void testCreateRow() throws Exception { - Column col = newTestColumn(); - col.setType(DataType.INT); - _columns.add(col); - col = newTestColumn(); - col.setType(DataType.TEXT); - _columns.add(col); - col = newTestColumn(); - col.setType(DataType.TEXT); - _columns.add(col); + reset(); + newTestColumn(DataType.INT, false); + newTestColumn(DataType.TEXT, false); + newTestColumn(DataType.TEXT, false); newTestTable(); int colCount = _columns.size(); @@ -77,13 +86,9 @@ public class TableTest extends TestCase { } public void testUnicodeCompression() throws Exception { - Column col = newTestColumn(); - col = newTestColumn(); - col.setType(DataType.TEXT); - _columns.add(col); - col = newTestColumn(); - col.setType(DataType.MEMO); - _columns.add(col); + reset(); + newTestColumn(DataType.TEXT, false); + newTestColumn(DataType.MEMO, false); newTestTable(); String small = "this is a string"; @@ -94,9 +99,10 @@ public class TableTest extends TestCase { ByteBuffer[] buf1 = encodeColumns(small, large); ByteBuffer[] buf2 = encodeColumns(smallNotAscii, largeNotAscii); - for(Column tmp : _columns) { - tmp.setCompressedUnicode(true); - } + reset(); + newTestColumn(DataType.TEXT, true); + newTestColumn(DataType.MEMO, true); + newTestTable(); ByteBuffer[] bufCmp1 = encodeColumns(small, large); ByteBuffer[] bufCmp2 = encodeColumns(smallNotAscii, largeNotAscii); @@ -129,7 +135,7 @@ public class TableTest extends TestCase { { ByteBuffer[] result = new ByteBuffer[_columns.size()]; for(int i = 0; i < _columns.size(); ++i) { - Column col = _columns.get(i); + ColumnImpl col = _columns.get(i); result[i] = col.write(row[i], _testTable.getFormat().MAX_ROW_SIZE); } return result; @@ -140,7 +146,7 @@ public class TableTest extends TestCase { { Object[] result = new Object[_columns.size()]; for(int i = 0; i < _columns.size(); ++i) { - Column col = _columns.get(i); + ColumnImpl col = _columns.get(i); result[i] = col.read(toBytes(buffers[i])); } return result; @@ -153,10 +159,10 @@ public class TableTest extends TestCase { return b; } - private Table newTestTable() + private TableImpl newTestTable() throws Exception { - _testTable = new Table(true, _columns) { + _testTable = new TableImpl(true, _columns) { @Override public PageChannel getPageChannel() { return _pageChannel; @@ -169,10 +175,22 @@ public class TableTest extends TestCase { return _testTable; } - private Column newTestColumn() { - return new Column(true, null) { + private void newTestColumn(DataType type, final boolean compressedUnicode) { + + int nextColIdx = _columns.size(); + int nextVarLenIdx = 0; + int nextFixedOff = 0; + + if(type.isVariableLength()) { + nextVarLenIdx = _varLenIdx++; + } else { + nextFixedOff = _fixedOffset; + _fixedOffset += type.getFixedSize(); + } + + ColumnImpl col = new ColumnImpl(null, type, nextColIdx, nextFixedOff, nextVarLenIdx) { @Override - public Table getTable() { + public TableImpl getTable() { return _testTable; } @Override @@ -184,14 +202,20 @@ public class TableTest extends TestCase { return getTable().getPageChannel(); } @Override - Charset getCharset() { + protected Charset getCharset() { return getFormat().CHARSET; } @Override - Calendar getCalendar() { + protected Calendar getCalendar() { return Calendar.getInstance(); } + @Override + public boolean isCompressedUnicode() { + return compressedUnicode; + } }; + + _columns.add(col); } } diff --git a/test/src/java/com/healthmarketscience/jackcess/CodecHandlerTest.java b/test/src/java/com/healthmarketscience/jackcess/impl/CodecHandlerTest.java similarity index 83% rename from test/src/java/com/healthmarketscience/jackcess/CodecHandlerTest.java rename to test/src/java/com/healthmarketscience/jackcess/impl/CodecHandlerTest.java index edcbf09..47a832a 100644 --- a/test/src/java/com/healthmarketscience/jackcess/CodecHandlerTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/impl/CodecHandlerTest.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.io.File; import java.io.IOException; @@ -36,7 +36,18 @@ import java.nio.charset.Charset; import java.util.Iterator; import java.util.Map; -import static com.healthmarketscience.jackcess.JetFormatTest.*; +import com.healthmarketscience.jackcess.ColumnBuilder; +import com.healthmarketscience.jackcess.Cursor; +import com.healthmarketscience.jackcess.CursorBuilder; +import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.Database; +import com.healthmarketscience.jackcess.DatabaseBuilder; +import com.healthmarketscience.jackcess.DatabaseTest; +import com.healthmarketscience.jackcess.DatabaseTest; +import com.healthmarketscience.jackcess.IndexBuilder; +import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.TableBuilder; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; import junit.framework.TestCase; /** @@ -75,7 +86,7 @@ public class CodecHandlerTest extends TestCase { for(Database.FileFormat ff : SUPPORTED_FILEFORMATS) { Database db = DatabaseTest.create(ff); - int pageSize = db.getFormat().PAGE_SIZE; + int pageSize = ((DatabaseImpl)db).getFormat().PAGE_SIZE; File dbFile = db.getFile(); db.close(); @@ -118,13 +129,13 @@ public class CodecHandlerTest extends TestCase t2.addRow(null, "rowdata-" + i + DatabaseTest.createString(100)); } - Cursor c1 = new CursorBuilder(t1).setIndex(t1.getPrimaryKeyIndex()) + Cursor c1 = t1.newCursor().setIndex(t1.getPrimaryKeyIndex()) .toCursor(); - Cursor c2 = new CursorBuilder(t2).setIndex(t2.getPrimaryKeyIndex()) + Cursor c2 = t2.newCursor().setIndex(t2.getPrimaryKeyIndex()) .toCursor(); - Iterator> i1 = c1.iterator(); - Iterator> i2 = c2.reverseIterable().iterator(); + Iterator> i1 = c1.iterator(); + Iterator> i2 = c2.newIterable().reverse().iterator(); int t1rows = 0; int t2rows = 0; @@ -225,9 +236,16 @@ public class CodecHandlerTest extends TestCase public boolean canEncodePartialPage() { return true; } + + public boolean canDecodeInline() { + return true; + } - public void decodePage(ByteBuffer page, int pageNumber) throws IOException { - byte[] arr = page.array(); + public void decodePage(ByteBuffer inPage, ByteBuffer outPage, + int pageNumber) + throws IOException + { + byte[] arr = inPage.array(); simpleDecode(arr, arr, pageNumber); } @@ -256,9 +274,16 @@ public class CodecHandlerTest extends TestCase public boolean canEncodePartialPage() { return false; } + + public boolean canDecodeInline() { + return true; + } - public void decodePage(ByteBuffer page, int pageNumber) throws IOException { - byte[] arr = page.array(); + public void decodePage(ByteBuffer inPage, ByteBuffer outPage, + int pageNumber) + throws IOException + { + byte[] arr = inPage.array(); fullDecode(arr, arr, pageNumber); } diff --git a/test/src/java/com/healthmarketscience/jackcess/FKEnforcerTest.java b/test/src/java/com/healthmarketscience/jackcess/impl/FKEnforcerTest.java similarity index 80% rename from test/src/java/com/healthmarketscience/jackcess/FKEnforcerTest.java rename to test/src/java/com/healthmarketscience/jackcess/impl/FKEnforcerTest.java index 9dd0c88..7ea3123 100644 --- a/test/src/java/com/healthmarketscience/jackcess/FKEnforcerTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/impl/FKEnforcerTest.java @@ -17,15 +17,21 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.io.IOException; import java.util.Iterator; import java.util.List; import java.util.Map; +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.Cursor; +import com.healthmarketscience.jackcess.CursorBuilder; +import com.healthmarketscience.jackcess.Database; import static com.healthmarketscience.jackcess.DatabaseTest.*; -import static com.healthmarketscience.jackcess.JetFormatTest.*; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.Table; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; import junit.framework.TestCase; /** @@ -43,17 +49,18 @@ public class FKEnforcerTest extends TestCase for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.INDEX)) { Database db = openCopy(testDB); + db.setEnforceForeignKeys(false); Table t1 = db.getTable("Table1"); Table t2 = db.getTable("Table2"); Table t3 = db.getTable("Table3"); t1.addRow(20, 0, 20, "some data", 20); - Cursor c = Cursor.createCursor(t2); + Cursor c = CursorBuilder.createCursor(t2); c.moveToNextRow(); c.updateCurrentRow(30, "foo30"); - c = Cursor.createCursor(t3); + c = CursorBuilder.createCursor(t3); c.moveToNextRow(); c.deleteCurrentRow(); @@ -66,7 +73,6 @@ public class FKEnforcerTest extends TestCase for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.INDEX)) { Database db = openCopy(testDB); - db.setEnforceForeignKeys(true); Table t1 = db.getTable("Table1"); Table t2 = db.getTable("Table2"); Table t3 = db.getTable("Table3"); @@ -80,7 +86,7 @@ public class FKEnforcerTest extends TestCase } try { - Cursor c = Cursor.createCursor(t2); + Cursor c = CursorBuilder.createCursor(t2); c.moveToNextRow(); c.updateCurrentRow(30, "foo30"); fail("IOException should have been thrown"); @@ -90,7 +96,7 @@ public class FKEnforcerTest extends TestCase } try { - Cursor c = Cursor.createCursor(t3); + Cursor c = CursorBuilder.createCursor(t3); c.moveToNextRow(); c.deleteCurrentRow(); fail("IOException should have been thrown"); @@ -99,7 +105,7 @@ public class FKEnforcerTest extends TestCase assertTrue(ignored.getMessage().contains("Table3[id]")); } - Cursor c = Cursor.createCursor(t3); + Cursor c = CursorBuilder.createCursor(t3); Column col = t3.getColumn("id"); for(Map row : c) { int id = (Integer)row.get("id"); @@ -107,7 +113,7 @@ public class FKEnforcerTest extends TestCase c.setCurrentRowValue(col, id); } - List> expectedRows = + List> expectedRows = createExpectedTable( createT1Row(0, 0, 30, "baz0", 0), createT1Row(1, 1, 31, "baz11", 0), @@ -116,7 +122,7 @@ public class FKEnforcerTest extends TestCase assertTable(expectedRows, t1); - c = Cursor.createCursor(t2); + c = CursorBuilder.createCursor(t2); for(Iterator iter = c.iterator(); iter.hasNext(); ) { iter.next(); iter.remove(); @@ -129,7 +135,7 @@ public class FKEnforcerTest extends TestCase } - private static Map createT1Row( + private static Row createT1Row( int id1, int fk1, int fk2, String data, int fk3) { return createExpectedRow("id", id1, "otherfk1", fk1, "otherfk2", fk2, diff --git a/test/src/java/com/healthmarketscience/jackcess/IndexCodesTest.java b/test/src/java/com/healthmarketscience/jackcess/impl/IndexCodesTest.java similarity index 95% rename from test/src/java/com/healthmarketscience/jackcess/IndexCodesTest.java rename to test/src/java/com/healthmarketscience/jackcess/impl/IndexCodesTest.java index ed71ebe..56f9096 100644 --- a/test/src/java/com/healthmarketscience/jackcess/IndexCodesTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/impl/IndexCodesTest.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.io.File; import java.lang.reflect.Field; @@ -37,11 +37,17 @@ import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; -import junit.framework.TestCase; - +import com.healthmarketscience.jackcess.ColumnBuilder; +import com.healthmarketscience.jackcess.Cursor; +import com.healthmarketscience.jackcess.CursorBuilder; +import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.Database; import static com.healthmarketscience.jackcess.DatabaseTest.*; -import static com.healthmarketscience.jackcess.JetFormatTest.*; - +import com.healthmarketscience.jackcess.Index; +import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.TableBuilder; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; +import junit.framework.TestCase; /** * @author James Ahlborn @@ -86,7 +92,7 @@ public class IndexCodesTest extends TestCase { // index.initialize(); // System.out.println("Ind " + index); - Cursor cursor = Cursor.createIndexCursor(t, index); + Cursor cursor = CursorBuilder.createCursor(t, index); while(cursor.moveToNextRow()) { Map row = cursor.getCurrentRow(); @@ -111,8 +117,8 @@ public class IndexCodesTest extends TestCase { Cursor.Position expectedPos) throws Exception { - Object[] idxRow = index.constructIndexRow(expectedRow); - Cursor cursor = Cursor.createIndexCursor(t, index, idxRow, idxRow); + Object[] idxRow = ((IndexImpl)index).constructIndexRow(expectedRow); + Cursor cursor = CursorBuilder.createCursor(t, index, idxRow, idxRow); Cursor.Position startPos = cursor.getSavepoint().getCurrentPosition(); @@ -258,11 +264,11 @@ public class IndexCodesTest extends TestCase { Table t = db.getTable("Table5"); Index ind = t.getIndexes().iterator().next(); - ind.initialize(); + ((IndexImpl)ind).initialize(); System.out.println("Ind " + ind); - Cursor cursor = Cursor.createIndexCursor(t, ind); + Cursor cursor = CursorBuilder.createCursor(t, ind); while(cursor.moveToNextRow()) { System.out.println("======="); String entryStr = @@ -311,10 +317,10 @@ public class IndexCodesTest extends TestCase { Table t = db.getTable("Table1"); Index index = t.getIndex("B"); - index.initialize(); + ((IndexImpl)index).initialize(); System.out.println("Ind " + index); - Cursor cursor = Cursor.createIndexCursor(t, index); + Cursor cursor = CursorBuilder.createCursor(t, index); while(cursor.moveToNextRow()) { System.out.println("======="); System.out.println("Savepoint: " + cursor.getSavepoint()); @@ -330,7 +336,7 @@ public class IndexCodesTest extends TestCase { Table t = db.getTable("Table1"); Index index = t.getIndexes().iterator().next(); - index.initialize(); + ((IndexImpl)index).initialize(); System.out.println("Ind " + index); Pattern inlinePat = Pattern.compile("7F 0E 02 0E 02 (.*)0E 02 0E 02 01 00"); @@ -349,7 +355,7 @@ public class IndexCodesTest extends TestCase { Map inat2CrazyCodes = new TreeMap(); - Cursor cursor = Cursor.createIndexCursor(t, index); + Cursor cursor = CursorBuilder.createCursor(t, index); while(cursor.moveToNextRow()) { // System.out.println("======="); // System.out.println("Savepoint: " + cursor.getSavepoint()); @@ -509,7 +515,7 @@ public class IndexCodesTest extends TestCase { Table t = db.getTable("Table1"); Index index = t.getIndexes().iterator().next(); - index.initialize(); + ((IndexImpl)index).initialize(); System.out.println("Ind " + index); Pattern inlinePat = Pattern.compile("7F 4A 4A (.*)4A 4A 01 00"); @@ -528,7 +534,7 @@ public class IndexCodesTest extends TestCase { Map inat2CrazyCodes = new TreeMap(); - Cursor cursor = Cursor.createIndexCursor(t, index); + Cursor cursor = CursorBuilder.createCursor(t, index); while(cursor.moveToNextRow()) { // System.out.println("======="); // System.out.println("Savepoint: " + cursor.getSavepoint()); @@ -774,7 +780,7 @@ public class IndexCodesTest extends TestCase { return builder.toString(); } - static String entryToString(Cursor.Position curPos) + public static String entryToString(Cursor.Position curPos) throws Exception { Field eField = curPos.getClass().getDeclaredField("_entry"); diff --git a/test/src/java/com/healthmarketscience/jackcess/JetFormatTest.java b/test/src/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java similarity index 87% rename from test/src/java/com/healthmarketscience/jackcess/JetFormatTest.java rename to test/src/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java index 9c75b6d..962a6f0 100644 --- a/test/src/java/com/healthmarketscience/jackcess/JetFormatTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java @@ -1,4 +1,4 @@ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.impl; import java.io.File; import java.io.IOException; @@ -8,10 +8,12 @@ import java.util.EnumSet; import java.util.List; import java.util.Set; -import junit.framework.TestCase; - +import com.healthmarketscience.jackcess.Database; import static com.healthmarketscience.jackcess.Database.*; +import com.healthmarketscience.jackcess.DatabaseBuilder; import static com.healthmarketscience.jackcess.DatabaseTest.*; +import junit.framework.TestCase; + /** * @author Dan Rollo @@ -20,7 +22,7 @@ import static com.healthmarketscience.jackcess.DatabaseTest.*; */ public class JetFormatTest extends TestCase { - static final File DIR_TEST_DATA = new File("test/data"); + public static final File DIR_TEST_DATA = new File("test/data"); /** * Defines known valid db test file base names. @@ -59,8 +61,8 @@ public class JetFormatTest extends TestCase { /** Defines currently supported db file formats. (can be modified at runtime via the system property "com.healthmarketscience.jackcess.testFormats") */ - final static FileFormat[] SUPPORTED_FILEFORMATS; - final static FileFormat[] SUPPORTED_FILEFORMATS_FOR_READ; + public final static FileFormat[] SUPPORTED_FILEFORMATS; + public final static FileFormat[] SUPPORTED_FILEFORMATS_FOR_READ; static { String testFormatStr = System.getProperty("com.healthmarketscience.jackcess.testFormats"); @@ -79,7 +81,8 @@ public class JetFormatTest extends TestCase { continue; } supportedForRead.add(ff); - if(ff.getJetFormat().READ_ONLY || (ff == FileFormat.MSISAM)) { + if(DatabaseImpl.getFileFormatDetails(ff).getFormat().READ_ONLY || + (ff == FileFormat.MSISAM)) { continue; } supported.add(ff); @@ -112,7 +115,7 @@ public class JetFormatTest extends TestCase { } public final JetFormat getExpectedFormat() { - return expectedFileFormat.getJetFormat(); + return DatabaseImpl.getFileFormatDetails(expectedFileFormat).getFormat(); } @Override @@ -139,7 +142,7 @@ public class JetFormatTest extends TestCase { // verify that the db is the file format expected try { - Database db = Database.open(testFile, true); + Database db = new DatabaseBuilder(testFile).setReadOnly(true).open(); FileFormat dbFileFormat = db.getFileFormat(); db.close(); if(dbFileFormat != fileFormat) { @@ -165,9 +168,9 @@ public class JetFormatTest extends TestCase { } } - static final List SUPPORTED_DBS_TEST = + public static final List SUPPORTED_DBS_TEST = TestDB.getSupportedForBasename(Basename.TEST); - static final List SUPPORTED_DBS_TEST_FOR_READ = + public static final List SUPPORTED_DBS_TEST_FOR_READ = TestDB.getSupportedForBasename(Basename.TEST, true); @@ -181,13 +184,13 @@ public class JetFormatTest extends TestCase { for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) { - final FileChannel channel = Database.openChannel(testDB.dbFile, false); + final FileChannel channel = DatabaseImpl.openChannel(testDB.dbFile, false); try { JetFormat fmtActual = JetFormat.getFormat(channel); assertEquals("Unexpected JetFormat for dbFile: " + testDB.dbFile.getAbsolutePath(), - testDB.expectedFileFormat.getJetFormat(), fmtActual); + testDB.getExpectedFormat(), fmtActual); } finally { channel.close(); diff --git a/test/src/java/com/healthmarketscience/jackcess/UsageMapTest.java b/test/src/java/com/healthmarketscience/jackcess/impl/UsageMapTest.java similarity index 84% rename from test/src/java/com/healthmarketscience/jackcess/UsageMapTest.java rename to test/src/java/com/healthmarketscience/jackcess/impl/UsageMapTest.java index 87fa5c3..aad1ddf 100644 --- a/test/src/java/com/healthmarketscience/jackcess/UsageMapTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/impl/UsageMapTest.java @@ -1,11 +1,12 @@ -package com.healthmarketscience.jackcess; - -import junit.framework.TestCase; +package com.healthmarketscience.jackcess.impl; import java.io.File; import java.io.IOException; -import static com.healthmarketscience.jackcess.JetFormatTest.*; +import com.healthmarketscience.jackcess.Database; +import com.healthmarketscience.jackcess.DatabaseBuilder; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; +import junit.framework.TestCase; /** * @author Dan Rollo @@ -42,8 +43,8 @@ public final class UsageMapTest extends TestCase { final int expectedFirstPage, final int expectedLastPage) throws IOException { - final Database db = Database.open(dbFile); - final UsageMap usageMap = UsageMap.read(db, + final Database db = DatabaseBuilder.open(dbFile); + final UsageMap usageMap = UsageMap.read((DatabaseImpl)db, PageChannel.PAGE_GLOBAL_USAGE_MAP, PageChannel.ROW_GLOBAL_USAGE_MAP, true); diff --git a/test/src/java/com/healthmarketscience/jackcess/scsu/CompressMain.java b/test/src/java/com/healthmarketscience/jackcess/impl/scsu/CompressMain.java similarity index 99% rename from test/src/java/com/healthmarketscience/jackcess/scsu/CompressMain.java rename to test/src/java/com/healthmarketscience/jackcess/impl/scsu/CompressMain.java index af3063d..52b9e86 100644 --- a/test/src/java/com/healthmarketscience/jackcess/scsu/CompressMain.java +++ b/test/src/java/com/healthmarketscience/jackcess/impl/scsu/CompressMain.java @@ -1,4 +1,4 @@ -package com.healthmarketscience.jackcess.scsu; +package com.healthmarketscience.jackcess.impl.scsu; import java.io.*; import java.util.*; diff --git a/test/src/java/com/healthmarketscience/jackcess/scsu/CompressTest.java b/test/src/java/com/healthmarketscience/jackcess/impl/scsu/CompressTest.java similarity index 95% rename from test/src/java/com/healthmarketscience/jackcess/scsu/CompressTest.java rename to test/src/java/com/healthmarketscience/jackcess/impl/scsu/CompressTest.java index 0f17e6c..b9dc13a 100644 --- a/test/src/java/com/healthmarketscience/jackcess/scsu/CompressTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/impl/scsu/CompressTest.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess.scsu; +package com.healthmarketscience.jackcess.impl.scsu; import junit.framework.TestCase; diff --git a/test/src/java/com/healthmarketscience/jackcess/query/QueryTest.java b/test/src/java/com/healthmarketscience/jackcess/query/QueryTest.java index 73d91b7..015f2fc 100644 --- a/test/src/java/com/healthmarketscience/jackcess/query/QueryTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/query/QueryTest.java @@ -37,14 +37,15 @@ import java.util.Map; import com.healthmarketscience.jackcess.DataType; import com.healthmarketscience.jackcess.Database; import com.healthmarketscience.jackcess.DatabaseTest; -import com.healthmarketscience.jackcess.query.Query.Row; +import com.healthmarketscience.jackcess.impl.query.QueryImpl; +import com.healthmarketscience.jackcess.impl.query.QueryImpl.Row; import junit.framework.TestCase; import org.apache.commons.lang.StringUtils; import static org.apache.commons.lang.SystemUtils.LINE_SEPARATOR; -import static com.healthmarketscience.jackcess.query.QueryFormat.*; +import static com.healthmarketscience.jackcess.impl.query.QueryFormat.*; -import static com.healthmarketscience.jackcess.JetFormatTest.*; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; /** @@ -169,7 +170,7 @@ public class QueryTest extends TestCase { List rowList = new ArrayList(); rowList.add(newRow(TYPE_ATTRIBUTE, null, -1, null, null)); - Query query = Query.create(-1, "TestQuery", rowList, 13); + QueryImpl query = QueryImpl.create(-1, "TestQuery", rowList, 13); try { query.toSQLString(); fail("UnsupportedOperationException should have been thrown"); @@ -187,7 +188,7 @@ public class QueryTest extends TestCase } try { - new Query("TestQuery", rowList, 13, Query.Type.UNION) { + new QueryImpl("TestQuery", rowList, 13, Query.Type.UNION) { @Override protected void toSQLString(StringBuilder builder) { throw new UnsupportedOperationException(); }}; @@ -468,7 +469,7 @@ public class QueryTest extends TestCase rowList.add(newRow(TYPE_ATTRIBUTE, typeExpr, type.getValue(), null, typeName1, null)); rowList.addAll(Arrays.asList(rows)); - return Query.create(type.getObjectFlag(), "TestQuery", rowList, 13); + return QueryImpl.create(type.getObjectFlag(), "TestQuery", rowList, 13); } private static Row newRow(Byte attr, String expr, String name1, String name2) @@ -487,7 +488,7 @@ public class QueryTest extends TestCase { Short flag = ((flagNum != null) ? flagNum.shortValue() : null); Integer extra = ((extraNum != null) ? extraNum.intValue() : null); - return new Row(attr, expr, flag, extra, name1, name2, null, null); + return new Row(null, attr, expr, flag, extra, name1, name2, null, null); } private static void setFlag(Query query, Number newFlagNum) @@ -498,7 +499,7 @@ public class QueryTest extends TestCase private static void addRows(Query query, Row... rows) { - query.getRows().addAll(Arrays.asList(rows)); + ((QueryImpl)query).getRows().addAll(Arrays.asList(rows)); } private static void replaceRows(Query query, Row... rows) @@ -509,7 +510,7 @@ public class QueryTest extends TestCase private static void removeRows(Query query, Byte attr) { - for(Iterator iter = query.getRows().iterator(); iter.hasNext(); ) { + for(Iterator iter = ((QueryImpl)query).getRows().iterator(); iter.hasNext(); ) { if(attr.equals(iter.next().attribute)) { iter.remove(); } @@ -518,7 +519,7 @@ public class QueryTest extends TestCase private static void removeLastRows(Query query, int num) { - List rows = query.getRows(); + List rows = ((QueryImpl)query).getRows(); int size = rows.size(); rows.subList(size - num, size).clear(); } diff --git a/test/src/java/com/healthmarketscience/jackcess/ErrorHandlerTest.java b/test/src/java/com/healthmarketscience/jackcess/util/ErrorHandlerTest.java similarity index 80% rename from test/src/java/com/healthmarketscience/jackcess/ErrorHandlerTest.java rename to test/src/java/com/healthmarketscience/jackcess/util/ErrorHandlerTest.java index afffdd5..6431ad8 100644 --- a/test/src/java/com/healthmarketscience/jackcess/ErrorHandlerTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/util/ErrorHandlerTest.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; import java.io.IOException; import java.lang.reflect.Field; @@ -33,10 +33,20 @@ import java.lang.reflect.Modifier; import java.nio.ByteOrder; import java.util.List; -import junit.framework.TestCase; - +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.ColumnBuilder; +import com.healthmarketscience.jackcess.Cursor; +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.Table; +import com.healthmarketscience.jackcess.TableBuilder; +import com.healthmarketscience.jackcess.impl.ColumnImpl; +import com.healthmarketscience.jackcess.impl.JetFormatTest; +import com.healthmarketscience.jackcess.impl.TableImpl; +import junit.framework.TestCase; /** * @author James Ahlborn @@ -95,12 +105,12 @@ public class ErrorHandlerTest extends TestCase "val", null)), table); - Cursor c1 = Cursor.createCursor(table); - Cursor c2 = Cursor.createCursor(table); - Cursor c3 = Cursor.createCursor(table); + Cursor c1 = CursorBuilder.createCursor(table); + Cursor c2 = CursorBuilder.createCursor(table); + Cursor c3 = CursorBuilder.createCursor(table); c2.setErrorHandler(new DebugErrorHandler("#error")); - c3.setErrorHandler(Database.DEFAULT_ERROR_HANDLER); + c3.setErrorHandler(ErrorHandler.DEFAULT); assertCursor(createExpectedTable( createExpectedRow("col", "row1", @@ -143,14 +153,15 @@ public class ErrorHandlerTest extends TestCase } @SuppressWarnings("unchecked") - private void replaceColumn(Table t, String colName) throws Exception + private static void replaceColumn(Table t, String colName) throws Exception { - Field colsField = Table.class.getDeclaredField("_columns"); + Field colsField = TableImpl.class.getDeclaredField("_columns"); colsField.setAccessible(true); List cols = (List)colsField.get(t); Column srcCol = null; - Column destCol = new BogusColumn(t); + ColumnImpl destCol = new BogusColumn(t); + destCol.setName(colName); for(int i = 0; i < cols.size(); ++i) { srcCol = cols.get(i); if(srcCol.getName().equals(colName)) { @@ -169,10 +180,10 @@ public class ErrorHandlerTest extends TestCase } - private static class BogusColumn extends Column + private static class BogusColumn extends ColumnImpl { private BogusColumn(Table table) { - super(true, table); + super((TableImpl)table, DataType.LONG, 1, 0, 0); } @Override diff --git a/test/src/java/com/healthmarketscience/jackcess/ExportTest.java b/test/src/java/com/healthmarketscience/jackcess/util/ExportTest.java similarity index 92% rename from test/src/java/com/healthmarketscience/jackcess/ExportTest.java rename to test/src/java/com/healthmarketscience/jackcess/util/ExportTest.java index 7046b8b..a271771 100644 --- a/test/src/java/com/healthmarketscience/jackcess/ExportTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/util/ExportTest.java @@ -25,19 +25,24 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; import java.io.BufferedWriter; import java.io.StringWriter; import java.text.DateFormat; import java.text.SimpleDateFormat; - -import junit.framework.TestCase; -import org.apache.commons.lang.SystemUtils; import java.util.Date; +import com.healthmarketscience.jackcess.ColumnBuilder; +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.Table; +import com.healthmarketscience.jackcess.TableBuilder; +import com.healthmarketscience.jackcess.impl.JetFormatTest; +import junit.framework.TestCase; +import org.apache.commons.lang.SystemUtils; /** * diff --git a/test/src/java/com/healthmarketscience/jackcess/ImportTest.java b/test/src/java/com/healthmarketscience/jackcess/util/ImportTest.java similarity index 94% rename from test/src/java/com/healthmarketscience/jackcess/ImportTest.java rename to test/src/java/com/healthmarketscience/jackcess/util/ImportTest.java index 0be36e1..49be97c 100644 --- a/test/src/java/com/healthmarketscience/jackcess/ImportTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/util/ImportTest.java @@ -25,7 +25,7 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; import java.io.File; import java.lang.reflect.InvocationHandler; @@ -39,10 +39,16 @@ import java.util.Arrays; import java.util.List; import java.util.Map; -import junit.framework.TestCase; - +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.ColumnBuilder; +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.Table; +import com.healthmarketscience.jackcess.TableBuilder; +import com.healthmarketscience.jackcess.impl.JetFormatTest; +import junit.framework.TestCase; /** * @author Rob Di Marco @@ -69,7 +75,7 @@ public class ImportTest extends TestCase } assertEquals(Arrays.asList("Test1", "Test2", "Test3"), colNames); - List> expectedRows = + List> expectedRows = createExpectedTable( createExpectedRow( "Test1", "Foo", @@ -221,13 +227,13 @@ public class ImportTest extends TestCase rs.addColumn(Types.VARCHAR, "col7", Integer.MAX_VALUE, 0, 0); Database db = create(fileFormat); - db.copyTable("Test1", (ResultSet)Proxy.newProxyInstance( + ImportUtil.importResultSet((ResultSet)Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), new Class[]{ResultSet.class}, - rs)); + rs), db, "Test1"); Table t = db.getTable("Test1"); - List columns = t.getColumns(); + List columns = t.getColumns(); assertEquals(7, columns.size()); Column c = columns.get(0); diff --git a/test/src/java/com/healthmarketscience/jackcess/JoinerTest.java b/test/src/java/com/healthmarketscience/jackcess/util/JoinerTest.java similarity index 80% rename from test/src/java/com/healthmarketscience/jackcess/JoinerTest.java rename to test/src/java/com/healthmarketscience/jackcess/util/JoinerTest.java index d2049c3..975b4fb 100644 --- a/test/src/java/com/healthmarketscience/jackcess/JoinerTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/util/JoinerTest.java @@ -17,7 +17,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; import java.util.ArrayList; import java.util.Arrays; @@ -27,8 +27,13 @@ import java.util.List; import java.util.Map; import java.util.Set; +import com.healthmarketscience.jackcess.Database; import static com.healthmarketscience.jackcess.DatabaseTest.*; -import static com.healthmarketscience.jackcess.JetFormatTest.*; +import com.healthmarketscience.jackcess.Index; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.impl.RowImpl; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; import junit.framework.TestCase; /** @@ -80,23 +85,23 @@ public class JoinerTest extends TestCase { } private static void doTestJoiner( - Joiner join, Map>> expectedData) + Joiner join, Map> expectedData) throws Exception { final Set colNames = new HashSet( Arrays.asList("id", "data")); Joiner revJoin = join.createReverse(); - for(Map row : join.getFromTable()) { + for(Row row : join.getFromTable()) { Integer id = (Integer)row.get("id"); - List> joinedRows = - new ArrayList>(); - for(Map t1Row : join.findRowsIterable(row)) { + List joinedRows = + new ArrayList(); + for(Row t1Row : join.findRowsIterable(row)) { joinedRows.add(t1Row); } - List> expectedRows = expectedData.get(id); + List expectedRows = expectedData.get(id); assertEquals(expectedData.get(id), joinedRows); if(!expectedRows.isEmpty()) { @@ -109,16 +114,15 @@ public class JoinerTest extends TestCase { assertNull(join.findFirstRow(row)); } - List> expectedRows2 = new - ArrayList>(); - for(Map tmpRow : expectedRows) { - Map tmpRow2 = new HashMap(tmpRow); + List expectedRows2 = new ArrayList(); + for(Row tmpRow : expectedRows) { + Row tmpRow2 = new RowImpl(tmpRow); tmpRow2.keySet().retainAll(colNames); expectedRows2.add(tmpRow2); } - joinedRows = new ArrayList>(); - for(Map t1Row : join.findRowsIterable(row, colNames)) { + joinedRows = new ArrayList(); + for(Row t1Row : join.findRowsIterable(row, colNames)) { joinedRows.add(t1Row); } @@ -136,7 +140,7 @@ public class JoinerTest extends TestCase { { assertEquals(4, countRows(t2t1Join.getToTable())); - Map row = createExpectedRow("id", 1); + Row row = createExpectedRow("id", 1); assertTrue(t2t1Join.hasRows(row)); assertTrue(t2t1Join.deleteRows(row)); @@ -145,15 +149,15 @@ public class JoinerTest extends TestCase { assertFalse(t2t1Join.deleteRows(row)); assertEquals(2, countRows(t2t1Join.getToTable())); - for(Map t1Row : t2t1Join.getToTable()) { + for(Row t1Row : t2t1Join.getToTable()) { assertFalse(t1Row.get("otherfk1").equals(1)); } } - private static Map>> createT2T1Data() + private static Map> createT2T1Data() { - Map>> data = new - HashMap>>(); + Map> data = new + HashMap>(); data.put(0, createExpectedTable( @@ -175,10 +179,9 @@ public class JoinerTest extends TestCase { return data; } - private static Map>> createT3T1Data() + private static Map> createT3T1Data() { - Map>> data = new - HashMap>>(); + Map> data = new HashMap>(); data.put(10, createExpectedTable( diff --git a/test/src/java/com/healthmarketscience/jackcess/MemFileChannelTest.java b/test/src/java/com/healthmarketscience/jackcess/util/MemFileChannelTest.java similarity index 97% rename from test/src/java/com/healthmarketscience/jackcess/MemFileChannelTest.java rename to test/src/java/com/healthmarketscience/jackcess/util/MemFileChannelTest.java index f84f0ab..3e78a2c 100644 --- a/test/src/java/com/healthmarketscience/jackcess/MemFileChannelTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/util/MemFileChannelTest.java @@ -17,7 +17,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; import java.io.File; import java.io.FileOutputStream; @@ -30,6 +30,8 @@ import java.util.Arrays; import junit.framework.TestCase; +import com.healthmarketscience.jackcess.DatabaseTest; + /** * * @author James Ahlborn diff --git a/test/src/java/com/healthmarketscience/jackcess/RowFilterTest.java b/test/src/java/com/healthmarketscience/jackcess/util/RowFilterTest.java similarity index 72% rename from test/src/java/com/healthmarketscience/jackcess/RowFilterTest.java rename to test/src/java/com/healthmarketscience/jackcess/util/RowFilterTest.java index 586ad9a..7808a08 100644 --- a/test/src/java/com/healthmarketscience/jackcess/RowFilterTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/util/RowFilterTest.java @@ -25,16 +25,17 @@ Suite 200 King of Prussia, PA 19406 */ -package com.healthmarketscience.jackcess; +package com.healthmarketscience.jackcess.util; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Map; - -import junit.framework.TestCase; +import com.healthmarketscience.jackcess.DataType; import static com.healthmarketscience.jackcess.DatabaseTest.*; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.impl.ColumnImpl; +import junit.framework.TestCase; /** * @author James Ahlborn @@ -54,24 +55,25 @@ public class RowFilterTest extends TestCase @SuppressWarnings("unchecked") public void testFilter() throws Exception { - Map row0 = createExpectedRow(ID_COL, 0, COL1, "foo", COL2, 13, COL3, "bar"); - Map row1 = createExpectedRow(ID_COL, 1, COL1, "bar", COL2, 42, COL3, null); - Map row2 = createExpectedRow(ID_COL, 2, COL1, "foo", COL2, 55, COL3, "bar"); - Map row3 = createExpectedRow(ID_COL, 3, COL1, "baz", COL2, 42, COL3, "bar"); - Map row4 = createExpectedRow(ID_COL, 4, COL1, "foo", COL2, 13, COL3, null); - Map row5 = createExpectedRow(ID_COL, 5, COL1, "bla", COL2, 13, COL3, "bar"); + Row row0 = createExpectedRow(ID_COL, 0, COL1, "foo", COL2, 13, COL3, "bar"); + Row row1 = createExpectedRow(ID_COL, 1, COL1, "bar", COL2, 42, COL3, null); + Row row2 = createExpectedRow(ID_COL, 2, COL1, "foo", COL2, 55, COL3, "bar"); + Row row3 = createExpectedRow(ID_COL, 3, COL1, "baz", COL2, 42, COL3, "bar"); + Row row4 = createExpectedRow(ID_COL, 4, COL1, "foo", COL2, 13, COL3, null); + Row row5 = createExpectedRow(ID_COL, 5, COL1, "bla", COL2, 13, COL3, "bar"); - List> rows = Arrays.asList(row0, row1, row2, row3, row4, row5); + List rows = Arrays.asList(row0, row1, row2, row3, row4, row5); + ColumnImpl testCol = new ColumnImpl(null, DataType.TEXT, 0, 0, 0) {}; + testCol.setName(COL1); assertEquals(Arrays.asList(row0, row2, row4), - toList(RowFilter.matchPattern( - new ColumnBuilder(COL1, DataType.TEXT).toColumn(), + toList(RowFilter.matchPattern(testCol, "foo").apply(rows))); assertEquals(Arrays.asList(row1, row3, row5), toList(RowFilter.invert( RowFilter.matchPattern( - new ColumnBuilder(COL1, DataType.TEXT).toColumn(), + testCol, "foo")).apply(rows))); assertEquals(Arrays.asList(row0, row2, row4), @@ -100,10 +102,10 @@ public class RowFilterTest extends TestCase rows))); } - static List> toList(Iterable> rows) + public static List toList(Iterable rows) { - List> rowList = new ArrayList>(); - for(Map row : rows) { + List rowList = new ArrayList(); + for(Row row : rows) { rowList.add(row); } return rowList; -- 2.39.5