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