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}