/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.vfs2.impl;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.vfs2.CacheStrategy;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.commons.vfs2.VfsLog;
import org.apache.commons.vfs2.operations.FileOperationProvider;
import org.apache.commons.vfs2.provider.FileProvider;
import org.apache.commons.vfs2.util.Messages;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

/**
 * A {@link org.apache.commons.vfs2.FileSystemManager} that configures itself
 * from an XML (Default: providers.xml) configuration file.<br>
 * Certain providers are only loaded and available if the dependend library is
 * in your classpath. You have to configure your debugging facility to log
 * "debug" messages to see if a provider was skipped due to
 * "unresolved externals".
 * 
 * @author <a href="http://commons.apache.org/vfs/team-list.html">Commons VFS
 *         team</a>
 */
public class StandardFileSystemManager extends DefaultFileSystemManager
{
	private static final String CONFIG_RESOURCE = "providers.xml";
	private static final String PLUGIN_CONFIG_RESOURCE = "META-INF/vfs-providers.xml";

	private URL configUri;
	private ClassLoader classLoader;

	/**
	 * Sets the configuration file for this manager.
	 * 
	 * @param configUri
	 *            The URI for this manager.
	 */
	public void setConfiguration(final String configUri)
	{
		try
		{
			setConfiguration(new URL(configUri));
		}
		catch (MalformedURLException e)
		{
			getLogger().warn(e.getLocalizedMessage(), e);
		}
	}

	/**
	 * Sets the configuration file for this manager.
	 * 
	 * @param configUri
	 *            The URI forthis manager.
	 */
	public void setConfiguration(final URL configUri)
	{
		this.configUri = configUri;
	}

	/**
	 * Sets the ClassLoader to use to load the providers. Default is to use the
	 * ClassLoader that loaded this class.
	 * 
	 * @param classLoader
	 *            The ClassLoader.
	 */
	public void setClassLoader(final ClassLoader classLoader)
	{
		this.classLoader = classLoader;
	}

	/**
	 * Initializes this manager. Adds the providers and replicator.
	 * 
	 * @throws FileSystemException
	 *             if an error occurs.
	 */
	@Override
	public void init() throws FileSystemException
	{
		// Set the replicator and temporary file store (use the same component)
		final DefaultFileReplicator replicator = createDefaultFileReplicator();
		setReplicator(new PrivilegedFileReplicator(replicator));
		setTemporaryFileStore(replicator);

		/*
		 * replaced by findClassLoader if (classLoader == null) { // Use default
		 * classloader classLoader = getClass().getClassLoader(); }
		 */

		if (configUri == null)
		{
			// Use default config
			final URL url = getClass().getResource(CONFIG_RESOURCE);
			if (url == null)
			{
				throw new FileSystemException(
						"vfs.impl/find-config-file.error", CONFIG_RESOURCE);
			}
			configUri = url;
		}

		// Configure
		configure(configUri);

		// Configure Plugins
		// do not use configured plugins. issues when including cifs jars in
		// classpath
		// configurePlugins();
		setCacheStrategy(CacheStrategy.ON_CALL);

		// Initialise super-class
		super.init();
	}

	/**
	 * Scans the classpath to find any droped plugin.<br />
	 * The plugin-description has to be in /META-INF/vfs-providers.xml
	 * 
	 * @throws FileSystemException
	 *             if an error occurs.
	 */
	protected void configurePlugins() throws FileSystemException
	{
		ClassLoader cl = findClassLoader();

		Enumeration<URL> enumResources;
		try
		{
			enumResources = cl.getResources(PLUGIN_CONFIG_RESOURCE);
		}
		catch (IOException e)
		{
			throw new FileSystemException(e);
		}

		while (enumResources.hasMoreElements())
		{
			URL url = enumResources.nextElement();
			configure(url);
		}
	}

	private ClassLoader findClassLoader()
	{
		if (classLoader != null)
		{
			return classLoader;
		}

		ClassLoader cl = Thread.currentThread().getContextClassLoader();
		if (cl == null)
		{
			cl = getClass().getClassLoader();
		}

		return cl;
	}

	protected DefaultFileReplicator createDefaultFileReplicator()
	{
		return new DefaultFileReplicator();
	}

	/**
	 * Configures this manager from an XML configuration file.
	 * 
	 * @param configUri
	 *            The URI of the configuration.
	 * @throws FileSystemException
	 *             if an error occus.
	 */
	private void configure(final URL configUri) throws FileSystemException
	{
		InputStream configStream = null;
		try
		{
			// Load up the config
			// TODO - validate
			/*
			 * final DocumentBuilder builder = createDocumentBuilder();
			 * configStream = configUri.openStream(); final Element config =
			 * builder.parse(configStream).getDocumentElement();
			 * 
			 * configure(config);
			 */
			hardCodedConfiguration();
		}
		catch (final Exception e)
		{
			throw new FileSystemException("vfs.impl/load-config.error",
					configUri.toString(), e);
		}
		finally
		{
			if (configStream != null)
			{
				try
				{
					configStream.close();
				}
				catch (IOException e)
				{
					getLogger().warn(e.getLocalizedMessage(), e);
				}
			}
		}
	}

	private void hardCodedConfiguration() throws FileSystemException
	{
		// addProvider(providerClassName, schemas, ifAvailableClasses,
		// ifAvailableSchemas, isDefault);
		addProvider("org.apache.commons.vfs2.provider.url.UrlFileProvider",
				null, null, null, true);
		addProvider(
				"org.apache.commons.vfs2.provider.local.DefaultLocalFileProvider",
				new String[] { "file" }, null, null, false);
		addProvider("org.apache.commons.vfs2.provider.zip.ZipFileProvider",
				new String[] { "zip" }, null, null, false);
		addProvider(
				"org.apache.commons.vfs2.provider.tar.TarFileProvider",
				new String[] { "tar" },
				new String[] { "org.apache.commons.vfs2.provider.tar.TarInputStream" },
				null, false);
		addProvider(
				"org.apache.commons.vfs2.provider.bzip2.Bzip2FileProvider",
				new String[] { "bz2" },
				new String[] { "org.apache.commons.vfs2.provider.bzip2.CBZip2InputStream" },
				null, false);
		addProvider("org.apache.commons.vfs2.provider.gzip.GzipFileProvider",
				new String[] { "gz" }, null, null, false);
		addProvider("org.apache.commons.vfs2.provider.jar.JarFileProvider",
				new String[] { "jar", "sar", "ear", "par", "ejb3", "war" },
				null, null, false);
		addProvider(
				"org.apache.commons.vfs2.provider.temp.TemporaryFileProvider",
				new String[] { "tmp" }, null, null, false);
		addProvider("org.apache.commons.vfs2.provider.ftp.FtpFileProvider",
				new String[] { "ftp" },
				new String[] { "org.apache.commons.net.ftp.FTPFile" }, null,
				false);
		try
		{
			addProvider("org.rzo.vfs.dropbox.DropboxFileProvider",
					new String[] { "dbx" },
					new String[] { "com.dropbox.core.DbxClient" }, null, false);
		}
		catch (Error ex)
		{
			getLogger().info(
					"could not load vfs-dbx provider: " + ex.getMessage());
		}
		catch (Exception ex)
		{
			getLogger().info(
					"could not load vfs-dbx provider: " + ex.getMessage());
		}
		addProvider("org.apache.commons.vfs2.provider.ftps.FtpsFileProvider",
				new String[] { "ftps" },
				new String[] { "org.apache.commons.net.ftp.FTPFile" }, null,
				false);
		addProvider("org.apache.commons.vfs2.provider.http.HttpFileProvider",
				new String[] { "http" },
				new String[] { "org.apache.commons.httpclient.HttpClient" },
				null, false);
		addProvider("org.apache.commons.vfs2.provider.https.HttpsFileProvider",
				new String[] { "https" },
				new String[] { "org.apache.commons.httpclient.HttpClient" },
				null, false);
		addProvider("org.apache.commons.vfs2.provider.sftp.SftpFileProvider",
				new String[] { "sftp" }, new String[] { "javax.crypto.Cipher",
						"com.jcraft.jsch.JSch" }, null, false);
		addProvider(
				"org.apache.commons.vfs2.provider.res.ResourceFileProvider",
				new String[] { "res" }, null, null, false);
		addProvider(
				"org.apache.commons.vfs2.provider.webdav.WebdavFileProvider",
				new String[] { "webdav" },
				new String[] { "org.apache.commons.httpclient.HttpClient",
						"org.apache.jackrabbit.webdav.client.methods.DavMethod" },
				null, false);
		addProvider("org.apache.commons.vfs2.provider.tar.TarFileProvider",
				new String[] { "tbz2" }, null, new String[] { "bz2", "tar" },
				false);
		addProvider("org.apache.commons.vfs2.provider.ram.RamFileProvider",
				new String[] { "ram" }, null, null, false);

		if (findClass("org.apache.commons.vfs2.provider.smb.SmbFileProvider"))
		{
			addProvider("org.apache.commons.vfs2.provider.smb.SmbFileProvider",
					new String[] { "smb" },
					new String[] { "jcifs.smb.SmbFile" }, null, false);
		}

		addMimeTypeMap("application/zip", "zip");
		addMimeTypeMap("application/x-tar", "tar");
		addMimeTypeMap("application/x-gzip", "gz");

		addExtensionMap("zip", "zip");
		addExtensionMap("tar", "tar");
		addExtensionMap("jar", "jar");
		addExtensionMap("bz2", "bz2");
		addExtensionMap("gz", "gz");
		addExtensionMap("tgz", "tar");
		addExtensionMap("tbz2", "tar");
		addExtensionMap("jar", "jar");
	}

	private void addProvider(String providerClassName, String[] schemas,
			String[] requiredClasses, String[] requiredSchemes,
			boolean isDefault) throws FileSystemException
	{
		// Make sure all required schemes are available
		if (requiredSchemes != null)
			for (int i = 0; i < requiredSchemes.length; i++)
			{
				final String requiredScheme = requiredSchemes[i];
				if (!hasProvider(requiredScheme))
				{
					final String msg = Messages.getString(
							"vfs.impl/skipping-provider-scheme.debug",
							new String[] { providerClassName, requiredScheme });
					VfsLog.debug(getLogger(), getLogger(), msg);
					return;
				}
			}

		// Make sure all required classes are in classpath
		if (requiredClasses != null)
			for (int i = 0; i < requiredClasses.length; i++)
			{
				final String requiredClass = requiredClasses[i];
				if (!findClass(requiredClass))
				{
					final String msg = Messages.getString(
							"vfs.impl/skipping-provider.debug", new String[] {
									providerClassName, requiredClass });
					VfsLog.debug(getLogger(), getLogger(), msg);
					return;
				}
			}

		// Create and register the provider
		final FileProvider provider = (FileProvider) createInstance(providerClassName);
		if (schemas != null && schemas.length > 0)
		{
			addProvider(schemas, provider);
		}

		// Set as default, if required
		if (isDefault)
		{
			setDefaultProvider(provider);
		}

	}

	/**
	 * Configures this manager from an XML configuration file.
	 * 
	 * @param configUri
	 *            The URI of the configuration.
	 * @param configStream
	 *            An InputStream containing the configuration.
	 * @throws FileSystemException
	 *             if an error occurs.
	 */
	@SuppressWarnings("unused")
	private void configure(final String configUri,
			final InputStream configStream) throws FileSystemException
	{
		try
		{
			// Load up the config
			// TODO - validate
			final DocumentBuilder builder = createDocumentBuilder();
			final Element config = builder.parse(configStream)
					.getDocumentElement();

			configure(config);

		}
		catch (final Exception e)
		{
			throw new FileSystemException("vfs.impl/load-config.error",
					configUri, e);
		}
	}

	/**
	 * Configure and create a DocumentBuilder
	 * 
	 * @return A DocumentBuilder for the configuration.
	 * @throws ParserConfigurationException
	 *             if an error occurs.
	 */
	private DocumentBuilder createDocumentBuilder()
			throws ParserConfigurationException
	{
		final DocumentBuilderFactory factory = DocumentBuilderFactory
				.newInstance();
		factory.setIgnoringElementContentWhitespace(true);
		factory.setIgnoringComments(true);
		factory.setExpandEntityReferences(true);
		return factory.newDocumentBuilder();
	}

	/**
	 * Configures this manager from an parsed XML configuration file
	 * 
	 * @param config
	 *            The configuration Element.
	 * @throws FileSystemException
	 *             if an error occurs.
	 */
	private void configure(final Element config) throws FileSystemException
	{
		// Add the providers
		final NodeList providers = config.getElementsByTagName("provider");
		final int count = providers.getLength();
		for (int i = 0; i < count; i++)
		{
			final Element provider = (Element) providers.item(i);
			addProvider(provider, false);
		}

		// Add the operation providers
		final NodeList operationProviders = config
				.getElementsByTagName("operationProvider");
		for (int i = 0; i < operationProviders.getLength(); i++)
		{
			final Element operationProvider = (Element) operationProviders
					.item(i);
			addOperationProvider(operationProvider);
		}

		// Add the default provider
		final NodeList defProviders = config
				.getElementsByTagName("default-provider");
		if (defProviders.getLength() > 0)
		{
			final Element provider = (Element) defProviders.item(0);
			addProvider(provider, true);
		}

		// Add the mime-type maps
		final NodeList mimeTypes = config.getElementsByTagName("mime-type-map");
		for (int i = 0; i < mimeTypes.getLength(); i++)
		{
			final Element map = (Element) mimeTypes.item(i);
			addMimeTypeMap(map);
		}

		// Add the extension maps
		final NodeList extensions = config
				.getElementsByTagName("extension-map");
		for (int i = 0; i < extensions.getLength(); i++)
		{
			final Element map = (Element) extensions.item(i);
			addExtensionMap(map);
		}
	}

	/**
	 * Adds an extension map.
	 * 
	 * @param map
	 *            containing the Elements.
	 */
	private void addExtensionMap(final Element map)
	{
		final String extension = map.getAttribute("extension");
		final String scheme = map.getAttribute("scheme");
		if (scheme != null && scheme.length() > 0)
		{
			addExtensionMap(extension, scheme);
		}
	}

	/**
	 * Adds a mime-type map.
	 * 
	 * @param map
	 *            containing the Elements.
	 */
	private void addMimeTypeMap(final Element map)
	{
		final String mimeType = map.getAttribute("mime-type");
		final String scheme = map.getAttribute("scheme");
		addMimeTypeMap(mimeType, scheme);
	}

	/**
	 * Adds a provider from a provider definition.
	 * 
	 * @param providerDef
	 *            the provider definition
	 * @param isDefault
	 *            true if the default should be used.
	 * @throws FileSystemException
	 *             if an error occurs.
	 */
	private void addProvider(final Element providerDef, final boolean isDefault)
			throws FileSystemException
	{
		final String classname = providerDef.getAttribute("class-name");

		// Make sure all required schemes are available
		final String[] requiredSchemes = getRequiredSchemes(providerDef);
		for (int i = 0; i < requiredSchemes.length; i++)
		{
			final String requiredScheme = requiredSchemes[i];
			if (!hasProvider(requiredScheme))
			{
				final String msg = Messages.getString(
						"vfs.impl/skipping-provider-scheme.debug",
						new String[] { classname, requiredScheme });
				VfsLog.debug(getLogger(), getLogger(), msg);
				return;
			}
		}

		// Make sure all required classes are in classpath
		final String[] requiredClasses = getRequiredClasses(providerDef);
		for (int i = 0; i < requiredClasses.length; i++)
		{
			final String requiredClass = requiredClasses[i];
			if (!findClass(requiredClass))
			{
				final String msg = Messages.getString(
						"vfs.impl/skipping-provider.debug", new String[] {
								classname, requiredClass });
				VfsLog.debug(getLogger(), getLogger(), msg);
				return;
			}
		}

		// Create and register the provider
		final FileProvider provider = (FileProvider) createInstance(classname);
		final String[] schemas = getSchemas(providerDef);
		if (schemas.length > 0)
		{
			addProvider(schemas, provider);
		}

		// Set as default, if required
		if (isDefault)
		{
			setDefaultProvider(provider);
		}
	}

	/**
	 * Adds a operationProvider from a operationProvider definition.
	 */
	private void addOperationProvider(final Element providerDef)
			throws FileSystemException
	{
		final String classname = providerDef.getAttribute("class-name");

		// Attach only to available schemas
		final String[] schemas = getSchemas(providerDef);
		for (int i = 0; i < schemas.length; i++)
		{
			final String schema = schemas[i];
			if (hasProvider(schema))
			{
				final FileOperationProvider operationProvider = (FileOperationProvider) createInstance(classname);
				addOperationProvider(schema, operationProvider);
			}
		}
	}

	/**
	 * Tests if a class is available.
	 */
	private boolean findClass(final String className)
	{
		try
		{
			findClassLoader().loadClass(className);
			return true;
		}
		catch (final ClassNotFoundException e)
		{
			return false;
		}
	}

	/**
	 * Extracts the required classes from a provider definition.
	 */
	private String[] getRequiredClasses(final Element providerDef)
	{
		final ArrayList<String> classes = new ArrayList<String>();
		final NodeList deps = providerDef.getElementsByTagName("if-available");
		final int count = deps.getLength();
		for (int i = 0; i < count; i++)
		{
			final Element dep = (Element) deps.item(i);
			String className = dep.getAttribute("class-name");
			if (className != null && className.length() > 0)
			{
				classes.add(className);
			}
		}
		return classes.toArray(new String[classes.size()]);
	}

	/**
	 * Extracts the required schemes from a provider definition.
	 */
	private String[] getRequiredSchemes(final Element providerDef)
	{
		final ArrayList<String> schemes = new ArrayList<String>();
		final NodeList deps = providerDef.getElementsByTagName("if-available");
		final int count = deps.getLength();
		for (int i = 0; i < count; i++)
		{
			final Element dep = (Element) deps.item(i);
			String scheme = dep.getAttribute("scheme");
			if (scheme != null && scheme.length() > 0)
			{
				schemes.add(scheme);
			}
		}
		return schemes.toArray(new String[schemes.size()]);
	}

	/**
	 * Extracts the schema names from a provider definition.
	 */
	private String[] getSchemas(final Element provider)
	{
		final ArrayList<String> schemas = new ArrayList<String>();
		final NodeList schemaElements = provider.getElementsByTagName("scheme");
		final int count = schemaElements.getLength();
		for (int i = 0; i < count; i++)
		{
			final Element scheme = (Element) schemaElements.item(i);
			schemas.add(scheme.getAttribute("name"));
		}
		return schemas.toArray(new String[schemas.size()]);
	}

	/**
	 * Creates a provider.
	 */
	private Object createInstance(final String className)
			throws FileSystemException
	{
		try
		{
			final Class<?> clazz = findClassLoader().loadClass(className);
			return clazz.newInstance();
		}
		catch (final Exception e)
		{
			throw new FileSystemException("vfs.impl/create-provider.error",
					className, e);
		}
	}
}
