aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJames Ahlborn <jtahlborn@yahoo.com>2013-07-30 02:17:15 +0000
committerJames Ahlborn <jtahlborn@yahoo.com>2013-07-30 02:17:15 +0000
commitd1a79d0064632cca220409abb799ab1757c6caa7 (patch)
tree5cea8606b34a37ff241f9b24f0d5e6b2178a10b5
parent50a356790e619903269a2aa52db7f4a72d1d802d (diff)
downloadjackcess-d1a79d0064632cca220409abb799ab1757c6caa7.tar.gz
jackcess-d1a79d0064632cca220409abb799ab1757c6caa7.zip
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
-rw-r--r--TODO.txt54
-rw-r--r--pom.xml15
-rw-r--r--project.properties26
-rw-r--r--project.xml118
-rw-r--r--src/java/com/healthmarketscience/jackcess/BigIndexData.java86
-rw-r--r--src/java/com/healthmarketscience/jackcess/Column.java2461
-rw-r--r--src/java/com/healthmarketscience/jackcess/ColumnBuilder.java214
-rw-r--r--src/java/com/healthmarketscience/jackcess/Cursor.java1407
-rw-r--r--src/java/com/healthmarketscience/jackcess/CursorBuilder.java228
-rw-r--r--src/java/com/healthmarketscience/jackcess/DataType.java2
-rw-r--r--src/java/com/healthmarketscience/jackcess/Database.java2670
-rw-r--r--src/java/com/healthmarketscience/jackcess/DatabaseBuilder.java57
-rw-r--r--src/java/com/healthmarketscience/jackcess/Index.java439
-rw-r--r--src/java/com/healthmarketscience/jackcess/IndexBuilder.java7
-rw-r--r--src/java/com/healthmarketscience/jackcess/IndexCursor.java554
-rw-r--r--src/java/com/healthmarketscience/jackcess/PropertyMap.java122
-rw-r--r--src/java/com/healthmarketscience/jackcess/Relationship.java123
-rw-r--r--src/java/com/healthmarketscience/jackcess/Row.java (renamed from src/java/com/healthmarketscience/jackcess/LinkResolver.java)14
-rw-r--r--src/java/com/healthmarketscience/jackcess/RowId.java112
-rw-r--r--src/java/com/healthmarketscience/jackcess/RuntimeIOException.java42
-rw-r--r--src/java/com/healthmarketscience/jackcess/SimpleIndexData.java241
-rw-r--r--src/java/com/healthmarketscience/jackcess/Table.java2553
-rw-r--r--src/java/com/healthmarketscience/jackcess/TableBuilder.java103
-rw-r--r--src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java493
-rw-r--r--src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java425
-rw-r--r--src/java/com/healthmarketscience/jackcess/complex/ComplexValue.java91
-rw-r--r--src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java323
-rw-r--r--src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java110
-rw-r--r--src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java107
-rw-r--r--src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java212
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/ByteUtil.java (renamed from src/java/com/healthmarketscience/jackcess/ByteUtil.java)4
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/CodecHandler.java (renamed from src/java/com/healthmarketscience/jackcess/CodecHandler.java)18
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/CodecProvider.java (renamed from src/java/com/healthmarketscience/jackcess/CodecProvider.java)2
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java2280
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/ComplexColumnSupport.java201
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/CursorImpl.java961
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java2114
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/DefaultCodecProvider.java (renamed from src/java/com/healthmarketscience/jackcess/DefaultCodecProvider.java)20
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/FKEnforcer.java (renamed from src/java/com/healthmarketscience/jackcess/FKEnforcer.java)40
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/GeneralIndexCodes.java (renamed from src/java/com/healthmarketscience/jackcess/GeneralIndexCodes.java)6
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/GeneralLegacyIndexCodes.java (renamed from src/java/com/healthmarketscience/jackcess/GeneralLegacyIndexCodes.java)12
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/IndexCodes.java (renamed from src/java/com/healthmarketscience/jackcess/IndexCodes.java)2
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/IndexCursorImpl.java510
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/IndexData.java (renamed from src/java/com/healthmarketscience/jackcess/IndexData.java)278
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/IndexImpl.java458
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java (renamed from src/java/com/healthmarketscience/jackcess/IndexPageCache.java)78
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/JetFormat.java (renamed from src/java/com/healthmarketscience/jackcess/JetFormat.java)29
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/NullMask.java (renamed from src/java/com/healthmarketscience/jackcess/NullMask.java)6
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/PageChannel.java (renamed from src/java/com/healthmarketscience/jackcess/PageChannel.java)111
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/PageTypes.java (renamed from src/java/com/healthmarketscience/jackcess/PageTypes.java)2
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/PropertyMapImpl.java146
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java (renamed from src/java/com/healthmarketscience/jackcess/PropertyMaps.java)63
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/RelationshipImpl.java152
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/RowIdImpl.java138
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/RowImpl.java65
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/TableCreator.java (renamed from src/java/com/healthmarketscience/jackcess/TableCreator.java)76
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/TableImpl.java2589
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/TableScanCursor.java220
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/TempBufferHolder.java (renamed from src/java/com/healthmarketscience/jackcess/TempBufferHolder.java)2
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/TempPageHolder.java (renamed from src/java/com/healthmarketscience/jackcess/TempPageHolder.java)2
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/UnsupportedCodecException.java (renamed from src/java/com/healthmarketscience/jackcess/UnsupportedCodecException.java)5
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/UsageMap.java (renamed from src/java/com/healthmarketscience/jackcess/UsageMap.java)56
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/complex/AttachmentColumnInfoImpl.java482
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/complex/ComplexColumnInfoImpl.java419
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/complex/ComplexValueForeignKeyImpl.java289
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/complex/MultiValueColumnInfoImpl.java125
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/complex/UnsupportedColumnInfoImpl.java141
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/complex/VersionHistoryColumnInfoImpl.java224
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/query/AppendQueryImpl.java92
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/query/BaseSelectQueryImpl.java177
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/query/CrossTabQueryImpl.java100
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/query/DataDefinitionQueryImpl.java65
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/query/DeleteQueryImpl.java54
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/query/MakeTableQueryImpl.java73
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/query/PassthroughQueryImpl.java69
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/query/QueryFormat.java (renamed from src/java/com/healthmarketscience/jackcess/query/QueryFormat.java)2
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/query/QueryImpl.java721
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/query/SelectQueryImpl.java53
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/query/UnionQueryImpl.java96
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/query/UpdateQueryImpl.java94
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/scsu/Compress.java (renamed from src/java/com/healthmarketscience/jackcess/scsu/Compress.java)2
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/scsu/Debug.java (renamed from src/java/com/healthmarketscience/jackcess/scsu/Debug.java)2
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/scsu/EndOfInputException.java (renamed from src/java/com/healthmarketscience/jackcess/scsu/EndOfInputException.java)2
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/scsu/EndOfOutputException.java (renamed from src/java/com/healthmarketscience/jackcess/scsu/EndOfOutputException.java)2
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/scsu/Expand.java (renamed from src/java/com/healthmarketscience/jackcess/scsu/Expand.java)2
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/scsu/IllegalInputException.java (renamed from src/java/com/healthmarketscience/jackcess/scsu/IllegalInputException.java)2
-rw-r--r--src/java/com/healthmarketscience/jackcess/impl/scsu/SCSU.java (renamed from src/java/com/healthmarketscience/jackcess/scsu/SCSU.java)2
-rw-r--r--src/java/com/healthmarketscience/jackcess/query/AppendQuery.java64
-rw-r--r--src/java/com/healthmarketscience/jackcess/query/BaseSelectQuery.java165
-rw-r--r--src/java/com/healthmarketscience/jackcess/query/CrossTabQuery.java71
-rw-r--r--src/java/com/healthmarketscience/jackcess/query/DataDefinitionQuery.java38
-rw-r--r--src/java/com/healthmarketscience/jackcess/query/DeleteQuery.java26
-rw-r--r--src/java/com/healthmarketscience/jackcess/query/MakeTableQuery.java45
-rw-r--r--src/java/com/healthmarketscience/jackcess/query/PassthroughQuery.java44
-rw-r--r--src/java/com/healthmarketscience/jackcess/query/Query.java665
-rw-r--r--src/java/com/healthmarketscience/jackcess/query/SelectQuery.java26
-rw-r--r--src/java/com/healthmarketscience/jackcess/query/UnionQuery.java69
-rw-r--r--src/java/com/healthmarketscience/jackcess/query/UpdateQuery.java66
-rw-r--r--src/java/com/healthmarketscience/jackcess/util/CaseInsensitiveColumnMatcher.java (renamed from src/java/com/healthmarketscience/jackcess/CaseInsensitiveColumnMatcher.java)15
-rw-r--r--src/java/com/healthmarketscience/jackcess/util/ColumnMatcher.java (renamed from src/java/com/healthmarketscience/jackcess/ColumnMatcher.java)4
-rw-r--r--src/java/com/healthmarketscience/jackcess/util/DebugErrorHandler.java (renamed from src/java/com/healthmarketscience/jackcess/DebugErrorHandler.java)16
-rw-r--r--src/java/com/healthmarketscience/jackcess/util/EntryIterableBuilder.java114
-rw-r--r--src/java/com/healthmarketscience/jackcess/util/ErrorHandler.java (renamed from src/java/com/healthmarketscience/jackcess/ErrorHandler.java)44
-rw-r--r--src/java/com/healthmarketscience/jackcess/util/ExportFilter.java (renamed from src/java/com/healthmarketscience/jackcess/ExportFilter.java)6
-rw-r--r--src/java/com/healthmarketscience/jackcess/util/ExportUtil.java (renamed from src/java/com/healthmarketscience/jackcess/ExportUtil.java)15
-rw-r--r--src/java/com/healthmarketscience/jackcess/util/ImportFilter.java (renamed from src/java/com/healthmarketscience/jackcess/ImportFilter.java)7
-rw-r--r--src/java/com/healthmarketscience/jackcess/util/ImportUtil.java (renamed from src/java/com/healthmarketscience/jackcess/ImportUtil.java)31
-rw-r--r--src/java/com/healthmarketscience/jackcess/util/IterableBuilder.java186
-rw-r--r--src/java/com/healthmarketscience/jackcess/util/Joiner.java (renamed from src/java/com/healthmarketscience/jackcess/Joiner.java)61
-rw-r--r--src/java/com/healthmarketscience/jackcess/util/LinkResolver.java54
-rw-r--r--src/java/com/healthmarketscience/jackcess/util/MemFileChannel.java (renamed from src/java/com/healthmarketscience/jackcess/MemFileChannel.java)14
-rw-r--r--src/java/com/healthmarketscience/jackcess/util/ReplacementErrorHandler.java (renamed from src/java/com/healthmarketscience/jackcess/ReplacementErrorHandler.java)10
-rw-r--r--src/java/com/healthmarketscience/jackcess/util/RowFilter.java (renamed from src/java/com/healthmarketscience/jackcess/RowFilter.java)48
-rw-r--r--src/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java (renamed from src/java/com/healthmarketscience/jackcess/SimpleColumnMatcher.java)4
-rw-r--r--src/java/com/healthmarketscience/jackcess/util/SimpleExportFilter.java (renamed from src/java/com/healthmarketscience/jackcess/SimpleExportFilter.java)3
-rw-r--r--src/java/com/healthmarketscience/jackcess/util/SimpleImportFilter.java (renamed from src/java/com/healthmarketscience/jackcess/SimpleImportFilter.java)10
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/BigIndexTest.java63
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java13
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/CursorBuilderTest.java25
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/CursorTest.java211
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java210
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/IndexTest.java90
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/PropertiesTest.java17
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/RelationshipTest.java9
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/TableTest.java86
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/impl/CodecHandlerTest.java (renamed from test/src/java/com/healthmarketscience/jackcess/CodecHandlerTest.java)47
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/impl/FKEnforcerTest.java (renamed from test/src/java/com/healthmarketscience/jackcess/FKEnforcerTest.java)28
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/impl/IndexCodesTest.java (renamed from test/src/java/com/healthmarketscience/jackcess/IndexCodesTest.java)40
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java (renamed from test/src/java/com/healthmarketscience/jackcess/JetFormatTest.java)29
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/impl/UsageMapTest.java (renamed from test/src/java/com/healthmarketscience/jackcess/UsageMapTest.java)13
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/impl/scsu/CompressMain.java (renamed from test/src/java/com/healthmarketscience/jackcess/scsu/CompressMain.java)2
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/impl/scsu/CompressTest.java (renamed from test/src/java/com/healthmarketscience/jackcess/scsu/CompressTest.java)2
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/query/QueryTest.java21
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/util/ErrorHandlerTest.java (renamed from test/src/java/com/healthmarketscience/jackcess/ErrorHandlerTest.java)35
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/util/ExportTest.java (renamed from test/src/java/com/healthmarketscience/jackcess/ExportTest.java)13
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/util/ImportTest.java (renamed from test/src/java/com/healthmarketscience/jackcess/ImportTest.java)20
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/util/JoinerTest.java (renamed from test/src/java/com/healthmarketscience/jackcess/JoinerTest.java)47
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/util/MemFileChannelTest.java (renamed from test/src/java/com/healthmarketscience/jackcess/MemFileChannelTest.java)4
-rw-r--r--test/src/java/com/healthmarketscience/jackcess/util/RowFilterTest.java (renamed from test/src/java/com/healthmarketscience/jackcess/RowFilterTest.java)36
139 files changed, 16158 insertions, 14169 deletions
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 @@
<artifactId>jackcess</artifactId>
<name>Jackcess</name>
<description>A pure Java library for reading from and writing to MS Access databases.</description>
- <version>1.2.15-SNAPSHOT</version>
+ <version>2.0.0-SNAPSHOT</version>
<url>http://jackcess.sf.net</url>
<inceptionYear>2005</inceptionYear>
<developers>
@@ -118,8 +118,11 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
+ <version>2.15</version>
<configuration>
<forkMode>once</forkMode>
+ <parallel>classes</parallel>
+ <threadCount>1</threadCount>
<argLine>-Xmx256M -server</argLine>
<systemProperties>
<property>
@@ -164,7 +167,7 @@
<configuration>
<instrumentation>
<excludes>
- <exclude>com/healthmarketscience/jackcess/scsu/**</exclude>
+ <exclude>com/healthmarketscience/jackcess/impl/scsu/**</exclude>
</excludes>
</instrumentation>
</configuration>
@@ -231,7 +234,7 @@
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
- <version>4.0</version>
+ <version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
@@ -254,11 +257,11 @@
<minmemory>128m</minmemory>
<maxmemory>512</maxmemory>
<links>
- <list>http://download.oracle.com/javase/1.5.0/docs/api</list>
- <list>http://download.oracle.com/javaee/5/api</list>
+ <list>http://docs.oracle.com/javase/1.5.0/docs/api/</list>
+ <list>http://docs.oracle.com/javaee/5/api/</list>
</links>
<source>1.5</source>
- <excludePackageNames>com.healthmarketscience.jackcess.scsu</excludePackageNames>
+ <excludePackageNames>com.healthmarketscience.jackcess.impl.scsu</excludePackageNames>
<show>public</show>
<stylesheetfile>${basedir}/src/site/javadoc/stylesheet.css</stylesheetfile>
<tags>
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 @@
-<?xml version="1.0" encoding="ISO-8859-1"?>
-<project>
- <pomVersion>3</pomVersion>
- <groupId>jackcess</groupId>
- <artifactId>jackcess</artifactId>
- <name>Jackcess</name>
- <currentVersion>1.1.10</currentVersion>
- <organization>
- <name>Health Market Science, Inc.</name>
- <url>http://www.healthmarketscience.com</url>
- <logo>http://www.healthmarketscience.com/images/HMS_logo.gif</logo>
- </organization>
- <inceptionYear>2005</inceptionYear>
- <package>com.healthmarketscience.jackcess</package>
- <description>A pure Java library for reading from and writing to MS Access databases.</description>
- <url>http://jackcess.sf.net</url>
- <issueTrackingUrl>http://sf.net/tracker/?group_id=134943&amp;atid=731445</issueTrackingUrl>
- <siteAddress>jackcess.sf.net</siteAddress>
- <siteDirectory>/home/groups/j/ja/jackcess/htdocs</siteDirectory>
- <repository>
- <connection>scm:cvs:pserver:anonymous@jackcess.cvs.sf.net:/cvsroot/jackcess:jackcess</connection>
- <url>http://jackcess.cvs.sourceforge.net/jackcess/jackcess/</url>
- </repository>
- <mailingLists>
- <mailingList>
- <name>jackcess-users</name>
- <subscribe>http://lists.sf.net/lists/listinfo/jackcess-users</subscribe>
- <unsubscribe>http://lists.sf.net/lists/listinfo/jackcess-users</unsubscribe>
- <archive>http://sf.net/mailarchive/forum.php?forum=jackcess-users</archive>
- </mailingList>
- </mailingLists>
- <developers>
- <developer>
- <name>Tim McCune</name>
- <id>javajedi</id>
- <email>javajedi@users.sf.net</email>
- <organization>Health Market Science, Inc.</organization>
- <timezone>-5</timezone>
- </developer>
- <developer>
- <name>James Ahlborn</name>
- <id>jahlborn</id>
- <email>jahlborn@users.sf.net</email>
- <organization>Health Market Science, Inc.</organization>
- <timezone>-5</timezone>
- </developer>
- <developer>
- <name>Rob Di Marco</name>
- <organization>Health Market Science, Inc.</organization>
- <timezone>-5</timezone>
- </developer>
- <developer>
- <name>Dan Rollo</name>
- <id>bhamail</id>
- <email>bhamail@users.sf.net</email>
- <organization>Composite Software, Inc.</organization>
- <timezone>-5</timezone>
- </developer>
- </developers>
- <licenses>
- <license>
- <name>GNU Lesser General Public License</name>
- <url>http://www.gnu.org/copyleft/lesser.txt</url>
- <distribution>manual</distribution>
- </license>
- </licenses>
- <build>
- <sourceDirectory>src/java</sourceDirectory>
- <unitTestSourceDirectory>test/src/java</unitTestSourceDirectory>
- <resources>
- <resource>
- <directory>src/resources</directory>
- </resource>
- </resources>
- </build>
- <dependencies>
- <dependency>
- <groupId>commons-collections</groupId>
- <artifactId>commons-collections</artifactId>
- <version>3.0</version>
- </dependency>
- <dependency>
- <groupId>commons-lang</groupId>
- <artifactId>commons-lang</artifactId>
- <version>2.0</version>
- </dependency>
- <dependency>
- <groupId>commons-logging</groupId>
- <artifactId>commons-logging</artifactId>
- <version>1.0.3</version>
- </dependency>
- <dependency>
- <groupId>log4j</groupId>
- <artifactId>log4j</artifactId>
- <version>1.2.7</version>
- </dependency>
- <dependency>
- <groupId>maven-plugins</groupId>
- <artifactId>maven-sourceforge-plugin</artifactId>
- <version>1.3</version>
- <type>plugin</type>
- </dependency>
- <dependency>
- <groupId>statcvs</groupId>
- <artifactId>maven-statcvs-plugin</artifactId>
- <version>2.5</version>
- <type>plugin</type>
- </dependency>
- </dependencies>
- <reports>
- <report>maven-faq-plugin</report>
- <report>maven-changes-plugin</report>
- <report>maven-javadoc-plugin</report>
- <report>maven-jxr-plugin</report>
- <report>maven-jdepend-plugin</report>
- <report>maven-statcvs-plugin</report>
- </reports>
-</project>
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<Column> {
-
- 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
@@ -86,2472 +49,108 @@ public class Column implements Comparable<Column> {
public static final Object KEEP_VALUE = "<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<? extends ComplexValue> _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<? extends ComplexValue> 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<? extends ComplexValue> 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<String,Object> rowMap, Object value) {
- rowMap.put(_name, value);
- return value;
- }
-
- public Object getRowValue(Object[] rowArray) {
- return rowArray[_columnIndex];
- }
-
- public Object getRowValue(Map<String,?> 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<Column> 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<Column> 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.
- * <code>null</code> 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.
- * <code>null</code> 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<Column> 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.
- * <p>
- * <i>Warning, calling this externally will result in this value being
- * "lost" for the table.</i>
- */
- 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<String,Object> 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<String,?> 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.
* <p>
- * 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.
* <p>
* Is not thread-safe.
*
* @author James Ahlborn
*/
-public abstract class Cursor implements Iterable<Map<String, Object>>
-{
- 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<Row>
+{
- /**
- * 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.
- * <p>
- * 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.
- * <p>
- * 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.
- * <p>
- * 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.
- * <p>
- * Warning, this method <i>always</i> 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<String,Object> findRow(Table table,
- Map<String,?> 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.
- * <p>
- * 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.
- * <p>
- * Warning, this method <i>always</i> 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<String,Object> findRow(Table table, Index index,
- Map<String,?> 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.
- * <p>
- * 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<Map<String, Object>>
* 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<Map<String, Object>>
* 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 <code>afterLast</code>
- * 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 <code>getPreviousRow</code>.
- * @throws IllegalStateException if an IOException is thrown by one of the
- * operations, the actual exception will be contained within
- */
- public Iterable<Map<String, Object>> reverseIterable() {
- return reverseIterable(null);
- }
-
- /**
- * Returns an Iterable whose iterator() method calls <code>afterLast</code>
- * 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 <code>getPreviousRow</code>.
- * @throws IllegalStateException if an IOException is thrown by one of the
- * operations, the actual exception will be contained within
- */
- public Iterable<Map<String, Object>> reverseIterable(
- final Collection<String> columnNames)
- {
- return new Iterable<Map<String, Object>>() {
- public Iterator<Map<String, Object>> iterator() {
- return new RowIterator(columnNames, MOVE_REVERSE);
- }
- };
- }
-
- /**
- * Calls <code>beforeFirst</code> 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
- * <code>getNextRow</code>.
- * @throws IllegalStateException if an IOException is thrown by one of the
- * operations, the actual exception will be contained within
- */
- public Iterator<Map<String, Object>> 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}.
+ * <p/>
+ * 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<Map<String, Object>> iterable(
- final Collection<String> columnNames)
- {
- return new Iterable<Map<String, Object>>() {
- public Iterator<Map<String, Object>> iterator() {
- return Cursor.this.iterator(columnNames);
- }
- };
- }
-
- /**
- * Calls <code>beforeFirst</code> 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 <code>getNextRow</code>.
- * @throws IllegalStateException if an IOException is thrown by one of the
- * operations, the actual exception will be contained within
- */
- public Iterator<Map<String, Object>> iterator(Collection<String> columnNames)
- {
- return new RowIterator(columnNames, MOVE_FORWARD);
- }
+ public Iterator<Row> 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<Map<String, Object>> columnMatchIterable(
- Column columnPattern, Object valuePattern)
- {
- return columnMatchIterable(null, columnPattern, valuePattern);
- }
-
- /**
- * Calls <code>beforeFirst</code> 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 <code>getNextRow</code>. 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<Map<String, Object>> 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<Map<String, Object>> columnMatchIterable(
- final Collection<String> columnNames,
- final Column columnPattern, final Object valuePattern)
- {
- return new Iterable<Map<String, Object>>() {
- public Iterator<Map<String, Object>> iterator() {
- return Cursor.this.columnMatchIterator(
- columnNames, columnPattern, valuePattern);
- }
- };
- }
-
- /**
- * Calls <code>beforeFirst</code> 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
- * <code>getNextRow</code>. 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<Map<String, Object>> columnMatchIterator(
- Collection<String> 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<Map<String, Object>> rowMatchIterable(
- Map<String,?> rowPattern)
- {
- return rowMatchIterable(null, rowPattern);
- }
-
- /**
- * Calls <code>beforeFirst</code> 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 <code>getNextRow</code>. 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<Map<String, Object>> rowMatchIterator(
- Map<String,?> 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<Map<String, Object>> rowMatchIterable(
- final Collection<String> columnNames,
- final Map<String,?> rowPattern)
- {
- return new Iterable<Map<String, Object>>() {
- public Iterator<Map<String, Object>> iterator() {
- return Cursor.this.rowMatchIterator(
- columnNames, rowPattern);
- }
- };
- }
-
- /**
- * Calls <code>beforeFirst</code> 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
- * <code>getNextRow</code>. 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.
+ * <p/>
+ * 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<Map<String, Object>> rowMatchIterator(
- Collection<String> columnNames, Map<String,?> 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 extends Map<String,Object>> 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<String, Object> 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<Map<String, Object>>
* @return The next row in this table (Column name -> Column value), or
* {@code null} if no next row is found
*/
- public Map<String, Object> getNextRow(Collection<String> columnNames)
- throws IOException
- {
- return getAnotherRow(columnNames, MOVE_FORWARD);
- }
+ public Row getNextRow(Collection<String> 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<String, Object> 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<Map<String, Object>>
* @return The previous row in this table (Column name -> Column value), or
* {@code null} if no previous row is found
*/
- public Map<String, Object> getPreviousRow(Collection<String> 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<String, Object> getAnotherRow(Collection<String> columnNames,
- boolean moveForward)
- throws IOException
- {
- if(moveToAnotherRow(moveForward)) {
- return getCurrentRow(columnNames);
- }
- return null;
- }
+ public Row getPreviousRow(Collection<String> 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<Map<String, Object>>
* {@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<Map<String, Object>>
* {@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<String,?> 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<Map<String, Object>>
* @return {@code true} if a valid row was found with the given values,
* {@code false} if no row was found
*/
- public boolean findFirstRow(Map<String,?> 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<String,?> 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<Map<String, Object>>
* @return {@code true} if a valid row was found with the given values,
* {@code false} if no row was found
*/
- public boolean findNextRow(Map<String,?> 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<String,?> rowPattern) throws IOException;
/**
* Returns {@code true} if the current row matches the given pattern.
@@ -994,146 +290,43 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
* 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<String,?> rowPattern)
- throws IOException
- {
- Map<String,Object> row = getCurrentRow(rowPattern.keySet());
-
- if(rowPattern.size() != row.size()) {
- return false;
- }
-
- for(Map.Entry<String,Object> 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.
- * <p>
- * 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.
- * <p>
- * 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<String,?> rowPattern)
- throws IOException
- {
- while(moveToNextRow()) {
- if(currentRowMatches(rowPattern)) {
- return true;
- }
- }
- return false;
- }
+ public boolean currentRowMatches(Map<String,?> 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<String, Object> 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<String, Object> getCurrentRow(Collection<String> columnNames)
- throws IOException
- {
- return _table.getRow(_rowState, _curPos.getRowId(), columnNames);
- }
+ public Row getCurrentRow(Collection<String> 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<Map<String, Object>>
* 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<Map<String, Object>>
- {
- protected final Collection<String> _columnNames;
- protected Boolean _hasNext;
- protected boolean _validRow;
-
- protected BaseIterator(Collection<String> 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<String, Object> next() {
- if(!hasNext()) {
- throw new NoSuchElementException();
- }
- try {
- Map<String, Object> 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<String> 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<String> 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<String,?> _rowPattern;
-
- private RowMatchIterator(Collection<String> columnNames,
- Map<String,?> 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<String> searchColumns) {
boolean found = false;
- for(Index index : _table.getIndexes()) {
+ for(IndexImpl index : _table.getIndexes()) {
- Collection<IndexData.ColumnDescriptor> indexColumns = index.getColumns();
+ Collection<? extends Index.Column> indexColumns = index.getColumns();
if(indexColumns.size() != searchColumns.size()) {
continue;
}
Iterator<String> sIter = searchColumns.iterator();
- Iterator<IndexData.ColumnDescriptor> iIter = indexColumns.iterator();
+ Iterator<? extends Index.Column> iIter = indexColumns.iterator();
boolean matches = true;
while(sIter.hasNext()) {
String sColName = sIter.next();
@@ -177,7 +184,7 @@ public class CursorBuilder {
* <p>
* 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 {
* <p>
* 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 {
* <p>
* 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * Warning, this method <i>always</i> 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<String,?> 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.
+ * <p>
+ * 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.
+ * <p>
+ * Warning, this method <i>always</i> 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<String,?> 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.
+ * <p>
+ * 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
+ * <b>always</b> be closed ({@link #close}) to avoid corruption.
* <p>
- * There is optional support for large indexes (enabled by default). This
- * optional support can be disabled via a few different means:
- * <ul>
- * <li>Setting the system property {@value #USE_BIG_INDEX_PROPERTY} to
- * {@code "false"} will disable "large" index support across the jvm</li>
- * <li>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</li>
- * <li>Calling {@link #getTable(String,boolean)} can selectively
- * enable/disable "large" index support on a per-table basis (overriding
- * any Database or system property setting)</li>
- * </ul>
+ * Note, Database instances (and all the related objects) are <i>not</i>
+ * 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<Table>, Closeable, Flushable
+public interface Database extends Iterable<Table>, 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,1166 +107,52 @@ 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(); }
- }
-
- /** 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<String> SYSTEM_CATALOG_COLUMNS =
- new HashSet<String>(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<String> SYSTEM_CATALOG_TABLE_NAME_COLUMNS =
- new HashSet<String>(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<String> SYSTEM_CATALOG_PROPS_COLUMNS =
- new HashSet<String>(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
- */
- private static final Set<String> RESERVED_WORDS = new HashSet<String>();
- 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"
- ));
- }
-
- /** 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<String, TableInfo> _tableLookup =
- new LinkedHashMap<String, TableInfo>() {
- private static final long serialVersionUID = 0L;
- @Override
- protected boolean removeEldestEntry(Map.Entry<String, TableInfo> e) {
- return(size() > MAX_CACHED_LOOKUP_TABLES);
- }
- };
- /** set of table names as stored in the mdb file, created on demand */
- private Set<String> _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<byte[]> _newTableSIDs = new ArrayList<byte[]>();
- /** "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<String,Database> _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, the
- * file will be opened read-only. Auto-syncing is enabled for the returned
- * Database.
- * <p>
- * 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_
- */
- public static Database open(File mdbFile) throws IOException {
- return open(mdbFile, false);
- }
-
- /**
- * 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.
- * <p>
- * 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
- * @usage _general_method_
- */
- public static Database open(File mdbFile, boolean readOnly)
- throws IOException
- {
- return open(mdbFile, readOnly, DEFAULT_AUTO_SYNC);
- }
-
- /**
- * 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
- * @usage _general_method_
- */
- public static Database open(File mdbFile, boolean readOnly, boolean autoSync)
- throws IOException
- {
- return open(mdbFile, readOnly, autoSync, null, null);
- }
-
- /**
- * 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
- * @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);
- }
-
- /**
- * 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
- * @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) {
-
- // 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
- * <p>
- * Equivalent to:
- * {@code create(FileFormat.V2000, mdbFile, DEFAULT_AUTO_SYNC);}
- *
- * @param mdbFile Location to write the new database to. <b>If this file
- * already exists, it will be overwritten.</b>
- *
- * @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
- * <p>
- * Equivalent to:
- * {@code create(fileFormat, mdbFile, DEFAULT_AUTO_SYNC);}
- *
- * @param fileFormat version of new database.
- * @param mdbFile Location to write the new database to. <b>If this file
- * already exists, it will be overwritten.</b>
- *
- * @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
- * <p>
- * Equivalent to:
- * {@code create(FileFormat.V2000, mdbFile, DEFAULT_AUTO_SYNC);}
- *
- * @param mdbFile Location to write the new database to. <b>If this file
- * already exists, it will be overwritten.</b>
- * @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_
- */
- public static Database create(File mdbFile, boolean autoSync)
- throws IOException
- {
- return create(FileFormat.V2000, mdbFile, autoSync);
- }
-
- /**
- * Create a new Database for the given fileFormat
- * @param fileFormat version of new database.
- * @param mdbFile Location to write the new database to. <b>If this file
- * already exists, it will be overwritten.</b>
- * @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_
- */
- public static Database create(FileFormat fileFormat, File mdbFile,
- boolean autoSync)
- throws IOException
- {
- return create(fileFormat, mdbFile, autoSync, null, null);
- }
-
- /**
- * Create a new Database for the given fileFormat
- * @param fileFormat version of new database.
- * @param mdbFile Location to write the new database to. <b>If this file
- * already exists, it will be overwritten.</b>
- * @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
- * @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);
- }
-
- /**
- * Create a new Database for the given fileFormat
- * @param fileFormat version of new database.
- * @param mdbFile Location to write the new database to. <b>If this file
- * already exists, it will be overwritten.</b>
- * @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_
- */
- 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 String toString() {
+ return name() + " [" + DatabaseImpl.getFileFormatDetails(this).getFormat() + "]";
}
}
/**
- * 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 <tt>"r"</tt> but the given file object does
- * not denote an existing regular file, or if the mode begins
- * with <tt>"rw"</tt> 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 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();
- }
-
- /**
* Returns the File underlying this Database
*/
- 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 Table getSystemCatalog() {
- return _systemCatalog;
- }
-
- /**
- * @return The system Access Control Entries table (loaded on demand)
- * @usage _advanced_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;
- }
-
- /**
- * @return the complex column system table (loaded on demand)
- * @usage _advanced_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;
- }
-
- /**
- * Whether or not big index support is enabled for tables.
- * @usage _advanced_method_
- */
- public boolean doUseBigIndex() {
- return (_useBigIndex != null ? _useBigIndex : true);
- }
-
- /**
- * Set whether or not big index support is enabled for tables.
- * @usage _intermediate_method_
- */
- public void setUseBigIndex(boolean useBigIndex) {
- _useBigIndex = useBigIndex;
- }
-
- /**
- * Gets the currently configured ErrorHandler (always non-{@code null}).
- * This will be used to handle all errors unless overridden at the Table or
- * Cursor level.
- * @usage _intermediate_method_
- */
- public ErrorHandler getErrorHandler() {
- return((_dbErrorHandler != null) ? _dbErrorHandler :
- DEFAULT_ERROR_HANDLER);
- }
-
- /**
- * Sets a new ErrorHandler. If {@code null}, resets to the
- * {@link #DEFAULT_ERROR_HANDLER}.
- * @usage _intermediate_method_
- */
- public void setErrorHandler(ErrorHandler newErrorHandler) {
- _dbErrorHandler = 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);
- }
-
- /**
- * Sets a new LinkResolver. If {@code null}, resets to the
- * {@link #DEFAULT_LINK_RESOLVER}.
- * @usage _intermediate_method_
- */
- public void setLinkResolver(LinkResolver newLinkResolver) {
- _linkResolver = newLinkResolver;
- }
-
- /**
- * Returns an unmodifiable view of the currently loaded linked databases,
- * mapped from the linked database file name to the linked database. This
- * information may be useful for implementing a LinkResolver.
- * @usage _intermediate_method_
- */
- public Map<String,Database> getLinkedDatabases() {
- return ((_linkedDbs == null) ? Collections.<String,Database>emptyMap() :
- Collections.unmodifiableMap(_linkedDbs));
- }
-
- /**
- * Gets currently configured TimeZone (always non-{@code null}).
- * @usage _intermediate_method_
- */
- public TimeZone getTimeZone() {
- return _timeZone;
- }
-
- /**
- * Sets a new TimeZone. If {@code null}, resets to the value returned by
- * {@link #getDefaultTimeZone}.
- * @usage _intermediate_method_
- */
- public void setTimeZone(TimeZone newTimeZone) {
- if(newTimeZone == null) {
- newTimeZone = getDefaultTimeZone();
- }
- _timeZone = newTimeZone;
- // clear cached calendar when timezone is changed
- _calendar = null;
- }
-
- /**
- * Gets currently configured Charset (always non-{@code null}).
- * @usage _intermediate_method_
- */
- public Charset getCharset()
- {
- return _charset;
- }
-
- /**
- * Sets a new Charset. If {@code null}, resets to the value returned by
- * {@link #getDefaultCharset}.
- * @usage _intermediate_method_
- */
- public void setCharset(Charset newCharset) {
- if(newCharset == null) {
- newCharset = getDefaultCharset(getFormat());
- }
- _charset = newCharset;
- }
-
- /**
- * Gets currently configured {@link Table.ColumnOrder} (always non-{@code
- * null}).
- * @usage _intermediate_method_
- */
- public Table.ColumnOrder getColumnOrder() {
- return _columnOrder;
- }
-
- /**
- * Sets a new Table.ColumnOrder. If {@code null}, resets to the value
- * returned by {@link #getDefaultColumnOrder}.
- * @usage _intermediate_method_
- */
- public void setColumnOrder(Table.ColumnOrder newColumnOrder) {
- if(newColumnOrder == null) {
- newColumnOrder = getDefaultColumnOrder();
- }
- _columnOrder = newColumnOrder;
- }
-
- /**
- * Gets currently foreign-key enforcement policy.
- * @usage _intermediate_method_
- */
- public boolean isEnforceForeignKeys() {
- return _enforceForeignKeys;
- }
-
- /**
- * Sets a new foreign-key enforcement policy. If {@code null}, resets to
- * the value returned by {@link #isEnforceForeignKeys}.
- * @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;
- }
-
- /**
- * Returns the FileFormat of this database (which may involve inspecting the
- * database itself).
- * @throws IllegalStateException if the file format cannot be determined
- * @usage _general_method_
- */
- public FileFormat getFileFormat() throws IOException {
-
- if(_fileFormat == null) {
-
- Map<String,Database.FileFormat> 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());
+ public File getFile();
- 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)
+ * @return The names of all of the user tables
* @usage _general_method_
*/
- public Set<String> getTableNames() throws IOException {
- if(_tableNames == null) {
- Set<String> tableNames =
- new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
- _tableFinder.getTableNames(tableNames, false);
- _tableNames = tableNames;
- }
- return _tableNames;
- }
+ public Set<String> getTableNames() throws IOException;
/**
* @return The names of all of the system tables (String). Note, in order
@@ -1334,227 +161,38 @@ public class Database
* directly!</i>.
* @usage _intermediate_method_
*/
- public Set<String> getSystemTableNames() throws IOException {
- Set<String> sysTableNames =
- new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
- _tableFinder.getTableNames(sysTableNames, true);
- return sysTableNames;
- }
+ public Set<String> getSystemTableNames() throws IOException;
/**
* @return an unmodifiable Iterator of the user Tables in this Database.
- * @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
* @throws ConcurrentModificationException if a table is added to the
* database while an Iterator is in use.
* @usage _general_method_
*/
- public Iterator<Table> 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);
- }
+ public Iterator<Table> iterator();
/**
- * @param tableDefPageNumber the page number of a table definition
+ * @param name Table name (case-insensitive)
* @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<String,Object> 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,Database>();
- }
-
- 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<Column> 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<Column> columns,
- List<IndexBuilder> 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);
- }
+ public Table getTable(String name) throws IOException;
/**
* Finds all the relationships in the database between the given tables.
* @usage _intermediate_method_
*/
public List<Relationship> 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;
- }
+ throws IOException;
- return getRelationshipsImpl(table1, table2, true);
- }
-
/**
* Finds all the relationships in the database for the given table.
* @usage _intermediate_method_
*/
- public List<Relationship> 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);
- }
-
+ public List<Relationship> getRelationships(Table table) throws IOException;
+
/**
* Finds all the relationships in the database in <i>non-system</i> tables.
* </p>
@@ -1562,12 +200,8 @@ public class Database
* database which could cause memory issues.
* @usage _intermediate_method_
*/
- public List<Relationship> getRelationships()
- throws IOException
- {
- return getRelationshipsImpl(null, null, false);
- }
-
+ public List<Relationship> getRelationships() throws IOException;
+
/**
* Finds <i>all</i> the relationships in the database, <i>including system
* tables</i>.
@@ -1577,96 +211,13 @@ public class Database
* @usage _intermediate_method_
*/
public List<Relationship> getSystemRelationships()
- throws IOException
- {
- return getRelationshipsImpl(null, null, true);
- }
-
- private List<Relationship> 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<Relationship> relationships = new ArrayList<Relationship>();
-
- 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;
- }
+ throws IOException;
/**
* Finds all the queries in the database.
* @usage _intermediate_method_
*/
- public List<Query> 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<Map<String,Object>> queryInfo = new ArrayList<Map<String,Object>>();
- Map<Integer,List<Query.Row>> queryRowMap =
- new HashMap<Integer,List<Query.Row>>();
- for(Map<String,Object> 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<Query.Row>());
- }
- }
-
- // find all the query rows
- for(Map<String,Object> row : Cursor.createCursor(_queries)) {
- Query.Row queryRow = new Query.Row(row);
- List<Query.Row> 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<Query> queries = new ArrayList<Query>();
- for(Map<String,Object> 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<Query.Row> queryRows = queryRowMap.get(id);
- queries.add(Query.create(flags, name, queryRows, id));
- }
-
- return queries;
- }
+ public List<Query> getQueries() throws IOException;
/**
* Returns a reference to <i>any</i> available table in this access
@@ -1681,1134 +232,153 @@ public class Database
* @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());
- }
+ public Table getSystemTable(String tableName) throws IOException;
/**
* @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();
- }
+ public PropertyMap getDatabaseProperties() throws IOException;
/**
* @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();
- }
+ public PropertyMap getSummaryProperties() throws IOException;
/**
* @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<String,Object> 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<String,Object> 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 PropertyMap getUserDefinedProperties() throws IOException;
/**
* @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<Relationship> relationships, boolean includeSystemTables)
- throws IOException
- {
- String fromTableName = ((fromTable != null) ? fromTable.getName() : null);
- String toTableName = ((toTable != null) ? toTable.getName() : null);
-
- for(Map<String,Object> 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<Column> 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<Object[]> aceRows = new ArrayList<Object[]>(_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<String, Object> 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);
- }
+ public String getDatabasePassword() throws IOException;
/**
- * 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)
+ * 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 String importReader(String name, BufferedReader in, String delim,
- ImportFilter filter)
- throws IOException
- {
- return ImportUtil.importReader(in, this, name, delim, filter);
- }
+ public void createLinkedTable(String name, String linkedDbName,
+ String linkedTableName)
+ throws IOException;
/**
* 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;
- }
+ public void flush() throws IOException;
/**
- * @return {@code true} if the given string is a reserved word,
- * {@code false} otherwise
+ * Close the database file (and any linked databases). A Database
+ * <b>must</b> 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 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);
- }
+ public void close() throws IOException;
/**
- * @return {@code true} if the given flags indicate that an object is some
- * sort of system object, {@code false} otherwise.
+ * Gets the currently configured ErrorHandler (always non-{@code null}).
+ * This will be used to handle all errors unless overridden at the Table or
+ * Cursor level.
+ * @usage _intermediate_method_
*/
- private static boolean isSystemObject(int flags) {
- return ((flags & SYSTEM_OBJECT_FLAGS) != 0);
- }
+ public ErrorHandler getErrorHandler();
/**
- * 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_
+ * Sets a new ErrorHandler. If {@code null}, resets to the
+ * {@link ErrorHandler#DEFAULT}.
+ * @usage _intermediate_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);
- }
- }
+ public void setErrorHandler(ErrorHandler newErrorHandler);
- // 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_
+ * Gets the currently configured LinkResolver (always non-{@code null}).
+ * This will be used to handle all linked database loading.
+ * @usage _intermediate_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);
- }
- }
+ public LinkResolver getLinkResolver();
- // 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_
+ * Sets a new LinkResolver. If {@code null}, resets to the
+ * {@link LinkResolver#DEFAULT}.
+ * @usage _intermediate_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);
- }
- }
+ public void setLinkResolver(LinkResolver newLinkResolver);
- // 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.
+ * Returns an unmodifiable view of the currently loaded linked databases,
+ * mapped from the linked database file name to the linked database. This
+ * information may be useful for implementing a LinkResolver.
+ * @usage _intermediate_method_
*/
- 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();
- }
- }
- }
+ public Map<String,Database> getLinkedDatabases();
/**
- * Returns the password mask retrieved from the given header page and
- * format, or {@code null} if this format does not use a password mask.
+ * Gets currently configured TimeZone (always non-{@code null}).
+ * @usage _intermediate_method_
*/
- 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));
- }
+ public TimeZone getTimeZone();
/**
- * Utility class for storing table page number and actual name.
+ * Sets a new TimeZone. If {@code null}, resets to the default value.
+ * @usage _intermediate_method_
*/
- 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;
- }
- }
+ public void setTimeZone(TimeZone newTimeZone);
/**
- * Utility class for storing linked table info
+ * Gets currently configured Charset (always non-{@code null}).
+ * @usage _intermediate_method_
*/
- 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;
- }
- }
+ public Charset getCharset();
/**
- * Table iterator for this database, unmodifiable.
+ * Sets a new Charset. If {@code null}, resets to the default value.
+ * @usage _intermediate_method_
*/
- private class TableIterator implements Iterator<Table>
- {
- private Iterator<String> _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);
- }
- }
- }
+ public void setCharset(Charset newCharset);
/**
- * Utility class for handling table lookups.
+ * Gets currently configured {@link Table.ColumnOrder} (always non-{@code
+ * null}).
+ * @usage _intermediate_method_
*/
- 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<String,Object> getObjectRow(Integer parentId, String name,
- Collection<String> columns)
- throws IOException
- {
- Cursor cur = findRow(parentId, name);
- return ((cur != null) ? cur.getCurrentRow(columns) : null);
- }
-
- public Map<String,Object> getObjectRow(
- Integer objectId, Collection<String> columns)
- throws IOException
- {
- Cursor cur = findRow(objectId);
- return ((cur != null) ? cur.getCurrentRow(columns) : null);
- }
-
- public void getTableNames(Set<String> tableNames,
- boolean systemTables)
- throws IOException
- {
- for(Map<String,Object> 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;
- }
- }
+ public Table.ColumnOrder getColumnOrder();
/**
- * Normal table lookup handler, using catalog table index.
+ * Sets a new Table.ColumnOrder. If {@code null}, resets to the default value.
+ * @usage _intermediate_method_
*/
- private final class DefaultTableFinder extends TableFinder
- {
- private final IndexCursor _systemCatalogCursor;
- private IndexCursor _systemCatalogIdCursor;
+ public void setColumnOrder(Table.ColumnOrder newColumnOrder);
- 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<String,Object> 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.
+ * Gets currently foreign-key enforcement policy.
+ * @usage _intermediate_method_
*/
- 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<String,Object> rowPat = new HashMap<String,Object>();
- 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<String,Object> 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;
- }
- }
+ public boolean isEnforceForeignKeys();
/**
- * WeakReference for a Table which holds the table pageNumber (for later
- * cache purging).
+ * Sets a new foreign-key enforcement policy. If {@code null}, resets to
+ * the default value.
+ * @usage _intermediate_method_
*/
- private static final class WeakTableReference extends WeakReference<Table>
- {
- private final Integer _pageNumber;
-
- private WeakTableReference(Integer pageNumber, Table table,
- ReferenceQueue<Table> queue) {
- super(table, queue);
- _pageNumber = pageNumber;
- }
-
- public Integer getPageNumber() {
- return _pageNumber;
- }
- }
+ public void setEnforceForeignKeys(Boolean newEnforceForeignKeys);
/**
- * Cache of currently in-use tables, allows re-use of existing tables.
+ * Returns the FileFormat of this database (which may involve inspecting the
+ * database itself).
+ * @throws IllegalStateException if the file format cannot be determined
+ * @usage _general_method_
*/
- private static final class TableCache
- {
- private final Map<Integer,WeakTableReference> _tables =
- new HashMap<Integer,WeakTableReference>();
- private final ReferenceQueue<Table> _queue = new ReferenceQueue<Table>();
-
- 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);
+ public FileFormat getFileFormat() throws IOException;
- 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. <b>If this file
+ * already exists, it will be overwritten.</b>
+ *
+ * @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<Index> {
-
- 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<IndexData> 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<? extends Index.Column> 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<Index> {
* case <i>will violate</i> the unique constraint</li>
* </ul>
*/
- public boolean isUnique() {
- return getIndexData().isUnique();
- }
-
- /**
- * Returns the Columns for this index (unmodifiable)
- */
- public List<IndexData.ColumnDescriptor> 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.
- * <p>
- * Forces index initialization.
- */
- public void update() throws IOException {
- getIndexData().update();
- }
+ public com.healthmarketscience.jackcess.Column getColumn();
- /**
- * Adds a row to this index
- * <p>
- * 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
- * <p>
- * 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.
- * <p>
- * 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.
- * <p>
- * 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<String,?> 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<String> _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.
- * <p>
- * 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.
- * <p>
- * 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.
- * <p>
- * 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<Map<String,Object>> entryIterator(Object... entryValues)
- {
- return entryIterator((Collection<String>)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<Map<String,Object>> entryIterator(
- Collection<String> 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<Map<String,Object>> entryIterable(Object... entryValues)
- {
- return entryIterable((Collection<String>)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<Map<String,Object>> entryIterable(
- final Collection<String> columnNames, final Object... entryValues)
- {
- return new Iterable<Map<String, Object>>() {
- public Iterator<Map<String, Object>> 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<String,?> 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<String,?> 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<String,Object> tmpRowPattern = new LinkedHashMap<String,Object>();
- 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<String>();
- for(IndexData.ColumnDescriptor col : getIndex().getColumns()) {
- _indexEntryPattern.add(col.getName());
- }
- }
-
- // check the next row to see if it actually matches
- Map<String,Object> 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<String> 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<PropertyMap.Property>
+public interface PropertyMap extends Iterable<PropertyMap.Property>
{
public static final String ACCESS_VERSION_PROP = "AccessVersion";
public static final String TITLE_PROP = "Title";
@@ -47,124 +43,40 @@ public class PropertyMap implements Iterable<PropertyMap.Property>
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<String,Property> _props =
- new LinkedHashMap<String,Property>();
-
- 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<Property> iterator() {
- return _props.values().iterator();
- }
-
- @Override
- public String toString() {
- StringBuilder sb = new StringBuilder();
- sb.append(PropertyMaps.DEFAULT_NAME.equals(getName()) ?
- "<DEFAULT>" : getName())
- .append(" {");
- for(Iterator<Property> 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<Column> _toColumns;
- /** the columns in the "to" table in this relationship (aligned w/
- toColumns list) */
- private List<Column> _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<Column>(
- Collections.nCopies(numCols, (Column)null));
- _toTable = toTable;
- _toColumns = new ArrayList<Column>(
- 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<Column> getFromColumns() {
- return _fromColumns;
- }
+ public Table getFromTable();
- public Table getToTable() {
- return _toTable;
- }
+ public List<Column> getFromColumns();
- public List<Column> getToColumns() {
- return _toColumns;
- }
+ public Table getToTable();
- public int getFlags() {
- return _flags;
- }
+ public List<Column> 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
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<String,Object>
{
/**
- * 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<RowId>
+public interface RowId extends Comparable<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 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 <code>RowId</code> 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.<Entry>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<Entry> 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<Entry> _entries;
-
- private SimpleDataPage(int pageNumber) {
- this(pageNumber, false, null);
- }
-
- private SimpleDataPage(int pageNumber, boolean leaf, List<Entry> 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<Entry> getEntries() {
- return _entries;
- }
-
- @Override
- public void setEntries(List<Entry> 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.
* <p>
* Is not thread-safe.
- *
- * @author Tim McCune
+ *
+ * @author James Ahlborn
* @usage _general_class_
*/
-public class Table
- implements Iterable<Map<String, Object>>
+public interface Table extends Iterable<Row>
{
-
- 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<Column> VAR_LEN_COLUMN_COMPARATOR =
- new Comparator<Column>() {
- 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<Column> DISPLAY_ORDER_COMPARATOR =
- new Comparator<Column>() {
- 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<Column> _columns = new ArrayList<Column>();
- /** List of variable length columns in this table, ordered by offset */
- private final List<Column> _varColumns = new ArrayList<Column>();
- /** List of autonumber columns in this table, ordered by column number */
- private List<Column> _autoNumColumns;
- /** List of indexes on this table (multiple logical indexes may be backed by
- the same index data) */
- private final List<Index> _indexes = new ArrayList<Index>();
- /** List of index datas on this table (the actual backing data for an
- index) */
- private final List<IndexData> _indexDatas = new ArrayList<IndexData>();
- /** List of columns in this table which are in one or more indexes */
- private final Set<Column> _indexColumns = new LinkedHashSet<Column>();
- /** 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<Column> 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 <i>approximate</i> number of database pages owned by this
- * table and all related indexes (this number does <i>not</i> take into
- * account pages used for large OLE/MEMO fields).
- * <p>
- * To calculate the approximate number of bytes owned by a table:
- * <code>
- * int approxTableBytes = (table.getApproximateOwnedPageCount() *
- * table.getFormat().PAGE_SIZE);
- * </code>
- * @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<Column> getColumns() {
- return Collections.unmodifiableList(_columns);
- }
+ public List<? extends Column> 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<Column> 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<Index> getIndexes() {
- return Collections.unmodifiableList(_indexes);
- }
+ public List<? extends Index> 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<IndexData> 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.
- * <p>
- * 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<String, Object> 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<String, Object> getNextRow(Collection<String> columnNames)
- throws IOException
- {
- return getInternalCursor().getNextRow(columnNames);
- }
-
- /**
- * Reads a single column from the given row.
- * <p>
- * 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<String, Object> getRow(
- RowState rowState, RowId rowId, Collection<String> 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<String, Object> getRow(
- JetFormat format,
- RowState rowState,
- ByteBuffer rowBuffer,
- Collection<Column> columns,
- Collection<String> columnNames)
- throws IOException
- {
- Map<String, Object> rtn = new LinkedHashMap<String, Object>(
- 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<Column,byte[]> 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 <code>reset</code> 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
- * <code>getNextRow</code>.
- * @throws IllegalStateException if an IOException is thrown by one of the
- * operations, the actual exception will be contained within
- * @usage _general_method_
- */
- public Iterator<Map<String, Object>> iterator()
- {
- return iterator(null);
- }
-
- /**
- * Calls <code>reset</code> 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 <code>getNextRow</code>.
- * @throws IllegalStateException if an IOException is thrown by one of the
- * operations, the actual exception will be contained within
- * @usage _general_method_
- */
- public Iterator<Map<String, Object>> iterator(Collection<String> 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<Column> 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<Column> 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<String,?> rowMap) {
- return asRow(rowMap, null);
- }
-
+ public Object[] asRow(Map<String,?> 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<String,?> rowMap) {
- return asRow(rowMap, Column.KEEP_VALUE);
- }
+ public Object[] asUpdateRow(Map<String,?> rowMap);
/**
- * Converts a map of columnName -> columnValue to an array of row values.
+ * @usage _general_method_
*/
- private Object[] asRow(Map<String,?> 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.
* <p>
* 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}.
+ * <p/>
+ * 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 extends Map<String,Object>> 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 <code>addRow</code> multiple times.
+ * is much more efficient than calling {@link #addRow} multiple times.
* <p>
* 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<? extends Object[]> 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<? extends Object[]> inRows,
- TempBufferHolder writeRowBufferH)
- throws IOException
- {
- if(inRows.isEmpty()) {
- return;
- }
-
- // copy the input rows to a modifiable list so we can update the elements
- List<Object[]> rows = new ArrayList<Object[]>(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<? extends Object[]> addRows(List<? extends Object[]> rows)
+ throws IOException;
/**
- * Updates the current row to the new values.
- * <p>
- * 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}.
+ * <p/>
+ * 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.
- * <p>
- * 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<Column,byte[]> keepRawVarValues =
- (!_varColumns.isEmpty() ? new HashMap<Column,byte[]>() : 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 <M extends Map<String,Object>> List<M> addRowsFromMaps(List<M> 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.<Column,byte[]>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<Column,byte[]> 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}.
+ * <p/>
+ * 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<Row> 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<Column> iter = _columns.iterator(); iter.hasNext(); ) {
- Column col = iter.next();
- rtn.append(col.getName());
- if (iter.hasNext()) {
- rtn.append("\t");
- }
- }
- rtn.append("\n");
- Map<String, Object> row;
- int rowCount = 0;
- while ((rowCount++ < limit) && (row = getNextRow()) != null) {
- for(Iterator<Object> 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<Column> getAutoNumberColumns(Collection<Column> columns) {
- List<Column> autoCols = new ArrayList<Column>(1);
- for(Column c : columns) {
- if(c.isAutoNumber()) {
- autoCols.add(c);
- }
- }
- return (!autoCols.isEmpty() ? autoCols : Collections.<Column>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<String> VALUES =
+ new HashSet<String>(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<Column> _columns = new ArrayList<Column>();
+ private List<ColumnBuilder> _columns = new ArrayList<ColumnBuilder>();
/** indexes for the new table */
private List<IndexBuilder> _indexes = new ArrayList<IndexBuilder>();
/** 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<? extends ColumnBuilder> 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<Attachment>
+public interface AttachmentColumnInfo extends ComplexColumnInfo<Attachment>
{
- /** some file formats which may not be worth re-compressing */
- private static final Set<String> COMPRESSED_FORMATS = new HashSet<String>(
- 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<String,Object> 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<Column> 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<V extends ComplexValue>
+public interface ComplexColumnInfo<V extends ComplexValue>
{
- 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<Column> _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<Column> typeCols = new ArrayList<Column>();
- List<Column> otherCols = new ArrayList<Column>();
- 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<? extends ComplexValue> 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<String,Object> 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<Column> getTypeColumns() {
- return _typeCols;
- }
-
- public int countValues(int complexValueFk) throws IOException {
- return getRawValues(complexValueFk,
- Collections.singleton(_complexValFkCol.getName()))
- .size();
- }
-
- public List<Map<String,Object>> getRawValues(int complexValueFk)
- throws IOException
- {
- return getRawValues(complexValueFk, null);
- }
-
- private Iterator<Map<String,Object>> getComplexValFkIter(
- int complexValueFk, Collection<String> columnNames)
- throws IOException
- {
- if(_complexValIdCursor == null) {
- _complexValIdCursor = new CursorBuilder(_flatTable)
- .setIndexByColumns(_complexValFkCol)
- .toIndexCursor();
- }
-
- return _complexValIdCursor.entryIterator(columnNames, complexValueFk);
- }
-
- public List<Map<String,Object>> getRawValues(int complexValueFk,
- Collection<String> columnNames)
- throws IOException
- {
- Iterator<Map<String,Object>> entryIter =
- getComplexValFkIter(complexValueFk, columnNames);
- if(!entryIter.hasNext()) {
- return Collections.emptyList();
- }
+ public List<Row> getRawValues(int complexValueFk)
+ throws IOException;
- List<Map<String,Object>> values = new ArrayList<Map<String,Object>>();
- while(entryIter.hasNext()) {
- values.add(entryIter.next());
- }
-
- return values;
- }
+ public List<Row> getRawValues(int complexValueFk,
+ Collection<String> columnNames)
+ throws IOException;
public List<V> getValues(ComplexValueForeignKey complexValueFk)
- throws IOException
- {
- List<Map<String,Object>> rawValues = getRawValues(complexValueFk.get());
- if(rawValues.isEmpty()) {
- return Collections.emptyList();
- }
+ throws IOException;
- return toValues(complexValueFk, rawValues);
- }
-
- protected List<V> toValues(ComplexValueForeignKey complexValueFk,
- List<Map<String,Object>> rawValues)
- throws IOException
- {
- List<V> values = new ArrayList<V>();
- for(Map<String,Object> rawValue : rawValues) {
- values.add(toValue(complexValueFk, rawValue));
- }
+ public ComplexValue.Id addRawValue(Map<String,?> rawValue)
+ throws IOException;
- return values;
- }
+ public ComplexValue.Id addValue(V value) throws IOException;
- public int addRawValue(Map<String,Object> rawValue) throws IOException {
- Object[] row = _flatTable.asRow(rawValue);
- _flatTable.addRow(row);
- return (Integer)_pkCol.getRowValue(row);
- }
+ public void addValues(Collection<? extends V> 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<? extends V> values) throws IOException {
- for(V value : values) {
- addValue(value);
- }
- }
+ public ComplexValue.Id updateValue(V value) throws IOException;
- public int updateRawValue(Map<String,Object> 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<? extends V> values) throws IOException;
- public void updateValues(Collection<? extends V> values) throws IOException {
- for(V value : values) {
- updateValue(value);
- }
- }
+ public void deleteRawValue(Row rawValue) throws IOException;
- public void deleteRawValue(Map<String,Object> 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<? extends V> values) throws IOException {
- for(V value : values) {
- deleteValue(value);
- }
- }
+ public void deleteValues(Collection<? extends V> values) throws IOException;
- public void deleteAllValues(int complexValueFk) throws IOException {
- Iterator<Map<String,Object>> entryIter =
- getComplexValFkIter(complexValueFk, Collections.<String>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<Column> typeCols,
- List<Column> 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<String,Object> 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<? extends ComplexValue> _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<? extends ComplexValue> 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<Map<String,Object>> getRawValues()
- throws IOException
- {
- return getComplexInfo().getRawValues(get());
- }
-
- public List<? extends ComplexValue> getValues()
- throws IOException
- {
- if(_values == null) {
- _values = getComplexInfo().getValues(this);
- }
- return _values;
- }
-
- @SuppressWarnings("unchecked")
- public List<Version> getVersions()
- throws IOException
- {
- if(getComplexType() != ComplexDataType.VERSION_HISTORY) {
- throw new UnsupportedOperationException();
- }
- return (List<Version>)getValues();
- }
-
- @SuppressWarnings("unchecked")
- public List<Attachment> getAttachments()
- throws IOException
- {
- if(getComplexType() != ComplexDataType.ATTACHMENT) {
- throw new UnsupportedOperationException();
- }
- return (List<Attachment>)getValues();
- }
-
- @SuppressWarnings("unchecked")
- public List<SingleValue> getMultiValues()
- throws IOException
- {
- if(getComplexType() != ComplexDataType.MULTI_VALUE) {
- throw new UnsupportedOperationException();
- }
- return (List<SingleValue>)getValues();
- }
-
- @SuppressWarnings("unchecked")
- public List<UnsupportedValue> getUnsupportedValues()
- throws IOException
- {
- if(getComplexType() != ComplexDataType.UNSUPPORTED) {
- throw new UnsupportedOperationException();
- }
- return (List<UnsupportedValue>)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<String,?> 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<? extends ComplexValue> getValues() throws IOException;
+
+ public abstract List<Version> getVersions() throws IOException;
+
+ public abstract List<Attachment> getAttachments()
+ throws IOException;
+
+ public abstract List<SingleValue> getMultiValues()
+ throws IOException;
+
+ public abstract List<UnsupportedValue> 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<String,?> 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<SingleValue>
+public interface MultiValueColumnInfo extends ComplexColumnInfo<SingleValue>
{
- private static final Set<DataType> 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<String,Object> 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<Column> 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<UnsupportedValue>
+public interface UnsupportedColumnInfo extends ComplexColumnInfo<UnsupportedValue>
{
- public UnsupportedColumnInfo(Column column, int complexId, Table typeObjTable,
- Table flatTable)
- throws IOException
- {
- super(column, complexId, typeObjTable, flatTable);
- }
-
- public List<Column> getValueColumns() {
- return getTypeColumns();
- }
-
- @Override
- public ComplexDataType getType()
- {
- return ComplexDataType.UNSUPPORTED;
- }
-
- @Override
- protected UnsupportedValueImpl toValue(
- ComplexValueForeignKey complexValueFk,
- Map<String,Object> rawValue)
- {
- int id = (Integer)getPrimaryKeyColumn().getRowValue(rawValue);
-
- Map<String,Object> values = new LinkedHashMap<String,Object>();
- 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<String,Object> values = value.getValues();
- for(Column col : getValueColumns()) {
- col.setRowValue(row, col.getRowValue(values));
- }
-
- return row;
- }
-
- public static UnsupportedValue newValue(Map<String,?> values) {
- return newValue(INVALID_COMPLEX_VALUE_ID, values);
- }
-
- public static UnsupportedValue newValue(
- ComplexValueForeignKey complexValueFk, Map<String,?> values) {
- return new UnsupportedValueImpl(INVALID_ID, complexValueFk,
- new LinkedHashMap<String,Object>(values));
- }
-
- private static class UnsupportedValueImpl extends ComplexValueImpl
- implements UnsupportedValue
- {
- private Map<String,Object> _values;
-
- private UnsupportedValueImpl(int id, ComplexValueForeignKey complexValueFk,
- Map<String,Object> values)
- {
- super(id, complexValueFk);
- _values = values;
- }
-
- public Map<String,Object> 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<Version>
+public interface VersionHistoryColumnInfo extends ComplexColumnInfo<Version>
{
- 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<Version> toValues(ComplexValueForeignKey complexValueFk,
- List<Map<String,Object>> rawValues)
- throws IOException
- {
- List<Version> versions = super.toValues(complexValueFk, rawValues);
-
- // order versions newest to oldest
- Collections.sort(versions);
-
- return versions;
- }
-
- @Override
- protected VersionImpl toValue(ComplexValueForeignKey complexValueFk,
- Map<String,Object> 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 <value>(MEMO),
- // <modified>(SHORT_DATE_TIME)
- List<Column> 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
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
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
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<ColumnImpl> {
+
+ 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 = "<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<? extends ComplexValue> _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<? extends ComplexValue>)_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<? extends ComplexValue> 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<String,Object> rowMap, Object value) {
+ rowMap.put(_name, value);
+ return value;
+ }
+
+ public Object getRowValue(Object[] rowArray) {
+ return rowArray[_columnIndex];
+ }
+
+ public Object getRowValue(Map<String,?> 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<ColumnBuilder> 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<ColumnBuilder> 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.
+ * <code>null</code> 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.
+ * <code>null</code> 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<ColumnBuilder> 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.
+ * <p>
+ * <i>Warning, calling this externally will result in this value being
+ * "lost" for the table.</i>
+ */
+ 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<DataType> 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<? extends ComplexValue> 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<? extends Column> 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<? extends Column> 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 <value>(MEMO),
+ // <modified>(SHORT_DATE_TIME)
+ List<? extends Column> 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.
+ * <p>
+ * 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}.
+ * <p>
+ * 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<Row> iterator() {
+ return new RowIterator(null, true, MOVE_FORWARD);
+ }
+
+ public IterableBuilder newIterable() {
+ return new IterableBuilder(this);
+ }
+
+ public Iterator<Row> iterator(IterableBuilder iterBuilder) {
+
+ switch(iterBuilder.getType()) {
+ case SIMPLE:
+ return new RowIterator(iterBuilder.getColumnNames(),
+ iterBuilder.isReset(), iterBuilder.isForward());
+ case COLUMN_MATCH: {
+ @SuppressWarnings("unchecked")
+ Map.Entry<Column,Object> matchPattern = (Map.Entry<Column,Object>)
+ iterBuilder.getMatchPattern();
+ return new ColumnMatchIterator(
+ iterBuilder.getColumnNames(), (ColumnImpl)matchPattern.getKey(),
+ matchPattern.getValue(), iterBuilder.isReset(),
+ iterBuilder.isForward(), iterBuilder.getColumnMatcher());
+ }
+ case ROW_MATCH: {
+ @SuppressWarnings("unchecked")
+ Map<String,?> matchPattern = (Map<String,?>)
+ 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 extends Map<String,Object>> 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<String> columnNames)
+ throws IOException
+ {
+ return getAnotherRow(columnNames, MOVE_FORWARD);
+ }
+
+ public Row getPreviousRow() throws IOException {
+ return getPreviousRow(null);
+ }
+
+ public Row getPreviousRow(Collection<String> 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<String> 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<String,?> rowPattern) throws IOException
+ {
+ return findAnotherRow(rowPattern, true, MOVE_FORWARD, _columnMatcher);
+ }
+
+ public boolean findNextRow(Map<String,?> rowPattern)
+ throws IOException
+ {
+ return findAnotherRow(rowPattern, false, MOVE_FORWARD, _columnMatcher);
+ }
+
+ protected boolean findAnotherRow(Map<String,?> 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<String,?> rowPattern)
+ throws IOException
+ {
+ return currentRowMatchesImpl(rowPattern, _columnMatcher);
+ }
+
+ protected boolean currentRowMatchesImpl(Map<String,?> rowPattern,
+ ColumnMatcher columnMatcher)
+ throws IOException
+ {
+ Row row = getCurrentRow(rowPattern.keySet());
+
+ if(rowPattern.size() != row.size()) {
+ return false;
+ }
+
+ for(Map.Entry<String,Object> 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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<String,?> 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<String> 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<Row>
+ {
+ protected final Collection<String> _columnNames;
+ protected final boolean _moveForward;
+ protected final ColumnMatcher _colMatcher;
+ protected Boolean _hasNext;
+ protected boolean _validRow;
+
+ protected BaseIterator(Collection<String> 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<String> 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<String> 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<String,?> _rowPattern;
+
+ private RowMatchIterator(Collection<String> columnNames,
+ Map<String,?> 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<Database.FileFormat,FileFormatDetails> FILE_FORMAT_DETAILS =
+ new EnumMap<Database.FileFormat,FileFormatDetails>(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<String> SYSTEM_CATALOG_COLUMNS =
+ new HashSet<String>(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<String> SYSTEM_CATALOG_TABLE_NAME_COLUMNS =
+ new HashSet<String>(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<String> SYSTEM_CATALOG_PROPS_COLUMNS =
+ new HashSet<String>(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<String, TableInfo> _tableLookup =
+ new LinkedHashMap<String, TableInfo>() {
+ private static final long serialVersionUID = 0L;
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<String, TableInfo> e) {
+ return(size() > MAX_CACHED_LOOKUP_TABLES);
+ }
+ };
+ /** set of table names as stored in the mdb file, created on demand */
+ private Set<String> _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<byte[]> _newTableSIDs = new ArrayList<byte[]>();
+ /** 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<String,Database> _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. <b>If this file
+ * already exists, it will be overwritten.</b>
+ * @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 <tt>"r"</tt> but the given file object does
+ * not denote an existing regular file, or if the mode begins
+ * with <tt>"rw"</tt> 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<String,Database> getLinkedDatabases() {
+ return ((_linkedDbs == null) ? Collections.<String,Database>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<String,FileFormat> 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<String> getTableNames() throws IOException {
+ if(_tableNames == null) {
+ Set<String> tableNames =
+ new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
+ _tableFinder.getTableNames(tableNames, false);
+ _tableNames = tableNames;
+ }
+ return _tableNames;
+ }
+
+ public Set<String> getSystemTableNames() throws IOException {
+ Set<String> sysTableNames =
+ new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
+ _tableFinder.getTableNames(sysTableNames, true);
+ return sysTableNames;
+ }
+
+ public Iterator<Table> 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,Database>();
+ }
+
+ 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<ColumnBuilder> 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<ColumnBuilder> columns,
+ List<IndexBuilder> 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<Relationship> getRelationships(Table table1, Table table2)
+ throws IOException
+ {
+ return getRelationships((TableImpl)table1, (TableImpl)table2);
+ }
+
+ public List<Relationship> 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<Relationship> 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<Relationship> getRelationships()
+ throws IOException
+ {
+ return getRelationshipsImpl(null, null, false);
+ }
+
+ public List<Relationship> getSystemRelationships()
+ throws IOException
+ {
+ return getRelationshipsImpl(null, null, true);
+ }
+
+ private List<Relationship> 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<Relationship> relationships = new ArrayList<Relationship>();
+
+ 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<Query> 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<Row> queryInfo = new ArrayList<Row>();
+ Map<Integer,List<QueryImpl.Row>> queryRowMap =
+ new HashMap<Integer,List<QueryImpl.Row>>();
+ 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<QueryImpl.Row>());
+ }
+ }
+
+ // find all the query rows
+ for(Row row : CursorImpl.createCursor(_queries)) {
+ QueryImpl.Row queryRow = new QueryImpl.Row(row);
+ List<QueryImpl.Row> 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<Query> queries = new ArrayList<Query>();
+ 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<QueryImpl.Row> 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<Relationship> 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<ColumnImpl> 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<Object[]> aceRows = new ArrayList<Object[]>(_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<Table>
+ {
+ private Iterator<String> _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<String> columns)
+ throws IOException
+ {
+ Cursor cur = findRow(parentId, name);
+ return ((cur != null) ? cur.getCurrentRow(columns) : null);
+ }
+
+ public Row getObjectRow(
+ Integer objectId, Collection<String> columns)
+ throws IOException
+ {
+ Cursor cur = findRow(objectId);
+ return ((cur != null) ? cur.getCurrentRow(columns) : null);
+ }
+
+ public void getTableNames(Set<String> 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<String,Object> rowPat = new HashMap<String,Object>();
+ 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<TableImpl>
+ {
+ private final Integer _pageNumber;
+
+ private WeakTableReference(Integer pageNumber, TableImpl table,
+ ReferenceQueue<TableImpl> 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<Integer,WeakTableReference> _tables =
+ new HashMap<Integer,WeakTableReference>();
+ private final ReferenceQueue<TableImpl> _queue =
+ new ReferenceQueue<TableImpl>();
+
+ 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
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
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<Column> _cols;
+ private final TableImpl _table;
+ private final List<ColumnImpl> _cols;
private List<Joiner> _primaryJoinersChkUp;
private List<Joiner> _primaryJoinersChkDel;
private List<Joiner> _primaryJoinersDoUp;
private List<Joiner> _primaryJoinersDoDel;
private List<Joiner> _secondaryJoiners;
- FKEnforcer(Table table) {
+ FKEnforcer(TableImpl table) {
_table = table;
// at this point, only init the index columns
- Set<Column> cols = new TreeSet<Column>();
- for(Index idx : _table.getIndexes()) {
- Index.ForeignKeyReference ref = idx.getReference();
+ Set<ColumnImpl> cols = new TreeSet<ColumnImpl>();
+ 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<Column>(cols)) :
- Collections.<Column>emptyList();
+ Collections.unmodifiableList(new ArrayList<ColumnImpl>(cols)) :
+ Collections.<ColumnImpl>emptyList();
}
/**
@@ -88,8 +96,8 @@ final class FKEnforcer
_primaryJoinersDoDel = new ArrayList<Joiner>(1);
_secondaryJoiners = new ArrayList<Joiner>(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<IndexData.ColumnDescriptor> fromCols = joiner.getColumns();
- List<IndexData.ColumnDescriptor> toCols = joiner.getToIndex().getColumns();
+ List<? extends Index.Column> fromCols = joiner.getColumns();
+ List<? extends Index.Column> toCols = joiner.getToIndex().getColumns();
Object[] toRow = new Object[joiner.getToTable().getColumnCount()];
- for(Iterator<Map<String,Object>> iter = joiner.findRows(
+ for(Iterator<Row> iter = joiner.findRows(
oldFromRow, Collections.<String>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
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
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
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<String> _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.
+ * <p>
+ * 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<Row> 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<String,?> 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<String,?> 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<String,Object> tmpRowPattern = new LinkedHashMap<String,Object>();
+ 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<String>();
+ 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<String> 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
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[]> BYTE_CODE_COMPARATOR =
+ public static final Comparator<byte[]> BYTE_CODE_COMPARATOR =
new Comparator<byte[]>() {
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() {
@@ -343,12 +346,21 @@ public abstract class IndexData {
}
/**
+ * 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.
* <p>
* 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<Column> availableColumns)
+ public void read(ByteBuffer tableBuffer, List<ColumnImpl> 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<Entry>
{
/** 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<Entry> getEntries() { return Collections.emptyList(); }
+ @Override
+ public void setEntries(List<Entry> 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<IndexImpl>
+{
+ 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<IndexData> 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<IndexData.ColumnDescriptor> 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.
+ * <p>
+ * Forces index initialization.
+ */
+ public void update() throws IOException {
+ getIndexData().update();
+ }
+
+ /**
+ * Adds a row to this index
+ * <p>
+ * 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
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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<String,?> 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
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<Integer, DataPageMain> _dataPages =
- new HashMap<Integer, DataPageMain>();
+ new LinkedHashMap<Integer, DataPageMain>(16, 0.75f, true) {
+ private static final long serialVersionUID = 0L;
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<Integer, DataPageMain> 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<CacheDataPage> _modifiedPages =
new ArrayList<CacheDataPage>();
- 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<DataPageMain>(_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<DataPageMain> 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
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<String,Database.FileFormat> 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
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
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,10 +79,24 @@ 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;
}
@@ -133,6 +142,45 @@ public class PageChannel implements Channel, Flushable {
}
/**
+ * 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.
*/
private int getNextPageNumber(long 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
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<String,Property> _props =
+ new LinkedHashMap<String,Property>();
+
+ 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<Property> iterator() {
+ return _props.values().iterator();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(PropertyMaps.DEFAULT_NAME.equals(getName()) ?
+ "<DEFAULT>" : getName())
+ .append(" {");
+ for(Iterator<Property> 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
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<PropertyMap>
+public class PropertyMaps implements Iterable<PropertyMapImpl>
{
/** 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<PropertyMap>
/** maps the PropertyMap name (case-insensitive) to the PropertyMap
instance */
- private final Map<String,PropertyMap> _maps =
- new LinkedHashMap<String,PropertyMap>();
+ private final Map<String,PropertyMapImpl> _maps =
+ new LinkedHashMap<String,PropertyMapImpl>();
private final int _objectId;
public PropertyMaps(int objectId) {
@@ -68,7 +71,7 @@ public class PropertyMaps implements Iterable<PropertyMap>
* @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<PropertyMap>
* @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<PropertyMap>
* @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<PropertyMap>
/**
* 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<PropertyMap> iterator() {
+ public Iterator<PropertyMapImpl> iterator() {
return _maps.values().iterator();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
- for(Iterator<PropertyMap> iter = iterator(); iter.hasNext(); ) {
+ for(Iterator<PropertyMapImpl> iter = iterator(); iter.hasNext(); ) {
sb.append(iter.next());
if(iter.hasNext()) {
sb.append("\n");
@@ -123,12 +126,12 @@ public class PropertyMaps implements Iterable<PropertyMap>
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<DataType,PropColumn> _columns =
new HashMap<DataType,PropColumn>();
- Handler(Database database) {
+ Handler(DatabaseImpl database) {
_database = database;
}
@@ -204,7 +207,7 @@ public class PropertyMaps implements Iterable<PropertyMap>
* @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<String> propNames, short blockType)
throws IOException
{
@@ -221,7 +224,7 @@ public class PropertyMaps implements Iterable<PropertyMap>
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<PropertyMap>
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<PropertyMap>
// 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<PropertyMap>
/**
* 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<PropertyMap>
*/
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<Column> _toColumns;
+ /** the columns in the "to" table in this relationship (aligned w/
+ toColumns list) */
+ private final List<Column> _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<Column>(
+ Collections.nCopies(numCols, (Column)null));
+ _toTable = toTable;
+ _toColumns = new ArrayList<Column>(
+ Collections.nCopies(numCols, (Column)null));
+ _flags = flags;
+ }
+
+ public String getName() {
+ return _name;
+ }
+
+ public Table getFromTable() {
+ return _fromTable;
+ }
+
+ public List<Column> getFromColumns() {
+ return _fromColumns;
+ }
+
+ public Table getToTable() {
+ return _toTable;
+ }
+
+ public List<Column> 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 <code>RowId</code> 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.
+ * </p>
+ * Note that the {@link #equals} and {@link #hashCode} methods work on the row
+ * contents <i>only</i> (i.e. they ignore the id).
+ *
+ * @author James Ahlborn
+ */
+public class RowImpl extends LinkedHashMap<String,Object> 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
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<Column> _columns;
+ private final List<ColumnBuilder> _columns;
private final List<IndexBuilder> _indexes;
private final Map<IndexBuilder,IndexState> _indexStates =
new IdentityHashMap<IndexBuilder,IndexState>();
- private final Map<Column,ColumnState> _columnStates =
- new IdentityHashMap<Column,ColumnState>();
- private final List<Column> _lvalCols = new ArrayList<Column>();
+ private final Map<ColumnBuilder,ColumnState> _columnStates =
+ new IdentityHashMap<ColumnBuilder,ColumnState>();
+ private final List<ColumnBuilder> _lvalCols = new ArrayList<ColumnBuilder>();
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<Column> columns,
+ public TableCreator(DatabaseImpl database, String name, List<ColumnBuilder> columns,
List<IndexBuilder> indexes) {
_database = database;
_name = name;
@@ -81,7 +85,7 @@ class TableCreator
return _umapPageNumber;
}
- public List<Column> getColumns() {
+ public List<ColumnBuilder> 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<Column> getLongValueColumns() {
+ public List<ColumnBuilder> 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<String> colNames = new HashSet<String>();
// 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<Column> autoCols = Table.getAutoNumberColumns(_columns);
+ List<ColumnBuilder> autoCols = getAutoNumberColumns();
if(autoCols.size() > 1) {
// for most autonumber types, we can only have one of each type
Set<DataType> 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<ColumnBuilder> getAutoNumberColumns()
+ {
+ List<ColumnBuilder> autoCols = new ArrayList<ColumnBuilder>(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
+ * <p>
+ * 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<ColumnImpl> VAR_LEN_COLUMN_COMPARATOR =
+ new Comparator<ColumnImpl>() {
+ 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<ColumnImpl> DISPLAY_ORDER_COMPARATOR =
+ new Comparator<ColumnImpl>() {
+ 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<ColumnImpl> _columns = new ArrayList<ColumnImpl>();
+ /** List of variable length columns in this table, ordered by offset */
+ private final List<ColumnImpl> _varColumns = new ArrayList<ColumnImpl>();
+ /** List of autonumber columns in this table, ordered by column number */
+ private final List<ColumnImpl> _autoNumColumns = new ArrayList<ColumnImpl>(1);
+ /** List of indexes on this table (multiple logical indexes may be backed by
+ the same index data) */
+ private final List<IndexImpl> _indexes = new ArrayList<IndexImpl>();
+ /** List of index datas on this table (the actual backing data for an
+ index) */
+ private final List<IndexData> _indexDatas = new ArrayList<IndexData>();
+ /** List of columns in this table which are in one or more indexes */
+ private final Set<ColumnImpl> _indexColumns = new LinkedHashSet<ColumnImpl>();
+ /** 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<ColumnImpl> 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 <i>approximate</i> number of database pages owned by this
+ * table and all related indexes (this number does <i>not</i> take into
+ * account pages used for large OLE/MEMO fields).
+ * <p>
+ * To calculate the approximate number of bytes owned by a table:
+ * <code>
+ * int approxTableBytes = (table.getApproximateOwnedPageCount() *
+ * table.getFormat().PAGE_SIZE);
+ * </code>
+ * @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<ColumnImpl> 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<IndexImpl> 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<IndexData> 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<String> 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<ColumnImpl> columns,
+ Collection<String> 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<ColumnImpl,byte[]> 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<Row> 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<ColumnBuilder> 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<ColumnBuilder> 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<String,?> 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<String,?> rowMap) {
+ return asRow(rowMap, null, true);
+ }
+
+ public Object[] asUpdateRow(Map<String,?> 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<String,?> 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 extends Map<String,Object>> M addRowFromMap(M row)
+ throws IOException
+ {
+ Object[] rowValues = asRow(row);
+
+ addRow(rowValues);
+
+ returnRowValues(row, rowValues, _autoNumColumns);
+ return row;
+ }
+
+ public List<? extends Object[]> addRows(List<? extends Object[]> rows)
+ throws IOException
+ {
+ return addRows(rows, _multiRowBufferH);
+ }
+
+ public <M extends Map<String,Object>> List<M> addRowsFromMaps(List<M> rows)
+ throws IOException
+ {
+ List<Object[]> rowValuesList = new ArrayList<Object[]>(rows.size());
+ for(Map<String,Object> row : rows) {
+ rowValuesList.add(asRow(row));
+ }
+
+ addRows(rowValuesList);
+
+ if(!_autoNumColumns.isEmpty()) {
+ for(int i = 0; i < rowValuesList.size(); ++i) {
+ Map<String,Object> row = rows.get(i);
+ Object[] rowValues = rowValuesList.get(i);
+ returnRowValues(row, rowValues, _autoNumColumns);
+ }
+ }
+ return rows;
+ }
+
+ private static void returnRowValues(Map<String,Object> row, Object[] rowValues,
+ List<ColumnImpl> 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<? extends Object[]> addRows(List<? extends Object[]> rows,
+ TempBufferHolder writeRowBufferH)
+ throws IOException
+ {
+ if(rows.isEmpty()) {
+ return rows;
+ }
+
+ getPageChannel().startWrite();
+ try {
+
+ List<Object[]> 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<Object[]>(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 extends Map<String,Object>> 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<ColumnImpl,byte[]> keepRawVarValues =
+ (!_varColumns.isEmpty() ? new HashMap<ColumnImpl,byte[]>() : 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.<ColumnImpl,byte[]>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<ColumnImpl,byte[]> 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<ColumnImpl> 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<Object> 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
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
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
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
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<Attachment>
+ implements AttachmentColumnInfo
+{
+ /** some file formats which may not be worth re-compressing */
+ private static final Set<String> COMPRESSED_FORMATS = new HashSet<String>(
+ 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<V extends ComplexValue>
+ implements ComplexColumnInfo<V>
+{
+ 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<Column> _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<Column> typeCols = new ArrayList<Column>();
+ List<Column> otherCols = new ArrayList<Column>();
+ 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<Column> getTypeColumns() {
+ return _typeCols;
+ }
+
+ public int countValues(int complexValueFk) throws IOException {
+ return getRawValues(complexValueFk,
+ Collections.singleton(_complexValFkCol.getName()))
+ .size();
+ }
+
+ public List<Row> getRawValues(int complexValueFk)
+ throws IOException
+ {
+ return getRawValues(complexValueFk, null);
+ }
+
+ private Iterator<Row> getComplexValFkIter(
+ int complexValueFk, Collection<String> columnNames)
+ throws IOException
+ {
+ if(_complexValIdCursor == null) {
+ _complexValIdCursor = _flatTable.newCursor()
+ .setIndexByColumns(_complexValFkCol)
+ .toIndexCursor();
+ }
+
+ return _complexValIdCursor.newEntryIterable(complexValueFk)
+ .setColumnNames(columnNames).iterator();
+ }
+
+ public List<Row> getRawValues(int complexValueFk,
+ Collection<String> columnNames)
+ throws IOException
+ {
+ Iterator<Row> entryIter =
+ getComplexValFkIter(complexValueFk, columnNames);
+ if(!entryIter.hasNext()) {
+ return Collections.emptyList();
+ }
+
+ List<Row> values = new ArrayList<Row>();
+ while(entryIter.hasNext()) {
+ values.add(entryIter.next());
+ }
+
+ return values;
+ }
+
+ public List<V> getValues(ComplexValueForeignKey complexValueFk)
+ throws IOException
+ {
+ List<Row> rawValues = getRawValues(complexValueFk.get());
+ if(rawValues.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ return toValues(complexValueFk, rawValues);
+ }
+
+ protected List<V> toValues(ComplexValueForeignKey complexValueFk,
+ List<Row> rawValues)
+ throws IOException
+ {
+ List<V> values = new ArrayList<V>();
+ for(Row rawValue : rawValues) {
+ values.add(toValue(complexValueFk, rawValue));
+ }
+
+ return values;
+ }
+
+ public ComplexValue.Id addRawValue(Map<String,?> 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<? extends V> 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<? extends V> 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<? extends V> values) throws IOException {
+ for(V value : values) {
+ deleteValue(value);
+ }
+ }
+
+ public void deleteAllValues(int complexValueFk) throws IOException {
+ Iterator<Row> entryIter =
+ getComplexValFkIter(complexValueFk, Collections.<String>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<Column> typeCols,
+ List<Column> 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.
+ * <p>
+ * 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<? extends ComplexValue> _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<? extends ComplexValue> 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<Row> getRawValues() throws IOException {
+ return getComplexInfo().getRawValues(get());
+ }
+
+ @Override
+ public List<? extends ComplexValue> getValues() throws IOException {
+ if(_values == null) {
+ _values = getComplexInfo().getValues(this);
+ }
+ return _values;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<Version> getVersions() throws IOException {
+ if(getComplexType() != ComplexDataType.VERSION_HISTORY) {
+ throw new UnsupportedOperationException();
+ }
+ return (List<Version>)getValues();
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<Attachment> getAttachments() throws IOException {
+ if(getComplexType() != ComplexDataType.ATTACHMENT) {
+ throw new UnsupportedOperationException();
+ }
+ return (List<Attachment>)getValues();
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<SingleValue> getMultiValues() throws IOException {
+ if(getComplexType() != ComplexDataType.MULTI_VALUE) {
+ throw new UnsupportedOperationException();
+ }
+ return (List<SingleValue>)getValues();
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<UnsupportedValue> getUnsupportedValues() throws IOException {
+ if(getComplexType() != ComplexDataType.UNSUPPORTED) {
+ throw new UnsupportedOperationException();
+ }
+ return (List<UnsupportedValue>)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<String,?> 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<SingleValue>
+ 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<UnsupportedValue>
+ implements UnsupportedColumnInfo
+{
+
+ public UnsupportedColumnInfoImpl(Column column, int complexId,
+ Table typeObjTable, Table flatTable)
+ throws IOException
+ {
+ super(column, complexId, typeObjTable, flatTable);
+ }
+
+ public List<Column> getValueColumns() {
+ return getTypeColumns();
+ }
+
+ @Override
+ public ComplexDataType getType()
+ {
+ return ComplexDataType.UNSUPPORTED;
+ }
+
+ @Override
+ protected UnsupportedValueImpl toValue(
+ ComplexValueForeignKey complexValueFk,
+ Row rawValue)
+ {
+ ComplexValue.Id id = getValueId(rawValue);
+
+ Map<String,Object> values = new LinkedHashMap<String,Object>();
+ 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<String,Object> values = value.getValues();
+ for(Column col : getValueColumns()) {
+ col.setRowValue(row, col.getRowValue(values));
+ }
+
+ return row;
+ }
+
+ public static UnsupportedValue newValue(Map<String,?> values) {
+ return newValue(INVALID_FK, values);
+ }
+
+ public static UnsupportedValue newValue(
+ ComplexValueForeignKey complexValueFk, Map<String,?> values) {
+ return new UnsupportedValueImpl(INVALID_ID, complexValueFk,
+ new LinkedHashMap<String,Object>(values));
+ }
+
+ private static class UnsupportedValueImpl extends ComplexValueImpl
+ implements UnsupportedValue
+ {
+ private Map<String,Object> _values;
+
+ private UnsupportedValueImpl(Id id, ComplexValueForeignKey complexValueFk,
+ Map<String,Object> values)
+ {
+ super(id, complexValueFk);
+ _values = values;
+ }
+
+ public Map<String,Object> 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.
+ * <p>
+ * Note, the strongly typed update/delete methods are <i>not</i> supported for
+ * version history columns (the data is supposed to be immutable). That said,
+ * the "raw" update/delete methods are supported for those that <i>really</i>
+ * want to muck with the version history data.
+ *
+ * @author James Ahlborn
+ */
+public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl<Version>
+ 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<Version> toValues(ComplexValueForeignKey complexValueFk,
+ List<Row> rawValues)
+ throws IOException
+ {
+ List<Version> 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 <table> VALUES (<values>)}
+ *
+ * @author James Ahlborn
+ */
+public class AppendQueryImpl extends BaseSelectQueryImpl implements AppendQuery
+{
+
+ public AppendQueryImpl(String name, List<Row> 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<Row> getValueRows() {
+ return filterRowsByFlag(super.getColumnRows(), APPEND_VALUE_FLAG);
+ }
+
+ @Override
+ protected List<Row> getColumnRows() {
+ return filterRowsByNotFlag(super.getColumnRows(), APPEND_VALUE_FLAG);
+ }
+
+ public List<String> 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<String> 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<Row> 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<String> 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<String> 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<String> 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<String> getSelectColumns()
+ {
+ List<String> 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<String> 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<String> 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<String> 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 <expr> SELECT <query> PIVOT <expr>}
+ *
+ * @author James Ahlborn
+ */
+public class CrossTabQueryImpl extends BaseSelectQueryImpl
+ implements CrossTabQuery
+{
+
+ public CrossTabQueryImpl(String name, List<Row> 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<Row> getColumnRows() {
+ return filterRowsByFlag(super.getColumnRows(), CROSSTAB_NORMAL_FLAG);
+ }
+
+ @Override
+ protected List<Row> 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<Row> 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 <table> WHERE <expression>}
+ *
+ * @author James Ahlborn
+ */
+public class DeleteQueryImpl extends BaseSelectQueryImpl implements DeleteQuery
+{
+
+ public DeleteQueryImpl(String name, List<Row> 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 <query> INTO <newTable>}
+ *
+ * @author James Ahlborn
+ */
+public class MakeTableQueryImpl extends BaseSelectQueryImpl
+ implements MakeTableQuery
+{
+
+ public MakeTableQueryImpl(String name, List<Row> 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<Row> 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
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<Row> _rows;
+ private final int _objectId;
+ private final Type _type;
+
+ protected QueryImpl(String name, List<Row> 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<Row> getRows() {
+ return _rows;
+ }
+
+ protected List<Row> 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<Row> getParameterRows() {
+ return getRowsByAttribute(PARAMETER_ATTRIBUTE);
+ }
+
+ protected Row getFlagRow() {
+ return getRowByAttribute(FLAG_ATTRIBUTE);
+ }
+
+ protected Row getRemoteDatabaseRow() {
+ return getRowByAttribute(REMOTEDB_ATTRIBUTE);
+ }
+
+ protected List<Row> getTableRows() {
+ return getRowsByAttribute(TABLE_ATTRIBUTE);
+ }
+
+ protected List<Row> getColumnRows() {
+ return getRowsByAttribute(COLUMN_ATTRIBUTE);
+ }
+
+ protected List<Row> getJoinRows() {
+ return getRowsByAttribute(JOIN_ATTRIBUTE);
+ }
+
+ protected Row getWhereRow() {
+ return getRowByAttribute(WHERE_ATTRIBUTE);
+ }
+
+ protected List<Row> getGroupByRows() {
+ return getRowsByAttribute(GROUPBY_ATTRIBUTE);
+ }
+
+ protected Row getHavingRow() {
+ return getRowByAttribute(HAVING_ATTRIBUTE);
+ }
+
+ protected List<Row> getOrderByRows() {
+ return getRowsByAttribute(ORDERBY_ATTRIBUTE);
+ }
+
+ protected abstract void toSQLString(StringBuilder builder);
+
+ protected void toSQLParameterString(StringBuilder builder) {
+ // handle any parameters
+ List<String> params = getParameters();
+ if(!params.isEmpty()) {
+ builder.append("PARAMETERS ").append(params)
+ .append(';').append(NEWLINE);
+ }
+ }
+
+ public List<String> 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<String> getFromTables()
+ {
+ List<Join> joinExprs = new ArrayList<Join>();
+ 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<Row> joins = getJoinRows();
+ if(!joins.isEmpty()) {
+
+ // combine any multi-column joins
+ Collection<List<Row>> comboJoins = combineJoins(joins);
+
+ for(List<Row> comboJoin : comboJoins) {
+
+ Row join = comboJoin.get(0);
+ String joinExpr = join.expression;
+
+ if(comboJoin.size() > 1) {
+
+ // combine all the join expressions with "AND"
+ AppendableList<String> comboExprs = new AppendableList<String>() {
+ 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<String> result = new AppendableList<String>();
+ for(Join joinExpr : joinExprs) {
+ result.add(joinExpr.expression);
+ }
+
+ return result;
+ }
+
+ private static Join getJoinExpr(String table, List<Join> joinExprs)
+ {
+ for(Iterator<Join> 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<List<Row>> combineJoins(List<Row> joins)
+ {
+ // combine joins with the same to/from tables
+ Map<List<String>,List<Row>> comboJoinMap =
+ new LinkedHashMap<List<String>,List<Row>>();
+ for(Row join : joins) {
+ List<String> key = Arrays.asList(join.name1, join.name2);
+ List<Row> comboJoins = comboJoinMap.get(key);
+ if(comboJoins == null) {
+ comboJoins = new ArrayList<Row>();
+ 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<String> 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<Row> 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<Row> rows)
+ {
+ return getUniqueRow(getRowsByAttribute(rows, TYPE_ATTRIBUTE)).flag;
+ }
+
+ private static List<Row> getRowsByAttribute(List<Row> rows, Byte attribute) {
+ List<Row> result = new ArrayList<Row>();
+ for(Row row : rows) {
+ if(attribute.equals(row.attribute)) {
+ result.add(row);
+ }
+ }
+ return result;
+ }
+
+ protected static Row getUniqueRow(List<Row> 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<Row> filterRowsByFlag(
+ List<Row> rows, final short flag)
+ {
+ return new RowFilter() {
+ @Override protected boolean keep(Row row) {
+ return hasFlag(row, flag);
+ }
+ }.filter(rows);
+ }
+
+ protected static List<Row> filterRowsByNotFlag(
+ List<Row> 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<Row> 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<Row> _list;
+
+ protected RowFormatter(List<Row> list) {
+ _list = list;
+ }
+
+ public List<String> format() {
+ return format(new AppendableList<String>());
+ }
+
+ public List<String> format(List<String> 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<Row> filter(List<Row> list) {
+ for(Iterator<Row> iter = list.iterator(); iter.hasNext(); ) {
+ if(!keep(iter.next())) {
+ iter.remove();
+ }
+ }
+ return list;
+ }
+
+ protected abstract boolean keep(Row row);
+ }
+
+ protected static class AppendableList<E> extends ArrayList<E>
+ {
+ private static final long serialVersionUID = 0L;
+
+ protected AppendableList() {
+ }
+
+ protected AppendableList(Collection<? extends E> c) {
+ super(c);
+ }
+
+ protected String getSeparator() {
+ return ", ";
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ for(Iterator<E> 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<String> tables = new ArrayList<String>();
+ 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 <columns> FROM <tables> WHERE <expression>}
+ *
+ * @author James Ahlborn
+ */
+public class SelectQueryImpl extends BaseSelectQueryImpl implements SelectQuery
+{
+
+ public SelectQueryImpl(String name, List<Row> 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 <query1> UNION SELECT <query2>}
+ *
+ * @author James Ahlborn
+ */
+public class UnionQueryImpl extends QueryImpl implements UnionQuery
+{
+ public UnionQueryImpl(String name, List<Row> 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<String> 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<String> 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 <table> SET <newValues>}
+ *
+ * @author James Ahlborn
+ */
+public class UpdateQueryImpl extends QueryImpl implements UpdateQuery
+{
+
+ public UpdateQueryImpl(String name, List<Row> rows, int objectId) {
+ super(name, rows, objectId, Type.UPDATE);
+ }
+
+ public List<String> getTargetTables()
+ {
+ return super.getFromTables();
+ }
+
+ public String getRemoteDbPath()
+ {
+ return super.getFromRemoteDbPath();
+ }
+
+ public String getRemoteDbType()
+ {
+ return super.getFromRemoteDbType();
+ }
+
+ public List<String> 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
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
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
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
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
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
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
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 <table> VALUES (<values>)}
*
* @author James Ahlborn
*/
-public class AppendQuery extends BaseSelectQuery
+public interface AppendQuery extends BaseSelectQuery
{
- public AppendQuery(String name, List<Row> 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<Row> getValueRows() {
- return filterRowsByFlag(super.getColumnRows(), APPEND_VALUE_FLAG);
- }
+ public String getTargetTable();
- @Override
- protected List<Row> getColumnRows() {
- return filterRowsByNotFlag(super.getColumnRows(), APPEND_VALUE_FLAG);
- }
+ public String getRemoteDbPath();
- public List<String> 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<String> values = getValues();
- if(!values.isEmpty()) {
- builder.append("VALUES (").append(values).append(')');
- } else {
- toSQLSelectString(builder, true);
- }
- }
-
+ public List<String> 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<Row> 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<String> 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<String> 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<String> 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<String> getSelectColumns()
- {
- List<String> 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<String> 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<String> 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<String> getOrderings()
- {
- return super.getOrderings();
- }
-
+ public String getSelectType();
+
+ public List<String> getSelectColumns();
+
+ public List<String> getFromTables();
+
+ public String getFromRemoteDbPath();
+
+ public String getFromRemoteDbType();
+
+ public String getWhereExpression();
+
+ public List<String> getGroupings();
+
+ public String getHavingExpression();
+
+ public List<String> 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 <expr> SELECT <query> PIVOT <expr>}
*
* @author James Ahlborn
*/
-public class CrossTabQuery extends BaseSelectQuery
+public interface CrossTabQuery extends BaseSelectQuery
{
- public CrossTabQuery(String name, List<Row> 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<Row> getColumnRows() {
- return filterRowsByFlag(super.getColumnRows(), CROSSTAB_NORMAL_FLAG);
- }
-
- @Override
- protected List<Row> 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<Row> 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 <table> WHERE <expression>}
*
* @author James Ahlborn
*/
-public class DeleteQuery extends BaseSelectQuery
+public interface DeleteQuery extends BaseSelectQuery
{
- public DeleteQuery(String name, List<Row> 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 <query> INTO <newTable>}
*
* @author James Ahlborn
*/
-public class MakeTableQuery extends BaseSelectQuery
+public interface MakeTableQuery extends BaseSelectQuery
{
- public MakeTableQuery(String name, List<Row> 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<Row> 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.<String,Object>emptyMap());
public enum Type
{
@@ -87,649 +64,35 @@ public abstract class Query
}
}
- private final String _name;
- private final List<Row> _rows;
- private final int _objectId;
- private final Type _type;
-
- protected Query(String name, List<Row> 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<Row> getRows() {
- return _rows;
- }
-
- protected List<Row> 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<Row> getParameterRows() {
- return getRowsByAttribute(PARAMETER_ATTRIBUTE);
- }
-
- protected Row getFlagRow() {
- return getRowByAttribute(FLAG_ATTRIBUTE);
- }
-
- protected Row getRemoteDatabaseRow() {
- return getRowByAttribute(REMOTEDB_ATTRIBUTE);
- }
-
- protected List<Row> getTableRows() {
- return getRowsByAttribute(TABLE_ATTRIBUTE);
- }
-
- protected List<Row> getColumnRows() {
- return getRowsByAttribute(COLUMN_ATTRIBUTE);
- }
-
- protected List<Row> getJoinRows() {
- return getRowsByAttribute(JOIN_ATTRIBUTE);
- }
-
- protected Row getWhereRow() {
- return getRowByAttribute(WHERE_ATTRIBUTE);
- }
-
- protected List<Row> getGroupByRows() {
- return getRowsByAttribute(GROUPBY_ATTRIBUTE);
- }
-
- protected Row getHavingRow() {
- return getRowByAttribute(HAVING_ATTRIBUTE);
- }
-
- protected List<Row> getOrderByRows() {
- return getRowsByAttribute(ORDERBY_ATTRIBUTE);
- }
-
- protected abstract void toSQLString(StringBuilder builder);
-
- protected void toSQLParameterString(StringBuilder builder) {
- // handle any parameters
- List<String> params = getParameters();
- if(!params.isEmpty()) {
- builder.append("PARAMETERS ").append(params)
- .append(';').append(NEWLINE);
- }
- }
-
- public List<String> 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<String> getFromTables()
- {
- List<Join> joinExprs = new ArrayList<Join>();
- 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<Row> getRows();
- List<Row> joins = getJoinRows();
- if(!joins.isEmpty()) {
+ public List<String> getParameters();
- // combine any multi-column joins
- Collection<List<Row>> comboJoins = combineJoins(joins);
-
- for(List<Row> comboJoin : comboJoins) {
-
- Row join = comboJoin.get(0);
- String joinExpr = join.expression;
-
- if(comboJoin.size() > 1) {
-
- // combine all the join expressions with "AND"
- AppendableList<String> comboExprs = new AppendableList<String>() {
- 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<String> result = new AppendableList<String>();
- for(Join joinExpr : joinExprs) {
- result.add(joinExpr.expression);
- }
-
- return result;
- }
-
- private Join getJoinExpr(String table, List<Join> joinExprs)
- {
- for(Iterator<Join> 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<List<Row>> combineJoins(List<Row> joins)
- {
- // combine joins with the same to/from tables
- Map<List<String>,List<Row>> comboJoinMap =
- new LinkedHashMap<List<String>,List<Row>>();
- for(Row join : joins) {
- List<String> key = Arrays.asList(join.name1, join.name2);
- List<Row> comboJoins = comboJoinMap.get(key);
- if(comboJoins == null) {
- comboJoins = new ArrayList<Row>();
- 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<String> 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<Row> 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<Row> rows)
- {
- return getUniqueRow(getRowsByAttribute(rows, TYPE_ATTRIBUTE)).flag;
- }
-
- private static List<Row> getRowsByAttribute(List<Row> rows, Byte attribute) {
- List<Row> result = new ArrayList<Row>();
- for(Row row : rows) {
- if(attribute.equals(row.attribute)) {
- result.add(row);
- }
- }
- return result;
- }
-
- protected static Row getUniqueRow(List<Row> 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<Row> filterRowsByFlag(
- List<Row> rows, final short flag)
- {
- return new RowFilter() {
- @Override protected boolean keep(Row row) {
- return hasFlag(row, flag);
- }
- }.filter(rows);
- }
-
- protected static List<Row> filterRowsByNotFlag(
- List<Row> 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<Row> 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<String,Object> 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<String,Object> toTableRow()
- {
- Map<String,Object> tableRow = new LinkedHashMap<String,Object>();
-
- 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<Row> _list;
-
- protected RowFormatter(List<Row> list) {
- _list = list;
- }
-
- public List<String> format() {
- return format(new AppendableList<String>());
- }
-
- public List<String> format(List<String> 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<Row> filter(List<Row> list) {
- for(Iterator<Row> iter = list.iterator(); iter.hasNext(); ) {
- if(!keep(iter.next())) {
- iter.remove();
- }
- }
- return list;
- }
-
- protected abstract boolean keep(Row row);
- }
-
- protected static class AppendableList<E> extends ArrayList<E>
- {
- private static final long serialVersionUID = 0L;
-
- protected AppendableList() {
- }
-
- protected AppendableList(Collection<? extends E> c) {
- super(c);
- }
-
- protected String getSeparator() {
- return ", ";
- }
-
- @Override
- public String toString() {
- StringBuilder builder = new StringBuilder();
- for(Iterator<E> 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<String> tables = new ArrayList<String>();
- 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 <columns> FROM <tables> WHERE <expression>}
*
* @author James Ahlborn
*/
-public class SelectQuery extends BaseSelectQuery
+public interface SelectQuery extends BaseSelectQuery
{
-
- public SelectQuery(String name, List<Row> 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 <query1> UNION SELECT <query2>}
*
* @author James Ahlborn
*/
-public class UnionQuery extends Query
+public interface UnionQuery extends Query
{
- public UnionQuery(String name, List<Row> 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<String> 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<String> 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<String> 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 <table> SET <newValues>}
*
* @author James Ahlborn
*/
-public class UpdateQuery extends Query
+public interface UpdateQuery extends Query
{
- public UpdateQuery(String name, List<Row> rows, int objectId) {
- super(name, rows, objectId, Type.UPDATE);
- }
-
- public List<String> getTargetTables()
- {
- return super.getFromTables();
- }
-
- public String getRemoteDbPath()
- {
- return super.getFromRemoteDbPath();
- }
-
- public String getRemoteDbType()
- {
- return super.getFromRemoteDbType();
- }
-
- public List<String> 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<String> 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<String> getNewValues();
+ public String getWhereExpression();
}
diff --git a/src/java/com/healthmarketscience/jackcess/CaseInsensitiveColumnMatcher.java b/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
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
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<Row>
+{
+ private final IndexCursor _cursor;
+
+ private Collection<String> _columnNames;
+ private Object[] _entryValues;
+ private ColumnMatcher _columnMatcher;
+
+ public EntryIterableBuilder(IndexCursor cursor, Object... entryValues) {
+ _cursor = cursor;
+ _entryValues = entryValues;
+ }
+
+ public Collection<String> getColumnNames() {
+ return _columnNames;
+ }
+
+ public ColumnMatcher getColumnMatcher() {
+ return _columnMatcher;
+ }
+
+ public Object[] getEntryValues() {
+ return _entryValues;
+ }
+
+ public EntryIterableBuilder setColumnNames(Collection<String> columnNames) {
+ _columnNames = columnNames;
+ return this;
+ }
+
+ public EntryIterableBuilder addColumnNames(Iterable<String> columnNames) {
+ if(columnNames != null) {
+ for(String name : columnNames) {
+ addColumnName(name);
+ }
+ }
+ return this;
+ }
+
+ public EntryIterableBuilder addColumns(Iterable<? extends Column> 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<String>();
+ }
+ _columnNames.add(columnName);
+ }
+
+ public EntryIterableBuilder setEntryValues(Object... entryValues) {
+ _entryValues = entryValues;
+ return this;
+ }
+
+ public EntryIterableBuilder setColumnMatcher(ColumnMatcher columnMatcher) {
+ _columnMatcher = columnMatcher;
+ return this;
+ }
+
+ public Iterator<Row> 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
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
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<Column> filterColumns(List<Column> columns) throws IOException;
+ public List<Column> filterColumns(List<Column> 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
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<Column> origCols = cursor.getTable().getColumns();
+ List<? extends Column> origCols = cursor.getTable().getColumns();
List<Column> columns = new ArrayList<Column>(origCols);
columns = filter.filterColumns(columns);
@@ -331,8 +336,8 @@ public class ExportUtil {
}
// print the data rows
- Map<String, Object> 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
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<Column> filterColumns(List<Column> destColumns,
- ResultSetMetaData srcColumns)
+ public List<ColumnBuilder> filterColumns(List<ColumnBuilder> 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
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<Column> toColumns(ResultSetMetaData md)
+ public static List<ColumnBuilder> toColumns(ResultSetMetaData md)
throws SQLException
{
- List<Column> columns = new LinkedList<Column>();
+ List<ColumnBuilder> columns = new LinkedList<ColumnBuilder>();
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<Column> columns = toColumns(md);
+ List<ColumnBuilder> 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<Column> columns = new LinkedList<Column>();
+ List<ColumnBuilder> columns = new LinkedList<ColumnBuilder>();
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<Column> columns,
+ List<ColumnBuilder> 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<Row>
+{
+ 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<String> _columnNames;
+ private ColumnMatcher _columnMatcher;
+ private Object _matchPattern;
+
+ public IterableBuilder(Cursor cursor) {
+ _cursor = cursor;
+ }
+
+ public Collection<String> 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<String> columnNames) {
+ _columnNames = columnNames;
+ return this;
+ }
+
+ public IterableBuilder addColumnNames(Iterable<String> columnNames) {
+ if(columnNames != null) {
+ for(String name : columnNames) {
+ addColumnName(name);
+ }
+ }
+ return this;
+ }
+
+ public IterableBuilder addColumns(Iterable<? extends Column> 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<String>();
+ }
+ _columnNames.add(columnName);
+ }
+
+ public IterableBuilder setMatchPattern(Column columnPattern,
+ Object valuePattern) {
+ _type = Type.COLUMN_MATCH;
+ _matchPattern = new AbstractMap.SimpleImmutableEntry<Column,Object>(
+ columnPattern, valuePattern);
+ return this;
+ }
+
+ public IterableBuilder setMatchPattern(String columnNamePattern,
+ Object valuePattern) {
+ return setMatchPattern(_cursor.getTable().getColumn(columnNamePattern),
+ valuePattern);
+ }
+
+ public IterableBuilder setMatchPattern(Map<String,?> rowPattern) {
+ _type = Type.ROW_MATCH;
+ _matchPattern = rowPattern;
+ return this;
+ }
+
+ public IterableBuilder addMatchPattern(String columnNamePattern,
+ Object valuePattern)
+ {
+ _type = Type.ROW_MATCH;
+ @SuppressWarnings("unchecked")
+ Map<String,Object> matchPattern = ((Map<String,Object>)_matchPattern);
+ if(matchPattern == null) {
+ matchPattern = new HashMap<String,Object>();
+ _matchPattern = matchPattern;
+ }
+ matchPattern.put(columnNamePattern, valuePattern);
+ return this;
+ }
+
+ public IterableBuilder setColumnMatcher(ColumnMatcher columnMatcher) {
+ _columnMatcher = columnMatcher;
+ return this;
+ }
+
+ public Iterator<Row> 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
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<IndexData.ColumnDescriptor> _fromCols;
+ private final List<? extends Index.Column> _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<IndexData.ColumnDescriptor> getColumns() {
+ public List<? extends Index.Column> 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<String,Object> findFirstRow(Map<String,?> fromRow)
+ public Row findFirstRow(Map<String,?> 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<String,Object> findFirstRow(Map<String,?> fromRow,
+ public Row findFirstRow(Map<String,?> fromRow,
Collection<String> 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<Map<String,Object>> findRows(Map<String,?> fromRow)
+ public Iterator<Row> findRows(Map<String,?> 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<Map<String,Object>> findRows(Map<String,?> fromRow,
- Collection<String> columnNames)
+ public Iterator<Row> findRows(Map<String,?> fromRow,
+ Collection<String> 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<Map<String,Object>> findRows(Object[] fromRow,
- Collection<String> columnNames)
+ public Iterator<Row> findRows(Object[] fromRow,
+ Collection<String> 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<Map<String,Object>> findRowsIterable(Map<String,?> fromRow)
+ public Iterable<Row> findRowsIterable(Map<String,?> 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<Map<String,Object>> findRowsIterable(
+ public Iterable<Row> findRowsIterable(
final Map<String,?> fromRow, final Collection<String> columnNames)
{
- return new Iterable<Map<String, Object>>() {
- public Iterator<Map<String, Object>> iterator() {
+ return new Iterable<Row>() {
+ public Iterator<Row> 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.<String>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<Map<String,Object>> iter)
+ private static boolean deleteRowsImpl(Iterator<Row> 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<IndexData.ColumnDescriptor> toCols = _toCursor.getIndex().getColumns();
+ List<? extends Index.Column> 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
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
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
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<String, Object> 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<Map<String, Object>> apply(
- Iterable<Map<String, Object>> iterable)
+ public Iterable<Row> apply(Iterable<? extends Row> 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<String, Object> rowPattern)
+ public static RowFilter matchPattern(final Map<String,?> rowPattern)
{
return new RowFilter() {
@Override
- public boolean matches(Map<String, Object> row)
+ public boolean matches(Row row)
{
- for(Map.Entry<String,Object> e : rowPattern.entrySet()) {
+ for(Map.Entry<String,?> 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<String, Object> 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<String, Object> 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<Map<String, Object>> apply(
- RowFilter rowFilter,
- Iterable<Map<String, Object>> iterable)
+ @SuppressWarnings("unchecked")
+ public static Iterable<Row> apply(RowFilter rowFilter,
+ Iterable<? extends Row> iterable)
{
- return((rowFilter != null) ? rowFilter.apply(iterable) : iterable);
+ return((rowFilter != null) ? rowFilter.apply(iterable) :
+ (Iterable<Row>)iterable);
}
/**
* Iterable which creates a filtered view of a another row iterable.
*/
- private class FilterIterable implements Iterable<Map<String, Object>>
+ private class FilterIterable implements Iterable<Row>
{
- private final Iterable<Map<String, Object>> _iterable;
+ private final Iterable<? extends Row> _iterable;
- private FilterIterable(Iterable<Map<String, Object>> iterable)
+ private FilterIterable(Iterable<? extends Row> 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<Map<String, Object>> iterator()
+ public Iterator<Row> iterator()
{
- return new Iterator<Map<String, Object>>() {
- private final Iterator<Map<String, Object>> _iter =
- _iterable.iterator();
- private Map<String, Object> _next;
+ return new Iterator<Row>() {
+ private final Iterator<? extends Row> _iter = _iterable.iterator();
+ private Row _next;
public boolean hasNext() {
while(_iter.hasNext()) {
@@ -187,7 +189,7 @@ public abstract class RowFilter
return false;
}
- public Map<String, Object> 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
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
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
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<Column> filterColumns(List<Column> destColumns,
- ResultSetMetaData srcColumns)
+
+ public List<ColumnBuilder> filterColumns(List<ColumnBuilder> 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<String> firstTwo = new ArrayList<String>();
- for(Map<String,Object> row : Cursor.createIndexCursor(t, index)) {
+ for(Map<String,Object> 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<String> found = new ArrayList<String>();
- for(Map<String,Object> row : Cursor.createIndexCursor(t, index)) {
+ for(Map<String,Object> 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<RowId> sortedRowIds = new ArrayList<RowId>(new TreeSet<RowId>(
- Arrays.asList(rowId1, rowId2, rowId3, RowId.FIRST_ROW_ID,
- RowId.LAST_ROW_ID)));
+ List<RowIdImpl> sortedRowIds =
+ new ArrayList<RowIdImpl>(new TreeSet<RowIdImpl>(
+ 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<Map<String, Object>> expectedRows = createTestTableData();
+ List<Map<String, Object>> foundRows = new ArrayList<Map<String, Object>>();
+
+ Iterator<Row> 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<String, Object> 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<Map<String, Object>> foundRows =
new ArrayList<Map<String, Object>>();
- for(Map<String, Object> row : cursor.reverseIterable()) {
+ for(Map<String, Object> 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<Map<String,Object>> rows = RowFilterTest.toList(
- cursor.columnMatchIterable(valCol, "data2"));
+ List<? extends Map<String,Object>> rows = RowFilterTest.toList(
+ cursor.newIterable().setMatchPattern("value", "data2"));
- List<Map<String, Object>> expectedRows = null;
+ List<? extends Map<String, Object>> 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<String,Object> row : table) {
- expectedRows = new ArrayList<Map<String,Object>>();
+ List<Map<String,Object>> tmpRows = new ArrayList<Map<String,Object>>();
for(Map<String,Object> 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<String> expectedData = new ArrayList<String>();
- for(Map<String,Object> row : cursor.entryIterable(
- Arrays.asList("data"), 1)) {
+ for(Map<String,Object> row : cursor.newEntryIterable(1)
+ .addColumnNames("data")) {
expectedData.add((String)row.get("data"));
}
assertEquals(Arrays.asList("baz11", "baz11-2"), expectedData);
expectedData = new ArrayList<String>();
- for(Iterator<Map<String,Object>> iter = cursor.entryIterator(1);
+ for(Iterator<? extends Map<String,Object>> 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<String>();
- for(Map<String,Object> row : cursor.entryIterable(
- Arrays.asList("data"), 1)) {
+ for(Map<String,Object> 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<String> expectedData = new ArrayList<String>();
- for(Map<String,Object> row : cursor.iterable(
+ for(Map<String,Object> 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<String>();
- for(Iterator<Map<String,Object>> iter = cursor.iterator();
+ for(Iterator<? extends Map<String,Object>> iter = cursor.iterator();
iter.hasNext(); ) {
Map<String,Object> 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<String>();
- for(Map<String,Object> row : cursor.iterable(
+ for(Map<String,Object> 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.<Column>emptyList());
+ ((DatabaseImpl)db).createTable("test", Collections.<ColumnBuilder>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<Column> columns = open(testDB).getTable("Table1").getColumns();
+ List<? extends Column> 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<Column> columns, int columnNumber, String name,
- DataType dataType)
+ static void checkColumn(List<? extends Column> 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<String,Object> row1 = createTestRowMap("Tim1");
+ Map<String,Object> row2 = createTestRowMap("Tim2");
+ Map<String,Object> row3 = createTestRowMap("Tim3");
Table table = db.getTable("Test");
- table.addRows(Arrays.asList(row1, row2, row3));
+ @SuppressWarnings("unchecked")
+ List<Map<String,Object>> 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<Row> 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<Column> columns = open(testDB).getTable("Table2").getColumns();
+ List<? extends Column> 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<Column> columns = new ArrayList<Column>();
+ List<ColumnBuilder> columns = new ArrayList<ColumnBuilder>();
List<String> colNames = new ArrayList<String>();
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<Map<String, Object>> expectedRows =
+ List<? extends Map<String, Object>> 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<String,Object> 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<String,Object> 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<Row> 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<Map<String, Object>> expectedRows =
+ List<? extends Map<String, Object>> 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<String,Object> 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<String, Object> row : Cursor.createCursor(table)) {
+ for(Map<String, Object> row : CursorBuilder.createCursor(table)) {
rtn++;
}
return rtn;
}
- static void assertTable(List<Map<String, Object>> expectedTable, Table table)
+ public static void assertTable(
+ List<? extends Map<String, Object>> expectedTable,
+ Table table)
+ throws IOException
{
- assertCursor(expectedTable, Cursor.createCursor(table));
+ assertCursor(expectedTable, CursorBuilder.createCursor(table));
}
- static void assertCursor(List<Map<String, Object>> expectedTable,
- Cursor cursor)
+ public static void assertCursor(
+ List<? extends Map<String, Object>> expectedTable,
+ Cursor cursor)
{
List<Map<String, Object>> foundTable =
new ArrayList<Map<String, Object>>();
@@ -1430,8 +1528,8 @@ public class DatabaseTest extends TestCase {
assertEquals(expectedTable, foundTable);
}
- static Map<String, Object> createExpectedRow(Object... rowElements) {
- Map<String, Object> row = new LinkedHashMap<String, Object>();
+ 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<Map<String, Object>> createExpectedTable(Map... rows) {
- return Arrays.<Map<String, Object>>asList(rows);
+ public static List<Row> createExpectedTable(Row... rows) {
+ return Arrays.<Row>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<String, Object> row : Cursor.createCursor(table)) {
+ for(Map<String, Object> 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<String,Object> 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<String,Object> 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<String,Object> row : db.getSystemCatalog()) {
+ for(Map<String,Object> 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<Column> _columns = new ArrayList<Column>();
- private Table _testTable;
+ private final PageChannel _pageChannel = new PageChannel(true) {};
+ private List<ColumnImpl> _columns = new ArrayList<ColumnImpl>();
+ private TableImpl _testTable;
+ private int _varLenIdx;
+ private int _fixedOffset;
+
public TableTest(String name) {
super(name);
}
+
+ private void reset() {
+ _testTable = null;
+ _columns = new ArrayList<ColumnImpl>();
+ _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
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<Map<String,Object>> i1 = c1.iterator();
- Iterator<Map<String,Object>> i2 = c2.reverseIterable().iterator();
+ Iterator<? extends Map<String,Object>> i1 = c1.iterator();
+ Iterator<? extends Map<String,Object>> 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
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<String,Object> row : c) {
int id = (Integer)row.get("id");
@@ -107,7 +113,7 @@ public class FKEnforcerTest extends TestCase
c.setCurrentRowValue(col, id);
}
- List<Map<String, Object>> expectedRows =
+ List<? extends Map<String, Object>> 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<String,Object> 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
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<String,Object> 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<Character,String[]> inat2CrazyCodes = new TreeMap<Character,String[]>();
- 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<Character,String[]> inat2CrazyCodes = new TreeMap<Character,String[]>();
- 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
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<TestDB> SUPPORTED_DBS_TEST =
+ public static final List<TestDB> SUPPORTED_DBS_TEST =
TestDB.getSupportedForBasename(Basename.TEST);
- static final List<TestDB> SUPPORTED_DBS_TEST_FOR_READ =
+ public static final List<TestDB> 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
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
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
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<Row> rowList = new ArrayList<Row>();
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<Row> iter = query.getRows().iterator(); iter.hasNext(); ) {
+ for(Iterator<Row> 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<Row> rows = query.getRows();
+ List<Row> 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
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<Column> cols = (List<Column>)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
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
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<Map<String, Object>> expectedRows =
+ List<? extends Map<String, Object>> 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<Column> columns = t.getColumns();
+ List<? extends Column> 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
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<Integer,List<Map<String,Object>>> expectedData)
+ Joiner join, Map<Integer,List<Row>> expectedData)
throws Exception
{
final Set<String> colNames = new HashSet<String>(
Arrays.asList("id", "data"));
Joiner revJoin = join.createReverse();
- for(Map<String,Object> row : join.getFromTable()) {
+ for(Row row : join.getFromTable()) {
Integer id = (Integer)row.get("id");
- List<Map<String,Object>> joinedRows =
- new ArrayList<Map<String,Object>>();
- for(Map<String,Object> t1Row : join.findRowsIterable(row)) {
+ List<Row> joinedRows =
+ new ArrayList<Row>();
+ for(Row t1Row : join.findRowsIterable(row)) {
joinedRows.add(t1Row);
}
- List<Map<String,Object>> expectedRows = expectedData.get(id);
+ List<Row> 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<Map<String,Object>> expectedRows2 = new
- ArrayList<Map<String,Object>>();
- for(Map<String,Object> tmpRow : expectedRows) {
- Map<String,Object> tmpRow2 = new HashMap<String,Object>(tmpRow);
+ List<Row> expectedRows2 = new ArrayList<Row>();
+ for(Row tmpRow : expectedRows) {
+ Row tmpRow2 = new RowImpl(tmpRow);
tmpRow2.keySet().retainAll(colNames);
expectedRows2.add(tmpRow2);
}
- joinedRows = new ArrayList<Map<String,Object>>();
- for(Map<String,Object> t1Row : join.findRowsIterable(row, colNames)) {
+ joinedRows = new ArrayList<Row>();
+ 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<String,Object> 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<String,Object> t1Row : t2t1Join.getToTable()) {
+ for(Row t1Row : t2t1Join.getToTable()) {
assertFalse(t1Row.get("otherfk1").equals(1));
}
}
- private static Map<Integer,List<Map<String,Object>>> createT2T1Data()
+ private static Map<Integer,List<Row>> createT2T1Data()
{
- Map<Integer,List<Map<String,Object>>> data = new
- HashMap<Integer,List<Map<String,Object>>>();
+ Map<Integer,List<Row>> data = new
+ HashMap<Integer,List<Row>>();
data.put(0,
createExpectedTable(
@@ -175,10 +179,9 @@ public class JoinerTest extends TestCase {
return data;
}
- private static Map<Integer,List<Map<String,Object>>> createT3T1Data()
+ private static Map<Integer,List<Row>> createT3T1Data()
{
- Map<Integer,List<Map<String,Object>>> data = new
- HashMap<Integer,List<Map<String,Object>>>();
+ Map<Integer,List<Row>> data = new HashMap<Integer,List<Row>>();
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
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
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<String,Object> row0 = createExpectedRow(ID_COL, 0, COL1, "foo", COL2, 13, COL3, "bar");
- Map<String,Object> row1 = createExpectedRow(ID_COL, 1, COL1, "bar", COL2, 42, COL3, null);
- Map<String,Object> row2 = createExpectedRow(ID_COL, 2, COL1, "foo", COL2, 55, COL3, "bar");
- Map<String,Object> row3 = createExpectedRow(ID_COL, 3, COL1, "baz", COL2, 42, COL3, "bar");
- Map<String,Object> row4 = createExpectedRow(ID_COL, 4, COL1, "foo", COL2, 13, COL3, null);
- Map<String,Object> 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<Map<String,Object>> rows = Arrays.asList(row0, row1, row2, row3, row4, row5);
+ List<Row> 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<Map<String,Object>> toList(Iterable<Map<String,Object>> rows)
+ public static List<Row> toList(Iterable<Row> rows)
{
- List<Map<String,Object>> rowList = new ArrayList<Map<String,Object>>();
- for(Map<String,Object> row : rows) {
+ List<Row> rowList = new ArrayList<Row>();
+ for(Row row : rows) {
rowList.add(row);
}
return rowList;