You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@felix.apache.org by nb...@apache.org on 2018/03/08 14:57:58 UTC

svn commit: r1826244 [2/3] - in /felix/trunk: ./ fileinstall-plugins/ fileinstall-plugins/installer.test/ fileinstall-plugins/installer.test/src/ fileinstall-plugins/installer.test/src/test/ fileinstall-plugins/installer.test/src/test/java/ fileinstall...

Added: felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/DeploymentInstaller.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/DeploymentInstaller.java?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/DeploymentInstaller.java (added)
+++ felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/DeploymentInstaller.java Thu Mar  8 14:57:57 2018
@@ -0,0 +1,715 @@
+/*******************************************************************************
+ * Copyright (c) Intel Corporation
+ * Copyright (c) 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.apache.felix.fileinstall.plugins.installer.impl;
+
+import static org.apache.felix.fileinstall.plugins.installer.impl.Locking.withLock;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Queue;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Supplier;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.stream.Collectors;
+
+import org.apache.felix.fileinstall.ArtifactInstaller;
+import org.apache.felix.fileinstall.plugins.installer.Artifact;
+import org.apache.felix.fileinstall.plugins.installer.FrameworkInstaller;
+import org.apache.felix.fileinstall.plugins.installer.Hash;
+import org.apache.felix.fileinstall.plugins.installer.InstallableListener;
+import org.apache.felix.fileinstall.plugins.installer.InstallableManager;
+import org.apache.felix.fileinstall.plugins.installer.InstallableUnit;
+import org.apache.felix.fileinstall.plugins.installer.InstallableUnitEvent;
+import org.apache.felix.fileinstall.plugins.installer.State;
+import org.apache.felix.fileinstall.plugins.resolver.PluginResolver;
+import org.apache.felix.fileinstall.plugins.resolver.ResolveRequest;
+import org.apache.felix.fileinstall.plugins.resolver.ResolveResult;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.BundleListener;
+import org.osgi.framework.FrameworkEvent;
+import org.osgi.framework.FrameworkListener;
+import org.osgi.framework.Version;
+import org.osgi.framework.namespace.IdentityNamespace;
+import org.osgi.framework.wiring.BundleRevision;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+import org.osgi.resource.Resource;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
+import org.osgi.service.log.LogService;
+import org.osgi.service.repository.ContentNamespace;
+import org.osgi.util.promise.Promise;
+import org.osgi.util.promise.Promises;
+
+import aQute.lib.hex.Hex;
+
+@Component(immediate = true)
+public class DeploymentInstaller implements ArtifactInstaller, InstallableManager {
+
+    /**
+     * Set environment variable "org.apache.felix.fileinstall.plugins.installer.debug" to enable develeroper-level debug output to the console.
+     */
+    public static final boolean DEBUG = Boolean.getBoolean("org.apache.felix.fileinstall.plugins.installer.debug");
+
+    // If Deployment version is not specified, the version to return
+    private static final String UNKNOWN_DEPLOYMENT_VERSION = "0.0.0";
+
+    /**
+     * OSGi Repository indexes are documented to use SHA-256 for their content hashes.
+     * @see ContentNamespace#CONTENT_NAMESPACE
+     */
+    private static final String SHA256 = "SHA-256";
+
+    // Magic number (not officially but good enough for our purposes) of a ZIP file is PK\0x03\0x04
+    private static final byte[] ZIP_FILE_MAGIC = new byte[] { 0x50, 0x4b, 0x03, 0x04 };
+
+    // Queues for pending resolve and install operations.
+    private final Queue<Job<List<Bundle>>> pendingInstalls = new LinkedList<>();
+    private final Map<File, Job<InstallableUnitImpl>> pendingResolves = new LinkedHashMap<>();
+
+    // Currently available Installable Units, and a lock for modifying that map safely.
+    private final Map<File, InstallableUnitImpl> units = new HashMap<>();
+    private final ReadWriteLock unitsLock = new ReentrantReadWriteLock();
+
+    // Listeners for install/uninstall events
+    private final List<InstallableListener> installListeners = new CopyOnWriteArrayList<>();
+
+    // The framework listener invalidates any RESOLVED install units whenever the OSGi Framework packages are refreshed.
+    private final FrameworkListener frameworkListener = new FrameworkListener() {
+        @Override
+        public void frameworkEvent(FrameworkEvent event) {
+            if (event.getType() == FrameworkEvent.PACKAGES_REFRESHED) {
+                invalidateAllUnits("Packaged Refreshed");
+            }
+        }
+    };
+    // The bundle listener invalidated any RESOLVED install units whenever OSGi Bundles are installed, resolved, unresolved, updated or uninstalled.
+    private final BundleListener bundleListener = new BundleListener() {
+        @Override
+        public void bundleChanged(BundleEvent event) {
+            int mask = BundleEvent.INSTALLED | BundleEvent.RESOLVED | BundleEvent.UNRESOLVED | BundleEvent.UPDATED | BundleEvent.UNINSTALLED;
+            if ((mask & event.getType()) > 0) {
+                invalidateAllUnits(String.format("Bundle %s entered state %s", event.getBundle().getSymbolicName(), bundleEventToString(event.getType())));
+            }
+        }
+    };
+
+    // Component instance state.
+    private BundleContext context;
+    private Thread processorThread;
+
+    // The PluginResolver service that calculates resolution results for us, but doesn't produce any effects.
+    @Reference(target = "(!(" + org.osgi.framework.Constants.SERVICE_BUNDLEID + "=0))")
+    PluginResolver resolver;
+
+    // The FrameworkInstaller service is responsible for ensuring required bundles are present in the OSGi Framework -- installing them when necessary.
+    @Reference
+    FrameworkInstaller frameworkInstaller;
+
+    @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY)
+    void addInstallListener(InstallableListener listener) {
+        this.installListeners.add(listener);
+    }
+    void removeInstallListener(InstallableListener listener) {
+        this.installListeners.remove(listener);
+    }
+
+    @Reference(cardinality = ReferenceCardinality.OPTIONAL, policyOption = ReferencePolicyOption.GREEDY)
+    LogService log;
+
+    @Activate
+    void activate(BundleContext context) {
+        this.context = context;
+        context.addFrameworkListener(this.frameworkListener);
+        context.addBundleListener(this.bundleListener);
+
+        this.processorThread = new Thread(() -> {
+            this.log.log(LogService.LOG_INFO, "Deployment Installer thread starting");
+            debug("Deployment Install thread starting");
+            while (!Thread.interrupted()) {
+                try {
+                    Runnable job = waitForJob();
+                    job.run();
+                } catch (InterruptedException e) {
+                    debug("Deployment Install thread interrupted");
+                    this.log.log(LogService.LOG_INFO, "Deployment Installer thread interrupted");
+                    break;
+                } catch (Exception e) {
+                    this.log.log(LogService.LOG_ERROR, "Error processing job on Deployment Installer thread", e);
+                }
+            }
+            debug("Deployment Install thread terminated.");
+            this.log.log(LogService.LOG_INFO, "Deployment Installer thread terminated");
+        });
+        this.processorThread.start();
+    }
+
+    @Deactivate
+    void deactivate() {
+        this.processorThread.interrupt();
+        this.context.removeBundleListener(this.bundleListener);
+        this.context.removeFrameworkListener(this.frameworkListener);
+    }
+
+    /**
+     * Wait for the next job to do from the two pending queues. Note that we
+     * always favour the install queue, because performing an install/uninstall
+     * will invalidate any completed resolves. Therefore the resolves are only
+     * processed when the install queue is empty.
+     */
+    private Job<?> waitForJob() throws InterruptedException {
+        synchronized (this.processorThread) {
+            while (true) {
+                if (Thread.interrupted()) {
+                    throw new InterruptedException();
+                }
+
+                // Try to get an InstallableUnit to install
+                Job<List<Bundle>> installJob = this.pendingInstalls.poll();
+                if (installJob != null) {
+                    return installJob;
+                }
+
+                // Try to get a File to resolve. An iterator is needed to remove the first entry from the LinkedHashMap.
+                Iterator<Job<InstallableUnitImpl>> resolvesIter = this.pendingResolves.values().iterator();
+                if (resolvesIter.hasNext()) {
+                    Job<InstallableUnitImpl> resolveJob = resolvesIter.next();
+                    resolvesIter.remove();
+                    return resolveJob;
+                }
+
+                // Wait for a notification
+                this.processorThread.wait();
+            }
+        }
+    }
+
+    /**
+     * Put an install job onto the queue
+     */
+    Promise<List<Bundle>> putInstallJob(InstallableUnitImpl unit) {
+        Job<List<Bundle>> job = new Job<>(() -> installArtifacts(unit));
+        synchronized (this.processorThread) {
+            this.pendingInstalls.add(job);
+            this.processorThread.notifyAll();
+        }
+        return job.getPromise();
+    }
+
+    /**
+     * Put an uninstall job onto the queue
+     */
+    Promise<List<Bundle>> putUninstallJob(InstallableUnitImpl unit) {
+        Job<List<Bundle>> job = new Job<>(() -> uninstallArtifacts(unit));
+        synchronized (this.processorThread) {
+            this.pendingInstalls.add(job);
+            this.processorThread.notifyAll();
+        }
+        return job.getPromise();
+    }
+
+    private List<Bundle> installArtifacts(InstallableUnitImpl unit) {
+        Supplier<List<Bundle>> func = () -> {
+            State oldState = unit.getState();
+            unit.setState(State.INSTALLING);
+            notifyListeners(Collections.singleton(new InstallableUnitEvent(oldState, State.INSTALLING, unit)));
+
+            try {
+                // Install Bundles
+                Collection<Artifact> artifacts = unit.getArtifacts();
+                List<String> locations = artifacts.stream().map(Artifact::getLocation).collect(Collectors.toList());
+                List<Bundle> installed = this.frameworkInstaller.addLocations(unit, locations);
+
+                // Start bundles
+                for (Bundle bundle : installed) {
+                    if (!isFragment(bundle)) {
+                        bundle.start();
+                    }
+                }
+
+                // Mark unit installed
+                oldState = unit.getState();
+                unit.setState(State.INSTALLED);
+                notifyListeners(Collections.singleton(new InstallableUnitEvent(oldState, State.INSTALLED, unit)));
+
+                return installed;
+            } catch (BundleException | IOException e) {
+                oldState = unit.getState();
+                unit.setState(State.ERROR);
+                unit.setErrorMessage(e.getMessage());
+                if (log != null) {
+                    log.log(LogService.LOG_ERROR, "Error installing artifact(s)", e);
+                }
+                notifyListeners(Collections.singleton(new InstallableUnitEvent(oldState, State.ERROR, unit)));
+                return Collections.emptyList();
+            }
+        };
+        return withLock(this.unitsLock.writeLock(), func);
+    }
+
+    private List<Bundle> uninstallArtifacts(InstallableUnitImpl unit) {
+        List<Bundle> bundles = this.frameworkInstaller.removeSponsor(unit);
+
+        // Mark unit uninstalled
+        withLock(this.unitsLock.writeLock(), () -> {
+            State oldState = unit.getState();
+            if (unit.setState(State.REMOVED)) {
+                notifyListeners(Collections.singleton(new InstallableUnitEvent(oldState, State.REMOVED, unit)));
+            }
+        });
+
+        return bundles;
+    }
+
+    /**
+     * Put a resolve job onto the queue. The resolve queue is a little
+     * different: because there is no need to resolve the same file twice, we
+     * check whether there's already a pending resolve job for the given file.
+     * If so, just return the same promise.
+     */
+    private Promise<InstallableUnitImpl> putResolveJob(File file) {
+        Promise<InstallableUnitImpl> promise;
+
+        Job<InstallableUnitImpl> newJob = new Job<>(() -> {
+            InstallableUnitImpl unit = performResolve(file);
+            replaceUnit(file, null, unit);
+            return unit;
+        });
+        synchronized (this.processorThread) {
+            Job<InstallableUnitImpl> existing = this.pendingResolves.putIfAbsent(file, newJob);
+            if (existing != null) {
+                // no need to unblock the processor thread because we haven't added anything
+                debug("Not adding resolve job for %s as it is already on the queue", file);
+                promise = existing.getPromise();
+            } else {
+                debug("Added resolve job for %s", file);
+                promise = newJob.getPromise();
+                this.processorThread.notifyAll();
+            }
+        }
+        return promise;
+    }
+
+    private InstallableUnitImpl performResolve(File file) {
+        debug("Starting resolve for file: %s", file);
+        ResolveRequest request;
+        try {
+            request = analyseFile(file);
+        } catch (Exception e) {
+            debug("Failed to analyse file %s: %s", file, e.getMessage());
+            return newFailedUnit(file, file.getName(), file.getName(), "UNKNOWN_VERSION",  String.format("Error reading archive file %s: %s", file.getAbsolutePath(), e.getMessage()));
+        }
+
+        ResolveResult result;
+        try {
+            result = this.resolver.resolve(request);
+        } catch (Exception e) {
+            debug("Failed to resolve file %s: %s", file, e.getMessage());
+            return newFailedUnit(file, request.getName(), request.getSymbolicName(), request.getVersion(),  "Resolution failed: " + e.getMessage());
+
+        }
+
+        List<Artifact> artifacts = new ArrayList<>(result.getResources().size());
+        for (Entry<Resource, String> resourceEntry : result.getResources().entrySet()) {
+            Capability idCap = getIdentityCapability(resourceEntry.getKey());
+            ArtifactImpl artifact = new ArtifactImpl(getIdentity(idCap), getVersion(idCap), resourceEntry.getValue(), getContentHash(resourceEntry.getKey()));
+            artifacts.add(artifact);
+        }
+        debug("Sucessful resolve for file %s: Deployment-Name=%s, Deployment-SymbolicName=%s, Deployment-Version= %s", file, request.getName(), request.getSymbolicName(), request.getVersion());
+        return newResolvedUnit(file, request.getName(), request.getSymbolicName(), request.getVersion(), artifacts);
+        
+    }
+
+    private InstallableUnitImpl newResolvedUnit(File file, String name, String symbolicName, String version, List<Artifact> artifacts) {
+        InstallableUnitImpl newUnit = new InstallableUnitImpl(this, file, name, symbolicName, version,  artifacts);
+        newUnit.setState(State.RESOLVED);
+        return newUnit;
+    }
+
+    private InstallableUnitImpl newFailedUnit(File file, String name, String symbolicName, String version, String message) {
+        InstallableUnitImpl newUnit = new InstallableUnitImpl(this, file, name, symbolicName, version, Collections.emptyList());
+        newUnit.setState(State.ERROR);
+        newUnit.setErrorMessage(message);
+        return newUnit;
+    }
+
+    @Override
+    public boolean canHandle(File file) {
+        String fileName = file.getName();
+        if (!fileName.toLowerCase().endsWith(Constants.ARTIFACT_EXTENSION.toLowerCase())) {
+            log(LogService.LOG_DEBUG, null, "Ignoring %s, does not end with '%s' extension", file.getAbsolutePath(), Constants.ARTIFACT_EXTENSION);
+            return false;
+        }
+
+        // Check if it's a valid ZIP
+        try {
+            if (!isZipFile(file)) {
+                log(LogService.LOG_WARNING, null, "Not a valid ZIP file, ignoring file: %s", file.getAbsolutePath());
+                return false;
+            }
+        } catch (IOException e) {
+            log(LogService.LOG_ERROR, e, "Failed to check ZIP header on %s", file.getAbsolutePath());
+            return false;
+        }
+
+        // Read the requires header and index
+        try (JarFile jar = new JarFile(file, true)) {
+            Manifest manifest = jar.getManifest();
+            if (manifest == null) {
+                log(LogService.LOG_WARNING, null, "Not a valid bundle archive: no META-INF/MANIFEST.MF in %s", file.getAbsolutePath());
+                return false;
+            }
+            String requireBundleStr = manifest.getMainAttributes().getValue(org.osgi.framework.Constants.REQUIRE_BUNDLE);
+            String requireCapsStr = manifest.getMainAttributes().getValue(org.osgi.framework.Constants.REQUIRE_CAPABILITY);
+            if (requireBundleStr == null && requireCapsStr == null) {
+                log(LogService.LOG_WARNING, null, "Not a valid bundle archive: missing %s or %s header in manifest in %s", org.osgi.framework.Constants.REQUIRE_BUNDLE, org.osgi.framework.Constants.REQUIRE_CAPABILITY, file.getAbsolutePath());
+                return false;
+            }
+
+            JarEntry indexEntry = findEntry(jar, Constants.INDEX_FILE, Constants.DEFAULT_INDEX_FILE);
+            if (indexEntry == null) {
+                log(LogService.LOG_WARNING, null, "Not a valid bundle archive: no index entry found in %s", file.getAbsolutePath());
+                return false;
+            }
+
+            // Everything seems present and correct
+            log(LogService.LOG_INFO, null, "Detected valid bundle archive in file %s", file.getAbsolutePath());
+            return true;
+        } catch (IOException e) {
+            log(LogService.LOG_ERROR, e, "Failed to check PKZIP header for candidate archive file %s", file.getAbsolutePath());
+            return false;
+        }
+    }
+
+    private ResolveRequest analyseFile(File file) throws IOException {
+        log(LogService.LOG_INFO, null, "Resolving bundle archive: %s", file.getAbsolutePath());
+
+        String fileUriStr = file.toURI().toString();
+        String indexUriStr;
+        String name;
+        String symbolicName;
+        String version = "";
+
+        List<Requirement> requirements = new LinkedList<>();
+        try (JarFile jar = new JarFile(file)) {
+            Attributes manifestAttribs = jar.getManifest().getMainAttributes();
+
+            symbolicName = manifestAttribs.getValue(Constants.DEPLOYMENT_SYMBOLIC_NAME);
+            if (symbolicName == null) {
+                symbolicName = file.getName();
+            }
+
+            name = manifestAttribs.getValue(Constants.DEPLOYMENT_NAME);
+            if (name == null) {
+                name = symbolicName;
+            }
+
+            version = manifestAttribs.getValue(Constants.DEPLOYMENT_VERSION);
+            if (version == null) {
+                version = UNKNOWN_DEPLOYMENT_VERSION;
+            }
+            requirements.addAll(RequirementParser.parseRequireBundle(manifestAttribs.getValue(org.osgi.framework.Constants.REQUIRE_BUNDLE)));
+            requirements.addAll(RequirementParser.parseRequireCapability(manifestAttribs.getValue(org.osgi.framework.Constants.REQUIRE_CAPABILITY)));
+            if (requirements.isEmpty()) {
+                throw new IllegalArgumentException(String.format("Missing %s or %s header in manifest in %s", org.osgi.framework.Constants.REQUIRE_BUNDLE, org.osgi.framework.Constants.REQUIRE_CAPABILITY, file.getAbsolutePath()));
+            }
+
+            JarEntry indexEntry = findEntry(jar, Constants.INDEX_FILE, Constants.DEFAULT_INDEX_FILE);
+            if (indexEntry == null) {
+                throw new IllegalArgumentException("Missing index entry in " + file.getAbsolutePath());
+            }
+            indexUriStr = "jar:" + fileUriStr + "!/" + indexEntry.getName();
+        }
+
+        try {
+            ResolveRequest request = new ResolveRequest(name, symbolicName, version,
+                    Collections.singletonList(new URI(indexUriStr)), requirements);
+
+            return request;
+        } catch (URISyntaxException e) {
+            throw new IOException("Unable to convert index URI " + indexUriStr, e);
+        }
+    }
+
+    @Override
+    public void install(File file) {
+        log(LogService.LOG_INFO, null, "Installing bundle archive: %s", file.getAbsolutePath());
+        putResolveJob(file);
+    }
+    
+    private static String getIdentity(Capability identityCap) {
+        Object idObj = identityCap.getAttributes().get(IdentityNamespace.IDENTITY_NAMESPACE);
+        if (!(idObj instanceof String)) {
+            throw new IllegalArgumentException("Missing identity capability on resource, or incorrect type");
+        }
+        
+        return (String) idObj;
+    }
+    
+    private static String getVersion(Capability identityCap) {
+        Object versionObj = identityCap.getAttributes().get(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE);
+        if (versionObj == null) {
+            return Version.emptyVersion.toString();
+        }
+        if (versionObj instanceof Version) {
+            return ((Version) versionObj).toString();
+        }
+        if (versionObj instanceof String) {
+            return Version.parseVersion((String) versionObj).toString();
+        }
+        throw new IllegalArgumentException("Incorrect type on identity version");
+    }
+
+    private static Capability getIdentityCapability(Resource resource) {
+        List<Capability> caps = resource.getCapabilities(IdentityNamespace.IDENTITY_NAMESPACE);
+        if (caps == null || caps.isEmpty()) {
+            throw new IllegalArgumentException("Missing identity capability on resource");
+        }
+        return caps.get(0);
+    }
+
+    @Override
+    public void uninstall(File file) throws Exception {
+        log(LogService.LOG_INFO, null, "Uninstalling bundle archive: %s", file.getAbsolutePath());
+        unsponsorUnit(file);
+    }
+
+    @Override
+    public void update(File file) throws Exception {
+        log(LogService.LOG_INFO, null, "Uninstalling bundle archive: %s", file.getAbsolutePath());
+        unsponsorUnit(file).flatMap(lb -> {
+            return putResolveJob(file);
+        });
+    }
+
+    @Override
+    public Collection<InstallableUnit> getInstallableUnits() {
+        return withLock(this.unitsLock.readLock(), ArrayList<InstallableUnit>::new, this.units.values());
+    }
+
+    /**
+     * Unsponsor the installable unit. Returns a promise which is resolved when the uninstall is completed.
+     */
+    private Promise<List<Bundle>> unsponsorUnit(File file) {
+        List<InstallableUnitEvent> events = new LinkedList<>();
+
+        Promise<List<Bundle>> bundles = withLock(this.unitsLock.writeLock(), () -> {
+            Promise<List<Bundle>> promise = Promises.resolved(Collections.emptyList());
+            InstallableUnitImpl existing = this.units.remove(file);
+            if (existing != null) {
+                State origState = existing.getState();
+                if (origState.equals(State.INSTALLED)) {
+                    promise = putUninstallJob(existing);
+                }
+
+                if (existing.setState(State.REMOVED)) {
+                    events.add(new InstallableUnitEvent(origState, State.REMOVED, existing));
+                }
+            }
+            return promise;
+        });
+
+        notifyListeners(events);
+        return bundles;
+    }
+
+    private void invalidateAllUnits(String reason) {
+        List<File> affectedFiles = new LinkedList<>();
+        List<InstallableUnitEvent> events = new LinkedList<>();
+        withLock(this.unitsLock.writeLock(), () -> {
+            for (Entry<File, InstallableUnitImpl> entry : this.units.entrySet()) {
+                InstallableUnitImpl unit = entry.getValue();
+                State currentState = unit.getState();
+                if (EnumSet.of(State.RESOLVED, State.ERROR).contains(currentState)) {
+                    unit.setState(State.REMOVED);
+                    affectedFiles.add(entry.getKey());
+                    events.add(new InstallableUnitEvent(currentState, State.REMOVED, unit));
+                    debug("invalidated %s because: %s%n", entry.getKey(), reason);
+                }
+            }
+        });
+        notifyListeners(events);
+
+        // Schedule re-resolution of the affected files.
+        for (File file : affectedFiles) {
+            putResolveJob(file);
+        }
+    }
+
+    private void replaceUnit(File file, State origState, InstallableUnitImpl newUnit) {
+        Collection<InstallableUnitEvent> events = new ArrayList<>(2);
+        events.add(new InstallableUnitEvent(origState, newUnit.getState(), newUnit));
+        events.addAll(withLock(this.unitsLock.writeLock(), () -> {
+            InstallableUnitImpl existing = this.units.put(file, newUnit);
+            if (existing != null) {
+                State oldState = existing.getState();
+                if (existing.setState(State.REMOVED)) {
+                    return Collections.singleton(new InstallableUnitEvent(oldState, State.REMOVED, existing));
+                }
+            }
+            return Collections.emptyList();
+        }));
+        notifyListeners(events);
+    }
+
+    private void notifyListeners(Collection<InstallableUnitEvent> events) {
+        if (events == null || events.isEmpty()) {
+            return;
+        }
+        for (InstallableListener listener : this.installListeners) {
+            try {
+                listener.installableUnitsChanged(events);
+            } catch (Exception e) {
+                log(LogService.LOG_ERROR, e, "Error dispatching installable unit event to registered listener");
+            }
+        }
+    }
+
+    private void log(int level, Throwable t, String format, Object... args) {
+        if (this.log != null) {
+            this.log.log(level, String.format(format, args), t);
+        }
+    }
+
+    private static boolean isZipFile(File file) throws IOException {
+        byte[] tmp = new byte[4];
+
+        try (InputStream in = new FileInputStream(file)) {
+            int bytesRead = in.read(tmp, 0, 4);
+            if (bytesRead < 4) {
+                throw new IOException("Insufficient bytes read for ZIP header test: possibly truncated file?");
+            }
+        }
+
+        return Arrays.equals(tmp, ZIP_FILE_MAGIC);
+    }
+
+    private static JarEntry findEntry(JarFile jar, String headerName, String defaultPath) throws IOException {
+        String path = defaultPath;
+        String headerPath = jar.getManifest().getMainAttributes().getValue(headerName);
+        if (headerPath != null) {
+            path = headerPath.trim();
+        }
+
+        if (path == null) {
+            // Could happen if no default and no header
+            return null;
+        }
+
+        return jar.getJarEntry(path);
+    }
+
+    private static boolean isFragment(Bundle bundle) {
+        return (bundle.adapt(BundleRevision.class).getTypes() & BundleRevision.TYPE_FRAGMENT) > 0;
+    }
+
+    private static String bundleEventToString(int type) {
+        String s;
+        switch (type) {
+        case BundleEvent.INSTALLED:
+            s = "INSTALLED";
+            break;
+        case BundleEvent.RESOLVED:
+            s = "RESOLVED";
+            break;
+        case BundleEvent.STARTING:
+            s = "STARTING";
+            break;
+        case BundleEvent.STARTED:
+            s = "STARTED";
+            break;
+        case BundleEvent.STOPPING:
+            s = "STOPPING";
+            break;
+        case BundleEvent.STOPPED:
+            s = "STOPPED";
+            break;
+        case BundleEvent.UNRESOLVED:
+            s = "UNRESOLVED";
+            break;
+        case BundleEvent.UPDATED:
+            s = "UPDATED";
+            break;
+        case BundleEvent.UNINSTALLED:
+            s = "UNINSTALLED";
+            break;
+        case BundleEvent.LAZY_ACTIVATION:
+            s = "LAZY_ACTIVATION";
+            break;
+        default:
+            s = "UNKNOWN";
+        }
+        return s;
+    }
+
+    private static Hash getContentHash(Resource resource) throws IllegalArgumentException {
+        Hash result = null;
+
+        List<Capability> contentCaps = resource.getCapabilities(ContentNamespace.CONTENT_NAMESPACE);
+        if (contentCaps != null) {
+            for (Capability contentCap : contentCaps) {
+                String hashHexStr = (String) contentCap.getAttributes().get(ContentNamespace.CONTENT_NAMESPACE);
+                byte[] hashHex = Hex.toByteArray(hashHexStr);
+                Hash hash = new Hash(SHA256, hashHex);
+
+                if (result != null && !hash.equals(result)) {
+                    throw new IllegalArgumentException("Resource '" + resource + "' has multiple inconsistent content hashes.");
+                }
+                result = hash;
+            }
+        }
+
+        return result;
+    }
+
+    private void debug(String format, Object... args) {
+        if (DEBUG) {
+            System.out.printf("DEBUG: " + format + "%n", args);
+        }
+    }
+
+
+}

Added: felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/FrameworkInstallerComponent.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/FrameworkInstallerComponent.java?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/FrameworkInstallerComponent.java (added)
+++ felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/FrameworkInstallerComponent.java Thu Mar  8 14:57:57 2018
@@ -0,0 +1,144 @@
+/*******************************************************************************
+ * Copyright (c) Intel Corporation
+ * Copyright (c) 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.apache.felix.fileinstall.plugins.installer.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.apache.felix.fileinstall.plugins.installer.FrameworkInstaller;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.log.LogService;
+
+@Component
+public class FrameworkInstallerComponent implements FrameworkInstaller {
+
+	private final Map<Long, Set<Object>> bundleSponsors = new HashMap<>();
+
+	@Reference(cardinality = ReferenceCardinality.OPTIONAL)
+	private LogService log;
+
+	private BundleContext context;
+
+	@Activate
+	void activate(BundleContext context) {
+		this.context = context;
+	}
+
+	@Override
+	public synchronized List<Bundle> addLocations(Object sponsor, List<String> bundleLocations) throws BundleException, IOException {
+		List<Bundle> installed = new ArrayList<>(bundleLocations.size());
+
+		for (String location : bundleLocations) {
+			// Find an existing bundle with that location.
+			Bundle existing = null;
+			for (Bundle bundle : this.context.getBundles()) {
+				if (bundle.getLocation().equals(location)) {
+					existing = bundle;
+
+					// If the existing bundle was previously installed by us then add to the sponsors.
+					Set<Object> sponsors = this.bundleSponsors.get(bundle.getBundleId());
+					if (sponsors != null) {
+                        sponsors.add(sponsor);
+                    }
+					break;
+				}
+			}
+
+			if (existing == null) {
+				// No existing bundle with that location. Install it and add the sponsor.
+				try {
+					// Ensure that the URLConnection doesn't cache. Mostly useful for JarURLConnection,
+					// which leaks file handles if this is not done.
+					URI locationUri = new URI(location);
+					URLConnection connection = locationUri.toURL().openConnection();
+					connection.setUseCaches(false);
+					try (InputStream stream = connection.getInputStream()) {
+						Bundle bundle = this.context.installBundle(location, stream);
+						installed.add(bundle);
+
+						Set<Object> sponsors = new HashSet<>();
+						sponsors.add(sponsor);
+						this.bundleSponsors.put(bundle.getBundleId(), sponsors);
+					} catch (BundleException e) {
+					    if (e.getType() == BundleException.DUPLICATE_BUNDLE_ERROR) {
+					        log.log(LogService.LOG_WARNING, "Duplicate bundle symbolic-name/version in install for location " + location, e);
+					    } else {
+					        throw e;
+					    }
+					}
+				} catch (URISyntaxException e) {
+					throw new IOException("Invalid bundle location URI: " + location, e);
+				}
+			}
+		}
+
+		return installed;
+	}
+
+	@Override
+	public synchronized List<Bundle> removeSponsor(Object sponsor) {
+		List<Long> toRemove = new LinkedList<>();
+
+		for (Iterator<Entry<Long, Set<Object>>> iter = this.bundleSponsors.entrySet().iterator(); iter.hasNext(); ) {
+			Entry<Long, Set<Object>> entry = iter.next();
+			long bundleId = entry.getKey();
+			Set<Object> sponsors = entry.getValue();
+
+			if (sponsors.remove(sponsor) && sponsors.isEmpty()) {
+				// We removed our sponsor and the sponsor set is now empty => this bundle should be removed.
+				toRemove.add(bundleId);
+				// Also remove the entry from the sponsor map.
+				iter.remove();
+			}
+		}
+
+		List<Bundle> removed = new LinkedList<>();
+		for (long bundleId : toRemove) {
+			Bundle bundle = this.context.getBundle(bundleId);
+			if (bundle != null) { // just in case the bundle was already removed by somebody else
+				try {
+					bundle.uninstall();
+					removed.add(bundle);
+				} catch (BundleException e) {
+					if (this.log != null) {
+                        this.log.log(LogService.LOG_ERROR, "Error uninstalling bundle: " + bundle.getSymbolicName(), e);
+                    }
+				}
+			}
+		}
+		return removed;
+	}
+
+}

Added: felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/InstallableUnitImpl.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/InstallableUnitImpl.java?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/InstallableUnitImpl.java (added)
+++ felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/InstallableUnitImpl.java Thu Mar  8 14:57:57 2018
@@ -0,0 +1,116 @@
+/*******************************************************************************
+ * Copyright (c) Intel Corporation
+ * Copyright (c) 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.apache.felix.fileinstall.plugins.installer.impl;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.felix.fileinstall.plugins.installer.Artifact;
+import org.apache.felix.fileinstall.plugins.installer.InstallableUnit;
+import org.apache.felix.fileinstall.plugins.installer.State;
+import org.osgi.framework.Bundle;
+import org.osgi.util.promise.Promise;
+
+class InstallableUnitImpl implements InstallableUnit {
+
+    private final DeploymentInstaller installer;
+    private final File originFile;
+    private final String name;
+    private final String symbolicName;
+    private final String version;
+    private final Collection<Artifact> artifacts;
+
+    private State state;
+    private String errorMessage = null;
+
+    InstallableUnitImpl(DeploymentInstaller installer, File originFile, String name, String symbolicName, String version, Collection<Artifact> artifacts) {
+        this.installer = installer;
+        this.originFile = originFile;
+        this.name = name;
+        this.symbolicName = symbolicName;
+        this.artifacts = artifacts;
+        this.version = version;
+    }
+
+    @Override
+    public File getOrigin() {
+        return this.originFile;
+    }
+
+    @Override
+    public String getName() {
+        return this.name;
+    }
+
+    @Override
+    public String getSymbolicName() {
+        return this.symbolicName;
+    }
+
+    @Override
+    public Collection<Artifact> getArtifacts() {
+        return Collections.unmodifiableCollection(this.artifacts);
+    }
+
+    @Override
+    public State getState() {
+        return this.state;
+    }
+
+    @Override
+    public Promise<List<Bundle>> install() {
+        if (!this.state.equals(State.RESOLVED)) {
+            throw new IllegalStateException(String.format("Cannot install unit %s: not in the %s state", this.symbolicName, State.RESOLVED));
+        }
+        return this.installer.putInstallJob(this);
+    }
+
+    @Override
+    public Promise<List<Bundle>> uninstall() {
+        return this.installer.putUninstallJob(this);
+    }
+
+    /**
+     * Set the state, return true if the state changed.
+     */
+    boolean setState(State state) {
+        if (state == null) {
+            throw new IllegalArgumentException("null state not permitted!");
+        }
+        State oldState = this.state;
+
+        this.state = state;
+        return !this.state.equals(oldState);
+    }
+
+    void setErrorMessage(String message) {
+        this.errorMessage = message;
+    }
+
+    @Override
+    public String getErrorMessage() {
+        return this.errorMessage;
+    }
+
+    @Override
+    public String getVersion() {
+        return this.version;
+    }
+
+}

Added: felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/Job.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/Job.java?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/Job.java (added)
+++ felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/Job.java Thu Mar  8 14:57:57 2018
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * Copyright (c) Intel Corporation
+ * Copyright (c) 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.apache.felix.fileinstall.plugins.installer.impl;
+
+import java.util.concurrent.Callable;
+
+import org.osgi.util.promise.Deferred;
+import org.osgi.util.promise.Promise;
+
+class Job<T> implements Runnable {
+
+	private final Deferred<T> deferred = new Deferred<>();
+	private final Callable<T> call;
+
+	Job(Callable<T> call) {
+		this.call = call;
+	}
+
+	@Override
+	public void run() {
+		// Avoid repeating the work
+		if (this.deferred.getPromise().isDone()) {
+            return;
+        }
+
+		try {
+			T result = this.call.call();
+			this.deferred.resolve(result);
+		} catch (Exception e) {
+			this.deferred.fail(e);
+		}
+	}
+
+	public Promise<T> getPromise() {
+		return this.deferred.getPromise();
+	}
+
+
+}

Added: felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/Locking.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/Locking.java?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/Locking.java (added)
+++ felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/Locking.java Thu Mar  8 14:57:57 2018
@@ -0,0 +1,52 @@
+/*******************************************************************************
+ * Copyright (c) Intel Corporation
+ * Copyright (c) 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.apache.felix.fileinstall.plugins.installer.impl;
+
+import java.util.concurrent.locks.Lock;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+final class Locking {
+
+	public static void withLock(Lock lock, Runnable cmd) {
+		lock.lock();
+		try {
+			cmd.run();
+		} finally {
+			lock.unlock();
+		}
+	}
+
+	public static <R> R withLock(Lock lock, Supplier<R> fun) {
+		lock.lock();
+		try {
+			return fun.get();
+		} finally {
+			lock.unlock();
+		}
+	}
+
+	public static <T, R> R withLock(Lock lock, Function<T, R> fun, T in) {
+		lock.lock();
+		try {
+			return fun.apply(in);
+		} finally {
+			lock.unlock();
+		}
+	}
+
+}

Added: felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/RequirementParser.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/RequirementParser.java?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/RequirementParser.java (added)
+++ felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/RequirementParser.java Thu Mar  8 14:57:57 2018
@@ -0,0 +1,130 @@
+/*******************************************************************************
+ * Copyright (c) Intel Corporation
+ * Copyright (c) 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.apache.felix.fileinstall.plugins.installer.impl;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.felix.utils.manifest.Attribute;
+import org.apache.felix.utils.manifest.Clause;
+import org.apache.felix.utils.manifest.Directive;
+import org.apache.felix.utils.manifest.Parser;
+import org.osgi.framework.VersionRange;
+import org.osgi.framework.namespace.BundleNamespace;
+import org.osgi.resource.Namespace;
+import org.osgi.resource.Requirement;
+
+import aQute.bnd.osgi.resource.RequirementBuilder;
+
+class RequirementParser {
+
+    static List<Requirement> parseRequireBundle(String header) throws IllegalArgumentException {
+        if (header == null) {
+            return Collections.emptyList();
+        }
+
+        Clause[] clauses = Parser.parseHeader(header);
+        List<Requirement> requirements = new ArrayList<>(clauses.length);
+        for (Clause requireClause : clauses) {
+            String bsn = requireClause.getName();
+            String versionRangeStr = requireClause.getAttribute(org.osgi.framework.Constants.BUNDLE_VERSION_ATTRIBUTE);
+
+            String filter = toBundleFilter(bsn, versionRangeStr);
+            Requirement requirement = new RequirementBuilder(BundleNamespace.BUNDLE_NAMESPACE)
+                    .addDirective(Namespace.REQUIREMENT_FILTER_DIRECTIVE, filter).buildSyntheticRequirement();
+            requirements.add(requirement);
+        }
+        return requirements;
+    }
+
+    static List<Requirement> parseRequireCapability(String header) throws IllegalArgumentException {
+        if (header == null) {
+            return Collections.emptyList();
+        }
+
+        Clause[] clauses = Parser.parseHeader(header);
+        List<Requirement> reqs = new ArrayList<>(clauses.length);
+        for (Clause clause : clauses) {
+            String namespace = clause.getName();
+
+            RequirementBuilder reqBuilder = new RequirementBuilder(namespace);
+            for (Attribute attrib : clause.getAttributes()) {
+                try {
+                    reqBuilder.addAttribute(attrib.getName(), attrib.getValue());
+                } catch (Exception e) {
+                    throw new IllegalArgumentException("Invalid requirement attribute", e);
+                }
+            }
+            for (Directive directive : clause.getDirectives()) {
+                reqBuilder.addDirective(directive.getName(), directive.getValue());
+            }
+
+            reqs.add(reqBuilder.buildSyntheticRequirement());
+        }
+
+        return reqs;
+    }
+
+    private static String toBundleFilter(String bsn, String versionRangeStr) {
+        final String filterStr;
+
+        String bsnFilter = String.format("(%s=%s)", BundleNamespace.BUNDLE_NAMESPACE, bsn);
+
+        if (versionRangeStr == null) {
+            filterStr = bsnFilter;
+        } else {
+            VersionRange versionRange = new VersionRange(versionRangeStr);
+            if (versionRange.isExact()) {
+                String exactVersionFilter = String.format("(%s=%s)",
+                        BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE, versionRange.getLeft());
+                filterStr = String.format("(&%s%s)", bsnFilter, exactVersionFilter);
+            } else if (versionRange.getRight() == null) {
+                filterStr = String.format("(&%s%s)", bsnFilter, lowerVersionFilter(versionRange));
+            } else {
+                filterStr = String.format("(&%s%s%s)", bsnFilter, lowerVersionFilter(versionRange),
+                        upperVersionFilter(versionRange));
+            }
+        }
+        return filterStr;
+    }
+
+    private static String upperVersionFilter(VersionRange versionRange) {
+        String upperVersionFilter;
+        if (versionRange.getRightType() == VersionRange.RIGHT_CLOSED) {
+            upperVersionFilter = String.format("(%s<=%s)", BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE,
+                    versionRange.getRight());
+        } else {
+            upperVersionFilter = String.format("(!(%s>=%s))", BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE,
+                    versionRange.getRight());
+        }
+        return upperVersionFilter;
+    }
+
+    private static String lowerVersionFilter(VersionRange versionRange) {
+        String lowerVersionFilter;
+        if (versionRange.getLeftType() == VersionRange.LEFT_CLOSED) {
+            lowerVersionFilter = String.format("(%s>=%s)", BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE,
+                    versionRange.getLeft());
+        } else {
+            lowerVersionFilter = String.format("(!(%s<=%s))", BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE,
+                    versionRange.getLeft());
+        }
+        return lowerVersionFilter;
+    }
+
+}

Added: felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/package-info.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/package-info.java?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/package-info.java (added)
+++ felix/trunk/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/package-info.java Thu Mar  8 14:57:57 2018
@@ -0,0 +1,18 @@
+/*******************************************************************************
+ * Copyright (c) Intel Corporation
+ * Copyright (c) 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+@org.osgi.annotation.versioning.Version("1.0.0")
+package org.apache.felix.fileinstall.plugins.installer;

Added: felix/trunk/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/CompositeBundleInstallerTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/CompositeBundleInstallerTest.java?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/CompositeBundleInstallerTest.java (added)
+++ felix/trunk/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/CompositeBundleInstallerTest.java Thu Mar  8 14:57:57 2018
@@ -0,0 +1,107 @@
+/*******************************************************************************
+ * Copyright (c) Intel Corporation
+ * Copyright (c) 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.apache.felix.fileinstall.plugins.installer.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+
+import org.apache.felix.fileinstall.plugins.installer.impl.DeploymentInstaller;
+import org.junit.Before;
+import org.junit.Test;
+
+public class CompositeBundleInstallerTest {
+
+	private TestLogService log;
+	private DeploymentInstaller installer;
+	
+	@Before
+	public void setup() {
+		log = new TestLogService();
+		installer = new DeploymentInstaller();
+		installer.log = log;
+	}
+
+	@Test
+	public void testValidArchiveCanBeHandled() {
+		File jar = new File("target/test-classes/valid1.bar");
+
+		assertTrue("test archive not found", jar.isFile());
+		assertTrue("test archive not handled", installer.canHandle(jar));
+		
+		assertEquals(1, log.logged.size());
+		assertTrue("missing info message", log.logged.get(0).startsWith("INFO: Detected valid bundle archive"));
+	}
+	
+	@Test
+	public void testIndexPathInManifest() {
+		File jar = new File("target/test-classes/valid2.bar");
+
+		assertTrue("test archive not found", jar.isFile());
+		assertTrue("test archive not handled", installer.canHandle(jar));
+
+		assertEquals(1, log.logged.size());
+		assertTrue("missing info message", log.logged.get(0).startsWith("INFO: Detected valid bundle archive"));
+	}
+	
+	@Test
+	public void testInvalidExtension() {
+		File jar = new File("target/test-classes/invalid-wrong-extension.xxx");
+
+		assertTrue("test archive not found", jar.isFile());
+		assertFalse("invalid test archive should not be handled", installer.canHandle(jar));
+
+		assertEquals(1, log.logged.size());
+		assertTrue("missing debug message", log.logged.get(0).startsWith("DEBUG: Ignoring"));
+	}
+
+	@Test
+	public void testMissingRequireHeader() {
+		File jar = new File("target/test-classes/invalid-missing-requires.bar");
+
+		assertTrue("test archive not found", jar.isFile());
+		assertFalse("invalid test archive should not be handled", installer.canHandle(jar));
+
+		assertEquals(1, log.logged.size());
+		assertTrue("missing warning message", log.logged.get(0).startsWith("WARNING: Not a valid bundle archive"));
+	}
+
+	@Test
+	public void testNoIndex() {
+		File jar = new File("target/test-classes/invalid-no-index.bar");
+
+		assertTrue("test archive not found", jar.isFile());
+		assertFalse("invalid test archive should not be handled", installer.canHandle(jar));
+
+		assertEquals(1, log.logged.size());
+		assertTrue("missing warning message", log.logged.get(0).startsWith("WARNING: Not a valid bundle archive"));
+	}
+
+	@Test
+	public void testMissingIndexPath() {
+		File jar = new File("target/test-classes/invalid-missing-index-path.bar");
+
+		assertTrue("test archive not found", jar.isFile());
+		assertFalse("invalid test archive should not be handled", installer.canHandle(jar));
+
+		assertEquals(1, log.logged.size());
+		assertTrue("missing warning message", log.logged.get(0).startsWith("WARNING: Not a valid bundle archive"));
+	}
+
+}

Added: felix/trunk/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/RequirementParserTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/RequirementParserTest.java?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/RequirementParserTest.java (added)
+++ felix/trunk/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/RequirementParserTest.java Thu Mar  8 14:57:57 2018
@@ -0,0 +1,49 @@
+/*******************************************************************************
+ * Copyright (c) Intel Corporation
+ * Copyright (c) 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.apache.felix.fileinstall.plugins.installer.impl;
+
+import static org.junit.Assert.*;
+
+import java.util.List;
+
+import org.apache.felix.fileinstall.plugins.installer.impl.RequirementParser;
+import org.junit.Test;
+import org.osgi.resource.Requirement;
+
+public class RequirementParserTest {
+
+	@Test
+	public void testParseRequireBundle() {
+		List<Requirement> actual = RequirementParser.parseRequireBundle("foo;bundle-version=1.0.0, bar;bundle-version=\"[1.0,1.1)\", baz;bundle-version=\"[2.0,3.0)\", fnarg");
+		
+		assertEquals(4, actual.size());
+		assertEquals("(&(osgi.wiring.bundle=foo)(bundle-version>=1.0.0))", actual.get(0).getDirectives().get("filter"));
+		assertEquals("(&(osgi.wiring.bundle=bar)(bundle-version>=1.0.0)(!(bundle-version>=1.1.0)))", actual.get(1).getDirectives().get("filter"));
+		assertEquals("(&(osgi.wiring.bundle=baz)(bundle-version>=2.0.0)(!(bundle-version>=3.0.0)))", actual.get(2).getDirectives().get("filter"));
+		assertEquals("(osgi.wiring.bundle=fnarg)", actual.get(3).getDirectives().get("filter"));
+	}
+
+	@Test
+	public void testParseRequireCapability() {
+		List<Requirement> actual = RequirementParser.parseRequireCapability("osgi.extender; filter:=\"(&(osgi.extender=osgi.ds)(version>=1.0))\"; effective:=active, osgi.service; filter:=\"(objectClass=org.example.Foo)\"");
+		
+		assertEquals(2, actual.size());
+		assertEquals("(&(osgi.extender=osgi.ds)(version>=1.0))", actual.get(0).getDirectives().get("filter"));
+		assertEquals("active", actual.get(0).getDirectives().get("effective"));
+	}
+
+}

Added: felix/trunk/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/TestLogService.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/TestLogService.java?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/TestLogService.java (added)
+++ felix/trunk/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/TestLogService.java Thu Mar  8 14:57:57 2018
@@ -0,0 +1,69 @@
+/*******************************************************************************
+ * Copyright (c) Intel Corporation
+ * Copyright (c) 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.apache.felix.fileinstall.plugins.installer.impl;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.log.LogService;
+
+class TestLogService implements LogService {
+
+	List<String> logged = new LinkedList<>();
+
+	@Override
+	public void log(int level, String message) {
+		log(null, level, message, null);
+	}
+
+	@Override
+	public void log(int level, String message, Throwable exception) {
+		log(null, level, message, exception);
+	}
+
+	@SuppressWarnings("rawtypes")
+    @Override
+	public void log(ServiceReference sr, int level, String message) {
+		log(sr, level, message, null);
+	}
+
+	@SuppressWarnings("rawtypes")
+	@Override
+	public void log(ServiceReference sr, int level, String message, Throwable exception) {
+		String levelStr;
+		switch (level) {
+		case LogService.LOG_DEBUG:
+			levelStr = "DEBUG";
+			break;
+		case LogService.LOG_INFO:
+			levelStr = "INFO";
+			break;
+		case LogService.LOG_WARNING:
+			levelStr = "WARNING";
+			break;
+		case LogService.LOG_ERROR:
+			levelStr = "ERROR";
+			break;
+		default:
+			levelStr = "UNKNOWN";
+		}
+		String formatted = String.format("%s: %s %s", levelStr, message, exception != null ? exception : "");
+		this.logged.add(formatted);
+	}
+
+}

Added: felix/trunk/fileinstall-plugins/installer/src/test/resources/invalid-missing-index-path.bar
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/installer/src/test/resources/invalid-missing-index-path.bar?rev=1826244&view=auto
==============================================================================
Binary files felix/trunk/fileinstall-plugins/installer/src/test/resources/invalid-missing-index-path.bar (added) and felix/trunk/fileinstall-plugins/installer/src/test/resources/invalid-missing-index-path.bar Thu Mar  8 14:57:57 2018 differ

Added: felix/trunk/fileinstall-plugins/installer/src/test/resources/invalid-missing-requires.bar
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/installer/src/test/resources/invalid-missing-requires.bar?rev=1826244&view=auto
==============================================================================
Binary files felix/trunk/fileinstall-plugins/installer/src/test/resources/invalid-missing-requires.bar (added) and felix/trunk/fileinstall-plugins/installer/src/test/resources/invalid-missing-requires.bar Thu Mar  8 14:57:57 2018 differ

Added: felix/trunk/fileinstall-plugins/installer/src/test/resources/invalid-no-index.bar
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/installer/src/test/resources/invalid-no-index.bar?rev=1826244&view=auto
==============================================================================
Binary files felix/trunk/fileinstall-plugins/installer/src/test/resources/invalid-no-index.bar (added) and felix/trunk/fileinstall-plugins/installer/src/test/resources/invalid-no-index.bar Thu Mar  8 14:57:57 2018 differ

Added: felix/trunk/fileinstall-plugins/installer/src/test/resources/invalid-wrong-extension.xxx
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/installer/src/test/resources/invalid-wrong-extension.xxx?rev=1826244&view=auto
==============================================================================
Binary files felix/trunk/fileinstall-plugins/installer/src/test/resources/invalid-wrong-extension.xxx (added) and felix/trunk/fileinstall-plugins/installer/src/test/resources/invalid-wrong-extension.xxx Thu Mar  8 14:57:57 2018 differ

Added: felix/trunk/fileinstall-plugins/installer/src/test/resources/valid1.bar
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/installer/src/test/resources/valid1.bar?rev=1826244&view=auto
==============================================================================
Binary files felix/trunk/fileinstall-plugins/installer/src/test/resources/valid1.bar (added) and felix/trunk/fileinstall-plugins/installer/src/test/resources/valid1.bar Thu Mar  8 14:57:57 2018 differ

Added: felix/trunk/fileinstall-plugins/installer/src/test/resources/valid2.bar
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/installer/src/test/resources/valid2.bar?rev=1826244&view=auto
==============================================================================
Binary files felix/trunk/fileinstall-plugins/installer/src/test/resources/valid2.bar (added) and felix/trunk/fileinstall-plugins/installer/src/test/resources/valid2.bar Thu Mar  8 14:57:57 2018 differ

Added: felix/trunk/fileinstall-plugins/resolver/.gitignore
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/resolver/.gitignore?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/resolver/.gitignore (added)
+++ felix/trunk/fileinstall-plugins/resolver/.gitignore Thu Mar  8 14:57:57 2018
@@ -0,0 +1 @@
+/bin/

Added: felix/trunk/fileinstall-plugins/resolver/bnd.bnd
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/resolver/bnd.bnd?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/resolver/bnd.bnd (added)
+++ felix/trunk/fileinstall-plugins/resolver/bnd.bnd Thu Mar  8 14:57:57 2018
@@ -0,0 +1,4 @@
+-conditionalpackage: net.jcip.annotations
+
+-exportcontents: ${packages;VERSIONED}
+ 
\ No newline at end of file

Added: felix/trunk/fileinstall-plugins/resolver/pom.xml
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/resolver/pom.xml?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/resolver/pom.xml (added)
+++ felix/trunk/fileinstall-plugins/resolver/pom.xml Thu Mar  8 14:57:57 2018
@@ -0,0 +1,84 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>org.apache.felix</groupId>
+		<artifactId>felix-parent</artifactId>
+		<version>3</version>
+		<relativePath>../../pom/pom.xml</relativePath>
+	</parent>
+
+	<artifactId>org.apache.felix.fileinstall.plugins.resolver</artifactId>
+	<name>Apache Felix File Install Resolver Plugin</name>
+	<version>1.0.0-SNAPSHOT</version>
+
+	<scm>
+		<connection>scm:svn:http://svn.apache.org/repos/asf/felix/trunk/fileinstall-plugins/resolver</connection>
+		<developerConnection>scm:svn:https://svn.apache.org/repos/asf/felix/trunk/fileinstall-plugins/resolver</developerConnection>
+		<url>http://svn.apache.org/repos/asf/felix/fileinstall</url>
+	</scm>
+
+	<properties>
+		<felix.java.version>7</felix.java.version>
+	</properties>
+
+	<dependencies>
+		<dependency>
+			<groupId>org.osgi</groupId>
+			<artifactId>osgi.core</artifactId>
+			<version>6.0.0</version>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.osgi</groupId>
+			<artifactId>osgi.cmpn</artifactId>
+			<version>6.0.0</version>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.osgi</groupId>
+			<artifactId>osgi.annotation</artifactId>
+			<version>6.0.1</version>
+		</dependency>
+		<dependency>
+			<groupId>biz.aQute.bnd</groupId>
+			<artifactId>biz.aQute.bndlib</artifactId>
+			<version>3.3.0</version>
+		</dependency>
+		<dependency>
+			<groupId>net.jcip</groupId>
+			<artifactId>jcip-annotations</artifactId>
+			<version>1.0</version>
+		</dependency>
+		<dependency>
+			<groupId>biz.aQute.bnd</groupId>
+			<artifactId>biz.aQute.repository</artifactId>
+			<version>3.4.0</version>
+		</dependency>
+		<dependency>
+			<groupId>junit</groupId>
+			<artifactId>junit</artifactId>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>biz.aQute.bnd</groupId>
+				<artifactId>bnd-maven-plugin</artifactId>
+				<version>3.4.0</version>
+				<executions>
+					<execution>
+						<id>default-bnd-process</id>
+						<goals>
+							<goal>bnd-process</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+
+</project>

Added: felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/PluginResolver.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/PluginResolver.java?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/PluginResolver.java (added)
+++ felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/PluginResolver.java Thu Mar  8 14:57:57 2018
@@ -0,0 +1,31 @@
+/*******************************************************************************
+ * Copyright (c) Intel Corporation
+ * Copyright (c) 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.apache.felix.fileinstall.plugins.resolver;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Service interface for a Plugin Resolver. Takes a request consisting of a list
+ * of index URIs and returns a resolution result, consisting of a list of
+ * resources to be installed.
+ */
+@ProviderType
+public interface PluginResolver {
+
+	ResolveResult resolve(ResolveRequest request) throws Exception;
+
+}

Added: felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/ResolveRequest.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/ResolveRequest.java?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/ResolveRequest.java (added)
+++ felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/ResolveRequest.java Thu Mar  8 14:57:57 2018
@@ -0,0 +1,75 @@
+/*******************************************************************************
+ * Copyright (c) Intel Corporation
+ * Copyright (c) 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.apache.felix.fileinstall.plugins.resolver;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.osgi.resource.Requirement;
+
+/**
+ * Represents a request to resolve certain requirements in the context of the
+ * current OSGi Framework.
+ */
+public class ResolveRequest {
+
+    private final String name;
+    private final String symbolicName;
+    private final String version;
+    private final List<URI> indexes;
+    private final Collection<Requirement> requirements;
+
+    /**
+     * Construct a request.
+     *
+     * @param indexes
+     *            The list of repository indexes to search for installable
+     *            resources.
+     * @param requirements
+     *            The list of requirements to resolve.
+     */
+    public ResolveRequest(String name, String symbolicName, String version, List<URI> indexes, Collection<Requirement> requirements) {
+        this.name = name;
+        this.symbolicName = symbolicName;
+        this.version = version;
+        this.indexes = new ArrayList<>(indexes);
+        this.requirements = new ArrayList<>(requirements);
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
+    public String getSymbolicName() {
+        return this.symbolicName;
+    }
+
+    public String getVersion() {
+        return this.version;
+    }
+
+    public List<URI> getIndexes() {
+        return Collections.unmodifiableList(this.indexes);
+    }
+
+    public Collection<Requirement> getRequirements() {
+        return Collections.unmodifiableCollection(this.requirements);
+    }
+}

Added: felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/ResolveResult.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/ResolveResult.java?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/ResolveResult.java (added)
+++ felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/ResolveResult.java Thu Mar  8 14:57:57 2018
@@ -0,0 +1,37 @@
+/*******************************************************************************
+ * Copyright (c) Intel Corporation
+ * Copyright (c) 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.apache.felix.fileinstall.plugins.resolver;
+
+import java.util.Map;
+
+import org.osgi.resource.Resource;
+
+public interface ResolveResult {
+
+	/**
+	 * Return the request that was satisfied by this result.
+	 */
+	ResolveRequest getRequest();
+
+	/**
+	 * Return the resources to install. Each resource is mapped to a location,
+	 * which is either the location field of an existing installed bundle or the
+	 * physical URL of a location from which the bundle can be installed.
+	 */
+	Map<Resource, String> getResources();
+
+}

Added: felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/BasicRegistry.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/BasicRegistry.java?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/BasicRegistry.java (added)
+++ felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/BasicRegistry.java Thu Mar  8 14:57:57 2018
@@ -0,0 +1,54 @@
+/*******************************************************************************
+ * Copyright (c) Intel Corporation
+ * Copyright (c) 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.apache.felix.fileinstall.plugins.resolver.impl;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import aQute.bnd.service.Registry;
+
+class BasicRegistry implements Registry {
+	
+	private final Map<Class<?>, List<Object>> plugins = new HashMap<>();
+	
+	synchronized <T> BasicRegistry put(Class<T> clazz, T plugin) {
+		List<Object> list = plugins.get(clazz);
+		if (list == null) {
+			list = new LinkedList<>();
+			plugins.put(clazz, list);
+		}
+		list.add(plugin);
+		return this;
+	}
+
+	@Override
+	@SuppressWarnings("unchecked")
+	public synchronized <T> List<T> getPlugins(Class<T> clazz) {
+		List<T> objs = (List<T>) plugins.get(clazz);
+		return objs != null ? Collections.<T>unmodifiableList(objs) : Collections.<T>emptyList();
+	}
+
+	@Override
+	public <T> T getPlugin(Class<T> clazz) {
+		List<T> l = getPlugins(clazz);
+		return (l != null && !l.isEmpty()) ? l.get(0) : null;
+	}
+
+}

Added: felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/CapReqBase.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/CapReqBase.java?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/CapReqBase.java (added)
+++ felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/CapReqBase.java Thu Mar  8 14:57:57 2018
@@ -0,0 +1,55 @@
+/*******************************************************************************
+ * Copyright (c) Intel Corporation
+ * Copyright (c) 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.apache.felix.fileinstall.plugins.resolver.impl;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.osgi.resource.Resource;
+
+public class CapReqBase {
+
+	protected final String namespace;
+	protected final Map<String, String> directives;
+	protected final Map<String, Object> attribs;
+	protected final Resource resource;
+
+	public CapReqBase(String namespace, Map<String,String> directives, Map<String, Object> attribs, Resource resource) {
+		this.namespace = namespace;
+		this.directives = new HashMap<>(directives);
+		this.attribs = new HashMap<>(attribs);
+		this.resource = resource;
+	}
+
+	public String getNamespace() {
+		return namespace;
+	}
+
+	public Map<String, String> getDirectives() {
+		return Collections.unmodifiableMap(directives);
+	}
+
+	public Map<String, Object> getAttributes() {
+		return Collections.unmodifiableMap(attribs);
+	}
+
+	public Resource getResource() {
+		return resource;
+	}
+	
+}

Added: felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/CapabilityImpl.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/CapabilityImpl.java?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/CapabilityImpl.java (added)
+++ felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/CapabilityImpl.java Thu Mar  8 14:57:57 2018
@@ -0,0 +1,59 @@
+/*******************************************************************************
+ * Copyright (c) Intel Corporation
+ * Copyright (c) 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.apache.felix.fileinstall.plugins.resolver.impl;
+
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.osgi.resource.Capability;
+import org.osgi.resource.Resource;
+
+public class CapabilityImpl extends CapReqBase implements Capability {
+
+	public static CapabilityImpl copy(Capability cap, Resource resource) {
+		return new CapabilityImpl(cap.getNamespace(), cap.getDirectives(), cap.getAttributes(), resource);
+	}
+
+	public CapabilityImpl(String namespace, Map<String, String> directives, Map<String, Object> attribs, Resource resource) {
+		super(namespace, directives, attribs, resource);
+	}
+
+	@Override
+	public String toString() {
+		StringBuilder sb = new StringBuilder();
+		sb.append(this.namespace);
+
+		boolean first = true;
+		for (Entry<String,String> directive : this.directives.entrySet()) {
+			if (first) {
+                sb.append(":");
+            }
+			first = false;
+			sb.append(", ").append(directive.getKey()).append(":=").append(directive.getValue());
+		}
+		for (Entry<String,Object> attrib : this.attribs.entrySet()) {
+			if (first) {
+                sb.append(":");
+            }
+			first = false;
+			sb.append(", ").append(attrib.getKey()).append("=").append(attrib.getValue());
+		}
+
+		return sb.toString();
+	}
+
+}

Added: felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/JarURLConnector.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/JarURLConnector.java?rev=1826244&view=auto
==============================================================================
--- felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/JarURLConnector.java (added)
+++ felix/trunk/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/JarURLConnector.java Thu Mar  8 14:57:57 2018
@@ -0,0 +1,49 @@
+/*******************************************************************************
+ * Copyright (c) Intel Corporation
+ * Copyright (c) 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.apache.felix.fileinstall.plugins.resolver.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+
+import aQute.bnd.service.url.TaggedData;
+import aQute.bnd.service.url.URLConnector;
+
+public class JarURLConnector implements URLConnector {
+
+	@Override
+	public TaggedData connectTagged(URL url) throws Exception {
+		URLConnection connection = url.openConnection();
+		if (connection instanceof JarURLConnection) {
+            connection.setUseCaches(false);
+        }
+		return new TaggedData(connection, connection.getInputStream());
+	}
+
+	@Override
+	public InputStream connect(URL url) throws IOException, Exception {
+		return connectTagged(url).getInputStream();
+	}
+
+	@Override
+	public TaggedData connectTagged(URL url, String tag) throws Exception {
+		return connectTagged(url);
+	}
+
+}