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 implementation 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, see {@link #loadCustomFile}.
47 * Mdb files fall back to the default resolver, which refuses to open them
48 * unless link resolution has been enabled, see
49 * {@link #resolveLinkedDatabase}</li>
50 * <li>Temp db can be an actual file or entirely in memory</li>
51 * <li>Linked tables are loaded on-demand, see {@link #loadCustomTable}</li>
52 * <li>Temp db files will be automatically deleted on close</li>
53 * </ul>
54 *
55 * @author James Ahlborn
56 * @usage _intermediate_class_
57 */
58 public abstract class CustomLinkResolver implements LinkResolver
59 {
60 private static final Random DB_ID = new Random();
61
62 private static final String MEM_DB_PREFIX = "memdb_";
63 private static final String FILE_DB_PREFIX = "linkeddb_";
64
65 /** the default file format used for temp dbs */
66 public static final FileFormat DEFAULT_FORMAT = FileFormat.V2000;
67 /** temp dbs default to the filesystem, not in memory */
68 public static final boolean DEFAULT_IN_MEMORY = false;
69 /** temp dbs end up in the system temp dir by default */
70 public static final Path DEFAULT_TEMP_DIR = null;
71
72 private final FileFormat _defaultFormat;
73 private final boolean _defaultInMemory;
74 private final Path _defaultTempDir;
75
76 /**
77 * Creates a CustomLinkResolver using the default behavior for creating temp
78 * dbs, see {@link #DEFAULT_FORMAT}, {@link #DEFAULT_IN_MEMORY} and
79 * {@link #DEFAULT_TEMP_DIR}.
80 */
81 protected CustomLinkResolver() {
82 this(DEFAULT_FORMAT, DEFAULT_IN_MEMORY, DEFAULT_TEMP_DIR);
83 }
84
85 /**
86 * Creates a CustomLinkResolver with the given default behavior for creating
87 * temp dbs.
88 *
89 * @param defaultFormat the default format for the temp db
90 * @param defaultInMemory whether or not the temp db should be entirely in
91 * memory by default (while this will be faster, it
92 * should only be used if table data is expected to
93 * fit entirely in memory)
94 * @param defaultTempDir the default temp dir for a file based temp db
95 * ({@code null} for the system defaqult temp
96 * directory)
97 */
98 protected CustomLinkResolver(FileFormat defaultFormat, boolean defaultInMemory,
99 Path defaultTempDir)
100 {
101 _defaultFormat = defaultFormat;
102 _defaultInMemory = defaultInMemory;
103 _defaultTempDir = defaultTempDir;
104 }
105
106 protected FileFormat getDefaultFormat() {
107 return _defaultFormat;
108 }
109
110 protected boolean isDefaultInMemory() {
111 return _defaultInMemory;
112 }
113
114 protected Path getDefaultTempDirectory() {
115 return _defaultTempDir;
116 }
117
118 /**
119 * Custom implementation is:
120 * <pre>
121 * // attempt to load the linkeeFileName as a custom file
122 * Object customFile = loadCustomFile(linkerDb, linkeeFileName);
123 *
124 * if(customFile != null) {
125 * // this is a custom file, create and return relevant temp db
126 * return createTempDb(customFile, getDefaultFormat(), isDefaultInMemory(),
127 * getDefaultTempDirectory());
128 * }
129 *
130 * // not a custom file, load using the default behavior
131 * return DatabaseImpl.getDefaultLinkResolver()
132 * .resolveLinkedDatabase(linkerDb, linkeeFileName);
133 * </pre>
134 * The fallback is the resolver a Database uses when none has been
135 * configured, so it refuses to open the linked database. Override this
136 * method and delegate to {@link LinkResolver#UNRESTRICTED} only when the
137 * linked database file names can be trusted.
138 *
139 * @see #loadCustomFile
140 * @see #createTempDb
141 * @see LinkResolver#DEFAULT
142 * @see LinkResolver#UNRESTRICTED
143 */
144 @Override
145 public Databasem/healthmarketscience/jackcess/Database.html#Database">Database resolveLinkedDatabase(Database linkerDb, String linkeeFileName)
146 throws IOException
147 {
148 Object customFile = loadCustomFile(linkerDb, linkeeFileName);
149 if(customFile != null) {
150 // if linker is read-only, open linkee read-only
151 boolean readOnly = ((linkerDb instanceof DatabaseImpl) &&
152 ((DatabaseImpl)linkerDb).isReadOnly());
153 return createTempDb(customFile, getDefaultFormat(), isDefaultInMemory(),
154 getDefaultTempDirectory(), readOnly);
155 }
156 return DatabaseImpl.getDefaultLinkResolver()
157 .resolveLinkedDatabase(linkerDb, linkeeFileName);
158 }
159
160 /**
161 * Creates a temporary database for holding the table data from
162 * linkeeFileName.
163 *
164 * @param customFile custom file state returned from {@link #loadCustomFile}
165 * @param format the access format for the temp db
166 * @param inMemory whether or not the temp db should be entirely in memory
167 * (while this will be faster, it should only be used if
168 * table data is expected to fit entirely in memory)
169 * @param tempDir the temp dir for a file based temp db ({@code null} for
170 * the system default temp directory)
171 *
172 * @return the temp db for holding the linked table info
173 */
174 protected Database createTempDb(Object customFile, FileFormat format,
175 boolean inMemory, Path tempDir,
176 boolean readOnly)
177 throws IOException
178 {
179 Path dbFile = null;
180 FileChannel channel = null;
181 boolean success = false;
182 try {
183
184 if(inMemory) {
185 dbFile = Paths.get(MEM_DB_PREFIX + DB_ID.nextLong() +
186 format.getFileExtension());
187 channel = MemFileChannel.newChannel();
188 } else {
189 dbFile = ((tempDir != null) ?
190 Files.createTempFile(tempDir, FILE_DB_PREFIX,
191 format.getFileExtension()) :
192 Files.createTempFile(FILE_DB_PREFIX,
193 format.getFileExtension()));
194 channel = FileChannel.open(dbFile, MemFileChannel.RW_CHANNEL_OPTS);
195 }
196
197 TempDatabaseImpl.initDbChannel(channel, format);
198 TempDatabaseImpl db = new TempDatabaseImpl(this, customFile, dbFile,
199 channel, format, readOnly);
200 success = true;
201 return db;
202
203 } finally {
204 if(!success) {
205 ByteUtil.closeQuietly(channel);
206 deleteDbFile(dbFile);
207 closeCustomFile(customFile);
208 }
209 }
210 }
211
212 private static void deleteDbFile(Path dbFile) {
213 Path namePath = ((dbFile != null) ? dbFile.getFileName() : null);
214 if((namePath != null) &&
215 namePath.toString().startsWith(FILE_DB_PREFIX)) {
216 try {
217 Files.deleteIfExists(dbFile);
218 } catch(IOException ignores) {}
219 }
220 }
221
222 private static void closeCustomFile(Object customFile) {
223 if(customFile instanceof Closeable) {
224 ByteUtil.closeQuietly((Closeable)customFile);
225 }
226 }
227
228 /**
229 * Called by {@link #resolveLinkedDatabase} to determine whether the
230 * linkeeFileName should be treated as a custom file (thus utiliziing a temp
231 * db) or a normal access db (loaded via the default behavior). Loads any
232 * state necessary for subsequently loading data from linkeeFileName.
233 * <p>
234 * The returned custom file state object will be maintained with the temp db
235 * and passed to {@link #loadCustomTable} whenever a new table needs to be
236 * loaded. Also, if this object is {@link Closeable}, it will be closed
237 * with the temp db.
238 *
239 * @param linkerDb the primary database in which the link is defined
240 * @param linkeeFileName the name of the linked file
241 *
242 * @return non-{@code null} if linkeeFileName should be treated as a custom
243 * file (using a temp db) or {@code null} if it should be treated as
244 * a normal access db.
245 */
246 protected abstract Object loadCustomFile(
247 Database linkerDb, String linkeeFileName) throws IOException;
248
249 /**
250 * Called by an instance of a temp db when a missing table is first requested.
251 *
252 * @param tempDb the temp db instance which should be populated with the
253 * relevant table info for the given tableName
254 * @param customFile custom file state returned from {@link #loadCustomFile}
255 * @param tableName the name of the table which is requested from the linked
256 * file
257 *
258 * @return {@code true} if the table was available in the linked file,
259 * {@code false} otherwise
260 */
261 protected abstract boolean loadCustomTable(
262 Database tempDb, Object customFile, String tableName)
263 throws IOException;
264
265
266 /**
267 * Subclass of DatabaseImpl which allows us to load tables "on demand" as
268 * well as delete the temporary db on close.
269 */
270 private static class TempDatabaseImpl extends DatabaseImpl
271 {
272 private final CustomLinkResolver _resolver;
273 private final Object _customFile;
274
275 protected TempDatabaseImpl(CustomLinkResolver resolver, Object customFile,
276 Path file, FileChannel channel,
277 FileFormat fileFormat, boolean readOnly)
278 throws IOException
279 {
280 super(file, channel, true, false, fileFormat, null, null, null,
281 readOnly, false);
282 _resolver = resolver;
283 _customFile = customFile;
284 }
285
286 @Override
287 protected TableImpl getTable(String name, boolean includeSystemTables)
288 throws IOException
289 {
290 TableImpl table = super.getTable(name, includeSystemTables);
291 if((table == null) &&
292 _resolver.loadCustomTable(this, _customFile, name)) {
293 table = super.getTable(name, includeSystemTables);
294 }
295 return table;
296 }
297
298 @Override
299 public void close() throws IOException {
300 try {
301 super.close();
302 } finally {
303 deleteDbFile(getPath());
304 closeCustomFile(_customFile);
305 }
306 }
307
308 static FileChannel initDbChannel(FileChannel channel, FileFormat format)
309 throws IOException
310 {
311 FileFormatDetails details = getFileFormatDetails(format);
312 transferDbFrom(channel, getResourceAsStream(details.getEmptyFilePath()));
313 return channel;
314 }
315 }
316
317 }