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.

CustomLinkResolver.java 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. /*
  2. Copyright (c) 2017 James Ahlborn
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package com.healthmarketscience.jackcess.util;
  14. import java.io.Closeable;
  15. import java.io.IOException;
  16. import java.nio.channels.FileChannel;
  17. import java.nio.file.Files;
  18. import java.nio.file.Path;
  19. import java.nio.file.Paths;
  20. import java.util.Random;
  21. import com.healthmarketscience.jackcess.Database;
  22. import com.healthmarketscience.jackcess.Database.FileFormat;
  23. import com.healthmarketscience.jackcess.Table;
  24. import com.healthmarketscience.jackcess.impl.ByteUtil;
  25. import com.healthmarketscience.jackcess.impl.DatabaseImpl;
  26. import com.healthmarketscience.jackcess.impl.TableImpl;
  27. /**
  28. * Utility base implementaton of LinkResolver which facilitates loading linked
  29. * tables from files which are not access databases. The LinkResolver API
  30. * ultimately presents linked table information to the primary database using
  31. * the jackcess {@link Database} and {@link Table} classes. In order to
  32. * consume linked tables in non-mdb files, they need to somehow be coerced
  33. * into the appropriate form. The approach taken by this utility is to make
  34. * it easy to copy the external tables into a temporary mdb file for
  35. * consumption by the primary database.
  36. * <p>
  37. * The primary features of this utility:
  38. * <ul>
  39. * <li>Supports custom behavior for non-mdb files and default behavior for mdb
  40. * files, see {@link #loadCustomFile}</li>
  41. * <li>Temp db can be an actual file or entirely in memory</li>
  42. * <li>Linked tables are loaded on-demand, see {@link #loadCustomTable}</li>
  43. * <li>Temp db files will be automatically deleted on close</li>
  44. * </ul>
  45. *
  46. * @author James Ahlborn
  47. * @usage _intermediate_class_
  48. */
  49. public abstract class CustomLinkResolver implements LinkResolver
  50. {
  51. private static final Random DB_ID = new Random();
  52. private static final String MEM_DB_PREFIX = "memdb_";
  53. private static final String FILE_DB_PREFIX = "linkeddb_";
  54. /** the default file format used for temp dbs */
  55. public static final FileFormat DEFAULT_FORMAT = FileFormat.V2000;
  56. /** temp dbs default to the filesystem, not in memory */
  57. public static final boolean DEFAULT_IN_MEMORY = false;
  58. /** temp dbs end up in the system temp dir by default */
  59. public static final Path DEFAULT_TEMP_DIR = null;
  60. private final FileFormat _defaultFormat;
  61. private final boolean _defaultInMemory;
  62. private final Path _defaultTempDir;
  63. /**
  64. * Creates a CustomLinkResolver using the default behavior for creating temp
  65. * dbs, see {@link #DEFAULT_FORMAT}, {@link #DEFAULT_IN_MEMORY} and
  66. * {@link #DEFAULT_TEMP_DIR}.
  67. */
  68. protected CustomLinkResolver() {
  69. this(DEFAULT_FORMAT, DEFAULT_IN_MEMORY, DEFAULT_TEMP_DIR);
  70. }
  71. /**
  72. * Creates a CustomLinkResolver with the given default behavior for creating
  73. * temp dbs.
  74. *
  75. * @param defaultFormat the default format for the temp db
  76. * @param defaultInMemory whether or not the temp db should be entirely in
  77. * memory by default (while this will be faster, it
  78. * should only be used if table data is expected to
  79. * fit entirely in memory)
  80. * @param defaultTempDir the default temp dir for a file based temp db
  81. * ({@code null} for the system defaqult temp
  82. * directory)
  83. */
  84. protected CustomLinkResolver(FileFormat defaultFormat, boolean defaultInMemory,
  85. Path defaultTempDir)
  86. {
  87. _defaultFormat = defaultFormat;
  88. _defaultInMemory = defaultInMemory;
  89. _defaultTempDir = defaultTempDir;
  90. }
  91. protected FileFormat getDefaultFormat() {
  92. return _defaultFormat;
  93. }
  94. protected boolean isDefaultInMemory() {
  95. return _defaultInMemory;
  96. }
  97. protected Path getDefaultTempDirectory() {
  98. return _defaultTempDir;
  99. }
  100. /**
  101. * Custom implementation is:
  102. * <pre>
  103. * // attempt to load the linkeeFileName as a custom file
  104. * Object customFile = loadCustomFile(linkerDb, linkeeFileName);
  105. *
  106. * if(customFile != null) {
  107. * // this is a custom file, create and return relevant temp db
  108. * return createTempDb(customFile, getDefaultFormat(), isDefaultInMemory(),
  109. * getDefaultTempDirectory());
  110. * }
  111. *
  112. * // not a custmom file, load using the default behavior
  113. * return LinkResolver.DEFAULT.resolveLinkedDatabase(linkerDb, linkeeFileName);
  114. * </pre>
  115. *
  116. * @see #loadCustomFile
  117. * @see #createTempDb
  118. * @see LinkResolver#DEFAULT
  119. */
  120. @Override
  121. public Database resolveLinkedDatabase(Database linkerDb, String linkeeFileName)
  122. throws IOException
  123. {
  124. Object customFile = loadCustomFile(linkerDb, linkeeFileName);
  125. if(customFile != null) {
  126. // if linker is read-only, open linkee read-only
  127. boolean readOnly = ((linkerDb instanceof DatabaseImpl) ?
  128. ((DatabaseImpl)linkerDb).isReadOnly() : false);
  129. return createTempDb(customFile, getDefaultFormat(), isDefaultInMemory(),
  130. getDefaultTempDirectory(), readOnly);
  131. }
  132. return LinkResolver.DEFAULT.resolveLinkedDatabase(linkerDb, linkeeFileName);
  133. }
  134. /**
  135. * Creates a temporary database for holding the table data from
  136. * linkeeFileName.
  137. *
  138. * @param customFile custom file state returned from {@link #loadCustomFile}
  139. * @param format the access format for the temp db
  140. * @param inMemory whether or not the temp db should be entirely in memory
  141. * (while this will be faster, it should only be used if
  142. * table data is expected to fit entirely in memory)
  143. * @param tempDir the temp dir for a file based temp db ({@code null} for
  144. * the system default temp directory)
  145. *
  146. * @return the temp db for holding the linked table info
  147. */
  148. protected Database createTempDb(Object customFile, FileFormat format,
  149. boolean inMemory, Path tempDir,
  150. boolean readOnly)
  151. throws IOException
  152. {
  153. Path dbFile = null;
  154. FileChannel channel = null;
  155. boolean success = false;
  156. try {
  157. if(inMemory) {
  158. dbFile = Paths.get(MEM_DB_PREFIX + DB_ID.nextLong() +
  159. format.getFileExtension());
  160. channel = MemFileChannel.newChannel();
  161. } else {
  162. dbFile = ((tempDir != null) ?
  163. Files.createTempFile(tempDir, FILE_DB_PREFIX,
  164. format.getFileExtension()) :
  165. Files.createTempFile(FILE_DB_PREFIX,
  166. format.getFileExtension()));
  167. channel = FileChannel.open(dbFile, DatabaseImpl.RW_CHANNEL_OPTS);
  168. }
  169. TempDatabaseImpl.initDbChannel(channel, format);
  170. TempDatabaseImpl db = new TempDatabaseImpl(this, customFile, dbFile,
  171. channel, format, readOnly);
  172. success = true;
  173. return db;
  174. } finally {
  175. if(!success) {
  176. ByteUtil.closeQuietly(channel);
  177. deleteDbFile(dbFile);
  178. closeCustomFile(customFile);
  179. }
  180. }
  181. }
  182. private static void deleteDbFile(Path dbFile) {
  183. if((dbFile != null) &&
  184. dbFile.getFileName().toString().startsWith(FILE_DB_PREFIX)) {
  185. try {
  186. Files.deleteIfExists(dbFile);
  187. } catch(IOException ignores) {}
  188. }
  189. }
  190. private static void closeCustomFile(Object customFile) {
  191. if(customFile instanceof Closeable) {
  192. ByteUtil.closeQuietly((Closeable)customFile);
  193. }
  194. }
  195. /**
  196. * Called by {@link #resolveLinkedDatabase} to determine whether the
  197. * linkeeFileName should be treated as a custom file (thus utiliziing a temp
  198. * db) or a normal access db (loaded via the default behavior). Loads any
  199. * state necessary for subsequently loading data from linkeeFileName.
  200. * <p>
  201. * The returned custom file state object will be maintained with the temp db
  202. * and passed to {@link #loadCustomTable} whenever a new table needs to be
  203. * loaded. Also, if this object is {@link Closeable}, it will be closed
  204. * with the temp db.
  205. *
  206. * @param linkerDb the primary database in which the link is defined
  207. * @param linkeeFileName the name of the linked file
  208. *
  209. * @return non-{@code null} if linkeeFileName should be treated as a custom
  210. * file (using a temp db) or {@code null} if it should be treated as
  211. * a normal access db.
  212. */
  213. protected abstract Object loadCustomFile(
  214. Database linkerDb, String linkeeFileName) throws IOException;
  215. /**
  216. * Called by an instance of a temp db when a missing table is first requested.
  217. *
  218. * @param tempDb the temp db instance which should be populated with the
  219. * relevant table info for the given tableName
  220. * @param customFile custom file state returned from {@link #loadCustomFile}
  221. * @param tableName the name of the table which is requested from the linked
  222. * file
  223. *
  224. * @return {@code true} if the table was available in the linked file,
  225. * {@code false} otherwise
  226. */
  227. protected abstract boolean loadCustomTable(
  228. Database tempDb, Object customFile, String tableName)
  229. throws IOException;
  230. /**
  231. * Subclass of DatabaseImpl which allows us to load tables "on demand" as
  232. * well as delete the temporary db on close.
  233. */
  234. private static class TempDatabaseImpl extends DatabaseImpl
  235. {
  236. private final CustomLinkResolver _resolver;
  237. private final Object _customFile;
  238. protected TempDatabaseImpl(CustomLinkResolver resolver, Object customFile,
  239. Path file, FileChannel channel,
  240. FileFormat fileFormat, boolean readOnly)
  241. throws IOException
  242. {
  243. super(file, channel, true, false, fileFormat, null, null, null,
  244. readOnly, false);
  245. _resolver = resolver;
  246. _customFile = customFile;
  247. }
  248. @Override
  249. protected TableImpl getTable(String name, boolean includeSystemTables)
  250. throws IOException
  251. {
  252. TableImpl table = super.getTable(name, includeSystemTables);
  253. if((table == null) &&
  254. _resolver.loadCustomTable(this, _customFile, name)) {
  255. table = super.getTable(name, includeSystemTables);
  256. }
  257. return table;
  258. }
  259. @Override
  260. public void close() throws IOException {
  261. try {
  262. super.close();
  263. } finally {
  264. deleteDbFile(getPath());
  265. closeCustomFile(_customFile);
  266. }
  267. }
  268. static FileChannel initDbChannel(FileChannel channel, FileFormat format)
  269. throws IOException
  270. {
  271. FileFormatDetails details = getFileFormatDetails(format);
  272. transferDbFrom(channel, getResourceAsStream(details.getEmptyFilePath()));
  273. return channel;
  274. }
  275. }
  276. }