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