/****************************************************************************** * 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; } } }