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 * @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 * @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 * @After 181 * public void shutDown() { 182 * config.close(); 183 * } 184 * 185 * @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 }