/****************************************************************************** * 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.IOException; import java.net.URL; import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.eclipse.gemini.blueprint.io.internal.OsgiHeaderUtils; import org.eclipse.gemini.blueprint.io.internal.OsgiResourceUtils; import org.eclipse.gemini.blueprint.io.internal.OsgiUtils; import org.eclipse.gemini.blueprint.io.internal.resolver.DependencyResolver; import org.eclipse.gemini.blueprint.io.internal.resolver.ImportedBundle; import org.eclipse.gemini.blueprint.io.internal.resolver.PackageAdminResolver; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; import org.springframework.core.io.ContextResource; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.UrlResource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; /** * OSGi-aware {@link ResourcePatternResolver}. * * Can find resources in the <em>bundle jar</em> and <em>bundle space</em>. See {@link OsgiBundleResource} for more * information. * * <p/> <b>ClassPath support</b> * * <p/> As mentioned by {@link PathMatchingResourcePatternResolver}, class-path pattern matching needs to resolve the * class-path structure to a file-system location (be it an actual folder or a jar). Inside the OSGi environment this is * problematic as the bundles can be loaded in memory directly from input streams. To avoid relying on each platform * bundle storage structure, this implementation tries to determine the bundles that assemble the given bundle * class-path and analyze each of them individually. This involves the bundle archive (including special handling of the * <code>Bundle-Classpath</code> as it is computed at runtime), the bundle required packages and its attached fragments. * * Depending on the configuration of running environment, this might cause significant IO activity which can affect * performance. * * <p/> <b>Note:</b> Currently, <em>static</em> imports as well as <code>Bundle-Classpath</code> and * <code>Required-Bundle</code> entries are supported. Support for <code>DynamicPackage-Import</code> depends on * how/when the underlying platform does the wiring between the dynamically imported bundle and the given bundle. * * <p/> <b>Portability Note:</b> Since it relies only on the OSGi API, this implementation depends heavily on how * closely the platform implements the OSGi spec. While significant tests have been made to ensure compatibility, one * <em>might</em> experience different behaviour especially when dealing with jars with missing folder entries or * boot-path delegation. It is strongly recommended that wildcard resolution be thoroughly tested before switching to a * different platform before you rely on it. * * @see Bundle * @see OsgiBundleResource * @see PathMatchingResourcePatternResolver * * @author Costin Leau * */ public class OsgiBundleResourcePatternResolver extends PathMatchingResourcePatternResolver { /** * Our own logger to protect against incompatible class changes. */ private static final Log logger = LogFactory.getLog(OsgiBundleResourcePatternResolver.class); /** * The bundle on which this resolver works on. */ private final Bundle bundle; /** * The bundle context associated with this bundle. */ private final BundleContext bundleContext; private static final String FOLDER_SEPARATOR = "/"; private static final String FOLDER_WILDCARD = "**"; private static final String JAR_EXTENSION = ".jar"; private static final String BUNDLE_DEFAULT_CP = "."; private static final char SLASH = '/'; private static final char DOT = '.'; // use the default package admin version private final DependencyResolver resolver; public OsgiBundleResourcePatternResolver(Bundle bundle) { this(new OsgiBundleResourceLoader(bundle)); } public OsgiBundleResourcePatternResolver(ResourceLoader resourceLoader) { super(resourceLoader); if (resourceLoader instanceof OsgiBundleResourceLoader) { this.bundle = ((OsgiBundleResourceLoader) resourceLoader).getBundle(); } else { this.bundle = null; } this.bundleContext = (bundle != null ? OsgiUtils.getBundleContext(this.bundle) : null); this.resolver = (bundleContext != null ? new PackageAdminResolver(bundleContext) : null); } /** * Finds existing resources. This method returns the actual resources found w/o adding any extra decoration (such as * non-existing resources). * * @param locationPattern location pattern * @return found resources (w/o any decoration) * @throws IOException in case of I/O errors */ protected Resource[] findResources(String locationPattern) throws IOException { Assert.notNull(locationPattern, "Location pattern must not be null"); int type = OsgiResourceUtils.getSearchType(locationPattern); // look for patterns (includes classpath*:) if (getPathMatcher().isPattern(locationPattern)) { // treat classpath as a special case if (OsgiResourceUtils.isClassPathType(type)) return findClassPathMatchingResources(locationPattern, type); return findPathMatchingResources(locationPattern, type); } // even though we have no pattern // the OSGi space can return multiple entries for the same resource name // - treat this case below else { Resource[] result = null; OsgiBundleResource resource = new OsgiBundleResource(bundle, locationPattern); switch (type) { // same as bundle space case OsgiResourceUtils.PREFIX_TYPE_NOT_SPECIFIED: // consider bundle-space which can return multiple URLs case OsgiResourceUtils.PREFIX_TYPE_BUNDLE_SPACE: result = resource.getAllUrlsFromBundleSpace(locationPattern); break; // for the rest go with the normal resolving default: if (!resource.exists()) result = new Resource[] { resource }; break; } return result; } } // add a non-existing resource, if none was found and no pattern was specified public Resource[] getResources(final String locationPattern) throws IOException { Resource[] resources = findResources(locationPattern); // check whether we found something or we should fall-back to a // non-existing resource if (ObjectUtils.isEmpty(resources) && (!getPathMatcher().isPattern(locationPattern))) { return new Resource[] { getResourceLoader().getResource(locationPattern) }; } // return the original array return resources; } /** * Special classpath method. Will try to detect the imported bundles (which are part of the classpath) and look for * resources in all of them. This implementation will try to determine the bundles that compose the current bundle * classpath and then it will inspect the bundle space of each of them individually. * * <p/> Since the bundle space is considered, runtime classpath entries such as dynamic imports are not supported * (yet). * * @param locationPattern * @param type * @return classpath resources */ @SuppressWarnings("unchecked") private Resource[] findClassPathMatchingResources(String locationPattern, int type) throws IOException { if (resolver == null) throw new IllegalArgumentException( "PackageAdmin service/a started bundle is required for classpath matching"); final ImportedBundle[] importedBundles = resolver.getImportedBundles(bundle); // eliminate classpath path final String path = OsgiResourceUtils.stripPrefix(locationPattern); final Collection<String> foundPaths = new LinkedHashSet<String>(); // 1. search the imported packages // find folder path matching final String rootDirPath = determineFolderPattern(path); if (System.getSecurityManager() != null) { try { AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() { public Object run() throws IOException { for (int i = 0; i < importedBundles.length; i++) { final ImportedBundle importedBundle = importedBundles[i]; if (!bundle.equals(importedBundle.getBundle())) { findImportedBundleMatchingResource(importedBundle, rootDirPath, path, foundPaths); } } return null; } }); } catch (PrivilegedActionException pe) { throw (IOException) pe.getException(); } } else { for (int i = 0; i < importedBundles.length; i++) { final ImportedBundle importedBundle = importedBundles[i]; if (!bundle.equals(importedBundle.getBundle())) { findImportedBundleMatchingResource(importedBundle, rootDirPath, path, foundPaths); } } } // 2. search the target bundle findSyntheticClassPathMatchingResource(bundle, path, foundPaths); // 3. resolve the entries using the official class-path method (as some of them might be hidden) List<Resource> resources = new ArrayList<Resource>(foundPaths.size()); for (String resourcePath : foundPaths) { // classpath*: -> getResources() if (OsgiResourceUtils.PREFIX_TYPE_CLASS_ALL_SPACE == type) { CollectionUtils.mergeArrayIntoCollection(convertURLEnumerationToResourceArray(bundle .getResources(resourcePath), resourcePath), resources); } // classpath -> getResource() else { URL url = bundle.getResource(resourcePath); if (url != null) resources.add(new UrlContextResource(url, resourcePath)); } } if (logger.isTraceEnabled()) { logger.trace("Fitered " + foundPaths + " to " + resources); } return (Resource[]) resources.toArray(new Resource[resources.size()]); } private String determineFolderPattern(String path) { int index = path.lastIndexOf(FOLDER_SEPARATOR); return (index > 0 ? path.substring(0, index + 1) : ""); } private ContextResource[] convertURLEnumerationToResourceArray(Enumeration<URL> enm, String path) { Set<ContextResource> resources = new LinkedHashSet<ContextResource>(4); while (enm != null && enm.hasMoreElements()) { resources.add(new UrlContextResource(enm.nextElement(), path)); } return (ContextResource[]) resources.toArray(new ContextResource[resources.size()]); } /** * Searches for the given pattern inside the imported bundle. This translates to pattern matching on the imported * packages. * * @param importedBundle imported bundle * @param path path used for pattern matching * @param foundPaths collection of found results */ @SuppressWarnings("unchecked") private void findImportedBundleMatchingResource(final ImportedBundle importedBundle, String rootPath, String path, final Collection<String> foundPaths) throws IOException { final boolean trace = logger.isTraceEnabled(); String[] packages = importedBundle.getImportedPackages(); if (trace) logger.trace("Searching path [" + path + "] on imported pkgs " + ObjectUtils.nullSafeToString(packages) + "..."); final boolean startsWithSlash = rootPath.startsWith(FOLDER_SEPARATOR); for (int i = 0; i < packages.length; i++) { // transform the package name into a path String pkg = packages[i].replace(DOT, SLASH) + SLASH; if (startsWithSlash) { pkg = FOLDER_SEPARATOR + pkg; } final PathMatcher matcher = getPathMatcher(); // if the imported package matches the path if (matcher.matchStart(path, pkg)) { Bundle bundle = importedBundle.getBundle(); // 1. look at the Bundle jar root Enumeration<String> entries = bundle.getEntryPaths(pkg); while (entries != null && entries.hasMoreElements()) { String entry = entries.nextElement(); if (startsWithSlash) entry = FOLDER_SEPARATOR + entry; if (matcher.match(path, entry)) { if (trace) logger.trace("Found entry [" + entry + "]"); foundPaths.add(entry); } } // 2. Do a Bundle-Classpath lookup (since the jar might use a different classpath) Collection<String> cpMatchingPaths = findBundleClassPathMatchingPaths(bundle, path); foundPaths.addAll(cpMatchingPaths); } } } /** * Applies synthetic class-path analysis. That is, search the bundle space and the bundle class-path for entries * matching the given path. * * @param bundle * @param path * @param foundPaths * @throws IOException */ private void findSyntheticClassPathMatchingResource(Bundle bundle, String path, Collection<String> foundPaths) throws IOException { // 1. bundle space lookup OsgiBundleResourcePatternResolver localPatternResolver = new OsgiBundleResourcePatternResolver(bundle); Resource[] foundResources = localPatternResolver.findResources(path); boolean trace = logger.isTraceEnabled(); if (trace) logger.trace("Found synthetic cp resources " + ObjectUtils.nullSafeToString(foundResources)); for (int j = 0; j < foundResources.length; j++) { // assemble only the OSGi paths foundPaths.add(foundResources[j].getURL().getPath()); } // 2. Bundle-Classpath lookup (on the path stripped of the prefix) Collection<String> cpMatchingPaths = findBundleClassPathMatchingPaths(bundle, path); if (trace) logger.trace("Found Bundle-ClassPath matches " + cpMatchingPaths); foundPaths.addAll(cpMatchingPaths); // 3. Required-Bundle is considered already by the dependency resolver } /** * Searches the bundle classpath (Bundle-Classpath) entries for the given pattern. * * @param bundle * @param pattern * @return * @throws IOException */ private Collection<String> findBundleClassPathMatchingPaths(Bundle bundle, String pattern) throws IOException { // list of strings pointing to the matching resources List<String> list = new ArrayList<String>(4); boolean trace = logger.isTraceEnabled(); if (trace) logger.trace("Analyzing " + Constants.BUNDLE_CLASSPATH + " entries for bundle [" + bundle.getBundleId() + "|" + bundle.getSymbolicName() + "]"); // see if there is a bundle class-path defined String[] entries = OsgiHeaderUtils.getBundleClassPath(bundle); if (trace) logger.trace("Found " + Constants.BUNDLE_CLASSPATH + " entries " + ObjectUtils.nullSafeToString(entries)); // 1. if so, look at the entries for (int i = 0; i < entries.length; i++) { String entry = entries[i]; // make sure to exclude the default entry if (!entry.equals(BUNDLE_DEFAULT_CP)) { // 2. locate resource first from the bundle space (since it might not exist) OsgiBundleResource entryResource = new OsgiBundleResource(bundle, entry); // call the internal method to avoid catching an exception URL url = null; ContextResource res = entryResource.getResourceFromBundleSpace(entry); if (res != null) { url = res.getURL(); } if (trace) logger.trace("Classpath entry [" + entry + "] resolves to [" + url + "]"); // we've got a valid entry so let's parse it if (url != null) { String cpEntryPath = url.getPath(); // is it a jar ? if (entry.endsWith(JAR_EXTENSION)) findBundleClassPathMatchingJarEntries(list, url, pattern); // no, so it must be a folder else findBundleClassPathMatchingFolders(list, bundle, cpEntryPath, pattern); } } } return list; } /** * Checks the jar entries from the Bundle-Classpath for the given pattern. * * @param list * @param ur */ private void findBundleClassPathMatchingJarEntries(List<String> list, URL url, String pattern) throws IOException { // get the stream to the resource and read it as a jar JarInputStream jis = new JarInputStream(url.openStream()); Set<String> result = new LinkedHashSet<String>(8); boolean patternWithFolderSlash = pattern.startsWith(FOLDER_SEPARATOR); // parse the jar and do pattern matching try { while (jis.available() > 0) { JarEntry jarEntry = jis.getNextJarEntry(); // if the jar has ended, the entry can be null (on Sun JDK at least) if (jarEntry != null) { String entryPath = jarEntry.getName(); // check if leading "/" is needed or not (it depends how the jar was created) if (entryPath.startsWith(FOLDER_SEPARATOR)) { if (!patternWithFolderSlash) { entryPath = entryPath.substring(FOLDER_SEPARATOR.length()); } } else { if (patternWithFolderSlash) { entryPath = FOLDER_SEPARATOR.concat(entryPath); } } if (getPathMatcher().match(pattern, entryPath)) { result.add(entryPath); } } } } finally { try { jis.close(); } catch (IOException io) { // ignore it - nothing we can't do about it } } if (logger.isTraceEnabled()) logger.trace("Found in nested jar [" + url + "] matching entries " + result); list.addAll(result); } /** * Checks the folder entries from the Bundle-Classpath for the given pattern. * * @param list * @param bundle * @param cpEntryPath * @param pattern * @throws IOException */ private void findBundleClassPathMatchingFolders(List<String> list, Bundle bundle, String cpEntryPath, String pattern) throws IOException { // append path to the pattern and do a normal search // folder/<pattern> starts being applied String bundlePathPattern; boolean entryWithFolderSlash = cpEntryPath.endsWith(FOLDER_SEPARATOR); boolean patternWithFolderSlash = pattern.startsWith(FOLDER_SEPARATOR); // concatenate entry + pattern w/o double slashes if (entryWithFolderSlash) { if (patternWithFolderSlash) bundlePathPattern = cpEntryPath + pattern.substring(1, pattern.length()); else bundlePathPattern = cpEntryPath + pattern; } else { if (patternWithFolderSlash) bundlePathPattern = cpEntryPath + pattern; else bundlePathPattern = cpEntryPath + FOLDER_SEPARATOR + pattern; } // search the bundle space for the detected resource OsgiBundleResourcePatternResolver localResolver = new OsgiBundleResourcePatternResolver(bundle); Resource[] resources = localResolver.getResources(bundlePathPattern); boolean trace = logger.isTraceEnabled(); List<String> foundResources = (trace ? new ArrayList<String>(resources.length) : null); try { // skip when dealing with non-existing resources if (resources.length == 1 && !resources[0].exists()) { return; } else { int cutStartingIndex = cpEntryPath.length(); // add the resource stripping the cp for (int i = 0; i < resources.length; i++) { String path = resources[i].getURL().getPath().substring(cutStartingIndex); list.add(path); if (trace) foundResources.add(path); } } } finally { if (trace) logger.trace("Searching for [" + bundlePathPattern + "] revealed resources (relative to the cp entry [" + cpEntryPath + "]): " + foundResources); } } /** * Replace the super class implementation to pass in the searchType parameter. * * @see PathMatchingResourcePatternResolver#findPathMatchingResources(String) */ private Resource[] findPathMatchingResources(String locationPattern, int searchType) throws IOException { String rootDirPath = determineRootDir(locationPattern); String subPattern = locationPattern.substring(rootDirPath.length()); Resource[] rootDirResources = getResources(rootDirPath); boolean trace = logger.isTraceEnabled(); if (trace) logger.trace("Found root resources for [" + rootDirPath + "] :" + ObjectUtils.nullSafeToString(rootDirResources)); Set<Resource> result = new LinkedHashSet<Resource>(); for (int i = 0; i < rootDirResources.length; i++) { Resource rootDirResource = rootDirResources[i]; if (isJarResource(rootDirResource)) { result.addAll(doFindPathMatchingJarResources(rootDirResource, subPattern)); } else { result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern, searchType)); } } if (logger.isTraceEnabled()) { logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result); } return result.toArray(new Resource[result.size()]); } /** * {@inheritDoc} * * Overrides the default check up since computing the URL can be fairly expensive operation as there is no caching * (due to the framework dynamic nature). */ protected boolean isJarResource(Resource resource) throws IOException { if (resource instanceof OsgiBundleResource) { // check the resource type OsgiBundleResource bundleResource = (OsgiBundleResource) resource; // if it's known, then it's not a jar if (bundleResource.getSearchType() != OsgiResourceUtils.PREFIX_TYPE_UNKNOWN) { return false; } // otherwise the normal parsing occur } return super.isJarResource(resource); } /** * Based on the search type, uses the appropriate searching method. * * @see OsgiBundleResource#BUNDLE_URL_PREFIX * @see org.springframework.core.io.support.PathMatchingResourcePatternResolver#getResources(java.lang.String) */ private Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern, int searchType) throws IOException { String rootPath = null; if (rootDirResource instanceof OsgiBundleResource) { OsgiBundleResource bundleResource = (OsgiBundleResource) rootDirResource; rootPath = bundleResource.getPath(); searchType = bundleResource.getSearchType(); } else if (rootDirResource instanceof UrlResource) { rootPath = rootDirResource.getURL().getPath(); } if (rootPath != null) { String cleanPath = OsgiResourceUtils.stripPrefix(rootPath); // sanitize the root folder (since it's possible to not specify the root which fails any further matches) if (!cleanPath.endsWith(FOLDER_SEPARATOR)) { cleanPath = cleanPath + FOLDER_SEPARATOR; } String fullPattern = cleanPath + subPattern; Set<Resource> result = new LinkedHashSet<Resource>(); doRetrieveMatchingBundleEntries(bundle, fullPattern, cleanPath, result, searchType); return result; } else { return super.doFindPathMatchingFileResources(rootDirResource, subPattern); } } /** * Searches each level inside the bundle for entries based on the search strategy chosen. * * @param bundle the bundle to do the lookup * @param fullPattern matching pattern * @param dir directory inside the bundle * @param result set of results (used to concatenate matching sub dirs) * @param searchType the search strategy to use * @throws IOException */ private void doRetrieveMatchingBundleEntries(Bundle bundle, String fullPattern, String dir, Set<Resource> result, int searchType) throws IOException { Enumeration<?> candidates; switch (searchType) { case OsgiResourceUtils.PREFIX_TYPE_NOT_SPECIFIED: case OsgiResourceUtils.PREFIX_TYPE_BUNDLE_SPACE: // returns an enumeration of URLs candidates = bundle.findEntries(dir, null, false); break; case OsgiResourceUtils.PREFIX_TYPE_BUNDLE_JAR: // returns an enumeration of Strings candidates = bundle.getEntryPaths(dir); break; case OsgiResourceUtils.PREFIX_TYPE_CLASS_SPACE: // returns an enumeration of URLs throw new IllegalArgumentException("class space does not support pattern matching"); default: throw new IllegalArgumentException("unknown searchType " + searchType); } // entries are relative to the root path - miss the leading / if (candidates != null) { boolean dirDepthNotFixed = (fullPattern.indexOf(FOLDER_WILDCARD) != -1); while (candidates.hasMoreElements()) { Object path = candidates.nextElement(); String currPath; if (path instanceof String) currPath = handleString((String) path); else currPath = handleURL((URL) path); if (!currPath.startsWith(dir)) { // Returned resource path does not start with relative // directory: // assuming absolute path returned -> strip absolute path. int dirIndex = currPath.indexOf(dir); if (dirIndex != -1) { currPath = currPath.substring(dirIndex); } } if (currPath.endsWith(FOLDER_SEPARATOR) && (dirDepthNotFixed || StringUtils.countOccurrencesOf(currPath, FOLDER_SEPARATOR) < StringUtils .countOccurrencesOf(fullPattern, FOLDER_SEPARATOR))) { // Search subdirectories recursively: we manually get the // folders on only one level doRetrieveMatchingBundleEntries(bundle, fullPattern, currPath, result, searchType); } if (getPathMatcher().match(fullPattern, currPath)) { if (path instanceof URL) result.add(new UrlContextResource((URL) path, currPath)); else result.add(new OsgiBundleResource(bundle, currPath)); } } } } /** * Handles candidates returned as URLs. * * @param path * @return */ private String handleURL(URL path) { return path.getPath(); } /** * Handles candidates returned as Strings. * * @param path * @return */ private String handleString(String path) { return FOLDER_SEPARATOR.concat(path); } }