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.io.IOUtils;
032import org.apache.commons.lang3.ArrayUtils;
033import org.apache.commons.lang3.StringUtils;
034import org.apache.commons.vfs2.FileSystemException;
035import org.apache.commons.vfs2.VfsLog;
036import org.apache.commons.vfs2.operations.FileOperationProvider;
037import org.apache.commons.vfs2.provider.FileProvider;
038import org.apache.commons.vfs2.util.Messages;
039import org.w3c.dom.Element;
040import org.w3c.dom.NodeList;
041
042/**
043 * A {@link org.apache.commons.vfs2.FileSystemManager} that configures itself from an XML (Default: providers.xml)
044 * configuration file.
045 * <p>
046 * Certain providers are only loaded and available if the dependent library is in your classpath. You have to configure
047 * your debugging facility to log "debug" messages to see if a provider was skipped due to "unresolved externals".
048 * </p>
049 */
050public class StandardFileSystemManager extends DefaultFileSystemManager {
051    private static final String CONFIG_RESOURCE = "providers.xml";
052    private static final String PLUGIN_CONFIG_RESOURCE = "META-INF/vfs-providers.xml";
053
054    private URL configUri;
055    private ClassLoader classLoader;
056
057    /**
058     * Constructs a new instance.
059     */
060    public StandardFileSystemManager() {
061        // empty
062    }
063
064    /**
065     * Adds an extension map.
066     *
067     * @param map containing the Elements.
068     */
069    private void addExtensionMap(final Element map) {
070        final String extension = map.getAttribute("extension");
071        final String scheme = map.getAttribute("scheme");
072        if (!StringUtils.isEmpty(scheme)) {
073            addExtensionMap(extension, scheme);
074        }
075    }
076
077    /**
078     * Adds a mime-type map.
079     *
080     * @param map containing the Elements.
081     */
082    private void addMimeTypeMap(final Element map) {
083        final String mimeType = map.getAttribute("mime-type");
084        final String scheme = map.getAttribute("scheme");
085        addMimeTypeMap(mimeType, scheme);
086    }
087
088    /**
089     * Adds a operationProvider from a operationProvider definition.
090     */
091    private void addOperationProvider(final Element providerDef) throws FileSystemException {
092        final String className = providerDef.getAttribute("class-name");
093
094        // Attach only to available schemas
095        final String[] schemas = getSchemas(providerDef);
096        for (final String schema : schemas) {
097            if (hasProvider(schema)) {
098                final FileOperationProvider operationProvider = (FileOperationProvider) createInstance(className);
099                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}