blob: 3d7d11e6766d2d036f2e47da103f48825643d56f [file] [log] [blame]
/******************************************************************************
* 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;
}
}
}