Temporarily include bndlib 1.47 for testing purposes (not yet on central)
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1185095 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/AbstractBaseOBR.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/AbstractBaseOBR.java
new file mode 100644
index 0000000..c6a1af5
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/AbstractBaseOBR.java
@@ -0,0 +1,564 @@
+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;
+ }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/Capability.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/Capability.java
new file mode 100644
index 0000000..983afed
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/Capability.java
@@ -0,0 +1,93 @@
+package aQute.lib.deployer.obr;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+public class Capability {
+
+ private final String name;
+ private final List<Property> properties;
+
+ private Capability(String name, List<Property> properties) {
+ this.name = name;
+ this.properties = properties;
+ }
+
+ public static class Builder {
+ private String name;
+ private final List<Property> properties = new LinkedList<Property>();
+
+ public Builder setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder addProperty(Property property) {
+ this.properties.add(property);
+ return this;
+ }
+
+ public Capability build() {
+ if (name == null) throw new IllegalStateException("'name' field is not initialised.");
+ return new Capability(name, Collections.unmodifiableList(properties));
+ }
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public List<Property> getProperties() {
+ return properties;
+ }
+
+ public Property findProperty(String propertyName) {
+ assert propertyName != null;
+ for (Property prop : properties) {
+ if (propertyName.equals(prop.getName()))
+ return prop;
+ }
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Capability [name=").append(name).append(", properties=").append(properties).append("]");
+ return builder.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ result = prime * result
+ + ((properties == null) ? 0 : properties.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Capability other = (Capability) obj;
+ if (name == null) {
+ if (other.name != null)
+ return false;
+ } else if (!name.equals(other.name))
+ return false;
+ if (properties == null) {
+ if (other.properties != null)
+ return false;
+ } else if (!properties.equals(other.properties))
+ return false;
+ return true;
+ }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/CapabilityType.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/CapabilityType.java
new file mode 100644
index 0000000..d3f8ae3
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/CapabilityType.java
@@ -0,0 +1,31 @@
+package aQute.lib.deployer.obr;
+
+public enum CapabilityType {
+
+ PACKAGE("package"),
+ EE("ee"),
+ BUNDLE("bundle"),
+ MODE("mode"),
+ OTHER(null);
+
+ private String typeName;
+
+ CapabilityType(String name) {
+ this.typeName = name;
+ }
+
+ public String getTypeName() {
+ return typeName;
+ }
+
+ /**
+ * @throws IllegalArgumentException
+ */
+ public static CapabilityType getForTypeName(String typeName) {
+ for (CapabilityType type : CapabilityType.values()) {
+ if (type.typeName != null && type.typeName.equals(typeName))
+ return type;
+ }
+ throw new IllegalArgumentException("Unknown capability type: " + typeName);
+ }
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/IResourceListener.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/IResourceListener.java
new file mode 100644
index 0000000..22dae7a
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/IResourceListener.java
@@ -0,0 +1,15 @@
+package aQute.lib.deployer.obr;
+
+public interface IResourceListener {
+ /**
+ * Process an OBR resource descriptor from the index document, and possibly
+ * request early termination of the parser.
+ *
+ * @param resource
+ * The resource descriptor to be processed.
+ * @return Whether to continue parsing the document; returning {@code false}
+ * will result in the parser being stopped with a
+ * {@link StopParseException}.
+ */
+ boolean processResource(Resource resource);
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/LocalOBR.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/LocalOBR.java
new file mode 100644
index 0000000..e59266a
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/LocalOBR.java
@@ -0,0 +1,198 @@
+package aQute.lib.deployer.obr;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.xml.transform.stream.StreamResult;
+
+import org.osgi.service.bindex.BundleIndexer;
+import org.xml.sax.InputSource;
+import org.xml.sax.XMLReader;
+
+import aQute.bnd.service.Refreshable;
+import aQute.bnd.service.Registry;
+import aQute.bnd.service.RegistryPlugin;
+import aQute.lib.deployer.FileRepo;
+import aQute.lib.io.IO;
+import aQute.lib.osgi.Jar;
+import aQute.libg.reporter.Reporter;
+import aQute.libg.sax.SAXUtil;
+import aQute.libg.sax.filters.MergeContentFilter;
+import aQute.libg.version.Version;
+
+public class LocalOBR extends OBR implements Refreshable, RegistryPlugin {
+
+ public static final String PROP_LOCAL_DIR = "local";
+ public static final String PROP_READONLY = "readonly";
+
+ private final FileRepo storageRepo = new FileRepo();
+
+ private Registry registry;
+ private File storageDir;
+ private File localIndex;
+
+ private List<URL> indexUrls;
+
+ public void setRegistry(Registry registry) {
+ this.registry = registry;
+ }
+
+ @Override
+ public void setReporter(Reporter reporter) {
+ super.setReporter(reporter);
+ storageRepo.setReporter(reporter);
+ }
+
+ @Override
+ public void setProperties(Map<String, String> map) {
+ super.setProperties(map);
+
+ // Load essential properties
+ String localDirPath = map.get(PROP_LOCAL_DIR);
+ if (localDirPath == null)
+ throw new IllegalArgumentException(String.format("Attribute '%s' must be set on LocalOBR plugin.", PROP_LOCAL_DIR));
+ storageDir = new File(localDirPath);
+ if (!storageDir.isDirectory())
+ throw new IllegalArgumentException(String.format("Local path '%s' does not exist or is not a directory.", localDirPath));
+
+ // Configure the storage repository
+ Map<String, String> storageRepoConfig = new HashMap<String, String>(2);
+ storageRepoConfig.put(FileRepo.LOCATION, localDirPath);
+ storageRepoConfig.put(FileRepo.READONLY, map.get(PROP_READONLY));
+ storageRepo.setProperties(storageRepoConfig);
+
+ // Set the local index and cache directory locations
+ localIndex = new File(storageDir, REPOSITORY_FILE_NAME);
+ if (localIndex.exists() && !localIndex.isFile())
+ throw new IllegalArgumentException(String.format("Cannot build local repository index: '%s' already exists but is not a plain file.", localIndex.getAbsolutePath()));
+ cacheDir = new File(storageDir, ".obrcache");
+ if (cacheDir.exists() && !cacheDir.isDirectory())
+ throw new IllegalArgumentException(String.format("Cannot create repository cache: '%s' already exists but is not directory.", cacheDir.getAbsolutePath()));
+ }
+
+ @Override
+ protected void initialiseIndexes() throws Exception {
+ if (!localIndex.exists()) {
+ regenerateIndex();
+ }
+ try {
+ Collection<URL> remotes = super.getOBRIndexes();
+ indexUrls = new ArrayList<URL>(remotes.size() + 1);
+ indexUrls.add(localIndex.toURI().toURL());
+ indexUrls.addAll(remotes);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Error initialising local index URL", e);
+ }
+ }
+
+ private void regenerateIndex() throws Exception {
+ BundleIndexer indexer = registry.getPlugin(BundleIndexer.class);
+ if (indexer == null)
+ throw new IllegalStateException("Cannot index repository: no Bundle Indexer service or plugin found.");
+
+ Set<File> allFiles = new HashSet<File>();
+ gatherFiles(allFiles);
+
+ FileOutputStream out = null;
+ try {
+ out = new FileOutputStream(localIndex);
+ if (!allFiles.isEmpty()) {
+ Map<String, String> config = new HashMap<String, String>();
+ config.put(BundleIndexer.REPOSITORY_NAME, this.getName());
+ config.put(BundleIndexer.ROOT_URL, localIndex.toURI().toURL().toString());
+ indexer.index(allFiles, out, config);
+ } else {
+ ByteArrayInputStream emptyRepo = new ByteArrayInputStream("<?xml version='1.0' encoding='UTF-8'?>\n<repository lastmodified='0'/>".getBytes());
+ IO.copy(emptyRepo, out);
+ }
+ } finally {
+ out.close();
+ }
+ }
+
+ private void gatherFiles(Set<File> allFiles) throws Exception {
+ List<String> bsns = storageRepo.list(null);
+ if (bsns != null) for (String bsn : bsns) {
+ List<Version> versions = storageRepo.versions(bsn);
+ if (versions != null) for (Version version : versions) {
+ File file = storageRepo.get(bsn, version.toString(), Strategy.HIGHEST, null);
+ if (file != null)
+ allFiles.add(file);
+ }
+ }
+ }
+
+ @Override
+ public List<URL> getOBRIndexes() {
+ return indexUrls;
+ }
+
+ @Override
+ public boolean canWrite() {
+ return storageRepo.canWrite();
+ }
+
+ @Override
+ public synchronized File put(Jar jar) throws Exception {
+ File newFile = storageRepo.put(jar);
+
+ // Index the new file
+ BundleIndexer indexer = registry.getPlugin(BundleIndexer.class);
+ if (indexer == null)
+ throw new IllegalStateException("Cannot index repository: no Bundle Indexer service or plugin found.");
+ ByteArrayOutputStream newIndexBuffer = new ByteArrayOutputStream();
+ indexer.index(Collections.singleton(newFile), newIndexBuffer, null);
+
+ // Merge into main index
+ File tempIndex = File.createTempFile("repository", ".xml");
+ FileOutputStream tempIndexOutput = new FileOutputStream(tempIndex);
+ MergeContentFilter merger = new MergeContentFilter();
+ XMLReader reader = SAXUtil.buildPipeline(new StreamResult(tempIndexOutput), new UniqueResourceFilter(), merger);
+
+ try {
+ // Parse the newly generated index
+ reader.parse(new InputSource(new ByteArrayInputStream(newIndexBuffer.toByteArray())));
+
+ // Parse the existing index (which may be empty/missing)
+ try {
+ reader.parse(new InputSource(new FileInputStream(localIndex)));
+ } catch (Exception e) {
+ reporter.warning("Existing local index is invalid or missing, overwriting (%s).", localIndex.getAbsolutePath());
+ }
+
+ merger.closeRootAndDocument();
+ } finally {
+ tempIndexOutput.flush();
+ tempIndexOutput.close();
+ }
+ IO.copy(tempIndex, localIndex);
+
+ // Re-read the index
+ reset();
+ init();
+
+ return newFile;
+ }
+
+ public boolean refresh() {
+ reset();
+ return true;
+ }
+
+ public File getRoot() {
+ return storageDir;
+ }
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/OBR.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/OBR.java
new file mode 100644
index 0000000..ddf5fcc
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/OBR.java
@@ -0,0 +1,120 @@
+package aQute.lib.deployer.obr;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import aQute.lib.deployer.FileRepo;
+
+/**
+ * A simple read-only OBR-based repository that uses a list of index locations
+ * and a basic local cache.
+ *
+ * <p>
+ * <h2>Properties</h2>
+ * <ul>
+ * <li><b>locations:</b> comma-separated list of index URLs. <b>NB:</b> surround with single quotes!</li>
+ * <li><b>name:</b> repository name; defaults to the index URLs.
+ * <li><b>cache:</b> local cache directory. May be omitted, in which case the repository will only be
+ * able to serve resources with {@code file:} URLs.</li>
+ * <li><b>location:</b> (deprecated) alias for "locations".
+ * </ul>
+ *
+ * <p>
+ * <h2>Example</h2>
+ *
+ * <pre>
+ * -plugin: aQute.lib.deployer.obr.OBR;locations='http://www.example.com/repository.xml';cache=${workspace}/.cache
+ * </pre>
+ *
+ * @author Neil Bartlett
+ *
+ */
+public class OBR extends AbstractBaseOBR {
+
+ public static final String PROP_LOCATIONS = "locations";
+ @Deprecated
+ public static final String PROP_LOCATION = "location";
+ public static final String PROP_CACHE = "cache";
+
+ protected List<URL> locations;
+ protected File cacheDir;
+
+ public void setProperties(Map<String, String> map) {
+ super.setProperties(map);
+
+ String locationsStr = map.get(PROP_LOCATIONS);
+ // backwards compatibility
+ if (locationsStr == null) locationsStr = map.get(PROP_LOCATION);
+
+ try {
+ if (locationsStr != null)
+ locations = parseLocations(locationsStr);
+ else
+ locations = Collections.emptyList();
+ } catch (MalformedURLException e) {
+ throw new IllegalArgumentException(String.format("Invalid location, unable to parse as URL list: %s", locationsStr), e);
+ }
+
+ String cacheDirStr = map.get(PROP_CACHE);
+ if (cacheDirStr != null)
+ cacheDir = new File(cacheDirStr);
+ }
+
+ private FileRepo lookupCachedFileRepo() {
+ if (registry != null) {
+ List<FileRepo> repos = registry.getPlugins(FileRepo.class);
+ for (FileRepo repo : repos) {
+ if ("cache".equals(repo.getName()))
+ return repo;
+ }
+ }
+ return null;
+ }
+
+ public List<URL> getOBRIndexes() {
+ return locations;
+ }
+
+ @Override
+ public synchronized File getCacheDirectory() {
+ if (cacheDir == null) {
+ FileRepo cacheRepo = lookupCachedFileRepo();
+ if (cacheRepo != null) {
+ File temp = new File(cacheRepo.getRoot(), ".obr");
+ temp.mkdirs();
+ if (temp.exists())
+ cacheDir = temp;
+ }
+ }
+ return cacheDir;
+ }
+
+ public void setCacheDirectory(File cacheDir) {
+ this.cacheDir = cacheDir;
+ }
+
+ @Override
+ public String getName() {
+ if (name != null && name != this.getClass().getName())
+ return name;
+
+ StringBuilder builder = new StringBuilder();
+
+ int count = 0;
+ for (URL location : locations) {
+ if (count++ > 0 ) builder.append(',');
+ builder.append(location);
+ }
+ return builder.toString();
+ }
+
+ public void setLocations(URL[] urls) {
+ this.locations = Arrays.asList(urls);
+ }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/OBRSAXHandler.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/OBRSAXHandler.java
new file mode 100644
index 0000000..3634cb9
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/OBRSAXHandler.java
@@ -0,0 +1,79 @@
+package aQute.lib.deployer.obr;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+public class OBRSAXHandler extends DefaultHandler {
+
+ private static final String TAG_RESOURCE = "resource";
+ private static final String ATTR_RESOURCE_ID = "id";
+ private static final String ATTR_RESOURCE_PRESENTATION_NAME = "presentationname";
+ private static final String ATTR_RESOURCE_SYMBOLIC_NAME = "symbolicname";
+ private static final String ATTR_RESOURCE_URI = "uri";
+ private static final String ATTR_RESOURCE_VERSION = "version";
+
+ private static final String TAG_CAPABILITY = "capability";
+ private static final String ATTR_CAPABILITY_NAME = "name";
+
+ private static final String TAG_REQUIRE = "require";
+ private static final String ATTR_REQUIRE_NAME = "name";
+ private static final String ATTR_REQUIRE_FILTER = "filter";
+ private static final String ATTR_REQUIRE_OPTIONAL = "optional";
+
+ private static final String TAG_PROPERTY = "p";
+ private static final String ATTR_PROPERTY_NAME = "n";
+ private static final String ATTR_PROPERTY_TYPE = "t";
+ private static final String ATTR_PROPERTY_VALUE = "v";
+
+ private final String baseUrl;
+ private final IResourceListener resourceListener;
+
+ private Resource.Builder resourceBuilder = null;
+ private Capability.Builder capabilityBuilder = null;
+ private Require require = null;
+
+ public OBRSAXHandler(String baseUrl, IResourceListener listener) {
+ this.baseUrl = baseUrl;
+ this.resourceListener = listener;
+ }
+
+
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
+ if (TAG_RESOURCE.equals(qName)) {
+ resourceBuilder = new Resource.Builder()
+ .setId(atts.getValue(ATTR_RESOURCE_ID))
+ .setPresentationName(atts.getValue(ATTR_RESOURCE_PRESENTATION_NAME))
+ .setSymbolicName(atts.getValue(ATTR_RESOURCE_SYMBOLIC_NAME))
+ .setUrl(atts.getValue(ATTR_RESOURCE_URI))
+ .setVersion(atts.getValue(ATTR_RESOURCE_VERSION))
+ .setBaseUrl(baseUrl);
+ } else if (TAG_CAPABILITY.equals(qName)) {
+ capabilityBuilder = new Capability.Builder()
+ .setName(atts.getValue(ATTR_CAPABILITY_NAME));
+ } else if (TAG_REQUIRE.equals(qName)) {
+ require = new Require(atts.getValue(ATTR_REQUIRE_NAME), atts.getValue(ATTR_REQUIRE_FILTER), "true".equalsIgnoreCase(atts.getValue(ATTR_REQUIRE_OPTIONAL)));
+ } else if (TAG_PROPERTY.equals(qName)) {
+ Property p = new Property(atts.getValue(ATTR_PROPERTY_NAME), atts.getValue(ATTR_PROPERTY_TYPE), atts.getValue(ATTR_PROPERTY_VALUE));
+ if (capabilityBuilder != null)
+ capabilityBuilder.addProperty(p);
+ }
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+ if (TAG_CAPABILITY.equals(qName)) {
+ resourceBuilder.addCapability(capabilityBuilder);
+ capabilityBuilder = null;
+ } else if (TAG_RESOURCE.equals(qName)) {
+ if (!resourceListener.processResource(resourceBuilder.build()))
+ throw new StopParseException();
+ resourceBuilder = null;
+ } else if (TAG_REQUIRE.equals(qName)) {
+ resourceBuilder.addRequire(require);
+ require = null;
+ }
+ }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/Property.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/Property.java
new file mode 100644
index 0000000..1eaaa8a
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/Property.java
@@ -0,0 +1,79 @@
+package aQute.lib.deployer.obr;
+
+/**
+ * @immutable
+ * @author Neil Bartlett
+ */
+public class Property {
+
+ static final String PACKAGE = "package";
+ static final String USES = "uses";
+ static final String VERSION = "version";
+
+ private final String name;
+ private final String type;
+ private final String value;
+
+ public Property(String name, String type, String value) {
+ this.name = name;
+ this.type = type;
+ this.value = value;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Property [name=").append(name).append(", type=").append(type).append(", value=").append(value).append("]");
+ return builder.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ result = prime * result + ((type == null) ? 0 : type.hashCode());
+ result = prime * result + ((value == null) ? 0 : value.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Property other = (Property) obj;
+ if (name == null) {
+ if (other.name != null)
+ return false;
+ } else if (!name.equals(other.name))
+ return false;
+ if (type == null) {
+ if (other.type != null)
+ return false;
+ } else if (!type.equals(other.type))
+ return false;
+ if (value == null) {
+ if (other.value != null)
+ return false;
+ } else if (!value.equals(other.value))
+ return false;
+ return true;
+ }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/Require.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/Require.java
new file mode 100644
index 0000000..d8d0fbb
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/Require.java
@@ -0,0 +1,73 @@
+package aQute.lib.deployer.obr;
+
+public class Require {
+
+ private final String name;
+ private final String filter;
+ private final boolean optional;
+
+ public Require(String name, String filter, boolean optional) {
+ this.name = name;
+ this.filter = filter;
+ this.optional = optional;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getFilter() {
+ return filter;
+ }
+
+ public boolean isOptional() {
+ return optional;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Require [");
+ if (name != null)
+ builder.append("name=").append(name).append(", ");
+ if (filter != null)
+ builder.append("filter=").append(filter).append(", ");
+ builder.append("optional=").append(optional).append("]");
+ return builder.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((filter == null) ? 0 : filter.hashCode());
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ result = prime * result + (optional ? 1231 : 1237);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Require other = (Require) obj;
+ if (filter == null) {
+ if (other.filter != null)
+ return false;
+ } else if (!filter.equals(other.filter))
+ return false;
+ if (name == null) {
+ if (other.name != null)
+ return false;
+ } else if (!name.equals(other.name))
+ return false;
+ if (optional != other.optional)
+ return false;
+ return true;
+ }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/Resource.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/Resource.java
new file mode 100644
index 0000000..a355326
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/Resource.java
@@ -0,0 +1,240 @@
+package aQute.lib.deployer.obr;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @immutable
+ * @author Neil Bartlett
+ */
+public class Resource {
+
+ private final String id;
+ private final String presentationName;
+ private final String symbolicName;
+ private final String baseUrl;
+ private final String url;
+ private final String version;
+ private final List<Capability> capabilities;
+ private final List<Require> requires;
+
+ private Resource(String id, String presentationName, String symbolicName, String baseUrl, String url, String version, List<Capability> capabilities, List<Require> requires) {
+ this.id = id;
+ this.presentationName = presentationName;
+ this.symbolicName = symbolicName;
+ this.baseUrl = baseUrl;
+ this.url = url;
+ this.version = version;
+
+ this.capabilities = capabilities;
+ this.requires = requires;
+ }
+
+ public static class Builder {
+ private String id;
+ private String presentationName;
+ private String symbolicName;
+ private String baseUrl;
+ private String url;
+ private String version;
+ private final List<Capability> capabilities = new LinkedList<Capability>();
+ private final List<Require> requires = new LinkedList<Require>();
+
+ public Builder setId(String id) {
+ this.id = id;
+ return this;
+ }
+ public Builder setPresentationName(String presentationName) {
+ this.presentationName = presentationName;
+ return this;
+ }
+ public Builder setSymbolicName(String symbolicName) {
+ this.symbolicName = symbolicName;
+ return this;
+ }
+ public Builder setBaseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ return this;
+ }
+ public Builder setUrl(String url) {
+ this.url = url;
+ return this;
+ }
+ public Builder setVersion(String version) {
+ this.version = version;
+ return this;
+ }
+ public Builder addCapability(Capability capability) {
+ this.capabilities.add(capability);
+ return this;
+ }
+ public Builder addCapability(Capability.Builder capabilityBuilder) {
+ this.capabilities.add(capabilityBuilder.build());
+ return this;
+ }
+ public Builder addRequire(Require require) {
+ this.requires.add(require);
+ return this;
+ }
+
+ public Resource build() {
+ if (id == null) throw new IllegalStateException("'id' field is not initialised");
+ if (symbolicName == null) throw new IllegalStateException("'symbolicName' field is not initialised");
+ if (url == null) throw new IllegalStateException("'url' field is not initialised");
+
+ return new Resource(id, presentationName, symbolicName, baseUrl, url, version, Collections.unmodifiableList(capabilities), Collections.unmodifiableList(requires));
+ }
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getPresentationName() {
+ return presentationName;
+ }
+
+ public String getSymbolicName() {
+ return symbolicName;
+ }
+
+ public String getBaseUrl() {
+ return baseUrl;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public List<Capability> getCapabilities() {
+ return capabilities;
+ }
+
+ public Capability findPackageCapability(String pkgName) {
+ for (Capability capability : capabilities) {
+ if (CapabilityType.PACKAGE.getTypeName().equals(capability.getName())) {
+ List<Property> props = capability.getProperties();
+ for (Property prop : props) {
+ if (Property.PACKAGE.equals(prop.getName())) {
+ if (pkgName.equals(prop.getValue()))
+ return capability;
+ else
+ break;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+
+
+ public List<Require> getRequires() {
+ return requires;
+ }
+
+ public Require findRequire(String name) {
+ for (Require require : requires) {
+ if (name.equals(require.getName()))
+ return require;
+ }
+ return null;
+ }
+
+ public Require findPackageRequire(String usesPkgName) {
+ String matchString = String.format("(package=%s)", usesPkgName);
+
+ for (Require require : requires) {
+ if (CapabilityType.PACKAGE.getTypeName().equals(require.getName())) {
+ String filter = require.getFilter();
+ if (filter.indexOf(matchString) > -1)
+ return require;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Resource [id=").append(id)
+ .append(", presentationName=").append(presentationName)
+ .append(", symbolicName=").append(symbolicName)
+ .append(", baseUrl=").append(baseUrl)
+ .append(", url=").append(url).append(", version=")
+ .append(version).append(", capabilities=").append(capabilities)
+ .append("]");
+ return builder.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((baseUrl == null) ? 0 : baseUrl.hashCode());
+ result = prime * result
+ + ((capabilities == null) ? 0 : capabilities.hashCode());
+ result = prime * result + ((id == null) ? 0 : id.hashCode());
+ result = prime
+ * result
+ + ((presentationName == null) ? 0 : presentationName.hashCode());
+ result = prime * result
+ + ((symbolicName == null) ? 0 : symbolicName.hashCode());
+ result = prime * result + ((url == null) ? 0 : url.hashCode());
+ result = prime * result + ((version == null) ? 0 : version.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Resource other = (Resource) obj;
+ if (baseUrl == null) {
+ if (other.baseUrl != null)
+ return false;
+ } else if (!baseUrl.equals(other.baseUrl))
+ return false;
+ if (capabilities == null) {
+ if (other.capabilities != null)
+ return false;
+ } else if (!capabilities.equals(other.capabilities))
+ return false;
+ if (id == null) {
+ if (other.id != null)
+ return false;
+ } else if (!id.equals(other.id))
+ return false;
+ if (presentationName == null) {
+ if (other.presentationName != null)
+ return false;
+ } else if (!presentationName.equals(other.presentationName))
+ return false;
+ if (symbolicName == null) {
+ if (other.symbolicName != null)
+ return false;
+ } else if (!symbolicName.equals(other.symbolicName))
+ return false;
+ if (url == null) {
+ if (other.url != null)
+ return false;
+ } else if (!url.equals(other.url))
+ return false;
+ if (version == null) {
+ if (other.version != null)
+ return false;
+ } else if (!version.equals(other.version))
+ return false;
+ return true;
+ }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/StopParseException.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/StopParseException.java
new file mode 100644
index 0000000..2ca3005
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/StopParseException.java
@@ -0,0 +1,7 @@
+package aQute.lib.deployer.obr;
+
+import org.xml.sax.SAXException;
+
+public class StopParseException extends SAXException {
+ private static final long serialVersionUID = 1L;
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/URLResourceHandle.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/URLResourceHandle.java
new file mode 100644
index 0000000..75b9243
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/URLResourceHandle.java
@@ -0,0 +1,136 @@
+package aQute.lib.deployer.obr;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URL;
+import java.net.URLEncoder;
+
+import aQute.bnd.service.ResourceHandle;
+
+public class URLResourceHandle implements ResourceHandle {
+
+ static final String FILE_SCHEME = "file:";
+ static final String HTTP_SCHEME = "http:";
+
+ final File cacheDir;
+
+ // The resolved, absolute URL of the resource
+ final URL url;
+
+ // The local file, if the resource IS a file, otherwise null.
+ final File localFile;
+
+ // The cached file copy of the resource, if it is remote and has been downloaded.
+ final File cachedFile;
+
+ public URLResourceHandle(String url, String baseUrl, final File cacheDir) throws IOException {
+ this.cacheDir = cacheDir;
+ if (url.startsWith(FILE_SCHEME)) {
+ // File URL may be relative or absolute
+ File file = new File(url.substring(FILE_SCHEME.length()));
+ if (file.isAbsolute()) {
+ this.localFile = file;
+ } else {
+ if (!baseUrl.startsWith(FILE_SCHEME))
+ throw new IllegalArgumentException("Relative file URLs cannot be resolved if the base URL is a non-file URL.");
+ this.localFile = resolveFile(baseUrl.substring(FILE_SCHEME.length()), file.toString());
+ }
+ this.url = localFile.toURI().toURL();
+ if (!localFile.isFile() && !localFile.isDirectory())
+ throw new FileNotFoundException("File URL " + this.url + " points at a non-existing file.");
+ this.cachedFile = null;
+ } else if (url.startsWith(HTTP_SCHEME)) {
+ // HTTP URLs must be absolute
+ this.url = new URL(url);
+ this.localFile = null;
+ this.cachedFile = mapRemoteURL(this.url);
+ } else {
+ // A path with no scheme means resolve relative to the base URL
+ if (baseUrl.startsWith(FILE_SCHEME)) {
+ this.localFile = resolveFile(baseUrl.substring(FILE_SCHEME.length()), url);
+ this.url = localFile.toURI().toURL();
+ this.cachedFile = null;
+ } else {
+ URL base = new URL(baseUrl);
+ this.url = new URL(base, url);
+ this.localFile = null;
+ this.cachedFile = mapRemoteURL(this.url);
+ }
+ }
+ }
+
+ File resolveFile(String baseFileName, String fileName) {
+ File resolved;
+
+ File baseFile = new File(baseFileName);
+ if (baseFile.isDirectory())
+ resolved = new File(baseFile, fileName);
+ else if (baseFile.isFile())
+ resolved = new File(baseFile.getParentFile(), fileName);
+ else
+ throw new IllegalArgumentException("Cannot resolve relative to non-existant base file path: " + baseFileName);
+
+ return resolved;
+ }
+
+ private File mapRemoteURL(URL url) throws UnsupportedEncodingException {
+ String encoded = URLEncoder.encode(url.toString(), "UTF-8");
+ return new File(cacheDir, encoded);
+ }
+
+ public String getName() {
+ return url.toString();
+ }
+
+ public Location getLocation() {
+ Location result;
+
+ if (localFile != null)
+ result = Location.local;
+ else if (cachedFile.exists())
+ result = Location.remote_cached;
+ else
+ result = Location.remote;
+
+ return result;
+ }
+
+ public File request() throws IOException {
+ if (localFile != null)
+ return localFile;
+ if (cachedFile == null)
+ throw new IllegalStateException("Invalid URLResourceHandle: both local file and cache file are uninitialised.");
+
+ if (!cachedFile.exists()) {
+ cacheDir.mkdirs();
+ downloadToFile(url, cachedFile);
+ }
+
+ return cachedFile;
+ }
+
+ private static void downloadToFile(URL url, File file) throws IOException {
+ InputStream in = null;
+ OutputStream out = null;
+ try {
+ in = url.openStream();
+ out = new FileOutputStream(file);
+
+ byte[] buf = new byte[1024];
+ for(;;) {
+ int bytes = in.read(buf, 0, 1024);
+ if (bytes < 0) break;
+ out.write(buf, 0, bytes);
+ }
+ } finally {
+ try { if (in != null) in.close(); } catch (IOException e) {};
+ try { if (out != null) in.close(); } catch (IOException e) {};
+ }
+ }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/UniqueResourceFilter.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/UniqueResourceFilter.java
new file mode 100644
index 0000000..0ad2cfb
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/UniqueResourceFilter.java
@@ -0,0 +1,54 @@
+package aQute.lib.deployer.obr;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.xml.sax.Attributes;
+
+import aQute.libg.sax.filters.ElementSelectionFilter;
+
+public class UniqueResourceFilter extends ElementSelectionFilter {
+
+ final Set<String> uris = new HashSet<String>();
+ final Map<String, List<String>> filteredResources = new HashMap<String, List<String>>();
+
+ @Override
+ protected boolean select(int depth, String uri, String localName, String qName, Attributes attribs) {
+ if ("resource".equals(qName)) {
+ String resourceUri = attribs.getValue("uri");
+ if (uris.contains(resourceUri)) {
+ addFilteredBundle(attribs.getValue("symbolicname"), attribs.getValue("version"));
+ return false;
+ }
+ uris.add(resourceUri);
+ }
+ return true;
+ }
+
+ private void addFilteredBundle(String bsn, String version) {
+ List<String> versions = filteredResources.get(bsn);
+ if (versions == null) {
+ versions = new LinkedList<String>();
+ filteredResources.put(bsn, versions);
+ }
+ versions.add(version);
+ }
+
+ public Collection<String> getFilteredBSNs() {
+ return filteredResources.keySet();
+ }
+
+ public Collection<String> getFilteredVersions(String bsn) {
+ List<String> list = filteredResources.get(bsn);
+ if (list == null)
+ list = Collections.emptyList();
+ return list;
+ }
+
+}