brooklyn-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From geom...@apache.org
Subject [04/17] brooklyn-server git commit: refactor osgi installation to facilitate upgrades and richer return semantics
Date Tue, 06 Jun 2017 12:25:24 GMT
refactor osgi installation to facilitate upgrades and richer return semantics

rebind can break however -- we need to phase the bundle installs and the bundle starts


Project: http://git-wip-us.apache.org/repos/asf/brooklyn-server/repo
Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-server/commit/14524547
Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-server/tree/14524547
Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-server/diff/14524547

Branch: refs/heads/master
Commit: 145245479ea31c6aeffffb86533cb988b39f1820
Parents: f76118c
Author: Alex Heneveld <alex.heneveld@cloudsoftcorp.com>
Authored: Thu May 4 17:35:32 2017 +0100
Committer: Alex Heneveld <alex.heneveld@cloudsoftcorp.com>
Committed: Fri May 5 12:49:30 2017 +0100

----------------------------------------------------------------------
 .../catalog/CatalogMakeOsgiBundleTest.java      |   9 +-
 .../CatalogOsgiVersionMoreEntityRebindTest.java |  80 ++++-
 .../CatalogOsgiVersionMoreEntityTest.java       |  15 +-
 .../apache/brooklyn/core/BrooklynVersion.java   |   3 +-
 .../catalog/internal/BasicBrooklynCatalog.java  |  10 +-
 .../core/catalog/internal/CatalogUtils.java     |   3 +-
 .../core/mgmt/ha/OsgiArchiveInstaller.java      | 359 +++++++++++++++++++
 .../mgmt/ha/OsgiBundleInstallationResult.java   |  71 ++++
 .../brooklyn/core/mgmt/ha/OsgiManager.java      | 129 ++-----
 .../core/mgmt/rebind/RebindContextImpl.java     |   4 +-
 .../core/typereg/BasicManagedBundle.java        |   5 +
 .../apache/brooklyn/rest/domain/ApiError.java   |  21 +-
 .../rest/resources/CatalogResource.java         | 137 ++-----
 .../brooklyn/util/maven/MavenArtifact.java      |   3 +-
 .../brooklyn/util/text/VersionComparator.java   |   9 +-
 .../util/text/VersionComparatorTest.java        |   7 +
 16 files changed, 636 insertions(+), 229 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/14524547/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogMakeOsgiBundleTest.java
----------------------------------------------------------------------
diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogMakeOsgiBundleTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogMakeOsgiBundleTest.java
index 6eca121..044a2ea 100644
--- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogMakeOsgiBundleTest.java
+++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogMakeOsgiBundleTest.java
@@ -37,10 +37,11 @@ import org.apache.brooklyn.core.BrooklynFeatureEnablement;
 import org.apache.brooklyn.core.catalog.internal.CatalogBomScanner;
 import org.apache.brooklyn.core.entity.Entities;
 import org.apache.brooklyn.core.entity.EntityAsserts;
+import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult;
 import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
+import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
 import org.apache.brooklyn.core.sensor.Sensors;
 import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
-import org.apache.brooklyn.core.typereg.BasicManagedBundle;
 import org.apache.brooklyn.test.Asserts;
 import org.apache.brooklyn.util.collections.MutableList;
 import org.apache.brooklyn.util.collections.MutableMap;
@@ -172,10 +173,8 @@ public class CatalogMakeOsgiBundleTest extends AbstractYamlTest {
 
     private void installBundle(File jf) {
         try (FileInputStream fin = new FileInputStream(jf)) {
-            BasicManagedBundle bundleMetadata = new BasicManagedBundle();
-            Bundle bundle =
-                ((LocalManagementContext)mgmt()).getOsgiManager().get().installUploadedBundle(bundleMetadata, fin, true);
-            bundlesToRemove.add(bundle);
+            OsgiBundleInstallationResult br = ((ManagementContextInternal)mgmt()).getOsgiManager().get().install(fin).get();
+            bundlesToRemove.add(br.getBundle());
         } catch (Exception e) {
             throw Exceptions.propagate(e);
         }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/14524547/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityRebindTest.java
----------------------------------------------------------------------
diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityRebindTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityRebindTest.java
index e47f719..a258fcd 100644
--- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityRebindTest.java
+++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityRebindTest.java
@@ -33,10 +33,10 @@ import org.apache.brooklyn.camp.brooklyn.AbstractYamlRebindTest;
 import org.apache.brooklyn.core.effector.Effectors;
 import org.apache.brooklyn.core.entity.EntityInternal;
 import org.apache.brooklyn.core.entity.StartableApplication;
+import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult;
 import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
 import org.apache.brooklyn.core.mgmt.osgi.OsgiVersionMoreEntityTest;
 import org.apache.brooklyn.core.test.entity.TestEntity;
-import org.apache.brooklyn.core.typereg.BasicManagedBundle;
 import org.apache.brooklyn.entity.stock.BasicApplication;
 import org.apache.brooklyn.test.Asserts;
 import org.apache.brooklyn.test.support.TestResourceUnavailableException;
@@ -70,8 +70,8 @@ public class CatalogOsgiVersionMoreEntityRebindTest extends AbstractYamlRebindTe
     @Test
     public void testRebindAppIncludingBundle() throws Exception {
         TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_PATH);
-        ((ManagementContextInternal)mgmt()).getOsgiManager().get().installUploadedBundle(new BasicManagedBundle(), 
-            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V1_URL), true);
+        ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( 
+            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V1_URL) );
         
         createAndStartApplication("services: [ { type: "+BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY+" } ]");
         
@@ -208,15 +208,14 @@ public class CatalogOsgiVersionMoreEntityRebindTest extends AbstractYamlRebindTe
         TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_OSGI_MORE_ENTITIES_0_1_0_PATH);
         
         // install dependency
-        ((ManagementContextInternal)mgmt()).getOsgiManager().get().installUploadedBundle(new BasicManagedBundle(), 
-            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_OSGI_ENTITIES_URL), true);
+        ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( 
+            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_OSGI_ENTITIES_URL) );
 
         // now the v2 bundle
-        BasicManagedBundle mb = new BasicManagedBundle();
-        Bundle b2a = ((ManagementContextInternal)mgmt()).getOsgiManager().get().installUploadedBundle(mb, 
-            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_URL), true);
+        OsgiBundleInstallationResult b = ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( 
+            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_URL) ).get();
 
-        Assert.assertEquals(mb.getVersionedName().toString(), BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL+":"+"0.2.0");
+        Assert.assertEquals(b.getVersionedName().toString(), BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL+":"+"0.2.0");
         
         String yaml = Strings.lines("name: simple-app-yaml",
                 "services:",
@@ -228,8 +227,8 @@ public class CatalogOsgiVersionMoreEntityRebindTest extends AbstractYamlRebindTe
             more.invoke(Effectors.effector(String.class, "sayHI").buildAbstract(), MutableMap.of("name", "Bob")).get(),
             "HI BOB FROM V2");
         
-        ((ManagementContextInternal)mgmt()).getOsgiManager().get().uninstallUploadedBundle(mb);
-        Assert.assertEquals(b2a.getState(), Bundle.UNINSTALLED);
+        ((ManagementContextInternal)mgmt()).getOsgiManager().get().uninstallUploadedBundle(b.getMetadata());
+        Assert.assertEquals(b.getBundle().getState(), Bundle.UNINSTALLED);
 
         // can still call things
         Assert.assertEquals(
@@ -237,7 +236,7 @@ public class CatalogOsgiVersionMoreEntityRebindTest extends AbstractYamlRebindTe
             "HI CLAUDIA FROM V2");
         
         // but still uninstalled, and attempt to create makes error 
-        Assert.assertEquals(b2a.getState(), Bundle.UNINSTALLED);
+        Assert.assertEquals(b.getBundle().getState(), Bundle.UNINSTALLED);
         try {
             Entity app2 = createAndStartApplication(yaml);
             Asserts.shouldHaveFailedPreviously("Expected deployment to fail after uninstall; instead got "+app2);
@@ -254,5 +253,62 @@ public class CatalogOsgiVersionMoreEntityRebindTest extends AbstractYamlRebindTe
         }
     }
     
+    @Test
+    public void testClassAccessAfterUpgrade() throws Exception {
+        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_OSGI_MORE_ENTITIES_0_1_0_PATH);
+        
+        // install dependency
+        ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( 
+            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_OSGI_ENTITIES_URL) ).checkNoError();
+
+        // now the v2 bundle
+        OsgiBundleInstallationResult b2a = ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( 
+            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_URL) ).get();
+
+        Assert.assertEquals(b2a.getVersionedName().toString(), BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL+":"+"0.2.0");
+        Assert.assertEquals(b2a.getCode(), OsgiBundleInstallationResult.ResultCode.INSTALLED_NEW_BUNDLE);
+        
+        String yaml = Strings.lines("name: simple-app-yaml",
+                "services:",
+                "- type: " + BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY);
+        Entity app = createAndStartApplication(yaml);
+        Entity more = Iterables.getOnlyElement( app.getChildren() );
+        
+        Assert.assertEquals(
+            more.invoke(Effectors.effector(String.class, "sayHI").buildAbstract(), MutableMap.of("name", "Bob")).get(),
+            "HI BOB FROM V2");
+        
+        // unforced upgrade should report already installed
+        Assert.assertEquals( ((ManagementContextInternal)mgmt()).getOsgiManager().get().install(
+            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_URL) ).get().getCode(),
+            OsgiBundleInstallationResult.ResultCode.IGNORING_BUNDLE_AREADY_INSTALLED);
+        
+        // force upgrade
+        OsgiBundleInstallationResult b2b = ((ManagementContextInternal)mgmt()).getOsgiManager().get().install(b2a.getMetadata(), 
+            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_URL), true, true).get();
+        Assert.assertEquals(b2a.getBundle(), b2b.getBundle());
+        Assert.assertEquals(b2b.getCode(), OsgiBundleInstallationResult.ResultCode.UPDATED_EXISTING_BUNDLE);
+
+        // calls to things previously instantiated get the old behaviour
+        Assert.assertEquals(
+            more.invoke(Effectors.effector(String.class, "sayHI").buildAbstract(), MutableMap.of("name", "Claudia")).get(),
+            "HI CLAUDIA FROM V2");
+        
+        // but new deployment gets the new behaviour 
+        StartableApplication app2 = (StartableApplication) createAndStartApplication(yaml);
+        Entity more2 = Iterables.getOnlyElement( app2.getChildren() );
+        Assert.assertEquals(
+            more2.invoke(Effectors.effector(String.class, "sayHI").buildAbstract(), MutableMap.of("name", "Daphne")).get(),
+            "HO DAPHNE FROM V2 EVIL TWIN");
+        app2.stop();
+        
+        // and after rebind on the old we get new behaviour
+        StartableApplication app1 = rebind();
+        Entity more1 = Iterables.getOnlyElement( app1.getChildren() );
+        Assert.assertEquals(
+            more1.invoke(Effectors.effector(String.class, "sayHI").buildAbstract(), MutableMap.of("name", "Eric")).get(),
+            "HO ERIC FROM V2 EVIL TWIN");
+    }
+    
 
 }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/14524547/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityTest.java
----------------------------------------------------------------------
diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityTest.java
index cb38df5..aebfed9 100644
--- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityTest.java
+++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityTest.java
@@ -33,10 +33,10 @@ import org.apache.brooklyn.api.typereg.ManagedBundle;
 import org.apache.brooklyn.api.typereg.RegisteredType;
 import org.apache.brooklyn.camp.brooklyn.AbstractYamlTest;
 import org.apache.brooklyn.camp.brooklyn.spi.creation.BrooklynEntityMatcher;
+import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult;
 import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
 import org.apache.brooklyn.core.mgmt.osgi.OsgiVersionMoreEntityTest;
 import org.apache.brooklyn.core.objs.BrooklynTypes;
-import org.apache.brooklyn.core.typereg.BasicManagedBundle;
 import org.apache.brooklyn.core.typereg.RegisteredTypePredicates;
 import org.apache.brooklyn.core.typereg.RegisteredTypes;
 import org.apache.brooklyn.test.Asserts;
@@ -44,7 +44,6 @@ import org.apache.brooklyn.test.support.TestResourceUnavailableException;
 import org.apache.brooklyn.util.core.ResourceUtils;
 import org.apache.brooklyn.util.osgi.OsgiTestResources;
 import org.apache.brooklyn.util.text.Strings;
-import org.osgi.framework.Bundle;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.testng.Assert;
@@ -69,21 +68,19 @@ public class CatalogOsgiVersionMoreEntityTest extends AbstractYamlTest implement
 
     @Test
     public void testBrooklynManagedBundleInstall() throws Exception {
-        BasicManagedBundle mb = new BasicManagedBundle();
-        Bundle b = ((ManagementContextInternal)mgmt()).getOsgiManager().get().installUploadedBundle(mb, 
-            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V1_URL), true);
-        Assert.assertEquals(mb.getSymbolicName(), b.getSymbolicName());
-        Assert.assertEquals(mb.getVersion(), "0.1.0");
+        OsgiBundleInstallationResult br = ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( 
+            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V1_URL) ).get();
+        Assert.assertEquals(br.getVersionedName().toString(), BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL+":"+"0.1.0");
         
         // bundle installed
         Map<String, ManagedBundle> bundles = ((ManagementContextInternal)mgmt()).getOsgiManager().get().getManagedBundles();
         Asserts.assertSize(bundles.keySet(), 1);
-        Assert.assertEquals(mb.getId(), Iterables.getOnlyElement( bundles.keySet() ));
+        Assert.assertEquals(br.getMetadata().getId(), Iterables.getOnlyElement( bundles.keySet() ));
         
         // types installed
         RegisteredType t = mgmt().getTypeRegistry().get(BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY);
         Assert.assertNotNull(t);
-        Assert.assertEquals(t.getContainingBundle(), b.getSymbolicName()+":"+b.getVersion());
+        Assert.assertEquals(t.getContainingBundle(), br.getVersionedName().toString());
         
         // can deploy
         createAndStartApplication("services: [ { type: "+BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY+" } ]");

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/14524547/core/src/main/java/org/apache/brooklyn/core/BrooklynVersion.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/BrooklynVersion.java b/core/src/main/java/org/apache/brooklyn/core/BrooklynVersion.java
index fc9556d..161eff0 100644
--- a/core/src/main/java/org/apache/brooklyn/core/BrooklynVersion.java
+++ b/core/src/main/java/org/apache/brooklyn/core/BrooklynVersion.java
@@ -46,6 +46,7 @@ import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.osgi.OsgiUtil;
 import org.apache.brooklyn.util.stream.Streams;
 import org.apache.brooklyn.util.text.Strings;
+import org.apache.brooklyn.util.text.VersionComparator;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.Constants;
 import org.osgi.framework.FrameworkUtil;
@@ -155,7 +156,7 @@ public class BrooklynVersion implements BrooklynVersionService {
 
     @Override
     public boolean isSnapshot() {
-        return (getVersion().indexOf("-SNAPSHOT") >= 0);
+        return VersionComparator.isSnapshot(getVersion());
     }
 
     private void readPropertiesFromMavenResource(ClassLoader resourceLoader) {

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/14524547/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java
index 9769f7e..c91b7cd 100644
--- a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java
+++ b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java
@@ -425,17 +425,25 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
         return getFirstAsMap(itemDef, "brooklyn.catalog").orNull();        
     }
     
+    /** @deprecated since 0.12.0 - use {@link #getVersionedName(Map, boolean)} */
+    @Deprecated
     public static VersionedName getVersionedName(Map<?,?> catalogMetadata) {
+        return getVersionedName(catalogMetadata, true);
+    }
+    
+    public static VersionedName getVersionedName(Map<?,?> catalogMetadata, boolean required) {
         String version = getFirstAs(catalogMetadata, String.class, "version").orNull();
         String bundle = getFirstAs(catalogMetadata, String.class, "bundle").orNull();
         if (Strings.isBlank(bundle) && Strings.isBlank(version)) {
+            if (!required) return null;
             throw new IllegalStateException("Catalog BOM must define bundle and version");
         }
         if (Strings.isBlank(bundle)) {
+            if (!required) return null;
             throw new IllegalStateException("Catalog BOM must define bundle");
         }
         if (Strings.isBlank(version)) {
-            throw new IllegalStateException("Catalog BOM must define version");
+            throw new IllegalStateException("Catalog BOM must define version if bundle is defined");
         }
         return new VersionedName(bundle, Version.valueOf(version));
     }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/14524547/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogUtils.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogUtils.java b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogUtils.java
index 776894f..394814d 100644
--- a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogUtils.java
+++ b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogUtils.java
@@ -43,6 +43,7 @@ import org.apache.brooklyn.core.mgmt.ha.OsgiManager;
 import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
 import org.apache.brooklyn.core.mgmt.rebind.RebindManagerImpl.RebindTracker;
 import org.apache.brooklyn.core.objs.BrooklynObjectInternal;
+import org.apache.brooklyn.core.typereg.BasicManagedBundle;
 import org.apache.brooklyn.core.typereg.RegisteredTypeLoadingContexts;
 import org.apache.brooklyn.core.typereg.RegisteredTypePredicates;
 import org.apache.brooklyn.core.typereg.RegisteredTypes;
@@ -170,7 +171,7 @@ public class CatalogUtils {
                     new Object[] {managementContext, Joiner.on(", ").join(libraries)});
             Stopwatch timer = Stopwatch.createStarted();
             for (CatalogBundle bundleUrl : libraries) {
-                osgi.get().registerBundle(bundleUrl);
+                osgi.get().install(BasicManagedBundle.of(bundleUrl), null, true, false).get();
             }
             if (log.isDebugEnabled()) 
                 logDebugOrTraceIfRebinding(log, 

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/14524547/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiArchiveInstaller.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiArchiveInstaller.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiArchiveInstaller.java
new file mode 100644
index 0000000..0e069bd
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiArchiveInstaller.java
@@ -0,0 +1,359 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.brooklyn.core.mgmt.ha;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import org.apache.brooklyn.api.typereg.ManagedBundle;
+import org.apache.brooklyn.core.catalog.internal.BasicBrooklynCatalog;
+import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult.ResultCode;
+import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
+import org.apache.brooklyn.core.typereg.BasicManagedBundle;
+import org.apache.brooklyn.util.core.ResourceUtils;
+import org.apache.brooklyn.util.core.osgi.BundleMaker;
+import org.apache.brooklyn.util.core.osgi.Osgis;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.exceptions.ReferenceWithError;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.os.Os;
+import org.apache.brooklyn.util.osgi.VersionedName;
+import org.apache.brooklyn.util.stream.Streams;
+import org.apache.brooklyn.util.text.Strings;
+import org.apache.brooklyn.util.text.VersionComparator;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+
+import com.google.common.base.Objects;
+
+// package-private so we can move this one if/when we move OsgiManager
+class OsgiArchiveInstaller {
+
+    final private OsgiManager osgiManager;
+    private ManagedBundle suppliedKnownBundleMetadata;
+    private InputStream zipIn;
+    
+    private boolean loadCatalogBom;
+    private boolean force;
+    
+    private File zipFile;
+    private Manifest discoveredManifest;
+    private VersionedName discoveredBomVersionedName;
+    OsgiBundleInstallationResult result;
+    
+    private ManagedBundle inferredMetadata;
+    
+    OsgiArchiveInstaller(OsgiManager osgiManager, ManagedBundle knownBundleMetadata, InputStream zipIn) {
+        this.osgiManager = osgiManager;
+        this.suppliedKnownBundleMetadata = knownBundleMetadata;
+        this.zipIn = zipIn;
+    }
+
+    public void setLoadCatalogBom(boolean loadCatalogBom) {
+        this.loadCatalogBom = loadCatalogBom;
+    }
+    
+    public void setForce(boolean force) {
+        this.force = force;
+    }
+
+    private ManagementContextInternal mgmt() {
+        return (ManagementContextInternal) osgiManager.mgmt;
+    }
+    
+    private synchronized void init() {
+        if (result!=null) {
+            if (zipFile!=null || zipIn==null) return;
+            throw new IllegalStateException("This installer instance has already been used and the input stream discarded");
+        }
+        result = new OsgiBundleInstallationResult();
+        inferredMetadata = suppliedKnownBundleMetadata==null ? new BasicManagedBundle() : suppliedKnownBundleMetadata;
+    }
+    
+    private synchronized void makeLocalZipFileFromInputStreamOrUrl() {
+        if (zipIn==null) {
+            Maybe<Bundle> installedBundle = Maybe.absent();
+            if (suppliedKnownBundleMetadata!=null) {
+                // if no input stream, look for a URL and/or a matching bundle
+                if (installedBundle.isAbsent() && suppliedKnownBundleMetadata.getOsgiUniqueUrl()!=null) {
+                    installedBundle = Osgis.bundleFinder(osgiManager.framework).requiringFromUrl(suppliedKnownBundleMetadata.getOsgiUniqueUrl()).find();
+                }
+                if (installedBundle.isAbsent() && suppliedKnownBundleMetadata.getUrl()!=null) {
+                    installedBundle = Osgis.bundleFinder(osgiManager.framework).requiringFromUrl(suppliedKnownBundleMetadata.getUrl()).find();
+                }
+                if (installedBundle.isAbsent() && suppliedKnownBundleMetadata.isNameResolved()) {
+                    installedBundle = Osgis.bundleFinder(osgiManager.framework).symbolicName(suppliedKnownBundleMetadata.getSymbolicName()).version(suppliedKnownBundleMetadata.getVersion()).find();
+                }
+                if (suppliedKnownBundleMetadata.getUrl()!=null) {
+                    if (installedBundle.isAbsent() || force) {
+                        // reload 
+                        zipIn = ResourceUtils.create(mgmt()).getResourceFromUrl(suppliedKnownBundleMetadata.getUrl());
+                    }
+                }
+            }
+            
+            if (installedBundle.isPresent()) {
+                result.bundle = installedBundle.get();
+                
+                if (zipIn==null) {
+                    // no way to install (no url), or no need to install (not forced); just ignore it
+                    result.metadata = osgiManager.getManagedBundle(new VersionedName(installedBundle.get()));
+                    result.setIgnoringAlreadyInstalled();
+                    return;
+                }
+            } else {
+                result.metadata = suppliedKnownBundleMetadata;
+                throw new IllegalArgumentException("No input stream available and no URL could be found; nothing to install");
+            }
+        }
+        
+        zipFile = Os.newTempFile("brooklyn-bundle-transient-"+suppliedKnownBundleMetadata, "zip");
+        try {
+            FileOutputStream fos = new FileOutputStream(zipFile);
+            Streams.copy(zipIn, fos);
+            zipIn.close();
+            fos.close();
+        } catch (Exception e) {
+            throw Exceptions.propagate(e);
+        } finally {
+            zipIn = null;
+        }
+    }
+
+    private void discoverManifestFromCatalogBom(boolean isCatalogBomRequired) {
+        discoveredManifest = new BundleMaker(mgmt()).getManifest(zipFile);
+        ZipFile zf = null;
+        try {
+            try {
+                zf = new ZipFile(zipFile);
+            } catch (IOException e) {
+                throw new IllegalArgumentException("Invalid ZIP/JAR archive: "+e);
+            }
+            ZipEntry bom = zf.getEntry("catalog.bom");
+            if (bom==null) {
+                bom = zf.getEntry("/catalog.bom");
+            }
+            if (bom==null) {
+                if (isCatalogBomRequired) {
+                    throw new IllegalArgumentException("Archive must contain a catalog.bom file in the root");
+                } else {
+                    return;
+                }
+            }
+            String bomS;
+            try {
+                bomS = Streams.readFullyString(zf.getInputStream(bom));
+            } catch (IOException e) {
+                throw new IllegalArgumentException("Error reading catalog.bom from ZIP/JAR archive: "+e);
+            }
+            discoveredBomVersionedName = BasicBrooklynCatalog.getVersionedName( BasicBrooklynCatalog.getCatalogMetadata(bomS), false );
+        } finally {
+            Streams.closeQuietly(zf);
+        }
+    }
+    
+    private void updateManifestFromAllSourceInformation() {
+        if (discoveredBomVersionedName!=null) {
+            matchSetOrFail("catalog.bom in archive", discoveredBomVersionedName.getSymbolicName(), discoveredBomVersionedName.getVersion().toString());
+        }
+        
+        boolean manifestNeedsUpdating = false;
+        if (discoveredManifest==null) {
+            discoveredManifest = new Manifest();
+            manifestNeedsUpdating = true;
+        }
+        if (!matchSetOrFail("MANIFEST.MF in archive", discoveredManifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME),
+                discoveredManifest.getMainAttributes().getValue(Constants.BUNDLE_VERSION) )) {
+            manifestNeedsUpdating = true;                
+            discoveredManifest.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, inferredMetadata.getSymbolicName());
+            discoveredManifest.getMainAttributes().putValue(Constants.BUNDLE_VERSION, inferredMetadata.getVersion());
+        }
+        if (Strings.isBlank(inferredMetadata.getSymbolicName())) {
+            throw new IllegalArgumentException("Missing bundle symbolic name in BOM or MANIFEST");
+        }
+        if (Strings.isBlank(inferredMetadata.getVersion())) {
+            throw new IllegalArgumentException("Missing bundle version in BOM or MANIFEST");
+        }
+        if (discoveredManifest.getMainAttributes().getValue(Attributes.Name.MANIFEST_VERSION)==null) {
+            discoveredManifest.getMainAttributes().putValue(Attributes.Name.MANIFEST_VERSION.toString(), "1.0");
+            manifestNeedsUpdating = true;                
+        }
+        if (manifestNeedsUpdating) {
+            File zf2 = new BundleMaker(mgmt()).copyAddingManifest(zipFile, discoveredManifest);
+            zipFile.delete();
+            zipFile = zf2;
+        }
+    }
+    
+    private synchronized void close() {
+        if (zipFile!=null) {
+            zipFile.delete();
+            zipFile = null;
+        }
+    }
+    
+    /**
+     * Installs a bundle, taking from ZIP input stream if supplied, falling back to URL in the {@link ManagedBundle} metadata supplied.
+     * It will take metadata from any of: a MANIFEST.MF in the ZIP; a catalog.bom in the ZIP; the {@link ManagedBundle} metadata supplied.
+     * If metadata is supplied in multiple such places, it must match.
+     * Appropriate metadata will be added to the ZIP and installation attempted.
+     * <p>
+     * If a matching bundle is already installed, the installation will stop with a {@link ResultCode#IGNORING_BUNDLE_AREADY_INSTALLED}
+     * unless the bundle is a snapshot or "force" is specified.
+     * In the latter two cases, if there is an installed matching bundle, that bundle will be updated with the input stream here,
+     * with any catalog items from the old bundle removed and those from this bundle installed.
+     * <p>
+     * Default behaviour is {@link #setLoadCatalogBom(boolean)} true and {@link #setForce(boolean)} false.
+     * <p>
+     * The return value is extensive but should be self-evident, and will include a list of any registered types (catalog items) installed. 
+     */
+    public ReferenceWithError<OsgiBundleInstallationResult> install() {
+        boolean startedInstallation = false;
+        
+        try {
+            init();
+            makeLocalZipFileFromInputStreamOrUrl();
+            if (result.code!=null) return ReferenceWithError.newInstanceWithoutError(result);
+            discoverManifestFromCatalogBom(false);
+            if (result.code!=null) return ReferenceWithError.newInstanceWithoutError(result);
+            updateManifestFromAllSourceInformation();
+            if (result.code!=null) return ReferenceWithError.newInstanceWithoutError(result);
+            assert inferredMetadata.isNameResolved() : "Should have resolved "+inferredMetadata;
+
+            Boolean updating = null;
+            result.metadata = osgiManager.getManagedBundle(inferredMetadata.getVersionedName());
+            if (result.getMetadata()!=null) {
+                // already have a managed bundle
+                if (canUpdate()) { 
+                    result.bundle = osgiManager.framework.getBundleContext().getBundle(result.getMetadata().getOsgiUniqueUrl());
+                    if (result.getBundle()==null) {
+                        throw new IllegalStateException("Detected already managing bundle "+result.getMetadata().getVersionedName()+" but framework cannot find it");
+                    }
+                    updating = true;
+                } else {
+                    result.setIgnoringAlreadyInstalled();
+                    return ReferenceWithError.newInstanceWithoutError(result);
+                }
+            } else {
+                result.metadata = inferredMetadata;
+                // no such managed bundle
+                Maybe<Bundle> b = Osgis.bundleFinder(osgiManager.framework).symbolicName(result.getMetadata().getSymbolicName()).version(result.getMetadata().getVersion()).find();
+                if (b.isPresent()) {
+                    // if it's non-brooklyn installed then fail
+                    // (e.g. someone trying to install brooklyn or guice through this mechanism!)
+                    result.bundle = b.get();
+                    result.code = OsgiBundleInstallationResult.ResultCode.ERROR_INSTALLING_BUNDLE;
+                    throw new IllegalStateException("Bundle "+result.getMetadata().getVersionedName()+" already installed in framework but not managed by Brooklyn; cannot install or update through Brooklyn");
+                }
+                // normal install
+                updating = false;
+            }
+            
+            startedInstallation = true;
+            try (InputStream fin = new FileInputStream(zipFile)) {
+                if (!updating) {
+                    // install new
+                    assert result.getBundle()==null;
+                    result.bundle = osgiManager.framework.getBundleContext().installBundle(result.getMetadata().getOsgiUniqueUrl(), fin);
+                } else {
+                    result.bundle.update(fin);
+                }
+            }
+            
+            osgiManager.checkCorrectlyInstalled(result.getMetadata(), result.bundle);
+            ((BasicManagedBundle)result.getMetadata()).setTempLocalFileWhenJustUploaded(zipFile);
+            zipFile = null; // don't close/delete it here, we'll use it for uploading, then it will delete it
+            
+            if (!updating) { 
+                synchronized (osgiManager.managedBundles) {
+                    osgiManager.managedBundles.put(result.getMetadata().getId(), result.getMetadata());
+                    osgiManager.managedBundleIds.put(result.getMetadata().getVersionedName(), result.getMetadata().getId());
+                }
+                result.code = OsgiBundleInstallationResult.ResultCode.INSTALLED_NEW_BUNDLE;
+                result.message = "Installed "+result.getMetadata().getVersionedName()+" with ID "+result.getMetadata().getId();
+                mgmt().getRebindManager().getChangeListener().onManaged(result.getMetadata());
+            } else {
+                result.code = OsgiBundleInstallationResult.ResultCode.UPDATED_EXISTING_BUNDLE;
+                result.message = "Updated "+result.getMetadata().getVersionedName()+" as existing ID "+result.getMetadata().getId();
+                mgmt().getRebindManager().getChangeListener().onChanged(result.getMetadata());
+            }
+            // setting the above before the code below means if there is a problem starting or loading catalog items
+            // a user has to remove then add again, or forcibly reinstall;
+            // that seems fine and probably better than allowing bundles to start and catalog items to be installed 
+            // when brooklyn isn't aware it is supposed to be managing it
+            
+            // starting here  flags wiring issues earlier
+            // but may break some things running from the IDE
+            result.bundle.start();
+
+            if (updating!=null) {
+                osgiManager.uninstallCatalogItemsFromBundle( result.getVersionedName() );
+                // (ideally removal and addition would be atomic)
+            }
+            if (loadCatalogBom) {
+                osgiManager.loadCatalogBom(result.bundle);
+            }
+
+            return ReferenceWithError.newInstanceWithoutError(result);
+            
+        } catch (Exception e) {
+            Exceptions.propagateIfFatal(e);
+            result.code = startedInstallation ? OsgiBundleInstallationResult.ResultCode.ERROR_INSTALLING_BUNDLE : OsgiBundleInstallationResult.ResultCode.ERROR_PREPARING_BUNDLE;
+            result.message = "Bundle "+inferredMetadata+" failed "+
+                (startedInstallation ? "installation" : "preparation") + ": " + Exceptions.collapseText(e);
+            return ReferenceWithError.newInstanceThrowingError(result, new IllegalStateException(result.message, e));
+        } finally {
+            close();
+        }
+    }
+
+    private boolean canUpdate() {
+        return force || VersionComparator.isSnapshot(inferredMetadata.getVersion());
+    }
+
+    /** true if the supplied name and version are complete; updates if the known data is incomplete;
+     * throws if there is a mismatch; false if the supplied data is incomplete */
+    private boolean matchSetOrFail(String source, String name, String version) {
+        boolean suppliedIsComplete = true;
+        if (Strings.isBlank(name)) {
+            suppliedIsComplete = false;
+        } else if (Strings.isBlank(inferredMetadata.getSymbolicName())) {
+            ((BasicManagedBundle)inferredMetadata).setSymbolicName(name);
+        } else if (!Objects.equal(inferredMetadata.getSymbolicName(), name)){
+            throw new IllegalArgumentException("Symbolic name mismatch '"+name+"' from "+source+" (expected '"+inferredMetadata.getSymbolicName()+"')");
+        }
+        
+        if (Strings.isBlank(version)) {
+            suppliedIsComplete = false;
+        } else if (Strings.isBlank(inferredMetadata.getVersion())) {
+            ((BasicManagedBundle)inferredMetadata).setVersion(version);
+        } else if (!Objects.equal(inferredMetadata.getVersion(), version)){
+            throw new IllegalArgumentException("Bundle version mismatch '"+version+"' from "+source+" (expected '"+inferredMetadata.getVersion()+"')");
+        }
+        
+        return suppliedIsComplete;
+    }    
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/14524547/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java
new file mode 100644
index 0000000..50ca081
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.brooklyn.core.mgmt.ha;
+
+import java.util.List;
+
+import org.apache.brooklyn.api.typereg.ManagedBundle;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.osgi.VersionedName;
+import org.osgi.framework.Bundle;
+
+import com.google.common.annotations.Beta;
+import com.google.common.collect.ImmutableList;
+
+@Beta
+public class OsgiBundleInstallationResult {
+    String message;
+    ManagedBundle metadata;
+    Bundle bundle;
+    ResultCode code;
+    
+    public enum ResultCode { 
+        INSTALLED_NEW_BUNDLE,
+        UPDATED_EXISTING_BUNDLE, 
+        IGNORING_BUNDLE_AREADY_INSTALLED, 
+        ERROR_PREPARING_BUNDLE,
+        ERROR_INSTALLING_BUNDLE 
+    }
+    final List<String> catalogItemsInstalled = MutableList.of();
+    
+    public String getMessage() {
+        return message;
+    }
+    public Bundle getBundle() {
+        return bundle;
+    }
+    public ManagedBundle getMetadata() {
+        return metadata;
+    }
+    public ResultCode getCode() {
+        return code;
+    }
+    public List<String> getCatalogItemsInstalled() {
+        return ImmutableList.copyOf(catalogItemsInstalled);
+    }
+    public VersionedName getVersionedName() {
+        if (getMetadata()==null) return null;
+        return getMetadata().getVersionedName();
+    }
+    
+    void setIgnoringAlreadyInstalled() {
+        code = OsgiBundleInstallationResult.ResultCode.IGNORING_BUNDLE_AREADY_INSTALLED;
+        message = "Bundle "+getMetadata().getVersionedName()+" already installed as "+getMetadata().getId();
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/14524547/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java
index 17d5645..1685541 100644
--- a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java
+++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java
@@ -19,8 +19,6 @@
 package org.apache.brooklyn.core.mgmt.ha;
 
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.InputStream;
 import java.net.URL;
 import java.util.Arrays;
@@ -33,6 +31,8 @@ import java.util.concurrent.Callable;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
+import javax.annotation.Nullable;
+
 import org.apache.brooklyn.api.catalog.CatalogItem;
 import org.apache.brooklyn.api.catalog.CatalogItem.CatalogBundle;
 import org.apache.brooklyn.api.mgmt.ManagementContext;
@@ -47,7 +47,6 @@ import org.apache.brooklyn.core.config.ConfigKeys;
 import org.apache.brooklyn.core.mgmt.persist.OsgiClassPrefixer;
 import org.apache.brooklyn.core.server.BrooklynServerConfig;
 import org.apache.brooklyn.core.server.BrooklynServerPaths;
-import org.apache.brooklyn.core.typereg.BasicManagedBundle;
 import org.apache.brooklyn.util.collections.MutableList;
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.collections.MutableSet;
@@ -55,13 +54,13 @@ import org.apache.brooklyn.util.core.osgi.Osgis;
 import org.apache.brooklyn.util.core.osgi.Osgis.BundleFinder;
 import org.apache.brooklyn.util.core.osgi.SystemFrameworkLoader;
 import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.exceptions.ReferenceWithError;
 import org.apache.brooklyn.util.exceptions.UserFacingException;
 import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.os.Os;
 import org.apache.brooklyn.util.os.Os.DeletionResult;
 import org.apache.brooklyn.util.osgi.VersionedName;
 import org.apache.brooklyn.util.repeat.Repeater;
-import org.apache.brooklyn.util.stream.Streams;
 import org.apache.brooklyn.util.text.Strings;
 import org.apache.brooklyn.util.time.Duration;
 import org.osgi.framework.Bundle;
@@ -96,18 +95,18 @@ public class OsgiManager {
     
     /* see `Osgis` class for info on starting framework etc */
     
-    protected final ManagementContext mgmt;
-    protected final OsgiClassPrefixer osgiClassPrefixer;
-    protected Framework framework;
-    protected boolean reuseFramework;
+    final ManagementContext mgmt;
+    final OsgiClassPrefixer osgiClassPrefixer;
+    Framework framework;
+    
+    private boolean reuseFramework;
     private Set<Bundle> bundlesAtStartup;
-    protected File osgiCacheDir;
-    protected Map<String, ManagedBundle> managedBundles = MutableMap.of();
-    protected Map<VersionedName, String> managedBundleIds = MutableMap.of();
-    protected AtomicInteger numberOfReusableFrameworksCreated = new AtomicInteger();
-
+    private File osgiCacheDir;
+    Map<String, ManagedBundle> managedBundles = MutableMap.of();
+    Map<VersionedName, String> managedBundleIds = MutableMap.of();
     
-    protected static final List<Framework> OSGI_FRAMEWORK_CONTAINERS_FOR_REUSE = MutableList.of();
+    private static AtomicInteger numberOfReusableFrameworksCreated = new AtomicInteger();
+    private static final List<Framework> OSGI_FRAMEWORK_CONTAINERS_FOR_REUSE = MutableList.of();
     
     public OsgiManager(ManagementContext mgmt) {
         this.mgmt = mgmt;
@@ -230,76 +229,21 @@ public class OsgiManager {
         }
     }
 
-    public Bundle installUploadedBundle(ManagedBundle bundleMetadata, InputStream zipIn, boolean loadCatalogBom) {
-        try {
-            Bundle alreadyBundle = checkBundleInstalledThrowIfInconsistent(bundleMetadata, false);
-            if (alreadyBundle!=null) {
-                return alreadyBundle;
-            }
-
-            File zipF = Os.newTempFile("brooklyn-bundle-transient-"+bundleMetadata, "zip");
-            FileOutputStream fos = new FileOutputStream(zipF);
-            Streams.copy(zipIn, fos);
-            zipIn.close();
-            fos.close();
-            
-            ManagedBundle existingBundleToUpdate = null;
-            synchronized (managedBundles) {
-                String id = managedBundleIds.get(bundleMetadata.getVersionedName());
-                if (id!=null) {
-                    existingBundleToUpdate = managedBundles.get(id); 
-                }
-            }
-            
-            Bundle bundleInstalled;
-            if (existingBundleToUpdate==null) {
-                // install new
-                bundleInstalled = framework.getBundleContext().installBundle(bundleMetadata.getOsgiUniqueUrl(), 
-                    new FileInputStream(zipF));
-            } else {
-                // update
-                bundleInstalled = framework.getBundleContext().getBundle(existingBundleToUpdate.getOsgiUniqueUrl());
-                if (bundleInstalled==null) {
-                    throw new IllegalStateException("Detected bundle "+existingBundleToUpdate+" should be installed but framework cannot find it");
-                }
-                try (InputStream fin = new FileInputStream(zipF)) {
-                    bundleInstalled.update(fin);
-                }
-                bundleMetadata = existingBundleToUpdate;
-            }
-            checkCorrectlyInstalled(bundleMetadata, bundleInstalled);
-            if (!bundleMetadata.isNameResolved()) {
-                ((BasicManagedBundle)bundleMetadata).setSymbolicName(bundleInstalled.getSymbolicName());
-                ((BasicManagedBundle)bundleMetadata).setVersion(bundleInstalled.getVersion().toString());
-            }
-            ((BasicManagedBundle)bundleMetadata).setTempLocalFileWhenJustUploaded(zipF);
-            
-            synchronized (managedBundles) {
-                managedBundles.put(bundleMetadata.getId(), bundleMetadata);
-                managedBundleIds.put(bundleMetadata.getVersionedName(), bundleMetadata.getId());
-            }
-            mgmt.getRebindManager().getChangeListener().onChanged(bundleMetadata);
-            
-            // starting here  flags wiring issues earlier
-            // but may break some things running from the IDE
-            bundleInstalled.start();
-
-            if (existingBundleToUpdate!=null) {
-                // TODO remove old catalog items (see below)
-                // (ideally the removal and addition would be atomic)
-            }
-            if (loadCatalogBom) {
-                loadCatalogBom(bundleInstalled);
-            }
-            
-            return bundleInstalled;
-        } catch (Exception e) {
-            Exceptions.propagateIfFatal(e);
-            throw new IllegalStateException("Bundle "+bundleMetadata+" failed to install: " + Exceptions.collapseText(e), e);
-        }
+    /** See {@link OsgiArchiveInstaller#install()}, using default values */
+    public ReferenceWithError<OsgiBundleInstallationResult> install(InputStream zipIn) {
+        return install(null, zipIn, true, false);
     }
     
-    
+    /** See {@link OsgiArchiveInstaller#install()} */
+    public ReferenceWithError<OsgiBundleInstallationResult> install(@Nullable ManagedBundle knownBundleMetadata, @Nullable InputStream zipIn,
+            boolean loadCatalogBom, boolean forceUpdateOfNonSnapshots) {
+        
+        OsgiArchiveInstaller installer = new OsgiArchiveInstaller(this, knownBundleMetadata, zipIn);
+        installer.setLoadCatalogBom(loadCatalogBom);
+        installer.setForce(forceUpdateOfNonSnapshots);
+        
+        return installer.install();
+    }
     
     /**
      * Removes this bundle from Brooklyn management, 
@@ -321,12 +265,8 @@ public class OsgiManager {
             managedBundleIds.remove(bundleMetadata.getVersionedName());
         }
         mgmt.getRebindManager().getChangeListener().onUnmanaged(bundleMetadata);
-        
-        // uninstall things that come from this bundle
-        List<RegisteredType> thingsFromHere = ImmutableList.copyOf(getTypesFromBundle( bundleMetadata.getVersionedName() ));
-        for (RegisteredType t: thingsFromHere) {
-            mgmt.getCatalog().deleteCatalogItem(t.getSymbolicName(), t.getVersion());
-        }
+
+        uninstallCatalogItemsFromBundle( bundleMetadata.getVersionedName() );
         
         Bundle bundle = framework.getBundleContext().getBundle(bundleMetadata.getOsgiUniqueUrl());
         if (bundle==null) {
@@ -340,6 +280,13 @@ public class OsgiManager {
         }
     }
 
+    void uninstallCatalogItemsFromBundle(VersionedName bundle) {
+        List<RegisteredType> thingsFromHere = ImmutableList.copyOf(getTypesFromBundle( bundle ));
+        for (RegisteredType t: thingsFromHere) {
+            mgmt.getCatalog().deleteCatalogItem(t.getSymbolicName(), t.getVersion());
+        }
+    }
+
     protected Iterable<RegisteredType> getTypesFromBundle(final VersionedName vn) {
         final String bundleId = vn.toString();
         return mgmt.getTypeRegistry().getMatching(new Predicate<RegisteredType>() {
@@ -350,8 +297,8 @@ public class OsgiManager {
         });
     }
     
-    // TODO DO on snapshot install, uninstall old equivalent snapshots (items in use might stay in use though?)
-    
+    /** @deprecated since 0.12.0 use {@link #install(ManagedBundle, InputStream, boolean, boolean)} */
+    @Deprecated
     public synchronized Bundle registerBundle(CatalogBundle bundleMetadata) {
         try {
             Bundle alreadyBundle = checkBundleInstalledThrowIfInconsistent(bundleMetadata, true);
@@ -402,7 +349,7 @@ public class OsgiManager {
         return catalogItems;
     }
     
-    private void checkCorrectlyInstalled(OsgiBundleWithUrl bundle, Bundle b) {
+    void checkCorrectlyInstalled(OsgiBundleWithUrl bundle, Bundle b) {
         String nv = b.getSymbolicName()+":"+b.getVersion().toString();
 
         if (!isBundleNameEqualOrAbsent(bundle, b)) {

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/14524547/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindContextImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindContextImpl.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindContextImpl.java
index 1e9cbba..13c85f6 100644
--- a/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindContextImpl.java
+++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindContextImpl.java
@@ -36,7 +36,7 @@ import org.apache.brooklyn.api.policy.Policy;
 import org.apache.brooklyn.api.sensor.Enricher;
 import org.apache.brooklyn.api.sensor.Feed;
 import org.apache.brooklyn.api.typereg.ManagedBundle;
-import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
+import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
 import org.apache.brooklyn.util.collections.MutableMap;
 
 import com.google.common.collect.Maps;
@@ -91,7 +91,7 @@ public class RebindContextImpl implements RebindContext {
 
     // we don't track register/unregister of bundles; it isn't needed as it happens so early
     public void installBundle(ManagedBundle bundle, InputStream zipInput) {
-        ((LocalManagementContext)mgmt).getOsgiManager().get().installUploadedBundle(bundle, zipInput, true);
+        ((ManagementContextInternal)mgmt).getOsgiManager().get().install(bundle, zipInput, true, false).checkNoError();
     }
     
     public void unregisterPolicy(Policy policy) {

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/14524547/core/src/main/java/org/apache/brooklyn/core/typereg/BasicManagedBundle.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/typereg/BasicManagedBundle.java b/core/src/main/java/org/apache/brooklyn/core/typereg/BasicManagedBundle.java
index 37e706f..4799c06 100644
--- a/core/src/main/java/org/apache/brooklyn/core/typereg/BasicManagedBundle.java
+++ b/core/src/main/java/org/apache/brooklyn/core/typereg/BasicManagedBundle.java
@@ -21,6 +21,7 @@ package org.apache.brooklyn.core.typereg;
 import java.io.File;
 import java.util.Map;
 
+import org.apache.brooklyn.api.catalog.CatalogItem.CatalogBundle;
 import org.apache.brooklyn.api.mgmt.rebind.RebindSupport;
 import org.apache.brooklyn.api.typereg.ManagedBundle;
 import org.apache.brooklyn.api.typereg.OsgiBundleWithUrl;
@@ -181,4 +182,8 @@ public class BasicManagedBundle extends AbstractBrooklynObject implements Manage
         throw new UnsupportedOperationException();
     }
 
+    public static ManagedBundle of(CatalogBundle bundleUrl) {
+        return new BasicManagedBundle(bundleUrl.getSymbolicName(), bundleUrl.getVersion(), bundleUrl.getUrl());
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/14524547/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/ApiError.java
----------------------------------------------------------------------
diff --git a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/ApiError.java b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/ApiError.java
index bec524b..a2a1834 100644
--- a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/ApiError.java
+++ b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/ApiError.java
@@ -72,6 +72,7 @@ public class ApiError implements Serializable {
     public static class Builder {
         private String message;
         private String details;
+        private Object data;
         private Integer errorCode;
 
         public Builder message(String message) {
@@ -80,7 +81,12 @@ public class ApiError implements Serializable {
         }
 
         public Builder details(String details) {
-            this.details = checkNotNull(details, "details");
+            this.details = details;
+            return this;
+        }
+
+        public Builder data(Object data) {
+            this.data = data;
             return this;
         }
 
@@ -111,7 +117,7 @@ public class ApiError implements Serializable {
         }
 
         public ApiError build() {
-            return new ApiError(message, details, errorCode);
+            return new ApiError(message, details, data, errorCode);
         }
 
         /** @deprecated since 0.7.0; use {@link #copy(ApiError)} */
@@ -136,18 +142,23 @@ public class ApiError implements Serializable {
 
     @JsonInclude(JsonInclude.Include.NON_EMPTY)
     private final String details;
+    
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    private final Object data;
 
     @JsonInclude(JsonInclude.Include.NON_NULL)
     private final Integer error;
 
     public ApiError(String message) { this(message, null); }
-    public ApiError(String message, String details) { this(message, details, null); }
+    public ApiError(String message, String details) { this(message, details, null, null); }
     public ApiError(
             @JsonProperty("message") String message,
             @JsonProperty("details") String details,
+            @JsonProperty("data") Object data,
             @JsonProperty("error") Integer error) {
         this.message = checkNotNull(message, "message");
         this.details = details != null ? details : "";
+        this.data = data;
         this.error = error;
     }
 
@@ -159,6 +170,10 @@ public class ApiError implements Serializable {
         return details;
     }
 
+    public Object getData() {
+        return data;
+    }
+    
     public Integer getError() {
         return error;
     }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/14524547/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java
index d0f7270..7359e4d 100644
--- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java
@@ -19,9 +19,6 @@
 package org.apache.brooklyn.rest.resources;
 
 import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
 import java.io.InputStreamReader;
 import java.net.URI;
 import java.util.ArrayList;
@@ -29,8 +26,6 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.jar.Attributes;
-import java.util.jar.Manifest;
 
 import javax.annotation.Nullable;
 import javax.ws.rs.core.MediaType;
@@ -44,8 +39,10 @@ import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.entity.EntitySpec;
 import org.apache.brooklyn.api.location.Location;
 import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
 import org.apache.brooklyn.api.policy.Policy;
 import org.apache.brooklyn.api.policy.PolicySpec;
+import org.apache.brooklyn.api.typereg.ManagedBundle;
 import org.apache.brooklyn.api.typereg.RegisteredType;
 import org.apache.brooklyn.core.catalog.CatalogPredicates;
 import org.apache.brooklyn.core.catalog.internal.BasicBrooklynCatalog;
@@ -54,8 +51,8 @@ import org.apache.brooklyn.core.catalog.internal.CatalogItemComparator;
 import org.apache.brooklyn.core.catalog.internal.CatalogUtils;
 import org.apache.brooklyn.core.mgmt.entitlement.Entitlements;
 import org.apache.brooklyn.core.mgmt.entitlement.Entitlements.StringAndArgument;
+import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult;
 import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
-import org.apache.brooklyn.core.typereg.BasicManagedBundle;
 import org.apache.brooklyn.core.typereg.RegisteredTypePredicates;
 import org.apache.brooklyn.rest.api.CatalogApi;
 import org.apache.brooklyn.rest.domain.ApiError;
@@ -65,24 +62,16 @@ import org.apache.brooklyn.rest.domain.CatalogLocationSummary;
 import org.apache.brooklyn.rest.domain.CatalogPolicySummary;
 import org.apache.brooklyn.rest.filter.HaHotStateRequired;
 import org.apache.brooklyn.rest.transform.CatalogTransformer;
-import org.apache.brooklyn.rest.util.DefaultExceptionMapper;
 import org.apache.brooklyn.rest.util.WebResourceUtils;
 import org.apache.brooklyn.util.collections.MutableList;
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.collections.MutableSet;
 import org.apache.brooklyn.util.core.ResourceUtils;
-import org.apache.brooklyn.util.core.osgi.BundleMaker;
 import org.apache.brooklyn.util.exceptions.Exceptions;
-import org.apache.brooklyn.util.os.Os;
-import org.apache.brooklyn.util.osgi.VersionedName;
-import org.apache.brooklyn.util.stream.Streams;
+import org.apache.brooklyn.util.exceptions.ReferenceWithError;
 import org.apache.brooklyn.util.text.StringPredicates;
 import org.apache.brooklyn.util.text.Strings;
 import org.apache.brooklyn.util.yaml.Yamls;
-import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
-import org.apache.commons.compress.archivers.zip.ZipFile;
-import org.osgi.framework.Bundle;
-import org.osgi.framework.Constants;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -161,6 +150,29 @@ public class CatalogResource extends AbstractBrooklynRestResource implements Cat
         }
     }
 
+    public static class BundleInstallationRestResult {
+        String message;
+        ManagedBundle metadata;
+        
+        enum ResultCode { 
+            INSTALLED_NEW_BUNDLE,
+            UPDATED_EXISTING_BUNDLE, 
+            IGNORING_BUNDLE_AREADY_INSTALLED, 
+            ERROR_PREPARING_BUNDLE,
+            ERROR_INSTALLING_BUNDLE 
+        }
+        Map<String,Object> catalogItemsInstalled;
+        
+        public String getMessage() {
+            return message;
+        }
+        
+        public static BundleInstallationRestResult of(OsgiBundleInstallationResult result, ManagementContext mgmt) {
+            // TODO 
+            return null;
+        }
+    }
+    
     @Override
     @Beta
     public Response createFromArchive(byte[] zipInput) {
@@ -169,92 +181,15 @@ public class CatalogResource extends AbstractBrooklynRestResource implements Cat
                 Entitlements.getEntitlementContext().user());
         }
 
-        BundleMaker bm = new BundleMaker(mgmtInternal());
-        File f=null, f2=null;
-        try {
-            f = Os.newTempFile("brooklyn-posted-archive", "zip");
-            try {
-                Files.write(zipInput, f);
-            } catch (IOException e) {
-                Exceptions.propagate(e);
-            }
-            
-            ZipFile zf;
-            try {
-                zf = new ZipFile(f);
-            } catch (IOException e) {
-                throw new IllegalArgumentException("Invalid ZIP/JAR archive: "+e);
-            }
-            ZipArchiveEntry bom = zf.getEntry("catalog.bom");
-            if (bom==null) {
-                bom = zf.getEntry("/catalog.bom");
-            }
-            if (bom==null) {
-                throw new IllegalArgumentException("Archive must contain a catalog.bom file in the root");
-            }
-            String bomS;
-            try {
-                bomS = Streams.readFullyString(zf.getInputStream(bom));
-            } catch (IOException e) {
-                throw new IllegalArgumentException("Error reading catalog.bom from ZIP/JAR archive: "+e);
-            }
-
-            try {
-                zf.close();
-            } catch (IOException e) {
-                log.debug("Swallowed exception closing zipfile. Full error logged at trace: {}", e.getMessage());
-                log.trace("Exception closing zipfile", e);
-            }
-
-            VersionedName vn = BasicBrooklynCatalog.getVersionedName( BasicBrooklynCatalog.getCatalogMetadata(bomS) );
-            
-            Manifest mf = bm.getManifest(f);
-            if (mf==null) {
-                mf = new Manifest();
-            }
-            String bundleNameInMF = mf.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
-            if (Strings.isNonBlank(bundleNameInMF)) {
-                if (!bundleNameInMF.equals(vn.getSymbolicName())) {
-                    throw new IllegalArgumentException("JAR MANIFEST symbolic-name '"+bundleNameInMF+"' does not match '"+vn.getSymbolicName()+"' defined in BOM");
-                }
-            } else {
-                bundleNameInMF = vn.getSymbolicName();
-                mf.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, bundleNameInMF);
-            }
-            
-            String bundleVersionInMF = mf.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
-            if (Strings.isNonBlank(bundleVersionInMF)) {
-                if (!bundleVersionInMF.equals(vn.getVersion().toString())) {
-                    throw new IllegalArgumentException("JAR MANIFEST version '"+bundleVersionInMF+"' does not match '"+vn.getVersion()+"' defined in BOM");
-                }
-            } else {
-                bundleVersionInMF = vn.getVersion().toString();
-                mf.getMainAttributes().putValue(Constants.BUNDLE_VERSION, bundleVersionInMF);
-            }
-            if (mf.getMainAttributes().getValue(Attributes.Name.MANIFEST_VERSION)==null) {
-                mf.getMainAttributes().putValue(Attributes.Name.MANIFEST_VERSION.toString(), "1.0");
-            }
-            
-            f2 = bm.copyAddingManifest(f, mf);
-            
-            BasicManagedBundle bundleMetadata = new BasicManagedBundle(bundleNameInMF, bundleVersionInMF, null);
-            Bundle bundle;
-            try (FileInputStream f2in = new FileInputStream(f2)) {
-                bundle = ((ManagementContextInternal)mgmt()).getOsgiManager().get().installUploadedBundle(bundleMetadata, f2in, false);
-            } catch (Exception e) {
-                throw Exceptions.propagate(e);
-            }
-
-            Iterable<? extends CatalogItem<?, ?>> catalogItems = MutableList.copyOf( 
-                ((ManagementContextInternal)mgmt()).getOsgiManager().get().loadCatalogBom(bundle) );
-
-            return buildCreateResponse(catalogItems);
-        } catch (RuntimeException ex) {
-            throw WebResourceUtils.badRequest(ex);
-        } finally {
-            if (f!=null) f.delete();
-            if (f2!=null) f2.delete();
+        ReferenceWithError<OsgiBundleInstallationResult> result = ((ManagementContextInternal)mgmt()).getOsgiManager().get()
+            .install(new ByteArrayInputStream(zipInput));
+        
+        if (result.hasError()) {
+            return ApiError.builder().errorCode(Status.BAD_REQUEST).message(result.getWithoutError().getMessage())
+                .data(result).build().asJsonResponse();
         }
+
+        return Response.status(Status.CREATED).entity( BundleInstallationRestResult.of(result.get(), mgmt()) ).build();
     }
 
     private Response buildCreateResponse(Iterable<? extends CatalogItem<?, ?>> catalogItems) {
@@ -575,4 +510,4 @@ public class CatalogResource extends AbstractBrooklynRestResource implements Cat
         return result;
     }
 }
- 
\ No newline at end of file
+ 

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/14524547/utils/common/src/main/java/org/apache/brooklyn/util/maven/MavenArtifact.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/maven/MavenArtifact.java b/utils/common/src/main/java/org/apache/brooklyn/util/maven/MavenArtifact.java
index b9de2f1..248b72e 100644
--- a/utils/common/src/main/java/org/apache/brooklyn/util/maven/MavenArtifact.java
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/maven/MavenArtifact.java
@@ -26,6 +26,7 @@ import javax.annotation.Nullable;
 import org.apache.brooklyn.util.collections.MutableList;
 import org.apache.brooklyn.util.javalang.JavaClassNames;
 import org.apache.brooklyn.util.text.Strings;
+import org.apache.brooklyn.util.text.VersionComparator;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -100,7 +101,7 @@ public class MavenArtifact {
     }
 
     public boolean isSnapshot() {
-        return getVersion().toUpperCase().contains("SNAPSHOT");
+        return VersionComparator.isSnapshot(getVersion());
     }
     
     /** @see #customFileNameAfterArtifactMarker */

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/14524547/utils/common/src/main/java/org/apache/brooklyn/util/text/VersionComparator.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/text/VersionComparator.java b/utils/common/src/main/java/org/apache/brooklyn/util/text/VersionComparator.java
index 94553b0..071f591 100644
--- a/utils/common/src/main/java/org/apache/brooklyn/util/text/VersionComparator.java
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/text/VersionComparator.java
@@ -52,14 +52,19 @@ public class VersionComparator implements Comparator<String> {
         return INSTANCE;
     }
 
+    public static boolean isSnapshot(String version) {
+        if (version==null) return false;
+        return version.toUpperCase().contains(SNAPSHOT);
+    }
+    
     @Override
     public int compare(String v1, String v2) {
         if (v1==null && v2==null) return 0;
         if (v1==null) return -1;
         if (v2==null) return 1;
         
-        boolean isV1Snapshot = v1.toUpperCase().contains(SNAPSHOT);
-        boolean isV2Snapshot = v2.toUpperCase().contains(SNAPSHOT);
+        boolean isV1Snapshot = isSnapshot(v1);
+        boolean isV2Snapshot = isSnapshot(v2);
         if (isV1Snapshot == isV2Snapshot) {
             // if snapshot status is the same, look at dot-split parts first
             return compareDotSplitParts(splitOnDot(v1), splitOnDot(v2));

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/14524547/utils/common/src/test/java/org/apache/brooklyn/util/text/VersionComparatorTest.java
----------------------------------------------------------------------
diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/text/VersionComparatorTest.java b/utils/common/src/test/java/org/apache/brooklyn/util/text/VersionComparatorTest.java
index 3a4a71c..6c65a8a 100644
--- a/utils/common/src/test/java/org/apache/brooklyn/util/text/VersionComparatorTest.java
+++ b/utils/common/src/test/java/org/apache/brooklyn/util/text/VersionComparatorTest.java
@@ -92,6 +92,13 @@ public class VersionComparatorTest {
         assertVersionOrder("0.10.0-SNAPSHOT", "0.10.0.SNAPSHOT", "0.10.0-GA", "0.10.0.GA", "0.10.0");
     }
     
+    @Test
+    public void testIsSnapshot() {
+        Assert.assertTrue(VersionComparator.isSnapshot("0.10.0-SNAPSHOT"));
+        Assert.assertTrue(VersionComparator.isSnapshot("0.10.0.snapshot"));
+        Assert.assertFalse(VersionComparator.isSnapshot("0.10.0"));
+    }
+    
     private static void assertVersionOrder(String v1, String v2, String ...otherVersions) {
         List<String> versions = MutableList.<String>of().append(v1, v2, otherVersions);
         


Mime
View raw message