View Javadoc
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 }