You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Database.java 53KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487
  1. /*
  2. Copyright (c) 2005 Health Market Science, Inc.
  3. This library is free software; you can redistribute it and/or
  4. modify it under the terms of the GNU Lesser General Public
  5. License as published by the Free Software Foundation; either
  6. version 2.1 of the License, or (at your option) any later version.
  7. This library is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  10. Lesser General Public License for more details.
  11. You should have received a copy of the GNU Lesser General Public
  12. License along with this library; if not, write to the Free Software
  13. Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  14. USA
  15. You can contact Health Market Science at info@healthmarketscience.com
  16. or at the following address:
  17. Health Market Science
  18. 2700 Horizon Drive
  19. Suite 200
  20. King of Prussia, PA 19406
  21. */
  22. package com.healthmarketscience.jackcess;
  23. import java.io.BufferedReader;
  24. import java.io.Closeable;
  25. import java.io.File;
  26. import java.io.FileNotFoundException;
  27. import java.io.Flushable;
  28. import java.io.IOException;
  29. import java.io.RandomAccessFile;
  30. import java.nio.ByteBuffer;
  31. import java.nio.channels.Channels;
  32. import java.nio.channels.FileChannel;
  33. import java.nio.charset.Charset;
  34. import java.sql.ResultSet;
  35. import java.sql.SQLException;
  36. import java.util.ArrayList;
  37. import java.util.Arrays;
  38. import java.util.Collection;
  39. import java.util.ConcurrentModificationException;
  40. import java.util.Date;
  41. import java.util.EnumSet;
  42. import java.util.HashMap;
  43. import java.util.HashSet;
  44. import java.util.Iterator;
  45. import java.util.List;
  46. import java.util.Map;
  47. import java.util.NoSuchElementException;
  48. import java.util.Set;
  49. import java.util.TimeZone;
  50. import java.util.TreeSet;
  51. import com.healthmarketscience.jackcess.query.Query;
  52. import org.apache.commons.lang.builder.ToStringBuilder;
  53. import org.apache.commons.logging.Log;
  54. import org.apache.commons.logging.LogFactory;
  55. /**
  56. * An Access database.
  57. * <p>
  58. * There is optional support for large indexes (enabled by default). This
  59. * optional support can be disabled via a few different means:
  60. * <ul>
  61. * <li>Setting the system property {@value #USE_BIG_INDEX_PROPERTY} to
  62. * {@code "false"} will disable "large" index support across the jvm</li>
  63. * <li>Calling {@link #setUseBigIndex} on a Database instance will override
  64. * any system property setting for "large" index support for all tables
  65. * subsequently created from that instance</li>
  66. * <li>Calling {@link #getTable(String,boolean)} can selectively
  67. * enable/disable "large" index support on a per-table basis (overriding
  68. * any Database or system property setting)</li>
  69. * </ul>
  70. *
  71. * @author Tim McCune
  72. */
  73. public class Database
  74. implements Iterable<Table>, Closeable, Flushable
  75. {
  76. private static final Log LOG = LogFactory.getLog(Database.class);
  77. /** this is the default "userId" used if we cannot find existing info. this
  78. seems to be some standard "Admin" userId for access files */
  79. private static final byte[] SYS_DEFAULT_SID = new byte[2];
  80. static {
  81. SYS_DEFAULT_SID[0] = (byte) 0xA6;
  82. SYS_DEFAULT_SID[1] = (byte) 0x33;
  83. }
  84. /** default value for the auto-sync value ({@code true}). this is slower,
  85. but leaves more chance of a useable database in the face of failures. */
  86. public static final boolean DEFAULT_AUTO_SYNC = true;
  87. /** system property which can be used to make big index support the
  88. default. */
  89. public static final String USE_BIG_INDEX_PROPERTY =
  90. "com.healthmarketscience.jackcess.bigIndex";
  91. /** system property which can be used to set the default TimeZone used for
  92. date calculations. */
  93. public static final String TIMEZONE_PROPERTY =
  94. "com.healthmarketscience.jackcess.timeZone";
  95. /** system property prefix which can be used to set the default Charset
  96. used for text data (full property includes the JetFormat version). */
  97. public static final String CHARSET_PROPERTY_PREFIX =
  98. "com.healthmarketscience.jackcess.charset.";
  99. /** default error handler used if none provided (just rethrows exception) */
  100. public static final ErrorHandler DEFAULT_ERROR_HANDLER = new ErrorHandler() {
  101. public Object handleRowError(Column column,
  102. byte[] columnData,
  103. Table.RowState rowState,
  104. Exception error)
  105. throws IOException
  106. {
  107. // really can only be RuntimeException or IOException
  108. if(error instanceof IOException) {
  109. throw (IOException)error;
  110. }
  111. throw (RuntimeException)error;
  112. }
  113. };
  114. /** System catalog always lives on page 2 */
  115. private static final int PAGE_SYSTEM_CATALOG = 2;
  116. /** Name of the system catalog */
  117. private static final String TABLE_SYSTEM_CATALOG = "MSysObjects";
  118. /** this is the access control bit field for created tables. the value used
  119. is equivalent to full access (Visual Basic DAO PermissionEnum constant:
  120. dbSecFullAccess) */
  121. private static final Integer SYS_FULL_ACCESS_ACM = 1048575;
  122. /** ACE table column name of the actual access control entry */
  123. private static final String ACE_COL_ACM = "ACM";
  124. /** ACE table column name of the inheritable attributes flag */
  125. private static final String ACE_COL_F_INHERITABLE = "FInheritable";
  126. /** ACE table column name of the relevant objectId */
  127. private static final String ACE_COL_OBJECT_ID = "ObjectId";
  128. /** ACE table column name of the relevant userId */
  129. private static final String ACE_COL_SID = "SID";
  130. /** Relationship table column name of the column count */
  131. private static final String REL_COL_COLUMN_COUNT = "ccolumn";
  132. /** Relationship table column name of the flags */
  133. private static final String REL_COL_FLAGS = "grbit";
  134. /** Relationship table column name of the index of the columns */
  135. private static final String REL_COL_COLUMN_INDEX = "icolumn";
  136. /** Relationship table column name of the "to" column name */
  137. private static final String REL_COL_TO_COLUMN = "szColumn";
  138. /** Relationship table column name of the "to" table name */
  139. private static final String REL_COL_TO_TABLE = "szObject";
  140. /** Relationship table column name of the "from" column name */
  141. private static final String REL_COL_FROM_COLUMN = "szReferencedColumn";
  142. /** Relationship table column name of the "from" table name */
  143. private static final String REL_COL_FROM_TABLE = "szReferencedObject";
  144. /** Relationship table column name of the relationship */
  145. private static final String REL_COL_NAME = "szRelationship";
  146. /** System catalog column name of the page on which system object definitions
  147. are stored */
  148. private static final String CAT_COL_ID = "Id";
  149. /** System catalog column name of the name of a system object */
  150. private static final String CAT_COL_NAME = "Name";
  151. private static final String CAT_COL_OWNER = "Owner";
  152. /** System catalog column name of a system object's parent's id */
  153. private static final String CAT_COL_PARENT_ID = "ParentId";
  154. /** System catalog column name of the type of a system object */
  155. private static final String CAT_COL_TYPE = "Type";
  156. /** System catalog column name of the date a system object was created */
  157. private static final String CAT_COL_DATE_CREATE = "DateCreate";
  158. /** System catalog column name of the date a system object was updated */
  159. private static final String CAT_COL_DATE_UPDATE = "DateUpdate";
  160. /** System catalog column name of the flags column */
  161. private static final String CAT_COL_FLAGS = "Flags";
  162. /** System catalog column name of the properties column */
  163. private static final String CAT_COL_PROPS = "LvProp";
  164. public static enum FileFormat {
  165. V1997(null, JetFormat.VERSION_3),
  166. V2000("com/healthmarketscience/jackcess/empty.mdb", JetFormat.VERSION_4),
  167. V2003("com/healthmarketscience/jackcess/empty2003.mdb", JetFormat.VERSION_4),
  168. V2007("com/healthmarketscience/jackcess/empty2007.accdb", JetFormat.VERSION_5, ".accdb");
  169. private final String _emptyFile;
  170. private final JetFormat _format;
  171. private final String _ext;
  172. private FileFormat(String emptyDBFile, JetFormat jetFormat) {
  173. this(emptyDBFile, jetFormat, ".mdb");
  174. }
  175. private FileFormat(String emptyDBFile, JetFormat jetFormat, String ext) {
  176. _emptyFile = emptyDBFile;
  177. _format = jetFormat;
  178. _ext = ext;
  179. }
  180. public JetFormat getJetFormat() { return _format; }
  181. public String getFileExtension() { return _ext; }
  182. @Override
  183. public String toString() { return name() + ", jetFormat: " + getJetFormat(); }
  184. }
  185. /** Prefix for column or table names that are reserved words */
  186. private static final String ESCAPE_PREFIX = "x";
  187. /** Prefix that flags system tables */
  188. private static final String PREFIX_SYSTEM = "MSys";
  189. /** Name of the system object that is the parent of all tables */
  190. private static final String SYSTEM_OBJECT_NAME_TABLES = "Tables";
  191. /** Name of the table that contains system access control entries */
  192. private static final String TABLE_SYSTEM_ACES = "MSysACEs";
  193. /** Name of the table that contains table relationships */
  194. private static final String TABLE_SYSTEM_RELATIONSHIPS = "MSysRelationships";
  195. /** Name of the table that contains queries */
  196. private static final String TABLE_SYSTEM_QUERIES = "MSysQueries";
  197. /** Name of the table that contains queries */
  198. private static final String OBJECT_NAME_DBPROPS = "MSysDb";
  199. /** System object type for table definitions */
  200. private static final Short TYPE_TABLE = (short) 1;
  201. /** System object type for query definitions */
  202. private static final Short TYPE_QUERY = (short) 5;
  203. /** the columns to read when reading system catalog initially */
  204. private static Collection<String> SYSTEM_CATALOG_COLUMNS =
  205. new HashSet<String>(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID));
  206. /** the columns to read when finding queries */
  207. private static Collection<String> SYSTEM_CATALOG_QUERY_COLUMNS =
  208. new HashSet<String>(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID,
  209. CAT_COL_FLAGS));
  210. /**
  211. * All of the reserved words in Access that should be escaped when creating
  212. * table or column names
  213. */
  214. private static final Set<String> RESERVED_WORDS = new HashSet<String>();
  215. static {
  216. //Yup, there's a lot.
  217. RESERVED_WORDS.addAll(Arrays.asList(
  218. "add", "all", "alphanumeric", "alter", "and", "any", "application", "as",
  219. "asc", "assistant", "autoincrement", "avg", "between", "binary", "bit",
  220. "boolean", "by", "byte", "char", "character", "column", "compactdatabase",
  221. "constraint", "container", "count", "counter", "create", "createdatabase",
  222. "createfield", "creategroup", "createindex", "createobject", "createproperty",
  223. "createrelation", "createtabledef", "createuser", "createworkspace",
  224. "currency", "currentuser", "database", "date", "datetime", "delete",
  225. "desc", "description", "disallow", "distinct", "distinctrow", "document",
  226. "double", "drop", "echo", "else", "end", "eqv", "error", "exists", "exit",
  227. "false", "field", "fields", "fillcache", "float", "float4", "float8",
  228. "foreign", "form", "forms", "from", "full", "function", "general",
  229. "getobject", "getoption", "gotopage", "group", "group by", "guid", "having",
  230. "idle", "ieeedouble", "ieeesingle", "if", "ignore", "imp", "in", "index",
  231. "indexes", "inner", "insert", "inserttext", "int", "integer", "integer1",
  232. "integer2", "integer4", "into", "is", "join", "key", "lastmodified", "left",
  233. "level", "like", "logical", "logical1", "long", "longbinary", "longtext",
  234. "macro", "match", "max", "min", "mod", "memo", "module", "money", "move",
  235. "name", "newpassword", "no", "not", "null", "number", "numeric", "object",
  236. "oleobject", "off", "on", "openrecordset", "option", "or", "order", "outer",
  237. "owneraccess", "parameter", "parameters", "partial", "percent", "pivot",
  238. "primary", "procedure", "property", "queries", "query", "quit", "real",
  239. "recalc", "recordset", "references", "refresh", "refreshlink",
  240. "registerdatabase", "relation", "repaint", "repairdatabase", "report",
  241. "reports", "requery", "right", "screen", "section", "select", "set",
  242. "setfocus", "setoption", "short", "single", "smallint", "some", "sql",
  243. "stdev", "stdevp", "string", "sum", "table", "tabledef", "tabledefs",
  244. "tableid", "text", "time", "timestamp", "top", "transform", "true", "type",
  245. "union", "unique", "update", "user", "value", "values", "var", "varp",
  246. "varbinary", "varchar", "where", "with", "workspace", "xor", "year", "yes",
  247. "yesno"
  248. ));
  249. }
  250. /** Buffer to hold database pages */
  251. private ByteBuffer _buffer;
  252. /** ID of the Tables system object */
  253. private Integer _tableParentId;
  254. /** Format that the containing database is in */
  255. private final JetFormat _format;
  256. /**
  257. * Map of UPPERCASE table names to page numbers containing their definition
  258. * and their stored table name.
  259. */
  260. private Map<String, TableInfo> _tableLookup =
  261. new HashMap<String, TableInfo>();
  262. /** set of table names as stored in the mdb file, created on demand */
  263. private Set<String> _tableNames;
  264. /** Reads and writes database pages */
  265. private final PageChannel _pageChannel;
  266. /** System catalog table */
  267. private Table _systemCatalog;
  268. /** System access control entries table */
  269. private Table _accessControlEntries;
  270. /** System relationships table (initialized on first use) */
  271. private Table _relationships;
  272. /** System queries table (initialized on first use) */
  273. private Table _queries;
  274. /** SIDs to use for the ACEs added for new tables */
  275. private final List<byte[]> _newTableSIDs = new ArrayList<byte[]>();
  276. /** "big index support" is optional, but enabled by default */
  277. private Boolean _useBigIndex;
  278. /** optional error handler to use when row errors are encountered */
  279. private ErrorHandler _dbErrorHandler;
  280. /** the file format of the database */
  281. private FileFormat _fileFormat;
  282. /** charset to use when handling text */
  283. private Charset _charset;
  284. /** timezone to use when handling dates */
  285. private TimeZone _timeZone;
  286. /**
  287. * Open an existing Database. If the existing file is not writeable, the
  288. * file will be opened read-only. Auto-syncing is enabled for the returned
  289. * Database.
  290. * <p>
  291. * Equivalent to:
  292. * {@code open(mdbFile, false);}
  293. *
  294. * @param mdbFile File containing the database
  295. *
  296. * @see #open(File,boolean)
  297. */
  298. public static Database open(File mdbFile) throws IOException {
  299. return open(mdbFile, false);
  300. }
  301. /**
  302. * Open an existing Database. If the existing file is not writeable or the
  303. * readOnly flag is <code>true</code>, the file will be opened read-only.
  304. * Auto-syncing is enabled for the returned Database.
  305. * <p>
  306. * Equivalent to:
  307. * {@code open(mdbFile, readOnly, DEFAULT_AUTO_SYNC);}
  308. *
  309. * @param mdbFile File containing the database
  310. * @param readOnly iff <code>true</code>, force opening file in read-only
  311. * mode
  312. *
  313. * @see #open(File,boolean,boolean)
  314. */
  315. public static Database open(File mdbFile, boolean readOnly)
  316. throws IOException
  317. {
  318. return open(mdbFile, readOnly, DEFAULT_AUTO_SYNC);
  319. }
  320. /**
  321. * Open an existing Database. If the existing file is not writeable or the
  322. * readOnly flag is <code>true</code>, the file will be opened read-only.
  323. * @param mdbFile File containing the database
  324. * @param readOnly iff <code>true</code>, force opening file in read-only
  325. * mode
  326. * @param autoSync whether or not to enable auto-syncing on write. if
  327. * {@code true}, writes will be immediately flushed to disk.
  328. * This leaves the database in a (fairly) consistent state
  329. * on each write, but can be very inefficient for many
  330. * updates. if {@code false}, flushing to disk happens at
  331. * the jvm's leisure, which can be much faster, but may
  332. * leave the database in an inconsistent state if failures
  333. * are encountered during writing.
  334. */
  335. public static Database open(File mdbFile, boolean readOnly, boolean autoSync)
  336. throws IOException
  337. {
  338. return open(mdbFile, readOnly, autoSync, null, null);
  339. }
  340. /**
  341. * Open an existing Database. If the existing file is not writeable or the
  342. * readOnly flag is <code>true</code>, the file will be opened read-only.
  343. * @param mdbFile File containing the database
  344. * @param readOnly iff <code>true</code>, force opening file in read-only
  345. * mode
  346. * @param autoSync whether or not to enable auto-syncing on write. if
  347. * {@code true}, writes will be immediately flushed to disk.
  348. * This leaves the database in a (fairly) consistent state
  349. * on each write, but can be very inefficient for many
  350. * updates. if {@code false}, flushing to disk happens at
  351. * the jvm's leisure, which can be much faster, but may
  352. * leave the database in an inconsistent state if failures
  353. * are encountered during writing.
  354. * @param charset Charset to use, if {@code null}, uses default
  355. * @param timeZone TimeZone to use, if {@code null}, uses default
  356. */
  357. public static Database open(File mdbFile, boolean readOnly, boolean autoSync,
  358. Charset charset, TimeZone timeZone)
  359. throws IOException
  360. {
  361. if(!mdbFile.exists() || !mdbFile.canRead()) {
  362. throw new FileNotFoundException("given file does not exist: " + mdbFile);
  363. }
  364. // force read-only for non-writable files
  365. readOnly |= !mdbFile.canWrite();
  366. // open file channel
  367. FileChannel channel = openChannel(mdbFile, readOnly);
  368. if(!readOnly) {
  369. // verify that format supports writing
  370. JetFormat jetFormat = JetFormat.getFormat(channel);
  371. if(jetFormat.READ_ONLY) {
  372. // shutdown the channel (quietly)
  373. try {
  374. channel.close();
  375. } catch(Exception ignored) {
  376. // we don't care
  377. }
  378. throw new IOException("jet format '" + jetFormat + "' does not support writing");
  379. }
  380. }
  381. return new Database(channel, autoSync, null, charset, timeZone);
  382. }
  383. /**
  384. * Create a new Access 2000 Database
  385. * <p>
  386. * Equivalent to:
  387. * {@code create(FileFormat.V2000, mdbFile, DEFAULT_AUTO_SYNC);}
  388. *
  389. * @param mdbFile Location to write the new database to. <b>If this file
  390. * already exists, it will be overwritten.</b>
  391. *
  392. * @see #create(File,boolean)
  393. */
  394. public static Database create(File mdbFile) throws IOException {
  395. return create(mdbFile, DEFAULT_AUTO_SYNC);
  396. }
  397. /**
  398. * Create a new Database for the given fileFormat
  399. * <p>
  400. * Equivalent to:
  401. * {@code create(fileFormat, mdbFile, DEFAULT_AUTO_SYNC);}
  402. *
  403. * @param fileFormat version of new database.
  404. * @param mdbFile Location to write the new database to. <b>If this file
  405. * already exists, it will be overwritten.</b>
  406. *
  407. * @see #create(File,boolean)
  408. */
  409. public static Database create(FileFormat fileFormat, File mdbFile)
  410. throws IOException
  411. {
  412. return create(fileFormat, mdbFile, DEFAULT_AUTO_SYNC);
  413. }
  414. /**
  415. * Create a new Access 2000 Database
  416. * <p>
  417. * Equivalent to:
  418. * {@code create(FileFormat.V2000, mdbFile, DEFAULT_AUTO_SYNC);}
  419. *
  420. * @param mdbFile Location to write the new database to. <b>If this file
  421. * already exists, it will be overwritten.</b>
  422. * @param autoSync whether or not to enable auto-syncing on write. if
  423. * {@code true}, writes will be immediately flushed to disk.
  424. * This leaves the database in a (fairly) consistent state
  425. * on each write, but can be very inefficient for many
  426. * updates. if {@code false}, flushing to disk happens at
  427. * the jvm's leisure, which can be much faster, but may
  428. * leave the database in an inconsistent state if failures
  429. * are encountered during writing.
  430. */
  431. public static Database create(File mdbFile, boolean autoSync)
  432. throws IOException
  433. {
  434. return create(FileFormat.V2000, mdbFile, autoSync);
  435. }
  436. /**
  437. * Create a new Database for the given fileFormat
  438. * @param fileFormat version of new database.
  439. * @param mdbFile Location to write the new database to. <b>If this file
  440. * already exists, it will be overwritten.</b>
  441. * @param autoSync whether or not to enable auto-syncing on write. if
  442. * {@code true}, writes will be immediately flushed to disk.
  443. * This leaves the database in a (fairly) consistent state
  444. * on each write, but can be very inefficient for many
  445. * updates. if {@code false}, flushing to disk happens at
  446. * the jvm's leisure, which can be much faster, but may
  447. * leave the database in an inconsistent state if failures
  448. * are encountered during writing.
  449. */
  450. public static Database create(FileFormat fileFormat, File mdbFile,
  451. boolean autoSync)
  452. throws IOException
  453. {
  454. return create(fileFormat, mdbFile, autoSync, null, null);
  455. }
  456. /**
  457. * Create a new Database for the given fileFormat
  458. * @param fileFormat version of new database.
  459. * @param mdbFile Location to write the new database to. <b>If this file
  460. * already exists, it will be overwritten.</b>
  461. * @param autoSync whether or not to enable auto-syncing on write. if
  462. * {@code true}, writes will be immediately flushed to disk.
  463. * This leaves the database in a (fairly) consistent state
  464. * on each write, but can be very inefficient for many
  465. * updates. if {@code false}, flushing to disk happens at
  466. * the jvm's leisure, which can be much faster, but may
  467. * leave the database in an inconsistent state if failures
  468. * are encountered during writing.
  469. * @param charset Charset to use, if {@code null}, uses default
  470. * @param timeZone TimeZone to use, if {@code null}, uses default
  471. */
  472. public static Database create(FileFormat fileFormat, File mdbFile,
  473. boolean autoSync, Charset charset,
  474. TimeZone timeZone)
  475. throws IOException
  476. {
  477. if (fileFormat.getJetFormat().READ_ONLY) {
  478. throw new IOException("jet format '" + fileFormat.getJetFormat() + "' does not support writing");
  479. }
  480. FileChannel channel = openChannel(mdbFile, false);
  481. channel.truncate(0);
  482. channel.transferFrom(Channels.newChannel(
  483. Thread.currentThread().getContextClassLoader().getResourceAsStream(
  484. fileFormat._emptyFile)), 0, Integer.MAX_VALUE);
  485. return new Database(channel, autoSync, fileFormat, charset, timeZone);
  486. }
  487. /**
  488. * Package visible only to support unit tests via DatabaseTest.openChannel().
  489. * @param mdbFile file to open
  490. * @param readOnly true if read-only
  491. * @return a FileChannel on the given file.
  492. * @exception FileNotFoundException
  493. * if the mode is <tt>"r"</tt> but the given file object does
  494. * not denote an existing regular file, or if the mode begins
  495. * with <tt>"rw"</tt> but the given file object does not denote
  496. * an existing, writable regular file and a new regular file of
  497. * that name cannot be created, or if some other error occurs
  498. * while opening or creating the file
  499. */
  500. static FileChannel openChannel(final File mdbFile, final boolean readOnly)
  501. throws FileNotFoundException
  502. {
  503. final String mode = (readOnly ? "r" : "rw");
  504. return new RandomAccessFile(mdbFile, mode).getChannel();
  505. }
  506. /**
  507. * Create a new database by reading it in from a FileChannel.
  508. * @param channel File channel of the database. This needs to be a
  509. * FileChannel instead of a ReadableByteChannel because we need to
  510. * randomly jump around to various points in the file.
  511. * @param autoSync whether or not to enable auto-syncing on write. if
  512. * {@code true}, writes will be immediately flushed to disk.
  513. * This leaves the database in a (fairly) consistent state
  514. * on each write, but can be very inefficient for many
  515. * updates. if {@code false}, flushing to disk happens at
  516. * the jvm's leisure, which can be much faster, but may
  517. * leave the database in an inconsistent state if failures
  518. * are encountered during writing.
  519. * @param fileFormat version of new database (if known)
  520. * @param charset Charset to use, if {@code null}, uses default
  521. * @param timeZone TimeZone to use, if {@code null}, uses default
  522. */
  523. protected Database(FileChannel channel, boolean autoSync,
  524. FileFormat fileFormat, Charset charset, TimeZone timeZone)
  525. throws IOException
  526. {
  527. boolean success = false;
  528. try {
  529. _format = JetFormat.getFormat(channel);
  530. _charset = ((charset == null) ? getDefaultCharset(_format) : charset);
  531. _fileFormat = fileFormat;
  532. _pageChannel = new PageChannel(channel, _format, autoSync);
  533. _timeZone = ((timeZone == null) ? getDefaultTimeZone() : timeZone);
  534. // note, it's slighly sketchy to pass ourselves along partially
  535. // constructed, but only our _format and _pageChannel refs should be
  536. // needed
  537. _pageChannel.initialize(this);
  538. _buffer = _pageChannel.createPageBuffer();
  539. readSystemCatalog();
  540. success = true;
  541. } finally {
  542. if(!success && (channel != null)) {
  543. // something blew up, shutdown the channel (quietly)
  544. try {
  545. channel.close();
  546. } catch(Exception ignored) {
  547. // we don't care
  548. }
  549. }
  550. }
  551. }
  552. public PageChannel getPageChannel() {
  553. return _pageChannel;
  554. }
  555. public JetFormat getFormat() {
  556. return _format;
  557. }
  558. /**
  559. * @return The system catalog table
  560. */
  561. public Table getSystemCatalog() {
  562. return _systemCatalog;
  563. }
  564. /**
  565. * @return The system Access Control Entries table
  566. */
  567. public Table getAccessControlEntries() {
  568. return _accessControlEntries;
  569. }
  570. /**
  571. * Whether or not big index support is enabled for tables.
  572. */
  573. public boolean doUseBigIndex() {
  574. return (_useBigIndex != null ? _useBigIndex : true);
  575. }
  576. /**
  577. * Set whether or not big index support is enabled for tables.
  578. */
  579. public void setUseBigIndex(boolean useBigIndex) {
  580. _useBigIndex = useBigIndex;
  581. }
  582. /**
  583. * Gets the currently configured ErrorHandler (always non-{@code null}).
  584. * This will be used to handle all errors unless overridden at the Table or
  585. * Cursor level.
  586. */
  587. public ErrorHandler getErrorHandler() {
  588. return((_dbErrorHandler != null) ? _dbErrorHandler :
  589. DEFAULT_ERROR_HANDLER);
  590. }
  591. /**
  592. * Sets a new ErrorHandler. If {@code null}, resets to the
  593. * {@link #DEFAULT_ERROR_HANDLER}.
  594. */
  595. public void setErrorHandler(ErrorHandler newErrorHandler) {
  596. _dbErrorHandler = newErrorHandler;
  597. }
  598. /**
  599. * Gets currently configured TimeZone (always non-{@code null}).
  600. */
  601. public TimeZone getTimeZone()
  602. {
  603. return _timeZone;
  604. }
  605. /**
  606. * Sets a new TimeZone. If {@code null}, resets to the value returned by
  607. * {@link #getDefaultTimeZone}.
  608. */
  609. public void setTimeZone(TimeZone newTimeZone) {
  610. if(newTimeZone == null) {
  611. newTimeZone = getDefaultTimeZone();
  612. }
  613. _timeZone = newTimeZone;
  614. }
  615. /**
  616. * Gets currently configured Charset (always non-{@code null}).
  617. */
  618. public Charset getCharset()
  619. {
  620. return _charset;
  621. }
  622. /**
  623. * Sets a new Charset. If {@code null}, resets to the value returned by
  624. * {@link #getDefaultCharset}.
  625. */
  626. public void setCharset(Charset newCharset) {
  627. if(newCharset == null) {
  628. newCharset = getDefaultCharset(getFormat());
  629. }
  630. _charset = newCharset;
  631. }
  632. /**
  633. * Returns the FileFormat of this database (which may involve inspecting the
  634. * database itself).
  635. * @throws IllegalStateException if the file format cannot be determined
  636. */
  637. public FileFormat getFileFormat()
  638. {
  639. if(_fileFormat == null) {
  640. Map<Database.FileFormat,byte[]> possibleFileFormats =
  641. getFormat().getPossibleFileFormats();
  642. if(possibleFileFormats.size() == 1) {
  643. // single possible format, easy enough
  644. _fileFormat = possibleFileFormats.keySet().iterator().next();
  645. } else {
  646. // need to check the "AccessVersion" property
  647. byte[] dbProps = null;
  648. for(Map<String,Object> row :
  649. Cursor.createCursor(_systemCatalog).iterable(
  650. Arrays.asList(CAT_COL_NAME, CAT_COL_PROPS))) {
  651. if(OBJECT_NAME_DBPROPS.equals(row.get(CAT_COL_NAME))) {
  652. dbProps = (byte[])row.get(CAT_COL_PROPS);
  653. break;
  654. }
  655. }
  656. if(dbProps != null) {
  657. // search for certain "version strings" in the properties (we
  658. // can't fully parse the properties objects, but we can still
  659. // find the byte pattern)
  660. ByteBuffer dbPropBuf = ByteBuffer.wrap(dbProps);
  661. for(Map.Entry<Database.FileFormat,byte[]> possible :
  662. possibleFileFormats.entrySet()) {
  663. if(ByteUtil.findRange(dbPropBuf, 0, possible.getValue()) >= 0) {
  664. _fileFormat = possible.getKey();
  665. break;
  666. }
  667. }
  668. }
  669. if(_fileFormat == null) {
  670. throw new IllegalStateException("Could not determine FileFormat");
  671. }
  672. }
  673. }
  674. return _fileFormat;
  675. }
  676. /**
  677. * Read the system catalog
  678. */
  679. private void readSystemCatalog() throws IOException {
  680. _systemCatalog = readTable(TABLE_SYSTEM_CATALOG, PAGE_SYSTEM_CATALOG,
  681. defaultUseBigIndex());
  682. for(Map<String,Object> row :
  683. Cursor.createCursor(_systemCatalog).iterable(
  684. SYSTEM_CATALOG_COLUMNS))
  685. {
  686. String name = (String) row.get(CAT_COL_NAME);
  687. if (name != null && TYPE_TABLE.equals(row.get(CAT_COL_TYPE))) {
  688. if (!name.startsWith(PREFIX_SYSTEM)) {
  689. addTable((String) row.get(CAT_COL_NAME), (Integer) row.get(CAT_COL_ID));
  690. } else if(TABLE_SYSTEM_ACES.equals(name)) {
  691. int pageNumber = (Integer)row.get(CAT_COL_ID);
  692. _accessControlEntries = readTable(TABLE_SYSTEM_ACES, pageNumber,
  693. defaultUseBigIndex());
  694. }
  695. } else if (SYSTEM_OBJECT_NAME_TABLES.equals(name)) {
  696. _tableParentId = (Integer) row.get(CAT_COL_ID);
  697. }
  698. }
  699. // check for required system values
  700. if(_accessControlEntries == null) {
  701. throw new IOException("Did not find required " + TABLE_SYSTEM_ACES +
  702. " table");
  703. }
  704. if(_tableParentId == null) {
  705. throw new IOException("Did not find required parent table id");
  706. }
  707. if (LOG.isDebugEnabled()) {
  708. LOG.debug("Finished reading system catalog. Tables: " +
  709. getTableNames());
  710. }
  711. }
  712. /**
  713. * @return The names of all of the user tables (String)
  714. */
  715. public Set<String> getTableNames() {
  716. if(_tableNames == null) {
  717. _tableNames = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
  718. for(TableInfo tableInfo : _tableLookup.values()) {
  719. _tableNames.add(tableInfo.tableName);
  720. }
  721. }
  722. return _tableNames;
  723. }
  724. /**
  725. * @return an unmodifiable Iterator of the user Tables in this Database.
  726. * @throws IllegalStateException if an IOException is thrown by one of the
  727. * operations, the actual exception will be contained within
  728. * @throws ConcurrentModificationException if a table is added to the
  729. * database while an Iterator is in use.
  730. */
  731. public Iterator<Table> iterator() {
  732. return new TableIterator();
  733. }
  734. /**
  735. * @param name Table name
  736. * @return The table, or null if it doesn't exist
  737. */
  738. public Table getTable(String name) throws IOException {
  739. return getTable(name, defaultUseBigIndex());
  740. }
  741. /**
  742. * @param name Table name
  743. * @param useBigIndex whether or not "big index support" should be enabled
  744. * for the table (this value will override any other
  745. * settings)
  746. * @return The table, or null if it doesn't exist
  747. */
  748. public Table getTable(String name, boolean useBigIndex) throws IOException {
  749. TableInfo tableInfo = lookupTable(name);
  750. if ((tableInfo == null) || (tableInfo.pageNumber == null)) {
  751. return null;
  752. }
  753. return readTable(tableInfo.tableName, tableInfo.pageNumber, useBigIndex);
  754. }
  755. /**
  756. * Create a new table in this database
  757. * @param name Name of the table to create
  758. * @param columns List of Columns in the table
  759. */
  760. public void createTable(String name, List<Column> columns)
  761. throws IOException
  762. {
  763. validateIdentifierName(name, _format.MAX_TABLE_NAME_LENGTH, "table");
  764. if(getTable(name) != null) {
  765. throw new IllegalArgumentException(
  766. "Cannot create table with name of existing table");
  767. }
  768. if(columns.isEmpty()) {
  769. throw new IllegalArgumentException(
  770. "Cannot create table with no columns");
  771. }
  772. if(columns.size() > _format.MAX_COLUMNS_PER_TABLE) {
  773. throw new IllegalArgumentException(
  774. "Cannot create table with more than " +
  775. _format.MAX_COLUMNS_PER_TABLE + " columns");
  776. }
  777. Set<String> colNames = new HashSet<String>();
  778. // next, validate the column definitions
  779. for(Column column : columns) {
  780. column.validate(_format);
  781. if(!colNames.add(column.getName().toUpperCase())) {
  782. throw new IllegalArgumentException("duplicate column name: " +
  783. column.getName());
  784. }
  785. }
  786. List<Column> autoCols = Table.getAutoNumberColumns(columns);
  787. if(autoCols.size() > 1) {
  788. // we can have one of each type
  789. Set<DataType> autoTypes = EnumSet.noneOf(DataType.class);
  790. for(Column c : autoCols) {
  791. if(!autoTypes.add(c.getType())) {
  792. throw new IllegalArgumentException(
  793. "Can have at most one AutoNumber column of type " + c.getType() + " per table");
  794. }
  795. }
  796. }
  797. //Write the tdef page to disk.
  798. int tdefPageNumber = Table.writeTableDefinition(columns, _pageChannel,
  799. _format, getCharset());
  800. //Add this table to our internal list.
  801. addTable(name, Integer.valueOf(tdefPageNumber));
  802. //Add this table to system tables
  803. addToSystemCatalog(name, tdefPageNumber);
  804. addToAccessControlEntries(tdefPageNumber);
  805. }
  806. /**
  807. * Finds all the relationships in the database between the given tables.
  808. */
  809. public List<Relationship> getRelationships(Table table1, Table table2)
  810. throws IOException
  811. {
  812. // the relationships table does not get loaded until first accessed
  813. if(_relationships == null) {
  814. _relationships = getSystemTable(TABLE_SYSTEM_RELATIONSHIPS);
  815. if(_relationships == null) {
  816. throw new IOException("Could not find system relationships table");
  817. }
  818. }
  819. int nameCmp = table1.getName().compareTo(table2.getName());
  820. if(nameCmp == 0) {
  821. throw new IllegalArgumentException("Must provide two different tables");
  822. }
  823. if(nameCmp > 0) {
  824. // we "order" the two tables given so that we will return a collection
  825. // of relationships in the same order regardless of whether we are given
  826. // (TableFoo, TableBar) or (TableBar, TableFoo).
  827. Table tmp = table1;
  828. table1 = table2;
  829. table2 = tmp;
  830. }
  831. List<Relationship> relationships = new ArrayList<Relationship>();
  832. Cursor cursor = createCursorWithOptionalIndex(
  833. _relationships, REL_COL_FROM_TABLE, table1.getName());
  834. collectRelationships(cursor, table1, table2, relationships);
  835. cursor = createCursorWithOptionalIndex(
  836. _relationships, REL_COL_TO_TABLE, table1.getName());
  837. collectRelationships(cursor, table2, table1, relationships);
  838. return relationships;
  839. }
  840. /**
  841. * Finds all the queries in the database.
  842. */
  843. public List<Query> getQueries()
  844. throws IOException
  845. {
  846. // the queries table does not get loaded until first accessed
  847. if(_queries == null) {
  848. _queries = getSystemTable(TABLE_SYSTEM_QUERIES);
  849. if(_queries == null) {
  850. throw new IOException("Could not find system queries table");
  851. }
  852. }
  853. // find all the queries from the system catalog
  854. List<Map<String,Object>> queryInfo = new ArrayList<Map<String,Object>>();
  855. Map<Integer,List<Query.Row>> queryRowMap =
  856. new HashMap<Integer,List<Query.Row>>();
  857. for(Map<String,Object> row :
  858. Cursor.createCursor(_systemCatalog).iterable(
  859. SYSTEM_CATALOG_QUERY_COLUMNS))
  860. {
  861. String name = (String) row.get(CAT_COL_NAME);
  862. if (name != null && TYPE_QUERY.equals(row.get(CAT_COL_TYPE))) {
  863. queryInfo.add(row);
  864. Integer id = (Integer)row.get(CAT_COL_ID);
  865. queryRowMap.put(id, new ArrayList<Query.Row>());
  866. }
  867. }
  868. // find all the query rows
  869. for(Map<String,Object> row : Cursor.createCursor(_queries)) {
  870. Query.Row queryRow = new Query.Row(row);
  871. List<Query.Row> queryRows = queryRowMap.get(queryRow.objectId);
  872. if(queryRows == null) {
  873. LOG.warn("Found rows for query with id " + queryRow.objectId +
  874. " missing from system catalog");
  875. continue;
  876. }
  877. queryRows.add(queryRow);
  878. }
  879. // lastly, generate all the queries
  880. List<Query> queries = new ArrayList<Query>();
  881. for(Map<String,Object> row : queryInfo) {
  882. String name = (String) row.get(CAT_COL_NAME);
  883. Integer id = (Integer)row.get(CAT_COL_ID);
  884. int flags = (Integer)row.get(CAT_COL_FLAGS);
  885. List<Query.Row> queryRows = queryRowMap.get(id);
  886. queries.add(Query.create(flags, name, queryRows, id));
  887. }
  888. return queries;
  889. }
  890. /**
  891. * Returns a reference to <i>any</i> available table in this access
  892. * database, including system tables.
  893. * <p>
  894. * Warning, this method is not designed for common use, only for the
  895. * occassional time when access to a system table is necessary. Messing
  896. * with system tables can strip the paint off your house and give your whole
  897. * family a permanent, orange afro. You have been warned.
  898. *
  899. * @param tableName Table name, may be a system table
  900. * @return The table, or {@code null} if it doesn't exist
  901. */
  902. public Table getSystemTable(String tableName)
  903. throws IOException
  904. {
  905. for(Map<String,Object> row :
  906. Cursor.createCursor(_systemCatalog).iterable(
  907. SYSTEM_CATALOG_COLUMNS))
  908. {
  909. String name = (String) row.get(CAT_COL_NAME);
  910. if (tableName.equalsIgnoreCase(name) &&
  911. TYPE_TABLE.equals(row.get(CAT_COL_TYPE))) {
  912. Integer pageNumber = (Integer) row.get(CAT_COL_ID);
  913. if(pageNumber != null) {
  914. return readTable(name, pageNumber, defaultUseBigIndex());
  915. }
  916. }
  917. }
  918. return null;
  919. }
  920. /**
  921. * Finds the relationships matching the given from and to tables from the
  922. * given cursor and adds them to the given list.
  923. */
  924. private void collectRelationships(
  925. Cursor cursor, Table fromTable, Table toTable,
  926. List<Relationship> relationships)
  927. {
  928. for(Map<String,Object> row : cursor) {
  929. String fromName = (String)row.get(REL_COL_FROM_TABLE);
  930. String toName = (String)row.get(REL_COL_TO_TABLE);
  931. if(fromTable.getName().equalsIgnoreCase(fromName) &&
  932. toTable.getName().equalsIgnoreCase(toName))
  933. {
  934. String relName = (String)row.get(REL_COL_NAME);
  935. // found more info for a relationship. see if we already have some
  936. // info for this relationship
  937. Relationship rel = null;
  938. for(Relationship tmp : relationships) {
  939. if(tmp.getName().equalsIgnoreCase(relName)) {
  940. rel = tmp;
  941. break;
  942. }
  943. }
  944. if(rel == null) {
  945. // new relationship
  946. int numCols = (Integer)row.get(REL_COL_COLUMN_COUNT);
  947. int flags = (Integer)row.get(REL_COL_FLAGS);
  948. rel = new Relationship(relName, fromTable, toTable,
  949. flags, numCols);
  950. relationships.add(rel);
  951. }
  952. // add column info
  953. int colIdx = (Integer)row.get(REL_COL_COLUMN_INDEX);
  954. Column fromCol = fromTable.getColumn(
  955. (String)row.get(REL_COL_FROM_COLUMN));
  956. Column toCol = toTable.getColumn(
  957. (String)row.get(REL_COL_TO_COLUMN));
  958. rel.getFromColumns().set(colIdx, fromCol);
  959. rel.getToColumns().set(colIdx, toCol);
  960. }
  961. }
  962. }
  963. /**
  964. * Add a new table to the system catalog
  965. * @param name Table name
  966. * @param pageNumber Page number that contains the table definition
  967. */
  968. private void addToSystemCatalog(String name, int pageNumber)
  969. throws IOException
  970. {
  971. Object[] catalogRow = new Object[_systemCatalog.getColumnCount()];
  972. int idx = 0;
  973. Date creationTime = new Date();
  974. for (Iterator<Column> iter = _systemCatalog.getColumns().iterator();
  975. iter.hasNext(); idx++)
  976. {
  977. Column col = iter.next();
  978. if (CAT_COL_ID.equals(col.getName())) {
  979. catalogRow[idx] = Integer.valueOf(pageNumber);
  980. } else if (CAT_COL_NAME.equals(col.getName())) {
  981. catalogRow[idx] = name;
  982. } else if (CAT_COL_TYPE.equals(col.getName())) {
  983. catalogRow[idx] = TYPE_TABLE;
  984. } else if (CAT_COL_DATE_CREATE.equals(col.getName()) ||
  985. CAT_COL_DATE_UPDATE.equals(col.getName())) {
  986. catalogRow[idx] = creationTime;
  987. } else if (CAT_COL_PARENT_ID.equals(col.getName())) {
  988. catalogRow[idx] = _tableParentId;
  989. } else if (CAT_COL_FLAGS.equals(col.getName())) {
  990. catalogRow[idx] = Integer.valueOf(0);
  991. } else if (CAT_COL_OWNER.equals(col.getName())) {
  992. byte[] owner = new byte[2];
  993. catalogRow[idx] = owner;
  994. owner[0] = (byte) 0xcf;
  995. owner[1] = (byte) 0x5f;
  996. }
  997. }
  998. _systemCatalog.addRow(catalogRow);
  999. }
  1000. /**
  1001. * Add a new table to the system's access control entries
  1002. * @param pageNumber Page number that contains the table definition
  1003. */
  1004. private void addToAccessControlEntries(int pageNumber) throws IOException {
  1005. if(_newTableSIDs.isEmpty()) {
  1006. initNewTableSIDs();
  1007. }
  1008. Column acmCol = _accessControlEntries.getColumn(ACE_COL_ACM);
  1009. Column inheritCol = _accessControlEntries.getColumn(ACE_COL_F_INHERITABLE);
  1010. Column objIdCol = _accessControlEntries.getColumn(ACE_COL_OBJECT_ID);
  1011. Column sidCol = _accessControlEntries.getColumn(ACE_COL_SID);
  1012. // construct a collection of ACE entries mimicing those of our parent, the
  1013. // "Tables" system object
  1014. List<Object[]> aceRows = new ArrayList<Object[]>(_newTableSIDs.size());
  1015. for(byte[] sid : _newTableSIDs) {
  1016. Object[] aceRow = new Object[_accessControlEntries.getColumnCount()];
  1017. aceRow[acmCol.getColumnIndex()] = SYS_FULL_ACCESS_ACM;
  1018. aceRow[inheritCol.getColumnIndex()] = Boolean.FALSE;
  1019. aceRow[objIdCol.getColumnIndex()] = Integer.valueOf(pageNumber);
  1020. aceRow[sidCol.getColumnIndex()] = sid;
  1021. aceRows.add(aceRow);
  1022. }
  1023. _accessControlEntries.addRows(aceRows);
  1024. }
  1025. /**
  1026. * Determines the collection of SIDs which need to be added to new tables.
  1027. */
  1028. private void initNewTableSIDs() throws IOException
  1029. {
  1030. // search for ACEs matching the tableParentId. use the index on the
  1031. // objectId column if found (should be there)
  1032. Cursor cursor = createCursorWithOptionalIndex(
  1033. _accessControlEntries, ACE_COL_OBJECT_ID, _tableParentId);
  1034. for(Map<String, Object> row : cursor) {
  1035. Integer objId = (Integer)row.get(ACE_COL_OBJECT_ID);
  1036. if(_tableParentId.equals(objId)) {
  1037. _newTableSIDs.add((byte[])row.get(ACE_COL_SID));
  1038. }
  1039. }
  1040. if(_newTableSIDs.isEmpty()) {
  1041. // if all else fails, use the hard-coded default
  1042. _newTableSIDs.add(SYS_DEFAULT_SID);
  1043. }
  1044. }
  1045. /**
  1046. * Reads a table with the given name from the given pageNumber.
  1047. */
  1048. private Table readTable(String name, int pageNumber, boolean useBigIndex)
  1049. throws IOException
  1050. {
  1051. _pageChannel.readPage(_buffer, pageNumber);
  1052. byte pageType = _buffer.get(0);
  1053. if (pageType != PageTypes.TABLE_DEF) {
  1054. throw new IOException("Looking for " + name + " at page " + pageNumber +
  1055. ", but page type is " + pageType);
  1056. }
  1057. return new Table(this, _buffer, pageNumber, name, useBigIndex);
  1058. }
  1059. /**
  1060. * Creates a Cursor restricted to the given column value if possible (using
  1061. * an existing index), otherwise a simple table cursor.
  1062. */
  1063. private static Cursor createCursorWithOptionalIndex(
  1064. Table table, String colName, Object colValue)
  1065. throws IOException
  1066. {
  1067. try {
  1068. return new CursorBuilder(table)
  1069. .setIndexByColumns(table.getColumn(colName))
  1070. .setSpecificEntry(colValue)
  1071. .toCursor();
  1072. } catch(IllegalArgumentException e) {
  1073. LOG.info("Could not find expected index on table " + table.getName());
  1074. }
  1075. // use table scan instead
  1076. return Cursor.createCursor(table);
  1077. }
  1078. /**
  1079. * Copy an existing JDBC ResultSet into a new table in this database
  1080. *
  1081. * @param name Name of the new table to create
  1082. * @param source ResultSet to copy from
  1083. *
  1084. * @return the name of the copied table
  1085. *
  1086. * @see ImportUtil#importResultSet(ResultSet,Database,String)
  1087. */
  1088. public String copyTable(String name, ResultSet source)
  1089. throws SQLException, IOException
  1090. {
  1091. return ImportUtil.importResultSet(source, this, name);
  1092. }
  1093. /**
  1094. * Copy an existing JDBC ResultSet into a new table in this database
  1095. *
  1096. * @param name Name of the new table to create
  1097. * @param source ResultSet to copy from
  1098. * @param filter valid import filter
  1099. *
  1100. * @return the name of the imported table
  1101. *
  1102. * @see ImportUtil#importResultSet(ResultSet,Database,String,ImportFilter)
  1103. */
  1104. public String copyTable(String name, ResultSet source, ImportFilter filter)
  1105. throws SQLException, IOException
  1106. {
  1107. return ImportUtil.importResultSet(source, this, name, filter);
  1108. }
  1109. /**
  1110. * Copy a delimited text file into a new table in this database
  1111. *
  1112. * @param name Name of the new table to create
  1113. * @param f Source file to import
  1114. * @param delim Regular expression representing the delimiter string.
  1115. *
  1116. * @return the name of the imported table
  1117. *
  1118. * @see ImportUtil#importFile(File,Database,String,String)
  1119. */
  1120. public String importFile(String name, File f, String delim)
  1121. throws IOException
  1122. {
  1123. return ImportUtil.importFile(f, this, name, delim);
  1124. }
  1125. /**
  1126. * Copy a delimited text file into a new table in this database
  1127. *
  1128. * @param name Name of the new table to create
  1129. * @param f Source file to import
  1130. * @param delim Regular expression representing the delimiter string.
  1131. * @param filter valid import filter
  1132. *
  1133. * @return the name of the imported table
  1134. *
  1135. * @see ImportUtil#importFile(File,Database,String,String,ImportFilter)
  1136. */
  1137. public String importFile(String name, File f, String delim,
  1138. ImportFilter filter)
  1139. throws IOException
  1140. {
  1141. return ImportUtil.importFile(f, this, name, delim, filter);
  1142. }
  1143. /**
  1144. * Copy a delimited text file into a new table in this database
  1145. *
  1146. * @param name Name of the new table to create
  1147. * @param in Source reader to import
  1148. * @param delim Regular expression representing the delimiter string.
  1149. *
  1150. * @return the name of the imported table
  1151. *
  1152. * @see ImportUtil#importReader(BufferedReader,Database,String,String)
  1153. */
  1154. public String importReader(String name, BufferedReader in, String delim)
  1155. throws IOException
  1156. {
  1157. return ImportUtil.importReader(in, this, name, delim);
  1158. }
  1159. /**
  1160. * Copy a delimited text file into a new table in this database
  1161. * @param name Name of the new table to create
  1162. * @param in Source reader to import
  1163. * @param delim Regular expression representing the delimiter string.
  1164. * @param filter valid import filter
  1165. *
  1166. * @return the name of the imported table
  1167. *
  1168. * @see ImportUtil#importReader(BufferedReader,Database,String,String,ImportFilter)
  1169. */
  1170. public String importReader(String name, BufferedReader in, String delim,
  1171. ImportFilter filter)
  1172. throws IOException
  1173. {
  1174. return ImportUtil.importReader(in, this, name, delim, filter);
  1175. }
  1176. /**
  1177. * Flushes any current changes to the database file to disk.
  1178. */
  1179. public void flush() throws IOException {
  1180. _pageChannel.flush();
  1181. }
  1182. /**
  1183. * Close the database file
  1184. */
  1185. public void close() throws IOException {
  1186. _pageChannel.close();
  1187. }
  1188. /**
  1189. * @return A table or column name escaped for Access
  1190. */
  1191. public static String escapeIdentifier(String s) {
  1192. if (isReservedWord(s)) {
  1193. return ESCAPE_PREFIX + s;
  1194. }
  1195. return s;
  1196. }
  1197. /**
  1198. * @return {@code true} if the given string is a reserved word,
  1199. * {@code false} otherwise
  1200. */
  1201. public static boolean isReservedWord(String s) {
  1202. return RESERVED_WORDS.contains(s.toLowerCase());
  1203. }
  1204. /**
  1205. * Validates an identifier name.
  1206. */
  1207. public static void validateIdentifierName(String name,
  1208. int maxLength,
  1209. String identifierType)
  1210. {
  1211. if((name == null) || (name.trim().length() == 0)) {
  1212. throw new IllegalArgumentException(
  1213. identifierType + " must have non-empty name");
  1214. }
  1215. if(name.length() > maxLength) {
  1216. throw new IllegalArgumentException(
  1217. identifierType + " name is longer than max length of " + maxLength +
  1218. ": " + name);
  1219. }
  1220. }
  1221. @Override
  1222. public String toString() {
  1223. return ToStringBuilder.reflectionToString(this);
  1224. }
  1225. /**
  1226. * Adds a table to the _tableLookup and resets the _tableNames set
  1227. */
  1228. private void addTable(String tableName, Integer pageNumber)
  1229. {
  1230. _tableLookup.put(toLookupTableName(tableName),
  1231. new TableInfo(pageNumber, tableName));
  1232. // clear this, will be created next time needed
  1233. _tableNames = null;
  1234. }
  1235. /**
  1236. * @return the tableInfo of the given table, if any
  1237. */
  1238. private TableInfo lookupTable(String tableName) {
  1239. return _tableLookup.get(toLookupTableName(tableName));
  1240. }
  1241. /**
  1242. * @return a string usable in the _tableLookup map.
  1243. */
  1244. private String toLookupTableName(String tableName) {
  1245. return ((tableName != null) ? tableName.toUpperCase() : null);
  1246. }
  1247. /**
  1248. * Returns {@code false} if "big index support" has been disabled explicity
  1249. * on the this Database or via a system property, {@code true} otherwise.
  1250. */
  1251. public boolean defaultUseBigIndex() {
  1252. if(_useBigIndex != null) {
  1253. return _useBigIndex;
  1254. }
  1255. String prop = System.getProperty(USE_BIG_INDEX_PROPERTY);
  1256. if(prop != null) {
  1257. return Boolean.TRUE.toString().equalsIgnoreCase(prop);
  1258. }
  1259. return true;
  1260. }
  1261. /**
  1262. * Returns the default TimeZone. This is normally the platform default
  1263. * TimeZone as returned by {@link TimeZone#getDefault}, but can be
  1264. * overridden using the system property {@value #TIMEZONE_PROPERTY}.
  1265. */
  1266. public static TimeZone getDefaultTimeZone()
  1267. {
  1268. String tzProp = System.getProperty(TIMEZONE_PROPERTY);
  1269. if(tzProp != null) {
  1270. tzProp = tzProp.trim();
  1271. if(tzProp.length() > 0) {
  1272. return TimeZone.getTimeZone(tzProp);
  1273. }
  1274. }
  1275. // use system default
  1276. return TimeZone.getDefault();
  1277. }
  1278. /**
  1279. * Returns the default Charset for the given JetFormat. This may or may not
  1280. * be platform specific, depending on the format, but can be overridden
  1281. * using a system property composed of the prefix
  1282. * {@value #CHARSET_PROPERTY_PREFIX} followed by the JetFormat version to
  1283. * which the charset should apply, e.g. {@code
  1284. * "com.healthmarketscience.jackcess.charset.VERSION_3"}.
  1285. */
  1286. public static Charset getDefaultCharset(JetFormat format)
  1287. {
  1288. String csProp = System.getProperty(CHARSET_PROPERTY_PREFIX + format);
  1289. if(csProp != null) {
  1290. csProp = csProp.trim();
  1291. if(csProp.length() > 0) {
  1292. return Charset.forName(csProp);
  1293. }
  1294. }
  1295. // use format default
  1296. return format.CHARSET;
  1297. }
  1298. /**
  1299. * Utility class for storing table page number and actual name.
  1300. */
  1301. private static class TableInfo
  1302. {
  1303. public final Integer pageNumber;
  1304. public final String tableName;
  1305. private TableInfo(Integer newPageNumber,
  1306. String newTableName) {
  1307. pageNumber = newPageNumber;
  1308. tableName = newTableName;
  1309. }
  1310. }
  1311. /**
  1312. * Table iterator for this database, unmodifiable.
  1313. */
  1314. private class TableIterator implements Iterator<Table>
  1315. {
  1316. private Iterator<String> _tableNameIter;
  1317. private TableIterator() {
  1318. _tableNameIter = getTableNames().iterator();
  1319. }
  1320. public boolean hasNext() {
  1321. return _tableNameIter.hasNext();
  1322. }
  1323. public void remove() {
  1324. throw new UnsupportedOperationException();
  1325. }
  1326. public Table next() {
  1327. if(!hasNext()) {
  1328. throw new NoSuchElementException();
  1329. }
  1330. try {
  1331. return getTable(_tableNameIter.next());
  1332. } catch(IOException e) {
  1333. throw new IllegalStateException(e);
  1334. }
  1335. }
  1336. }
  1337. }