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