blob: c6a1af5258f27458c11f66d7d8043584ef80c306 [file] [log] [blame]
package aQute.lib.deployer.obr;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.SortedMap;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.regex.Pattern;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.SAXException;
import aQute.bnd.build.ResolverMode;
import aQute.bnd.service.OBRIndexProvider;
import aQute.bnd.service.OBRResolutionMode;
import aQute.bnd.service.Plugin;
import aQute.bnd.service.Registry;
import aQute.bnd.service.RegistryPlugin;
import aQute.bnd.service.RemoteRepositoryPlugin;
import aQute.bnd.service.ResourceHandle;
import aQute.bnd.service.ResourceHandle.Location;
import aQute.lib.filter.Filter;
import aQute.lib.osgi.Jar;
import aQute.libg.generics.Create;
import aQute.libg.reporter.Reporter;
import aQute.libg.version.Version;
import aQute.libg.version.VersionRange;
/**
* Abstract base class for OBR-based repositories.
*
* <p>
* The repository implementation is read-only by default. To implement a writable
* repository, subclasses should override {@link #canWrite()} and {@link #put(Jar)}.
*
* @author Neil Bartlett
*
*/
public abstract class AbstractBaseOBR implements RegistryPlugin, Plugin, RemoteRepositoryPlugin, OBRIndexProvider {
public static final String PROP_NAME = "name";
public static final String PROP_RESOLUTION_MODE = "mode";
public static final String PROP_RESOLUTION_MODE_ANY = "any";
public static final String REPOSITORY_FILE_NAME = "repository.xml";
protected Registry registry;
protected Reporter reporter;
protected String name = this.getClass().getName();
protected Set<OBRResolutionMode> supportedModes = EnumSet.allOf(OBRResolutionMode.class);
private boolean initialised = false;
private final Map<String, SortedMap<Version, Resource>> pkgResourceMap = new HashMap<String, SortedMap<Version, Resource>>();
private final Map<String, SortedMap<Version, Resource>> bsnMap = new HashMap<String, SortedMap<Version, Resource>>();
protected abstract File getCacheDirectory();
protected void addResourceToIndex(Resource resource) {
addBundleSymbolicNameToIndex(resource);
addPackagesToIndex(resource);
}
protected synchronized void reset() {
initialised = false;
}
/**
* Initialise the indexes prior to main initialisation of internal
* data structures. This implementation does nothing, but subclasses
* may override if they need to perform such initialisation.
* @throws Exception
*/
protected void initialiseIndexes() throws Exception {
}
protected final synchronized void init() throws Exception {
if (!initialised) {
bsnMap.clear();
pkgResourceMap.clear();
initialiseIndexes();
IResourceListener listener = new IResourceListener() {
public boolean processResource(Resource resource) {
addResourceToIndex(resource);
return true;
}
};
Collection<URL> indexes = getOBRIndexes();
for (URL indexLocation : indexes) {
try {
InputStream stream = indexLocation.openStream();
readIndex(indexLocation.toString(), stream, listener);
} catch (Exception e) {
e.printStackTrace();
reporter.error("Unable to read index at URL '%s'.", indexLocation);
}
}
initialised = true;
}
}
public void setRegistry(Registry registry) {
this.registry = registry;
}
public void setProperties(Map<String, String> map) {
if (map.containsKey(PROP_NAME))
name = map.get(PROP_NAME);
if (map.containsKey(PROP_RESOLUTION_MODE)) {
supportedModes = EnumSet.noneOf(OBRResolutionMode.class);
StringTokenizer tokenizer = new StringTokenizer(map.get(PROP_RESOLUTION_MODE), ",");
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken().trim();
if (PROP_RESOLUTION_MODE_ANY.equalsIgnoreCase(token))
supportedModes = EnumSet.allOf(OBRResolutionMode.class);
else {
try {
supportedModes.add(OBRResolutionMode.valueOf(token));
} catch (Exception e) {
if (reporter != null) reporter.error("Unknown OBR resolution mode: " + token);
}
}
}
}
}
public File[] get(String bsn, String range) throws Exception {
ResourceHandle[] handles = getHandles(bsn, range);
return requestAll(handles);
}
protected static File[] requestAll(ResourceHandle[] handles) throws IOException {
File[] result = (handles == null) ? new File[0] : new File[handles.length];
for (int i = 0; i < result.length; i++) {
result[i] = handles[i].request();
}
return result;
}
protected ResourceHandle[] getHandles(String bsn, String rangeStr) throws Exception {
init();
// If the range is set to "project", we cannot resolve it.
if ("project".equals(rangeStr))
return null;
SortedMap<Version, Resource> versionMap = bsnMap.get(bsn);
if (versionMap == null || versionMap.isEmpty())
return null;
List<Resource> resources = narrowVersionsByVersionRange(versionMap, rangeStr);
List<ResourceHandle> handles = mapResourcesToHandles(resources);
return (ResourceHandle[]) handles.toArray(new ResourceHandle[handles.size()]);
}
public void setReporter(Reporter reporter) {
this.reporter = reporter;
}
public File get(String bsn, String range, Strategy strategy, Map<String, String> properties) throws Exception {
ResourceHandle handle = getHandle(bsn, range, strategy, properties);
return handle != null ? handle.request() : null;
}
public ResourceHandle getHandle(String bsn, String range, Strategy strategy, Map<String, String> properties) throws Exception {
ResourceHandle result;
if (bsn != null)
result = resolveBundle(bsn, range, strategy);
else {
String pkgName = properties.get(CapabilityType.PACKAGE.getTypeName());
String modeName = properties.get(CapabilityType.MODE.getTypeName());
ResolverMode mode = (modeName != null) ? ResolverMode.valueOf(modeName) : null;
if (pkgName != null)
result = resolvePackage(pkgName, range, strategy, mode, properties);
else
throw new IllegalArgumentException("Cannot resolve bundle: neither bsn nor package specified.");
}
return result;
}
public boolean canWrite() {
return false;
}
public File put(Jar jar) throws Exception {
throw new UnsupportedOperationException("Read-only repository.");
}
public List<String> list(String regex) throws Exception {
init();
Pattern pattern = regex != null ? Pattern.compile(regex) : null;
List<String> result = new LinkedList<String>();
for (String bsn : bsnMap.keySet()) {
if (pattern == null || pattern.matcher(bsn).matches())
result.add(bsn);
}
return result;
}
public List<Version> versions(String bsn) throws Exception {
init();
SortedMap<Version, Resource> versionMap = bsnMap.get(bsn);
List<Version> list;
if (versionMap != null) {
list = new ArrayList<Version>(versionMap.size());
list.addAll(versionMap.keySet());
} else {
list = Collections.emptyList();
}
return list;
}
public String getName() {
return name;
}
void addBundleSymbolicNameToIndex(Resource resource) {
String bsn = resource.getSymbolicName();
Version version;
String versionStr = resource.getVersion();
try {
version = new Version(versionStr);
} catch (Exception e) {
version = new Version("0.0.0");
}
SortedMap<Version, Resource> versionMap = bsnMap.get(bsn);
if (versionMap == null) {
versionMap = new TreeMap<Version, Resource>();
bsnMap.put(bsn, versionMap);
}
versionMap.put(version, resource);
}
void addPackagesToIndex(Resource resource) {
for (Capability capability : resource.getCapabilities()) {
if (CapabilityType.PACKAGE.getTypeName().equals(capability.getName())) {
String pkgName = null;
String versionStr = null;
for (Property prop : capability.getProperties()) {
if (Property.PACKAGE.equals(prop.getName()))
pkgName = prop.getValue();
else if (Property.VERSION.equals(prop.getName()))
versionStr = prop.getValue();
}
Version version;
try {
version = new Version(versionStr);
} catch (Exception e) {
version = new Version("0.0.0");
}
if (pkgName != null) {
SortedMap<Version, Resource> versionMap = pkgResourceMap.get(pkgName);
if (versionMap == null) {
versionMap = new TreeMap<Version, Resource>();
pkgResourceMap.put(pkgName, versionMap);
}
versionMap.put(version, resource);
}
}
}
}
/**
* @return Whether to continue parsing other indexes
* @throws IOException
*/
boolean readIndex(String baseUrl, InputStream stream, IResourceListener listener) throws ParserConfigurationException, SAXException, IOException {
SAXParserFactory parserFactory = SAXParserFactory.newInstance();
SAXParser parser = parserFactory.newSAXParser();
try {
parser.parse(stream, new OBRSAXHandler(baseUrl, listener));
return true;
} catch (StopParseException e) {
return false;
} finally {
stream.close();
}
}
List<Resource> narrowVersionsByFilter(String pkgName, SortedMap<Version, Resource> versionMap, Filter filter) {
List<Resource> result = new ArrayList<Resource>(versionMap.size());
Dictionary<String, String> dict = new Hashtable<String, String>();
dict.put("package", pkgName);
for (Version version : versionMap.keySet()) {
dict.put("version", version.toString());
if (filter.match(dict))
result.add(versionMap.get(version));
}
return result;
}
List<Resource> narrowVersionsByVersionRange(SortedMap<Version, Resource> versionMap, String rangeStr) {
List<Resource> result;
if ("latest".equals(rangeStr)) {
Version highest = versionMap.lastKey();
result = Create.list(new Resource[] { versionMap.get(highest) });
} else {
VersionRange range = rangeStr != null ? new VersionRange(rangeStr) : null;
// optimisation: skip versions definitely less than the range
if (range != null && range.getLow() != null)
versionMap = versionMap.tailMap(range.getLow());
result = new ArrayList<Resource>(versionMap.size());
for (Version version : versionMap.keySet()) {
if (range == null || range.includes(version))
result.add(versionMap.get(version));
// optimisation: skip versions definitely higher than the range
if (range != null && range.isRange() && version.compareTo(range.getHigh()) >= 0)
break;
}
}
return result;
}
void filterResourcesByResolverMode(Collection<Resource> resources, ResolverMode mode) {
assert mode != null;
Properties modeCapability = new Properties();
modeCapability.setProperty(CapabilityType.MODE.getTypeName(), mode.name());
for (Iterator<Resource> iter = resources.iterator(); iter.hasNext(); ) {
Resource resource = iter.next();
Require modeRequire = resource.findRequire(CapabilityType.MODE.getTypeName());
if (modeRequire == null)
continue;
else if (modeRequire.getFilter() == null)
iter.remove();
else {
try {
Filter filter = new Filter(modeRequire.getFilter());
if (!filter.match(modeCapability))
iter.remove();
} catch (IllegalArgumentException e) {
if (reporter != null)
reporter.error("Error parsing mode filter requirement on resource %s: %s", resource.getUrl(), modeRequire.getFilter());
iter.remove();
}
}
}
}
List<ResourceHandle> mapResourcesToHandles(Collection<Resource> resources) throws Exception {
List<ResourceHandle> result = new ArrayList<ResourceHandle>(resources.size());
for (Resource resource : resources) {
ResourceHandle handle = mapResourceToHandle(resource);
if (handle != null)
result.add(handle);
}
return result;
}
ResourceHandle mapResourceToHandle(Resource resource) throws Exception {
ResourceHandle result = null;
URLResourceHandle handle ;
try {
handle = new URLResourceHandle(resource.getUrl(), resource.getBaseUrl(), getCacheDirectory());
} catch (FileNotFoundException e) {
throw new FileNotFoundException("Broken link in repository index: " + e.getMessage());
}
if (handle.getLocation() == Location.local || getCacheDirectory() != null)
result = handle;
return result;
}
ResourceHandle resolveBundle(String bsn, String rangeStr, Strategy strategy) throws Exception {
if (rangeStr == null) rangeStr = "0.0.0";
if (strategy == Strategy.EXACT) {
return findExactMatch(bsn, rangeStr, bsnMap);
}
ResourceHandle[] handles = getHandles(bsn, rangeStr);
ResourceHandle selected;
if (handles == null || handles.length == 0)
selected = null;
else {
switch(strategy) {
case LOWEST:
selected = handles[0];
break;
default:
selected = handles[handles.length - 1];
}
}
return selected;
}
ResourceHandle resolvePackage(String pkgName, String rangeStr, Strategy strategy, ResolverMode mode, Map<String, String> props) throws Exception {
init();
if (rangeStr == null) rangeStr = "0.0.0";
SortedMap<Version, Resource> versionMap = pkgResourceMap.get(pkgName);
if (versionMap == null)
return null;
// Was a filter expression supplied?
Filter filter = null;
String filterStr = props.get("filter");
if (filterStr != null) {
filter = new Filter(filterStr);
}
// Narrow the resources by version range string or filter.
List<Resource> resources;
if (filter != null)
resources = narrowVersionsByFilter(pkgName, versionMap, filter);
else
resources = narrowVersionsByVersionRange(versionMap, rangeStr);
// Remove resources that are invalid for the current resolution mode
if (mode != null)
filterResourcesByResolverMode(resources, mode);
// Select the most suitable one
Resource selected;
if (resources == null || resources.isEmpty())
selected = null;
else {
switch (strategy) {
case LOWEST:
selected = resources.get(0);
break;
default:
selected = resources.get(resources.size() - 1);
}
expandPackageUses(pkgName, selected, props);
}
return selected != null ? mapResourceToHandle(selected) : null;
}
void expandPackageUses(String pkgName, Resource resource, Map<String, String> props) {
List<String> internalUses = new LinkedList<String>();
Map<String, Require> externalUses = new HashMap<String, Require>();
internalUses.add(pkgName);
Capability capability = resource.findPackageCapability(pkgName);
Property usesProp = capability.findProperty(Property.USES);
if (usesProp != null) {
StringTokenizer tokenizer = new StringTokenizer(usesProp.getValue(), ",");
while (tokenizer.hasMoreTokens()) {
String usesPkgName = tokenizer.nextToken();
Capability usesPkgCap = resource.findPackageCapability(usesPkgName);
if (usesPkgCap != null)
internalUses.add(usesPkgName);
else {
Require require = resource.findPackageRequire(usesPkgName);
if (require != null)
externalUses.put(usesPkgName, require);
}
}
}
props.put("packages", listToString(internalUses));
props.put("import-uses", formatPackageRequires(externalUses));
}
String listToString(List<?> list) {
StringBuilder builder = new StringBuilder();
int count = 0;
for (Object item : list) {
if (count++ > 0) builder.append(',');
builder.append(item);
}
return builder.toString();
}
String formatPackageRequires(Map<String, Require> externalUses) {
StringBuilder builder = new StringBuilder();
int count = 0;
for (Entry<String, Require> entry : externalUses.entrySet()) {
String pkgName = entry.getKey();
String filter = entry.getValue().getFilter();
if (count++ > 0)
builder.append(',');
builder.append(pkgName);
builder.append(";filter='");
builder.append(filter);
builder.append('\'');
}
return builder.toString();
}
ResourceHandle findExactMatch(String identity, String version, Map<String, SortedMap<Version, Resource>> resourceMap) throws Exception {
Resource resource;
VersionRange range = new VersionRange(version);
if (range.isRange())
return null;
SortedMap<Version, Resource> versions = resourceMap.get(identity);
resource = versions.get(range.getLow());
return mapResourceToHandle(resource);
}
/**
* Utility function for parsing lists of URLs.
*
* @param locationsStr
* Comma-separated list of URLs
* @throws MalformedURLException
*/
protected static List<URL> parseLocations(String locationsStr) throws MalformedURLException {
StringTokenizer tok = new StringTokenizer(locationsStr, ",");
List<URL> urls = new ArrayList<URL>(tok.countTokens());
while (tok.hasMoreTokens()) {
String urlStr = tok.nextToken().trim();
urls.add(new URL(urlStr));
}
return urls;
}
public Set<OBRResolutionMode> getSupportedModes() {
return supportedModes;
}
}