001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.vfs2.impl;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.net.MalformedURLException;
022import java.net.URL;
023import java.util.ArrayList;
024import java.util.Enumeration;
025import java.util.Objects;
026
027import javax.xml.parsers.DocumentBuilder;
028import javax.xml.parsers.DocumentBuilderFactory;
029import javax.xml.parsers.ParserConfigurationException;
030
031import org.apache.commons.lang3.ArrayUtils;
032import org.apache.commons.lang3.StringUtils;
033import org.apache.commons.vfs2.FileSystemException;
034import org.apache.commons.vfs2.VfsLog;
035import org.apache.commons.vfs2.operations.FileOperationProvider;
036import org.apache.commons.vfs2.provider.FileProvider;
037import org.apache.commons.vfs2.util.Messages;
038import org.w3c.dom.Element;
039import org.w3c.dom.NodeList;
040
041/**
042 * A {@link org.apache.commons.vfs2.FileSystemManager} that configures itself from an XML (Default: providers.xml)
043 * configuration file.
044 * <p>
045 * Certain providers are only loaded and available if the dependent library is in your classpath. You have to configure
046 * your debugging facility to log "debug" messages to see if a provider was skipped due to "unresolved externals".
047 * </p>
048 */
049public class StandardFileSystemManager extends DefaultFileSystemManager {
050    private static final String CONFIG_RESOURCE = "providers.xml";
051    private static final String PLUGIN_CONFIG_RESOURCE = "META-INF/vfs-providers.xml";
052
053    private URL configUri;
054    private ClassLoader classLoader;
055
056    /**
057     * Constructs a new instance.
058     */
059    public StandardFileSystemManager() {
060        // empty
061    }
062
063    /**
064     * Adds an extension map.
065     *
066     * @param map containing the Elements.
067     */
068    private void addExtensionMap(final Element map) {
069        final String extension = map.getAttribute("extension");
070        final String scheme = map.getAttribute("scheme");
071        if (!StringUtils.isEmpty(scheme)) {
072            addExtensionMap(extension, scheme);
073        }
074    }
075
076    /**
077     * Adds a mime-type map.
078     *
079     * @param map containing the Elements.
080     */
081    private void addMimeTypeMap(final Element map) {
082        final String mimeType = map.getAttribute("mime-type");
083        final String scheme = map.getAttribute("scheme");
084        addMimeTypeMap(mimeType, scheme);
085    }
086
087    /**
088     * Adds a operationProvider from a operationProvider definition.
089     */
090    private void addOperationProvider(final Element providerDef) throws FileSystemException {
091        final String className = providerDef.getAttribute("class-name");
092
093        // Attach only to available schemas
094        final String[] schemas = getSchemas(providerDef);
095        for (final String schema : schemas) {
096            if (hasProvider(schema)) {
097                final FileOperationProvider operationProvider = (FileOperationProvider) createInstance(className);
098                addOperationProvider(schema, operationProvider);
099            }
100        }
101    }
102
103    /**
104     * Adds a provider from a provider definition.
105     *
106     * @param providerDef the provider definition
107     * @param isDefault true if the default should be used.
108     * @throws FileSystemException if an error occurs.
109     */
110    private void addProvider(final Element providerDef, final boolean isDefault) throws FileSystemException {
111        final String className = providerDef.getAttribute("class-name");
112
113        // Make sure all required schemes are available
114        final String[] requiredSchemes = getRequiredSchemes(providerDef);
115        for (final String requiredScheme : requiredSchemes) {
116            if (!hasProvider(requiredScheme)) {
117                final String msg = Messages.getString("vfs.impl/skipping-provider-scheme.debug", className,
118                        requiredScheme);
119                VfsLog.debug(getLogger(), getLogger(), msg);
120                return;
121            }
122        }
123
124        // Make sure all required classes are in classpath
125        final String[] requiredClasses = getRequiredClasses(providerDef);
126        for (final String requiredClass : requiredClasses) {
127            if (!findClass(requiredClass)) {
128                final String msg = Messages.getString("vfs.impl/skipping-provider.debug", className, requiredClass);
129                VfsLog.debug(getLogger(), getLogger(), msg);
130                return;
131            }
132        }
133
134        // Create and register the provider
135        final FileProvider provider = (FileProvider) createInstance(className);
136        final String[] schemas = getSchemas(providerDef);
137        if (schemas.length > 0) {
138            addProvider(schemas, provider);
139        }
140
141        // Set as default, if required
142        if (isDefault) {
143            setDefaultProvider(provider);
144        }
145    }
146
147    /**
148     * Configures this manager from a parsed XML configuration file
149     *
150     * @param config The configuration Element.
151     * @throws FileSystemException if an error occurs.
152     */
153    private void configure(final Element config) throws FileSystemException {
154        // Add the providers
155        final NodeList providers = config.getElementsByTagName("provider");
156        final int count = providers.getLength();
157        for (int i = 0; i < count; i++) {
158            final Element provider = (Element) providers.item(i);
159            addProvider(provider, false);
160        }
161
162        // Add the operation providers
163        final NodeList operationProviders = config.getElementsByTagName("operationProvider");
164        for (int i = 0; i < operationProviders.getLength(); i++) {
165            final Element operationProvider = (Element) operationProviders.item(i);
166            addOperationProvider(operationProvider);
167        }
168
169        // Add the default provider
170        final NodeList defProviders = config.getElementsByTagName("default-provider");
171        if (defProviders.getLength() > 0) {
172            final Element provider = (Element) defProviders.item(0);
173            addProvider(provider, true);
174        }
175
176        // Add the mime-type maps
177        final NodeList mimeTypes = config.getElementsByTagName("mime-type-map");
178        for (int i = 0; i < mimeTypes.getLength(); i++) {
179            final Element map = (Element) mimeTypes.item(i);
180            addMimeTypeMap(map);
181        }
182
183        // Add the extension maps
184        final NodeList extensions = config.getElementsByTagName("extension-map");
185        for (int i = 0; i < extensions.getLength(); i++) {
186            final Element map = (Element) extensions.item(i);
187            addExtensionMap(map);
188        }
189    }
190
191    /**
192     * Configures this manager from an XML configuration file.
193     *
194     * @param configUri The URI of the configuration.
195     * @throws FileSystemException if an error occurs.
196     */
197    private void configure(final URL configUri) throws FileSystemException {
198        InputStream configStream = null;
199        try {
200            // Load up the config
201            // TODO - validate
202            final DocumentBuilder builder = createDocumentBuilder();
203            configStream = configUri.openStream();
204            final Element config = builder.parse(configStream).getDocumentElement();
205
206            configure(config);
207        } catch (final Exception e) {
208            throw new FileSystemException("vfs.impl/load-config.error", configUri.toString(), e);
209        } finally {
210            if (configStream != null) {
211                try {
212                    configStream.close();
213                } catch (final IOException e) {
214                    getLogger().warn(e.getLocalizedMessage(), e);
215                }
216            }
217        }
218    }
219
220    /**
221     * Scans the classpath to find any dropped plugin.
222     * <p>
223     * The plugin-description has to be in {@code /META-INF/vfs-providers.xml}.
224     * </p>
225     *
226     * @throws FileSystemException if an error occurs.
227     */
228    protected void configurePlugins() throws FileSystemException {
229        final Enumeration<URL> enumResources;
230        try {
231            enumResources = enumerateResources(PLUGIN_CONFIG_RESOURCE);
232        } catch (final IOException e) {
233            throw new FileSystemException(e);
234        }
235
236        while (enumResources.hasMoreElements()) {
237            configure(enumResources.nextElement());
238        }
239    }
240
241    /**
242     * Gets a new DefaultFileReplicator.
243     *
244     * @return a new DefaultFileReplicator.
245     */
246    protected DefaultFileReplicator createDefaultFileReplicator() {
247        return new DefaultFileReplicator();
248    }
249
250    /**
251     * Configure and create a DocumentBuilder
252     *
253     * @return A DocumentBuilder for the configuration.
254     * @throws ParserConfigurationException if an error occurs.
255     */
256    private DocumentBuilder createDocumentBuilder() throws ParserConfigurationException {
257        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
258        factory.setIgnoringElementContentWhitespace(true);
259        factory.setIgnoringComments(true);
260        factory.setExpandEntityReferences(true);
261        return factory.newDocumentBuilder();
262    }
263
264    /**
265     * Creates a provider.
266     */
267    private Object createInstance(final String className) throws FileSystemException {
268        try {
269            return loadClass(className).getConstructor().newInstance();
270        } catch (final Exception e) {
271            throw new FileSystemException("vfs.impl/create-provider.error", className, e);
272        }
273    }
274
275    /**
276     * Enumerates resources from different class loaders.
277     *
278     * @throws IOException if {@code getResource} failed.
279     * @see #findClassLoader()
280     */
281    private Enumeration<URL> enumerateResources(final String name) throws IOException {
282        Enumeration<URL> enumeration = findClassLoader().getResources(name);
283        if (enumeration == null || !enumeration.hasMoreElements()) {
284            enumeration = getValidClassLoader(getClass()).getResources(name);
285        }
286        return enumeration;
287    }
288
289    /**
290     * Tests if a class is available.
291     */
292    private boolean findClass(final String className) {
293        try {
294            loadClass(className);
295            return true;
296        } catch (final ClassNotFoundException e) {
297            return false;
298        }
299    }
300
301    /**
302     * Returns a class loader or null since some Java implementation is null for the bootstrap class loader.
303     *
304     * @return A class loader or null since some Java implementation is null for the bootstrap class loader.
305     */
306    private ClassLoader findClassLoader() {
307        if (classLoader != null) {
308            return classLoader;
309        }
310        final ClassLoader cl = Thread.currentThread().getContextClassLoader();
311        if (cl != null) {
312            return cl;
313        }
314        return getValidClassLoader(getClass());
315    }
316
317    /**
318     * Extracts the required classes from a provider definition.
319     */
320    private String[] getRequiredClasses(final Element providerDef) {
321        final ArrayList<String> classes = new ArrayList<>();
322        final NodeList deps = providerDef.getElementsByTagName("if-available");
323        final int count = deps.getLength();
324        for (int i = 0; i < count; i++) {
325            final Element dep = (Element) deps.item(i);
326            final String className = dep.getAttribute("class-name");
327            if (!StringUtils.isEmpty(className)) {
328                classes.add(className);
329            }
330        }
331        return classes.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
332    }
333
334    /**
335     * Extracts the required schemes from a provider definition.
336     */
337    private String[] getRequiredSchemes(final Element providerDef) {
338        final ArrayList<String> schemes = new ArrayList<>();
339        final NodeList deps = providerDef.getElementsByTagName("if-available");
340        final int count = deps.getLength();
341        for (int i = 0; i < count; i++) {
342            final Element dep = (Element) deps.item(i);
343            final String scheme = dep.getAttribute("scheme");
344            if (!StringUtils.isEmpty(scheme)) {
345                schemes.add(scheme);
346            }
347        }
348        return schemes.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
349    }
350
351    /**
352     * Extracts the schema names from a provider definition.
353     */
354    private String[] getSchemas(final Element provider) {
355        final ArrayList<String> schemas = new ArrayList<>();
356        final NodeList schemaElements = provider.getElementsByTagName("scheme");
357        final int count = schemaElements.getLength();
358        for (int i = 0; i < count; i++) {
359            final Element scheme = (Element) schemaElements.item(i);
360            schemas.add(scheme.getAttribute("name"));
361        }
362        return schemas.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
363    }
364
365    private ClassLoader getValidClassLoader(final Class<?> clazz) {
366        return validateClassLoader(clazz.getClassLoader(), clazz);
367    }
368
369    /**
370     * Initializes this manager. Adds the providers and replicator.
371     *
372     * @throws FileSystemException if an error occurs.
373     */
374    @Override
375    public void init() throws FileSystemException {
376        // Set the replicator and temporary file store (use the same component)
377        final DefaultFileReplicator replicator = createDefaultFileReplicator();
378        setReplicator(new PrivilegedFileReplicator(replicator));
379        setTemporaryFileStore(replicator);
380
381        if (configUri == null) {
382            // Use default config
383            final URL url = getClass().getResource(CONFIG_RESOURCE);
384            FileSystemException.requireNonNull(url, "vfs.impl/find-config-file.error", CONFIG_RESOURCE);
385            configUri = url;
386        }
387
388        configure(configUri);
389        configurePlugins();
390
391        // Initialize super-class
392        super.init();
393    }
394
395    /**
396     * Load a class from different class loaders.
397     *
398     * @throws ClassNotFoundException if last {@code loadClass} failed.
399     * @see #findClassLoader()
400     */
401    private Class<?> loadClass(final String className) throws ClassNotFoundException {
402        try {
403            return findClassLoader().loadClass(className);
404        } catch (final ClassNotFoundException e) {
405            return getValidClassLoader(getClass()).loadClass(className);
406        }
407    }
408
409    /**
410     * Sets the ClassLoader to use to load the providers. Default is to use the ClassLoader that loaded this class.
411     *
412     * @param classLoader The ClassLoader.
413     */
414    public void setClassLoader(final ClassLoader classLoader) {
415        this.classLoader = classLoader;
416    }
417
418    /**
419     * Sets the configuration file for this manager.
420     *
421     * @param configUri The URI for this manager.
422     */
423    public void setConfiguration(final String configUri) {
424        try {
425            setConfiguration(new URL(configUri));
426        } catch (final MalformedURLException e) {
427            getLogger().warn(e.getLocalizedMessage(), e);
428        }
429    }
430
431    /**
432     * Sets the configuration file for this manager.
433     *
434     * @param configUri The URI for this manager.
435     */
436    public void setConfiguration(final URL configUri) {
437        this.configUri = configUri;
438    }
439
440    private ClassLoader validateClassLoader(final ClassLoader clazzLoader, final Class<?> clazz) {
441        return Objects.requireNonNull(clazzLoader, "The class loader for " + clazz
442                + " is null; some Java implementations use null for the bootstrap class loader.");
443    }
444
445}