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}