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