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