/******************************************************************************
 * Copyright (c) 2006, 2010 VMware Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Apache License v2.0 which accompanies this distribution. 
 * The Eclipse Public License is available at 
 * http://www.eclipse.org/legal/epl-v10.html and the Apache License v2.0
 * is available at http://www.opensource.org/licenses/apache2.0.php.
 * You may elect to redistribute this code under either of these licenses. 
 * 
 * Contributors:
 *   VMware Inc.
 *****************************************************************************/

package org.eclipse.gemini.blueprint.io;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.Set;

import org.eclipse.gemini.blueprint.io.internal.OsgiResourceUtils;
import org.osgi.framework.Bundle;
import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.ContextResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;

/**
 * Resource implementation for OSGi environments.
 * 
 * <p/>
 * Lazy evaluation of the resource will be used.
 * 
 * This implementation allows resource location inside:
 * 
 * <ul>
 * <li><em>bundle space</em> - if <code>osgibundle:</code>/
 * {@link #BUNDLE_URL_PREFIX} prefix is being used or none is specified. This
 * space cotnains the bundle jar and its attached fragments.</li>
 * <li><em>bundle jar</em> - if <code>osgibundlejar:</code>/
 * {@link #BUNDLE_JAR_URL_PREFIX} is specified. This space contains just the
 * bundle jar.</li>
 * <li><em>class space</em> - if
 * {@link org.springframework.util.ResourceUtils#CLASSPATH_URL_PREFIX} is
 * encountered. This space contains the bundle classpath, namely the bundle jar,
 * its attached fragments and imported packages.</li>
 * </ul>
 * 
 * For more explanations on resource locations in OSGi, please see the
 * <em>Access to Resources</em> chapter from the OSGi spec.
 * 
 * <p/>
 * OSGi framework specific prefixes (such as <code>bundleentry:</code> and
 * <code>bundleresource:</code>under Equinox, <code>bundle:</code> under
 * Knopflefish and Felix, etc..) are supported. Resources outside the OSGi space
 * (<code>file:</code>, <code>http:</code>, etc..) are supported as well as the
 * path is being resolved to an <code>URL</code>.
 * 
 * <p/>
 * If no prefix is specified, the <em>bundle space</em> will be used for
 * locating a resource.
 * 
 * <p/>
 * <strong>Note:</strong> When the <em>bundle space</em> (bundle jar and its
 * attached fragments) is being searched, multiple URLs can be found but this
 * implementation will return only the first one. Consider using
 * {@link OsgiBundleResourcePatternResolver} to retrieve all entries.
 * 
 * @author Costin Leau
 * @author Adrian Colyer
 */
public class OsgiBundleResource extends AbstractResource implements ContextResource {

	/**
	 * Prefix for searching inside the owning bundle space. This translates to
	 * searching the bundle and its attached fragments. If no prefix is
	 * specified, this one will be used.
	 */
	public static final String BUNDLE_URL_PREFIX = "osgibundle:";

	/**
	 * Prefix for searching only the bundle raw jar. Will ignore attached
	 * fragments. Not used at the moment.
	 */
	public static final String BUNDLE_JAR_URL_PREFIX = "osgibundlejar:";

	private static final char PREFIX_SEPARATOR = ':';

	private static final String ABSOLUTE_PATH_PREFIX = "/";

	private final Bundle bundle;

	private final String path;

	// used to avoid removing the prefix every time the URL is required
	private final String pathWithoutPrefix;

	// Bundle resource possible searches
	private int searchType = OsgiResourceUtils.PREFIX_TYPE_NOT_SPECIFIED;


	/**
	 * 
	 * Constructs a new <code>OsgiBundleResource</code> instance.
	 * 
	 * @param bundle OSGi bundle used by this resource
	 * @param path resource path inside the bundle.
	 */
	public OsgiBundleResource(Bundle bundle, String path) {
		Assert.notNull(bundle, "Bundle must not be null");
		this.bundle = bundle;

		// check path
		Assert.notNull(path, "Path must not be null");

		this.path = StringUtils.cleanPath(path);

		this.searchType = OsgiResourceUtils.getSearchType(this.path);

		switch (this.searchType) {
			case OsgiResourceUtils.PREFIX_TYPE_NOT_SPECIFIED:
				pathWithoutPrefix = path;
				break;
			case OsgiResourceUtils.PREFIX_TYPE_BUNDLE_SPACE:
				pathWithoutPrefix = path.substring(BUNDLE_URL_PREFIX.length());
				break;
			case OsgiResourceUtils.PREFIX_TYPE_BUNDLE_JAR:
				pathWithoutPrefix = path.substring(BUNDLE_JAR_URL_PREFIX.length());
				break;
			case OsgiResourceUtils.PREFIX_TYPE_CLASS_SPACE:
				pathWithoutPrefix = path.substring(ResourceLoader.CLASSPATH_URL_PREFIX.length());
				break;
			// prefix unknown so the path will be resolved outside the context
			default:
				pathWithoutPrefix = null;
		}
	}

	/**
	 * Returns the path for this resource.
	 * 
	 * @return this resource path
	 */
	final String getPath() {
		return path;
	}

	/**
	 * Returns the bundle for this resource.
	 * 
	 * @return the resource bundle
	 */
	final Bundle getBundle() {
		return bundle;
	}

	/**
	 * Returns an <code>InputStream</code> to this resource. This implementation
	 * opens an <code>InputStream<code> for the given <code>URL</code>. It sets
	 * the <em>UseCaches</em> flag to <code>false</code>, mainly to avoid jar
	 * file locking on Windows.
	 * 
	 * @return input stream to the underlying resource
	 * @throws IOException if the stream could not be opened
	 * @see java.net.URL#openConnection()
	 * @see java.net.URLConnection#setUseCaches(boolean)
	 * @see java.net.URLConnection#getInputStream()
	 * 
	 */
	public InputStream getInputStream() throws IOException {
		URLConnection con = getURL().openConnection();
		con.setUseCaches(false);
		return con.getInputStream();
	}

	/**
	 * Locates the resource in the underlying bundle based on the prefix, if it
	 * exists. Note that the location happens per call since due to the dynamic
	 * nature of OSGi, the classpath of the bundle (among others) can change
	 * during a bundle lifecycle (depending on its imports).
	 * 
	 * @return URL to this resource
	 * @throws IOException if the resource cannot be resolved as URL, i.e. if
	 *         the resource is not available as descriptor
	 * 
	 * @see org.osgi.framework.Bundle#getEntry(String)
	 * @see org.osgi.framework.Bundle#getResource(String)
	 */
	public URL getURL() throws IOException {
		ContextResource res = null;
		URL url = null;

		switch (searchType) {
			// same as bundle space but with a different string
			case OsgiResourceUtils.PREFIX_TYPE_NOT_SPECIFIED:
				res = getResourceFromBundleSpace(pathWithoutPrefix);
				break;
			case OsgiResourceUtils.PREFIX_TYPE_BUNDLE_SPACE:
				res = getResourceFromBundleSpace(pathWithoutPrefix);
				break;
			case OsgiResourceUtils.PREFIX_TYPE_BUNDLE_JAR:
				url = getResourceFromBundleJar(pathWithoutPrefix);
				break;
			case OsgiResourceUtils.PREFIX_TYPE_CLASS_SPACE:
				url = getResourceFromBundleClasspath(pathWithoutPrefix);
				break;
			// fallback
			default:
				// just try to convert it to an URL
				url = new URL(path);
				break;
		}

		if (res != null) {
			url = res.getURL();
		}

		if (url == null) {
			throw new FileNotFoundException(getDescription() + " cannot be resolved to URL because it does not exist");
		}

		return url;
	}

	/**
	 * Resolves a resource from *the bundle space* only. Only the bundle and its
	 * attached fragments are searched for the given resource. Note that this
	 * method returns only the first URL found, discarding the rest. To retrieve
	 * the entire set, consider using {@link OsgiBundleResourcePatternResolver}.
	 * 
	 * @param bundlePath the path to resolve
	 * @return a URL to the returned resource or null if none is found
	 * @throws IOException
	 * 
	 * @see {@link org.osgi.framework.Bundle#findEntries(String, String, boolean)}
	 */
	ContextResource getResourceFromBundleSpace(String bundlePath) throws IOException {
		ContextResource[] res = getAllUrlsFromBundleSpace(bundlePath);
		return (ObjectUtils.isEmpty(res) ? null : res[0]);
	}

	/**
	 * Resolves a resource from the *bundle jar* only. Only the bundle jar is
	 * searched (its attached fragments are ignored).
	 * 
	 * @param bundlePath the path to resolve
	 * @return URL to the specified path or null if none is found
	 * @throws IOException
	 * 
	 * @see {@link Bundle#getEntry(String)}
	 */
	URL getResourceFromBundleJar(String bundlePath) throws IOException {
		return bundle.getEntry(bundlePath);
	}

	/**
	 * Resolves a resource from the bundle's classpath. This will find resources
	 * in this bundle and also in imported packages from other bundles.
	 * 
	 * @param bundlePath
	 * @return a URL to the returned resource or null if none is found
	 * 
	 * @see org.osgi.framework.Bundle#getResource(String)
	 */
	URL getResourceFromBundleClasspath(String bundlePath) {
		return bundle.getResource(bundlePath);
	}

	/**
	 * Determine if the given path is relative or absolute.
	 * 
	 * @param locationPath
	 * @return
	 */
	boolean isRelativePath(String locationPath) {
		return ((locationPath.indexOf(PREFIX_SEPARATOR) == -1) && !locationPath.startsWith(ABSOLUTE_PATH_PREFIX));
	}

	/**
	 * Returns a resource relative to this resource. This implementation creates
	 * an <code>OsgiBundleResource</code>, applying the given path relative to
	 * the path of the underlying resource of this descriptor.
	 * 
	 * @param relativePath the relative path (relative to this resource)
	 * @return the resource handle for the relative resource
	 * @throws IOException if the relative resource cannot be determined
	 * @see org.springframework.util.StringUtils#applyRelativePath(String,
	 *      String)
	 */
	public Resource createRelative(String relativePath) {
		String pathToUse = StringUtils.applyRelativePath(this.path, relativePath);
		return new OsgiBundleResource(this.bundle, pathToUse);
	}

	/**
	 * Returns the filename of this resources. This implementation returns the
	 * name of the file that this bundle path resource refers to.
	 * 
	 * @return resource filename
	 * @see org.springframework.util.StringUtils#getFilename(String)
	 */
	public String getFilename() {
		return StringUtils.getFilename(this.path);
	}

	/**
	 * Returns a <code>File</code> handle for this resource. This method does a
	 * best-effort attempt to locate the bundle resource on the file system. It
	 * is strongly recommended to use {@link #getInputStream()} method instead
	 * which works no matter if the bundles are saved (in exploded form or not)
	 * on the file system.
	 * 
	 * @return File handle to this resource
	 * @throws IOException if the resource cannot be resolved as absolute file
	 *         path, i.e. if the resource is not available in a file system
	 */
	public File getFile() throws IOException {
		// locate the file inside the bundle only known prefixes
		if (searchType != OsgiResourceUtils.PREFIX_TYPE_UNKNOWN) {
			String bundleLocation = bundle.getLocation();
			int prefixIndex = bundleLocation.indexOf(ResourceUtils.FILE_URL_PREFIX);
			if (prefixIndex > -1) {
				bundleLocation = bundleLocation.substring(prefixIndex + ResourceUtils.FILE_URL_PREFIX.length());
			}
			File file = new File(bundleLocation, path);
			if (file.exists()) {
				return file;
			}
			// fall back to the URL discovery (just in case)
		}

		try {
			return ResourceUtils.getFile(getURI(), getDescription());
		}
		catch (IOException ioe) {
			throw (IOException) new FileNotFoundException(getDescription()
					+ " cannot be resolved to absolute file path").initCause(ioe);
		}
	}

	/**
	 * <p/>
	 * This implementation returns a description that includes the bundle
	 * location.
	 */
	public String getDescription() {
		StringBuilder buf = new StringBuilder();
		buf.append("OSGi resource[");
		buf.append(this.path);
		buf.append("|bnd.id=");
		buf.append(bundle.getBundleId());
		buf.append("|bnd.sym=");
		buf.append(bundle.getSymbolicName());
		buf.append("]");

		return buf.toString();
	}

	/**
	 * <p/>
	 * This implementation compares the underlying bundle and path locations.
	 */
	public boolean equals(Object obj) {
		if (obj == this) {
			return true;
		}
		if (obj instanceof OsgiBundleResource) {
			OsgiBundleResource otherRes = (OsgiBundleResource) obj;
			return (this.path.equals(otherRes.path) && ObjectUtils.nullSafeEquals(this.bundle, otherRes.bundle));
		}
		return false;
	}

	/**
	 * <p/>
	 * This implementation returns the hash code of the underlying class path
	 * location.
	 */
	public int hashCode() {
		return this.path.hashCode();
	}

	public long lastModified() throws IOException {
		URLConnection con = getURL().openConnection();
		con.setUseCaches(false);
		long time = con.getLastModified();
		// the implementation doesn't return the proper time stamp
		if (time == 0) {
			if (OsgiResourceUtils.PREFIX_TYPE_BUNDLE_JAR == searchType)
				return bundle.getLastModified();
		}
		// there is nothing else we can do
		return time;
	}

	/**
	 * @return Returns the searchType.
	 */
	int getSearchType() {
		return searchType;
	}

	/**
	 * Used internally to get all the URLs matching a certain location. The
	 * method is required to extract the folder from the given location as well
	 * the file.
	 * 
	 * @param location location to look for
	 * @return an array of URLs
	 * @throws IOException
	 */
	@SuppressWarnings("unchecked")
	ContextResource[] getAllUrlsFromBundleSpace(String location) throws IOException {
		if (bundle == null)
			throw new IllegalArgumentException(
				"cannot locate items in bundle-space w/o a bundle; specify one when creating this resolver");

		Assert.notNull(location);
		Set<UrlContextResource> resources = new LinkedHashSet<UrlContextResource>(5);

		location = StringUtils.cleanPath(location);
		location = OsgiResourceUtils.stripPrefix(location);

		if (!StringUtils.hasText(location))
			location = OsgiResourceUtils.FOLDER_DELIMITER;

		// the root folder is requested (special case)
		if (OsgiResourceUtils.FOLDER_DELIMITER.equals(location)) {
			// there is no way to determine the URL to the root directly
			// through findEntries so we'll have to use another way

			// getEntry can't be used since it doesn't consider fragments
			// so we have to rely on findEntries 

			// we could ask for a known entry (such as META-INF)
			// but not all jars have a dedicated entry for it
			// so we'll just ask for whatever is present in the root
			Enumeration<URL> candidates = bundle.findEntries("/", null, false);

			// since there can be multiple root paths (when fragments are present)
			// iterate on all candidates
			while (candidates != null && candidates.hasMoreElements()) {

				URL url = candidates.nextElement();

				// determined the root path
				// we'll have to parse the string since some implementations
				// do not normalize the resulting URL resulting in mismatches
				String rootPath = OsgiResourceUtils.findUpperFolder(url.toExternalForm());
				resources.add(new UrlContextResource(rootPath));
			}
		}
		else {
			// remove leading and trailing / if any
			if (location.startsWith(OsgiResourceUtils.FOLDER_DELIMITER))
				location = location.substring(1);

			if (location.endsWith(OsgiResourceUtils.FOLDER_DELIMITER))
				location = location.substring(0, location.length() - 1);

			// do we have at least on folder or is this just a file
			boolean hasFolder = (location.indexOf(OsgiResourceUtils.FOLDER_DELIMITER) != -1);

			String path = (hasFolder ? location : OsgiResourceUtils.FOLDER_DELIMITER);
			String file = (hasFolder ? null : location);

			// find the file and path
			int separatorIndex = location.lastIndexOf(OsgiResourceUtils.FOLDER_DELIMITER);

			if (separatorIndex > -1 && separatorIndex + 1 < location.length()) {
				// update the path
				path = location.substring(0, separatorIndex);

				// determine file (if there is any)
				if (separatorIndex + 1 < location.length())
					file = location.substring(separatorIndex + 1);
			}

			Enumeration<URL> candidates = bundle.findEntries(path, file, false);
			// add the leading / to be consistent
			String contextPath = OsgiResourceUtils.FOLDER_DELIMITER + location;

			while (candidates != null && candidates.hasMoreElements()) {
				resources.add(new UrlContextResource(candidates.nextElement(), contextPath));
			}
		}

		return (ContextResource[]) resources.toArray(new ContextResource[resources.size()]);
	}

	// TODO: can this return null or throw an exception
	public String getPathWithinContext() {
		return pathWithoutPrefix;
	}

	/**
	 * Return whether this resource actually exists in physical form.
	 * <p>
	 * This method performs a definitive existence check, whereas the existence
	 * of a <code>Resource</code> handle only guarantees a valid descriptor
	 * handle.
	 * 
	 * <p/>
	 * The existence check is done by opening an InputStream to the underlying
	 * resource (overriding the default implementation which checks first for
	 * the presence of a File).
	 */
	public boolean exists() {
		try {
			InputStream is = getInputStream();
			is.close();
			return true;
		}
		catch (Throwable isEx) {
			return false;
		}
	}
}