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 java.io.File; 8 import java.net.URISyntaxException; 9 import java.util.*; 10 import java.util.Map.Entry; 11 import javax.annotation.CheckForNull; 12 import javax.annotation.concurrent.Immutable; 13 import javax.inject.Provider; 14 import net.java.truecommons.services.Loader; 15 import net.java.truecommons.shed.ExtensionSet; 16 import net.java.truecommons.shed.HashMaps; 17 import static net.java.truecommons.shed.HashMaps.initialCapacity; 18 import net.java.truevfs.kernel.spec.FsAbstractCompositeDriver; 19 import net.java.truevfs.kernel.spec.FsDriver; 20 import net.java.truevfs.kernel.spec.FsScheme; 21 import net.java.truevfs.kernel.spec.sl.FsDriverMapLocator; 22 23 /** 24 * Detects a <em>prospective</em> archive file and declares its file system 25 * scheme by mapping its file name extension to an archive driver. 26 * Note that this class does <em>not</em> access any file system! 27 * <p> 28 * The map of detectable archive file name extensions and corresponding archive 29 * drivers is configured by the constructors of this class. 30 * There are two types of constructors available: 31 * <ol> 32 * <li>Constructors which filter the driver map of a given file system driver 33 * provider by a given list of file name extensions. 34 * For example, the driver map of the provider 35 * {@link FsDriverMapLocator#SINGLETON} could be filtered by the file name 36 * extension list {@code "tar|zip"} in order to recognize only TAR and ZIP 37 * files. 38 * <li>Constructors which decorate a given file system driver provider with a 39 * given map of file system schemes to file system drivers. 40 * This can get used to specify custom archive file name extensions or 41 * archive drivers. 42 * For example, the file name extension list {@code "foo|bar"} could be used 43 * to detect a custom variant of the JAR file format (you need to provide 44 * a custom archive driver then, too). 45 * </ol> 46 * <p> 47 * Where a constructor expects a list of file name extensions as a parameter, 48 * it must obeye the syntax constraints for {@link ExtensionSet}s. 49 * As an example, the parameter {@code "zip|jar"} would cause 50 * the archive detector to recognize ZIP and JAR files in a path. 51 * The same would be true for {@code "||.ZiP||.JaR||ZIP||JAR||"}, 52 * but this notation is discouraged because it's not in canonical form. 53 * 54 * @author Christian Schlichtherle 55 */ 56 @Immutable 57 public final class TArchiveDetector extends FsAbstractCompositeDriver { 58 59 /** 60 * This instance never recognizes any archive files in a path. 61 * This can get used as the end of a chain of 62 * {@code TArchiveDetector} instances or if archive files 63 * shall be treated like regular files rather than (virtual) directories. 64 */ 65 public static final TArchiveDetector NULL = new TArchiveDetector(""); 66 67 /** 68 * This instance recognizes all archive file name extensions for which an 69 * archive driver can get located on the class path by the file system 70 * driver map locator singleton {@link FsDriverMapLocator#SINGLETON}. 71 */ 72 public static final TArchiveDetector ALL = new TArchiveDetector(null); 73 74 @SuppressWarnings("AccessingNonPublicFieldOfAnotherObject") 75 private static ExtensionSet extensions( 76 final Provider<Map<FsScheme, FsDriver>> provider) { 77 if (provider instanceof TArchiveDetector) 78 return new ExtensionSet(((TArchiveDetector) provider).extensions); 79 final Map<FsScheme, FsDriver> map = provider.get(); 80 final ExtensionSet set = new ExtensionSet(); 81 for (final Entry<FsScheme, FsDriver> entry : map.entrySet()) 82 if (entry.getValue().isArchiveDriver()) 83 set.add(entry.getKey().toString()); 84 return set; 85 } 86 87 private static Map<FsScheme, FsDriver> map(final Object[][] config) { 88 final Map<FsScheme, FsDriver> drivers = new HashMap<>( 89 HashMaps.initialCapacity(config.length) * 2); // heuristics 90 for (final Object[] param : config) { 91 final Collection<FsScheme> schemes = schemes(param[0]); 92 if (schemes.isEmpty()) 93 throw new IllegalArgumentException("No file system schemes!"); 94 final FsDriver driver = Loader.promote(param[1], FsDriver.class); 95 for (final FsScheme scheme : schemes) drivers.put(scheme, driver); 96 } 97 return Collections.unmodifiableMap(drivers); 98 } 99 100 private static Collection<FsScheme> schemes(final Object o) { 101 final Collection<FsScheme> set = new TreeSet<>(); 102 try { 103 if (o instanceof Collection<?>) 104 for (final Object p : (Collection<?>) o) 105 if (p instanceof FsScheme) set.add((FsScheme) p); 106 else for (final String q : new ExtensionSet(p.toString())) 107 set.add(new FsScheme(q)); 108 else if (o instanceof FsScheme) set.add((FsScheme) o); 109 else for (final String p : new ExtensionSet(o.toString())) 110 set.add(new FsScheme(p)); 111 } catch (final URISyntaxException ex) { 112 throw new IllegalArgumentException(ex); 113 } 114 return set; 115 } 116 117 /** 118 * The set of extensions recognized by this archive detector. 119 * This set is used to filter the registered archive file extensions in 120 * {@link #drivers}. 121 */ 122 private final ExtensionSet extensions; 123 124 private final Map<FsScheme, FsDriver> drivers; 125 126 /** 127 * Equivalent to 128 * {@link #TArchiveDetector(Provider, String) 129 * TArchiveDetector(FsDriverMapLocator.SINGLETON, extensions)}. 130 */ 131 public TArchiveDetector(@CheckForNull String extensions) { 132 this(FsDriverMapLocator.SINGLETON, extensions); 133 } 134 135 /** 136 * Constructs a new {@code TArchiveDetector} by filtering the given driver 137 * provider for all canonicalized extensions in the {@code extensions} list. 138 * 139 * @param provider the file system driver provider to filter. 140 * @param extensions A list of file name extensions which shall identify 141 * prospective archive files. 142 * If this is {@code null}, no filtering is applied and all drivers 143 * known by the given provider are available for use with this 144 * archive detector. 145 * @throws IllegalArgumentException If any of the extensions in the list 146 * names a extension for which no file system driver is known by the 147 * provider. 148 * @see ExtensionSet Syntax constraints for extension lists. 149 */ 150 public TArchiveDetector(final Provider<Map<FsScheme, FsDriver>> provider, 151 final @CheckForNull String extensions) { 152 final ExtensionSet available = extensions(provider); 153 ExtensionSet accepted; 154 if (null == extensions) { 155 accepted = available; 156 } else { 157 accepted = new ExtensionSet(extensions); 158 if (accepted.retainAll(available)) { 159 accepted = new ExtensionSet(extensions); 160 accepted.removeAll(available); 161 assert !accepted.isEmpty(); 162 throw new IllegalArgumentException( 163 "\"" + accepted + "\" (no archive driver installed for these extensions)"); 164 } 165 } 166 this.extensions = accepted; 167 this.drivers = provider.get(); 168 } 169 170 /** 171 * Equivalent to 172 * {@link #TArchiveDetector(Provider, String, FsDriver) 173 * TArchiveDetector(TArchiveDetector.NULL, extensions, driver)}. 174 */ 175 public TArchiveDetector(String extensions, @CheckForNull FsDriver driver) { 176 this(NULL, extensions, driver); 177 } 178 179 /** 180 * Constructs a new {@code TArchiveDetector} by 181 * decorating the configuration of {@code provider} with 182 * mappings for all canonicalized extensions in {@code extensions} to 183 * {@code driver}. 184 * 185 * @param provider the file system driver provider to decorate. 186 * @param extensions A list of file name extensions which shall identify 187 * prospective archive files. 188 * This must not be {@code null} and must not be empty. 189 * @param driver the file system driver to map for the extension list. 190 * {@code null} may be used to <i>shadow</i> a mapping for an equal 191 * file system scheme in {@code provider} by removing it from the 192 * resulting map for this detector. 193 * @throws NullPointerException if a required configuration element is 194 * {@code null}. 195 * @throws IllegalArgumentException if any other parameter precondition 196 * does not hold. 197 * @see ExtensionSet Syntax contraints for extension lists. 198 */ 199 public TArchiveDetector(Provider<Map<FsScheme, FsDriver>> provider, 200 String extensions, 201 @CheckForNull FsDriver driver) { 202 this(provider, new Object[][] {{ extensions, driver }}); 203 } 204 205 /** 206 * Creates a new {@code TArchiveDetector} by 207 * decorating the configuration of {@code provider} with 208 * mappings for all entries in {@code config}. 209 * 210 * @param provider the file system driver provider to decorate. 211 * @param config an array of key-value pair arrays. 212 * The first element of each inner array must either be a 213 * {@link FsScheme file system scheme}, an object {@code o} which 214 * can get converted to a set of file name extensions by calling 215 * {@link ExtensionSet#ExtensionSet(String) new ExtensionSet(o.toString())} 216 * or a {@link Collection collection} of these. 217 * The second element of each inner array must either be a 218 * {@link FsDriver file system driver object}, a 219 * {@link Class file system driver class}, a 220 * {@link String fully qualified name of a file system driver class}, 221 * or {@code null}. 222 * {@code null} may be used to <i>shadow</i> a mapping for an equal 223 * file system scheme in {@code provider} by removing it from the 224 * resulting map for this detector. 225 * @throws NullPointerException if a required configuration element is 226 * {@code null}. 227 * @throws IllegalArgumentException if any other parameter precondition 228 * does not hold. 229 * @see ExtensionSet Syntax contraints for extension lists. 230 */ 231 public TArchiveDetector(Provider<Map<FsScheme, FsDriver>> provider, Object[][] config) { 232 this(provider, map(config)); 233 } 234 235 /** 236 * Constructs a new {@code TArchiveDetector} by decorating the given driver 237 * provider with mappings for all entries in {@code config}. 238 * 239 * @param provider the file system driver provider to decorate. 240 * @param config a map of file system schemes to file system drivers. 241 * {@code null} may be used to <i>shadow</i> a mapping for an equal 242 * file system scheme in {@code provider} by removing it from the 243 * resulting map for this detector. 244 * @throws NullPointerException if a required configuration element is 245 * {@code null}. 246 * @throws ClassCastException if a configuration element is of the wrong 247 * type. 248 * @throws IllegalArgumentException if any other parameter precondition 249 * does not hold. 250 * @see ExtensionSet Syntax contraints for extension lists. 251 */ 252 public TArchiveDetector(final Provider<Map<FsScheme, FsDriver>> provider, 253 final Map<FsScheme, FsDriver> config) { 254 final ExtensionSet extensions = extensions(provider); 255 final Map<FsScheme, FsDriver> available = provider.get(); 256 final Map<FsScheme, FsDriver> drivers = new HashMap<>( 257 initialCapacity(available.size() + config.size())); 258 drivers.putAll(available); 259 for (final Map.Entry<FsScheme, FsDriver> entry : config.entrySet()) { 260 final FsScheme scheme = entry.getKey(); 261 final FsDriver driver = entry.getValue(); 262 if (null != driver) { 263 extensions.add(scheme.toString()); 264 drivers.put(scheme, driver); 265 } else { 266 extensions.remove(scheme.toString()); 267 //drivers.remove(scheme); // keep the driver! 268 } 269 } 270 this.extensions = extensions; 271 this.drivers = Collections.unmodifiableMap(drivers); 272 } 273 274 /** 275 * Returns the <i>canonical extension list</i> for all archive file system 276 * schemes recognized by this {@code TArchiveDetector}. 277 * 278 * @return Either {@code ""} to indicate an empty set or 279 * a string of the form {@code "extension[|extension]*"}, 280 * where {@code extension} is a combination of lower case 281 * letters which does <em>not</em> start with a dot. 282 * The string never contains empty or duplicated extensions and the 283 * extensions are sorted in natural order. 284 * @see #TArchiveDetector(String) 285 * @see ExtensionSet Syntax constraints for extension lists. 286 */ 287 public String getExtensions() { return extensions.toString(); } 288 289 /** 290 * Returns the immutable map of file system drivers. 291 * This is equivalent to {@link #get()}. 292 * 293 * @return the immutable map of file system drivers. 294 */ 295 @SuppressWarnings("ReturnOfCollectionOrArrayField") 296 public Map<FsScheme, FsDriver> getDrivers() { return drivers; } 297 298 @Override 299 @SuppressWarnings("ReturnOfCollectionOrArrayField") 300 public Map<FsScheme, FsDriver> get() { return drivers; } 301 302 /** 303 * Detects whether the given {@code path} name identifies a prospective 304 * archive file by matching its file name extension against the set of 305 * file system schemes in the file system driver map. 306 * If a match is found, the file name extension gets converted to a file 307 * system scheme and returned. 308 * Otherwise, {@code null} is returned. 309 * 310 * @param path the path name. 311 * @return A file system scheme to declare the file system type of the 312 * prospective archive file or {@code null} if no archive file name 313 * extension has been detected. 314 */ 315 public @CheckForNull FsScheme scheme(String path) { 316 // An archive file name extension may contain a dot (e.g. "tar.gz"), so 317 // we can't just look for the last dot in the file name and look up the 318 // remainder in the key set of the archive driver map. 319 // Likewise, a file name may contain additional dots, so we can't just 320 // look for the first dot in it and look up the remainder ... 321 path = path.replace('/', File.separatorChar); 322 int i = path.lastIndexOf(File.separatorChar) + 1; 323 path = path.substring(i); 324 final int l = path.length(); 325 for (i = 0; 0 < (i = path.indexOf('.', i) + 1) && i < l ;) { 326 final String scheme = path.substring(i); 327 if (extensions.contains(scheme)) { 328 try { 329 return new FsScheme(scheme); // TODO: http://java.net/jira/browse/TRUEZIP-132 330 } catch (URISyntaxException noSchemeNoArchiveBadLuck) { 331 } 332 } 333 } 334 return null; 335 } 336 337 @Override 338 @SuppressWarnings("AccessingNonPublicFieldOfAnotherObject") 339 public boolean equals(Object other) { 340 if (this == other) return true; 341 if (!(other instanceof TArchiveDetector)) return false; 342 final TArchiveDetector that = (TArchiveDetector) other; 343 return this.extensions.equals(that.extensions) 344 && this.drivers.equals(that.drivers); 345 } 346 347 @Override 348 public int hashCode() { 349 int hash = 3; 350 hash = 59 * hash + extensions.hashCode(); 351 hash = 59 * hash + drivers.hashCode(); 352 return hash; 353 } 354 355 @Override 356 public String toString() { 357 return String.format("%s[extensions=%s, drivers=%s]", 358 getClass().getName(), 359 extensions, 360 drivers); 361 } 362 }