brooklyn-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From henev...@apache.org
Subject [1/2] incubator-brooklyn git commit: support multiple instances of the same osgi symbolic-name:version
Date Tue, 11 Nov 2014 01:52:53 GMT
Repository: incubator-brooklyn
Updated Branches:
  refs/heads/master 3cfda7e8f -> dc53cec5d


support multiple instances of the same osgi symbolic-name:version


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

Branch: refs/heads/master
Commit: 72b29b950b0ad2839dc0e3beafb814089a466472
Parents: 3cfda7e
Author: Alex Heneveld <alex.heneveld@cloudsoftcorp.com>
Authored: Thu Nov 6 17:41:33 2014 +0000
Committer: Alex Heneveld <alex.heneveld@cloudsoftcorp.com>
Committed: Tue Nov 11 01:52:05 2014 +0000

----------------------------------------------------------------------
 .../brooklyn/management/ha/OsgiManager.java     |  78 +++--
 .../main/java/brooklyn/util/ResourceUtils.java  |   2 +-
 .../src/main/java/brooklyn/util/osgi/Osgis.java | 300 +++++++++++++++----
 .../osgi/more-entities-v2-evil-twin/pom.xml     |  88 ++++++
 .../brooklyn/osgi/tests/more/MoreEntity.java    |  38 +++
 .../osgi/tests/more/MoreEntityImpl.java         |  47 +++
 .../brooklyn/catalog/internal/CatalogItems.java |   8 +
 .../management/osgi/OsgiTestResources.java      |  16 +-
 .../osgi/OsgiVersionMoreEntityTest.java         | 116 +++++--
 ...-test-osgi-more-entities_evil-twin_0.2.0.jar | Bin 0 -> 12757 bytes
 ...-test-osgi-more-entities_evil-twin_0.2.0.txt |  21 ++
 .../util/exceptions/ReferenceWithError.java     |   6 +-
 12 files changed, 601 insertions(+), 119 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/72b29b95/core/src/main/java/brooklyn/management/ha/OsgiManager.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/ha/OsgiManager.java b/core/src/main/java/brooklyn/management/ha/OsgiManager.java
index 3dea979..44b30af 100644
--- a/core/src/main/java/brooklyn/management/ha/OsgiManager.java
+++ b/core/src/main/java/brooklyn/management/ha/OsgiManager.java
@@ -21,6 +21,7 @@ package brooklyn.management.ha;
 import java.io.File;
 import java.net.URL;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
 
 import org.osgi.framework.Bundle;
@@ -51,7 +52,9 @@ public class OsgiManager {
     
     protected Framework framework;
     protected File osgiTempDir;
-    protected Map<String,String> bundleUrlToNameVersionString = MutableMap.of();
+    
+    // we could manage without this map but it is useful to validate what is a user-supplied url
+    protected Map<String,String> urlToBundleIdentifier = MutableMap.of();
     
     public void start() {
         try {
@@ -87,19 +90,32 @@ public class OsgiManager {
 
     public void registerBundle(String bundleUrl) {
         try {
-            String nv = bundleUrlToNameVersionString.get(bundleUrl);
+            String nv = urlToBundleIdentifier.get(bundleUrl);
             if (nv!=null) {
-                if (Osgis.getBundle(framework, nv).isPresent()) {
+                if (Osgis.bundleFinder(framework).id(nv).requiringFromUrl(bundleUrl).find().isPresent()) {
                     log.trace("Bundle from "+bundleUrl+" already installed as "+nv+"; not re-registering");
                     return;
+                } else {
+                    log.debug("Bundle "+nv+" from "+bundleUrl+" is known in map but not installed; perhaps in the process of installing?");
                 }
             }
+            
             Bundle b = Osgis.install(framework, bundleUrl);
             nv = b.getSymbolicName()+":"+b.getVersion().toString();
-            // TODO if there is another entry for name:version we should log a warning at the very least,
-            // or better provide a way to get back *this* bundle
-            bundleUrlToNameVersionString.put(bundleUrl, nv);
-            log.debug("Bundle from "+bundleUrl+" successfully installed as " + nv + " ("+b+")");
+            
+            List<Bundle> matches = Osgis.bundleFinder(framework).id(nv).findAll();
+            if (matches.isEmpty()) {
+                log.error("OSGi could not find bundle "+nv+" in search after installing it from "+bundleUrl);
+            } else if (matches.size()==1) {
+                log.debug("Bundle from "+bundleUrl+" successfully installed as " + nv + " ("+b+")");
+            } else {
+                log.warn("OSGi has multiple bundles matching "+nv+", when just installed from "+bundleUrl+": "+matches+"; "
+                    + "brooklyn will prefer the URL-based bundle for top-level references but any dependencies or "
+                    + "import-packages will be at the mercy of OSGi. "
+                    + "It is recommended to use distinct versions for different bundles, and the same URL for the same bundles.");
+            }
+            urlToBundleIdentifier.put(bundleUrl, nv);
+            
         } catch (BundleException e) {
             log.debug("Bundle from "+bundleUrl+" failed to install (rethrowing): "+e);
             throw Throwables.propagate(e);
@@ -112,15 +128,9 @@ public class OsgiManager {
     public <T> Maybe<Class<T>> tryResolveClass(String type, Iterable<String> bundleUrlsOrNameVersionString) {
         Map<String,Throwable> bundleProblems = MutableMap.of();
         for (String bundleUrlOrNameVersionString: bundleUrlsOrNameVersionString) {
-            boolean noVersionInstalled = false;
             try {
-                String bundleNameVersion = bundleUrlToNameVersionString.get(bundleUrlOrNameVersionString);
-                if (bundleNameVersion==null) {
-                    noVersionInstalled = true;
-                    bundleNameVersion = bundleUrlOrNameVersionString;
-                }
-
-                Maybe<Bundle> bundle = Osgis.getBundle(framework, bundleNameVersion);
+                Maybe<Bundle> bundle = findBundle(bundleUrlOrNameVersionString);
+                
                 if (bundle.isPresent()) {
                     Bundle b = bundle.get();
                     Class<T> clazz;
@@ -137,20 +147,13 @@ public class OsgiManager {
                     }
                     return Maybe.of(clazz);
                 } else {
-                    bundleProblems.put(bundleUrlOrNameVersionString, new IllegalStateException("Unable to find bundle "+bundleUrlOrNameVersionString));
+                    bundleProblems.put(bundleUrlOrNameVersionString, ((Maybe.Absent<?>)bundle).getException());
                 }
+                
             } catch (Exception e) {
+                // should come from classloading now; name formatting or missing bundle errors will be caught above 
                 Exceptions.propagateIfFatal(e);
-                if (noVersionInstalled) {
-                    if (bundleUrlOrNameVersionString.contains("/")) {
-                        // suppress misleading nested trace if the input string looked like a URL
-                        bundleProblems.put(bundleUrlOrNameVersionString, new IllegalStateException("Bundle does not appear to be installed"));
-                    } else {
-                        bundleProblems.put(bundleUrlOrNameVersionString, new IllegalStateException("Bundle does not appear to be installed", e));
-                    }
-                } else {
-                    bundleProblems.put(bundleUrlOrNameVersionString, e);
-                }
+                bundleProblems.put(bundleUrlOrNameVersionString, e);
 
                 Throwable cause = e.getCause();
                 if (cause != null && cause.getMessage().contains("Unresolved constraint in bundle")) {
@@ -170,15 +173,22 @@ public class OsgiManager {
         }
     }
 
+    /** finds an installed bundle with the given URL or OSGi identifier ("symbolicName:version" string) */
+    public Maybe<Bundle> findBundle(String bundleUrlOrNameVersionString) {
+        String bundleNameVersion = urlToBundleIdentifier.get(bundleUrlOrNameVersionString);
+        if (bundleNameVersion==null) {
+            Maybe<String[]> nv = Osgis.parseOsgiIdentifier(bundleUrlOrNameVersionString);
+            if (nv.isPresent())
+                bundleNameVersion = bundleUrlOrNameVersionString;
+        }
+        Maybe<Bundle> bundle = Osgis.bundleFinder(framework).id(bundleNameVersion).preferringFromUrl(bundleUrlOrNameVersionString).find();
+        return bundle;
+    }
+
     public URL getResource(String name, Iterable<String> bundleUrlsOrNameVersionString) {
         for (String bundleUrlOrNameVersionString: bundleUrlsOrNameVersionString) {
             try {
-                String bundleNameVersion = bundleUrlToNameVersionString.get(bundleUrlOrNameVersionString);
-                if (bundleNameVersion==null) {
-                    bundleNameVersion = bundleUrlOrNameVersionString;
-                }
-                
-                Maybe<Bundle> bundle = Osgis.getBundle(framework, bundleNameVersion);
+                Maybe<Bundle> bundle = findBundle(bundleUrlOrNameVersionString);
                 if (bundle.isPresent()) {
                     URL result = bundle.get().getResource(name);
                     if (result!=null) return result;
@@ -190,4 +200,8 @@ public class OsgiManager {
         return null;
     }
 
+    public Framework getFramework() {
+        return framework;
+    }
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/72b29b95/core/src/main/java/brooklyn/util/ResourceUtils.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/util/ResourceUtils.java b/core/src/main/java/brooklyn/util/ResourceUtils.java
index 072789e..b40df0d 100644
--- a/core/src/main/java/brooklyn/util/ResourceUtils.java
+++ b/core/src/main/java/brooklyn/util/ResourceUtils.java
@@ -323,7 +323,7 @@ public class ResourceUtils {
         } catch (MalformedURLException e) {
             throw Exceptions.propagate(e);
         }
-        if (!urlOut.equals(in) && log.isDebugEnabled()) {
+        if (!urlOut.equals(url) && log.isDebugEnabled()) {
             log.debug("quietly changing " + url + " to " + urlOut);
         }
         return urlOut;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/72b29b95/core/src/main/java/brooklyn/util/osgi/Osgis.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/util/osgi/Osgis.java b/core/src/main/java/brooklyn/util/osgi/Osgis.java
index d8f20ae..f7508f6 100644
--- a/core/src/main/java/brooklyn/util/osgi/Osgis.java
+++ b/core/src/main/java/brooklyn/util/osgi/Osgis.java
@@ -28,10 +28,14 @@ import java.io.InputStreamReader;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.jar.Attributes;
 import java.util.jar.JarInputStream;
 import java.util.jar.JarOutputStream;
@@ -56,13 +60,16 @@ import org.slf4j.LoggerFactory;
 import brooklyn.util.ResourceUtils;
 import brooklyn.util.collections.MutableList;
 import brooklyn.util.collections.MutableMap;
+import brooklyn.util.collections.MutableSet;
 import brooklyn.util.exceptions.Exceptions;
 import brooklyn.util.exceptions.ReferenceWithError;
 import brooklyn.util.guava.Maybe;
 import brooklyn.util.net.Urls;
 import brooklyn.util.os.Os;
 import brooklyn.util.stream.Streams;
+import brooklyn.util.text.Strings;
 import brooklyn.util.time.Duration;
+import brooklyn.util.time.Time;
 
 import com.google.common.annotations.Beta;
 import com.google.common.base.Joiner;
@@ -82,54 +89,195 @@ public class Osgis {
 
     private static final String EXTENSION_PROTOCOL = "system";
     private static final String MANIFEST_PATH = "META-INF/MANIFEST.MF";
+    private static final Set<String> SYSTEM_BUNDLES = MutableSet.of();
+
+    public static class BundleFinder {
+        protected final Framework framework;
+        protected String symbolicName;
+        protected String version;
+        protected String url;
+        protected boolean urlMandatory = false;
+        protected final List<Predicate<? super Bundle>> predicates = MutableList.of();
+        
+        protected BundleFinder(Framework framework) {
+            this.framework = framework;
+        }
 
-    public static List<Bundle> getBundlesByName(Framework framework, String symbolicName, Predicate<Version> versionMatcher) {
-        List<Bundle> result = MutableList.of();
-        for (Bundle b: framework.getBundleContext().getBundles()) {
-            if (symbolicName.equals(b.getSymbolicName()) && versionMatcher.apply(b.getVersion())) {
+        public BundleFinder symbolicName(String symbolicName) {
+            this.symbolicName = symbolicName;
+            return this;
+        }
+
+        public BundleFinder version(String version) {
+            this.version = version;
+            return this;
+        }
+        
+        public BundleFinder id(String symbolicNameOptionallyWithVersion) {
+            if (Strings.isBlank(symbolicNameOptionallyWithVersion))
+                return this;
+            
+            Maybe<String[]> partsM = parseOsgiIdentifier(symbolicNameOptionallyWithVersion);
+            if (partsM.isAbsent())
+                throw new IllegalArgumentException("Cannot parse symbolic-name:version string '"+symbolicNameOptionallyWithVersion+"'");
+            String[] parts = partsM.get();
+            
+            symbolicName(parts[0]);
+            if (parts.length >= 2) version(parts[1]);
+            
+            return this;
+        }
+
+        /** Looks for a bundle matching the given URL;
+         * unlike {@link #requiringFromUrl(String)} however, if the URL does not match any bundles
+         * it will return other matching bundles <i>if</if> a {@link #symbolicName(String)} is specified.
+         */
+        public BundleFinder preferringFromUrl(String url) {
+            this.url = url;
+            urlMandatory = false;
+            return this;
+        }
+
+        /** Requires the bundle to have the given URL set as its location. */
+        public BundleFinder requiringFromUrl(String url) {
+            this.url = url;
+            urlMandatory = true;
+            return this;
+        }
+
+        /** Finds the best matching bundle. */
+        public Maybe<Bundle> find() {
+            return findOne(false);
+        }
+        
+        /** Finds the matching bundle, requiring it to be unique. */
+        public Maybe<Bundle> findUnique() {
+            return findOne(true);
+        }
+
+        protected Maybe<Bundle> findOne(boolean requireExactlyOne) {
+            if (symbolicName==null && url==null)
+                throw new IllegalStateException(this+" must be given either a symbolic name or a URL");
+            
+            List<Bundle> result = findAll();
+            if (result.isEmpty())
+                return Maybe.absent("No bundle matching "+getConstraintsDescription());
+            if (requireExactlyOne && result.size()>1)
+                return Maybe.absent("Multiple bundles ("+result.size()+") matching "+getConstraintsDescription());
+            
+            return Maybe.of(result.get(0));
+        }
+        
+        /** Finds all matching bundles, in decreasing version order. */
+        public List<Bundle> findAll() {
+            boolean urlMatched = false;
+            List<Bundle> result = MutableList.of();
+            for (Bundle b: framework.getBundleContext().getBundles()) {
+                if (symbolicName!=null && !symbolicName.equals(b.getSymbolicName())) continue;
+                if (version!=null && !Version.parseVersion(version).equals(b.getVersion())) continue;
+                for (Predicate<? super Bundle> predicate: predicates) {
+                    if (!predicate.apply(b)) continue;
+                }
+
+                // check url last, because if it isn't mandatory we should only clear if we find a url
+                // for which the other items also match
+                if (url!=null) {
+                    boolean matches = url.equals(b.getLocation());
+                    if (urlMandatory) {
+                        if (!matches) continue;
+                    } else {
+                        if (matches) {
+                            if (!urlMatched) {
+                                result.clear();
+                                urlMatched = true;
+                            }
+                        } else {
+                            if (urlMatched) {
+                                // can't use this bundle as we have previously found a preferred bundle, with a matching url
+                                continue;
+                            }
+                        }
+                    }
+                }
+                                
                 result.add(b);
             }
+            
+            if (symbolicName==null && url!=null && !urlMatched) {
+                // if we only "preferred" the url, and we did not match it, and we did not have a symbolic name,
+                // then clear the results list!
+                result.clear();
+            }
+
+            Collections.sort(result, new Comparator<Bundle>() {
+                @Override
+                public int compare(Bundle o1, Bundle o2) {
+                    return o2.getVersion().compareTo(o1.getVersion());
+                }
+            });
+            
+            return result;
+        }
+        
+        public String getConstraintsDescription() {
+            List<String> parts = MutableList.of();
+            if (symbolicName!=null) parts.add("symbolicName="+symbolicName);
+            if (version!=null) parts.add("version="+version);
+            if (url!=null)
+                parts.add("url["+(urlMandatory ? "required" : "preferred")+"]="+url);
+            if (!predicates.isEmpty())
+                parts.add("predicates="+predicates);
+            return Joiner.on(";").join(parts);
+        }
+        
+        public String toString() {
+            return getClass().getCanonicalName()+"["+getConstraintsDescription()+"]";
+        }
+
+        public BundleFinder version(final Predicate<Version> versionPredicate) {
+            return satisfying(new Predicate<Bundle>() {
+                @Override
+                public boolean apply(Bundle input) {
+                    return versionPredicate.apply(input.getVersion());
+                }
+            });
+        }
+        
+        public BundleFinder satisfying(Predicate<? super Bundle> predicate) {
+            predicates.add(predicate);
+            return this;
         }
-        return result;
+    }
+    
+    public static BundleFinder bundleFinder(Framework framework) {
+        return new BundleFinder(framework);
+    }
+
+    /** @deprecated since 0.7.0 use {@link #bundleFinder(Framework)} */ @Deprecated
+    public static List<Bundle> getBundlesByName(Framework framework, String symbolicName, Predicate<Version> versionMatcher) {
+        return bundleFinder(framework).symbolicName(symbolicName).version(versionMatcher).findAll();
     }
 
+    /** @deprecated since 0.7.0 use {@link #bundleFinder(Framework)} */ @Deprecated
     public static List<Bundle> getBundlesByName(Framework framework, String symbolicName) {
-        return getBundlesByName(framework, symbolicName, Predicates.<Version>alwaysTrue());
+        return bundleFinder(framework).symbolicName(symbolicName).findAll();
     }
 
     /**
      * Tries to find a bundle in the given framework with name matching either `name' or `name:version'.
-     */
+     * @deprecated since 0.7.0 use {@link #bundleFinder(Framework)} */ @Deprecated
     public static Maybe<Bundle> getBundle(Framework framework, String symbolicNameOptionallyWithVersion) {
-        String[] parts = symbolicNameOptionallyWithVersion.split(":");
-        Maybe<Bundle> result = Maybe.absent("No bundles matching "+symbolicNameOptionallyWithVersion);
-        if (parts.length == 2) {
-            result = getBundle(framework, parts[0], parts[1]);
-        } else if (parts.length == 1) {
-            // TODO: Select latest version rather than first result
-            List<Bundle> matches = getBundlesByName(framework, symbolicNameOptionallyWithVersion);
-            if (!matches.isEmpty()) {
-                result = Maybe.of(matches.iterator().next());
-            }
-        } else {
-            throw new IllegalArgumentException("Cannot parse symbolic-name:version string '"+symbolicNameOptionallyWithVersion+"'");
-        }
-        return result;
+        return bundleFinder(framework).id(symbolicNameOptionallyWithVersion).find();
     }
     
+    /** @deprecated since 0.7.0 use {@link #bundleFinder(Framework)} */ @Deprecated
     public static Maybe<Bundle> getBundle(Framework framework, String symbolicName, String version) {
-        return getBundle(framework, symbolicName, Version.parseVersion(version));
+        return bundleFinder(framework).symbolicName(symbolicName).version(version).find();
     }
 
+    /** @deprecated since 0.7.0 use {@link #bundleFinder(Framework)} */ @Deprecated
     public static Maybe<Bundle> getBundle(Framework framework, String symbolicName, Version version) {
-        List<Bundle> matches = getBundlesByName(framework, symbolicName, Predicates.equalTo(version));
-        if (matches.isEmpty()) {
-            return Maybe.absent("No bundles matching name=" + symbolicName + " version=" + version);
-        } else if (matches.size() > 1) {
-            LOG.warn("More than one bundle in framework={} matched name={}, version={}! Returning first of matches={}",
-                    new Object[]{framework, symbolicName, version, Joiner.on(", ").join(matches)});
-        }
-        return Maybe.of(matches.iterator().next());
+        return bundleFinder(framework).symbolicName(symbolicName).version(Predicates.equalTo(version)).findUnique();
     }
 
     // -------- creating
@@ -168,6 +316,7 @@ public class Osgis {
         Map<Object,Object> cfg = MutableMap.copyOf(extraStartupConfig);
         if (clean) cfg.put(Constants.FRAMEWORK_STORAGE_CLEAN, "onFirstInit");
         if (felixCacheDir!=null) cfg.put(Constants.FRAMEWORK_STORAGE, felixCacheDir);
+        cfg.put(Constants.FRAMEWORK_BSNVERSION, Constants.FRAMEWORK_BSNVERSION_MULTIPLE);
         FrameworkFactory factory = newFrameworkFactory();
 
         Stopwatch timer = Stopwatch.createStarted();
@@ -180,12 +329,14 @@ public class Osgis {
             // framework bundle start exceptions are not interesting to caller...
             throw Exceptions.propagate(e);
         }
+        LOG.debug("System bundles are: "+SYSTEM_BUNDLES);
         LOG.debug("OSGi framework started in " + Duration.of(timer));
-
         return framework;
     }
 
     private static void installBootBundles(Framework framework) {
+        Stopwatch timer = Stopwatch.createStarted();
+        LOG.debug("Installing OSGi boot bundles from "+Osgis.class.getClassLoader()+"...");
         Enumeration<URL> resources;
         try {
             resources = Osgis.class.getClassLoader().getResources(MANIFEST_PATH);
@@ -193,29 +344,32 @@ public class Osgis {
             throw Exceptions.propagate(e);
         }
         BundleContext bundleContext = framework.getBundleContext();
-        Map<String, Bundle> installedBundles = getInstalledBundles(bundleContext);
+        Map<String, Bundle> installedBundles = getInstalledBundlesById(bundleContext);
         while(resources.hasMoreElements()) {
             URL url = resources.nextElement();
-            ReferenceWithError<Boolean> installResult = installExtensionBundle(bundleContext, url, installedBundles, getVersionedId(framework));
-            if (installResult.hasError()) {
-                if (installResult.getWithoutError()) {
-                    // true return code means it was installed or trivially not installed
-                    if (LOG.isTraceEnabled())
-                        LOG.trace(installResult.getError().getMessage());
-                } else {
-                    if (installResult.masksErrorIfPresent()) {
-                        // if error is masked, then it's not so important (many of the bundles we are looking at won't have manifests)
-                        LOG.debug(installResult.getError().getMessage());
+            ReferenceWithError<?> installResult = installExtensionBundle(bundleContext, url, installedBundles, getVersionedId(framework));
+            if (installResult.hasError() && !installResult.masksErrorIfPresent()) {
+                // it's reported as a critical error, so warn here
+                LOG.warn("Unable to install manifest from "+url+": "+installResult.getError(), installResult.getError());
+            } else {
+                Object result = installResult.getWithoutError();
+                if (result instanceof Bundle) {
+                    String v = getVersionedId( (Bundle)result );
+                    SYSTEM_BUNDLES.add(v);
+                    if (installResult.hasError()) {
+                        LOG.debug(installResult.getError().getMessage()+(result!=null ? " ("+result+"/"+v+")" : ""));
                     } else {
-                        // it's reported as a critical error, so warn here
-                        LOG.warn("Unable to install manifest from "+url+": "+installResult.getError(), installResult.getError());
+                        LOG.debug("Installed "+v+" from "+url);
                     }
+                } else if (installResult.hasError()) {
+                    LOG.debug(installResult.getError().getMessage());
                 }
             }
         }
+        LOG.debug("Installed OSGi boot bundles in "+Time.makeTimeStringRounded(timer)+": "+Arrays.asList(framework.getBundleContext().getBundles()));
     }
 
-    private static Map<String, Bundle> getInstalledBundles(BundleContext bundleContext) {
+    private static Map<String, Bundle> getInstalledBundlesById(BundleContext bundleContext) {
         Map<String, Bundle> installedBundles = new HashMap<String, Bundle>();
         Bundle[] bundles = bundleContext.getBundles();
         for (Bundle b : bundles) {
@@ -224,15 +378,21 @@ public class Osgis {
         return installedBundles;
     }
 
-    private static ReferenceWithError<Boolean> installExtensionBundle(BundleContext bundleContext, URL manifestUrl, Map<String, Bundle> installedBundles, String frameworkVersionedId) {
+    /** Wraps the bundle if successful or already installed, wraps TRUE if it's the system entry,
+     * wraps null if the bundle is already installed from somewhere else;
+     * in all these cases <i>masking</i> an explanatory error if already installed or it's the system entry.
+     * <p>
+     * Returns an instance wrapping null and <i>throwing</i> an error if the bundle could not be installed.
+     */
+    private static ReferenceWithError<?> installExtensionBundle(BundleContext bundleContext, URL manifestUrl, Map<String, Bundle> installedBundles, String frameworkVersionedId) {
         //ignore http://felix.extensions:9/ system entry
         if("felix.extensions".equals(manifestUrl.getHost())) 
-            return ReferenceWithError.newInstanceMaskingError(true, new IllegalArgumentException("Skiping install of internal extension bundle from "+manifestUrl));
+            return ReferenceWithError.newInstanceMaskingError(null, new IllegalArgumentException("Skipping install of internal extension bundle from "+manifestUrl));
 
         try {
             Manifest manifest = readManifest(manifestUrl);
             if (!isValidBundle(manifest)) 
-                return ReferenceWithError.newInstanceMaskingError(false, new IllegalArgumentException("Resource at "+manifestUrl+" is not an OSGi bundle: no valid manifest"));
+                return ReferenceWithError.newInstanceMaskingError(null, new IllegalArgumentException("Resource at "+manifestUrl+" is not an OSGi bundle: no valid manifest"));
             
             String versionedId = getVersionedId(manifest);
             URL bundleUrl = ResourceUtils.getContainerUrl(manifestUrl, MANIFEST_PATH);
@@ -242,9 +402,9 @@ public class Osgis {
                 if (!bundleUrl.equals(existingBundle.getLocation()) &&
                         //the framework bundle is always pre-installed, don't display duplicate info
                         !versionedId.equals(frameworkVersionedId)) {
-                    return ReferenceWithError.newInstanceMaskingError(false, new IllegalArgumentException("Bundle "+versionedId+" (from manifest " + manifestUrl + ") is already installed, from " + existingBundle.getLocation()));
+                    return ReferenceWithError.newInstanceMaskingError(null, new IllegalArgumentException("Bundle "+versionedId+" (from manifest " + manifestUrl + ") is already installed, from " + existingBundle.getLocation()));
                 }
-                return ReferenceWithError.newInstanceMaskingError(true, new IllegalArgumentException("Bundle "+versionedId+" from manifest " + manifestUrl + " is already installed"));
+                return ReferenceWithError.newInstanceMaskingError(existingBundle, new IllegalArgumentException("Bundle "+versionedId+" from manifest " + manifestUrl + " is already installed"));
             }
             
             byte[] jar = buildExtensionBundle(manifest);
@@ -253,10 +413,10 @@ public class Osgis {
             //(since we cannot access BundleImpl.isExtension)
             Bundle newBundle = bundleContext.installBundle(EXTENSION_PROTOCOL + ":" + bundleUrl.toString(), new ByteArrayInputStream(jar));
             installedBundles.put(versionedId, newBundle);
-            return ReferenceWithError.newInstanceWithoutError(true);
+            return ReferenceWithError.newInstanceWithoutError(newBundle);
         } catch (Exception e) {
             Exceptions.propagateIfFatal(e);
-            return ReferenceWithError.newInstanceThrowingError(false, 
+            return ReferenceWithError.newInstanceThrowingError(null, 
                 new IllegalStateException("Problem installing extension bundle " + manifestUrl + ": "+e, e));
         }
     }
@@ -364,10 +524,11 @@ public class Osgis {
             return bundle;
         }
 
-        //Note that in OSGi 4.3+ it could be possible to have the same version installed
-        //multiple times in more advanced scenarios. In our case we don't support it.
+        // We now support same version installed multiple times (avail since OSGi 4.3+).
+        // However we do not support overriding *system* bundles, ie anything already on the classpath.
+        // If we wanted to disable multiple versions, see comments below, and reference to FRAMEWORK_BSNVERSION_MULTIPLE above.
         
-        //Felix already assumes the stream is pointing to a Jar
+        // Felix already assumes the stream is pointing to a JAR
         JarInputStream stream;
         try {
             stream = new JarInputStream(getUrlStream(url));
@@ -379,7 +540,15 @@ public class Osgis {
         String versionedId = getVersionedId(manifest);
         for (Bundle installedBundle : framework.getBundleContext().getBundles()) {
             if (versionedId.equals(getVersionedId(installedBundle))) {
-                return installedBundle;
+                if (SYSTEM_BUNDLES.contains(versionedId)) {
+                    LOG.debug("Already have system bundle "+versionedId+" from "+installedBundle+"/"+installedBundle.getLocation()+" when requested "+url+"; not installing");
+                    // "System bundles" (ie things on the classpath) cannot be overridden
+                    return installedBundle;
+                } else {
+                    LOG.debug("Already have bundle "+versionedId+" from "+installedBundle+"/"+installedBundle.getLocation()+" when requested "+url+"; but it is not a system bundle so proceeding");
+                    // Other bundles can be installed multiple times. To ignore multiples and continue to use the old one, 
+                    // just return the installedBundle as done just above for system bundles.
+                }
             }
         }
         return null;
@@ -395,6 +564,25 @@ public class Osgis {
                 EXTENSION_PROTOCOL.equals(Urls.getProtocol(location));
     }
 
+    /** Takes a string which might be of the form "symbolic-name" or "symbolic-name:version" (or something else entirely)
+     * and returns an array of 1 or 2 string items being the symbolic name or symbolic name and version if possible
+     * (or returning {@link Maybe#absent()} if not, with a suitable error message). */
+    public static Maybe<String[]> parseOsgiIdentifier(String symbolicNameOptionalWithVersion) {
+        if (Strings.isBlank(symbolicNameOptionalWithVersion))
+            return Maybe.absent("OSGi identifier is blank");
+        
+        String[] parts = symbolicNameOptionalWithVersion.split(":");
+        if (parts.length>2)
+            return Maybe.absent("OSGi identifier has too many parts; max one ':' symbol");
+        
+        try {
+            Version.parseVersion(parts[1]);
+        } catch (IllegalArgumentException e) {
+            return Maybe.absent("OSGi identifier has invalid version string");
+        }
+        
+        return Maybe.of(parts);
+    }
 
     /**
      * The class is not used, staying for future reference.

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/72b29b95/core/src/test/dependencies/osgi/more-entities-v2-evil-twin/pom.xml
----------------------------------------------------------------------
diff --git a/core/src/test/dependencies/osgi/more-entities-v2-evil-twin/pom.xml b/core/src/test/dependencies/osgi/more-entities-v2-evil-twin/pom.xml
new file mode 100644
index 0000000..28655c2
--- /dev/null
+++ b/core/src/test/dependencies/osgi/more-entities-v2-evil-twin/pom.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <packaging>jar</packaging>
+
+    <groupId>org.apache.brooklyn.test.resources.osgi.evil_twin</groupId>
+    <artifactId>brooklyn-test-osgi-more-entities</artifactId>
+    <version>0.2.0</version>
+
+    <name>OSGi bundled test entities</name>
+
+    <description>
+        Simple entities for testing the OSGi functionality
+    </description>
+
+    <parent>
+        <groupId>org.apache.brooklyn</groupId>
+        <artifactId>brooklyn-parent</artifactId>
+        <version>0.7.0-SNAPSHOT</version><!-- BROOKLYN_VERSION -->
+        <relativePath>../../../../../../pom.xml</relativePath>
+    </parent>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.brooklyn</groupId>
+            <artifactId>brooklyn-core</artifactId>
+            <version>${brooklyn.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.brooklyn</groupId>
+            <artifactId>brooklyn-api</artifactId>
+            <version>${brooklyn.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.brooklyn</groupId>
+            <artifactId>brooklyn-utils-common</artifactId>
+            <version>${brooklyn.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.brooklyn.test.resources.osgi</groupId>
+            <artifactId>brooklyn-test-osgi-entities</artifactId>
+            <version>0.1.0</version>
+        </dependency>
+    </dependencies>
+    
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>2.3.2</version>
+                <configuration>
+                    <outputDirectory>../../../resources/brooklyn/osgi</outputDirectory>
+                    <finalName>brooklyn-test-osgi-more-entities_evil-twin_${version}</finalName>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <version>2.5.3</version>
+                <configuration>
+                    <instructions>
+                        <Bundle-SymbolicName>org.apache.brooklyn.test.resources.osgi.brooklyn-test-osgi-more-entities</Bundle-SymbolicName>
+                        <Bundle-Version>${project.version}</Bundle-Version>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/72b29b95/core/src/test/dependencies/osgi/more-entities-v2-evil-twin/src/main/java/brooklyn/osgi/tests/more/MoreEntity.java
----------------------------------------------------------------------
diff --git a/core/src/test/dependencies/osgi/more-entities-v2-evil-twin/src/main/java/brooklyn/osgi/tests/more/MoreEntity.java b/core/src/test/dependencies/osgi/more-entities-v2-evil-twin/src/main/java/brooklyn/osgi/tests/more/MoreEntity.java
new file mode 100644
index 0000000..553cbc5
--- /dev/null
+++ b/core/src/test/dependencies/osgi/more-entities-v2-evil-twin/src/main/java/brooklyn/osgi/tests/more/MoreEntity.java
@@ -0,0 +1,38 @@
+/*
+ * 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 brooklyn.osgi.tests.more;
+
+
+import brooklyn.entity.Effector;
+import brooklyn.entity.Entity;
+import brooklyn.entity.effector.Effectors;
+import brooklyn.entity.proxying.ImplementedBy;
+
+@ImplementedBy(MoreEntityImpl.class)
+public interface MoreEntity extends Entity {
+
+    public static final Effector<String> SAY_HI = Effectors.effector(String.class, "sayHI")
+        .description("says HO to an uppercased name")
+        .parameter(String.class, "name")
+        .buildAbstract();
+
+    /** Makes a string saying HO to the given name, in contrast to v1 and v2. */
+    String sayHI(String name);
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/72b29b95/core/src/test/dependencies/osgi/more-entities-v2-evil-twin/src/main/java/brooklyn/osgi/tests/more/MoreEntityImpl.java
----------------------------------------------------------------------
diff --git a/core/src/test/dependencies/osgi/more-entities-v2-evil-twin/src/main/java/brooklyn/osgi/tests/more/MoreEntityImpl.java b/core/src/test/dependencies/osgi/more-entities-v2-evil-twin/src/main/java/brooklyn/osgi/tests/more/MoreEntityImpl.java
new file mode 100644
index 0000000..2788b8b
--- /dev/null
+++ b/core/src/test/dependencies/osgi/more-entities-v2-evil-twin/src/main/java/brooklyn/osgi/tests/more/MoreEntityImpl.java
@@ -0,0 +1,47 @@
+/*
+ * 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 brooklyn.osgi.tests.more;
+
+import brooklyn.entity.basic.AbstractEntity;
+import brooklyn.entity.effector.EffectorBody;
+import brooklyn.policy.PolicySpec;
+import brooklyn.util.config.ConfigBag;
+
+
+public class MoreEntityImpl extends AbstractEntity implements MoreEntity {
+
+    /** Unlike v1, this declares an explicit dependency on SimplePolicy */
+    @Override
+    public void init() {
+        super.init();
+        getMutableEntityType().addEffector(SAY_HI, new EffectorBody<String>() {
+            @Override
+            public String call(ConfigBag parameters) {
+                return sayHI((String)parameters.getStringKey("name"));
+            }
+        });
+        addPolicy(PolicySpec.create(brooklyn.osgi.tests.SimplePolicy.class));
+    }
+    
+    /** Returns HO instead of HI (like v2 non-evil twin) or Hi (like v1) */
+    public String sayHI(String name) {
+        return "HO "+name.toUpperCase();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/72b29b95/core/src/test/java/brooklyn/catalog/internal/CatalogItems.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/catalog/internal/CatalogItems.java b/core/src/test/java/brooklyn/catalog/internal/CatalogItems.java
index dcefbcc..80b41d5 100644
--- a/core/src/test/java/brooklyn/catalog/internal/CatalogItems.java
+++ b/core/src/test/java/brooklyn/catalog/internal/CatalogItems.java
@@ -18,6 +18,8 @@
  */
 package brooklyn.catalog.internal;
 
+import com.google.common.annotations.Beta;
+
 import io.brooklyn.camp.spi.pdp.DeploymentPlan;
 
 /** Only for internal use / use in tests. */
@@ -41,6 +43,12 @@ public class CatalogItems {
         return target;
     }
     
+    // TODO just added this method to expose registeredTypeName for tests; but it should all go away;
+    // so long as tests pass no need to keep deprecation imho
+    @Beta
+    public static CatalogEntityItemDto newEntityFromJavaWithRegisteredTypeName(String registeredTypeName, String javaType) {
+        return set(new CatalogEntityItemDto(), registeredTypeName, javaType, registeredTypeName, null, null);
+    }
     public static CatalogEntityItemDto newEntityFromJava(String javaType, String name) {
         return newEntityFromJava(javaType, name, null, null);
     }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/72b29b95/core/src/test/java/brooklyn/management/osgi/OsgiTestResources.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/management/osgi/OsgiTestResources.java b/core/src/test/java/brooklyn/management/osgi/OsgiTestResources.java
index de889be..48d01dc 100644
--- a/core/src/test/java/brooklyn/management/osgi/OsgiTestResources.java
+++ b/core/src/test/java/brooklyn/management/osgi/OsgiTestResources.java
@@ -28,6 +28,7 @@ package brooklyn.management.osgi;
  */
 public class OsgiTestResources {
 
+
     /**
      * brooklyn-osgi-test-a_0.1.0 -
      * defines TestA which has a "times" method and a static multiplier field;
@@ -48,7 +49,10 @@ public class OsgiTestResources {
      * another bundle with a minimal sayHi effector, used to test versioning and dependencies
      * (this one has no dependencies, but see also {@value #BROOKLYN_TEST_MORE_ENTITIES_V2_PATH})
      */
-    public static final String BROOKLYN_TEST_MORE_ENTITIES_V1_PATH = "/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.1.0.jar";
+    public static final String BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FINAL_PART = "brooklyn-test-osgi-more-entities";
+    public static final String BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL = 
+        "org.apache.brooklyn.test.resources.osgi."+BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FINAL_PART;
+    public static final String BROOKLYN_TEST_MORE_ENTITIES_V1_PATH = "/brooklyn/osgi/" + BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FINAL_PART + "_0.1.0.jar";
     public static final String BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY = "brooklyn.osgi.tests.more.MoreEntity";
     
     /**
@@ -56,6 +60,14 @@ public class OsgiTestResources {
      * similar to {@link #BROOKLYN_TEST_MORE_ENTITIES_V1_PATH} but saying "HI NAME" rather than "Hi NAME",
      * and declaring an explicit dependency on SimplePolicy from {@link #BROOKLYN_TEST_OSGI_ENTITIES_PATH}
      */
-    public static final String BROOKLYN_TEST_MORE_ENTITIES_V2_PATH = "/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.2.0.jar";
+    public static final String BROOKLYN_TEST_MORE_ENTITIES_V2_PATH = "/brooklyn/osgi/" + BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FINAL_PART + "_0.2.0.jar";
+    
+    /**
+     * bundle with identical metadata (same symbolic name and version -- hence being an evil twin) 
+     * as {@link #BROOKLYN_TEST_MORE_ENTITIES_V2_PATH},
+     * but slightly different behaviour -- saying "HO NAME" -- in order to make sure we can differentiate two two
+     * at runtime.
+     */
+    public static final String BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_PATH = "/brooklyn/osgi/" + BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FINAL_PART + "_evil-twin_0.2.0.jar";
     
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/72b29b95/core/src/test/java/brooklyn/management/osgi/OsgiVersionMoreEntityTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/management/osgi/OsgiVersionMoreEntityTest.java b/core/src/test/java/brooklyn/management/osgi/OsgiVersionMoreEntityTest.java
index c5df30d..4b653ab 100644
--- a/core/src/test/java/brooklyn/management/osgi/OsgiVersionMoreEntityTest.java
+++ b/core/src/test/java/brooklyn/management/osgi/OsgiVersionMoreEntityTest.java
@@ -21,6 +21,7 @@ package brooklyn.management.osgi;
 import java.io.File;
 import java.io.IOException;
 import java.lang.reflect.InvocationTargetException;
+import java.util.List;
 
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleException;
@@ -46,6 +47,7 @@ import brooklyn.management.internal.ManagementContextInternal;
 import brooklyn.policy.PolicySpec;
 import brooklyn.test.entity.LocalManagementContextForTests;
 import brooklyn.test.entity.TestApplication;
+import brooklyn.util.guava.Maybe;
 import brooklyn.util.os.Os;
 import brooklyn.util.osgi.Osgis;
 
@@ -66,6 +68,8 @@ public class OsgiVersionMoreEntityTest {
     public static final String BROOKLYN_TEST_MORE_ENTITIES_V1_URL = "classpath:"+BROOKLYN_TEST_MORE_ENTITIES_V1_PATH;
     public static final String BROOKLYN_TEST_MORE_ENTITIES_V2_PATH = OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_V2_PATH;
     public static final String BROOKLYN_TEST_MORE_ENTITIES_V2_URL = "classpath:"+BROOKLYN_TEST_MORE_ENTITIES_V2_PATH;
+    public static final String BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_PATH = OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_PATH;
+    public static final String BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_URL = "classpath:"+BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_PATH;
     
     protected LocalManagementContext mgmt;
     protected TestApplication app;
@@ -115,17 +119,23 @@ public class OsgiVersionMoreEntityTest {
         }
     }
     
+    protected CatalogItem<?, ?> addCatalogItemWithTypeAsName(String type, String ...libraries) {
+        return addCatalogItemWithNameAndType(type, type, libraries);
+    }
     @SuppressWarnings("deprecation")
-    protected CatalogItem<?, ?> addCatalogItem(String type, String ...libraries) {
-        CatalogEntityItemDto c1 = newCatalogItem(type, libraries);
+    protected CatalogItem<?, ?> addCatalogItemWithNameAndType(String symName, String type, String ...libraries) {
+        CatalogEntityItemDto c1 = newCatalogItemWithNameAndType(symName, type, libraries);
         mgmt.getCatalog().addItem(c1);
         CatalogItem<?, ?> c2 = mgmt.getCatalog().getCatalogItem(type);
         return c2;
     }
 
-    static CatalogEntityItemDto newCatalogItem(String type, String ...libraries) {
-        CatalogEntityItemDto c1 = CatalogItems.newEntityFromJava(type, type);
-        c1.setCatalogItemId(type);
+    static CatalogEntityItemDto newCatalogItemWithTypeAsName(String type, String ...libraries) {
+        return newCatalogItemWithNameAndType(type, type, libraries);
+    }
+    static CatalogEntityItemDto newCatalogItemWithNameAndType(String symName, String type, String ...libraries) {
+        CatalogEntityItemDto c1 = CatalogItems.newEntityFromJavaWithRegisteredTypeName(symName, type);
+        c1.setCatalogItemId(symName);
         for (String library: libraries)
             c1.getLibrariesDto().addBundle(library);
         return c1;
@@ -149,6 +159,9 @@ public class OsgiVersionMoreEntityTest {
     public static void assertV2MethodCall(Entity me) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
         Assert.assertEquals(doMethodCallBrooklyn(me), "HI BROOKLYN");
     }
+    public static void assertV2EvilTwinMethodCall(Entity me) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
+        Assert.assertEquals(doMethodCallBrooklyn(me), "HO BROOKLYN");
+    }
 
     public static Object doMethodCallBrooklyn(Entity me) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
         return me.getClass().getMethod("sayHI", String.class).invoke(me, "Brooklyn");
@@ -160,6 +173,9 @@ public class OsgiVersionMoreEntityTest {
     public static void assertV2EffectorCall(Entity me) {
         Assert.assertEquals(doEffectorCallBrooklyn(me), "HI BROOKLYN");
     }
+    public static void assertV2EvilTwinEffectorCall(Entity me) {
+        Assert.assertEquals(doEffectorCallBrooklyn(me), "HO BROOKLYN");
+    }
 
     public static String doEffectorCallBrooklyn(Entity me) {
         return me.invoke(Effectors.effector(String.class, "sayHI").buildAbstract(), ImmutableMap.of("name", "brooklyn")).getUnchecked();
@@ -167,7 +183,7 @@ public class OsgiVersionMoreEntityTest {
 
     @Test
     public void testMoreEntitiesV1() throws Exception {
-        CatalogItem<?, ?> c2 = addCatalogItem(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, BROOKLYN_TEST_MORE_ENTITIES_V1_URL);
+        CatalogItem<?, ?> c2 = addCatalogItemWithTypeAsName(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, BROOKLYN_TEST_MORE_ENTITIES_V1_URL);
         
         // test load and instantiate
         Entity me = addItemFromCatalog(c2);
@@ -193,12 +209,12 @@ public class OsgiVersionMoreEntityTest {
 
     @Test
     public void testMoreEntitiesV1Policy() throws Exception {
-        CatalogItem<?, ?> c2 = addCatalogItem(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, BROOKLYN_TEST_MORE_ENTITIES_V1_URL);
+        CatalogItem<?, ?> c2 = addCatalogItemWithTypeAsName(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, BROOKLYN_TEST_MORE_ENTITIES_V1_URL);
         
         // test load and instantiate
         Entity me = addItemFromCatalog(c2);
 
-        CatalogItem<?, ?> cp = addCatalogItem(OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_POLICY, 
+        CatalogItem<?, ?> cp = addCatalogItemWithTypeAsName(OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_POLICY, 
             BROOKLYN_TEST_OSGI_ENTITIES_URL);
         me.addPolicy(getPolicySpec(cp));
         
@@ -213,7 +229,7 @@ public class OsgiVersionMoreEntityTest {
 
     @Test
     public void testMoreEntitiesV2FailsWithoutBasicTestOsgiEntitiesBundle() throws Exception {
-        CatalogItem<?, ?> c2 = addCatalogItem(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, 
+        CatalogItem<?, ?> c2 = addCatalogItemWithTypeAsName(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, 
             BROOKLYN_TEST_MORE_ENTITIES_V2_URL);
         
         // test load and instantiate
@@ -231,7 +247,7 @@ public class OsgiVersionMoreEntityTest {
     // and has policy, with policy item catalog ID is reasonable
     @Test
     public void testMoreEntitiesV2() throws Exception {
-        CatalogItem<?, ?> c2 = addCatalogItem(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, 
+        CatalogItem<?, ?> c2 = addCatalogItemWithTypeAsName(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, 
             BROOKLYN_TEST_MORE_ENTITIES_V2_URL, BROOKLYN_TEST_OSGI_ENTITIES_URL);
         
         // test load and instantiate
@@ -250,9 +266,9 @@ public class OsgiVersionMoreEntityTest {
 
     @Test
     public void testMoreEntitiesV1ThenV2GivesV2() throws Exception {
-        addCatalogItem(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, 
+        addCatalogItemWithTypeAsName(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, 
             BROOKLYN_TEST_MORE_ENTITIES_V1_URL);
-        addCatalogItem(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, 
+        addCatalogItemWithTypeAsName(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, 
             BROOKLYN_TEST_MORE_ENTITIES_V2_URL, BROOKLYN_TEST_OSGI_ENTITIES_URL);
         
         // test load and instantiate
@@ -265,9 +281,9 @@ public class OsgiVersionMoreEntityTest {
 
     @Test
     public void testMoreEntitiesV2ThenV1GivesV1() throws Exception {
-        addCatalogItem(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, 
+        addCatalogItemWithTypeAsName(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, 
             BROOKLYN_TEST_MORE_ENTITIES_V2_URL, BROOKLYN_TEST_OSGI_ENTITIES_URL);
-        addCatalogItem(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, 
+        addCatalogItemWithTypeAsName(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, 
             BROOKLYN_TEST_MORE_ENTITIES_V1_URL);
         
         // test load and instantiate
@@ -301,18 +317,64 @@ public class OsgiVersionMoreEntityTest {
         Assert.assertEquals(me.getPolicies().size(), 0, "Wrong number of policies: "+me.getPolicies());
     }
 
-    // TODO test YAML for many of the above (in the camp project CAMP, using other code below)
-    
+    @Test
+    public void testUnfazedByMoreEntitiesV1AndV2AndV2EvilTwin() throws Exception {
+        addCatalogItemWithNameAndType("v1", OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY,
+            BROOKLYN_TEST_MORE_ENTITIES_V1_URL);
+        addCatalogItemWithNameAndType("v2", OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY,
+            BROOKLYN_TEST_MORE_ENTITIES_V2_URL, BROOKLYN_TEST_OSGI_ENTITIES_URL);
+        addCatalogItemWithNameAndType("v2-evil", OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY,
+            BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_URL, BROOKLYN_TEST_OSGI_ENTITIES_URL);
+
+        // test osgi finding
+        
+        List<Bundle> bundles = Osgis.bundleFinder(mgmt.getOsgiManager().get().getFramework())
+            .symbolicName(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL).findAll();
+        Assert.assertEquals(bundles.size(), 3, "Wrong list of bundles: "+bundles);
+        
+        Maybe<Bundle> preferredVersion = Osgis.bundleFinder(mgmt.getOsgiManager().get().getFramework())
+            .symbolicName(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL).find();
+        Assert.assertTrue(preferredVersion.isPresent());
+        Assert.assertEquals(preferredVersion.get().getVersion().toString(), "0.2.0");
+        
+        Maybe<Bundle> evilVersion = Osgis.bundleFinder(mgmt.getOsgiManager().get().getFramework()).
+            preferringFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_URL).find();
+        Assert.assertTrue(evilVersion.isPresent());
+        Assert.assertEquals(evilVersion.get().getVersion().toString(), "0.2.0");
+
+        // test preferredUrl vs requiredUrl
+
+        Maybe<Bundle> versionIgnoresInvalidPreferredUrl = Osgis.bundleFinder(mgmt.getOsgiManager().get().getFramework())
+            .symbolicName(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL)
+            .version("0.1.0")
+            .preferringFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_URL).find();
+        Assert.assertTrue(versionIgnoresInvalidPreferredUrl.isPresent());
+        Assert.assertEquals(versionIgnoresInvalidPreferredUrl.get().getVersion().toString(), "0.1.0");
+
+        Maybe<Bundle> versionRespectsInvalidRequiredUrl = Osgis.bundleFinder(mgmt.getOsgiManager().get().getFramework())
+            .symbolicName(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL)
+            .version("0.1.0")
+            .requiringFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_URL).find();
+        Assert.assertFalse(versionRespectsInvalidRequiredUrl.isPresent());
+
+        // test entity resolution
+        
+        Entity v2 = addItemFromCatalog( mgmt.getCatalog().getCatalogItem("v2") );
+        assertV2MethodCall(v2);
+        assertV2EffectorCall(v2);
+        Assert.assertEquals(v2.getPolicies().size(), 1, "Wrong number of policies: "+v2.getPolicies());
+
+        Entity v2_evil = addItemFromCatalog( mgmt.getCatalog().getCatalogItem("v2-evil") );
+        assertV2EvilTwinMethodCall(v2_evil);
+        assertV2EvilTwinEffectorCall(v2_evil);
+        Assert.assertEquals(v2_evil.getPolicies().size(), 1, "Wrong number of policies: "+v2_evil.getPolicies());
+
+        Entity v1 = addItemFromCatalog( mgmt.getCatalog().getCatalogItem("v1") );
+        assertV1MethodCall(v1);
+        assertV1EffectorCall(v1);
+        Assert.assertEquals(v1.getPolicies().size(), 0, "Wrong number of policies: "+v1.getPolicies());
+    }
+
     // TODO versioning (WIP until #92), install both V1 and V2 with version number, and test that both work
-    
-    // TODO other code which might be useful - but requires CAMP:
-//        mgmt.getCatalog().addItem(Strings.lines(
-//            "brooklyn.catalog:",
-//            "  id: my-entity",
-//            "brooklyn.library:",
-//            "- url: "+BROOKLYN_TEST_MORE_ENTITIES_V1_URL,
-//            "services:",
-//            "- type: "+OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY
-//        ));
-    
+        
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/72b29b95/core/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_evil-twin_0.2.0.jar
----------------------------------------------------------------------
diff --git a/core/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_evil-twin_0.2.0.jar b/core/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_evil-twin_0.2.0.jar
new file mode 100644
index 0000000..2d52340
Binary files /dev/null and b/core/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_evil-twin_0.2.0.jar differ

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/72b29b95/core/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_evil-twin_0.2.0.txt
----------------------------------------------------------------------
diff --git a/core/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_evil-twin_0.2.0.txt b/core/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_evil-twin_0.2.0.txt
new file mode 100644
index 0000000..9597d3b
--- /dev/null
+++ b/core/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_evil-twin_0.2.0.txt
@@ -0,0 +1,21 @@
+# 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.
+
+The file brooklyn-test-osgi-entities.jar is for testing a deployment of
+an OSGi bundle containing entities.
+
+The source is in core/src/test/dependencies/osgi/more-entities-v2-evil-twin

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/72b29b95/utils/common/src/main/java/brooklyn/util/exceptions/ReferenceWithError.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/brooklyn/util/exceptions/ReferenceWithError.java b/utils/common/src/main/java/brooklyn/util/exceptions/ReferenceWithError.java
index 2ffe119..bd52b3f 100644
--- a/utils/common/src/main/java/brooklyn/util/exceptions/ReferenceWithError.java
+++ b/utils/common/src/main/java/brooklyn/util/exceptions/ReferenceWithError.java
@@ -93,5 +93,9 @@ public class ReferenceWithError<T> implements Supplier<T> {
     public boolean hasError() {
         return error!=null;
     }
-    
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName()+"["+object+(error!=null?"/"+(maskError?"masking:":"throwing:")+error:"")+"]";
+    }
 }


Mime
View raw message