View Javadoc
1   /*
2    * Copyright (C) 2005-2015 Schlichtherle IT Services.
3    * All rights reserved. Use is subject to license terms.
4    */
5   package net.java.truevfs.access;
6   
7   import edu.umd.cs.findbugs.annotations.CleanupObligation;
8   import edu.umd.cs.findbugs.annotations.CreatesObligation;
9   import edu.umd.cs.findbugs.annotations.DischargesObligation;
10  import java.util.Objects;
11  import net.java.truecommons.shed.BitField;
12  import net.java.truecommons.shed.InheritableThreadLocalStack;
13  import net.java.truecommons.shed.Resource;
14  import net.java.truevfs.kernel.spec.FsAccessOption;
15  import static net.java.truevfs.kernel.spec.FsAccessOption.*;
16  import net.java.truevfs.kernel.spec.FsAccessOptions;
17  import static net.java.truevfs.kernel.spec.FsAccessOptions.ACCESS_PREFERENCES_MASK;
18  import net.java.truevfs.kernel.spec.FsManager;
19  import net.java.truevfs.kernel.spec.sl.FsManagerLocator;
20  
21  /**
22   * A mutable container for configuration options with global or inheritable
23   * thread local scope.
24   * <p>
25   * At any time, a thread can call {@link #current()} to get access to the
26   * mutable <i>current configuration</i>.
27   * If no configuration has been pushed onto the inheritable thread local
28   * configuration stack before, then this will return the
29   * <i>global configuration</i> which is shared by all threads.
30   * As an implication, accessing the global configuration may not be thread-safe.
31   * <p>
32   * To create an <i>inheritable thread local configuration</i>, a thread may
33   * call {@link #open()}.
34   * This will copy the <i>current configuration</i> (which may be the global
35   * configuration) and push the copy on top of the inheritable thread
36   * local configuration stack.
37   * <p>
38   * Later, the thread can use {@link #close()} to pop this configuration
39   * off the top of the inheritable thread local configuration stack again.
40   * <p>
41   * Finally, whenever a child thread gets started, it will share the
42   * <em>same</em> current configuration with its parent thread.
43   * If the parent's inheritable thread local configuration stack is empty, then
44   * the child will share the global configuration as its current configuration
45   * with its parent.
46   * Note that the child thread cannot {@link #close()} the inherited current
47   * configuration - this would result in an {@link IllegalStateException}.
48   *
49   * <h3><a name="examples">Examples</a></h3>
50   *
51   * <h4><a name="global">Changing The Global Configuration</a></h4>
52   * <p>
53   * If the thread local configuration stack is empty, i.e. no {@link #open()}
54   * without a corresponding {@link #close()} has been called before, then the
55   * {@link #current()} method will return the global configuration.
56   * This feature is intended to get used during application setup to change some
57   * configuration options with global scope like this:
58   * <pre>{@code
59   * class MyApplication extends TApplication<IOException> {
60   *
61   * &#64;Override
62   *     protected void setup() {
63   *         // This should obtain the global configuration.
64   *         TConfig config = TConfig.current();
65   *         // Configure custom application file format.
66   *         config.setArchiveDetector(new TArchiveDetector("aff", new JarDriver()));
67   *         // Set FsAccessOption.GROW for appending-to rather than reassembling
68   *         // existing archive files.
69   *         config.setAccessPreference(FsAccessOption.GROW, true);
70   *     }
71   *
72   *     ...
73   * }
74   * }</pre>
75   *
76   * <h4><a name="local">Setting The Archive Detector For The Current Thread</a></h4>
77   * <p>
78   * If an application needs to change the configuration for just the current
79   * thread rather than changing the global configuration, then the
80   * {@link #open()} method needs to get called like this:
81   * <pre>{@code
82   * TFile file1 = new TFile("file.aff");
83   * assert !file1.isArchive();
84   *
85   * // First, push a new current configuration onto the inheritable thread local
86   * // stack.
87   * try (TConfig config = TConfig.open()) {
88   *     // Configure custom application file format "aff".
89   *     config.setArchiveDetector(new TArchiveDetector("aff", new JarDriver()));
90   *
91   *     // Now use the current configuration.
92   *     TFile file2 = new TFile("file.aff");
93   *     assert file2.isArchive();
94   *     // Do some I/O here.
95   *     ...
96   * }
97   * }</pre>
98   *
99   * <h4><a name="appending">Appending To Archive Files For The Current Thread</a></h4>
100  * <p>
101  * By default, TrueVFS is configured to produce the smallest possible archive
102  * files.
103  * This is achieved by selecting the maximum compression ratio in the archive
104  * drivers and by performing an archive update whenever an existing archive
105  * entry is going to get overwritten with new contents or updated with new meta
106  * data in order to avoid the writing of redundant data to the resulting
107  * archive file.
108  * An archive update is basically a copy operation where all archive entries
109  * which haven't been written yet get copied from the input archive file to the
110  * output archive file.
111  * However, while this strategy produces the smallest possible archive files,
112  * it may yield bad performance if the number and contents of the archive
113  * entries to create or update are pretty small compared to the total size of
114  * the resulting archive file.
115  * <p>
116  * Therefore, you can change this strategy by setting the
117  * {@link FsAccessOption#GROW} output option preference when writing archive
118  * entry contents or updating their meta data.
119  * When set, this output option allows archive files to grow by appending new
120  * or updated archive entries to their end and inhibiting archive update
121  * operations.
122  * You can set this preference in the global configuration as shown above or
123  * you can set it on a case-by-case basis as follows:
124  * <pre>{@code
125  * // We are going to append "entry" to "archive.zip".
126  * TFile file = new TFile("archive.zip/entry");
127  *
128  * // First, push a new current configuration on the inheritable thread local
129  * // stack.
130  * try (TConfig config = TConfig.open()) {
131  *     // Set FsAccessOption.GROW for appending-to rather than reassembling
132  *     // existing archive files.
133  *     config.setAccessPreference(FsAccessOption.GROW, true);
134  *
135  *     // Now use the current configuration and append the entry to the archive
136  *     // file even if it's already present.
137  *     try (TFileOutputStream out = new TFileOutputStream(file)) {
138  *         // Do some output here.
139  *         ...
140  *     }
141  * }
142  * }</pre>
143  * <p>
144  * Note that it's specific to the archive file system driver if this output
145  * option preference is supported or not.
146  * If it's not supported, then it gets silently ignored, thereby falling back
147  * to the default strategy of performing a full archive update whenever
148  * required to avoid writing redundant archive entry data.
149  * <p>
150  * As of TrueVFS 0.11, the support is like this:
151  * <ul>
152  * <li>The drivers of the module TrueVFS Driver JAR fully support this output
153  *     option preference, so it's available for EAR, JAR, WAR files.</li>
154  * <li>The drivers of the module TrueVFS Driver ZIP fully support this output
155  *     option preference, so it's available for ZIP files.</li>
156  * <li>The drivers of the module TrueVFS Driver ZIP.RAES only allow redundant
157  *     archive entry contents and meta data.
158  *     You cannot append to an existing ZIP.RAES file, however.</li>
159  * <li>The drivers of the module TrueVFS Driver TAR only allow redundant
160  *     archive entry contents.
161  *     You cannot append to an existing TAR file, however.</li>
162  * </ul>
163  *
164  * <h4><a name="unit-testing">Unit Testing</a></h4>
165  * <p>
166  * Using the thread local inheritable configuration stack comes in handy when
167  * unit testing, e.g. with JUnit. Consider this pattern:
168  * <pre>{@code
169  * public class AppTest {
170  *
171  *     private TConfig config;
172  *
173  *     &#64;Before
174  *     public void setUp() {
175  *         config = TConfig.open();
176  *         // Let's just recognize ZIP files.
177  *         config.setArchiveDetector(new TArchiveDetector("zip"));
178  *     }
179  *
180  *     &#64;After
181  *     public void shutDown() {
182  *         config.close();
183  *     }
184  *
185  *     &#64;Test
186  *     public void testMethod() {
187  *         // Test accessing some ZIP files here.
188  *         ...
189  *     }
190  * }
191  * }</pre>
192  * <p>
193  * <b>Disclaimer</b>: Although this class internally uses an
194  * {@link InheritableThreadLocal}, it does not leak memory in multi class
195  * loader environments when used appropriately.
196  *
197  * @author Christian Schlichtherle
198  */
199 @CleanupObligation
200 public final class TConfig extends Resource<IllegalStateException> {
201 
202     private static final BitField<FsAccessOption>
203             NOT_ACCESS_PREFERENCES_MASK = ACCESS_PREFERENCES_MASK.not();
204 
205     private static final InheritableThreadLocalStack<TConfig>
206             configs = new InheritableThreadLocalStack<>();
207 
208     static final TConfig GLOBAL = new TConfig();
209 
210     /**
211      * Returns the current configuration.
212      * First, this method peeks the inheritable thread local configuration
213      * stack.
214      * If no configuration has been {@link #open()}ed yet, the global
215      * configuration gets returned.
216      * Note that accessing the global configuration is not thread-safe!
217      *
218      * @return The current configuration.
219      * @see    #open()
220      */
221     public static TConfig current() { return configs.peekOrElse(GLOBAL); }
222 
223     /**
224      * Creates a new current configuration by copying the current configuration
225      * and pushing the copy onto the inheritable thread local configuration
226      * stack.
227      *
228      * @return The new current configuration.
229      * @see    #current()
230      */
231     @CreatesObligation
232     public static TConfig open() { return configs.push(new TConfig(current())); }
233 
234     // I don't think these fields should be volatile.
235     // This would make a difference if and only if two threads were changing
236     // the GLOBAL configuration concurrently, which is discouraged.
237     // Instead, the global configuration should only get changed once at
238     // application startup and then each thread should modify only its thread
239     // local configuration which has been obtained by a call to TConfig.open().
240     private FsManager manager;
241     private TArchiveDetector detector;
242     private BitField<FsAccessOption> preferences;
243 
244     /** Default constructor for the global configuration. */
245     private TConfig() {
246         this.manager = FsManagerLocator.SINGLETON.get();
247         this.detector = TArchiveDetector.ALL;
248         this.preferences = BitField.of(CREATE_PARENTS);
249     }
250 
251     /** Copy constructor for inheritable thread local configurations. */
252     private TConfig(final TConfig template) {
253         this.manager = template.getManager();
254         this.detector = template.getArchiveDetector();
255         this.preferences = template.getAccessPreferences();
256     }
257 
258     private void checkOpen() {
259         if (!isOpen())
260             throw new IllegalStateException("This configuration has already been close()d.");
261     }
262 
263     /**
264      * Returns the file system manager.
265      *
266      * @return The file system manager.
267      */
268     FsManager getManager() {
269         checkOpen();
270         return manager;
271     }
272 
273     /**
274      * Sets the file system manager.
275      * This method is solely provided for testing purposes.
276      * Changing this property will show effect upon the next access to the
277      * virtual file system.
278      *
279      * @param manager The file system manager.
280      */
281     void setManager(final FsManager manager) {
282         checkOpen();
283         this.manager = Objects.requireNonNull(manager);
284     }
285 
286     /**
287      * Returns the {@link TArchiveDetector} to use for scanning path names for
288      * prospective archive files.
289      *
290      * @return The {@link TArchiveDetector} to use for scanning path names for
291      *         prospective archive files.
292      * @see #getArchiveDetector
293      */
294     public TArchiveDetector getArchiveDetector() {
295         checkOpen();
296         return detector;
297     }
298 
299     /**
300      * Sets the default {@link TArchiveDetector} to use for scanning path
301      * names for prospective archive files.
302      * Changing this property will show effect when a new {@link TFile} or
303      * {@link TPath} gets created.
304      *
305      * @param detector the default {@link TArchiveDetector} to use for
306      *        scanning path names for prospective archive files.
307      * @see   #getArchiveDetector()
308      */
309     public void setArchiveDetector(final TArchiveDetector detector) {
310         checkOpen();
311         this.detector = Objects.requireNonNull(detector);
312     }
313 
314     /**
315      * Returns the access preferences to apply for file system operations.
316      *
317      * @return The access preferences to apply for file system operations.
318      */
319     public BitField<FsAccessOption> getAccessPreferences() {
320         checkOpen();
321         return preferences;
322     }
323 
324     /**
325      * Sets the access preferences to apply for file system operations.
326      * Changing this property will show effect upon the next access to the
327      * virtual file system.
328      *
329      * @param  preferences the access preferences.
330      * @throws IllegalArgumentException if an option is present in
331      *         {@code accessPreferences} which is not present in
332      *         {@link FsAccessOptions#ACCESS_PREFERENCES_MASK} or if both
333      *         {@link FsAccessOption#STORE} and
334      *         {@link FsAccessOption#COMPRESS} have been set.
335      */
336     public void setAccessPreferences(final BitField<FsAccessOption> preferences) {
337         checkOpen();
338         if (preferences.equals(this.preferences)) return;
339         final BitField<FsAccessOption>
340                 illegal = preferences.and(NOT_ACCESS_PREFERENCES_MASK);
341         if (!illegal.isEmpty())
342             throw new IllegalArgumentException(illegal + " (illegal access preference(s))");
343         if (preferences.get(STORE) && preferences.get(COMPRESS))
344             throw new IllegalArgumentException(preferences + " (either STORE or COMPRESS may be set, but not both)");
345         this.preferences = preferences;
346     }
347 
348     /**
349      * Returns {@code true} if and only if the given access option is set in
350      * the access preferences.
351      *
352      * @param  option the access option to test.
353      * @return {@code true} if and only if the given access option is set in
354      *         the access preferences.
355      */
356     public boolean getAccessPreference(FsAccessOption option) {
357         return getAccessPreferences().get(option);
358     }
359 
360     /**
361      * Sets or clears the given access option in the access preferences.
362      * Changing this property will show effect upon the next access to the
363      * virtual file system.
364      *
365      * @param option the access option to set or clear.
366      * @param set {@code true} if you want the option to be set or
367      *        {@code false} if you want it to be cleared.
368      */
369     public void setAccessPreference(FsAccessOption option, boolean set) {
370         setAccessPreferences(getAccessPreferences().set(option, set));
371     }
372 
373     /**
374      * Returns the value of the property {@code lenient}, which is {@code true}
375      * if and only if the access preference {@link FsAccessOption#CREATE_PARENTS}
376      * is set in the {@linkplain #getAccessPreferences() access preferences}.
377      * This property controls whether archive files and their member
378      * directories get automatically created whenever required.
379      * <p>
380      * Consider the following path: {@code a/outer.zip/b/inner.zip/c}.
381      * Now let's assume that {@code a} exists as a plain directory in the
382      * platform file system, while all other segments of this path don't, and
383      * that the module TrueVFS Driver ZIP is present on the run-time class path
384      * in order to enable the detection of {@code outer.zip} and
385      * {@code inner.zip} as prospective ZIP files.
386      * <p>
387      * Now, if this property is set to {@code false}, then a client application
388      * needs to call {@code new TFile("a/outer.zip/b/inner.zip").mkdirs()}
389      * before it can actually create the innermost entry {@code c} as a file
390      * or directory.
391      * More formally, before an application can access an entry in an archive
392      * file system, all its parent directories need to exist, including archive
393      * files.
394      * <p>
395      * This emulates the behaviour of the platform file system.
396      * <p>
397      * If this property is set to {@code true} however, then any missing
398      * parent directories (including archive files) up to the outermost archive
399      * file {@code outer.zip} get automatically created when using operations
400      * to create the innermost entry {@code c}.
401      * This enables applications to succeed with doing this:
402      * {@code new TFile("a/outer.zip/b/inner.zip/c").createNewFile()},
403      * or that:
404      * {@code new TFileOutputStream("a/outer.zip/b/inner.zip/c")}.
405      * <p>
406      * A most desirable side effect of <i>being lenient</i> is that it will
407      * safe space in the target archive file.
408      * This is because the directory entry {@code b} in this exaple does not
409      * need to get output because there is no meta data associated with it.
410      * This is called a <em>ghost directory</em>.
411      * <p>
412      * Note that in either case the parent directory of the outermost archive
413      * file {@code a} must exist - TrueVFS does not automatically create
414      * directories in the platform file system!
415      *
416      * @return The value of the property {@code lenient}, which is {@code true}
417      *         if and only if the access preference
418      *         {@link FsAccessOption#CREATE_PARENTS} is set in the
419      *         {@linkplain #getAccessPreferences() access accessPreferences}.
420      */
421     public boolean isLenient() { return getAccessPreference(CREATE_PARENTS); }
422 
423     /**
424      * Sets the value of the property {@code lenient}.
425      * Changing this property will show effect upon the next access to the
426      * virtual file system.
427      *
428      * @param lenient the value of the property {@code lenient}.
429      */
430     public void setLenient(final boolean lenient) {
431         setAccessPreference(CREATE_PARENTS, lenient);
432     }
433 
434     @Override
435     @DischargesObligation
436     public void close() throws IllegalStateException { super.close(); }
437 
438     /**
439      * Pops this configuration off the inheritable thread local configuration
440      * stack.
441      *
442      * @throws IllegalStateException If this configuration is not the
443      *         {@linkplain #current() current configuration}.
444      */
445     @Override protected void onBeforeClose() throws IllegalStateException {
446         configs.popIf(this);
447     }
448 
449     @Override
450     public boolean equals(Object other) {
451         if (this == other) return true;
452         if (!(other instanceof TConfig)) return false;
453         final TConfig that = (TConfig) other;
454         return this.manager.equals(that.getManager())
455                 && this.detector.equals(that.getArchiveDetector())
456                 && this.preferences.equals(that.getAccessPreferences());
457     }
458 
459     @Override
460     public int hashCode() {
461         int hash = 7;
462         hash = 89 * hash + manager.hashCode();
463         hash = 89 * hash + detector.hashCode();
464         hash = 89 * hash + preferences.hashCode();
465         return hash;
466     }
467 
468     @Override
469     public String toString() {
470         return String.format("%s[manager=%s, detector=%s, preferences=%s]",
471                 getClass().getName(), manager, detector, preferences);
472     }
473 }