changeset 2573:006ac47d1506

Fix Version Checks in Dependency Analysis Fixes the dependency analysis code in Launcher and the Dependency analyzer command - Parses version strings from the OSGI manifest - When building the dependency graph, ensures that the correct versions of each bundle are used Reviewed-by: neugens, jkang Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2016-December/021832.html
author Joshua Matsuoka <jmatsuok@redhat.com>
date Thu, 26 Jan 2017 12:14:51 -0500
parents 8026471cdbea
children cd5b08c32052
files dependency-tool/command/src/main/java/com/redhat/thermostat/tools/dependency/internal/Dependency.java dependency-tool/command/src/main/java/com/redhat/thermostat/tools/dependency/internal/DependencyGraphBuilder.java dependency-tool/command/src/main/java/com/redhat/thermostat/tools/dependency/internal/OSGIManifestScanner.java dependency-tool/command/src/main/java/com/redhat/thermostat/tools/dependency/internal/OSGiSearchProcessor.java dependency-tool/command/src/main/java/com/redhat/thermostat/tools/dependency/internal/actions/PrintOSGIHeaderAction.java dependency-tool/command/src/test/java/com/redhat/thermostat/tools/dependency/DependencyAnalyzerCommandTest.java dependency-tool/command/src/test/java/com/redhat/thermostat/tools/dependency/internal/DependencyGraphBuilderTest.java dependency-tool/command/src/test/java/com/redhat/thermostat/tools/dependency/internal/DependencyTest.java dependency-tool/command/src/test/java/com/redhat/thermostat/tools/dependency/internal/OSGIManifestScannerTest.java dependency-tool/command/src/test/java/com/redhat/thermostat/tools/dependency/internal/actions/ListDependenciesActionTest.java dependency-tool/command/src/test/java/com/redhat/thermostat/tools/dependency/internal/utils/TestHelper.java launcher/src/main/java/com/redhat/thermostat/launcher/internal/DependencyManager.java launcher/src/main/java/com/redhat/thermostat/launcher/internal/DependencyResolver.java launcher/src/main/java/com/redhat/thermostat/launcher/internal/LauncherImpl.java launcher/src/main/java/com/redhat/thermostat/launcher/internal/MetadataHandler.java launcher/src/test/java/com/redhat/thermostat/launcher/internal/DependencyManagerTest.java launcher/src/test/java/com/redhat/thermostat/launcher/internal/MetadataHandlerTest.java
diffstat 17 files changed, 1138 insertions(+), 190 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dependency-tool/command/src/main/java/com/redhat/thermostat/tools/dependency/internal/Dependency.java	Thu Jan 26 12:14:51 2017 -0500
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.tools.dependency.internal;
+
+public class Dependency {
+
+    private String name;
+    private String version;
+
+    public static final String NO_VERSION = "0";
+
+    public Dependency(String bundleName, String bundleVersion) {
+        this.name = String.valueOf(bundleName);
+        this.version = String.valueOf(bundleVersion);
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    @Override
+    public String toString() {
+        return name + " version " + version;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        if (other == null) {
+            return false;
+        }
+
+        if (getClass() != other.getClass()) {
+            return false;
+        }
+
+        Dependency x = (Dependency) other;
+        return name.equals(x.name) && version.equals(x.version);
+    }
+
+}
--- a/dependency-tool/command/src/main/java/com/redhat/thermostat/tools/dependency/internal/DependencyGraphBuilder.java	Thu Jan 26 11:10:27 2017 -0500
+++ b/dependency-tool/command/src/main/java/com/redhat/thermostat/tools/dependency/internal/DependencyGraphBuilder.java	Thu Jan 26 12:14:51 2017 -0500
@@ -40,6 +40,7 @@
 import com.redhat.thermostat.collections.graph.HashGraph;
 import com.redhat.thermostat.collections.graph.Node;
 import com.redhat.thermostat.collections.graph.Relationship;
+import com.redhat.thermostat.common.utils.LoggingUtils;
 
 import java.io.IOException;
 import java.nio.file.Path;
@@ -51,23 +52,24 @@
 import java.util.jar.Attributes;
 import java.util.jar.JarFile;
 import java.util.jar.Manifest;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 /**
  */
 public class DependencyGraphBuilder extends PathProcessor {
 
-    private Map<String, Path> imports;
-    private Map<String, Path> exports;
+    private final Logger logger = LoggingUtils.getLogger(DependencyGraphBuilder.class);
+
+    private Map<Dependency, Path> exports;
 
     private Map<Path, Node> nodes;
 
     private Graph graph;
 
     public DependencyGraphBuilder() {
-        imports = new HashMap<>();
         exports = new HashMap<>();
         graph = new HashGraph();
-
         nodes = new HashMap<>();
     }
 
@@ -80,15 +82,15 @@
         try {
             Manifest manifest = new JarFile(jar.toFile()).getManifest();
 
-            List<String> thisExports = new ArrayList<>();
-            List<String> thisImports = new ArrayList<>();
+            List<Dependency> thisExports = new ArrayList<>();
+            List<Dependency> thisImports = new ArrayList<>();
 
             Attributes attributes = manifest.getMainAttributes();
 
             String exports = attributes.getValue(BundleProperties.EXPORT.id());
             if (exports != null) {
-                List<String> dependencies = OSGIManifestScanner.parseHeader(exports);
-                for (String dependency : dependencies) {
+                List<Dependency> dependencies = OSGIManifestScanner.parseHeader(exports);
+                for (Dependency dependency : dependencies) {
                     this.exports.put(dependency, jar);
                     thisExports.add(dependency);
                 }
@@ -96,9 +98,8 @@
 
             String imports = attributes.getValue(BundleProperties.IMPORT.id());
             if (imports != null) {
-                List<String> dependencies = OSGIManifestScanner.parseHeader(imports);
-                for (String dependency : dependencies) {
-                    this.imports.put(dependency, jar);
+                List<Dependency> dependencies = OSGIManifestScanner.parseHeader(imports);
+                for (Dependency dependency : dependencies) {
                     thisImports.add(dependency);
                 }
             }
@@ -112,7 +113,7 @@
             nodes.put(jar, node);
 
         } catch (IOException e) {
-            e.printStackTrace();
+            logger.log(Level.WARNING, e.getMessage());
         }
     }
 
@@ -126,22 +127,40 @@
 
     private Graph build(boolean swap) {
         for (Node source : nodes.values()) {
-            List<String> thisImports = source.getProperty(BundleProperties.IMPORT.id());
-
-            for (String dep : thisImports) {
-
-                Path who = exports.get(dep);
-                if (who != null) {
-                    Node destination = nodes.get(who);
-
+            List<Dependency> thisImports = source.getProperty(BundleProperties.IMPORT.id());
+            Path location;
+            for (Dependency dep : thisImports) {
+                location = exports.get(dep);
+                if (location == null) {
+                    for (Dependency bundle : exports.keySet()) {
+                        if (OSGIManifestScanner.isVersionRange(dep.getVersion())) {
+                            if ((bundle.getName().equals(dep.getName()))) {
+                                location = exports.get(OSGIManifestScanner.parseAndCheckBounds(
+                                        dep.getVersion(), bundle.getVersion(), bundle));
+                            }
+                        } else {
+                            // If a version range is specified as a single version, it must be interpreted
+                            // as the range [version, infinity) according to the osgi specification.
+                            if ((bundle.getName().equals(dep.getName()))) {
+                                location = exports.get(OSGIManifestScanner.parseAndCheckBounds(
+                                        "[" + dep.getVersion() + "," + Integer.MAX_VALUE + ")",
+                                        bundle.getVersion(), bundle));
+                            }
+                        }
+                        if (location != null) {
+                            break;
+                        }
+                    }
+                }
+                if (location != null) {
+                    Node destination = nodes.get(location);
                     // some package seems to have dependencies on themselves, if
                     // we create a relationship we will cause a cycle
                     if (source.equals(destination)) {
                         continue;
                     }
-
-                    Relationship relationship = null;
-                    Set<Relationship> relationships = null;
+                    Relationship relationship;
+                    Set<Relationship> relationships;
                     if (swap) {
                         relationship = new Relationship(destination, "<-", source);
                         relationships = graph.getRelationships(destination);
--- a/dependency-tool/command/src/main/java/com/redhat/thermostat/tools/dependency/internal/OSGIManifestScanner.java	Thu Jan 26 11:10:27 2017 -0500
+++ b/dependency-tool/command/src/main/java/com/redhat/thermostat/tools/dependency/internal/OSGIManifestScanner.java	Thu Jan 26 12:14:51 2017 -0500
@@ -42,11 +42,26 @@
 import java.util.jar.Attributes;
 import java.util.jar.JarFile;
 import java.util.jar.Manifest;
+import java.util.regex.Pattern;
 
 /**
  */
 public class OSGIManifestScanner {
 
+    private static final String LBRACK = "([\\(|\\[])";
+    private static final String VERSION = "(\\d+(\\.\\d+){0,2})";
+    private static final String RBRACK = "([\\)|\\]])";
+    private static final String COMMA = ",";
+    private static final String INCLUSIVE_LOWER = "([\\[])";
+    private static final String INCLUSIVE_UPPER = "([\\]])";
+    private static final String VERSION_RANGE = LBRACK + VERSION + COMMA + VERSION + RBRACK;
+    private static final String INCLUSIVE_UPPER_RANGE = LBRACK + VERSION + COMMA + VERSION + INCLUSIVE_UPPER;
+    private static final String INCLUSIVE_LOWER_RANGE = INCLUSIVE_LOWER + VERSION + COMMA + VERSION + RBRACK;
+
+    private static final Pattern VERSION_RANGE_PATTERN = Pattern.compile(VERSION_RANGE);
+    private static final Pattern INCLUSIVE_UPPER_PATTERN = Pattern.compile(INCLUSIVE_UPPER_RANGE);
+    private static final Pattern INCLUSIVE_LOWER_PATTERN = Pattern.compile(INCLUSIVE_LOWER_RANGE);
+
     public static String getAttributeFromManifest(Path jar, String attribute) {
         String value = null;
         try {
@@ -62,48 +77,153 @@
         return value;
     }
 
-    public static List<String> parseHeader(String header) {
+    public static List<Dependency> parseHeader(String header) {
         header = header.concat("\0");
-        List<String> packages = new ArrayList<>();
+        List<Dependency> packages = new ArrayList<>();
         int index = 0;
         int start = 0;
-
         boolean invalid = false;
+        boolean newSubstring = true;
+        boolean parsingDirective = false;
+        boolean parsingVersion = false;
+        boolean isVersionRange = false;
         boolean inQuotes = false;
-        boolean newSubstring = true;
-
+        String version;
+        Dependency lastPackage = new Dependency("","");
+        String directive;
         while (index < header.length()) {
             char charAtIndex = header.charAt(index);
-
+            if (parsingVersion) {
+                if (charAtIndex == '[' || charAtIndex == '(') {
+                    isVersionRange = true;
+                } else if (charAtIndex == ']' || charAtIndex == ')') {
+                    isVersionRange = false;
+                }
+            }
             if (charAtIndex == '\"') {
                 inQuotes = !inQuotes;
             }
-
-            if (!inQuotes) {
-                if (charAtIndex == '=') {
-                    invalid = true;
-                    newSubstring = false;
-                } else if (charAtIndex == ';' || charAtIndex == ',' || charAtIndex == '\0') {
-                    if (!invalid && !newSubstring) {
-                        packages.add(header.substring(start, index));
+            if (charAtIndex == '=') {
+                if (parsingDirective) {
+                    directive = header.substring(start, index);
+                    if (directive.equals("version")) {
+                        parsingVersion = true;
                     }
-                    start = index + 1;
-                    invalid = false;
-                    newSubstring = true;
-                } else if (newSubstring) {
-                    if (!Character.isJavaIdentifierStart(charAtIndex)) {
-                        invalid = true;
+                    parsingDirective = false;
+                }
+                invalid = true;
+                newSubstring = false;
+                start = index + 1;
+            }
+            if (charAtIndex == ';' || charAtIndex == ',' || charAtIndex == '\0') {
+                if (parsingVersion) {
+                    if (!isVersionRange) {
+                        version = header.substring(start, index);
+                        packages.remove(lastPackage);
+                        packages.add(new Dependency(
+                                lastPackage.getName(), version.replace("\"", "")));
+                        parsingVersion = false;
+                    } else {
+                        index++;
+                        continue;
                     }
-                    newSubstring = false;
-                } else if (!Character.isJavaIdentifierPart(charAtIndex) && charAtIndex != '.') {
+                } else {
+                    if (!inQuotes) {
+                        if (!invalid && !newSubstring) {
+                            // Packages are given a default version of 0. This is changed later if a version directive
+                            // is specified in the manifest.
+                            lastPackage = new Dependency(header.substring(start, index), Dependency.NO_VERSION);
+                            packages.add(lastPackage);
+                        }
+                        if (charAtIndex == ';') {
+                            parsingDirective = true;
+                        }
+                    }
+                }
+                start = index + 1;
+                invalid = false;
+                newSubstring = true;
+            } else if (newSubstring) {
+                if (!Character.isJavaIdentifierStart(charAtIndex) && !Character.isWhitespace(charAtIndex)) {
                     invalid = true;
                 }
+                newSubstring = false;
+            } else if ((!Character.isJavaIdentifierPart(charAtIndex)) && charAtIndex != '.'
+                    && !Character.isWhitespace(charAtIndex)) {
+                invalid = true;
             }
-
             index++;
         }
+        return packages;
+    }
 
-        return packages;
+    private static boolean satisfiesBound(int[] target, int[] bound, boolean exclusive) {
+        int major = Integer.compare(target[0], bound[0]);
+        int minor = Integer.compare(target[1], bound[1]);
+        int micro = Integer.compare(target[2], bound[2]);
+        if (major > 0) {
+            return true;
+        } else if (major == 0) {
+            if (minor > 0) {
+                return true;
+            } else if (minor == 0) {
+                if (micro > 0) {
+                    return true;
+                } else if (micro == 0 && !exclusive) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private static int[] extractVersions(String versionString) {
+        String[] split = versionString.split(Pattern.quote("."));
+        int[] versions = {0, 0, 0};
+        try {
+            for (int i = 0; i < Math.min(split.length, versions.length); i++) {
+                versions[i] = Integer.parseInt(split[i]);
+            }
+        } catch (NumberFormatException ignore) {
+        }
+        return versions;
+    }
+
+    static String[] parseVersionRange(String versionString) {
+        String[] raw = versionString.split(",");
+        String[] processed = new String[2];
+        for (String s : raw) {
+            if (!(s.equals(""))) {
+                if ((s.contains("[")) || (s.contains("("))) {
+                    processed[0] = s.substring(1, s.length());
+                } else {
+                    processed[1] = s.substring(0, s.length()-1);
+                }
+            }
+        }
+        return processed;
+    }
+
+    static Dependency parseAndCheckBounds(String versionString, String TargetVersion, Dependency target) {
+        if ((versionString == null || TargetVersion == null || target == null) || !isVersionRange(versionString)) {
+            return null;
+        }
+        boolean strictUpper = !INCLUSIVE_UPPER_PATTERN.matcher(versionString).matches();
+        boolean strictLower = !INCLUSIVE_LOWER_PATTERN.matcher(versionString).matches();
+        String [] bounds = parseVersionRange(versionString);
+        int[] parsedLowerBound = extractVersions(bounds[0]);
+        int[] parsedUpperBound = extractVersions(bounds[1]);
+        int[] parsedTarget = extractVersions(TargetVersion);
+        // Need to satisfy lowerBound <= versionString <= upperBound
+        if ((satisfiesBound(parsedTarget, parsedLowerBound, strictLower))
+                && (satisfiesBound(parsedUpperBound, parsedTarget, strictUpper))) {
+            return target;
+        }
+        return null;
+    }
+
+    static boolean isVersionRange(String versionString) {
+        return VERSION_RANGE_PATTERN.matcher(versionString).matches();
     }
 }
 
--- a/dependency-tool/command/src/main/java/com/redhat/thermostat/tools/dependency/internal/OSGiSearchProcessor.java	Thu Jan 26 11:10:27 2017 -0500
+++ b/dependency-tool/command/src/main/java/com/redhat/thermostat/tools/dependency/internal/OSGiSearchProcessor.java	Thu Jan 26 12:14:51 2017 -0500
@@ -78,8 +78,9 @@
             Attributes attributes = manifest.getMainAttributes();
             String bundleAttribute = attributes.getValue(what.id());
             if (bundleAttribute != null) {
-                List<String> dependencies = OSGIManifestScanner.parseHeader(bundleAttribute);
-                if (dependencies.contains(target)) {
+                List<Dependency> dependencies = OSGIManifestScanner.parseHeader(bundleAttribute);
+                // If no version is specified, a default version of 0 is given, See OSGIManifestScanner
+                if (dependencies.contains(new Dependency(target, Dependency.NO_VERSION))) {
                     info = new BundleInfo();
                     info.library = jar;
                     info.symbolicName = attributes.getValue(BundleProperties.SYMBOLIC_NAME.id());
--- a/dependency-tool/command/src/main/java/com/redhat/thermostat/tools/dependency/internal/actions/PrintOSGIHeaderAction.java	Thu Jan 26 11:10:27 2017 -0500
+++ b/dependency-tool/command/src/main/java/com/redhat/thermostat/tools/dependency/internal/actions/PrintOSGIHeaderAction.java	Thu Jan 26 12:14:51 2017 -0500
@@ -38,6 +38,7 @@
 
 import com.redhat.thermostat.common.cli.CommandContext;
 import com.redhat.thermostat.tools.dependency.internal.BundleProperties;
+import com.redhat.thermostat.tools.dependency.internal.Dependency;
 import com.redhat.thermostat.tools.dependency.internal.OSGIManifestScanner;
 import com.redhat.thermostat.tools.dependency.internal.Utils;
 
@@ -61,9 +62,9 @@
             return;
         }
 
-        List<String> parsedHeader = OSGIManifestScanner.parseHeader(header);
-        for (String entry : parsedHeader) {
-            Utils.getInstance().print(ctx, entry);
+        List<Dependency> parsedHeader = OSGIManifestScanner.parseHeader(header);
+        for (Dependency entry : parsedHeader) {
+            Utils.getInstance().print(ctx, entry.getName());
         }
     }
 }
--- a/dependency-tool/command/src/test/java/com/redhat/thermostat/tools/dependency/DependencyAnalyzerCommandTest.java	Thu Jan 26 11:10:27 2017 -0500
+++ b/dependency-tool/command/src/test/java/com/redhat/thermostat/tools/dependency/DependencyAnalyzerCommandTest.java	Thu Jan 26 12:14:51 2017 -0500
@@ -74,7 +74,7 @@
         locations.getLocations().add(underneathTheBridge.toPath());
         handler = new PathProcessorHandler(locations);
 
-        testJar = TestHelper.createJarWithPackageDependency("foobar", "com.real.package", underneathTheBridge.toPath());
+        testJar = TestHelper.createJarWithExports("foobar", "foobar,com.real.package", null, underneathTheBridge.toPath());
 
         when(ctx.getConsole()).thenReturn(console);
         when(console.getOutput()).thenReturn(mock(PrintStream.class));
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dependency-tool/command/src/test/java/com/redhat/thermostat/tools/dependency/internal/DependencyGraphBuilderTest.java	Thu Jan 26 12:14:51 2017 -0500
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.tools.dependency.internal;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Set;
+import com.redhat.thermostat.collections.graph.Graph;
+import com.redhat.thermostat.collections.graph.Node;
+import com.redhat.thermostat.collections.graph.Relationship;
+import com.redhat.thermostat.tools.dependency.internal.utils.TestHelper;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class DependencyGraphBuilderTest {
+
+    private File underneathTheBridge;
+
+    private Path b1, b2, b3, b4, b5, b6;
+    private Path v1, v2, v31, v41, v30, v40;
+
+    @Before
+    public void setup() throws Exception {
+        underneathTheBridge = TestHelper.createTestDirectory();
+        b1 = TestHelper.createJar("Bundle1", "Bundle2", underneathTheBridge.toPath());
+        b2 = TestHelper.createJar("Bundle2", "Bundle3,Bundle4", underneathTheBridge.toPath());
+        b3 = TestHelper.createJar("Bundle3", "Bundle4,Bundle5,Bundle6", underneathTheBridge.toPath());
+        b4 = TestHelper.createJar("Bundle4", "Bundle5,Bundle6", underneathTheBridge.toPath());
+        b5 = TestHelper.createJar("Bundle5", "Bundle6", underneathTheBridge.toPath());
+        b6 = TestHelper.createJar("Bundle6", "", underneathTheBridge.toPath());
+
+        v1 = TestHelper.createJarWithExports("test1", "test1;version=1.0", "test2;version=[1,2)", underneathTheBridge.toPath());
+        v2 = TestHelper.createJarWithExports("test2", "test2;version=1.0", "test3;version=[3.1,4)", underneathTheBridge.toPath());
+        v31 = TestHelper.createJarWithExports("test-3.1", "test3;version=3.1", "test4;version=[4.1,5)", underneathTheBridge.toPath());
+        v41 = TestHelper.createJarWithExports("test-4.1", "test4;version=4.1", "", underneathTheBridge.toPath());
+        v30 = TestHelper.createJarWithExports("test-3.0", "test3;version=3.0", "test4;version=[4.0,4.1)", underneathTheBridge.toPath());
+        v40 = TestHelper.createJarWithExports("test-4.0", "test4;version=4.0", "", underneathTheBridge.toPath());
+    }
+
+    @Test
+    public void testSimpleGraphBuild() {
+        JarLocations locations = mock(JarLocations.class);
+        when(locations.getLocations()).thenReturn(Arrays.asList(b1, b2, b3, b4, b5, b6));
+        Node n1 = new Node(b1.toString());
+        Node n2 = new Node(b2.toString());
+        Node n3 = new Node(b3.toString());
+        Node n4 = new Node(b4.toString());
+        Node n5 = new Node(b5.toString());
+        Node n6 = new Node(b6.toString());
+        PathProcessorHandler handler = new PathProcessorHandler(locations);
+        DependencyGraphBuilder dgb = new DependencyGraphBuilder();
+        handler.process(dgb);
+        Graph g = dgb.build();
+        assertNotNull(g);
+        Set<Relationship> edges = g.getRelationships(n1);
+        assertEquals(1, edges.size());
+        assertTrue(edges.contains(new Relationship(n1, "->", n2)));
+        edges = g.getRelationships(n2);
+        assertEquals(2, edges.size());
+        assertTrue(edges.contains(new Relationship(n2, "->", n3)));
+        assertTrue(edges.contains(new Relationship(n2, "->", n4)));
+        edges = g.getRelationships(n3);
+        assertEquals(3, edges.size());
+        assertTrue(edges.contains(new Relationship(n3, "->", n4)));
+        assertTrue(edges.contains(new Relationship(n3, "->", n5)));
+        assertTrue(edges.contains(new Relationship(n3, "->", n6)));
+        edges = g.getRelationships(n4);
+        assertEquals(2, edges.size());
+        assertTrue(edges.contains(new Relationship(n4, "->", n5)));
+        assertTrue(edges.contains(new Relationship(n4, "->", n6)));
+        edges = g.getRelationships(n5);
+        assertEquals(1, edges.size());
+        assertTrue(edges.contains(new Relationship(n5, "->", n6)));
+        edges = g.getRelationships(n6);
+        assertEquals(0, edges.size());
+    }
+
+    @Test
+    public void testGraphWithSpecificVersions() {
+        JarLocations locations = mock(JarLocations.class);
+        when(locations.getLocations()).thenReturn(Arrays.asList(v1, v2, v30, v31, v40, v41));
+        Node n1 = new Node(v1.toString());
+        Node n2 = new Node(v2.toString());
+        Node n3 = new Node(v30.toString());
+        Node n4 = new Node(v31.toString());
+        Node n5 = new Node(v40.toString());
+        Node n6 = new Node(v41.toString());
+        PathProcessorHandler handler = new PathProcessorHandler(locations);
+        DependencyGraphBuilder dgb = new DependencyGraphBuilder();
+        handler.process(dgb);
+        Graph g = dgb.build();
+        assertNotNull(g);
+        Set<Relationship> edges = g.getRelationships(n1);
+        assertEquals(1, edges.size());
+        assertTrue(edges.contains(new Relationship(n1, "->", n2)));
+        edges = g.getRelationships(n2);
+        assertEquals(1, edges.size());
+        assertTrue(edges.contains(new Relationship(n2, "->", n4)));
+        edges = g.getRelationships(n3);
+        assertEquals(1, edges.size());
+        assertTrue(edges.contains(new Relationship(n3, "->", n5)));
+        edges = g.getRelationships(n4);
+        assertEquals(1, edges.size());
+        assertTrue(edges.contains(new Relationship(n4, "->", n6)));
+        edges = g.getRelationships(n5);
+        assertEquals(0, edges.size());
+        edges = g.getRelationships(n6);
+        assertEquals(0, edges.size());
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dependency-tool/command/src/test/java/com/redhat/thermostat/tools/dependency/internal/DependencyTest.java	Thu Jan 26 12:14:51 2017 -0500
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.tools.dependency.internal;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class DependencyTest {
+
+    private Dependency test1;
+    private Dependency test2;
+    private Dependency test3;
+
+    @Before
+    public void setup() {
+        test1 = new Dependency("foo", "1.0.0");
+        test2 = new Dependency("foo", "1.0.0");
+        test3 = new Dependency("bar", "2.9.1");
+    }
+
+    @Test
+    public void testEquals() {
+        assertTrue(test1.equals(test1));
+        assertTrue(test1.equals(test2));
+        assertTrue(test2.equals(test1));
+    }
+
+    @Test
+    public void testNotEquals() {
+        String foo = "foo";
+        assertFalse(test3.equals(test1));
+        assertFalse(test3.equals(test2));
+        assertFalse(test1.equals(foo));
+        assertFalse(test1.equals(null));
+    }
+
+}
--- a/dependency-tool/command/src/test/java/com/redhat/thermostat/tools/dependency/internal/OSGIManifestScannerTest.java	Thu Jan 26 11:10:27 2017 -0500
+++ b/dependency-tool/command/src/test/java/com/redhat/thermostat/tools/dependency/internal/OSGIManifestScannerTest.java	Thu Jan 26 12:14:51 2017 -0500
@@ -134,39 +134,75 @@
 
     @Test
     public void testParseHeader() throws Exception {
-        List<String> packages = OSGIManifestScanner.parseHeader(Export_Package);
+        List<Dependency> packages = OSGIManifestScanner.parseHeader(Export_Package);
         Assert.assertEquals(31, packages.size());
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.handler.codec.serialization", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.util", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.handler.codec.compression", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.handler.execution", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.channel.local", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.bootstrap", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.handler.codec.base64", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.handler.timeout", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.channel.socket.nio", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.handler.codec.http.websocket", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.handler.codec.replay", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.handler.codec.string", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.channel", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.handler.ssl", "logging")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.container.microcontainer", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.handler.codec.rtsp", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.handler.codec.protobuf", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.channel.socket.http", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.channel.group", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.handler.codec.embedder", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.channel.socket.oio", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.handler.codec.frame", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.handler.codec.oneone", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.container.osgi", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.logging", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.buffer", "3.2.4 .Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.handler.codec.http", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.handler.queue", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.channel.socket", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.handler.logging", "3.2.4.Final")));
+        Assert.assertTrue(packages.contains(new Dependency("org.jboss.netty.handler.stream", "3.2.4.Final")));
+    }
 
-        Assert.assertTrue(packages.contains("org.jboss.netty.handler.codec.serialization"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.util"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.handler.codec.compression"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.handler.execution"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.channel.local"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.bootstrap"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.handler.codec.base64"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.handler.timeout"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.channel.socket.nio"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.handler.codec.http.websocket"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.handler.codec.replay"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.handler.codec.string"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.channel"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.handler.ssl"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.container.microcontainer"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.handler.codec.rtsp"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.handler.codec.protobuf"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.channel.socket.http"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.channel.group"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.handler.codec.embedder"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.channel.socket.oio"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.handler.codec.frame"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.handler.codec.oneone"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.container.osgi"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.logging"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.buffer"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.handler.codec.http"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.handler.queue"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.channel.socket"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.handler.logging"));
-        Assert.assertTrue(packages.contains("org.jboss.netty.handler.stream"));
+    @Test
+    public void testVersionExtractor() {
+        String versionRange = "[9.1.3,10.1.2)";
+        String[] extracted = OSGIManifestScanner.parseVersionRange(versionRange);
+        Assert.assertEquals("9.1.3", extracted[0]);
+        Assert.assertEquals("10.1.2", extracted[1]);
+    }
+
+    @Test
+    public void testVersionMatcher() {
+        String versionRange = "[3,4)";
+        Dependency result = OSGIManifestScanner.parseAndCheckBounds(versionRange, "3.1.2", new Dependency("TestBundle", "3.1.2"));
+        Assert.assertEquals(result, new Dependency("TestBundle", "3.1.2"));
     }
+
+    @Test
+    public void testVersionMatcher2() {
+        String versionRange = "[3,4)";
+        Dependency result = OSGIManifestScanner.parseAndCheckBounds(versionRange, "4", new Dependency("TestBundle", "4"));
+        Assert.assertEquals(result, null);
+    }
+
+    @Test
+    public void testVersionMatcher3() {
+        String versionRange = "[5,5.1.2]";
+        Dependency result = OSGIManifestScanner.parseAndCheckBounds(versionRange, "5.1.2", new Dependency("TestBundle", "5.1.2"));
+        Assert.assertEquals(result, new Dependency("TestBundle", "5.1.2"));
+    }
+
+    @Test
+    public void testNullDependency() {
+        String versionRange = "[1,2]";
+        Dependency result = OSGIManifestScanner.parseAndCheckBounds(versionRange, null, null);
+        Assert.assertEquals(result, null);
+    }
+
 }
--- a/dependency-tool/command/src/test/java/com/redhat/thermostat/tools/dependency/internal/actions/ListDependenciesActionTest.java	Thu Jan 26 11:10:27 2017 -0500
+++ b/dependency-tool/command/src/test/java/com/redhat/thermostat/tools/dependency/internal/actions/ListDependenciesActionTest.java	Thu Jan 26 12:14:51 2017 -0500
@@ -57,7 +57,7 @@
  */
 public class ListDependenciesActionTest {
 
-    private File underneathTheBrdige;
+    private File underneathTheBridge;
 
     private JarLocations paths;
 
@@ -69,6 +69,7 @@
     private Path c;
     private Path d;
     private Path e;
+    private Path c1;
 
     private static class TestUtils extends Utils {
         List<Node> results = new ArrayList<>();
@@ -92,16 +93,20 @@
         utils = new TestUtils();
         Utils.initSingletonForTest(utils);
 
-        underneathTheBrdige = TestHelper.createTestDirectory();
+        underneathTheBridge = TestHelper.createTestDirectory();
 
         paths = new JarLocations();
-        paths.getLocations().add(underneathTheBrdige.toPath());
+        paths.getLocations().add(underneathTheBridge.toPath());
 
-        a = TestHelper.createJar("a", null, underneathTheBrdige.toPath());
-        b = TestHelper.createJar("b", "a",   underneathTheBrdige.toPath());
-        c = TestHelper.createJar("c", "b",   underneathTheBrdige.toPath());
-        d = TestHelper.createJar("d", "b,c", underneathTheBrdige.toPath());
-        e = TestHelper.createJar("e", "d",   underneathTheBrdige.toPath());
+        a = TestHelper.createJar("a", null, underneathTheBridge.toPath());
+        b = TestHelper.createJar("b", "a",   underneathTheBridge.toPath());
+        c = TestHelper.createJar("c", "b",   underneathTheBridge.toPath());
+        d = TestHelper.createJar("d", "b,c", underneathTheBridge.toPath());
+        e = TestHelper.createJar("e", "d",   underneathTheBridge.toPath());
+
+        c1 = TestHelper.createJar("c1", "c2", underneathTheBridge.toPath());
+        TestHelper.createJar("c2", "c3", underneathTheBridge.toPath());
+        TestHelper.createJar("c3", "c1", underneathTheBridge.toPath());
     }
 
     @Test
@@ -137,6 +142,12 @@
         Assert.assertEquals(c.toString(), utils.results.get(1).getName());
         Assert.assertEquals(d.toString(), utils.results.get(2).getName());
         Assert.assertEquals(e.toString(), utils.results.get(3).getName());
+    }
 
+    // Test that an exception is thrown when a cycle is detected.
+    @Test(expected = IllegalStateException.class)
+    public void testCyclicDependencies() {
+        PathProcessorHandler handler = new PathProcessorHandler(paths);
+        ListDependenciesAction.execute(handler, c1.toString(), ctx, true);
     }
 }
\ No newline at end of file
--- a/dependency-tool/command/src/test/java/com/redhat/thermostat/tools/dependency/internal/utils/TestHelper.java	Thu Jan 26 11:10:27 2017 -0500
+++ b/dependency-tool/command/src/test/java/com/redhat/thermostat/tools/dependency/internal/utils/TestHelper.java	Thu Jan 26 12:14:51 2017 -0500
@@ -53,41 +53,27 @@
 public class TestHelper {
 
     public static File createTestDirectory() {
-        File underneathTheBrdige = null;
+        File underneathTheBridge = null;
         try {
-            underneathTheBrdige = Files.createTempDirectory("underneathTheBridge").toFile();
+            underneathTheBridge = Files.createTempDirectory("underneathTheBridge").toFile();
 
         } catch (IOException e) {
             e.printStackTrace();
         }
-        underneathTheBrdige.deleteOnExit();
-        return underneathTheBrdige;
+        underneathTheBridge.deleteOnExit();
+        return underneathTheBridge;
     }
 
     public static Path createJar(String exportsDirective, String importDirective, Path base) throws Exception {
-        Manifest manifest = new Manifest();
-        manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
-        manifest.getMainAttributes().put(new Attributes.Name(BundleProperties.EXPORT.id()), exportsDirective + ";");
-        if (importDirective != null) {
-            manifest.getMainAttributes().put(new Attributes.Name(BundleProperties.IMPORT.id()), importDirective + ";");
-        }
-
         Path path = Paths.get(base.toFile().getAbsoluteFile() + "/" + exportsDirective + ".jar");
         FileOutputStream stream = new FileOutputStream(path.toFile());
-        JarOutputStream target = new JarOutputStream(stream, manifest);
+        JarOutputStream target = new JarOutputStream(stream, createManifest(exportsDirective, importDirective));
         target.close();
-
         return path;
     }
 
-    public static Path createJarWithPackageDependency(String jarName, String packagePath, Path base) throws Exception {
-        Manifest manifest = new Manifest();
-        manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
-        manifest.getMainAttributes().put(new Attributes.Name(BundleProperties.EXPORT.id()), jarName + ";");
-        if (packagePath != null) {
-            manifest.getMainAttributes().put(new Attributes.Name(BundleProperties.EXPORT.id()), packagePath + ";");
-        }
-
+    public static Path createJarWithExports(String jarName, String exportDirective, String importDirective, Path base) throws Exception {
+        Manifest manifest = createManifest(exportDirective, importDirective);
         Path path = Paths.get(base.toFile().getAbsoluteFile() + "/" + jarName + ".jar");
         FileOutputStream stream = new FileOutputStream(path.toFile());
         JarOutputStream target = new JarOutputStream(stream, manifest);
@@ -95,4 +81,14 @@
         return path;
     }
 
+    private static Manifest createManifest(String exports, String imports) {
+        Manifest manifest = new Manifest();
+        manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
+        manifest.getMainAttributes().put(new Attributes.Name(BundleProperties.EXPORT.id()), exports + ";");
+        if (imports != null) {
+            manifest.getMainAttributes().put(new Attributes.Name(BundleProperties.IMPORT.id()), imports + ";");
+        }
+        return manifest;
+    }
+
 }
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/DependencyManager.java	Thu Jan 26 11:10:27 2017 -0500
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/DependencyManager.java	Thu Jan 26 12:14:51 2017 -0500
@@ -90,6 +90,9 @@
             return new LinkedList<>();
         }
         buildSubgraph(b);
+        if (!hasNoIncomingEdge(b)) {
+            throw new IllegalStateException("Node " + b + " has incoming edges.");
+        }
         LinkedList<BundleInformation> sorted = new LinkedList<>();
         LinkedList<BundleInformation> queue = new LinkedList<>();
         queue.add(b);
@@ -105,6 +108,9 @@
                 }
             }
         }
+        for (BundleInformation node : discovered) {
+            assertNoEdges(node);
+        }
         return sorted;
     }
 
@@ -127,6 +133,19 @@
         incoming.get(to).remove(from);
     }
 
+    private void assertNoEdges(BundleInformation node) throws IllegalStateException {
+        for (BundleInformation dest : getOutgoingRelationShips(node)) {
+            if (discovered.contains(dest)) {
+                throw new IllegalStateException("Graph contains a cycle.");
+            }
+        }
+        for (BundleInformation src : getIncomingRelationShips(node)) {
+            if (discovered.contains(src)) {
+                throw new IllegalStateException("Graph contains a cycle.");
+            }
+        }
+    }
+
     private boolean hasNoIncomingEdge(BundleInformation key) {
         if (getIncomingRelationShips(key).isEmpty()) {
             return true;
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/DependencyResolver.java	Thu Jan 26 11:10:27 2017 -0500
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/DependencyResolver.java	Thu Jan 26 12:14:51 2017 -0500
@@ -65,11 +65,14 @@
 public class DependencyResolver {
 
     private final Map<Path, BundleInformation> nodes;
-    private final Map<String, Path> exports;
-    private final Map<BundleInformation, List<String>> importsMap;
+    private final Map<BundleInformation, Path> exports;
+    private final Map<BundleInformation, List<BundleInformation>> importsMap;
     private final Map<BundleInformation, Set<BundleInformation>> outgoing;
     private final Map<BundleInformation, Set<BundleInformation>> incoming;
+    // Provides a mapping of package names to the specific <name, version> pairs that provide it.
+    private final Map<String, List<BundleInformation>> providedVersions;
     private final Logger logger = LoggingUtils.getLogger(DependencyResolver.class);
+    private final MetadataHandler handler;
 
 
     public DependencyResolver(List<Path> paths) {
@@ -78,6 +81,9 @@
         this.importsMap = new HashMap<>();
         this.outgoing = new HashMap<>();
         this.incoming = new HashMap<>();
+        this.handler = new MetadataHandler();
+        this.providedVersions = new HashMap<>();
+
         try {
             final PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:*.jar");
             FileVisitor visitor = new PluginDirFileVisitor() {
@@ -111,7 +117,7 @@
 
     protected void process(Path jar) {
         try {
-            List<String> bImports = new ArrayList<>();
+            List<BundleInformation> bImports = new ArrayList<>();
             Manifest m = new JarFile(jar.toFile()).getManifest();
             Attributes a = m.getMainAttributes();
             String bundleName = a.getValue(Constants.BUNDLE_SYMBOLICNAME);
@@ -119,14 +125,18 @@
             String bundleImports = a.getValue(Constants.IMPORT_PACKAGE);
             String bundleExports = a.getValue(Constants.EXPORT_PACKAGE);
             if (bundleExports != null) {
-                List<String> exports = parseHeader(bundleExports);
-                for (String dep : exports) {
+                List<BundleInformation> exports = handler.parseHeader(bundleExports);
+                for (BundleInformation dep : exports) {
                     this.exports.put(dep, jar);
+                    if (providedVersions.get(dep.getName()) == null) {
+                        providedVersions.put(dep.getName(), new ArrayList<BundleInformation>());
+                    }
+                    providedVersions.get(dep.getName()).add(dep);
                 }
             }
             if (bundleImports != null) {
-                List<String> imports = parseHeader(bundleImports);
-                for (String dep : imports) {
+                List<BundleInformation> imports = handler.parseHeader(bundleImports);
+                for (BundleInformation dep : imports) {
                     bImports.add(dep);
                 }
             }
@@ -140,9 +150,26 @@
 
     private void buildGraph() {
         for (BundleInformation source : nodes.values()) {
-            List<String> bundleImports = importsMap.get(source);
-            for (String dep : bundleImports) {
+            List<BundleInformation> bundleImports = importsMap.get(source);
+            for (BundleInformation dep : bundleImports) {
                 Path who = exports.get(dep);
+                if (who == null && providedVersions.get(dep.getName()) != null) {
+                    for (BundleInformation export : providedVersions.get(dep.getName())) {
+                        if (handler.isVersionRange(dep.getVersion())) {
+                            who = exports.get(handler.parseAndCheckBounds(
+                                    dep.getVersion(), export.getVersion(), export));
+                        } else {
+                            // If a version range is specified as a single version, it must be interpreted
+                            // as the range [version, infinity) according to the osgi specification.
+                            who = exports.get(handler.parseAndCheckBounds(
+                                    "[" + dep.getVersion() + "," + Integer.MAX_VALUE + ")",
+                                    export.getVersion(), export));
+                        }
+                        if (who != null) {
+                            break;
+                        }
+                    }
+                }
                 if (who != null) {
                     BundleInformation destination = nodes.get(who);
                     if (source.equals(destination)) {
@@ -160,44 +187,4 @@
             }
         }
     }
-
-    private List<String> parseHeader(String header) {
-        header = header.concat("\0");
-        List<String> packages = new ArrayList<>();
-        int index = 0;
-        int start = 0;
-
-        boolean invalid = false;
-        boolean inQuotes = false;
-        boolean newSubstring = true;
-
-        while (index < header.length()) {
-            char charAtIndex = header.charAt(index);
-            if (charAtIndex == '\"') {
-                inQuotes = !inQuotes;
-            }
-            if (!inQuotes) {
-                if (charAtIndex == '=') {
-                    invalid = true;
-                    newSubstring = false;
-                } else if (charAtIndex == ';' || charAtIndex == ',' || charAtIndex == '\0') {
-                    if (!invalid && !newSubstring) {
-                        packages.add(header.substring(start, index));
-                    }
-                    start = index + 1;
-                    invalid = false;
-                    newSubstring = true;
-                } else if (newSubstring) {
-                    if (!Character.isJavaIdentifierStart(charAtIndex)) {
-                        invalid = true;
-                    }
-                    newSubstring = false;
-                } else if (!Character.isJavaIdentifierPart(charAtIndex) && charAtIndex != '.') {
-                    invalid = true;
-                }
-            }
-            index++;
-        }
-        return packages;
-    }
 }
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/LauncherImpl.java	Thu Jan 26 11:10:27 2017 -0500
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/LauncherImpl.java	Thu Jan 26 12:14:51 2017 -0500
@@ -337,7 +337,7 @@
                 }
             }
             registry.loadBundlesByName(new ArrayList<>(bundlesToLoad));
-        } catch (BundleException | IOException e) {
+        } catch (BundleException | IOException | IllegalStateException e) {
             // If this happens we definitely need to do something about it, and the
             // trace will be immeasurably helpful in figuring out what is wrong.
             out.println(t.localize(LocaleResources.COMMAND_COULD_NOT_LOAD_BUNDLES, cmdName).getContents());
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/MetadataHandler.java	Thu Jan 26 12:14:51 2017 -0500
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.launcher.internal;
+
+import com.redhat.thermostat.common.utils.LoggingUtils;
+import com.redhat.thermostat.launcher.BundleInformation;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MetadataHandler {
+
+    private static final String LBRACK = "([\\(|\\[])";
+    private static final String VERSION = "(\\d+(\\.\\d+){0,2})";
+    private static final String RBRACK = "([\\)|\\]])";
+    private static final String COMMA = ",";
+    private static final String INCLUSIVE_LOWER = "([\\[])";
+    private static final String INCLUSIVE_UPPER = "([\\]])";
+    private static final String VERSION_RANGE = LBRACK + VERSION + COMMA + VERSION + RBRACK;
+    private static final String INCLUSIVE_UPPER_RANGE = LBRACK + VERSION + COMMA + VERSION + INCLUSIVE_UPPER;
+    private static final String INCLUSIVE_LOWER_RANGE = INCLUSIVE_LOWER + VERSION + COMMA + VERSION + RBRACK;
+    private static final String NO_VERSION = "0";
+
+    private static final Pattern VERSION_RANGE_PATTERN = Pattern.compile(VERSION_RANGE);
+    private static final Pattern INCLUSIVE_UPPER_PATTERN = Pattern.compile(INCLUSIVE_UPPER_RANGE);
+    private static final Pattern INCLUSIVE_LOWER_PATTERN = Pattern.compile(INCLUSIVE_LOWER_RANGE);
+    private static final Logger logger = LoggingUtils.getLogger(MetadataHandler.class);
+
+    public List<BundleInformation> parseHeader(String header) {
+        header = header.concat("\0");
+        List<BundleInformation> packages = new ArrayList<>();
+        int index = 0;
+        int start = 0;
+        boolean invalid = false;
+        boolean newSubstring = true;
+        boolean parsingDirective = false;
+        boolean parsingVersion = false;
+        boolean isVersionRange = false;
+        boolean inQuotes = false;
+        String version;
+        BundleInformation lastPackage = new BundleInformation("","");
+        String directive;
+        while (index < header.length()) {
+            char charAtIndex = header.charAt(index);
+            if (parsingVersion) {
+                if (charAtIndex == '[' || charAtIndex == '(') {
+                    isVersionRange = true;
+                } else if (charAtIndex == ']' || charAtIndex == ')') {
+                    isVersionRange = false;
+                }
+            }
+            if (charAtIndex == '\"') {
+                inQuotes = !inQuotes;
+            }
+            if (charAtIndex == '=') {
+                if (parsingDirective) {
+                    directive = header.substring(start, index);
+                    if (directive.equals("version")) {
+                        parsingVersion = true;
+                    }
+                    parsingDirective = false;
+                }
+                invalid = true;
+                newSubstring = false;
+                start = index + 1;
+            }
+            if (charAtIndex == ';' || charAtIndex == ',' || charAtIndex == '\0') {
+                if (parsingVersion) {
+                    if (!isVersionRange) {
+                        version = header.substring(start, index);
+                        packages.remove(lastPackage);
+                        packages.add(new BundleInformation(
+                                lastPackage.getName(), version.replace("\"", "")));
+                        parsingVersion = false;
+                    } else {
+                        index++;
+                        continue;
+                    }
+                } else {
+                    if (!inQuotes) {
+                        if (!invalid && !newSubstring) {
+                            // Packages are given a default version of 0. This is changed later if a version directive
+                            // is specified in the manifest.
+                            lastPackage = new BundleInformation(header.substring(start, index),
+                                    MetadataHandler.NO_VERSION);
+                            packages.add(lastPackage);
+                        }
+                        if (charAtIndex == ';') {
+                            parsingDirective = true;
+                        }
+                    }
+                }
+                start = index + 1;
+                invalid = false;
+                newSubstring = true;
+            } else if (newSubstring) {
+                if (!Character.isJavaIdentifierStart(charAtIndex) && !Character.isWhitespace(charAtIndex)) {
+                    invalid = true;
+                }
+                newSubstring = false;
+            } else if ((!Character.isJavaIdentifierPart(charAtIndex)) && charAtIndex != '.'
+                    && !Character.isWhitespace(charAtIndex)) {
+                invalid = true;
+            }
+            index++;
+        }
+        return packages;
+    }
+
+    public BundleInformation parseAndCheckBounds(String versionString, String TargetVersion, BundleInformation target) {
+        boolean strictUpper = !INCLUSIVE_UPPER_PATTERN.matcher(versionString).matches();
+        boolean strictLower = !INCLUSIVE_LOWER_PATTERN.matcher(versionString).matches();
+        String [] bounds = parseVersionRange(versionString);
+        int[] parsedLowerBound = extractVersions(bounds[0]);
+        int[] parsedUpperBound = extractVersions(bounds[1]);
+        int[] parsedTarget = extractVersions(TargetVersion);
+        // Need to satisfy lowerBound <= versionString <= upperBound
+        if ((satisfiesBound(parsedTarget, parsedLowerBound, strictLower))
+                && (satisfiesBound(parsedUpperBound, parsedTarget, strictUpper))) {
+            return target;
+        }
+        return null;
+    }
+
+    public boolean isVersionRange(String versionString) {
+        return VERSION_RANGE_PATTERN.matcher(versionString).matches();
+    }
+
+
+    // Package Private for testing
+    boolean satisfiesBound(int[] target, int[] bound, boolean exclusive) {
+        int major = Integer.compare(target[0], bound[0]);
+        int minor = Integer.compare(target[1], bound[1]);
+        int micro = Integer.compare(target[2], bound[2]);
+        if (major > 0) {
+            return true;
+        } else if (major == 0) {
+            if (target[1] != -1 && bound[1] != -1) {
+                if (minor > 0) {
+                    return true;
+                } else if (minor == 0) {
+                    if (target[2] != -1 && bound[2] != -1) {
+                        if (micro > 0) {
+                            return true;
+                        } else if (micro == 0 && !exclusive) {
+                            return true;
+                        }
+                    } else if (!exclusive && bound[2] == -1) {
+                        return true;
+                    }
+                }
+            } else if (!exclusive && bound[1] == -1) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public String[] parseVersionRange(String versionString) {
+        Matcher m = VERSION_RANGE_PATTERN.matcher(versionString);
+        if (m.matches()) {
+            return new String[]{m.group(2), m.group(4)};
+        }
+        return null;
+    }
+
+    public int[] extractVersions(String versionString) {
+        String[] split = versionString.split(Pattern.quote("."));
+        int[] versions = {-1, -1, -1};
+        try {
+            for (int i = 0; i < Math.min(split.length, versions.length); i++) {
+                versions[i] = Integer.parseInt(split[i]);
+            }
+        } catch (NumberFormatException nfe) {
+            // Should we get a malformed version string, make sure we log it
+            logger.log(Level.WARNING, "Malformed version string: " + versionString);
+        }
+        return versions;
+    }
+
+}
--- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/DependencyManagerTest.java	Thu Jan 26 11:10:27 2017 -0500
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/DependencyManagerTest.java	Thu Jan 26 12:14:51 2017 -0500
@@ -83,11 +83,22 @@
         underneathTheBridge.deleteOnExit();
         userPluginRoot.deleteOnExit();
         systemPluginRoot.deleteOnExit();
-        Path a = createJar("a", null, underneathTheBridge.toPath());
-        Path b = createJar("b", "a", underneathTheBridge.toPath());
-        Path c = createJar("c", "b", underneathTheBridge.toPath());
-        Path d = createJar("d", "b,c", underneathTheBridge.toPath());
-        Path e = createJar("e", "d", underneathTheBridge.toPath());
+        createJar("Bundle1", "com.redhat.thermostat.bundle1;version=\"1.1.1\",com.redhat.thermostat.bundle1.package1;version=\"1.1.1\",com.redhat.thermostat.bundle1.package2;version=\"1.1.2\"", "", "1.1.1", underneathTheBridge.toPath());
+        createJar("Bundle2", "com.redhat.thermostat.bundle2;version=\"1.1.1\",com.redhat.thermostat.bundle2.package1;version=\"1.1.1\",com.redhat.thermostat.bundle2.package2;version=\"1.1.3\"", "com.redhat.thermostat.bundle1;version=\"[1,2)\"", "1.1.1", underneathTheBridge.toPath());
+        createJar("Bundle3", "com.redhat.thermostat.bundle3;version=\"2.1.0\",com.redhat.thermostat.bundle3.package1;version=\"2.1.1\",com.redhat.thermostat.bundle3.package4;version=\"2.1.2\"", "com.redhat.thermostat.bundle1;version=\"[1.1.1,1.1.2)\",com.redhat.thermostat.bundle2.package1;version=\"[1.1.1,1.1.2)\"", "2.1.0", underneathTheBridge.toPath());
+        createJar("Bundle4-9.1.0", "com.redhat.thermostat.bundle4;version=\"9.1.0\",com.redhat.thermostat.bundle4.package1;version=\"9.1.0\",com.redhat.thermostat.bundle4.package2;version=\"9.1.0\"", "com.redhat.thermostat.bundle1,com.redhat.thermostat.bundle2.package1;version=\"1.1.1\",com.redhat.thermostat.bundle3.package4;version=\"[2,3)\"", "9.1.0", underneathTheBridge.toPath());
+        createJar("Bundle4-9.1.3", "com.redhat.thermostat.bundle4;version=\"9.1.3\",com.redhat.thermostat.bundle4.package1;version=\"9.1.3\",com.redhat.thermostat.bundle4.package2;version=\"9.1.3\"", "com.redhat.thermostat.bundle1,com.redhat.thermostat.bundle2.package1;version=\"1.1.1\",com.redhat.thermostat.bundle3.package4;version=\"[2,3)\"", "9.1.3", underneathTheBridge.toPath());
+        createJar("Bundle4-9.3", "com.redhat.thermostat.bundle4;version=\"9.3\",com.redhat.thermostat.bundle4.package1;version=\"9.3\",com.redhat.thermostat.bundle4.package2;version=\"9.3\"", "com.redhat.thermostat.bundle1,com.redhat.thermostat.bundle2.package1;version=\"1.1.1\",com.redhat.thermostat.bundle3.package4;version=\"[2,3)\"", "9.3", underneathTheBridge.toPath());
+        createJar("Bundle5", "com.redhat.thermostat.bundle5;version=\"1.1\"", "com.redhat.thermostat.bundle4.package1;version=\"[9.1.3,9.1.4)\"", "1.1", userPluginRoot.toPath());
+        createJar("Bundle6", "com.redhat.thermostat.bundle6;version=\"13.1\"", "com.redhat.thermostat.bundle4;version=\"[9.2,10)\"", "13.1", userPluginRoot.toPath());
+        createJar("Bundle7", "com.redhat.thermostat.bundle7;version=\"1.0\"", "com.redhat.thermostat.bundle1;version=\"3.1\",com.redhat.thermostat.bundle2;version=\"9.9.9\",com.redhat.thermostat.bundle4;version=\"[10,11]\"", "1.0", userPluginRoot.toPath());
+        createJar("Bundle8", "com.redhat.thermostat.bundle8;version=\"1.0\",com.redhat.thermostat.framework;version=\"4.2.0\"", "", "1.0", userPluginRoot.toPath());
+        createJar("Bundle9", "com.redhat.thermostat.bundle9;version=\"1.0\"", "com.redhat.thermostat.framework;version=\"4.2.0\"", "1.0", userPluginRoot.toPath());
+
+        createJar("Cycle-1", "cycle1;version=\"1.0\"", "cycle2;version=\"1.0\"", "1.0", underneathTheBridge.toPath());
+        createJar("Cycle-2", "cycle2;version=\"1.0\"", "cycle3;version=\"1.0\"", "1.0", underneathTheBridge.toPath());
+        createJar("Cycle-3", "cycle3;version=\"1.0\"", "cycle1;version=\"1.0\"", "1.0", underneathTheBridge.toPath());
+        createJar("Cycle-Connector", "cycle4;version=\"1.0\"", "cycle1;version=\"1.0\"", "1.0", underneathTheBridge.toPath());
         when(paths.getUserPluginRoot()).thenReturn(userPluginRoot);
         when(paths.getSystemPluginRoot()).thenReturn(systemPluginRoot);
         when(paths.getSystemLibRoot()).thenReturn(underneathTheBridge);
@@ -106,23 +117,58 @@
     }
 
     @Test
-    public void testGetBundle() throws Exception {
-        ArrayList<BundleInformation> bundles = new ArrayList<>(depManager.getDependencies(new BundleInformation("d", "1.0")));
-        assertEquals(4, bundles.size());
-        assertEquals("d", bundles.get(0).getName());
-        assertEquals("1.0", bundles.get(0).getVersion());
-        assertEquals("c", bundles.get(1).getName());
-        assertEquals("1.0", bundles.get(1).getVersion());
-        assertEquals("b", bundles.get(2).getName());
-        assertEquals("1.0", bundles.get(2).getVersion());
-        assertEquals("a", bundles.get(3).getName());
-        assertEquals("1.0", bundles.get(3).getVersion());
+    public void testBundleWithNoDependencies() {
+        ArrayList<BundleInformation> results = new ArrayList<>(depManager.getDependencies(new BundleInformation("Bundle1", "1.1.1")));
+        assertEquals(0, results.size());
+    }
+
+    @Test
+    public void testDependencySearch() {
+        ArrayList<BundleInformation> results = new ArrayList<>(depManager.getDependencies(new BundleInformation("Bundle4-9.1.0", "9.1.0")));
+        assertEquals("Bundle4-9.1.0", results.get(0).getName());
+        assertEquals("9.1.0", results.get(0).getVersion());
+        assertEquals("Bundle3", results.get(1).getName());
+        assertEquals("2.1.0", results.get(1).getVersion());
+        assertEquals("Bundle2", results.get(2).getName());
+        assertEquals("1.1.1", results.get(2).getVersion());
+        assertEquals("Bundle1", results.get(3).getName());
+        assertEquals("1.1.1", results.get(3).getVersion());
     }
 
     @Test
-    public void testMismatchBundleVersion() throws Exception {
-        List<BundleInformation> bundles = depManager.getDependencies(new BundleInformation("d", "1.2"));
-        assertEquals(0, bundles.size());
+    public void testVersionDependency() {
+        ArrayList<BundleInformation> results = new ArrayList<>(depManager.getDependencies(new BundleInformation("Bundle5", "1.1")));
+        assertEquals("Bundle5", results.get(0).getName());
+        assertEquals("1.1", results.get(0).getVersion());
+        assertEquals("Bundle4-9.1.3", results.get(1).getName());
+        assertEquals("9.1.3", results.get(1).getVersion());
+        assertEquals("Bundle3", results.get(2).getName());
+        assertEquals("2.1.0", results.get(2).getVersion());
+        assertEquals("Bundle2", results.get(3).getName());
+        assertEquals("1.1.1", results.get(3).getVersion());
+        assertEquals("Bundle1", results.get(4).getName());
+        assertEquals("1.1.1", results.get(4).getVersion());
+    }
+
+    @Test
+    public void testVersionDependency2() {
+        ArrayList<BundleInformation> results = new ArrayList<>(depManager.getDependencies(new BundleInformation("Bundle6", "13.1")));
+        assertEquals("Bundle6", results.get(0).getName());
+        assertEquals("13.1", results.get(0).getVersion());
+        assertEquals("Bundle4-9.3", results.get(1).getName());
+        assertEquals("9.3", results.get(1).getVersion());
+        assertEquals("Bundle3", results.get(2).getName());
+        assertEquals("2.1.0", results.get(2).getVersion());
+        assertEquals("Bundle2", results.get(3).getName());
+        assertEquals("1.1.1", results.get(3).getVersion());
+        assertEquals("Bundle1", results.get(4).getName());
+        assertEquals("1.1.1", results.get(4).getVersion());
+    }
+
+    @Test
+    public void testMissingVersion() {
+        ArrayList<BundleInformation> results = new ArrayList<>(depManager.getDependencies(new BundleInformation("Bundle7", "1.0")));
+        assertEquals(0, results.size());
     }
 
     @Test
@@ -152,16 +198,26 @@
         assertEquals(underneathTheBridge.toPath(), testManager.getLocations().get(2));
     }
 
-    private Path createJar(String exportsDirective, String importDirective, Path base) throws Exception {
+    @Test (expected = IllegalStateException.class)
+    public void testInvalidStart() {
+        ArrayList<BundleInformation> result = new ArrayList<>(depManager.getDependencies(new BundleInformation("Cycle-1", "1.0")));
+    }
+
+    @Test (expected = IllegalStateException.class)
+    public void testCycle() {
+        ArrayList<BundleInformation> result = new ArrayList<>(depManager.getDependencies(new BundleInformation("Cycle-Connector", "1.0")));
+    }
+
+    private Path createJar(String name, String exportsDirective, String importDirective, String version, Path base) throws Exception {
         Manifest manifest = new Manifest();
-        manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
+        manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, version);
         manifest.getMainAttributes().put(new Attributes.Name("Export-Package"), exportsDirective + ";");
         if (importDirective != null) {
-                manifest.getMainAttributes().put(new Attributes.Name("Import-Package"), importDirective + ";");
-            }
-        Path path = Paths.get(base.toFile().getAbsoluteFile() + "/" + exportsDirective + ".jar");
-        manifest.getMainAttributes().put(new Attributes.Name("Bundle-SymbolicName"), exportsDirective);
-        manifest.getMainAttributes().put(new Attributes.Name("Bundle-Version"), "1.0");
+            manifest.getMainAttributes().put(new Attributes.Name("Import-Package"), importDirective + ";");
+        }
+        Path path = Paths.get(base.toFile().getAbsoluteFile() + "/" + name + ".jar");
+        manifest.getMainAttributes().put(new Attributes.Name("Bundle-SymbolicName"), name);
+        manifest.getMainAttributes().put(new Attributes.Name("Bundle-Version"), version);
         FileOutputStream stream = new FileOutputStream(path.toFile());
         JarOutputStream target = new JarOutputStream(stream, manifest);
         target.close();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/MetadataHandlerTest.java	Thu Jan 26 12:14:51 2017 -0500
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.launcher.internal;
+
+import com.redhat.thermostat.launcher.BundleInformation;
+
+import java.util.List;
+import java.util.ArrayList;
+
+import junit.framework.Assert;
+import org.junit.Test;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.assertFalse;
+
+public class MetadataHandlerTest {
+
+    private static final MetadataHandler handler = new MetadataHandler();
+
+    @Test
+    public void testManifestParser() {
+        String exportHeader = "com.redhat.thermostat.bundle1;version=\"1.1.1\",com.redhat.thermosta"+
+                "t.bundle1.package1;version=\"1.1.1\",com.redhat.thermostat.bundle1.package2;version=\"1.1.2\"";
+        List<BundleInformation> exports = handler.parseHeader(exportHeader);
+        assertEquals(exports.size(), 3);
+        assertTrue(exports.contains(new BundleInformation("com.redhat.thermostat.bundle1", "1.1.1")));
+        assertTrue(exports.contains(new BundleInformation("com.redhat.thermostat.bundle1.package1", "1.1.1")));
+        assertTrue(exports.contains(new BundleInformation("com.redhat.thermostat.bundle1.package2", "1.1.2")));
+    }
+
+    @Test
+    public void testExtraDirectives() {
+        String importHeader = "com.redhat.thermostat.bundle1;version=\"[9.1,10)" +
+                "\",com.redhat.thermostat.bundle1.package1;version=\"[9.1,10)\",com.redhat.thermostat.bundle1." +
+                "package2;resolution:=optional;version=\"9.1\"";
+        List<BundleInformation> imports = handler.parseHeader(importHeader);
+        assertEquals(imports.size(), 3);
+        assertTrue(imports.contains(new BundleInformation("com.redhat.thermostat.bundle1", "[9.1,10)")));
+        assertTrue(imports.contains(new BundleInformation("com.redhat.thermostat.bundle1.package1", "[9.1,10)")));
+        assertTrue(imports.contains(new BundleInformation("com.redhat.thermostat.bundle1.package2", "9.1")));
+    }
+
+    @Test
+    public void testManifestParser2() {
+        String exportHeader = "com.redhat.thermostat.bundle1;version=\"[6.7.1,6.8)\",com.redhat.thermostat" +
+                ".bundle1.package1;version=\"[6.8.8,6.8.9]\",com.redhat.thermostat.bundle1.package2;version=\"(9.1.2,9.3)\"";
+        List<BundleInformation> exports = handler.parseHeader(exportHeader);
+        assertEquals(exports.size(), 3);
+        assertTrue(exports.contains(new BundleInformation("com.redhat.thermostat.bundle1", "[6.7.1,6.8)")));
+        assertTrue(exports.contains(new BundleInformation("com.redhat.thermostat.bundle1.package1", "[6.8.8,6.8.9]")));
+        assertTrue(exports.contains(new BundleInformation("com.redhat.thermostat.bundle1.package2", "(9.1.2,9.3)")));
+    }
+
+    @Test
+    public void testEmptyHeader() {
+        String header = "";
+        List<BundleInformation> exports = new ArrayList<>(handler.parseHeader(header));
+        assertEquals(0, exports.size());
+    }
+
+    @Test
+    public void testVersionParser() {
+        String versionString = "[1.1.2,1.4.3)";
+        String[] versions = handler.parseVersionRange(versionString);
+        assertEquals("1.1.2", versions[0]);
+        assertEquals("1.4.3", versions[1]);
+        int[] lowerBound = handler.extractVersions(versions[0]);
+        int[] upperBound = handler.extractVersions(versions[1]);
+        assertEquals(1, lowerBound[0]);
+        assertEquals(1, lowerBound[1]);
+        assertEquals(2, lowerBound[2]);
+        assertEquals(1, upperBound[0]);
+        assertEquals(4, upperBound[1]);
+        assertEquals(3, upperBound[2]);
+    }
+
+    @Test
+    public void testParserNonFullVersion() {
+        String versionString = "[1,3.2]";
+        String[] versions = handler.parseVersionRange(versionString);
+        Assert.assertEquals("1", versions[0]);
+        Assert.assertEquals("3.2", versions[1]);
+        // The version extractor returns -1 for missing versions
+        int[] lowerBound = handler.extractVersions(versions[0]);
+        int[] upperBound = handler.extractVersions(versions[1]);
+        assertEquals(1, lowerBound[0]);
+        assertEquals(-1, lowerBound[1]);
+        assertEquals(-1, lowerBound[2]);
+        assertEquals(3, upperBound[0]);
+        assertEquals(2, upperBound[1]);
+        assertEquals(-1, upperBound[2]);
+    }
+
+    @Test
+    public void testNotAVersion() {
+        String versionString = "foo.bar.baz";
+        int[] result = handler.extractVersions(versionString);
+        assertEquals(-1, result[0]);
+        assertEquals(-1, result[1]);
+        assertEquals(-1, result[2]);
+    }
+
+    @Test
+    public void testVersionMatcher() {
+        String versionString = "[1,2]";
+        String[] versions = handler.parseVersionRange(versionString);
+        int[] lowerBound = handler.extractVersions(versions[0]);
+        int[] upperBound = handler.extractVersions(versions[1]);
+        assertTrue(handler.satisfiesBound(new int[]{1, 1, 1}, lowerBound, false));
+        assertTrue(handler.satisfiesBound(upperBound, new int[]{1, 1, 1}, false));
+    }
+
+    @Test
+    public void testVersionMatcher2() {
+        String versionString = "[1.1,2]";
+        String[] versions = handler.parseVersionRange(versionString);
+        int[] lowerBound = handler.extractVersions(versions[0]);
+        int[] upperBound = handler.extractVersions(versions[1]);
+        assertFalse(handler.satisfiesBound(new int[]{1, -1, -1}, lowerBound, false));
+        assertTrue(handler.satisfiesBound(upperBound, new int[]{1, -1, -1}, false));
+    }
+
+    @Test
+    public void testVersionMatcher3() {
+        String versionString = "[1.1.1,2]";
+        String[] versions = handler.parseVersionRange(versionString);
+        int[] lowerBound = handler.extractVersions(versions[0]);
+        int[] upperBound = handler.extractVersions(versions[1]);
+        assertFalse(handler.satisfiesBound(new int[]{1, 1, -1}, lowerBound, false));
+        assertTrue(handler.satisfiesBound(upperBound, new int[]{1, 1, -1}, false));
+    }
+
+    @Test
+    public void testInclusiveRange() {
+        String versionString = "[2.3.4,3)";
+        String[] versions = handler.parseVersionRange(versionString);
+        int[] upperBound = handler.extractVersions(versions[1]);
+        assertTrue(handler.satisfiesBound(upperBound, new int[]{3, -1, -1}, false));
+        assertFalse(handler.satisfiesBound(upperBound, new int[]{3, -1, -1}, true));
+    }
+}