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