blob: a0fbbba15fddc5f0d93b20c5d12e70c02d24c5b5 [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.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
*
* TODO: Rework to use WIRE Admin
*/
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.
*/
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)) {
// FIXME: Spring 5 Migration
result.addAll(doFindPathMatchingJarResources(rootDirResource, new URL(""), 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);
}
}