git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/hms@2 f203690c-595d-4dc9-a70b-905162fa7fd2tags/rel_1_0
@@ -0,0 +1,2 @@ | |||
maven.log | |||
target |
@@ -0,0 +1,459 @@ | |||
GNU LESSER GENERAL PUBLIC LICENSE | |||
Version 2.1, February 1999 | |||
Copyright (C) 1991, 1999 Free Software Foundation, Inc. | |||
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | |||
Everyone is permitted to copy and distribute verbatim copies | |||
of this license document, but changing it is not allowed. | |||
[This is the first released version of the Lesser GPL. It also counts | |||
as the successor of the GNU Library Public License, version 2, hence | |||
the version number 2.1.] | |||
Preamble | |||
The licenses for most software are designed to take away your | |||
freedom to share and change it. By contrast, the GNU General Public | |||
Licenses are intended to guarantee your freedom to share and change | |||
free software--to make sure the software is free for all its users. | |||
This license, the Lesser General Public License, applies to some | |||
specially designated software packages--typically libraries--of the | |||
Free Software Foundation and other authors who decide to use it. You | |||
can use it too, but we suggest you first think carefully about whether | |||
this license or the ordinary General Public License is the better | |||
strategy to use in any particular case, based on the explanations below. | |||
When we speak of free software, we are referring to freedom of use, | |||
not price. Our General Public Licenses are designed to make sure that | |||
you have the freedom to distribute copies of free software (and charge | |||
for this service if you wish); that you receive source code or can get | |||
it if you want it; that you can change the software and use pieces of | |||
it in new free programs; and that you are informed that you can do | |||
these things. | |||
To protect your rights, we need to make restrictions that forbid | |||
distributors to deny you these rights or to ask you to surrender these | |||
rights. These restrictions translate to certain responsibilities for | |||
you if you distribute copies of the library or if you modify it. | |||
For example, if you distribute copies of the library, whether gratis | |||
or for a fee, you must give the recipients all the rights that we gave | |||
you. You must make sure that they, too, receive or can get the source | |||
code. If you link other code with the library, you must provide | |||
complete object files to the recipients, so that they can relink them | |||
with the library after making changes to the library and recompiling | |||
it. And you must show them these terms so they know their rights. | |||
We protect your rights with a two-step method: (1) we copyright the | |||
library, and (2) we offer you this license, which gives you legal | |||
permission to copy, distribute and/or modify the library. | |||
To protect each distributor, we want to make it very clear that | |||
there is no warranty for the free library. Also, if the library is | |||
modified by someone else and passed on, the recipients should know | |||
that what they have is not the original version, so that the original | |||
author's reputation will not be affected by problems that might be | |||
introduced by others. | |||
Finally, software patents pose a constant threat to the existence of | |||
any free program. We wish to make sure that a company cannot | |||
effectively restrict the users of a free program by obtaining a | |||
restrictive license from a patent holder. Therefore, we insist that | |||
any patent license obtained for a version of the library must be | |||
consistent with the full freedom of use specified in this license. | |||
Most GNU software, including some libraries, is covered by the | |||
ordinary GNU General Public License. This license, the GNU Lesser | |||
General Public License, applies to certain designated libraries, and | |||
is quite different from the ordinary General Public License. We use | |||
this license for certain libraries in order to permit linking those | |||
libraries into non-free programs. | |||
When a program is linked with a library, whether statically or using | |||
a shared library, the combination of the two is legally speaking a | |||
combined work, a derivative of the original library. The ordinary | |||
General Public License therefore permits such linking only if the | |||
entire combination fits its criteria of freedom. The Lesser General | |||
Public License permits more lax criteria for linking other code with | |||
the library. | |||
We call this license the "Lesser" General Public License because it | |||
does Less to protect the user's freedom than the ordinary General | |||
Public License. It also provides other free software developers Less | |||
of an advantage over competing non-free programs. These disadvantages | |||
are the reason we use the ordinary General Public License for many | |||
libraries. However, the Lesser license provides advantages in certain | |||
special circumstances. | |||
For example, on rare occasions, there may be a special need to | |||
encourage the widest possible use of a certain library, so that it becomes | |||
a de-facto standard. To achieve this, non-free programs must be | |||
allowed to use the library. A more frequent case is that a free | |||
library does the same job as widely used non-free libraries. In this | |||
case, there is little to gain by limiting the free library to free | |||
software only, so we use the Lesser General Public License. | |||
In other cases, permission to use a particular library in non-free | |||
programs enables a greater number of people to use a large body of | |||
free software. For example, permission to use the GNU C Library in | |||
non-free programs enables many more people to use the whole GNU | |||
operating system, as well as its variant, the GNU/Linux operating | |||
system. | |||
Although the Lesser General Public License is Less protective of the | |||
users' freedom, it does ensure that the user of a program that is | |||
linked with the Library has the freedom and the wherewithal to run | |||
that program using a modified version of the Library. | |||
The precise terms and conditions for copying, distribution and | |||
modification follow. Pay close attention to the difference between a | |||
"work based on the library" and a "work that uses the library". The | |||
former contains code derived from the library, whereas the latter must | |||
be combined with the library in order to run. | |||
GNU LESSER GENERAL PUBLIC LICENSE | |||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION | |||
0. This License Agreement applies to any software library or other | |||
program which contains a notice placed by the copyright holder or | |||
other authorized party saying it may be distributed under the terms of | |||
this Lesser General Public License (also called "this License"). | |||
Each licensee is addressed as "you". | |||
A "library" means a collection of software functions and/or data | |||
prepared so as to be conveniently linked with application programs | |||
(which use some of those functions and data) to form executables. | |||
The "Library", below, refers to any such software library or work | |||
which has been distributed under these terms. A "work based on the | |||
Library" means either the Library or any derivative work under | |||
copyright law: that is to say, a work containing the Library or a | |||
portion of it, either verbatim or with modifications and/or translated | |||
straightforwardly into another language. (Hereinafter, translation is | |||
included without limitation in the term "modification".) | |||
"Source code" for a work means the preferred form of the work for | |||
making modifications to it. For a library, complete source code means | |||
all the source code for all modules it contains, plus any associated | |||
interface definition files, plus the scripts used to control compilation | |||
and installation of the library. | |||
Activities other than copying, distribution and modification are not | |||
covered by this License; they are outside its scope. The act of | |||
running a program using the Library is not restricted, and output from | |||
such a program is covered only if its contents constitute a work based | |||
on the Library (independent of the use of the Library in a tool for | |||
writing it). Whether that is true depends on what the Library does | |||
and what the program that uses the Library does. | |||
1. You may copy and distribute verbatim copies of the Library's | |||
complete source code as you receive it, in any medium, provided that | |||
you conspicuously and appropriately publish on each copy an | |||
appropriate copyright notice and disclaimer of warranty; keep intact | |||
all the notices that refer to this License and to the absence of any | |||
warranty; and distribute a copy of this License along with the | |||
Library. | |||
You may charge a fee for the physical act of transferring a copy, | |||
and you may at your option offer warranty protection in exchange for a | |||
fee. | |||
2. You may modify your copy or copies of the Library or any portion | |||
of it, thus forming a work based on the Library, and copy and | |||
distribute such modifications or work under the terms of Section 1 | |||
above, provided that you also meet all of these conditions: | |||
a) The modified work must itself be a software library. | |||
b) You must cause the files modified to carry prominent notices | |||
stating that you changed the files and the date of any change. | |||
c) You must cause the whole of the work to be licensed at no | |||
charge to all third parties under the terms of this License. | |||
d) If a facility in the modified Library refers to a function or a | |||
table of data to be supplied by an application program that uses | |||
the facility, other than as an argument passed when the facility | |||
is invoked, then you must make a good faith effort to ensure that, | |||
in the event an application does not supply such function or | |||
table, the facility still operates, and performs whatever part of | |||
its purpose remains meaningful. | |||
(For example, a function in a library to compute square roots has | |||
a purpose that is entirely well-defined independent of the | |||
application. Therefore, Subsection 2d requires that any | |||
application-supplied function or table used by this function must | |||
be optional: if the application does not supply it, the square | |||
root function must still compute square roots.) | |||
These requirements apply to the modified work as a whole. If | |||
identifiable sections of that work are not derived from the Library, | |||
and can be reasonably considered independent and separate works in | |||
themselves, then this License, and its terms, do not apply to those | |||
sections when you distribute them as separate works. But when you | |||
distribute the same sections as part of a whole which is a work based | |||
on the Library, the distribution of the whole must be on the terms of | |||
this License, whose permissions for other licensees extend to the | |||
entire whole, and thus to each and every part regardless of who wrote | |||
it. | |||
Thus, it is not the intent of this section to claim rights or contest | |||
your rights to work written entirely by you; rather, the intent is to | |||
exercise the right to control the distribution of derivative or | |||
collective works based on the Library. | |||
In addition, mere aggregation of another work not based on the Library | |||
with the Library (or with a work based on the Library) on a volume of | |||
a storage or distribution medium does not bring the other work under | |||
the scope of this License. | |||
3. You may opt to apply the terms of the ordinary GNU General Public | |||
License instead of this License to a given copy of the Library. To do | |||
this, you must alter all the notices that refer to this License, so | |||
that they refer to the ordinary GNU General Public License, version 2, | |||
instead of to this License. (If a newer version than version 2 of the | |||
ordinary GNU General Public License has appeared, then you can specify | |||
that version instead if you wish.) Do not make any other change in | |||
these notices. | |||
Once this change is made in a given copy, it is irreversible for | |||
that copy, so the ordinary GNU General Public License applies to all | |||
subsequent copies and derivative works made from that copy. | |||
This option is useful when you wish to copy part of the code of | |||
the Library into a program that is not a library. | |||
4. You may copy and distribute the Library (or a portion or | |||
derivative of it, under Section 2) in object code or executable form | |||
under the terms of Sections 1 and 2 above provided that you accompany | |||
it with the complete corresponding machine-readable source code, which | |||
must be distributed under the terms of Sections 1 and 2 above on a | |||
medium customarily used for software interchange. | |||
If distribution of object code is made by offering access to copy | |||
from a designated place, then offering equivalent access to copy the | |||
source code from the same place satisfies the requirement to | |||
distribute the source code, even though third parties are not | |||
compelled to copy the source along with the object code. | |||
5. A program that contains no derivative of any portion of the | |||
Library, but is designed to work with the Library by being compiled or | |||
linked with it, is called a "work that uses the Library". Such a | |||
work, in isolation, is not a derivative work of the Library, and | |||
therefore falls outside the scope of this License. | |||
However, linking a "work that uses the Library" with the Library | |||
creates an executable that is a derivative of the Library (because it | |||
contains portions of the Library), rather than a "work that uses the | |||
library". The executable is therefore covered by this License. | |||
Section 6 states terms for distribution of such executables. | |||
When a "work that uses the Library" uses material from a header file | |||
that is part of the Library, the object code for the work may be a | |||
derivative work of the Library even though the source code is not. | |||
Whether this is true is especially significant if the work can be | |||
linked without the Library, or if the work is itself a library. The | |||
threshold for this to be true is not precisely defined by law. | |||
If such an object file uses only numerical parameters, data | |||
structure layouts and accessors, and small macros and small inline | |||
functions (ten lines or less in length), then the use of the object | |||
file is unrestricted, regardless of whether it is legally a derivative | |||
work. (Executables containing this object code plus portions of the | |||
Library will still fall under Section 6.) | |||
Otherwise, if the work is a derivative of the Library, you may | |||
distribute the object code for the work under the terms of Section 6. | |||
Any executables containing that work also fall under Section 6, | |||
whether or not they are linked directly with the Library itself. | |||
6. As an exception to the Sections above, you may also combine or | |||
link a "work that uses the Library" with the Library to produce a | |||
work containing portions of the Library, and distribute that work | |||
under terms of your choice, provided that the terms permit | |||
modification of the work for the customer's own use and reverse | |||
engineering for debugging such modifications. | |||
You must give prominent notice with each copy of the work that the | |||
Library is used in it and that the Library and its use are covered by | |||
this License. You must supply a copy of this License. If the work | |||
during execution displays copyright notices, you must include the | |||
copyright notice for the Library among them, as well as a reference | |||
directing the user to the copy of this License. Also, you must do one | |||
of these things: | |||
a) Accompany the work with the complete corresponding | |||
machine-readable source code for the Library including whatever | |||
changes were used in the work (which must be distributed under | |||
Sections 1 and 2 above); and, if the work is an executable linked | |||
with the Library, with the complete machine-readable "work that | |||
uses the Library", as object code and/or source code, so that the | |||
user can modify the Library and then relink to produce a modified | |||
executable containing the modified Library. (It is understood | |||
that the user who changes the contents of definitions files in the | |||
Library will not necessarily be able to recompile the application | |||
to use the modified definitions.) | |||
b) Use a suitable shared library mechanism for linking with the | |||
Library. A suitable mechanism is one that (1) uses at run time a | |||
copy of the library already present on the user's computer system, | |||
rather than copying library functions into the executable, and (2) | |||
will operate properly with a modified version of the library, if | |||
the user installs one, as long as the modified version is | |||
interface-compatible with the version that the work was made with. | |||
c) Accompany the work with a written offer, valid for at | |||
least three years, to give the same user the materials | |||
specified in Subsection 6a, above, for a charge no more | |||
than the cost of performing this distribution. | |||
d) If distribution of the work is made by offering access to copy | |||
from a designated place, offer equivalent access to copy the above | |||
specified materials from the same place. | |||
e) Verify that the user has already received a copy of these | |||
materials or that you have already sent this user a copy. | |||
For an executable, the required form of the "work that uses the | |||
Library" must include any data and utility programs needed for | |||
reproducing the executable from it. However, as a special exception, | |||
the materials to be distributed need not include anything that is | |||
normally distributed (in either source or binary form) with the major | |||
components (compiler, kernel, and so on) of the operating system on | |||
which the executable runs, unless that component itself accompanies | |||
the executable. | |||
It may happen that this requirement contradicts the license | |||
restrictions of other proprietary libraries that do not normally | |||
accompany the operating system. Such a contradiction means you cannot | |||
use both them and the Library together in an executable that you | |||
distribute. | |||
7. You may place library facilities that are a work based on the | |||
Library side-by-side in a single library together with other library | |||
facilities not covered by this License, and distribute such a combined | |||
library, provided that the separate distribution of the work based on | |||
the Library and of the other library facilities is otherwise | |||
permitted, and provided that you do these two things: | |||
a) Accompany the combined library with a copy of the same work | |||
based on the Library, uncombined with any other library | |||
facilities. This must be distributed under the terms of the | |||
Sections above. | |||
b) Give prominent notice with the combined library of the fact | |||
that part of it is a work based on the Library, and explaining | |||
where to find the accompanying uncombined form of the same work. | |||
8. You may not copy, modify, sublicense, link with, or distribute | |||
the Library except as expressly provided under this License. Any | |||
attempt otherwise to copy, modify, sublicense, link with, or | |||
distribute the Library is void, and will automatically terminate your | |||
rights under this License. However, parties who have received copies, | |||
or rights, from you under this License will not have their licenses | |||
terminated so long as such parties remain in full compliance. | |||
9. You are not required to accept this License, since you have not | |||
signed it. However, nothing else grants you permission to modify or | |||
distribute the Library or its derivative works. These actions are | |||
prohibited by law if you do not accept this License. Therefore, by | |||
modifying or distributing the Library (or any work based on the | |||
Library), you indicate your acceptance of this License to do so, and | |||
all its terms and conditions for copying, distributing or modifying | |||
the Library or works based on it. | |||
10. Each time you redistribute the Library (or any work based on the | |||
Library), the recipient automatically receives a license from the | |||
original licensor to copy, distribute, link with or modify the Library | |||
subject to these terms and conditions. You may not impose any further | |||
restrictions on the recipients' exercise of the rights granted herein. | |||
You are not responsible for enforcing compliance by third parties with | |||
this License. | |||
11. If, as a consequence of a court judgment or allegation of patent | |||
infringement or for any other reason (not limited to patent issues), | |||
conditions are imposed on you (whether by court order, agreement or | |||
otherwise) that contradict the conditions of this License, they do not | |||
excuse you from the conditions of this License. If you cannot | |||
distribute so as to satisfy simultaneously your obligations under this | |||
License and any other pertinent obligations, then as a consequence you | |||
may not distribute the Library at all. For example, if a patent | |||
license would not permit royalty-free redistribution of the Library by | |||
all those who receive copies directly or indirectly through you, then | |||
the only way you could satisfy both it and this License would be to | |||
refrain entirely from distribution of the Library. | |||
If any portion of this section is held invalid or unenforceable under any | |||
particular circumstance, the balance of the section is intended to apply, | |||
and the section as a whole is intended to apply in other circumstances. | |||
It is not the purpose of this section to induce you to infringe any | |||
patents or other property right claims or to contest validity of any | |||
such claims; this section has the sole purpose of protecting the | |||
integrity of the free software distribution system which is | |||
implemented by public license practices. Many people have made | |||
generous contributions to the wide range of software distributed | |||
through that system in reliance on consistent application of that | |||
system; it is up to the author/donor to decide if he or she is willing | |||
to distribute software through any other system and a licensee cannot | |||
impose that choice. | |||
This section is intended to make thoroughly clear what is believed to | |||
be a consequence of the rest of this License. | |||
12. If the distribution and/or use of the Library is restricted in | |||
certain countries either by patents or by copyrighted interfaces, the | |||
original copyright holder who places the Library under this License may add | |||
an explicit geographical distribution limitation excluding those countries, | |||
so that distribution is permitted only in or among countries not thus | |||
excluded. In such case, this License incorporates the limitation as if | |||
written in the body of this License. | |||
13. The Free Software Foundation may publish revised and/or new | |||
versions of the Lesser General Public License from time to time. | |||
Such new versions will be similar in spirit to the present version, | |||
but may differ in detail to address new problems or concerns. | |||
Each version is given a distinguishing version number. If the Library | |||
specifies a version number of this License which applies to it and | |||
"any later version", you have the option of following the terms and | |||
conditions either of that version or of any later version published by | |||
the Free Software Foundation. If the Library does not specify a | |||
license version number, you may choose any version ever published by | |||
the Free Software Foundation. | |||
14. If you wish to incorporate parts of the Library into other free | |||
programs whose distribution conditions are incompatible with these, | |||
write to the author to ask for permission. For software which is | |||
copyrighted by the Free Software Foundation, write to the Free | |||
Software Foundation; we sometimes make exceptions for this. Our | |||
decision will be guided by the two goals of preserving the free status | |||
of all derivatives of our free software and of promoting the sharing | |||
and reuse of software generally. | |||
NO WARRANTY | |||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO | |||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. | |||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR | |||
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY | |||
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE | |||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE | |||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME | |||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. | |||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN | |||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY | |||
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU | |||
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR | |||
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE | |||
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING | |||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A | |||
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF | |||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH | |||
DAMAGES. | |||
END OF TERMS AND CONDITIONS | |||
@@ -0,0 +1,18 @@ | |||
maven.artifact.legacy=false | |||
maven.changes.issue.template=http://sf.net/tracker/index.php?func=detail&aid=%ISSUE%&group_id=134943&atid=731445 | |||
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.sourceforge.project.groupId=134943 | |||
maven.sourceforge.username=javajedi | |||
maven.test.source=1.5 | |||
log4j.configuration=com/hmsonline/common/access/log4j.properties | |||
statcvs.include=**/*.java;**/*.xml |
@@ -0,0 +1,97 @@ | |||
<?xml version="1.0" encoding="ISO-8859-1"?> | |||
<project> | |||
<pomVersion>1</pomVersion> | |||
<id>jackcess</id> | |||
<name>Jackcess</name> | |||
<currentVersion>1.0</currentVersion> | |||
<organization> | |||
<name>Health Market Science, Inc.</name> | |||
<url>http://www.healthmarketscience.com</url> | |||
<logo>http://www.healthmarketscience.com/images/logo_red.jpg</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&atid=731445</issueTrackingUrl> | |||
<siteAddress>jackcess.sf.net</siteAddress> | |||
<siteDirectory>/home/groups/j/ja/jackcess/htdocs</siteDirectory> | |||
<repository> | |||
<connection>scm:cvs:pserver:anonymous@cvs.sf.net:/cvsroot/jackcess:jackcess</connection> | |||
<url>http://cvs.sf.net/viewcvs.py/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> | |||
</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.1</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-javadoc-plugin</report> | |||
<report>maven-jxr-plugin</report> | |||
<report>maven-jdepend-plugin</report> | |||
<report>maven-statcvs-plugin</report> | |||
</reports> | |||
</project> |
@@ -0,0 +1,114 @@ | |||
/* | |||
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; | |||
import java.nio.ByteBuffer; | |||
/** | |||
* Byte manipulation and display utilities | |||
* @author Tim McCune | |||
*/ | |||
public final class ByteUtil { | |||
private static final String[] HEX_CHARS = new String[] { | |||
"0", "1", "2", "3", "4", "5", "6", "7", | |||
"8", "9", "A", "B", "C", "D", "E", "F"}; | |||
private ByteUtil() {} | |||
/** | |||
* Convert an int from 4 bytes to 3 | |||
* @param i Int to convert | |||
* @return Array of 3 bytes in little-endian order | |||
*/ | |||
public static byte[] to3ByteInt(int i) { | |||
byte[] rtn = new byte[3]; | |||
rtn[0] = (byte) (i & 0xFF); | |||
rtn[1] = (byte) ((i >>> 8) & 0xFF); | |||
rtn[2] = (byte) ((i >>> 16) & 0xFF); | |||
return rtn; | |||
} | |||
/** | |||
* Read a 3 byte int from a buffer in little-endian order | |||
* @param buffer Buffer containing the bytes | |||
* @param offset Offset at which to start reading the int | |||
* @return The int | |||
*/ | |||
public static int get3ByteInt(ByteBuffer buffer, int offset) { | |||
int rtn = buffer.get(offset) & 0xff; | |||
rtn += ((((int) buffer.get(offset + 1)) & 0xFF) << 8); | |||
rtn += ((((int) buffer.get(offset + 2)) & 0xFF) << 16); | |||
rtn &= 16777215; //2 ^ (8 * 3) - 1 | |||
return rtn; | |||
} | |||
/** | |||
* Convert a byte buffer to a hexadecimal string for display | |||
* @param buffer Buffer to display, starting at offset 0 | |||
* @param size Number of bytes to read from the buffer | |||
* @return The display String | |||
*/ | |||
public static String toHexString(ByteBuffer buffer, int size) { | |||
return toHexString(buffer, 0, size); | |||
} | |||
/** | |||
* Convert a byte buffer to a hexadecimal string for display | |||
* @param buffer Buffer to display, starting at offset 0 | |||
* @param offset Offset at which to start reading the buffer | |||
* @param size Number of bytes to read from the buffer | |||
* @return The display String | |||
*/ | |||
public static String toHexString(ByteBuffer buffer, int offset, int size) { | |||
StringBuffer rtn = new StringBuffer(); | |||
int position = buffer.position(); | |||
buffer.position(offset); | |||
for (int i = 0; i < size; i++) { | |||
byte b = buffer.get(); | |||
byte h = (byte) (b & 0xF0); | |||
h = (byte) (h >>> 4); | |||
h = (byte) (h & 0x0F); | |||
rtn.append(HEX_CHARS[(int) h]); | |||
h = (byte) (b & 0x0F); | |||
rtn.append(HEX_CHARS[(int) h] + " "); | |||
if ((i + 1) % 4 == 0) { | |||
rtn.append(" "); | |||
} | |||
if ((i + 1) % 24 == 0) { | |||
rtn.append("\n"); | |||
} | |||
} | |||
buffer.position(position); | |||
return rtn.toString(); | |||
} | |||
} |
@@ -0,0 +1,549 @@ | |||
/* | |||
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; | |||
import java.io.IOException; | |||
import java.nio.ByteBuffer; | |||
import java.nio.ByteOrder; | |||
import java.nio.CharBuffer; | |||
import java.sql.SQLException; | |||
import java.util.Calendar; | |||
import java.util.Date; | |||
import java.util.Iterator; | |||
import java.util.List; | |||
import java.util.TimeZone; | |||
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 | |||
*/ | |||
public class Column implements Comparable { | |||
private static final Log LOG = LogFactory.getLog(Column.class); | |||
/** | |||
* Access starts counting dates at Jan 1, 1900. Java starts counting | |||
* at Jan 1, 1970. This is the # of days between them for conversion. | |||
*/ | |||
private static final double DAYS_BETWEEN_EPOCH_AND_1900 = 25569d; | |||
/** | |||
* Access stores numeric dates in days. Java stores them in milliseconds. | |||
*/ | |||
private static final double MILLISECONDS_PER_DAY = 86400000d; | |||
/** | |||
* Long value (LVAL) type that indicates that the value is stored on the same page | |||
*/ | |||
private static final short LONG_VALUE_TYPE_THIS_PAGE = (short) 0x8000; | |||
/** | |||
* Long value (LVAL) type that indicates that the value is stored on another page | |||
*/ | |||
private static final short LONG_VALUE_TYPE_OTHER_PAGE = (short) 0x4000; | |||
/** | |||
* Long value (LVAL) type that indicates that the value is stored on multiple other pages | |||
*/ | |||
private static final short LONG_VALUE_TYPE_OTHER_PAGES = (short) 0x0; | |||
/** For text columns, whether or not they are compressed */ | |||
private boolean _compressedUnicode = false; | |||
/** Whether or not the column is of variable length */ | |||
private boolean _variableLength; | |||
/** Numeric precision */ | |||
private byte _precision; | |||
/** Numeric scale */ | |||
private byte _scale; | |||
/** Data type */ | |||
private byte _type; | |||
/** Format that the containing database is in */ | |||
private JetFormat _format; | |||
/** Used to read in LVAL pages */ | |||
private PageChannel _pageChannel; | |||
/** Maximum column length */ | |||
private short _columnLength; | |||
/** 0-based column number */ | |||
private short _columnNumber; | |||
/** Column name */ | |||
private String _name; | |||
public Column() { | |||
this(JetFormat.VERSION_4); | |||
} | |||
public Column(JetFormat format) { | |||
_format = format; | |||
} | |||
/** | |||
* Read a column definition in from a buffer | |||
* @param buffer Buffer containing column definition | |||
* @param offset Offset in the buffer at which the column definition starts | |||
* @param format Format that the containing database is in | |||
*/ | |||
public Column(ByteBuffer buffer, int offset, PageChannel pageChannel, JetFormat format) { | |||
if (LOG.isDebugEnabled()) { | |||
LOG.debug("Column def block:\n" + ByteUtil.toHexString(buffer, offset, 25)); | |||
} | |||
_pageChannel = pageChannel; | |||
_format = format; | |||
setType(buffer.get(offset + format.OFFSET_COLUMN_TYPE)); | |||
_columnNumber = buffer.getShort(offset + format.OFFSET_COLUMN_NUMBER); | |||
_columnLength = buffer.getShort(offset + format.OFFSET_COLUMN_LENGTH); | |||
if (_type == DataTypes.NUMERIC) { | |||
_precision = buffer.get(offset + format.OFFSET_COLUMN_PRECISION); | |||
_scale = buffer.get(offset + format.OFFSET_COLUMN_SCALE); | |||
} | |||
_variableLength = ((buffer.get(offset + format.OFFSET_COLUMN_VARIABLE) | |||
& 1) != 1); | |||
_compressedUnicode = ((buffer.get(offset + | |||
format.OFFSET_COLUMN_COMPRESSED_UNICODE) & 1) == 1); | |||
} | |||
public String getName() { | |||
return _name; | |||
} | |||
public void setName(String name) { | |||
_name = name; | |||
} | |||
public boolean isVariableLength() { | |||
return _variableLength; | |||
} | |||
public void setVariableLength(boolean variableLength) { | |||
_variableLength = variableLength; | |||
} | |||
public short getColumnNumber() { | |||
return _columnNumber; | |||
} | |||
/** | |||
* Also sets the length and the variable length flag, inferred from the type | |||
*/ | |||
public void setType(byte type) { | |||
_type = type; | |||
setLength((short) size()); | |||
switch (type) { | |||
case DataTypes.BOOLEAN: | |||
case DataTypes.BYTE: | |||
case DataTypes.INT: | |||
case DataTypes.LONG: | |||
case DataTypes.DOUBLE: | |||
case DataTypes.FLOAT: | |||
case DataTypes.SHORT_DATE_TIME: | |||
setVariableLength(false); | |||
break; | |||
case DataTypes.BINARY: | |||
case DataTypes.TEXT: | |||
setVariableLength(true); | |||
break; | |||
} | |||
} | |||
public byte getType() { | |||
return _type; | |||
} | |||
public int getSQLType() throws SQLException { | |||
return DataTypes.toSQLType(_type); | |||
} | |||
public void setSQLType(int type) throws SQLException { | |||
setType(DataTypes.fromSQLType(type)); | |||
} | |||
public boolean isCompressedUnicode() { | |||
return _compressedUnicode; | |||
} | |||
public byte getPrecision() { | |||
return _precision; | |||
} | |||
public byte getScale() { | |||
return _scale; | |||
} | |||
public void setLength(short length) { | |||
_columnLength = length; | |||
} | |||
public short getLength() { | |||
return _columnLength; | |||
} | |||
/** | |||
* Deserialize a raw byte value for this column into an Object | |||
* @param data The raw byte value | |||
* @return The deserialized Object | |||
*/ | |||
public Object read(byte[] data) throws IOException { | |||
return read(data, ByteOrder.LITTLE_ENDIAN); | |||
} | |||
/** | |||
* 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 | |||
*/ | |||
public Object read(byte[] data, ByteOrder order) throws IOException { | |||
ByteBuffer buffer = ByteBuffer.wrap(data); | |||
buffer.order(order); | |||
switch (_type) { | |||
case DataTypes.BOOLEAN: | |||
throw new IOException("Tried to read a boolean from data instead of null mask."); | |||
case DataTypes.BYTE: | |||
return new Byte(buffer.get()); | |||
case DataTypes.INT: | |||
return new Short(buffer.getShort()); | |||
case DataTypes.LONG: | |||
return new Integer(buffer.getInt()); | |||
case DataTypes.DOUBLE: | |||
return new Double(buffer.getDouble()); | |||
case DataTypes.FLOAT: | |||
return new Float(buffer.getFloat()); | |||
case DataTypes.SHORT_DATE_TIME: | |||
long time = (long) ((buffer.getDouble() - DAYS_BETWEEN_EPOCH_AND_1900) * | |||
MILLISECONDS_PER_DAY); | |||
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); | |||
cal.setTimeInMillis(time); | |||
//Not sure why we're off by 1... | |||
cal.add(Calendar.DATE, 1); | |||
return cal.getTime(); | |||
case DataTypes.BINARY: | |||
return data; | |||
case DataTypes.TEXT: | |||
if (_compressedUnicode) { | |||
try { | |||
String rtn = new Expand().expand(data); | |||
//SCSU expander isn't handling the UTF-8-looking 2-byte combo that | |||
//prepends some of these strings. Rather than dig into that code, | |||
//I'm just stripping them off here. However, this is probably not | |||
//a great idea. | |||
if (rtn.length() > 2 && (int) rtn.charAt(0) == 255 && | |||
(int) rtn.charAt(1) == 254) | |||
{ | |||
rtn = rtn.substring(2); | |||
} | |||
//It also isn't handling short strings. | |||
if (rtn.length() > 1 && (int) rtn.charAt(1) == 0) { | |||
char[] fixed = new char[rtn.length() / 2]; | |||
for (int i = 0; i < fixed.length; i ++) { | |||
fixed[i] = rtn.charAt(i * 2); | |||
} | |||
rtn = new String(fixed); | |||
} | |||
return rtn; | |||
} catch (IllegalInputException e) { | |||
throw new IOException("Can't expand text column"); | |||
} catch (EndOfInputException e) { | |||
throw new IOException("Can't expand text column"); | |||
} | |||
} else { | |||
return _format.CHARSET.decode(ByteBuffer.wrap(data)).toString(); | |||
} | |||
case DataTypes.MONEY: | |||
//XXX | |||
return null; | |||
case DataTypes.OLE: | |||
if (data.length > 0) { | |||
return getLongValue(data); | |||
} else { | |||
return null; | |||
} | |||
case DataTypes.MEMO: | |||
if (data.length > 0) { | |||
return _format.CHARSET.decode(ByteBuffer.wrap(getLongValue(data))).toString(); | |||
} else { | |||
return null; | |||
} | |||
case DataTypes.NUMERIC: | |||
//XXX | |||
return null; | |||
case DataTypes.UNKNOWN_0D: | |||
case DataTypes.GUID: | |||
return null; | |||
default: | |||
throw new IOException("Unrecognized data type: " + _type); | |||
} | |||
} | |||
/** | |||
* @param lvalDefinition Column value that points to an LVAL record | |||
* @return The LVAL data | |||
*/ | |||
private byte[] getLongValue(byte[] lvalDefinition) throws IOException { | |||
ByteBuffer def = ByteBuffer.wrap(lvalDefinition); | |||
def.order(ByteOrder.LITTLE_ENDIAN); | |||
short length = def.getShort(); | |||
byte[] rtn = new byte[length]; | |||
short type = def.getShort(); | |||
switch (type) { | |||
case LONG_VALUE_TYPE_OTHER_PAGE: | |||
if (lvalDefinition.length != _format.SIZE_LONG_VALUE_DEF) { | |||
throw new IOException("Expected " + _format.SIZE_LONG_VALUE_DEF + | |||
" bytes in long value definition, but found " + lvalDefinition.length); | |||
} | |||
byte rowNum = def.get(); | |||
int pageNum = ByteUtil.get3ByteInt(def, def.position()); | |||
ByteBuffer lvalPage = _pageChannel.createPageBuffer(); | |||
_pageChannel.readPage(lvalPage, pageNum); | |||
short offset = lvalPage.getShort(14 + | |||
rowNum * _format.SIZE_ROW_LOCATION); | |||
lvalPage.position(offset); | |||
lvalPage.get(rtn); | |||
break; | |||
case LONG_VALUE_TYPE_THIS_PAGE: | |||
def.getLong(); //Skip over lval_dp and unknown | |||
def.get(rtn); | |||
case LONG_VALUE_TYPE_OTHER_PAGES: | |||
//XXX | |||
return null; | |||
default: | |||
throw new IOException("Unrecognized long value type: " + type); | |||
} | |||
return rtn; | |||
} | |||
/** | |||
* Write an LVAL column into a ByteBuffer inline (LONG_VALUE_TYPE_THIS_PAGE) | |||
* @param value Value of the LVAL column | |||
* @return A buffer containing the LVAL definition and the column value | |||
*/ | |||
public ByteBuffer writeLongValue(byte[] value) throws IOException { | |||
ByteBuffer def = ByteBuffer.allocate(_format.SIZE_LONG_VALUE_DEF + value.length); | |||
def.order(ByteOrder.LITTLE_ENDIAN); | |||
def.putShort((short) value.length); | |||
def.putShort(LONG_VALUE_TYPE_THIS_PAGE); | |||
def.putInt(0); | |||
def.putInt(0); //Unknown | |||
def.put(value); | |||
def.flip(); | |||
return def; | |||
} | |||
/** | |||
* Write an LVAL column into a ByteBuffer on another page | |||
* (LONG_VALUE_TYPE_OTHER_PAGE) | |||
* @param value Value of the LVAL column | |||
* @return A buffer containing the LVAL definition | |||
*/ | |||
public ByteBuffer writeLongValueInNewPage(byte[] value) throws IOException { | |||
ByteBuffer lvalPage = _pageChannel.createPageBuffer(); | |||
lvalPage.put(PageTypes.DATA); //Page type | |||
lvalPage.put((byte) 1); //Unknown | |||
lvalPage.putShort((short) (_format.PAGE_SIZE - | |||
_format.OFFSET_LVAL_ROW_LOCATION_BLOCK - _format.SIZE_ROW_LOCATION - | |||
value.length)); //Free space | |||
lvalPage.put((byte) 'L'); | |||
lvalPage.put((byte) 'V'); | |||
lvalPage.put((byte) 'A'); | |||
lvalPage.put((byte) 'L'); | |||
int offset = _format.PAGE_SIZE - value.length; | |||
lvalPage.position(14); | |||
lvalPage.putShort((short) offset); | |||
lvalPage.position(offset); | |||
lvalPage.put(value); | |||
ByteBuffer def = ByteBuffer.allocate(_format.SIZE_LONG_VALUE_DEF); | |||
def.order(ByteOrder.LITTLE_ENDIAN); | |||
def.putShort((short) value.length); | |||
def.putShort(LONG_VALUE_TYPE_OTHER_PAGE); | |||
def.put((byte) 0); //Row number | |||
def.put(ByteUtil.to3ByteInt(_pageChannel.writeNewPage(lvalPage))); //Page # | |||
def.putInt(0); //Unknown | |||
def.flip(); | |||
return def; | |||
} | |||
/** | |||
* 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 | |||
*/ | |||
public ByteBuffer write(Object obj) throws IOException { | |||
return write(obj, ByteOrder.LITTLE_ENDIAN); | |||
} | |||
/** | |||
* 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 | |||
*/ | |||
public ByteBuffer write(Object obj, ByteOrder order) throws IOException { | |||
int size = size(); | |||
if (_type == DataTypes.OLE || _type == DataTypes.MEMO) { | |||
size += ((byte[]) obj).length; | |||
} | |||
if (_type == DataTypes.TEXT) { | |||
size = getLength(); | |||
} | |||
ByteBuffer buffer = ByteBuffer.allocate(size); | |||
buffer.order(order); | |||
switch (_type) { | |||
case DataTypes.BOOLEAN: | |||
break; | |||
case DataTypes.BYTE: | |||
buffer.put(((Byte) obj).byteValue()); | |||
break; | |||
case DataTypes.INT: | |||
buffer.putShort(((Short) obj).shortValue()); | |||
break; | |||
case DataTypes.LONG: | |||
buffer.putInt(((Integer) obj).intValue()); | |||
break; | |||
case DataTypes.DOUBLE: | |||
buffer.putDouble(((Double) obj).doubleValue()); | |||
break; | |||
case DataTypes.FLOAT: | |||
buffer.putFloat(((Float) obj).floatValue()); | |||
break; | |||
case DataTypes.SHORT_DATE_TIME: | |||
Calendar cal = Calendar.getInstance(); | |||
cal.setTime((Date) obj); | |||
long ms = cal.getTimeInMillis(); | |||
ms += (long) TimeZone.getDefault().getOffset(ms); | |||
buffer.putDouble((double) ms / MILLISECONDS_PER_DAY + | |||
DAYS_BETWEEN_EPOCH_AND_1900); | |||
break; | |||
case DataTypes.BINARY: | |||
buffer.put((byte[]) obj); | |||
break; | |||
case DataTypes.TEXT: | |||
CharSequence text = (CharSequence) obj; | |||
int maxChars = size / 2; | |||
if (text.length() > maxChars) { | |||
text = text.subSequence(0, maxChars); | |||
} | |||
buffer.put(encodeText(text)); | |||
break; | |||
case DataTypes.OLE: | |||
buffer.put(writeLongValue((byte[]) obj)); | |||
break; | |||
case DataTypes.MEMO: | |||
buffer.put(writeLongValue(encodeText((CharSequence) obj).array())); | |||
break; | |||
default: | |||
throw new IOException("Unsupported data type: " + _type); | |||
} | |||
buffer.flip(); | |||
return buffer; | |||
} | |||
/** | |||
* @param text Text to encode | |||
* @return A buffer with the text encoded | |||
*/ | |||
private ByteBuffer encodeText(CharSequence text) { | |||
return _format.CHARSET.encode(CharBuffer.wrap(text)); | |||
} | |||
/** | |||
* @return Number of bytes that should be read for this column | |||
* (applies to fixed-width columns) | |||
*/ | |||
public int size() { | |||
switch (_type) { | |||
case DataTypes.BOOLEAN: | |||
return 0; | |||
case DataTypes.BYTE: | |||
return 1; | |||
case DataTypes.INT: | |||
return 2; | |||
case DataTypes.LONG: | |||
return 4; | |||
case DataTypes.MONEY: | |||
case DataTypes.DOUBLE: | |||
return 8; | |||
case DataTypes.FLOAT: | |||
return 4; | |||
case DataTypes.SHORT_DATE_TIME: | |||
return 8; | |||
case DataTypes.BINARY: | |||
return 255; | |||
case DataTypes.TEXT: | |||
return 50 * 2; | |||
case DataTypes.OLE: | |||
return _format.SIZE_LONG_VALUE_DEF; | |||
case DataTypes.MEMO: | |||
return _format.SIZE_LONG_VALUE_DEF; | |||
case DataTypes.NUMERIC: | |||
throw new IllegalArgumentException("FIX ME"); | |||
case DataTypes.UNKNOWN_0D: | |||
case DataTypes.GUID: | |||
throw new IllegalArgumentException("FIX ME"); | |||
default: | |||
throw new IllegalArgumentException("Unrecognized data type: " + _type); | |||
} | |||
} | |||
public String toString() { | |||
StringBuffer rtn = new StringBuffer(); | |||
rtn.append("\tName: " + _name); | |||
rtn.append("\n\tType: 0x" + Integer.toHexString((int)_type)); | |||
rtn.append("\n\tNumber: " + _columnNumber); | |||
rtn.append("\n\tLength: " + _columnLength); | |||
rtn.append("\n\tVariable length: " + _variableLength); | |||
rtn.append("\n\tCompressed Unicode: " + _compressedUnicode); | |||
rtn.append("\n\n"); | |||
return rtn.toString(); | |||
} | |||
public int compareTo(Object obj) { | |||
Column other = (Column) obj; | |||
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 | |||
*/ | |||
public static short countVariableLength(List columns) { | |||
short rtn = 0; | |||
Iterator iter = columns.iterator(); | |||
while (iter.hasNext()) { | |||
Column col = (Column) iter.next(); | |||
if (col.isVariableLength()) { | |||
rtn++; | |||
} | |||
} | |||
return rtn; | |||
} | |||
} |
@@ -0,0 +1,94 @@ | |||
/* | |||
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; | |||
import java.sql.SQLException; | |||
import java.sql.Types; | |||
import org.apache.commons.collections.bidimap.DualHashBidiMap; | |||
import org.apache.commons.collections.BidiMap; | |||
/** | |||
* Access data types | |||
* @author Tim McCune | |||
*/ | |||
public final class DataTypes { | |||
public static final byte BOOLEAN = 0x01; | |||
public static final byte BYTE = 0x02; | |||
public static final byte INT = 0x03; | |||
public static final byte LONG = 0x04; | |||
public static final byte MONEY = 0x05; | |||
public static final byte FLOAT = 0x06; | |||
public static final byte DOUBLE = 0x07; | |||
public static final byte SHORT_DATE_TIME = 0x08; | |||
public static final byte BINARY = 0x09; | |||
public static final byte TEXT = 0x0A; | |||
public static final byte OLE = 0x0B; | |||
public static final byte MEMO = 0x0C; | |||
public static final byte UNKNOWN_0D = 0x0D; | |||
public static final byte GUID = 0x0F; | |||
public static final byte NUMERIC = 0x10; | |||
/** Map of Access data types to SQL data types */ | |||
private static BidiMap SQL_TYPES = new DualHashBidiMap(); | |||
static { | |||
SQL_TYPES.put(new Byte(BOOLEAN), new Integer(Types.BOOLEAN)); | |||
SQL_TYPES.put(new Byte(BYTE), new Integer(Types.TINYINT)); | |||
SQL_TYPES.put(new Byte(INT), new Integer(Types.SMALLINT)); | |||
SQL_TYPES.put(new Byte(LONG), new Integer(Types.INTEGER)); | |||
SQL_TYPES.put(new Byte(MONEY), new Integer(Types.DECIMAL)); | |||
SQL_TYPES.put(new Byte(FLOAT), new Integer(Types.FLOAT)); | |||
SQL_TYPES.put(new Byte(DOUBLE), new Integer(Types.DOUBLE)); | |||
SQL_TYPES.put(new Byte(SHORT_DATE_TIME), new Integer(Types.TIMESTAMP)); | |||
SQL_TYPES.put(new Byte(BINARY), new Integer(Types.BINARY)); | |||
SQL_TYPES.put(new Byte(TEXT), new Integer(Types.VARCHAR)); | |||
SQL_TYPES.put(new Byte(OLE), new Integer(Types.LONGVARBINARY)); | |||
SQL_TYPES.put(new Byte(MEMO), new Integer(Types.LONGVARCHAR)); | |||
} | |||
private DataTypes() {} | |||
public static int toSQLType(byte dataType) throws SQLException { | |||
Integer i = (Integer) SQL_TYPES.get(new Byte(dataType)); | |||
if (i != null) { | |||
return i.intValue(); | |||
} else { | |||
throw new SQLException("Unsupported data type: " + dataType); | |||
} | |||
} | |||
public static byte fromSQLType(int sqlType) throws SQLException { | |||
Byte b = (Byte) SQL_TYPES.getKey(new Integer(sqlType)); | |||
if (b != null) { | |||
return b.byteValue(); | |||
} else { | |||
throw new SQLException("Unsupported SQL type: " + sqlType); | |||
} | |||
} | |||
} |
@@ -0,0 +1,717 @@ | |||
/* | |||
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; | |||
import java.io.BufferedReader; | |||
import java.io.File; | |||
import java.io.FileNotFoundException; | |||
import java.io.FileReader; | |||
import java.io.IOException; | |||
import java.io.RandomAccessFile; | |||
import java.nio.ByteBuffer; | |||
import java.nio.channels.Channels; | |||
import java.nio.channels.FileChannel; | |||
import java.sql.ResultSet; | |||
import java.sql.ResultSetMetaData; | |||
import java.sql.SQLException; | |||
import java.sql.Types; | |||
import java.util.ArrayList; | |||
import java.util.Arrays; | |||
import java.util.Date; | |||
import java.util.HashMap; | |||
import java.util.HashSet; | |||
import java.util.Iterator; | |||
import java.util.LinkedList; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import org.apache.commons.lang.builder.ToStringBuilder; | |||
import org.apache.commons.logging.Log; | |||
import org.apache.commons.logging.LogFactory; | |||
/** | |||
* An Access database. | |||
* | |||
* @author Tim McCune | |||
*/ | |||
public class Database { | |||
private static final Log LOG = LogFactory.getLog(Database.class); | |||
private static final byte[] SID = new byte[2]; | |||
static { | |||
SID[0] = (byte) 0xA6; | |||
SID[1] = (byte) 0x33; | |||
} | |||
/** Batch commit size for copying other result sets into this database */ | |||
private static final int COPY_TABLE_BATCH_SIZE = 200; | |||
/** System catalog always lives on page 2 */ | |||
private static final int PAGE_SYSTEM_CATALOG = 2; | |||
private static final Integer ACM = new Integer(1048319); | |||
/** Free space left in page for new usage map definition pages */ | |||
private static final short USAGE_MAP_DEF_FREE_SPACE = 3940; | |||
private static final String COL_ACM = "ACM"; | |||
/** System catalog column name of the date a system object was created */ | |||
private static final String COL_DATE_CREATE = "DateCreate"; | |||
/** System catalog column name of the date a system object was updated */ | |||
private static final String COL_DATE_UPDATE = "DateUpdate"; | |||
private static final String COL_F_INHERITABLE = "FInheritable"; | |||
private static final String COL_FLAGS = "Flags"; | |||
/** | |||
* System catalog column name of the page on which system object definitions | |||
* are stored | |||
*/ | |||
private static final String COL_ID = "Id"; | |||
/** System catalog column name of the name of a system object */ | |||
private static final String COL_NAME = "Name"; | |||
private static final String COL_OBJECT_ID = "ObjectId"; | |||
private static final String COL_OWNER = "Owner"; | |||
/** System catalog column name of a system object's parent's id */ | |||
private static final String COL_PARENT_ID = "ParentId"; | |||
private static final String COL_SID = "SID"; | |||
/** System catalog column name of the type of a system object */ | |||
private static final String COL_TYPE = "Type"; | |||
/** Empty database template for creating new databases */ | |||
private static final String EMPTY_MDB = "com/healthmarketscience/jackcess/empty.mdb"; | |||
/** Prefix for column or table names that are reserved words */ | |||
private static final String ESCAPE_PREFIX = "x"; | |||
/** Prefix that flags system tables */ | |||
private static final String PREFIX_SYSTEM = "MSys"; | |||
/** Name of the system object that is the parent of all tables */ | |||
private static final String SYSTEM_OBJECT_NAME_TABLES = "Tables"; | |||
/** Name of the table that contains system access control entries */ | |||
private static final String TABLE_SYSTEM_ACES = "MSysACEs"; | |||
/** System object type for table definitions */ | |||
private static final Short TYPE_TABLE = new Short((short) 1); | |||
/** | |||
* All of the reserved words in Access that should be escaped when creating | |||
* table or column names (String) | |||
*/ | |||
private static final Set RESERVED_WORDS = new HashSet(); | |||
static { | |||
//Yup, there's a lot. | |||
RESERVED_WORDS.addAll(Arrays.asList(new String[] { | |||
"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" | |||
})); | |||
} | |||
/** 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 JetFormat _format; | |||
/** | |||
* Map of table names to page numbers containing their definition | |||
* (String -> Integer) | |||
*/ | |||
private Map _tables = new HashMap(); | |||
/** Reads and writes database pages */ | |||
private PageChannel _pageChannel; | |||
/** System catalog table */ | |||
private Table _systemCatalog; | |||
/** System access control entries table */ | |||
private Table _accessControlEntries; | |||
/** | |||
* Open an existing Database | |||
* @param mdbFile File containing the database | |||
*/ | |||
public static Database open(File mdbFile) throws IOException { | |||
return new Database(openChannel(mdbFile)); | |||
} | |||
/** | |||
* Create a new Database | |||
* @param mdbFile Location to write the new database to. <b>If this file | |||
* already exists, it will be overwritten.</b> | |||
*/ | |||
public static Database create(File mdbFile) throws IOException { | |||
FileChannel channel = openChannel(mdbFile); | |||
channel.transferFrom(Channels.newChannel( | |||
Thread.currentThread().getContextClassLoader().getResourceAsStream( | |||
EMPTY_MDB)), 0, (long) Integer.MAX_VALUE); | |||
return new Database(channel); | |||
} | |||
private static FileChannel openChannel(File mdbFile) throws FileNotFoundException { | |||
return new RandomAccessFile(mdbFile, "rw").getChannel(); | |||
} | |||
/** | |||
* Create a new database by reading it in from a FileChannel. | |||
* @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. | |||
*/ | |||
protected Database(FileChannel channel) throws IOException { | |||
_format = JetFormat.getFormat(channel); | |||
_pageChannel = new PageChannel(channel, _format); | |||
_buffer = _pageChannel.createPageBuffer(); | |||
readSystemCatalog(); | |||
} | |||
public PageChannel getPageChannel() { | |||
return _pageChannel; | |||
} | |||
/** | |||
* @return The system catalog table | |||
*/ | |||
public Table getSystemCatalog() { | |||
return _systemCatalog; | |||
} | |||
public Table getAccessControlEntries() { | |||
return _accessControlEntries; | |||
} | |||
/** | |||
* Read the system catalog | |||
*/ | |||
private void readSystemCatalog() throws IOException { | |||
_pageChannel.readPage(_buffer, PAGE_SYSTEM_CATALOG); | |||
byte pageType = _buffer.get(); | |||
if (pageType != PageTypes.TABLE_DEF) { | |||
throw new IOException("Looking for system catalog at page " + | |||
PAGE_SYSTEM_CATALOG + ", but page type is " + pageType); | |||
} | |||
_systemCatalog = new Table(_buffer, _pageChannel, _format, PAGE_SYSTEM_CATALOG); | |||
Map row; | |||
while ( (row = _systemCatalog.getNextRow(Arrays.asList( | |||
new String[] {COL_NAME, COL_TYPE, COL_ID}))) != null) | |||
{ | |||
String name = (String) row.get(COL_NAME); | |||
if (name != null && TYPE_TABLE.equals(row.get(COL_TYPE))) { | |||
if (!name.startsWith(PREFIX_SYSTEM)) { | |||
_tables.put(row.get(COL_NAME), row.get(COL_ID)); | |||
} else if (TABLE_SYSTEM_ACES.equals(name)) { | |||
readAccessControlEntries(((Integer) row.get(COL_ID)).intValue()); | |||
} | |||
} else if (SYSTEM_OBJECT_NAME_TABLES.equals(name)) { | |||
_tableParentId = (Integer) row.get(COL_ID); | |||
} | |||
} | |||
if (LOG.isDebugEnabled()) { | |||
LOG.debug("Finished reading system catalog. Tables: " + _tables); | |||
} | |||
} | |||
/** | |||
* Read the system access control entries table | |||
* @param pageNum Page number of the table def | |||
*/ | |||
private void readAccessControlEntries(int pageNum) throws IOException { | |||
ByteBuffer buffer = _pageChannel.createPageBuffer(); | |||
_pageChannel.readPage(buffer, pageNum); | |||
byte pageType = buffer.get(); | |||
if (pageType != PageTypes.TABLE_DEF) { | |||
throw new IOException("Looking for MSysACEs at page " + pageNum + | |||
", but page type is " + pageType); | |||
} | |||
_accessControlEntries = new Table(buffer, _pageChannel, _format, pageNum); | |||
} | |||
/** | |||
* @return The names of all of the user tables (String) | |||
*/ | |||
public Set getTableNames() { | |||
return _tables.keySet(); | |||
} | |||
/** | |||
* @param name Table name | |||
* @return The table, or null if it doesn't exist | |||
*/ | |||
public Table getTable(String name) throws IOException { | |||
Integer pageNumber = (Integer) _tables.get(name); | |||
if (pageNumber == null) { | |||
// Bug workaround: | |||
pageNumber = (Integer) _tables.get(Character.toUpperCase(name.charAt(0)) + | |||
name.substring(1)); | |||
} | |||
if (pageNumber == null) { | |||
return null; | |||
} else { | |||
_pageChannel.readPage(_buffer, pageNumber.intValue()); | |||
return new Table(_buffer, _pageChannel, _format, pageNumber.intValue()); | |||
} | |||
} | |||
/** | |||
* Create a new table in this database | |||
* @param name Name of the table to create | |||
* @param columns List of Columns in the table | |||
*/ | |||
//XXX Set up 1-page rollback buffer? | |||
public void createTable(String name, List columns) throws IOException { | |||
//There is some really bizarre bug in here where tables that start with | |||
//the letters a-m (only lower case) won't open in Access. :) | |||
name = Character.toUpperCase(name.charAt(0)) + name.substring(1); | |||
//We are creating a new page at the end of the db for the tdef. | |||
int pageNumber = _pageChannel.getPageCount(); | |||
ByteBuffer buffer = _pageChannel.createPageBuffer(); | |||
writeTableDefinition(buffer, columns, pageNumber); | |||
writeColumnDefinitions(buffer, columns); | |||
//End of tabledef | |||
buffer.put((byte) 0xff); | |||
buffer.put((byte) 0xff); | |||
buffer.putInt(8, buffer.position()); //Overwrite length of data for this page | |||
//Write the tdef and usage map pages to disk. | |||
_pageChannel.writeNewPage(buffer); | |||
_pageChannel.writeNewPage(createUsageMapDefinitionBuffer(pageNumber)); | |||
_pageChannel.writeNewPage(createUsageMapDataBuffer()); //Usage map | |||
//Add this table to our internal list. | |||
_tables.put(name, new Integer(pageNumber)); | |||
//Add this table to system tables | |||
addToSystemCatalog(name, pageNumber); | |||
addToAccessControlEntries(pageNumber); | |||
} | |||
/** | |||
* @param buffer Buffer to write to | |||
* @param columns List of Columns in the table | |||
* @param pageNumber Page number that this table definition will be written to | |||
*/ | |||
private void writeTableDefinition(ByteBuffer buffer, List columns, int pageNumber) | |||
throws IOException { | |||
//Start writing the tdef | |||
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 | |||
buffer.putInt(0); //Length of data for this page | |||
buffer.put((byte) 0x59); //Unknown | |||
buffer.put((byte) 0x06); //Unknown | |||
buffer.putShort((short) 0); //Unknown | |||
buffer.putInt(0); //Number of rows | |||
buffer.putInt(0); //Autonumber | |||
for (int i = 0; i < 16; 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(0); //Number of indexes in table | |||
buffer.putInt(0); //Number of indexes in table | |||
buffer.put((byte) 0); //Usage map row number | |||
int usageMapPage = pageNumber + 1; | |||
buffer.put(ByteUtil.to3ByteInt(usageMapPage)); //Usage map page number | |||
buffer.put((byte) 1); //Free map row number | |||
buffer.put(ByteUtil.to3ByteInt(usageMapPage)); //Free map page number | |||
if (LOG.isDebugEnabled()) { | |||
int position = buffer.position(); | |||
buffer.rewind(); | |||
LOG.debug("Creating new table def block:\n" + ByteUtil.toHexString( | |||
buffer, _format.SIZE_TDEF_BLOCK)); | |||
buffer.position(position); | |||
} | |||
} | |||
/** | |||
* @param buffer Buffer to write to | |||
* @param columns List of Columns to write definitions for | |||
*/ | |||
private void writeColumnDefinitions(ByteBuffer buffer, List columns) | |||
throws IOException { | |||
Iterator iter; | |||
short columnNumber = (short) 0; | |||
short fixedOffset = (short) 0; | |||
short variableOffset = (short) 0; | |||
for (iter = columns.iterator(); iter.hasNext(); columnNumber++) { | |||
Column col = (Column) iter.next(); | |||
int position = buffer.position(); | |||
buffer.put(col.getType()); | |||
buffer.put((byte) 0x59); //Unknown | |||
buffer.put((byte) 0x06); //Unknown | |||
buffer.putShort((short) 0); //Unknown | |||
buffer.putShort(columnNumber); //Column Number | |||
if (col.isVariableLength()) { | |||
buffer.putShort(variableOffset++); | |||
} else { | |||
buffer.putShort((short) 0); | |||
} | |||
buffer.putShort(columnNumber); //Column Number again | |||
buffer.put((byte) 0x09); //Unknown | |||
buffer.put((byte) 0x04); //Unknown | |||
buffer.putShort((short) 0); //Unknown | |||
if (col.isVariableLength()) { //Variable length | |||
buffer.put((byte) 0x2); | |||
} else { | |||
buffer.put((byte) 0x3); | |||
} | |||
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.size(); | |||
} | |||
buffer.putShort(col.getLength()); //Column length | |||
if (LOG.isDebugEnabled()) { | |||
LOG.debug("Creating new column def block\n" + ByteUtil.toHexString( | |||
buffer, position, _format.SIZE_COLUMN_DEF_BLOCK)); | |||
} | |||
} | |||
iter = columns.iterator(); | |||
while (iter.hasNext()) { | |||
Column col = (Column) iter.next(); | |||
ByteBuffer colName = _format.CHARSET.encode(col.getName()); | |||
buffer.putShort((short) colName.remaining()); | |||
buffer.put(colName); | |||
} | |||
} | |||
/** | |||
* Create the usage map definition page buffer. It will be stored on the page | |||
* immediately after the tdef page. | |||
* @param pageNumber Page number that the corresponding table definition will | |||
* be written to | |||
*/ | |||
private ByteBuffer createUsageMapDefinitionBuffer(int pageNumber) throws IOException { | |||
ByteBuffer rtn = _pageChannel.createPageBuffer(); | |||
rtn.put(PageTypes.DATA); | |||
rtn.put((byte) 0x1); //Unknown | |||
rtn.putShort(USAGE_MAP_DEF_FREE_SPACE); //Free space in page | |||
rtn.putInt(0); //Table definition | |||
rtn.putInt(0); //Unknown | |||
rtn.putShort((short) 2); //Number of records on this page | |||
rtn.putShort((short) _format.OFFSET_USED_PAGES_USAGE_MAP_DEF); //First location | |||
rtn.putShort((short) _format.OFFSET_FREE_PAGES_USAGE_MAP_DEF); //Second location | |||
rtn.position(_format.OFFSET_USED_PAGES_USAGE_MAP_DEF); | |||
rtn.put((byte) UsageMap.MAP_TYPE_REFERENCE); | |||
rtn.putInt(pageNumber + 2); //First referenced page number | |||
rtn.position(_format.OFFSET_FREE_PAGES_USAGE_MAP_DEF); | |||
rtn.put((byte) UsageMap.MAP_TYPE_INLINE); | |||
return rtn; | |||
} | |||
/** | |||
* Create a usage map data page buffer. | |||
*/ | |||
private ByteBuffer createUsageMapDataBuffer() throws IOException { | |||
ByteBuffer rtn = _pageChannel.createPageBuffer(); | |||
rtn.put(PageTypes.USAGE_MAP); | |||
rtn.put((byte) 0x01); //Unknown | |||
rtn.putShort((short) 0); //Unknown | |||
return rtn; | |||
} | |||
/** | |||
* 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) throws IOException { | |||
Object[] catalogRow = new Object[_systemCatalog.getColumns().size()]; | |||
int idx = 0; | |||
Iterator iter; | |||
for (iter = _systemCatalog.getColumns().iterator(); iter.hasNext(); idx++) { | |||
Column col = (Column) iter.next(); | |||
if (COL_ID.equals(col.getName())) { | |||
catalogRow[idx] = new Integer(pageNumber); | |||
} else if (COL_NAME.equals(col.getName())) { | |||
catalogRow[idx] = name; | |||
} else if (COL_TYPE.equals(col.getName())) { | |||
catalogRow[idx] = TYPE_TABLE; | |||
} else if (COL_DATE_CREATE.equals(col.getName()) || | |||
COL_DATE_UPDATE.equals(col.getName())) | |||
{ | |||
catalogRow[idx] = new Date(); | |||
} else if (COL_PARENT_ID.equals(col.getName())) { | |||
catalogRow[idx] = _tableParentId; | |||
} else if (COL_FLAGS.equals(col.getName())) { | |||
catalogRow[idx] = new Integer(0); | |||
} else if (COL_OWNER.equals(col.getName())) { | |||
byte[] owner = new byte[2]; | |||
catalogRow[idx] = owner; | |||
owner[0] = (byte) 0xcf; | |||
owner[1] = (byte) 0x5f; | |||
} | |||
} | |||
_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 { | |||
Object[] aceRow = new Object[_accessControlEntries.getColumns().size()]; | |||
int idx = 0; | |||
Iterator iter; | |||
for (iter = _accessControlEntries.getColumns().iterator(); iter.hasNext(); idx++) { | |||
Column col = (Column) iter.next(); | |||
if (col.getName().equals(COL_ACM)) { | |||
aceRow[idx] = ACM; | |||
} else if (col.getName().equals(COL_F_INHERITABLE)) { | |||
aceRow[idx] = Boolean.FALSE; | |||
} else if (col.getName().equals(COL_OBJECT_ID)) { | |||
aceRow[idx] = new Integer(pageNumber); | |||
} else if (col.getName().equals(COL_SID)) { | |||
aceRow[idx] = SID; | |||
} | |||
} | |||
_accessControlEntries.addRow(aceRow); | |||
} | |||
/** | |||
* 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 | |||
*/ | |||
public void copyTable(String name, ResultSet source) throws SQLException, IOException { | |||
ResultSetMetaData md = source.getMetaData(); | |||
List columns = new LinkedList(); | |||
int textCount = 0; | |||
int totalSize = 0; | |||
for (int i = 1; i <= md.getColumnCount(); i++) { | |||
switch (md.getColumnType(i)) { | |||
case Types.INTEGER: | |||
case Types.FLOAT: | |||
totalSize += 4; | |||
break; | |||
case Types.DOUBLE: | |||
case Types.DATE: | |||
totalSize += 8; | |||
break; | |||
case Types.VARCHAR: | |||
textCount++; | |||
break; | |||
} | |||
} | |||
short textSize = 0; | |||
if (textCount > 0) { | |||
textSize = (short) ((_format.MAX_RECORD_SIZE - totalSize) / textCount); | |||
if (textSize > _format.TEXT_FIELD_MAX_LENGTH) { | |||
textSize = _format.TEXT_FIELD_MAX_LENGTH; | |||
} | |||
} | |||
for (int i = 1; i <= md.getColumnCount(); i++) { | |||
Column column = new Column(); | |||
column.setName(escape(md.getColumnName(i))); | |||
column.setType(DataTypes.fromSQLType(md.getColumnType(i))); | |||
if (column.getType() == DataTypes.TEXT) { | |||
column.setLength(textSize); | |||
} | |||
columns.add(column); | |||
} | |||
createTable(escape(name), columns); | |||
Table table = getTable(escape(name)); | |||
List rows = new ArrayList(); | |||
while (source.next()) { | |||
Object[] row = new Object[md.getColumnCount()]; | |||
for (int i = 0; i < row.length; i++) { | |||
row[i] = source.getObject(i + 1); | |||
} | |||
rows.add(row); | |||
if (rows.size() == COPY_TABLE_BATCH_SIZE) { | |||
table.addRows(rows); | |||
rows.clear(); | |||
} | |||
} | |||
if (rows.size() > 0) { | |||
table.addRows(rows); | |||
} | |||
} | |||
/** | |||
* 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. | |||
*/ | |||
public void importFile(String name, File f, | |||
String delim) | |||
throws IOException | |||
{ | |||
BufferedReader in = null; | |||
try | |||
{ | |||
in = new BufferedReader(new FileReader(f)); | |||
importReader(name, in, delim); | |||
} | |||
finally | |||
{ | |||
if (in != null) | |||
{ | |||
try | |||
{ | |||
in.close(); | |||
} | |||
catch (IOException ex) | |||
{ | |||
LOG.warn("Could not close file " + f.getAbsolutePath(), ex); | |||
} | |||
} | |||
} | |||
} | |||
/** | |||
* 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. | |||
*/ | |||
public void importReader(String name, BufferedReader in, | |||
String delim) | |||
throws IOException | |||
{ | |||
String line = in.readLine(); | |||
if (line == null || line.trim().length() == 0) | |||
{ | |||
return; | |||
} | |||
String tableName = escape(name); | |||
int counter = 0; | |||
while(getTable(tableName) != null) | |||
{ | |||
tableName = escape(name + (counter++)); | |||
} | |||
List columns = new LinkedList(); | |||
String[] columnNames = line.split(delim); | |||
short textSize = (short) ((_format.MAX_RECORD_SIZE) / columnNames.length); | |||
if (textSize > _format.TEXT_FIELD_MAX_LENGTH) { | |||
textSize = _format.TEXT_FIELD_MAX_LENGTH; | |||
} | |||
for (int i = 0; i < columnNames.length; i++) { | |||
Column column = new Column(); | |||
column.setName(escape(columnNames[i])); | |||
column.setType(DataTypes.TEXT); | |||
column.setLength(textSize); | |||
columns.add(column); | |||
} | |||
createTable(tableName, columns); | |||
Table table = getTable(tableName); | |||
List rows = new ArrayList(); | |||
while ((line = in.readLine()) != null) | |||
{ | |||
// | |||
// Handle the situation where the end of the line | |||
// may have null fields. We always want to add the | |||
// same number of columns to the table each time. | |||
// | |||
String[] data = new String[columnNames.length]; | |||
String[] splitData = line.split(delim); | |||
System.arraycopy(splitData, 0, data, 0, splitData.length); | |||
rows.add(data); | |||
if (rows.size() == COPY_TABLE_BATCH_SIZE) { | |||
table.addRows(rows); | |||
rows.clear(); | |||
} | |||
} | |||
if (rows.size() > 0) { | |||
table.addRows(rows); | |||
} | |||
} | |||
/** | |||
* Close the database file | |||
*/ | |||
public void close() throws IOException { | |||
_pageChannel.close(); | |||
} | |||
/** | |||
* @return A table or column name escaped for Access | |||
*/ | |||
private String escape(String s) { | |||
if (RESERVED_WORDS.contains(s.toLowerCase())) { | |||
return ESCAPE_PREFIX + s; | |||
} else { | |||
return s; | |||
} | |||
} | |||
public String toString() { | |||
return ToStringBuilder.reflectionToString(this); | |||
} | |||
} |
@@ -0,0 +1,506 @@ | |||
/* | |||
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; | |||
import java.io.IOException; | |||
import java.nio.ByteBuffer; | |||
import java.nio.ByteOrder; | |||
import java.util.ArrayList; | |||
import java.util.Iterator; | |||
import java.util.LinkedHashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.SortedSet; | |||
import java.util.TreeSet; | |||
import org.apache.commons.collections.bidimap.DualHashBidiMap; | |||
import org.apache.commons.collections.BidiMap; | |||
import org.apache.commons.lang.builder.CompareToBuilder; | |||
/** | |||
* Access table index | |||
* @author Tim McCune | |||
*/ | |||
public class Index implements Comparable { | |||
/** Max number of columns in an index */ | |||
private static final int MAX_COLUMNS = 10; | |||
private static final short COLUMN_UNUSED = -1; | |||
/** | |||
* Map of characters to bytes that Access uses in indexes (not ASCII) | |||
* (Character -> Byte) | |||
*/ | |||
private static BidiMap CODES = new DualHashBidiMap(); | |||
static { | |||
//These values are prefixed with a '43' | |||
CODES.put(new Character('^'), new Byte((byte) 2)); | |||
CODES.put(new Character('_'), new Byte((byte) 3)); | |||
CODES.put(new Character('{'), new Byte((byte) 9)); | |||
CODES.put(new Character('|'), new Byte((byte) 11)); | |||
CODES.put(new Character('}'), new Byte((byte) 13)); | |||
CODES.put(new Character('~'), new Byte((byte) 15)); | |||
//These values aren't. | |||
CODES.put(new Character(' '), new Byte((byte) 7)); | |||
CODES.put(new Character('#'), new Byte((byte) 12)); | |||
CODES.put(new Character('$'), new Byte((byte) 14)); | |||
CODES.put(new Character('%'), new Byte((byte) 16)); | |||
CODES.put(new Character('&'), new Byte((byte) 18)); | |||
CODES.put(new Character('('), new Byte((byte) 20)); | |||
CODES.put(new Character(')'), new Byte((byte) 22)); | |||
CODES.put(new Character('*'), new Byte((byte) 24)); | |||
CODES.put(new Character(','), new Byte((byte) 26)); | |||
CODES.put(new Character('/'), new Byte((byte) 30)); | |||
CODES.put(new Character(':'), new Byte((byte) 32)); | |||
CODES.put(new Character(';'), new Byte((byte) 34)); | |||
CODES.put(new Character('?'), new Byte((byte) 36)); | |||
CODES.put(new Character('@'), new Byte((byte) 38)); | |||
CODES.put(new Character('+'), new Byte((byte) 44)); | |||
CODES.put(new Character('<'), new Byte((byte) 46)); | |||
CODES.put(new Character('='), new Byte((byte) 48)); | |||
CODES.put(new Character('>'), new Byte((byte) 50)); | |||
CODES.put(new Character('0'), new Byte((byte) 54)); | |||
CODES.put(new Character('1'), new Byte((byte) 56)); | |||
CODES.put(new Character('2'), new Byte((byte) 58)); | |||
CODES.put(new Character('3'), new Byte((byte) 60)); | |||
CODES.put(new Character('4'), new Byte((byte) 62)); | |||
CODES.put(new Character('5'), new Byte((byte) 64)); | |||
CODES.put(new Character('6'), new Byte((byte) 66)); | |||
CODES.put(new Character('7'), new Byte((byte) 68)); | |||
CODES.put(new Character('8'), new Byte((byte) 70)); | |||
CODES.put(new Character('9'), new Byte((byte) 72)); | |||
CODES.put(new Character('A'), new Byte((byte) 74)); | |||
CODES.put(new Character('B'), new Byte((byte) 76)); | |||
CODES.put(new Character('C'), new Byte((byte) 77)); | |||
CODES.put(new Character('D'), new Byte((byte) 79)); | |||
CODES.put(new Character('E'), new Byte((byte) 81)); | |||
CODES.put(new Character('F'), new Byte((byte) 83)); | |||
CODES.put(new Character('G'), new Byte((byte) 85)); | |||
CODES.put(new Character('H'), new Byte((byte) 87)); | |||
CODES.put(new Character('I'), new Byte((byte) 89)); | |||
CODES.put(new Character('J'), new Byte((byte) 91)); | |||
CODES.put(new Character('K'), new Byte((byte) 92)); | |||
CODES.put(new Character('L'), new Byte((byte) 94)); | |||
CODES.put(new Character('M'), new Byte((byte) 96)); | |||
CODES.put(new Character('N'), new Byte((byte) 98)); | |||
CODES.put(new Character('O'), new Byte((byte) 100)); | |||
CODES.put(new Character('P'), new Byte((byte) 102)); | |||
CODES.put(new Character('Q'), new Byte((byte) 104)); | |||
CODES.put(new Character('R'), new Byte((byte) 105)); | |||
CODES.put(new Character('S'), new Byte((byte) 107)); | |||
CODES.put(new Character('T'), new Byte((byte) 109)); | |||
CODES.put(new Character('U'), new Byte((byte) 111)); | |||
CODES.put(new Character('V'), new Byte((byte) 113)); | |||
CODES.put(new Character('W'), new Byte((byte) 115)); | |||
CODES.put(new Character('X'), new Byte((byte) 117)); | |||
CODES.put(new Character('Y'), new Byte((byte) 118)); | |||
CODES.put(new Character('Z'), new Byte((byte) 120)); | |||
} | |||
/** Page number of the index data */ | |||
private int _pageNumber; | |||
private int _parentPageNumber; | |||
/** Number of rows in the index */ | |||
private int _rowCount; | |||
private JetFormat _format; | |||
private List _allColumns; | |||
private SortedSet _entries = new TreeSet(); | |||
/** Map of columns to order (Column -> Byte) */ | |||
private Map _columns = new LinkedHashMap(); | |||
private PageChannel _pageChannel; | |||
/** 0-based index number */ | |||
private int _indexNumber; | |||
/** Index name */ | |||
private String _name; | |||
public Index(int parentPageNumber, PageChannel channel, JetFormat format) { | |||
_parentPageNumber = parentPageNumber; | |||
_pageChannel = channel; | |||
_format = format; | |||
} | |||
public void setIndexNumber(int indexNumber) { | |||
_indexNumber = indexNumber; | |||
} | |||
public int getIndexNumber() { | |||
return _indexNumber; | |||
} | |||
public void setRowCount(int rowCount) { | |||
_rowCount = rowCount; | |||
} | |||
public void setName(String name) { | |||
_name = name; | |||
} | |||
public void update() throws IOException { | |||
_pageChannel.writePage(write(), _pageNumber); | |||
} | |||
/** | |||
* Write this index out to a buffer | |||
*/ | |||
public ByteBuffer write() throws IOException { | |||
ByteBuffer buffer = _pageChannel.createPageBuffer(); | |||
buffer.put((byte) 0x04); //Page type | |||
buffer.put((byte) 0x01); //Unknown | |||
buffer.putShort((short) 0); //Free space | |||
buffer.putInt(_parentPageNumber); | |||
buffer.putInt(0); //Prev page | |||
buffer.putInt(0); //Next page | |||
buffer.putInt(0); //Leaf page | |||
buffer.putInt(0); //Unknown | |||
buffer.put((byte) 0); //Unknown | |||
buffer.put((byte) 0); //Unknown | |||
buffer.put((byte) 0); //Unknown | |||
byte[] entryMask = new byte[_format.SIZE_INDEX_ENTRY_MASK]; | |||
int totalSize = 0; | |||
Iterator iter = _entries.iterator(); | |||
while (iter.hasNext()) { | |||
Entry entry = (Entry) iter.next(); | |||
int size = entry.size(); | |||
totalSize += size; | |||
int idx = totalSize / 8; | |||
entryMask[idx] |= (1 << (totalSize % 8)); | |||
} | |||
buffer.put(entryMask); | |||
iter = _entries.iterator(); | |||
while (iter.hasNext()) { | |||
Entry entry = (Entry) iter.next(); | |||
entry.write(buffer); | |||
} | |||
buffer.putShort(2, (short) (_format.PAGE_SIZE - buffer.position())); | |||
return buffer; | |||
} | |||
/** | |||
* Read this index in from a buffer | |||
* @param buffer Buffer to read from | |||
* @param availableColumns Columns that this index may use | |||
*/ | |||
public void read(ByteBuffer buffer, List availableColumns) | |||
throws IOException | |||
{ | |||
_allColumns = availableColumns; | |||
for (int i = 0; i < MAX_COLUMNS; i++) { | |||
short columnNumber = buffer.getShort(); | |||
Byte order = new Byte(buffer.get()); | |||
if (columnNumber != COLUMN_UNUSED) { | |||
_columns.put(availableColumns.get(columnNumber), order); | |||
} | |||
} | |||
buffer.getInt(); //Forward past Unknown | |||
_pageNumber = buffer.getInt(); | |||
buffer.position(buffer.position() + 10); //Forward past other stuff | |||
ByteBuffer indexPage = _pageChannel.createPageBuffer(); | |||
_pageChannel.readPage(indexPage, _pageNumber); | |||
indexPage.position(_format.OFFSET_INDEX_ENTRY_MASK); | |||
byte[] entryMask = new byte[_format.SIZE_INDEX_ENTRY_MASK]; | |||
indexPage.get(entryMask); | |||
int lastStart = 0; | |||
for (int i = 0; i < entryMask.length; i++) { | |||
for (int j = 0; j < 8; j++) { | |||
if ((entryMask[i] & (1 << j)) != 0) { | |||
int length = i * 8 + j - lastStart; | |||
_entries.add(new Entry(indexPage)); | |||
lastStart += length; | |||
} | |||
} | |||
} | |||
} | |||
/** | |||
* Add a row to this index | |||
* @param row Row to add | |||
* @param pageNumber Page number on which the row is stored | |||
* @param rowNumber Row number at which the row is stored | |||
*/ | |||
public void addRow(Object[] row, int pageNumber, byte rowNumber) { | |||
_entries.add(new Entry(row, pageNumber, rowNumber)); | |||
} | |||
public String toString() { | |||
StringBuffer rtn = new StringBuffer(); | |||
rtn.append("\tName: " + _name); | |||
rtn.append("\n\tNumber: " + _indexNumber); | |||
rtn.append("\n\tPage number: " + _pageNumber); | |||
rtn.append("\n\tColumns: " + _columns); | |||
rtn.append("\n\tEntries: " + _entries); | |||
rtn.append("\n\n"); | |||
return rtn.toString(); | |||
} | |||
public int compareTo(Object obj) { | |||
Index other = (Index) obj; | |||
if (_indexNumber > other.getIndexNumber()) { | |||
return 1; | |||
} else if (_indexNumber < other.getIndexNumber()) { | |||
return -1; | |||
} else { | |||
return 0; | |||
} | |||
} | |||
/** | |||
* A single entry in an index (points to a single row) | |||
*/ | |||
private class Entry implements Comparable { | |||
/** Page number on which the row is stored */ | |||
private int _page; | |||
/** Row number at which the row is stored */ | |||
private byte _row; | |||
/** Columns that are indexed */ | |||
private List _entryColumns = new ArrayList(); | |||
/** | |||
* Create a new entry | |||
* @param values Indexed row values | |||
* @param page Page number on which the row is stored | |||
* @param rowNumber Row number at which the row is stored | |||
*/ | |||
public Entry(Object[] values, int page, byte rowNumber) { | |||
_page = page; | |||
_row = rowNumber; | |||
Iterator iter = _columns.keySet().iterator(); | |||
while (iter.hasNext()) { | |||
Column col = (Column) iter.next(); | |||
Object value = values[col.getColumnNumber()]; | |||
_entryColumns.add(new EntryColumn(col, (Comparable) value)); | |||
} | |||
} | |||
/** | |||
* Read an existing entry in from a buffer | |||
*/ | |||
public Entry(ByteBuffer buffer) throws IOException { | |||
Iterator iter = _columns.keySet().iterator(); | |||
while (iter.hasNext()) { | |||
_entryColumns.add(new EntryColumn((Column) iter.next(), buffer)); | |||
} | |||
//3-byte int in big endian order! Gotta love those kooky MS programmers. :) | |||
_page = (((int) buffer.get()) & 0xFF) << 16; | |||
_page += (((int) buffer.get()) & 0xFF) << 8; | |||
_page += (int) buffer.get(); | |||
_row = buffer.get(); | |||
} | |||
public List getEntryColumns() { | |||
return _entryColumns; | |||
} | |||
public int getPage() { | |||
return _page; | |||
} | |||
public byte getRow() { | |||
return _row; | |||
} | |||
public int size() { | |||
int rtn = 5; | |||
Iterator iter = _entryColumns.iterator(); | |||
while (iter.hasNext()) { | |||
rtn += ((EntryColumn) iter.next()).size(); | |||
} | |||
return rtn; | |||
} | |||
/** | |||
* Write this entry into a buffer | |||
*/ | |||
public void write(ByteBuffer buffer) throws IOException { | |||
Iterator iter = _entryColumns.iterator(); | |||
while (iter.hasNext()) { | |||
((EntryColumn) iter.next()).write(buffer); | |||
} | |||
buffer.put((byte) (_page >>> 16)); | |||
buffer.put((byte) (_page >>> 8)); | |||
buffer.put((byte) _page); | |||
buffer.put(_row); | |||
} | |||
public String toString() { | |||
return ("Page = " + _page + ", Row = " + _row + ", Columns = " + _entryColumns + "\n"); | |||
} | |||
public int compareTo(Object obj) { | |||
if (this == obj) { | |||
return 0; | |||
} | |||
Entry other = (Entry) obj; | |||
Iterator myIter = _entryColumns.iterator(); | |||
Iterator otherIter = other.getEntryColumns().iterator(); | |||
while (myIter.hasNext()) { | |||
if (!otherIter.hasNext()) { | |||
throw new IllegalArgumentException( | |||
"Trying to compare index entries with a different number of entry columns"); | |||
} | |||
EntryColumn myCol = (EntryColumn) myIter.next(); | |||
EntryColumn otherCol = (EntryColumn) otherIter.next(); | |||
int i = myCol.compareTo(otherCol); | |||
if (i != 0) { | |||
return i; | |||
} | |||
} | |||
return new CompareToBuilder().append(_page, other.getPage()) | |||
.append(_row, other.getRow()).toComparison(); | |||
} | |||
} | |||
/** | |||
* A single column value within an index Entry; encapsulates column | |||
* definition and column value. | |||
*/ | |||
private class EntryColumn implements Comparable { | |||
/** Column definition */ | |||
private Column _column; | |||
/** Column value */ | |||
private Comparable _value; | |||
/** | |||
* Create a new EntryColumn | |||
*/ | |||
public EntryColumn(Column col, Comparable value) { | |||
_column = col; | |||
_value = value; | |||
} | |||
/** | |||
* Read in an existing EntryColumn from a buffer | |||
*/ | |||
public EntryColumn(Column col, ByteBuffer buffer) throws IOException { | |||
_column = col; | |||
byte flag = buffer.get(); | |||
if (flag != (byte) 0) { | |||
if (col.getType() == DataTypes.TEXT) { | |||
StringBuffer sb = new StringBuffer(); | |||
byte b; | |||
while ( (b = buffer.get()) != (byte) 1) { | |||
if ((int) b == 43) { | |||
b = buffer.get(); | |||
} | |||
Character c = (Character) CODES.getKey(new Byte(b)); | |||
if (c != null) { | |||
sb.append(c.charValue()); | |||
} | |||
} | |||
buffer.get(); //Forward past 0x00 | |||
_value = sb.toString(); | |||
} else { | |||
byte[] data = new byte[col.size()]; | |||
buffer.get(data); | |||
_value = (Comparable) col.read(data, ByteOrder.BIG_ENDIAN); | |||
//ints and shorts are stored in index as value + 2147483648 | |||
if (_value instanceof Integer) { | |||
_value = new Integer((int) (((Integer) _value).longValue() + (long) Integer.MAX_VALUE + 1L)); | |||
} else if (_value instanceof Short) { | |||
_value = new Short((short) (((Short) _value).longValue() + (long) Integer.MAX_VALUE + 1L)); | |||
} | |||
} | |||
} | |||
} | |||
public Comparable getValue() { | |||
return _value; | |||
} | |||
/** | |||
* Write this entry column to a buffer | |||
*/ | |||
public void write(ByteBuffer buffer) throws IOException { | |||
buffer.put((byte) 0x7F); | |||
if (_column.getType() == DataTypes.TEXT) { | |||
String s = (String) _value; | |||
for (int i = 0; i < s.length(); i++) { | |||
Byte b = (Byte) CODES.get(new Character(Character.toUpperCase(s.charAt(i)))); | |||
if (b == null) { | |||
throw new IOException("Unmapped index value: " + s.charAt(i)); | |||
} else { | |||
byte bv = b.byteValue(); | |||
//WTF is this? No idea why it's this way, but it is. :) | |||
if (bv == (byte) 2 || bv == (byte) 3 || bv == (byte) 9 || bv == (byte) 11 || | |||
bv == (byte) 13 || bv == (byte) 15) | |||
{ | |||
buffer.put((byte) 43); //Ah, the magic 43. | |||
} | |||
buffer.put(b.byteValue()); | |||
if (s.equals("_")) { | |||
buffer.put((byte) 3); | |||
} | |||
} | |||
} | |||
buffer.put((byte) 1); | |||
buffer.put((byte) 0); | |||
} else { | |||
Comparable value = _value; | |||
if (value instanceof Integer) { | |||
value = new Integer((int) (((Integer) value).longValue() - ((long) Integer.MAX_VALUE + 1L))); | |||
} else if (value instanceof Short) { | |||
value = new Short((short) (((Short) value).longValue() - ((long) Integer.MAX_VALUE + 1L))); | |||
} | |||
buffer.put(_column.write(value, ByteOrder.BIG_ENDIAN)); | |||
} | |||
} | |||
public int size() { | |||
if (_value == null) { | |||
return 0; | |||
} else if (_value instanceof String) { | |||
int rtn = 3; | |||
String s = (String) _value; | |||
for (int i = 0; i < s.length(); i++) { | |||
rtn++; | |||
if (s.charAt(i) == '^' || s.charAt(i) == '_' || s.charAt(i) == '{' || | |||
s.charAt(i) == '|' || s.charAt(i) == '}' || s.charAt(i) == '-') | |||
{ | |||
rtn++; | |||
} | |||
} | |||
return rtn; | |||
} else { | |||
return _column.size(); | |||
} | |||
} | |||
public String toString() { | |||
return String.valueOf(_value); | |||
} | |||
public int compareTo(Object obj) { | |||
return new CompareToBuilder().append(_value, ((EntryColumn) obj).getValue()) | |||
.toComparison(); | |||
} | |||
} | |||
} |
@@ -0,0 +1,98 @@ | |||
/* | |||
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; | |||
import java.io.IOException; | |||
import java.nio.ByteBuffer; | |||
/** | |||
* Usage map whose map is written inline in the same page. This type of map | |||
* can contain a maximum of 512 pages, and is always used for free space maps. | |||
* It has a start page, which all page numbers in its map are calculated as | |||
* starting from. | |||
* @author Tim McCune | |||
*/ | |||
public class InlineUsageMap extends UsageMap { | |||
/** Size in bytes of the map */ | |||
private static final int MAP_SIZE = 64; | |||
/** First page that this usage map applies to */ | |||
private int _startPage = 0; | |||
/** | |||
* @param pageChannel Used to read in pages | |||
* @param dataBuffer Buffer that contains this map's declaration | |||
* @param pageNum Page number that this usage map is contained in | |||
* @param format Format of the database that contains this usage map | |||
* @param rowStart Offset at which the declaration starts in the buffer | |||
*/ | |||
public InlineUsageMap(PageChannel pageChannel, ByteBuffer dataBuffer, | |||
int pageNum, JetFormat format, short rowStart) | |||
throws IOException | |||
{ | |||
super(pageChannel, dataBuffer, pageNum, format, rowStart); | |||
_startPage = dataBuffer.getInt(rowStart + 1); | |||
processMap(dataBuffer, 0, _startPage); | |||
} | |||
//Javadoc copied from UsageMap | |||
protected void addOrRemovePageNumber(final int pageNumber, boolean add) | |||
throws IOException | |||
{ | |||
if (add && pageNumber < _startPage) { | |||
throw new IOException("Can't add page number " + pageNumber + | |||
" because it is less than start page " + _startPage); | |||
} | |||
int relativePageNumber = pageNumber - _startPage; | |||
ByteBuffer buffer = getDataBuffer(); | |||
if ((!add && !getPageNumbers().remove(new Integer(pageNumber))) || (add && | |||
(relativePageNumber > MAP_SIZE * 8 - 1))) | |||
{ | |||
//Increase the start page to the current page and clear out the map. | |||
_startPage = pageNumber; | |||
buffer.position(getRowStart() + 1); | |||
buffer.putInt(_startPage); | |||
getPageNumbers().clear(); | |||
if (!add) { | |||
for (int j = 0; j < MAP_SIZE; j++) { | |||
buffer.put((byte) 0xff); //Fill bitmap with 1s | |||
} | |||
for (int j = _startPage; j < _startPage + MAP_SIZE * 8; j++) { | |||
getPageNumbers().add(new Integer(j)); //Fill our list with page numbers | |||
} | |||
} | |||
getPageChannel().writePage(buffer, getDataPageNumber()); | |||
relativePageNumber = pageNumber - _startPage; | |||
} | |||
updateMap(pageNumber, relativePageNumber, 1 << (relativePageNumber % 8), buffer, add); | |||
//Write the updated map back to disk | |||
getPageChannel().writePage(buffer, getDataPageNumber()); | |||
} | |||
} |
@@ -0,0 +1,302 @@ | |||
/* | |||
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; | |||
import java.io.IOException; | |||
import java.nio.ByteBuffer; | |||
import java.nio.channels.FileChannel; | |||
import java.nio.charset.Charset; | |||
/** | |||
* Encapsulates constants describing a specific version of the Access Jet format | |||
* @author Tim McCune | |||
*/ | |||
public abstract class JetFormat { | |||
/** Maximum size of a record minus OLE objects and Memo fields */ | |||
public static final int MAX_RECORD_SIZE = 1900; //2kb minus some overhead | |||
/** Maximum size of a text field */ | |||
public static final short TEXT_FIELD_MAX_LENGTH = 255 * 2; | |||
/** Offset in the file that holds the byte describing the Jet format version */ | |||
private static final long OFFSET_VERSION = 20L; | |||
/** Version code for Jet version 3 */ | |||
private static final byte CODE_VERSION_3 = 0x0; | |||
/** Version code for Jet version 4 */ | |||
private static final byte CODE_VERSION_4 = 0x1; | |||
//These constants are populated by this class's constructor. They can't be | |||
//populated by the subclass's constructor because they are final, and Java | |||
//doesn't allow this; hence all the abstract defineXXX() methods. | |||
/** Database page size in bytes */ | |||
public final int PAGE_SIZE; | |||
public final int MAX_ROW_SIZE; | |||
public final int OFFSET_NEXT_TABLE_DEF_PAGE; | |||
public final int OFFSET_NUM_ROWS; | |||
public final int OFFSET_TABLE_TYPE; | |||
public final int OFFSET_NUM_COLS; | |||
public final int OFFSET_NUM_INDEXES; | |||
public final int OFFSET_OWNED_PAGES; | |||
public final int OFFSET_FREE_SPACE_PAGES; | |||
public final int OFFSET_INDEX_DEF_BLOCK; | |||
public final int OFFSET_COLUMN_TYPE; | |||
public final int OFFSET_COLUMN_NUMBER; | |||
public final int OFFSET_COLUMN_PRECISION; | |||
public final int OFFSET_COLUMN_SCALE; | |||
public final int OFFSET_COLUMN_VARIABLE; | |||
public final int OFFSET_COLUMN_COMPRESSED_UNICODE; | |||
public final int OFFSET_COLUMN_LENGTH; | |||
public final int OFFSET_TABLE_DEF_LOCATION; | |||
public final int OFFSET_NUM_ROWS_ON_PAGE; | |||
public final int OFFSET_ROW_LOCATION_BLOCK; | |||
public final int OFFSET_ROW_START; | |||
public final int OFFSET_MAP_START; | |||
public final int OFFSET_USAGE_MAP_PAGE_DATA; | |||
public final int OFFSET_REFERENCE_MAP_PAGE_NUMBERS; | |||
public final int OFFSET_FREE_SPACE; | |||
public final int OFFSET_DATA_ROW_LOCATION_BLOCK; | |||
public final int OFFSET_NUM_ROWS_ON_DATA_PAGE; | |||
public final int OFFSET_LVAL_ROW_LOCATION_BLOCK; | |||
public final int OFFSET_USED_PAGES_USAGE_MAP_DEF; | |||
public final int OFFSET_FREE_PAGES_USAGE_MAP_DEF; | |||
public final int OFFSET_INDEX_ENTRY_MASK; | |||
public final int SIZE_INDEX_DEFINITION; | |||
public final int SIZE_COLUMN_HEADER; | |||
public final int SIZE_ROW_LOCATION; | |||
public final int SIZE_LONG_VALUE_DEF; | |||
public final int SIZE_TDEF_BLOCK; | |||
public final int SIZE_COLUMN_DEF_BLOCK; | |||
public final int SIZE_INDEX_ENTRY_MASK; | |||
public final int PAGES_PER_USAGE_MAP_PAGE; | |||
public final Charset CHARSET; | |||
public static final JetFormat VERSION_4 = new Jet4Format(); | |||
/** | |||
* @return The Jet Format represented in the passed-in file | |||
*/ | |||
public static JetFormat getFormat(FileChannel channel) throws IOException { | |||
ByteBuffer buffer = ByteBuffer.allocate(1); | |||
channel.read(buffer, OFFSET_VERSION); | |||
buffer.flip(); | |||
byte version = buffer.get(); | |||
if (version == CODE_VERSION_4) { | |||
return VERSION_4; | |||
} else { | |||
throw new IOException("Unsupported version: " + version); | |||
} | |||
} | |||
private JetFormat() { | |||
PAGE_SIZE = definePageSize(); | |||
MAX_ROW_SIZE = defineMaxRowSize(); | |||
OFFSET_NEXT_TABLE_DEF_PAGE = defineOffsetNextTableDefPage(); | |||
OFFSET_NUM_ROWS = defineOffsetNumRows(); | |||
OFFSET_TABLE_TYPE = defineOffsetTableType(); | |||
OFFSET_NUM_COLS = defineOffsetNumCols(); | |||
OFFSET_NUM_INDEXES = defineOffsetNumIndexes(); | |||
OFFSET_OWNED_PAGES = defineOffsetOwnedPages(); | |||
OFFSET_FREE_SPACE_PAGES = defineOffsetFreeSpacePages(); | |||
OFFSET_INDEX_DEF_BLOCK = defineOffsetIndexDefBlock(); | |||
OFFSET_COLUMN_TYPE = defineOffsetColumnType(); | |||
OFFSET_COLUMN_NUMBER = defineOffsetColumnNumber(); | |||
OFFSET_COLUMN_PRECISION = defineOffsetColumnPrecision(); | |||
OFFSET_COLUMN_SCALE = defineOffsetColumnScale(); | |||
OFFSET_COLUMN_VARIABLE = defineOffsetColumnVariable(); | |||
OFFSET_COLUMN_COMPRESSED_UNICODE = defineOffsetColumnCompressedUnicode(); | |||
OFFSET_COLUMN_LENGTH = defineOffsetColumnLength(); | |||
OFFSET_TABLE_DEF_LOCATION = defineOffsetTableDefLocation(); | |||
OFFSET_NUM_ROWS_ON_PAGE = defineOffsetNumRowsOnPage(); | |||
OFFSET_ROW_LOCATION_BLOCK = defineOffsetRowLocationBlock(); | |||
OFFSET_ROW_START = defineOffsetRowStart(); | |||
OFFSET_MAP_START = defineOffsetMapStart(); | |||
OFFSET_USAGE_MAP_PAGE_DATA = defineOffsetUsageMapPageData(); | |||
OFFSET_REFERENCE_MAP_PAGE_NUMBERS = defineOffsetReferenceMapPageNumbers(); | |||
OFFSET_FREE_SPACE = defineOffsetFreeSpace(); | |||
OFFSET_DATA_ROW_LOCATION_BLOCK = defineOffsetDataRowLocationBlock(); | |||
OFFSET_NUM_ROWS_ON_DATA_PAGE = defineOffsetNumRowsOnDataPage(); | |||
OFFSET_LVAL_ROW_LOCATION_BLOCK = defineOffsetLvalRowLocationBlock(); | |||
OFFSET_USED_PAGES_USAGE_MAP_DEF = defineOffsetUsedPagesUsageMapDef(); | |||
OFFSET_FREE_PAGES_USAGE_MAP_DEF = defineOffsetFreePagesUsageMapDef(); | |||
OFFSET_INDEX_ENTRY_MASK = defineOffsetIndexEntryMask(); | |||
SIZE_INDEX_DEFINITION = defineSizeIndexDefinition(); | |||
SIZE_COLUMN_HEADER = defineSizeColumnHeader(); | |||
SIZE_ROW_LOCATION = defineSizeRowLocation(); | |||
SIZE_LONG_VALUE_DEF = defineSizeLongValueDef(); | |||
SIZE_TDEF_BLOCK = defineSizeTdefBlock(); | |||
SIZE_COLUMN_DEF_BLOCK = defineSizeColumnDefBlock(); | |||
SIZE_INDEX_ENTRY_MASK = defineSizeIndexEntryMask(); | |||
PAGES_PER_USAGE_MAP_PAGE = definePagesPerUsageMapPage(); | |||
CHARSET = defineCharset(); | |||
} | |||
protected abstract int definePageSize(); | |||
protected abstract int defineMaxRowSize(); | |||
protected abstract int defineOffsetNextTableDefPage(); | |||
protected abstract int defineOffsetNumRows(); | |||
protected abstract int defineOffsetTableType(); | |||
protected abstract int defineOffsetNumCols(); | |||
protected abstract int defineOffsetNumIndexes(); | |||
protected abstract int defineOffsetOwnedPages(); | |||
protected abstract int defineOffsetFreeSpacePages(); | |||
protected abstract int defineOffsetIndexDefBlock(); | |||
protected abstract int defineOffsetColumnType(); | |||
protected abstract int defineOffsetColumnNumber(); | |||
protected abstract int defineOffsetColumnPrecision(); | |||
protected abstract int defineOffsetColumnScale(); | |||
protected abstract int defineOffsetColumnVariable(); | |||
protected abstract int defineOffsetColumnCompressedUnicode(); | |||
protected abstract int defineOffsetColumnLength(); | |||
protected abstract int defineOffsetTableDefLocation(); | |||
protected abstract int defineOffsetNumRowsOnPage(); | |||
protected abstract int defineOffsetRowLocationBlock(); | |||
protected abstract int defineOffsetRowStart(); | |||
protected abstract int defineOffsetMapStart(); | |||
protected abstract int defineOffsetUsageMapPageData(); | |||
protected abstract int defineOffsetReferenceMapPageNumbers(); | |||
protected abstract int defineOffsetFreeSpace(); | |||
protected abstract int defineOffsetDataRowLocationBlock(); | |||
protected abstract int defineOffsetNumRowsOnDataPage(); | |||
protected abstract int defineOffsetLvalRowLocationBlock(); | |||
protected abstract int defineOffsetUsedPagesUsageMapDef(); | |||
protected abstract int defineOffsetFreePagesUsageMapDef(); | |||
protected abstract int defineOffsetIndexEntryMask(); | |||
protected abstract int defineSizeIndexDefinition(); | |||
protected abstract int defineSizeColumnHeader(); | |||
protected abstract int defineSizeRowLocation(); | |||
protected abstract int defineSizeLongValueDef(); | |||
protected abstract int defineSizeTdefBlock(); | |||
protected abstract int defineSizeColumnDefBlock(); | |||
protected abstract int defineSizeIndexEntryMask(); | |||
protected abstract int definePagesPerUsageMapPage(); | |||
protected abstract Charset defineCharset(); | |||
private static final class Jet4Format extends JetFormat { | |||
protected int definePageSize() { return 4096; } | |||
protected int defineMaxRowSize() { return PAGE_SIZE - 18; } | |||
protected int defineOffsetNextTableDefPage() { return 4; } | |||
protected int defineOffsetNumRows() { return 16; } | |||
protected int defineOffsetTableType() { return 40; } | |||
protected int defineOffsetNumCols() { return 45; } | |||
protected int defineOffsetNumIndexes() { return 47; } | |||
protected int defineOffsetOwnedPages() { return 55; } | |||
protected int defineOffsetFreeSpacePages() { return 59; } | |||
protected int defineOffsetIndexDefBlock() { return 63; } | |||
protected int defineOffsetColumnType() { return 0; } | |||
protected int defineOffsetColumnNumber() { return 5; } | |||
protected int defineOffsetColumnPrecision() { return 11; } | |||
protected int defineOffsetColumnScale() { return 12; } | |||
protected int defineOffsetColumnVariable() { return 15; } | |||
protected int defineOffsetColumnCompressedUnicode() { return 16; } | |||
protected int defineOffsetColumnLength() { return 23; } | |||
protected int defineOffsetTableDefLocation() { return 4; } | |||
protected int defineOffsetNumRowsOnPage() { return 12; } | |||
protected int defineOffsetRowLocationBlock() { return 16; } | |||
protected int defineOffsetRowStart() { return 14; } | |||
protected int defineOffsetMapStart() { return 5; } | |||
protected int defineOffsetUsageMapPageData() { return 4; } | |||
protected int defineOffsetReferenceMapPageNumbers() { return 1; } | |||
protected int defineOffsetFreeSpace() { return 2; } | |||
protected int defineOffsetDataRowLocationBlock() { return 14; } | |||
protected int defineOffsetNumRowsOnDataPage() { return 12; } | |||
protected int defineOffsetLvalRowLocationBlock() { return 10; } | |||
protected int defineOffsetUsedPagesUsageMapDef() { return 4027; } | |||
protected int defineOffsetFreePagesUsageMapDef() { return 3958; } | |||
protected int defineOffsetIndexEntryMask() { return 27; } | |||
protected int defineSizeIndexDefinition() { return 12; } | |||
protected int defineSizeColumnHeader() { return 25; } | |||
protected int defineSizeRowLocation() { return 2; } | |||
protected int defineSizeLongValueDef() { return 12; } | |||
protected int defineSizeTdefBlock() { return 63; } | |||
protected int defineSizeColumnDefBlock() { return 25; } | |||
protected int defineSizeIndexEntryMask() { return 453; } | |||
protected int definePagesPerUsageMapPage() { return 4092 * 8; } | |||
protected Charset defineCharset() { return Charset.forName("UTF-16LE"); } | |||
} | |||
} |
@@ -0,0 +1,88 @@ | |||
/* | |||
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; | |||
import java.nio.ByteBuffer; | |||
/** | |||
* Bitmask that indicates whether or not each column in a row is null. Also | |||
* holds values of boolean columns. | |||
* @author Tim McCune | |||
*/ | |||
public class NullMask { | |||
/** The actual bitmask */ | |||
private byte[] _mask; | |||
/** | |||
* @param columnCount Number of columns in the row that this mask will be | |||
* used for | |||
*/ | |||
public NullMask(int columnCount) { | |||
_mask = new byte[(columnCount + 7) / 8]; | |||
for (int i = 0; i < _mask.length; i++) { | |||
_mask[i] = (byte) 0xff; | |||
} | |||
for (int i = columnCount; i < _mask.length * 8; i++) { | |||
markNull(i); | |||
} | |||
} | |||
/** | |||
* Read a mask in from a buffer | |||
*/ | |||
public void read(ByteBuffer buffer) { | |||
buffer.get(_mask); | |||
} | |||
public ByteBuffer wrap() { | |||
return ByteBuffer.wrap(_mask); | |||
} | |||
/** | |||
* @param columnNumber 0-based column number in this mask's row | |||
* @return Whether or not the value for that column is null. For boolean | |||
* columns, returns the actual value of the column. | |||
*/ | |||
public boolean isNull(int columnNumber) { | |||
return (_mask[columnNumber / 8] & (byte) (1 << (columnNumber % 8))) == 0; | |||
} | |||
public void markNull(int columnNumber) { | |||
int maskIndex = columnNumber / 8; | |||
_mask[maskIndex] = (byte) (_mask[maskIndex] & (byte) ~(1 << (columnNumber % 8))); | |||
} | |||
/** | |||
* @return Size in bytes of this mask | |||
*/ | |||
public int byteSize() { | |||
return _mask.length; | |||
} | |||
} |
@@ -0,0 +1,135 @@ | |||
/* | |||
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; | |||
import java.io.IOException; | |||
import java.nio.ByteBuffer; | |||
import java.nio.ByteOrder; | |||
import java.nio.channels.Channel; | |||
import java.nio.channels.FileChannel; | |||
import org.apache.commons.logging.Log; | |||
import org.apache.commons.logging.LogFactory; | |||
/** | |||
* Reads and writes individual pages in a database file | |||
* @author Tim McCune | |||
*/ | |||
public class PageChannel implements Channel { | |||
private static final Log LOG = LogFactory.getLog(PageChannel.class); | |||
/** Global usage map always lives on page 1 */ | |||
private static final int PAGE_GLOBAL_USAGE_MAP = 1; | |||
/** Channel containing the database */ | |||
private FileChannel _channel; | |||
/** Format of the database in the channel */ | |||
private JetFormat _format; | |||
/** Tracks free pages in the database. */ | |||
private UsageMap _globalUsageMap; | |||
/** | |||
* @param channel Channel containing the database | |||
* @param format Format of the database in the channel | |||
*/ | |||
public PageChannel(FileChannel channel, JetFormat format) throws IOException { | |||
_channel = channel; | |||
_format = format; | |||
//Null check only exists for unit tests. Channel should never normally be null. | |||
if (channel != null) { | |||
_globalUsageMap = UsageMap.read(this, PAGE_GLOBAL_USAGE_MAP, (byte) 0, format); | |||
} | |||
} | |||
/** | |||
* @param buffer Buffer to read the page into | |||
* @param pageNumber Number of the page to read in (starting at 0) | |||
* @return True if the page was successfully read into the buffer, false if | |||
* that page doesn't exist. | |||
*/ | |||
public boolean readPage(ByteBuffer buffer, int pageNumber) throws IOException { | |||
if (LOG.isDebugEnabled()) { | |||
LOG.debug("Reading in page " + Integer.toHexString(pageNumber)); | |||
} | |||
buffer.clear(); | |||
boolean rtn = _channel.read(buffer, (long) pageNumber * (long) _format.PAGE_SIZE) != -1; | |||
buffer.flip(); | |||
return rtn; | |||
} | |||
/** | |||
* Write a page to disk | |||
* @param page Page to write | |||
* @param pageNumber Page number to write the page to | |||
*/ | |||
public void writePage(ByteBuffer page, int pageNumber) throws IOException { | |||
page.rewind(); | |||
_channel.write(page, (long) pageNumber * (long) _format.PAGE_SIZE); | |||
_channel.force(true); | |||
} | |||
/** | |||
* Write a page to disk as a new page, appending it to the database | |||
* @param page Page to write | |||
* @return Page number at which the page was written | |||
*/ | |||
public int writeNewPage(ByteBuffer page) throws IOException { | |||
long size = _channel.size(); | |||
page.rewind(); | |||
_channel.write(page, size); | |||
int pageNumber = (int) (size / _format.PAGE_SIZE); | |||
_globalUsageMap.removePageNumber(pageNumber); //force is done here | |||
return pageNumber; | |||
} | |||
/** | |||
* @return Number of pages in the database | |||
*/ | |||
public int getPageCount() throws IOException { | |||
return (int) (_channel.size() / _format.PAGE_SIZE); | |||
} | |||
/** | |||
* @return A newly-allocated buffer that can be passed to readPage | |||
*/ | |||
public ByteBuffer createPageBuffer() { | |||
ByteBuffer rtn = ByteBuffer.allocate(_format.PAGE_SIZE); | |||
rtn.order(ByteOrder.LITTLE_ENDIAN); | |||
return rtn; | |||
} | |||
public void close() throws IOException { | |||
_channel.force(true); | |||
_channel.close(); | |||
} | |||
public boolean isOpen() { | |||
return _channel.isOpen(); | |||
} | |||
} |
@@ -0,0 +1,43 @@ | |||
/* | |||
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; | |||
/** | |||
* Codes for page types | |||
* @author Tim McCune | |||
*/ | |||
public interface PageTypes { | |||
/** Data page */ | |||
public static final byte DATA = 0x1; | |||
/** Table definition page */ | |||
public static final byte TABLE_DEF = 0x2; | |||
/** Table usage map page */ | |||
public static final byte USAGE_MAP = 0x5; | |||
} |
@@ -0,0 +1,118 @@ | |||
/* | |||
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; | |||
import java.io.IOException; | |||
import java.nio.ByteBuffer; | |||
/** | |||
* Usage map whose map is written across one or more entire separate pages of | |||
* page type USAGE_MAP. This type of map can contain 32736 pages per reference | |||
* page, and a maximum of 16 reference map pages for a total maximum of 523776 | |||
* pages (2 GB). | |||
* @author Tim McCune | |||
*/ | |||
public class ReferenceUsageMap extends UsageMap { | |||
/** Buffer that contains the current reference map page */ | |||
private ByteBuffer _mapPageBuffer; | |||
/** Page number of the reference map page that was last read */ | |||
private int _mapPageNum; | |||
/** | |||
* @param pageChannel Used to read in pages | |||
* @param dataBuffer Buffer that contains this map's declaration | |||
* @param pageNum Page number that this usage map is contained in | |||
* @param format Format of the database that contains this usage map | |||
* @param rowStart Offset at which the declaration starts in the buffer | |||
*/ | |||
public ReferenceUsageMap(PageChannel pageChannel, ByteBuffer dataBuffer, | |||
int pageNum, JetFormat format, short rowStart) | |||
throws IOException | |||
{ | |||
super(pageChannel, dataBuffer, pageNum, format, rowStart); | |||
_mapPageBuffer = pageChannel.createPageBuffer(); | |||
for (int i = 0; i < 17; i++) { | |||
_mapPageNum = dataBuffer.getInt(getRowStart() + | |||
format.OFFSET_REFERENCE_MAP_PAGE_NUMBERS + (4 * i)); | |||
if (_mapPageNum > 0) { | |||
pageChannel.readPage(_mapPageBuffer, _mapPageNum); | |||
byte pageType = _mapPageBuffer.get(); | |||
if (pageType != PageTypes.USAGE_MAP) { | |||
throw new IOException("Looking for usage map at page " + _mapPageNum + | |||
", but page type is " + pageType); | |||
} | |||
_mapPageBuffer.position(format.OFFSET_USAGE_MAP_PAGE_DATA); | |||
setStartOffset(_mapPageBuffer.position()); | |||
processMap(_mapPageBuffer, i, 0); | |||
} | |||
} | |||
} | |||
//Javadoc copied from UsageMap | |||
protected void addOrRemovePageNumber(final int pageNumber, boolean add) | |||
throws IOException | |||
{ | |||
int pageIndex = (int) Math.floor(pageNumber / getFormat().PAGES_PER_USAGE_MAP_PAGE); | |||
int mapPageNumber = getDataBuffer().getInt(calculateMapPagePointerOffset(pageIndex)); | |||
if (mapPageNumber > 0) { | |||
if (_mapPageNum != mapPageNumber) { | |||
//Need to read in the map page | |||
getPageChannel().readPage(_mapPageBuffer, mapPageNumber); | |||
_mapPageNum = mapPageNumber; | |||
} | |||
} else { | |||
//Need to create a new usage map page | |||
createNewUsageMapPage(pageIndex); | |||
} | |||
updateMap(pageNumber, pageNumber - (getFormat().PAGES_PER_USAGE_MAP_PAGE * pageIndex), | |||
1 << ((pageNumber - (getFormat().PAGES_PER_USAGE_MAP_PAGE * pageIndex)) % 8), | |||
_mapPageBuffer, add); | |||
getPageChannel().writePage(_mapPageBuffer, _mapPageNum); | |||
} | |||
/** | |||
* Create a new usage map page and update the map declaration with a pointer | |||
* to it. | |||
* @param pageIndex Index of the page reference within the map declaration | |||
*/ | |||
private void createNewUsageMapPage(int pageIndex) throws IOException { | |||
_mapPageBuffer = getPageChannel().createPageBuffer(); | |||
_mapPageBuffer.put(PageTypes.USAGE_MAP); | |||
_mapPageBuffer.put((byte) 0x01); //Unknown | |||
_mapPageBuffer.putShort((short) 0); //Unknown | |||
_mapPageNum = getPageChannel().writeNewPage(_mapPageBuffer); | |||
getDataBuffer().putInt(calculateMapPagePointerOffset(pageIndex), _mapPageNum); | |||
getPageChannel().writePage(getDataBuffer(), getDataPageNumber()); | |||
} | |||
private int calculateMapPagePointerOffset(int pageIndex) { | |||
return getRowStart() + getFormat().OFFSET_REFERENCE_MAP_PAGE_NUMBERS + (pageIndex * 4); | |||
} | |||
} |
@@ -0,0 +1,559 @@ | |||
/* | |||
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; | |||
import java.io.IOException; | |||
import java.nio.ByteBuffer; | |||
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.logging.Log; | |||
import org.apache.commons.logging.LogFactory; | |||
/** | |||
* A single database table | |||
* @author Tim McCune | |||
*/ | |||
public class Table { | |||
private static final Log LOG = LogFactory.getLog(Table.class); | |||
/** Table type code for system tables */ | |||
public static final byte TYPE_SYSTEM = 0x53; | |||
/** Table type code for user tables */ | |||
public static final byte TYPE_USER = 0x4e; | |||
/** Buffer used for reading the table */ | |||
private ByteBuffer _buffer; | |||
/** Type of the table (either TYPE_SYSTEM or TYPE_USER) */ | |||
private byte _tableType; | |||
/** Number of the current row in a data page */ | |||
private int _currentRowInPage; | |||
/** Number of indexes on the table */ | |||
private int _indexCount; | |||
/** Offset index in the buffer where the last row read started */ | |||
private short _lastRowStart; | |||
/** Number of rows in the table */ | |||
private int _rowCount; | |||
private int _tableDefPageNumber; | |||
/** Number of rows left to be read on the current page */ | |||
private short _rowsLeftOnPage = 0; | |||
/** Offset index in the buffer of the start of the current row */ | |||
private short _rowStart; | |||
/** Number of columns in the table */ | |||
private short _columnCount; | |||
/** Format of the database that contains this table */ | |||
private JetFormat _format; | |||
/** List of columns in this table (Column) */ | |||
private List _columns = new ArrayList(); | |||
/** List of indexes on this table (Index) */ | |||
private List _indexes = new ArrayList(); | |||
/** Used to read in pages */ | |||
private PageChannel _pageChannel; | |||
/** 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; | |||
/** | |||
* Only used by unit tests | |||
*/ | |||
Table() throws IOException { | |||
_pageChannel = new PageChannel(null, JetFormat.VERSION_4); | |||
} | |||
/** | |||
* @param buffer Buffer to read the table with | |||
* @param pageChannel Page channel to get database pages from | |||
* @param format Format of the database that contains this table | |||
* @param pageNumber Page number of the table definition | |||
*/ | |||
protected Table(ByteBuffer buffer, PageChannel pageChannel, JetFormat format, int pageNumber) | |||
throws IOException | |||
{ | |||
_buffer = buffer; | |||
_pageChannel = pageChannel; | |||
_format = format; | |||
_tableDefPageNumber = pageNumber; | |||
int nextPage; | |||
do { | |||
readPage(); | |||
nextPage = _buffer.getInt(_format.OFFSET_NEXT_TABLE_DEF_PAGE); | |||
} while (nextPage > 0); | |||
} | |||
/** | |||
* @return All of the columns in this table (unmodifiable List) | |||
*/ | |||
public List getColumns() { | |||
return Collections.unmodifiableList(_columns); | |||
} | |||
/** | |||
* Only called by unit tests | |||
*/ | |||
void setColumns(List columns) { | |||
_columns = columns; | |||
} | |||
/** | |||
* @return All of the Indexes on this table (unmodifiable List) | |||
*/ | |||
public List getIndexes() { | |||
return Collections.unmodifiableList(_indexes); | |||
} | |||
/** | |||
* After calling this method, getNextRow will return the first row in the table | |||
*/ | |||
public void reset() { | |||
_rowsLeftOnPage = 0; | |||
_ownedPages.reset(); | |||
} | |||
/** | |||
* @return The next row in this table (Column name (String) -> Column value (Object)) | |||
*/ | |||
public Map getNextRow() throws IOException { | |||
return getNextRow(null); | |||
} | |||
/** | |||
* @param columnNames Only column names in this collection will be returned | |||
* @return The next row in this table (Column name (String) -> Column value (Object)) | |||
*/ | |||
public Map getNextRow(Collection columnNames) throws IOException { | |||
if (!positionAtNextRow()) { | |||
return null; | |||
} | |||
if (LOG.isDebugEnabled()) { | |||
LOG.debug("Data block at position " + Integer.toHexString(_buffer.position()) + | |||
":\n" + ByteUtil.toHexString(_buffer, _buffer.position(), | |||
_buffer.limit() - _buffer.position())); | |||
} | |||
short columnCount = _buffer.getShort(); //Number of columns in this table | |||
Map rtn = new LinkedHashMap(columnCount); | |||
NullMask nullMask = new NullMask(columnCount); | |||
_buffer.position(_buffer.limit() - nullMask.byteSize()); //Null mask at end | |||
nullMask.read(_buffer); | |||
_buffer.position(_buffer.limit() - nullMask.byteSize() - 2); | |||
short varColumnCount = _buffer.getShort(); //Number of variable length columns | |||
byte[][] varColumnData = new byte[varColumnCount][]; //Holds variable length column data | |||
//Read in the offsets of each of the variable length columns | |||
short[] varColumnOffsets = new short[varColumnCount]; | |||
_buffer.position(_buffer.position() - 2 - (varColumnCount * 2) - 2); | |||
short lastVarColumnStart = _buffer.getShort(); | |||
for (short i = 0; i < varColumnCount; i++) { | |||
varColumnOffsets[i] = _buffer.getShort(); | |||
} | |||
//Read in the actual data for each of the variable length columns | |||
for (short i = 0; i < varColumnCount; i++) { | |||
_buffer.position(_rowStart + varColumnOffsets[i]); | |||
varColumnData[i] = new byte[lastVarColumnStart - varColumnOffsets[i]]; | |||
_buffer.get(varColumnData[i]); | |||
lastVarColumnStart = varColumnOffsets[i]; | |||
} | |||
int columnNumber = 0; | |||
int varColumnDataIndex = varColumnCount - 1; | |||
_buffer.position(_rowStart + 2); //Move back to the front of the buffer | |||
//Now read in the fixed length columns and populate the columnData array | |||
//with the combination of fixed length and variable length data. | |||
byte[] columnData; | |||
for (Iterator iter = _columns.iterator(); iter.hasNext(); columnNumber++) { | |||
Column column = (Column) iter.next(); | |||
boolean isNull = nullMask.isNull(columnNumber); | |||
Object value = null; | |||
if (column.getType() == DataTypes.BOOLEAN) { | |||
value = new Boolean(!isNull); //Boolean values are stored in the null mask | |||
} else if (!isNull) { | |||
if (!column.isVariableLength()) { | |||
//Read in fixed length column data | |||
columnData = new byte[column.size()]; | |||
_buffer.get(columnData); | |||
} else { | |||
//Refer to already-read-in variable length data | |||
columnData = varColumnData[varColumnDataIndex--]; | |||
} | |||
if (columnNames == null || columnNames.contains(column.getName())) { | |||
//Add the value if we are interested in it. | |||
value = column.read(columnData); | |||
} | |||
} | |||
rtn.put(column.getName(), value); | |||
} | |||
return rtn; | |||
} | |||
/** | |||
* Position the buffer at the next row in the table | |||
* @return True if another row was found, false if there are no more rows | |||
*/ | |||
private boolean positionAtNextRow() throws IOException { | |||
if (_rowsLeftOnPage == 0) { | |||
do { | |||
if (!_ownedPages.getNextPage(_buffer)) { | |||
//No more owned pages. No more rows. | |||
return false; | |||
} | |||
} while (_buffer.get() != PageTypes.DATA); //Only interested in data pages | |||
_rowsLeftOnPage = _buffer.getShort(_format.OFFSET_NUM_ROWS_ON_DATA_PAGE); | |||
_currentRowInPage = 0; | |||
_lastRowStart = (short) _format.PAGE_SIZE; | |||
} | |||
_rowStart = _buffer.getShort(_format.OFFSET_DATA_ROW_LOCATION_BLOCK + | |||
_currentRowInPage * _format.SIZE_ROW_LOCATION); | |||
// XXX - Handle overflow pages and deleted rows. | |||
_buffer.position(_rowStart); | |||
_buffer.limit(_lastRowStart); | |||
_rowsLeftOnPage--; | |||
_currentRowInPage++; | |||
_lastRowStart = _rowStart; | |||
return true; | |||
} | |||
/** | |||
* Read the table definition | |||
*/ | |||
private void readPage() throws IOException { | |||
if (LOG.isDebugEnabled()) { | |||
_buffer.rewind(); | |||
LOG.debug("Table def block:\n" + ByteUtil.toHexString(_buffer, | |||
_format.SIZE_TDEF_BLOCK)); | |||
} | |||
_rowCount = _buffer.getInt(_format.OFFSET_NUM_ROWS); | |||
_tableType = _buffer.get(_format.OFFSET_TABLE_TYPE); | |||
_columnCount = _buffer.getShort(_format.OFFSET_NUM_COLS); | |||
_indexCount = _buffer.getInt(_format.OFFSET_NUM_INDEXES); | |||
byte rowNum = _buffer.get(_format.OFFSET_OWNED_PAGES); | |||
int pageNum = ByteUtil.get3ByteInt(_buffer, _format.OFFSET_OWNED_PAGES + 1); | |||
_ownedPages = UsageMap.read(_pageChannel, pageNum, rowNum, _format); | |||
rowNum = _buffer.get(_format.OFFSET_FREE_SPACE_PAGES); | |||
pageNum = ByteUtil.get3ByteInt(_buffer, _format.OFFSET_FREE_SPACE_PAGES + 1); | |||
_freeSpacePages = UsageMap.read(_pageChannel, pageNum, rowNum, _format); | |||
for (int i = 0; i < _indexCount; i++) { | |||
Index index = new Index(_tableDefPageNumber, _pageChannel, _format); | |||
_indexes.add(index); | |||
index.setRowCount(_buffer.getInt(_format.OFFSET_INDEX_DEF_BLOCK + | |||
i * _format.SIZE_INDEX_DEFINITION + 4)); | |||
} | |||
int offset = _format.OFFSET_INDEX_DEF_BLOCK + | |||
_indexCount * _format.SIZE_INDEX_DEFINITION; | |||
Column column; | |||
for (int i = 0; i < _columnCount; i++) { | |||
column = new Column(_buffer, | |||
offset + i * _format.SIZE_COLUMN_HEADER, _pageChannel, _format); | |||
_columns.add(column); | |||
} | |||
offset += _columnCount * _format.SIZE_COLUMN_HEADER; | |||
for (int i = 0; i < _columnCount; i++) { | |||
column = (Column) _columns.get(i); | |||
short nameLength = _buffer.getShort(offset); | |||
offset += 2; | |||
byte[] nameBytes = new byte[nameLength]; | |||
_buffer.position(offset); | |||
_buffer.get(nameBytes, 0, (int) nameLength); | |||
column.setName(_format.CHARSET.decode(ByteBuffer.wrap(nameBytes)).toString()); | |||
offset += nameLength; | |||
} | |||
Collections.sort(_columns); | |||
for (int i = 0; i < _indexCount; i++) { | |||
_buffer.getInt(); //Forward past Unknown | |||
((Index) _indexes.get(i)).read(_buffer, _columns); | |||
} | |||
for (int i = 0; i < _indexCount; i++) { | |||
_buffer.getInt(); //Forward past Unknown | |||
((Index) _indexes.get(i)).setIndexNumber(_buffer.getInt()); | |||
_buffer.position(_buffer.position() + 20); | |||
} | |||
Collections.sort(_indexes); | |||
for (int i = 0; i < _indexCount; i++) { | |||
byte[] nameBytes = new byte[_buffer.getShort()]; | |||
_buffer.get(nameBytes); | |||
((Index) _indexes.get(i)).setName(_format.CHARSET.decode(ByteBuffer.wrap( | |||
nameBytes)).toString()); | |||
} | |||
} | |||
/** | |||
* Add a single row to this table and write it to disk | |||
*/ | |||
public void addRow(Object[] row) throws IOException { | |||
List rows = new ArrayList(1); | |||
rows.add(row); | |||
addRows(rows); | |||
} | |||
/** | |||
* 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. | |||
* @param rows List of Object[] row values | |||
*/ | |||
public void addRows(List rows) throws IOException { | |||
ByteBuffer dataPage = _pageChannel.createPageBuffer(); | |||
ByteBuffer[] rowData = new ByteBuffer[rows.size()]; | |||
Iterator iter = rows.iterator(); | |||
for (int i = 0; iter.hasNext(); i++) { | |||
rowData[i] = createRow((Object[]) iter.next()); | |||
} | |||
List pageNumbers = _ownedPages.getPageNumbers(); | |||
int pageNumber; | |||
int rowSize; | |||
if (pageNumbers.size() == 0) { | |||
//No data pages exist. Create a new one. | |||
pageNumber = newDataPage(dataPage, rowData[0]); | |||
} else { | |||
//Get the last data page. | |||
//Not bothering to check other pages for free space. | |||
pageNumber = ((Integer) pageNumbers.get(pageNumbers.size() - 1)).intValue(); | |||
_pageChannel.readPage(dataPage, pageNumber); | |||
} | |||
for (int i = 0; i < rowData.length; i++) { | |||
rowSize = rowData[i].limit(); | |||
short freeSpaceInPage = dataPage.getShort(_format.OFFSET_FREE_SPACE); | |||
if (freeSpaceInPage < (rowSize + _format.SIZE_ROW_LOCATION)) { | |||
//Last data page is full. Create a new one. | |||
if (rowSize + _format.SIZE_ROW_LOCATION > _format.MAX_ROW_SIZE) { | |||
throw new IOException("Row size " + rowSize + " is too large"); | |||
} | |||
_pageChannel.writePage(dataPage, pageNumber); | |||
dataPage.clear(); | |||
pageNumber = newDataPage(dataPage, rowData[i]); | |||
_freeSpacePages.removePageNumber(pageNumber); | |||
freeSpaceInPage = dataPage.getShort(_format.OFFSET_FREE_SPACE); | |||
} | |||
//Decrease free space record. | |||
dataPage.putShort(_format.OFFSET_FREE_SPACE, (short) (freeSpaceInPage - | |||
rowSize - _format.SIZE_ROW_LOCATION)); | |||
//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)); | |||
short rowLocation = (short) _format.PAGE_SIZE; | |||
if (rowCount > 0) { | |||
rowLocation = dataPage.getShort(_format.OFFSET_DATA_ROW_LOCATION_BLOCK + | |||
(rowCount - 1) * _format.SIZE_ROW_LOCATION); | |||
} | |||
rowLocation -= rowSize; | |||
dataPage.putShort(_format.OFFSET_DATA_ROW_LOCATION_BLOCK + | |||
rowCount * _format.SIZE_ROW_LOCATION, rowLocation); | |||
dataPage.position(rowLocation); | |||
dataPage.put(rowData[i]); | |||
iter = _indexes.iterator(); | |||
while (iter.hasNext()) { | |||
Index index = (Index) iter.next(); | |||
index.addRow((Object[]) rows.get(i), pageNumber, (byte) rowCount); | |||
} | |||
} | |||
_pageChannel.writePage(dataPage, pageNumber); | |||
//Update tdef page | |||
ByteBuffer tdefPage = _pageChannel.createPageBuffer(); | |||
_pageChannel.readPage(tdefPage, _tableDefPageNumber); | |||
tdefPage.putInt(_format.OFFSET_NUM_ROWS, ++_rowCount); | |||
iter = _indexes.iterator(); | |||
for (int i = 0; i < _indexes.size(); i++) { | |||
tdefPage.putInt(_format.OFFSET_INDEX_DEF_BLOCK + | |||
i * _format.SIZE_INDEX_DEFINITION + 4, _rowCount); | |||
Index index = (Index) iter.next(); | |||
index.update(); | |||
} | |||
_pageChannel.writePage(tdefPage, _tableDefPageNumber); | |||
} | |||
/** | |||
* Create a new data page | |||
* @return Page number of the new page | |||
*/ | |||
private int newDataPage(ByteBuffer dataPage, ByteBuffer rowData) throws IOException { | |||
if (LOG.isDebugEnabled()) { | |||
LOG.debug("Creating new data page"); | |||
} | |||
dataPage.put(PageTypes.DATA); //Page type | |||
dataPage.put((byte) 1); //Unknown | |||
dataPage.putShort((short) (_format.PAGE_SIZE - _format.OFFSET_DATA_ROW_LOCATION_BLOCK - | |||
(rowData.limit() - 1) - _format.SIZE_ROW_LOCATION)); //Free space in this page | |||
dataPage.putInt(_tableDefPageNumber); //Page pointer to table definition | |||
dataPage.putInt(0); //Unknown | |||
dataPage.putInt(0); //Number of records on this page | |||
int pageNumber = _pageChannel.writeNewPage(dataPage); | |||
_ownedPages.addPageNumber(pageNumber); | |||
_freeSpacePages.addPageNumber(pageNumber); | |||
return pageNumber; | |||
} | |||
/** | |||
* Serialize a row of Objects into a byte buffer | |||
*/ | |||
ByteBuffer createRow(Object[] rowArray) throws IOException { | |||
ByteBuffer buffer = _pageChannel.createPageBuffer(); | |||
buffer.putShort((short) _columns.size()); | |||
NullMask nullMask = new NullMask(_columns.size()); | |||
Iterator iter; | |||
int index = 0; | |||
Column col; | |||
List row = new ArrayList(Arrays.asList(rowArray)); | |||
//Append null for arrays that are too small | |||
for (int i = rowArray.length; i < _columnCount; i++) { | |||
row.add(null); | |||
} | |||
for (iter = _columns.iterator(); iter.hasNext() && index < row.size(); index++) { | |||
col = (Column) iter.next(); | |||
if (!col.isVariableLength()) { | |||
//Fixed length column data comes first | |||
if (row.get(index) != null) { | |||
buffer.put(col.write(row.get(index))); | |||
} | |||
} | |||
if (col.getType() == DataTypes.BOOLEAN) { | |||
if (row.get(index) != null) { | |||
if (!((Boolean) row.get(index)).booleanValue()) { | |||
//Booleans are stored in the null mask | |||
nullMask.markNull(index); | |||
} | |||
} | |||
} else if (row.get(index) == null) { | |||
nullMask.markNull(index); | |||
} | |||
} | |||
int varLengthCount = Column.countVariableLength(_columns); | |||
short[] varColumnOffsets = new short[varLengthCount]; | |||
index = 0; | |||
int varColumnOffsetsIndex = 0; | |||
//Now write out variable length column data | |||
for (iter = _columns.iterator(); iter.hasNext() && index < row.size(); index++) { | |||
col = (Column) iter.next(); | |||
short offset = (short) buffer.position(); | |||
if (col.isVariableLength()) { | |||
if (row.get(index) != null) { | |||
buffer.put(col.write(row.get(index))); | |||
} | |||
varColumnOffsets[varColumnOffsetsIndex++] = offset; | |||
} | |||
} | |||
buffer.putShort((short) buffer.position()); //EOD marker | |||
//Now write out variable length offsets | |||
//Offsets are stored in reverse order | |||
for (int i = varColumnOffsets.length - 1; i >= 0; i--) { | |||
buffer.putShort(varColumnOffsets[i]); | |||
} | |||
buffer.putShort((short) varLengthCount); //Number of var length columns | |||
buffer.put(nullMask.wrap()); //Null mask | |||
buffer.limit(buffer.position()); | |||
buffer.flip(); | |||
if (LOG.isDebugEnabled()) { | |||
LOG.debug("Creating new data block:\n" + ByteUtil.toHexString(buffer, buffer.limit())); | |||
} | |||
return buffer; | |||
} | |||
public String toString() { | |||
StringBuffer rtn = new StringBuffer(); | |||
rtn.append("Type: " + _tableType); | |||
rtn.append("\nRow count: " + _rowCount); | |||
rtn.append("\nColumn count: " + _columnCount); | |||
rtn.append("\nIndex count: " + _indexCount); | |||
rtn.append("\nColumns:\n"); | |||
Iterator iter = _columns.iterator(); | |||
while (iter.hasNext()) { | |||
rtn.append(iter.next().toString()); | |||
} | |||
rtn.append("\nIndexes:\n"); | |||
iter = _indexes.iterator(); | |||
while (iter.hasNext()) { | |||
rtn.append(iter.next().toString()); | |||
} | |||
rtn.append("\nOwned pages: " + _ownedPages + "\n"); | |||
return rtn.toString(); | |||
} | |||
/** | |||
* @return A simple String representation of the entire table in tab-delimited format | |||
*/ | |||
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 | |||
*/ | |||
public String display(long limit) throws IOException { | |||
reset(); | |||
StringBuffer rtn = new StringBuffer(); | |||
Iterator iter = _columns.iterator(); | |||
while (iter.hasNext()) { | |||
Column col = (Column) iter.next(); | |||
rtn.append(col.getName()); | |||
if (iter.hasNext()) { | |||
rtn.append("\t"); | |||
} | |||
} | |||
rtn.append("\n"); | |||
Map row; | |||
int rowCount = 0; | |||
while ((rowCount++ < limit) && (row = getNextRow()) != null) { | |||
iter = row.values().iterator(); | |||
while (iter.hasNext()) { | |||
Object obj = iter.next(); | |||
if (obj instanceof byte[]) { | |||
byte[] b = (byte[]) obj; | |||
rtn.append(ByteUtil.toHexString(ByteBuffer.wrap(b), b.length)); | |||
//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(); | |||
} | |||
} |
@@ -0,0 +1,239 @@ | |||
/* | |||
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; | |||
import java.io.IOException; | |||
import java.nio.ByteBuffer; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import org.apache.commons.logging.Log; | |||
import org.apache.commons.logging.LogFactory; | |||
/** | |||
* Describes which database pages a particular table uses | |||
* @author Tim McCune | |||
*/ | |||
public abstract class UsageMap { | |||
private static final Log LOG = LogFactory.getLog(UsageMap.class); | |||
/** Inline map type */ | |||
public static final byte MAP_TYPE_INLINE = 0x0; | |||
/** Reference map type, for maps that are too large to fit inline */ | |||
public static final byte MAP_TYPE_REFERENCE = 0x1; | |||
/** Index of the current page, incremented after calling getNextPage */ | |||
private int _currentPageIndex = 0; | |||
/** Page number of the map declaration */ | |||
private int _dataPageNum; | |||
/** Offset of the data page at which the usage map data starts */ | |||
private int _startOffset; | |||
/** Offset of the data page at which the usage map declaration starts */ | |||
private short _rowStart; | |||
/** Format of the database that contains this usage map */ | |||
private JetFormat _format; | |||
/** List of page numbers used (Integer) */ | |||
private List _pageNumbers = new ArrayList(); | |||
/** Buffer that contains the usage map declaration page */ | |||
private ByteBuffer _dataBuffer; | |||
/** Used to read in pages */ | |||
private PageChannel _pageChannel; | |||
/** | |||
* @param pageChannel Used to read in pages | |||
* @param pageNum Page number that this usage map is contained in | |||
* @param rowNum Number of the row on the page that contains this usage map | |||
* @param format Format of the database that contains this usage map | |||
* @return Either an InlineUsageMap or a ReferenceUsageMap, depending on which | |||
* type of map is found | |||
*/ | |||
public static UsageMap read(PageChannel pageChannel, int pageNum, byte rowNum, JetFormat format) | |||
throws IOException | |||
{ | |||
ByteBuffer dataBuffer = pageChannel.createPageBuffer(); | |||
pageChannel.readPage(dataBuffer, pageNum); | |||
short rowStart = dataBuffer.getShort(format.OFFSET_ROW_START + 2 * rowNum); | |||
int rowEnd; | |||
if (rowNum == 0) { | |||
rowEnd = format.PAGE_SIZE - 1; | |||
} else { | |||
rowEnd = (dataBuffer.getShort(format.OFFSET_ROW_START + (rowNum - 1) * 2) & 0x0FFF) - 1; | |||
} | |||
dataBuffer.limit(rowEnd + 1); | |||
byte mapType = dataBuffer.get(rowStart); | |||
UsageMap rtn; | |||
if (mapType == MAP_TYPE_INLINE) { | |||
rtn = new InlineUsageMap(pageChannel, dataBuffer, pageNum, format, rowStart); | |||
} else if (mapType == MAP_TYPE_REFERENCE) { | |||
rtn = new ReferenceUsageMap(pageChannel, dataBuffer, pageNum, format, rowStart); | |||
} else { | |||
throw new IOException("Unrecognized map type: " + mapType); | |||
} | |||
return rtn; | |||
} | |||
/** | |||
* @param pageChannel Used to read in pages | |||
* @param dataBuffer Buffer that contains this map's declaration | |||
* @param pageNum Page number that this usage map is contained in | |||
* @param format Format of the database that contains this usage map | |||
* @param rowStart Offset at which the declaration starts in the buffer | |||
*/ | |||
public UsageMap(PageChannel pageChannel, ByteBuffer dataBuffer, int pageNum, | |||
JetFormat format, short rowStart) | |||
throws IOException | |||
{ | |||
_pageChannel = pageChannel; | |||
_dataBuffer = dataBuffer; | |||
_dataPageNum = pageNum; | |||
_format = format; | |||
_rowStart = rowStart; | |||
_dataBuffer.position((int) _rowStart + format.OFFSET_MAP_START); | |||
_startOffset = _dataBuffer.position(); | |||
if (LOG.isDebugEnabled()) { | |||
LOG.debug("Usage map block:\n" + ByteUtil.toHexString(_dataBuffer, _rowStart, | |||
dataBuffer.limit() - _rowStart)); | |||
} | |||
} | |||
protected short getRowStart() { | |||
return _rowStart; | |||
} | |||
public List getPageNumbers() { | |||
return _pageNumbers; | |||
} | |||
protected void setStartOffset(int startOffset) { | |||
_startOffset = startOffset; | |||
} | |||
protected int getStartOffset() { | |||
return _startOffset; | |||
} | |||
protected ByteBuffer getDataBuffer() { | |||
return _dataBuffer; | |||
} | |||
protected int getDataPageNumber() { | |||
return _dataPageNum; | |||
} | |||
protected PageChannel getPageChannel() { | |||
return _pageChannel; | |||
} | |||
protected JetFormat getFormat() { | |||
return _format; | |||
} | |||
/** | |||
* After calling this method, getNextPage will return the first page in the map | |||
*/ | |||
public void reset() { | |||
_currentPageIndex = 0; | |||
} | |||
/** | |||
* @param buffer Buffer to read the next page into | |||
* @return Whether or not there was another page to read | |||
*/ | |||
public boolean getNextPage(ByteBuffer buffer) throws IOException { | |||
if (_pageNumbers.size() > _currentPageIndex) { | |||
Integer pageNumber = (Integer) _pageNumbers.get(_currentPageIndex++); | |||
_pageChannel.readPage(buffer, pageNumber.intValue()); | |||
return true; | |||
} else { | |||
return false; | |||
} | |||
} | |||
/** | |||
* Read in the page numbers in this inline map | |||
*/ | |||
protected void processMap(ByteBuffer buffer, int pageIndex, int startPage) { | |||
int byteCount = 0; | |||
while (buffer.hasRemaining()) { | |||
byte b = buffer.get(); | |||
for (int i = 0; i < 8; i++) { | |||
if ((b & (1 << i)) != 0) { | |||
Integer pageNumber = new Integer((startPage + byteCount * 8 + i) + | |||
(pageIndex * _format.PAGES_PER_USAGE_MAP_PAGE)); | |||
_pageNumbers.add(pageNumber); | |||
} | |||
} | |||
byteCount++; | |||
} | |||
} | |||
/** | |||
* Add a page number to this usage map | |||
*/ | |||
public void addPageNumber(int pageNumber) throws IOException { | |||
//Sanity check, only on in debug mode for performance considerations | |||
if (LOG.isDebugEnabled() && _pageNumbers.contains(new Integer(pageNumber))) { | |||
throw new IOException("Page number " + pageNumber + " already in usage map"); | |||
} | |||
addOrRemovePageNumber(pageNumber, true); | |||
} | |||
/** | |||
* Remove a page number from this usage map | |||
*/ | |||
public void removePageNumber(int pageNumber) throws IOException { | |||
addOrRemovePageNumber(pageNumber, false); | |||
} | |||
protected void updateMap(int absolutePageNumber, int relativePageNumber, | |||
int bitmask, ByteBuffer buffer, boolean add) | |||
{ | |||
//Find the byte to apply the bitmask to | |||
int offset = relativePageNumber / 8; | |||
byte b = buffer.get(_startOffset + offset); | |||
//Apply the bitmask | |||
if (add) { | |||
b |= bitmask; | |||
_pageNumbers.add(new Integer(absolutePageNumber)); | |||
} else { | |||
b &= ~bitmask; | |||
} | |||
buffer.put(_startOffset + offset, b); | |||
} | |||
public String toString() { | |||
return "page numbers: " + _pageNumbers; | |||
} | |||
/** | |||
* @param pageNumber Page number to add or remove from this map | |||
* @param add True to add it, false to remove it | |||
*/ | |||
protected abstract void addOrRemovePageNumber(int pageNumber, boolean add) throws IOException; | |||
} |
@@ -0,0 +1,151 @@ | |||
package com.healthmarketscience.jackcess.scsu; | |||
import org.apache.commons.logging.Log; | |||
import org.apache.commons.logging.LogFactory; | |||
/* | |||
* This sample software accompanies Unicode Technical Report #6 and | |||
* distributed as is by Unicode, Inc., subject to the following: | |||
* | |||
* Copyright © 1996-1997 Unicode, Inc.. All Rights Reserved. | |||
* | |||
* Permission to use, copy, modify, and distribute this software | |||
* without fee is hereby granted provided that this copyright notice | |||
* appears in all copies. | |||
* | |||
* UNICODE, INC. MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE | |||
* SUITABILITY OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING | |||
* BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, | |||
* FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. | |||
* UNICODE, INC., SHALL NOT BE LIABLE FOR ANY ERRORS OR OMISSIONS, AND | |||
* SHALL NOT BE LIABLE FOR ANY DAMAGES, INCLUDING CONSEQUENTIAL AND | |||
* INCIDENTAL DAMAGES, SUFFERED BY YOU AS A RESULT OF USING, MODIFYING | |||
* OR DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. | |||
* | |||
* @author Asmus Freytag | |||
* | |||
* @version 001 Dec 25 1996 | |||
* @version 002 Jun 25 1997 | |||
* @version 003 Jul 25 1997 | |||
* @version 004 Aug 25 1997 | |||
* | |||
* Unicode and the Unicode logo are trademarks of Unicode, Inc., | |||
* and are registered in some jurisdictions. | |||
**/ | |||
/** | |||
* A number of helpful output routines for debugging. Output can be | |||
* centrally enabled or disabled by calling Debug.set(true/false); | |||
* All methods are statics; | |||
*/ | |||
public class Debug | |||
{ | |||
private static final Log LOG = LogFactory.getLog(Debug.class); | |||
// debugging helper | |||
public static void out(char [] chars) | |||
{ | |||
out(chars, 0); | |||
} | |||
public static void out(char [] chars, int iStart) | |||
{ | |||
if (!LOG.isDebugEnabled()) return; | |||
StringBuffer msg = new StringBuffer(); | |||
for (int i = iStart; i < chars.length; i++) | |||
{ | |||
if (chars[i] >= 0 && chars[i] <= 26) | |||
{ | |||
msg.append("^"+(char)(chars[i]+0x40)); | |||
} | |||
else if (chars[i] <= 255) | |||
{ | |||
msg.append(chars[i]); | |||
} | |||
else | |||
{ | |||
msg.append("\\u"+Integer.toString(chars[i],16)); | |||
} | |||
} | |||
LOG.debug(msg.toString()); | |||
} | |||
public static void out(byte [] bytes) | |||
{ | |||
out(bytes, 0); | |||
} | |||
public static void out(byte [] bytes, int iStart) | |||
{ | |||
if (!LOG.isDebugEnabled()) return; | |||
StringBuffer msg = new StringBuffer(); | |||
for (int i = iStart; i < bytes.length; i++) | |||
{ | |||
msg.append(bytes[i]+","); | |||
} | |||
LOG.debug(msg.toString()); | |||
} | |||
public static void out(String str) | |||
{ | |||
if (!LOG.isDebugEnabled()) return; | |||
LOG.debug(str); | |||
} | |||
public static void out(String msg, int iData) | |||
{ | |||
if (!LOG.isDebugEnabled()) return; | |||
LOG.debug(msg + iData); | |||
} | |||
public static void out(String msg, char ch) | |||
{ | |||
if (!LOG.isDebugEnabled()) return; | |||
LOG.debug(msg + "[U+"+Integer.toString(ch,16)+"]" + ch); | |||
} | |||
public static void out(String msg, byte bData) | |||
{ | |||
if (!LOG.isDebugEnabled()) return; | |||
LOG.debug(msg + bData); | |||
} | |||
public static void out(String msg, String str) | |||
{ | |||
if (!LOG.isDebugEnabled()) return; | |||
LOG.debug(msg + str); | |||
} | |||
public static void out(String msg, char [] data) | |||
{ | |||
if (!LOG.isDebugEnabled()) return; | |||
LOG.debug(msg); | |||
out(data); | |||
} | |||
public static void out(String msg, byte [] data) | |||
{ | |||
if (!LOG.isDebugEnabled()) return; | |||
LOG.debug(msg); | |||
out(data); | |||
} | |||
public static void out(String msg, char [] data, int iStart) | |||
{ | |||
if (!LOG.isDebugEnabled()) return; | |||
LOG.debug(msg +"("+iStart+"): "); | |||
out(data, iStart); | |||
} | |||
public static void out(String msg, byte [] data, int iStart) | |||
{ | |||
if (!LOG.isDebugEnabled()) return; | |||
LOG.debug(msg+"("+iStart+"): "); | |||
out(data, iStart); | |||
} | |||
} |
@@ -0,0 +1,46 @@ | |||
package com.healthmarketscience.jackcess.scsu; | |||
/** | |||
* This sample software accompanies Unicode Technical Report #6 and | |||
* distributed as is by Unicode, Inc., subject to the following: | |||
* | |||
* Copyright © 1996-1997 Unicode, Inc.. All Rights Reserved. | |||
* | |||
* Permission to use, copy, modify, and distribute this software | |||
* without fee is hereby granted provided that this copyright notice | |||
* appears in all copies. | |||
* | |||
* UNICODE, INC. MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE | |||
* SUITABILITY OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING | |||
* BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, | |||
* FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. | |||
* UNICODE, INC., SHALL NOT BE LIABLE FOR ANY ERRORS OR OMISSIONS, AND | |||
* SHALL NOT BE LIABLE FOR ANY DAMAGES, INCLUDING CONSEQUENTIAL AND | |||
* INCIDENTAL DAMAGES, SUFFERED BY YOU AS A RESULT OF USING, MODIFYING | |||
* OR DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. | |||
* | |||
* @author Asmus Freytag | |||
* | |||
* @version 001 Dec 25 1996 | |||
* @version 002 Jun 25 1997 | |||
* @version 003 Jul 25 1997 | |||
* @version 004 Aug 25 1997 | |||
* | |||
* Unicode and the Unicode logo are trademarks of Unicode, Inc., | |||
* and are registered in some jurisdictions. | |||
**/ | |||
/** | |||
* The input string or input byte array ended prematurely | |||
* | |||
*/ | |||
public class EndOfInputException | |||
extends java.lang.Exception | |||
{ | |||
public EndOfInputException(){ | |||
super("The input string or input byte array ended prematurely"); | |||
} | |||
public EndOfInputException(String s) { | |||
super(s); | |||
} | |||
} |
@@ -0,0 +1,429 @@ | |||
package com.healthmarketscience.jackcess.scsu; | |||
/* | |||
* This sample software accompanies Unicode Technical Report #6 and | |||
* distributed as is by Unicode, Inc., subject to the following: | |||
* | |||
* Copyright © 1996-1998 Unicode, Inc.. All Rights Reserved. | |||
* | |||
* Permission to use, copy, modify, and distribute this software | |||
* without fee is hereby granted provided that this copyright notice | |||
* appears in all copies. | |||
* | |||
* UNICODE, INC. MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE | |||
* SUITABILITY OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING | |||
* BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, | |||
* FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. | |||
* UNICODE, INC., SHALL NOT BE LIABLE FOR ANY ERRORS OR OMISSIONS, AND | |||
* SHALL NOT BE LIABLE FOR ANY DAMAGES, INCLUDING CONSEQUENTIAL AND | |||
* INCIDENTAL DAMAGES, SUFFERED BY YOU AS A RESULT OF USING, MODIFYING | |||
* OR DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. | |||
* | |||
* @author Asmus Freytag | |||
* | |||
* @version 001 Dec 25 1996 | |||
* @version 002 Jun 25 1997 | |||
* @version 003 Jul 25 1997 | |||
* @version 004 Aug 25 1997 | |||
* @version 005 Sep 30 1998 | |||
* | |||
* Unicode and the Unicode logo are trademarks of Unicode, Inc., | |||
* and are registered in some jurisdictions. | |||
**/ | |||
/** | |||
Reference decoder for the Standard Compression Scheme for Unicode (SCSU) | |||
<H2>Notes on the Java implementation</H2> | |||
A limitation of Java is the exclusive use of a signed byte data type. | |||
The following work arounds are required: | |||
Copying a byte to an integer variable and adding 256 for 'negative' | |||
bytes gives an integer in the range 0-255. | |||
Values of char are between 0x0000 and 0xFFFF in Java. Arithmetic on | |||
char values is unsigned. | |||
Extended characters require an int to store them. The sign is not an | |||
issue because only 1024*1024 + 65536 extended characters exist. | |||
**/ | |||
public class Expand extends SCSU | |||
{ | |||
/** (re-)define (and select) a dynamic window | |||
A sliding window position cannot start at any Unicode value, | |||
so rather than providing an absolute offset, this function takes | |||
an index value which selects among the possible starting values. | |||
Most scripts in Unicode start on or near a half-block boundary | |||
so the default behaviour is to multiply the index by 0x80. Han, | |||
Hangul, Surrogates and other scripts between 0x3400 and 0xDFFF | |||
show very poor locality--therefore no sliding window can be set | |||
there. A jumpOffset is added to the index value to skip that region, | |||
and only 167 index values total are required to select all eligible | |||
half-blocks. | |||
Finally, a few scripts straddle half block boundaries. For them, a | |||
table of fixed offsets is used, and the index values from 0xF9 to | |||
0xFF are used to select these special offsets. | |||
After (re-)defining a windows location it is selected so it is ready | |||
for use. | |||
Recall that all Windows are of the same length (128 code positions). | |||
@param iWindow - index of the window to be (re-)defined | |||
@param bOffset - index for the new offset value | |||
**/ | |||
// @005 protected <-- private here and elsewhere | |||
protected void defineWindow(int iWindow, byte bOffset) | |||
throws IllegalInputException | |||
{ | |||
int iOffset = (bOffset < 0 ? bOffset + 256 : bOffset); | |||
// 0 is a reserved value | |||
if (iOffset == 0) | |||
{ | |||
throw new IllegalInputException(); | |||
} | |||
else if (iOffset < gapThreshold) | |||
{ | |||
dynamicOffset[iWindow] = iOffset << 7; | |||
} | |||
else if (iOffset < reservedStart) | |||
{ | |||
dynamicOffset[iWindow] = (iOffset << 7) + gapOffset; | |||
} | |||
else if (iOffset < fixedThreshold) | |||
{ | |||
// more reserved values | |||
throw new IllegalInputException("iOffset == "+iOffset); | |||
} | |||
else | |||
{ | |||
dynamicOffset[iWindow] = fixedOffset[iOffset - fixedThreshold]; | |||
} | |||
// make the redefined window the active one | |||
selectWindow(iWindow); | |||
} | |||
/** (re-)define (and select) a window as an extended dynamic window | |||
The surrogate area in Unicode allows access to 2**20 codes beyond the | |||
first 64K codes by combining one of 1024 characters from the High | |||
Surrogate Area with one of 1024 characters from the Low Surrogate | |||
Area (see Unicode 2.0 for the details). | |||
The tags SDX and UDX set the window such that each subsequent byte in | |||
the range 80 to FF represents a surrogate pair. The following diagram | |||
shows how the bits in the two bytes following the SDX or UDX, and a | |||
subsequent data byte, map onto the bits in the resulting surrogate pair. | |||
hbyte lbyte data | |||
nnnwwwww zzzzzyyy 1xxxxxxx | |||
high-surrogate low-surrogate | |||
110110wwwwwzzzzz 110111yyyxxxxxxx | |||
@param chOffset - Since the three top bits of chOffset are not needed to | |||
set the location of the extended Window, they are used instead | |||
to select the window, thereby reducing the number of needed command codes. | |||
The bottom 13 bits of chOffset are used to calculate the offset relative to | |||
a 7 bit input data byte to yield the 20 bits expressed by each surrogate pair. | |||
**/ | |||
protected void defineExtendedWindow(char chOffset) | |||
{ | |||
// The top 3 bits of iOffsetHi are the window index | |||
int iWindow = chOffset >>> 13; | |||
// Calculate the new offset | |||
dynamicOffset[iWindow] = ((chOffset & 0x1FFF) << 7) + (1 << 16); | |||
// make the redefined window the active one | |||
selectWindow(iWindow); | |||
} | |||
/** string buffer length used by the following functions */ | |||
protected int iOut = 0; | |||
/** input cursor used by the following functions */ | |||
protected int iIn = 0; | |||
/** expand input that is in Unicode mode | |||
@param in input byte array to be expanded | |||
@param iCur starting index | |||
@param sb string buffer to which to append expanded input | |||
@return the index for the lastc byte processed | |||
**/ | |||
protected int expandUnicode(byte []in, int iCur, StringBuffer sb) | |||
throws IllegalInputException, EndOfInputException | |||
{ | |||
for( ; iCur < in.length-1; iCur+=2 ) // step by 2: | |||
{ | |||
byte b = in[iCur]; | |||
if (b >= UC0 && b <= UC7) | |||
{ | |||
Debug.out("SelectWindow: ", b); | |||
selectWindow(b - UC0); | |||
return iCur; | |||
} | |||
else if (b >= UD0 && b <= UD7) | |||
{ | |||
defineWindow( b - UD0, in[iCur+1]); | |||
return iCur + 1; | |||
} | |||
else if (b == UDX) | |||
{ | |||
if( iCur >= in.length - 2) | |||
{ | |||
break; // buffer error | |||
} | |||
defineExtendedWindow(charFromTwoBytes(in[iCur+1], in[iCur+2])); | |||
return iCur + 2; | |||
} | |||
else if (b == UQU) | |||
{ | |||
if( iCur >= in.length - 2) | |||
{ | |||
break; // error | |||
} | |||
// Skip command byte and output Unicode character | |||
iCur++; | |||
} | |||
// output a Unicode character | |||
char ch = charFromTwoBytes(in[iCur], in[iCur+1]); | |||
sb.append((char)ch); | |||
iOut++; | |||
} | |||
if( iCur == in.length) | |||
{ | |||
return iCur; | |||
} | |||
// Error condition | |||
throw new EndOfInputException(); | |||
} | |||
/** assemble a char from two bytes | |||
In Java bytes are signed quantities, while chars are unsigned | |||
@return the character | |||
@param hi most significant byte | |||
@param lo least significant byte | |||
*/ | |||
public static char charFromTwoBytes(byte hi, byte lo) | |||
{ | |||
char ch = (char)(lo >= 0 ? lo : 256 + lo); | |||
return (char)(ch + (char)((hi >= 0 ? hi : 256 + hi)<<8)); | |||
} | |||
/** expand portion of the input that is in single byte mode **/ | |||
protected String expandSingleByte(byte []in) | |||
throws IllegalInputException, EndOfInputException | |||
{ | |||
/* Allocate the output buffer. Because of control codes, generally | |||
each byte of input results in fewer than one character of | |||
output. Using in.length as an intial allocation length should avoid | |||
the need to reallocate in mid-stream. The exception to this rule are | |||
surrogates. */ | |||
StringBuffer sb = new StringBuffer(in.length); | |||
iOut = 0; | |||
// Loop until all input is exhausted or an error occurred | |||
int iCur; | |||
Loop: | |||
for( iCur = 0; iCur < in.length; iCur++ ) | |||
{ | |||
// DEBUG Debug.out("Expanding: ", iCur); | |||
// Default behaviour is that ASCII characters are passed through | |||
// (staticOffset[0] == 0) and characters with the high bit on are | |||
// offset by the current dynamic (or sliding) window (this.iWindow) | |||
int iStaticWindow = 0; | |||
int iDynamicWindow = getCurrentWindow(); | |||
switch(in[iCur]) | |||
{ | |||
// Quote from a static Window | |||
case SQ0: | |||
case SQ1: | |||
case SQ2: | |||
case SQ3: | |||
case SQ4: | |||
case SQ5: | |||
case SQ6: | |||
case SQ7: | |||
Debug.out("SQn:", iStaticWindow); | |||
// skip the command byte and check for length | |||
if( iCur >= in.length - 1) | |||
{ | |||
Debug.out("SQn missing argument: ", in, iCur); | |||
break Loop; // buffer length error | |||
} | |||
// Select window pair to quote from | |||
iDynamicWindow = iStaticWindow = in[iCur] - SQ0; | |||
iCur ++; | |||
// FALL THROUGH | |||
default: | |||
// output as character | |||
if(in[iCur] >= 0) | |||
{ | |||
// use static window | |||
int ch = in[iCur] + staticOffset[iStaticWindow]; | |||
sb.append((char)ch); | |||
iOut++; | |||
} | |||
else | |||
{ | |||
// use dynamic window | |||
int ch = (in[iCur] + 256); // adjust for signed bytes | |||
ch -= 0x80; // reduce to range 00..7F | |||
ch += dynamicOffset[iDynamicWindow]; | |||
//DEBUG | |||
Debug.out("Dynamic: ", (char) ch); | |||
if (ch < 1<<16) | |||
{ | |||
// in Unicode range, output directly | |||
sb.append((char)ch); | |||
iOut++; | |||
} | |||
else | |||
{ | |||
// this is an extension character | |||
Debug.out("Extension character: ", ch); | |||
// compute and append the two surrogates: | |||
// translate from 10000..10FFFF to 0..FFFFF | |||
ch -= 0x10000; | |||
// high surrogate = top 10 bits added to D800 | |||
sb.append((char)(0xD800 + (ch>>10))); | |||
iOut++; | |||
// low surrogate = bottom 10 bits added to DC00 | |||
sb.append((char)(0xDC00 + (ch & ~0xFC00))); | |||
iOut++; | |||
} | |||
} | |||
break; | |||
// define a dynamic window as extended | |||
case SDX: | |||
iCur += 2; | |||
if( iCur >= in.length) | |||
{ | |||
Debug.out("SDn missing argument: ", in, iCur -1); | |||
break Loop; // buffer length error | |||
} | |||
defineExtendedWindow(charFromTwoBytes(in[iCur-1], in[iCur])); | |||
break; | |||
// Position a dynamic Window | |||
case SD0: | |||
case SD1: | |||
case SD2: | |||
case SD3: | |||
case SD4: | |||
case SD5: | |||
case SD6: | |||
case SD7: | |||
iCur ++; | |||
if( iCur >= in.length) | |||
{ | |||
Debug.out("SDn missing argument: ", in, iCur -1); | |||
break Loop; // buffer length error | |||
} | |||
defineWindow(in[iCur-1] - SD0, in[iCur]); | |||
break; | |||
// Select a new dynamic Window | |||
case SC0: | |||
case SC1: | |||
case SC2: | |||
case SC3: | |||
case SC4: | |||
case SC5: | |||
case SC6: | |||
case SC7: | |||
selectWindow(in[iCur] - SC0); | |||
break; | |||
case SCU: | |||
// switch to Unicode mode and continue parsing | |||
iCur = expandUnicode(in, iCur+1, sb); | |||
// DEBUG Debug.out("Expanded Unicode range until: ", iCur); | |||
break; | |||
case SQU: | |||
// directly extract one Unicode character | |||
iCur += 2; | |||
if( iCur >= in.length) | |||
{ | |||
Debug.out("SQU missing argument: ", in, iCur - 2); | |||
break Loop; // buffer length error | |||
} | |||
else | |||
{ | |||
char ch = charFromTwoBytes(in[iCur-1], in[iCur]); | |||
Debug.out("Quoted: ", ch); | |||
sb.append((char)ch); | |||
iOut++; | |||
} | |||
break; | |||
case Srs: | |||
throw new IllegalInputException(); | |||
// break; | |||
} | |||
} | |||
if( iCur >= in.length) | |||
{ | |||
//SUCCESS: all input used up | |||
sb.setLength(iOut); | |||
iIn = iCur; | |||
return sb.toString(); | |||
} | |||
Debug.out("Length ==" + in.length+" iCur =", iCur); | |||
//ERROR: premature end of input | |||
throw new EndOfInputException(); | |||
} | |||
/** expand a byte array containing compressed Unicode */ | |||
public String expand (byte []in) | |||
throws IllegalInputException, EndOfInputException | |||
{ | |||
String str = expandSingleByte(in); | |||
Debug.out("expand output: ", str.toCharArray()); | |||
return str; | |||
} | |||
/** reset is called to start with new input, w/o creating a new | |||
instance */ | |||
public void reset() | |||
{ | |||
iOut = 0; | |||
iIn = 0; | |||
super.reset(); | |||
} | |||
public int charsWritten() | |||
{ | |||
return iOut; | |||
} | |||
public int bytesRead() | |||
{ | |||
return iIn; | |||
} | |||
} |
@@ -0,0 +1,45 @@ | |||
package com.healthmarketscience.jackcess.scsu; | |||
/** | |||
* This sample software accompanies Unicode Technical Report #6 and | |||
* distributed as is by Unicode, Inc., subject to the following: | |||
* | |||
* Copyright © 1996-1997 Unicode, Inc.. All Rights Reserved. | |||
* | |||
* Permission to use, copy, modify, and distribute this software | |||
* without fee is hereby granted provided that this copyright notice | |||
* appears in all copies. | |||
* | |||
* UNICODE, INC. MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE | |||
* SUITABILITY OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING | |||
* BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, | |||
* FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. | |||
* UNICODE, INC., SHALL NOT BE LIABLE FOR ANY ERRORS OR OMISSIONS, AND | |||
* SHALL NOT BE LIABLE FOR ANY DAMAGES, INCLUDING CONSEQUENTIAL AND | |||
* INCIDENTAL DAMAGES, SUFFERED BY YOU AS A RESULT OF USING, MODIFYING | |||
* OR DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. | |||
* | |||
* @author Asmus Freytag | |||
* | |||
* @version 001 Dec 25 1996 | |||
* @version 002 Jun 25 1997 | |||
* @version 003 Jul 25 1997 | |||
* @version 004 Aug 25 1997 | |||
* | |||
* Unicode and the Unicode logo are trademarks of Unicode, Inc., | |||
* and are registered in some jurisdictions. | |||
**/ | |||
/** | |||
* The input character array or input byte array contained | |||
* illegal sequences of bytes or characters | |||
*/ | |||
public class IllegalInputException extends java.lang.Exception | |||
{ | |||
public IllegalInputException(){ | |||
super("The input character array or input byte array contained illegal sequences of bytes or characters"); | |||
} | |||
public IllegalInputException(String s) { | |||
super(s); | |||
} | |||
} |
@@ -0,0 +1,252 @@ | |||
package com.healthmarketscience.jackcess.scsu; | |||
/* | |||
* This sample software accompanies Unicode Technical Report #6 and | |||
* distributed as is by Unicode, Inc., subject to the following: | |||
* | |||
* Copyright © 1996-1998 Unicode, Inc.. All Rights Reserved. | |||
* | |||
* Permission to use, copy, modify, and distribute this software | |||
* without fee is hereby granted provided that this copyright notice | |||
* appears in all copies. | |||
* | |||
* UNICODE, INC. MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE | |||
* SUITABILITY OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING | |||
* BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, | |||
* FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. | |||
* UNICODE, INC., SHALL NOT BE LIABLE FOR ANY ERRORS OR OMISSIONS, AND | |||
* SHALL NOT BE LIABLE FOR ANY DAMAGES, INCLUDING CONSEQUENTIAL AND | |||
* INCIDENTAL DAMAGES, SUFFERED BY YOU AS A RESULT OF USING, MODIFYING | |||
* OR DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. | |||
* | |||
* @author Asmus Freytag | |||
* | |||
* @version 001 Dec 25 1996 | |||
* @version 002 Jun 25 1997 | |||
* @version 003 Jul 25 1997 | |||
* @version 004 Aug 25 1997 | |||
* @version 005 Sep 30 1998 | |||
* | |||
* Unicode and the Unicode logo are trademarks of Unicode, Inc., | |||
* and are registered in some jurisdictions. | |||
**/ | |||
/** | |||
Encoding text data in Unicode often requires more storage than using | |||
an existing 8-bit character set and limited to the subset of characters | |||
actually found in the text. The Unicode Compression Algorithm reduces | |||
the necessary storage while retaining the universality of Unicode. | |||
A full description of the algorithm can be found in document | |||
http://www.unicode.org/unicode/reports/tr6.html | |||
Summary | |||
The goal of the Unicode Compression Algorithm is the abilty to | |||
* Express all code points in Unicode | |||
* Approximate storage size for traditional character sets | |||
* Work well for short strings | |||
* Provide transparency for Latin-1 data | |||
* Support very simple decoders | |||
* Support simple as well as sophisticated encoders | |||
If needed, further compression can be achieved by layering standard | |||
file or disk-block based compression algorithms on top. | |||
<H2>Features</H2> | |||
Languages using small alphabets would contain runs of characters that | |||
are coded close together in Unicode. These runs are interrupted only | |||
by punctuation characters, which are themselves coded in proximity to | |||
each other in Unicode (usually in the ASCII range). | |||
Two basic mechanisms in the compression algorithm account for these two | |||
cases, sliding windows and static windows. A window is an area of 128 | |||
consecutive characters in Unicode. In the compressed data stream, each | |||
character from a sliding window would be represented as a byte between | |||
0x80 and 0xFF, while a byte from 0x20 to 0x7F (as well as CR, LF, and | |||
TAB) would always mean an ASCII character (or control). | |||
<H2>Notes on the Java implementation</H2> | |||
A limitation of Java is the exclusive use of a signed byte data type. | |||
The following work arounds are required: | |||
Copying a byte to an integer variable and adding 256 for 'negative' | |||
bytes gives an integer in the range 0-255. | |||
Values of char are between 0x0000 and 0xFFFF in Java. Arithmetic on | |||
char values is unsigned. | |||
Extended characters require an int to store them. The sign is not an | |||
issue because only 1024*1024 + 65536 extended characters exist. | |||
**/ | |||
public abstract class SCSU | |||
{ | |||
/** Single Byte mode command values */ | |||
/** SQ<i>n</i> Quote from Window . <p> | |||
If the following byte is less than 0x80, quote from | |||
static window <i>n</i>, else quote from dynamic window <i>n</i>. | |||
*/ | |||
static final byte SQ0 = 0x01; // Quote from window pair 0 | |||
static final byte SQ1 = 0x02; // Quote from window pair 1 | |||
static final byte SQ2 = 0x03; // Quote from window pair 2 | |||
static final byte SQ3 = 0x04; // Quote from window pair 3 | |||
static final byte SQ4 = 0x05; // Quote from window pair 4 | |||
static final byte SQ5 = 0x06; // Quote from window pair 5 | |||
static final byte SQ6 = 0x07; // Quote from window pair 6 | |||
static final byte SQ7 = 0x08; // Quote from window pair 7 | |||
static final byte SDX = 0x0B; // Define a window as extended | |||
static final byte Srs = 0x0C; // reserved | |||
static final byte SQU = 0x0E; // Quote a single Unicode character | |||
static final byte SCU = 0x0F; // Change to Unicode mode | |||
/** SC<i>n</i> Change to Window <i>n</i>. <p> | |||
If the following bytes are less than 0x80, interpret them | |||
as command bytes or pass them through, else add the offset | |||
for dynamic window <i>n</i>. */ | |||
static final byte SC0 = 0x10; // Select window 0 | |||
static final byte SC1 = 0x11; // Select window 1 | |||
static final byte SC2 = 0x12; // Select window 2 | |||
static final byte SC3 = 0x13; // Select window 3 | |||
static final byte SC4 = 0x14; // Select window 4 | |||
static final byte SC5 = 0x15; // Select window 5 | |||
static final byte SC6 = 0x16; // Select window 6 | |||
static final byte SC7 = 0x17; // Select window 7 | |||
static final byte SD0 = 0x18; // Define and select window 0 | |||
static final byte SD1 = 0x19; // Define and select window 1 | |||
static final byte SD2 = 0x1A; // Define and select window 2 | |||
static final byte SD3 = 0x1B; // Define and select window 3 | |||
static final byte SD4 = 0x1C; // Define and select window 4 | |||
static final byte SD5 = 0x1D; // Define and select window 5 | |||
static final byte SD6 = 0x1E; // Define and select window 6 | |||
static final byte SD7 = 0x1F; // Define and select window 7 | |||
static final byte UC0 = (byte) 0xE0; // Select window 0 | |||
static final byte UC1 = (byte) 0xE1; // Select window 1 | |||
static final byte UC2 = (byte) 0xE2; // Select window 2 | |||
static final byte UC3 = (byte) 0xE3; // Select window 3 | |||
static final byte UC4 = (byte) 0xE4; // Select window 4 | |||
static final byte UC5 = (byte) 0xE5; // Select window 5 | |||
static final byte UC6 = (byte) 0xE6; // Select window 6 | |||
static final byte UC7 = (byte) 0xE7; // Select window 7 | |||
static final byte UD0 = (byte) 0xE8; // Define and select window 0 | |||
static final byte UD1 = (byte) 0xE9; // Define and select window 1 | |||
static final byte UD2 = (byte) 0xEA; // Define and select window 2 | |||
static final byte UD3 = (byte) 0xEB; // Define and select window 3 | |||
static final byte UD4 = (byte) 0xEC; // Define and select window 4 | |||
static final byte UD5 = (byte) 0xED; // Define and select window 5 | |||
static final byte UD6 = (byte) 0xEE; // Define and select window 6 | |||
static final byte UD7 = (byte) 0xEF; // Define and select window 7 | |||
static final byte UQU = (byte) 0xF0; // Quote a single Unicode character | |||
static final byte UDX = (byte) 0xF1; // Define a Window as extended | |||
static final byte Urs = (byte) 0xF2; // reserved | |||
/** constant offsets for the 8 static windows */ | |||
static final int staticOffset[] = | |||
{ | |||
0x0000, // ASCII for quoted tags | |||
0x0080, // Latin - 1 Supplement (for access to punctuation) | |||
0x0100, // Latin Extended-A | |||
0x0300, // Combining Diacritical Marks | |||
0x2000, // General Punctuation | |||
0x2080, // Currency Symbols | |||
0x2100, // Letterlike Symbols and Number Forms | |||
0x3000 // CJK Symbols and punctuation | |||
}; | |||
/** initial offsets for the 8 dynamic (sliding) windows */ | |||
static final int initialDynamicOffset[] = | |||
{ | |||
0x0080, // Latin-1 | |||
0x00C0, // Latin Extended A //@005 fixed from 0x0100 | |||
0x0400, // Cyrillic | |||
0x0600, // Arabic | |||
0x0900, // Devanagari | |||
0x3040, // Hiragana | |||
0x30A0, // Katakana | |||
0xFF00 // Fullwidth ASCII | |||
}; | |||
/** dynamic window offsets, intitialize to default values. */ | |||
int dynamicOffset[] = | |||
{ | |||
initialDynamicOffset[0], | |||
initialDynamicOffset[1], | |||
initialDynamicOffset[2], | |||
initialDynamicOffset[3], | |||
initialDynamicOffset[4], | |||
initialDynamicOffset[5], | |||
initialDynamicOffset[6], | |||
initialDynamicOffset[7] | |||
}; | |||
// The following method is common to encoder and decoder | |||
private int iWindow = 0; // current active window | |||
/** select the active dynamic window **/ | |||
protected void selectWindow(int iWindow) | |||
{ | |||
this.iWindow = iWindow; | |||
} | |||
/** select the active dynamic window **/ | |||
protected int getCurrentWindow() | |||
{ | |||
return this.iWindow; | |||
} | |||
/** | |||
These values are used in defineWindow | |||
**/ | |||
/** | |||
* Unicode code points from 3400 to E000 are not adressible by | |||
* dynamic window, since in these areas no short run alphabets are | |||
* found. Therefore add gapOffset to all values from gapThreshold */ | |||
static final int gapThreshold = 0x68; | |||
static final int gapOffset = 0xAC00; | |||
/* values between reservedStart and fixedThreshold are reserved */ | |||
static final int reservedStart = 0xA8; | |||
/* use table of predefined fixed offsets for values from fixedThreshold */ | |||
static final int fixedThreshold = 0xF9; | |||
/** Table of fixed predefined Offsets, and byte values that index into **/ | |||
static final int fixedOffset[] = | |||
{ | |||
/* 0xF9 */ 0x00C0, // Latin-1 Letters + half of Latin Extended A | |||
/* 0xFA */ 0x0250, // IPA extensions | |||
/* 0xFB */ 0x0370, // Greek | |||
/* 0xFC */ 0x0530, // Armenian | |||
/* 0xFD */ 0x3040, // Hiragana | |||
/* 0xFE */ 0x30A0, // Katakana | |||
/* 0xFF */ 0xFF60 // Halfwidth Katakana | |||
}; | |||
/** whether a character is compressible */ | |||
public static boolean isCompressible(char ch) | |||
{ | |||
return (ch < 0x3400 || ch >= 0xE000); | |||
} | |||
/** reset is only needed to bail out after an exception and | |||
restart with new input */ | |||
public void reset() | |||
{ | |||
// reset the dynamic windows | |||
for (int i = 0; i < dynamicOffset.length; i++) | |||
{ | |||
dynamicOffset[i] = initialDynamicOffset[i]; | |||
} | |||
this.iWindow = 0; | |||
} | |||
} |
@@ -0,0 +1,6 @@ | |||
log4j.rootCategory=INFO, stdout | |||
log4j.appender.stdout=org.apache.log4j.ConsoleAppender | |||
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout | |||
log4j.appender.stdout.layout.ConversionPattern=**** %-5p %d{MMM d HH:mm:ss} [%F] - %m%n | |||
log4j.category.com.healthmarketscience.jackcess=INFO |
@@ -0,0 +1 @@ | |||
RESULT_PHYS_ID FIRST MIDDLE LAST OUTLIER RANK CLAIM_COUNT PROCEDURE_COUNT WEIGHTED_CLAIM_COUNT WEIGHTED_PROCEDURE_COUNT |
@@ -0,0 +1,3 @@ | |||
Test1 Test2 Test3 | |||
Foo Bar Ralph | |||
S Mouse Rocks |
@@ -0,0 +1,203 @@ | |||
// Copyright (c) 2004 Health Market Science, Inc. | |||
package com.healthmarketscience.jackcess; | |||
import java.io.File; | |||
import java.util.ArrayList; | |||
import java.util.Calendar; | |||
import java.util.Date; | |||
import java.util.List; | |||
import java.util.Map; | |||
import com.healthmarketscience.jackcess.Column; | |||
import com.healthmarketscience.jackcess.DataTypes; | |||
import com.healthmarketscience.jackcess.Database; | |||
import com.healthmarketscience.jackcess.Table; | |||
import junit.framework.TestCase; | |||
/** | |||
* @author Tim McCune | |||
*/ | |||
public class DatabaseTest extends TestCase { | |||
public DatabaseTest(String name) throws Exception { | |||
super(name); | |||
} | |||
private Database open() throws Exception { | |||
return Database.open(new File("test/data/test.mdb")); | |||
} | |||
private Database create() throws Exception { | |||
File tmp = File.createTempFile("databaseTest", ".mdb"); | |||
tmp.deleteOnExit(); | |||
return Database.create(tmp); | |||
} | |||
public void testGetColumns() throws Exception { | |||
List columns = open().getTable("Table1").getColumns(); | |||
assertEquals(9, columns.size()); | |||
checkColumn(columns, 0, "A", DataTypes.TEXT); | |||
checkColumn(columns, 1, "B", DataTypes.TEXT); | |||
checkColumn(columns, 2, "C", DataTypes.BYTE); | |||
checkColumn(columns, 3, "D", DataTypes.INT); | |||
checkColumn(columns, 4, "E", DataTypes.LONG); | |||
checkColumn(columns, 5, "F", DataTypes.DOUBLE); | |||
checkColumn(columns, 6, "G", DataTypes.SHORT_DATE_TIME); | |||
checkColumn(columns, 7, "H", DataTypes.MONEY); | |||
checkColumn(columns, 8, "I", DataTypes.BOOLEAN); | |||
} | |||
private void checkColumn(List columns, int columnNumber, String name, byte dataType) | |||
throws Exception { | |||
Column column = (Column) columns.get(columnNumber); | |||
assertEquals(name, column.getName()); | |||
assertEquals(dataType, column.getType()); | |||
} | |||
public void testGetNextRow() throws Exception { | |||
Database db = open(); | |||
assertEquals(1, db.getTableNames().size()); | |||
Table table = db.getTable("Table1"); | |||
Map row = table.getNextRow(); | |||
assertEquals("abcdefg", row.get("A")); | |||
assertEquals("hijklmnop", row.get("B")); | |||
assertEquals(new Byte((byte) 2), row.get("C")); | |||
assertEquals(new Short((short) 222), row.get("D")); | |||
assertEquals(new Integer(333333333), row.get("E")); | |||
assertEquals(new Double(444.555d), row.get("F")); | |||
Calendar cal = Calendar.getInstance(); | |||
cal.setTime((Date) row.get("G")); | |||
assertEquals(Calendar.SEPTEMBER, cal.get(Calendar.MONTH)); | |||
assertEquals(21, cal.get(Calendar.DAY_OF_MONTH)); | |||
assertEquals(1974, cal.get(Calendar.YEAR)); | |||
assertEquals(Boolean.TRUE, row.get("I")); | |||
row = table.getNextRow(); | |||
assertEquals("a", row.get("A")); | |||
assertEquals("b", row.get("B")); | |||
assertEquals(new Byte((byte) 0), row.get("C")); | |||
assertEquals(new Short((short) 0), row.get("D")); | |||
assertEquals(new Integer(0), row.get("E")); | |||
assertEquals(new Double(0d), row.get("F")); | |||
cal = Calendar.getInstance(); | |||
cal.setTime((Date) row.get("G")); | |||
assertEquals(Calendar.DECEMBER, cal.get(Calendar.MONTH)); | |||
assertEquals(12, cal.get(Calendar.DAY_OF_MONTH)); | |||
assertEquals(1981, cal.get(Calendar.YEAR)); | |||
assertEquals(Boolean.FALSE, row.get("I")); | |||
} | |||
public void testCreate() throws Exception { | |||
Database db = create(); | |||
assertEquals(0, db.getTableNames().size()); | |||
} | |||
public void testWriteAndRead() throws Exception { | |||
Database db = create(); | |||
createTestTable(db); | |||
Object[] row = new Object[9]; | |||
row[0] = "Tim"; | |||
row[1] = "R"; | |||
row[2] = "McCune"; | |||
row[3] = new Integer(1234); | |||
row[4] = new Byte((byte) 0xad); | |||
row[5] = new Double(555.66d); | |||
row[6] = new Float(777.88d); | |||
row[7] = new Short((short) 999); | |||
row[8] = new Date(); | |||
Table table = db.getTable("Test"); | |||
int count = 1000; | |||
for (int i = 0; i < count; i++) { | |||
table.addRow(row); | |||
} | |||
for (int i = 0; i < count; i++) { | |||
Map readRow = table.getNextRow(); | |||
assertEquals(row[0], readRow.get("A")); | |||
assertEquals(row[1], readRow.get("B")); | |||
assertEquals(row[2], readRow.get("C")); | |||
assertEquals(row[3], readRow.get("D")); | |||
assertEquals(row[4], readRow.get("E")); | |||
assertEquals(row[5], readRow.get("F")); | |||
assertEquals(row[6], readRow.get("G")); | |||
assertEquals(row[7], readRow.get("H")); | |||
} | |||
} | |||
public void testWriteAndReadInBatch() throws Exception { | |||
Database db = create(); | |||
createTestTable(db); | |||
int count = 1000; | |||
List rows = new ArrayList(count); | |||
Object[] row = new Object[9]; | |||
row[0] = "Tim"; | |||
row[1] = "R"; | |||
row[2] = "McCune"; | |||
row[3] = new Integer(1234); | |||
row[4] = new Byte((byte) 0xad); | |||
row[5] = new Double(555.66d); | |||
row[6] = new Float(777.88d); | |||
row[7] = new Short((short) 999); | |||
row[8] = new Date(); | |||
for (int i = 0; i < count; i++) { | |||
rows.add(row); | |||
} | |||
Table table = db.getTable("Test"); | |||
table.addRows(rows); | |||
for (int i = 0; i < count; i++) { | |||
Map readRow = table.getNextRow(); | |||
assertEquals(row[0], readRow.get("A")); | |||
assertEquals(row[1], readRow.get("B")); | |||
assertEquals(row[2], readRow.get("C")); | |||
assertEquals(row[3], readRow.get("D")); | |||
assertEquals(row[4], readRow.get("E")); | |||
assertEquals(row[5], readRow.get("F")); | |||
assertEquals(row[6], readRow.get("G")); | |||
assertEquals(row[7], readRow.get("H")); | |||
} | |||
} | |||
private void createTestTable(Database db) throws Exception { | |||
List columns = new ArrayList(); | |||
Column col = new Column(); | |||
col.setName("A"); | |||
col.setType(DataTypes.TEXT); | |||
columns.add(col); | |||
col = new Column(); | |||
col.setName("B"); | |||
col.setType(DataTypes.TEXT); | |||
columns.add(col); | |||
col = new Column(); | |||
col.setName("C"); | |||
col.setType(DataTypes.TEXT); | |||
columns.add(col); | |||
col = new Column(); | |||
col.setName("D"); | |||
col.setType(DataTypes.LONG); | |||
columns.add(col); | |||
col = new Column(); | |||
col.setName("E"); | |||
col.setType(DataTypes.BYTE); | |||
columns.add(col); | |||
col = new Column(); | |||
col.setName("F"); | |||
col.setType(DataTypes.DOUBLE); | |||
columns.add(col); | |||
col = new Column(); | |||
col.setName("G"); | |||
col.setType(DataTypes.FLOAT); | |||
columns.add(col); | |||
col = new Column(); | |||
col.setName("H"); | |||
col.setType(DataTypes.INT); | |||
columns.add(col); | |||
col = new Column(); | |||
col.setName("I"); | |||
col.setType(DataTypes.SHORT_DATE_TIME); | |||
columns.add(col); | |||
db.createTable("test", columns); | |||
} | |||
} |
@@ -0,0 +1,45 @@ | |||
// Copyright (c) 2004 Health Market Science, Inc. | |||
package com.healthmarketscience.jackcess; | |||
import org.apache.commons.logging.Log; | |||
import org.apache.commons.logging.LogFactory; | |||
import com.healthmarketscience.jackcess.Database; | |||
import java.io.File; | |||
import junit.framework.TestCase; | |||
/** | |||
* @author Rob Di Marco | |||
*/ | |||
public class ImportTest extends TestCase | |||
{ | |||
/** The logger to use. */ | |||
private static final Log LOG = LogFactory.getLog(ImportTest.class); | |||
public ImportTest(String name) | |||
{ | |||
super(name); | |||
} | |||
private Database create() throws Exception { | |||
File tmp = File.createTempFile("databaseTest", ".mdb"); | |||
tmp.deleteOnExit(); | |||
return Database.create(tmp); | |||
} | |||
public void testImportFromFile() throws Exception | |||
{ | |||
Database db = create(); | |||
db.importFile("test", new File("test/data/sample-input.tab"), "\\t"); | |||
} | |||
public void testImportFromFileWithOnlyHeaders() throws Exception | |||
{ | |||
Database db = create(); | |||
db.importFile("test", new File("test/data/sample-input-only-headers.tab"), | |||
"\\t"); | |||
} | |||
} |
@@ -0,0 +1,51 @@ | |||
// Copyright (c) 2004 Health Market Science, Inc. | |||
package com.healthmarketscience.jackcess; | |||
import java.nio.ByteBuffer; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import com.healthmarketscience.jackcess.Column; | |||
import com.healthmarketscience.jackcess.DataTypes; | |||
import com.healthmarketscience.jackcess.Table; | |||
import junit.framework.TestCase; | |||
/** | |||
* @author Tim McCune | |||
*/ | |||
public class TableTest extends TestCase { | |||
public TableTest(String name) { | |||
super(name); | |||
} | |||
public void testCreateRow() throws Exception { | |||
Table table = new Table(); | |||
List columns = new ArrayList(); | |||
Column col = new Column(); | |||
col.setType(DataTypes.INT); | |||
columns.add(col); | |||
col = new Column(); | |||
col.setType(DataTypes.TEXT); | |||
columns.add(col); | |||
columns.add(col); | |||
table.setColumns(columns); | |||
int colCount = 3; | |||
Object[] row = new Object[colCount]; | |||
row[0] = new Short((short) 9); | |||
row[1] = "Tim"; | |||
row[2] = "McCune"; | |||
ByteBuffer buffer = table.createRow(row); | |||
assertEquals((short) colCount, buffer.getShort()); | |||
assertEquals((short) 9, buffer.getShort()); | |||
assertEquals((byte) 'T', buffer.get()); | |||
assertEquals((short) 22, buffer.getShort(22)); | |||
assertEquals((short) 10, buffer.getShort(24)); | |||
assertEquals((short) 4, buffer.getShort(26)); | |||
assertEquals((short) 2, buffer.getShort(28)); | |||
assertEquals((byte) 7, buffer.get(30)); | |||
} | |||
} |
@@ -0,0 +1,110 @@ | |||
<?xml version="1.0"?> | |||
<faqs title="Frequently Asked Questions"> | |||
<part id="general"> | |||
<title>General</title> | |||
<faq id="linux"> | |||
<question>Does this work on Linux/Unix?</question> | |||
<answer> | |||
<p>Yep, Jackcess is pure Java. It will work on any | |||
Java Virtual Machine (1.4+).</p> | |||
</answer> | |||
</faq> | |||
<faq id="formats"> | |||
<question>What Access formats does it support?</question> | |||
<answer> | |||
<p>Jackcess currently supports <i>only</i> Access 2000 | |||
databases. Access 2003 is not supported.</p> | |||
</answer> | |||
</faq> | |||
<faq id="mdbtools"> | |||
<question> | |||
How is this different from | |||
<a href="http://mdbtools.sf.net">mdbtools</a>? | |||
</question> | |||
<answer> | |||
<p> | |||
We want to give a lot of credit to mdbtools. They have | |||
been around much longer than Jackcess, and, along with | |||
<a href="http://jakarta.apache.org/poi">POI</a>, | |||
inspired us that a project like this could be done. | |||
mdbtools is written in C. There is a Java port of it, | |||
but if you've ever read or used a Java port of a C | |||
library, you can appreciate the difference between such | |||
a library and one written from scratch in Java. | |||
</p> | |||
<p> | |||
At the time of this writing, mdbtools could only read | |||
Access databases. Jackcess can also write to them. | |||
According to their web site, "Write support is currently being | |||
worked on and the first cut is expected to be included in the | |||
0.6 release." This status hasn't changed since we first | |||
started work on Jackcess. | |||
</p> | |||
<p> | |||
mdbtools supports Access 97 databases, which Jackcess does not. | |||
The Java port of mdbtools also includes an implementation of | |||
a small subset of the JDBC APIs. Jackcess does not currently, | |||
but a pure Java JDBC driver for Access could certainly be written | |||
on top of Jackcess. | |||
</p> | |||
</answer> | |||
</faq> | |||
<faq id="poi"> | |||
<question> | |||
This looks like a logical addition to | |||
<a href="http://jakarta.apache.org/poi">POI</a>. Why not integrate | |||
with that project? | |||
</question> | |||
<answer> | |||
<p> | |||
POI is released under | |||
<a href="http://www.apache.org/foundation/licence-FAQ.html">The Apache License</a>. | |||
Jackcess is released under | |||
<a href="http://www.gnu.org/copyleft/lesser.html">The GNU Lesser General Public License</a>. | |||
The Apache license allows closed-source and/or commercial forks. | |||
The LGPL does not. If you change or enhance Jackcess, you must contribute | |||
your changes back to the project. | |||
</p> | |||
</answer> | |||
</faq> | |||
<faq id="hms"> | |||
<question>Who is Health Market Science?</question> | |||
<answer> | |||
<p> | |||
HMS is a small company located in suburban Philadelphia. | |||
Using proprietary matching and consolidation software, | |||
HMS scientifically manufactures the most comprehensive | |||
and accurate healthcare data sets in the market today. | |||
<a href="http://www.healthmarketscience.com/careers.htm">We're hiring!</a> | |||
HMS is always looking for talented individuals, especially | |||
<a href="http://www.healthmarketscience.com/hr_web/active/hms_software_developer.htm">Java developers</a>. | |||
</p> | |||
</answer> | |||
</faq> | |||
<faq id="bugs"> | |||
<question>It doesn't work!</question> | |||
<answer> | |||
<p> | |||
Ok, that wasn't a question, but we'll try to respond anyway. :) | |||
Jackcess is young, and not that robust yet. As you might imagine, | |||
it's kind of hard to test, simply by its nature. There are | |||
bugs that we are aware of, and certainly many more that we are not. | |||
If you find what looks like a bug, please | |||
<a href="http://sf.net/tracker/?group_id=134943&atid=731445">report it.</a> | |||
Even better, fix it, and | |||
<a href="http://sf.net/tracker/?group_id=134943&atid=731447">submit a patch.</a> | |||
</p> | |||
</answer> | |||
</faq> | |||
</part> | |||
</faqs> |
@@ -0,0 +1,47 @@ | |||
<?xml version="1.0"?> | |||
<document> | |||
<properties> | |||
<author email="javajedi@users.sf.net">Tim McCune</author> | |||
</properties> | |||
<body> | |||
<section name="Jackcess"> | |||
<p> | |||
Jackcess ia a pure Java library for reading from and | |||
writing to MS Access databases. It is not an application. | |||
There is no GUI. It's a library, intended for other | |||
developers to use to build Java applications. Take a look | |||
at our <a href="faq.html">Frequently Asked Questions</a> | |||
for more info. | |||
</p> | |||
</section> | |||
<section name="Sample code"> | |||
<p> | |||
<ul> | |||
<li>Displaying the contents of a table: | |||
<pre>Database.open(new File("my.mdb")).getTable("MyTable").display();</pre> | |||
</li> | |||
<li>Creating a new table and writing data into it: | |||
<pre>Database db = Database.create(new File("new.mdb")); | |||
Column a = new Column(); | |||
a.setName("a"); | |||
a.setSQLType(Types.INTEGER); | |||
Column b = new Column(); | |||
b.setName("b"); | |||
b.setSQLType(Types.VARCHAR); | |||
db.createTable("NewTable", Arrays.asList(a, b)); | |||
Table newTable = db.getTable("NewTable"); | |||
newTable.addRow(new Object[] {1, "foo"});</pre> | |||
</li> | |||
<li>Copying the contents of a JDBC ResultSet (e.g. from an | |||
external database) into a new table: | |||
<pre>Database.open(new File("my.mdb")).copyTable("Imported", resultSet);</pre> | |||
</li> | |||
<li>Copying the contents of a CSV file into a new table: | |||
<pre>Database.open(new File("my.mdb")).importFile("Imported2", new File("my.csv"), ",");</pre> | |||
</li> | |||
</ul> | |||
</p> | |||
</section> | |||
</body> | |||
</document> |