View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.vfs2.impl;
18  
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.net.MalformedURLException;
22  import java.net.URL;
23  import java.util.ArrayList;
24  import java.util.Enumeration;
25  import java.util.Objects;
26  
27  import javax.xml.parsers.DocumentBuilder;
28  import javax.xml.parsers.DocumentBuilderFactory;
29  import javax.xml.parsers.ParserConfigurationException;
30  
31  import org.apache.commons.io.IOUtils;
32  import org.apache.commons.lang3.ArrayUtils;
33  import org.apache.commons.lang3.StringUtils;
34  import org.apache.commons.vfs2.FileSystemException;
35  import org.apache.commons.vfs2.VfsLog;
36  import org.apache.commons.vfs2.operations.FileOperationProvider;
37  import org.apache.commons.vfs2.provider.FileProvider;
38  import org.apache.commons.vfs2.util.Messages;
39  import org.w3c.dom.Element;
40  import org.w3c.dom.NodeList;
41  
42  /**
43   * A {@link org.apache.commons.vfs2.FileSystemManager} that configures itself from an XML (Default: providers.xml)
44   * configuration file.
45   * <p>
46   * Certain providers are only loaded and available if the dependent library is in your classpath. You have to configure
47   * your debugging facility to log "debug" messages to see if a provider was skipped due to "unresolved externals".
48   * </p>
49   */
50  public class StandardFileSystemManager extends DefaultFileSystemManager {
51      private static final String CONFIG_RESOURCE = "providers.xml";
52      private static final String PLUGIN_CONFIG_RESOURCE = "META-INF/vfs-providers.xml";
53  
54      private URL configUri;
55      private ClassLoader classLoader;
56  
57      /**
58       * Constructs a new instance.
59       */
60      public StandardFileSystemManager() {
61          // empty
62      }
63  
64      /**
65       * Adds an extension map.
66       *
67       * @param map containing the Elements.
68       */
69      private void addExtensionMap(final Element map) {
70          final String extension = map.getAttribute("extension");
71          final String scheme = map.getAttribute("scheme");
72          if (!StringUtils.isEmpty(scheme)) {
73              addExtensionMap(extension, scheme);
74          }
75      }
76  
77      /**
78       * Adds a mime-type map.
79       *
80       * @param map containing the Elements.
81       */
82      private void addMimeTypeMap(final Element map) {
83          final String mimeType = map.getAttribute("mime-type");
84          final String scheme = map.getAttribute("scheme");
85          addMimeTypeMap(mimeType, scheme);
86      }
87  
88      /**
89       * Adds a operationProvider from a operationProvider definition.
90       */
91      private void addOperationProvider(final Element providerDef) throws FileSystemException {
92          final String className = providerDef.getAttribute("class-name");
93  
94          // Attach only to available schemas
95          final String[] schemas = getSchemas(providerDef);
96          for (final String schema : schemas) {
97              if (hasProvider(schema)) {
98                  final FileOperationProvider operationProvider = (FileOperationProvider) createInstance(className);
99                  addOperationProvider(schema, operationProvider);
100             }
101         }
102     }
103 
104     /**
105      * Adds a provider from a provider definition.
106      *
107      * @param providerDef the provider definition
108      * @param isDefault true if the default should be used.
109      * @throws FileSystemException if an error occurs.
110      */
111     private void addProvider(final Element providerDef, final boolean isDefault) throws FileSystemException {
112         final String className = providerDef.getAttribute("class-name");
113 
114         // Make sure all required schemes are available
115         final String[] requiredSchemes = getRequiredSchemes(providerDef);
116         for (final String requiredScheme : requiredSchemes) {
117             if (!hasProvider(requiredScheme)) {
118                 final String msg = Messages.getString("vfs.impl/skipping-provider-scheme.debug", className,
119                         requiredScheme);
120                 VfsLog.debug(getLogger(), getLogger(), msg);
121                 return;
122             }
123         }
124 
125         // Make sure all required classes are in classpath
126         final String[] requiredClasses = getRequiredClasses(providerDef);
127         for (final String requiredClass : requiredClasses) {
128             if (!findClass(requiredClass)) {
129                 final String msg = Messages.getString("vfs.impl/skipping-provider.debug", className, requiredClass);
130                 VfsLog.debug(getLogger(), getLogger(), msg);
131                 return;
132             }
133         }
134 
135         // Create and register the provider
136         final FileProvider provider = (FileProvider) createInstance(className);
137         final String[] schemas = getSchemas(providerDef);
138         if (schemas.length > 0) {
139             addProvider(schemas, provider);
140         }
141 
142         // Set as default, if required
143         if (isDefault) {
144             setDefaultProvider(provider);
145         }
146     }
147 
148     /**
149      * Configures this manager from a parsed XML configuration file
150      *
151      * @param config The configuration Element.
152      * @throws FileSystemException if an error occurs.
153      */
154     private void configure(final Element config) throws FileSystemException {
155         // Add the providers
156         final NodeList providers = config.getElementsByTagName("provider");
157         final int count = providers.getLength();
158         for (int i = 0; i < count; i++) {
159             final Element provider = (Element) providers.item(i);
160             addProvider(provider, false);
161         }
162 
163         // Add the operation providers
164         final NodeList operationProviders = config.getElementsByTagName("operationProvider");
165         for (int i = 0; i < operationProviders.getLength(); i++) {
166             final Element operationProvider = (Element) operationProviders.item(i);
167             addOperationProvider(operationProvider);
168         }
169 
170         // Add the default provider
171         final NodeList defProviders = config.getElementsByTagName("default-provider");
172         if (defProviders.getLength() > 0) {
173             final Element provider = (Element) defProviders.item(0);
174             addProvider(provider, true);
175         }
176 
177         // Add the mime-type maps
178         final NodeList mimeTypes = config.getElementsByTagName("mime-type-map");
179         for (int i = 0; i < mimeTypes.getLength(); i++) {
180             final Element map = (Element) mimeTypes.item(i);
181             addMimeTypeMap(map);
182         }
183 
184         // Add the extension maps
185         final NodeList extensions = config.getElementsByTagName("extension-map");
186         for (int i = 0; i < extensions.getLength(); i++) {
187             final Element map = (Element) extensions.item(i);
188             addExtensionMap(map);
189         }
190     }
191 
192     /**
193      * Configures this manager from an XML configuration file.
194      *
195      * @param configUri The URI of the configuration.
196      * @throws FileSystemException if an error occurs.
197      */
198     private void configure(final URL configUri) throws FileSystemException {
199         InputStream configStream = null;
200         try {
201             // Load up the config
202             // TODO - validate
203             final DocumentBuilder builder = createDocumentBuilder();
204             configStream = configUri.openStream();
205             final Element config = builder.parse(configStream).getDocumentElement();
206 
207             configure(config);
208         } catch (final Exception e) {
209             throw new FileSystemException("vfs.impl/load-config.error", configUri.toString(), e);
210         } finally {
211             IOUtils.closeQuietly(configStream, e -> getLogger().warn(e.getLocalizedMessage(), e));
212         }
213     }
214 
215     /**
216      * Scans the classpath to find any dropped plugin.
217      * <p>
218      * The plugin-description has to be in {@code /META-INF/vfs-providers.xml}.
219      * </p>
220      *
221      * @throws FileSystemException if an error occurs.
222      */
223     protected void configurePlugins() throws FileSystemException {
224         final Enumeration<URL> enumResources;
225         try {
226             enumResources = enumerateResources(PLUGIN_CONFIG_RESOURCE);
227         } catch (final IOException e) {
228             throw new FileSystemException(e);
229         }
230 
231         while (enumResources.hasMoreElements()) {
232             configure(enumResources.nextElement());
233         }
234     }
235 
236     /**
237      * Gets a new DefaultFileReplicator.
238      *
239      * @return a new DefaultFileReplicator.
240      */
241     protected DefaultFileReplicator createDefaultFileReplicator() {
242         return new DefaultFileReplicator();
243     }
244 
245     /**
246      * Configure and create a DocumentBuilder
247      *
248      * @return A DocumentBuilder for the configuration.
249      * @throws ParserConfigurationException if an error occurs.
250      */
251     private DocumentBuilder createDocumentBuilder() throws ParserConfigurationException {
252         final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
253         factory.setIgnoringElementContentWhitespace(true);
254         factory.setIgnoringComments(true);
255         factory.setExpandEntityReferences(true);
256         return factory.newDocumentBuilder();
257     }
258 
259     /**
260      * Creates a provider.
261      */
262     private Object createInstance(final String className) throws FileSystemException {
263         try {
264             return loadClass(className).getConstructor().newInstance();
265         } catch (final Exception e) {
266             throw new FileSystemException("vfs.impl/create-provider.error", className, e);
267         }
268     }
269 
270     /**
271      * Enumerates resources from different class loaders.
272      *
273      * @throws IOException if {@code getResource} failed.
274      * @see #findClassLoader()
275      */
276     private Enumeration<URL> enumerateResources(final String name) throws IOException {
277         Enumeration<URL> enumeration = findClassLoader().getResources(name);
278         if (enumeration == null || !enumeration.hasMoreElements()) {
279             enumeration = getValidClassLoader(getClass()).getResources(name);
280         }
281         return enumeration;
282     }
283 
284     /**
285      * Tests if a class is available.
286      */
287     private boolean findClass(final String className) {
288         try {
289             loadClass(className);
290             return true;
291         } catch (final ClassNotFoundException e) {
292             return false;
293         }
294     }
295 
296     /**
297      * Returns a class loader or null since some Java implementation is null for the bootstrap class loader.
298      *
299      * @return A class loader or null since some Java implementation is null for the bootstrap class loader.
300      */
301     private ClassLoader findClassLoader() {
302         if (classLoader != null) {
303             return classLoader;
304         }
305         final ClassLoader cl = Thread.currentThread().getContextClassLoader();
306         if (cl != null) {
307             return cl;
308         }
309         return getValidClassLoader(getClass());
310     }
311 
312     /**
313      * Extracts the required classes from a provider definition.
314      */
315     private String[] getRequiredClasses(final Element providerDef) {
316         final ArrayList<String> classes = new ArrayList<>();
317         final NodeList deps = providerDef.getElementsByTagName("if-available");
318         final int count = deps.getLength();
319         for (int i = 0; i < count; i++) {
320             final Element dep = (Element) deps.item(i);
321             final String className = dep.getAttribute("class-name");
322             if (!StringUtils.isEmpty(className)) {
323                 classes.add(className);
324             }
325         }
326         return classes.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
327     }
328 
329     /**
330      * Extracts the required schemes from a provider definition.
331      */
332     private String[] getRequiredSchemes(final Element providerDef) {
333         final ArrayList<String> schemes = new ArrayList<>();
334         final NodeList deps = providerDef.getElementsByTagName("if-available");
335         final int count = deps.getLength();
336         for (int i = 0; i < count; i++) {
337             final Element dep = (Element) deps.item(i);
338             final String scheme = dep.getAttribute("scheme");
339             if (!StringUtils.isEmpty(scheme)) {
340                 schemes.add(scheme);
341             }
342         }
343         return schemes.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
344     }
345 
346     /**
347      * Extracts the schema names from a provider definition.
348      */
349     private String[] getSchemas(final Element provider) {
350         final ArrayList<String> schemas = new ArrayList<>();
351         final NodeList schemaElements = provider.getElementsByTagName("scheme");
352         final int count = schemaElements.getLength();
353         for (int i = 0; i < count; i++) {
354             final Element scheme = (Element) schemaElements.item(i);
355             schemas.add(scheme.getAttribute("name"));
356         }
357         return schemas.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
358     }
359 
360     private ClassLoader getValidClassLoader(final Class<?> clazz) {
361         return validateClassLoader(clazz.getClassLoader(), clazz);
362     }
363 
364     /**
365      * Initializes this manager. Adds the providers and replicator.
366      *
367      * @throws FileSystemException if an error occurs.
368      */
369     @Override
370     public void init() throws FileSystemException {
371         // Set the replicator and temporary file store (use the same component)
372         final DefaultFileReplicator replicator = createDefaultFileReplicator();
373         setReplicator(new PrivilegedFileReplicator(replicator));
374         setTemporaryFileStore(replicator);
375 
376         if (configUri == null) {
377             // Use default config
378             final URL url = getClass().getResource(CONFIG_RESOURCE);
379             FileSystemException.requireNonNull(url, "vfs.impl/find-config-file.error", CONFIG_RESOURCE);
380             configUri = url;
381         }
382 
383         configure(configUri);
384         configurePlugins();
385 
386         // Initialize super-class
387         super.init();
388     }
389 
390     /**
391      * Load a class from different class loaders.
392      *
393      * @throws ClassNotFoundException if last {@code loadClass} failed.
394      * @see #findClassLoader()
395      */
396     private Class<?> loadClass(final String className) throws ClassNotFoundException {
397         try {
398             return findClassLoader().loadClass(className);
399         } catch (final ClassNotFoundException e) {
400             return getValidClassLoader(getClass()).loadClass(className);
401         }
402     }
403 
404     /**
405      * Sets the ClassLoader to use to load the providers. Default is to use the ClassLoader that loaded this class.
406      *
407      * @param classLoader The ClassLoader.
408      */
409     public void setClassLoader(final ClassLoader classLoader) {
410         this.classLoader = classLoader;
411     }
412 
413     /**
414      * Sets the configuration file for this manager.
415      *
416      * @param configUri The URI for this manager.
417      */
418     public void setConfiguration(final String configUri) {
419         try {
420             setConfiguration(new URL(configUri));
421         } catch (final MalformedURLException e) {
422             getLogger().warn(e.getLocalizedMessage(), e);
423         }
424     }
425 
426     /**
427      * Sets the configuration file for this manager.
428      *
429      * @param configUri The URI for this manager.
430      */
431     public void setConfiguration(final URL configUri) {
432         this.configUri = configUri;
433     }
434 
435     private ClassLoader validateClassLoader(final ClassLoader clazzLoader, final Class<?> clazz) {
436         return Objects.requireNonNull(clazzLoader, "The class loader for " + clazz
437                 + " is null; some Java implementations use null for the bootstrap class loader.");
438     }
439 
440 }