From c267108cbb260d384a335fa20e3a151a4ff8571d Mon Sep 17 00:00:00 2001 From: Ioannis Canellos Date: Fri, 7 Dec 2012 02:02:14 +0200 Subject: [PATCH] fix issue #1044: Added listener interfaces for Providers and Apis. --- core/pom.xml | 6 + core/src/main/java/org/jclouds/apis/Apis.java | 7 +- .../main/java/org/jclouds/osgi/Activator.java | 138 ++++++--- .../java/org/jclouds/osgi/ApiListener.java | 46 +++ .../main/java/org/jclouds/osgi/Bundles.java | 143 +++++++++ .../jclouds/osgi/MetadataBundleListener.java | 281 ++++++++---------- .../org/jclouds/osgi/ProviderListener.java | 47 +++ .../jclouds/providers/ProviderRegistry.java | 5 +- .../java/org/jclouds/providers/Providers.java | 6 +- .../java/org/jclouds/osgi/BundlesTest.java | 115 +++++++ .../osgi/MetadataBundleListenerTest.java | 77 ++++- project/pom.xml | 1 + 12 files changed, 665 insertions(+), 207 deletions(-) create mode 100644 core/src/main/java/org/jclouds/osgi/ApiListener.java create mode 100644 core/src/main/java/org/jclouds/osgi/Bundles.java create mode 100644 core/src/main/java/org/jclouds/osgi/ProviderListener.java create mode 100644 core/src/test/java/org/jclouds/osgi/BundlesTest.java diff --git a/core/pom.xml b/core/pom.xml index 37f5844415..2dfb7387b6 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -114,6 +114,12 @@ 4.2.0 provided + + org.osgi + org.osgi.compendium + 4.2.0 + provided + diff --git a/core/src/main/java/org/jclouds/apis/Apis.java b/core/src/main/java/org/jclouds/apis/Apis.java index 484a6c5eaa..7631018613 100644 --- a/core/src/main/java/org/jclouds/apis/Apis.java +++ b/core/src/main/java/org/jclouds/apis/Apis.java @@ -29,6 +29,7 @@ import org.jclouds.View; import com.google.common.base.Function; import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.reflect.TypeToken; @@ -69,7 +70,9 @@ public class Apis { * @return all available apis */ public static Iterable all() { - return Iterables.concat(fromServiceLoader(), ApiRegistry.fromRegistry()); + return ImmutableSet.builder() + .addAll(fromServiceLoader()) + .addAll(ApiRegistry.fromRegistry()).build(); } /** @@ -136,4 +139,4 @@ public class Apis { }); } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/jclouds/osgi/Activator.java b/core/src/main/java/org/jclouds/osgi/Activator.java index e6ee8f1398..f8a5cc9b6a 100644 --- a/core/src/main/java/org/jclouds/osgi/Activator.java +++ b/core/src/main/java/org/jclouds/osgi/Activator.java @@ -22,54 +22,102 @@ import org.jclouds.apis.ApiRegistry; import org.jclouds.providers.ProviderRegistry; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; public class Activator implements BundleActivator { - MetadataBundleListener bundleListener = new MetadataBundleListener(); + private ServiceTracker providerListenerTracker = null; + private ServiceTracker apiListenerTracker = null; + private MetadataBundleListener bundleListener = new MetadataBundleListener(); - /** - * Called when this bundle is started so the Framework can perform the - * bundle-specific activities necessary to start this bundle. This method - * can be used to register services or to allocate any resources that this - * bundle needs. - *

- *

- * This method must complete and return to its caller in a timely manner. - * - * @param context The execution context of the bundle being started. - * @throws Exception If this method throws an exception, this - * bundle is marked as stopped and the Framework will remove this - * bundle's listeners, unregister all services registered by this - * bundle, and release all services used by this bundle. - */ - @Override - public void start(BundleContext context) throws Exception { - bundleListener.start(context); - context.addBundleListener(bundleListener); - } + /** + * Called when this bundle is started so the Framework can perform the + * bundle-specific activities necessary to start this bundle. This method + * can be used to register services or to allocate any resources that this + * bundle needs. + *

+ *

+ * This method must complete and return to its caller in a timely manner. + * + * @param context The execution context of the bundle being started. + * @throws Exception If this method throws an exception, this + * bundle is marked as stopped and the Framework will remove this + * bundle's listeners, unregister all services registered by this + * bundle, and release all services used by this bundle. + */ + @Override + public void start(BundleContext context) throws Exception { + bundleListener.start(context); + providerListenerTracker = new ServiceTracker(context, ProviderListener.class.getName(), null) { + @Override + public Object addingService(ServiceReference reference) { + Object obj = super.addingService(reference); + if (ProviderListener.class.isAssignableFrom(obj.getClass())) { + bundleListener.addProviderListener((ProviderListener) obj); + } + return obj; + } - /** - * Called when this bundle is stopped so the Framework can perform the - * bundle-specific activities necessary to stop the bundle. In general, this - * method should undo the work that the BundleActivator.start - * method started. There should be no active threads that were started by - * this bundle when this bundle returns. A stopped bundle must not call any - * Framework objects. - *

- *

- * This method must complete and return to its caller in a timely manner. - * - * @param context The execution context of the bundle being stopped. - * @throws Exception If this method throws an exception, the - * bundle is still marked as stopped, and the Framework will remove - * the bundle's listeners, unregister all services registered by the - * bundle, and release all services used by the bundle. - */ - @Override - public void stop(BundleContext context) throws Exception { - bundleListener.stop(context); - context.removeBundleListener(bundleListener); - ProviderRegistry.clear(); - ApiRegistry.clear(); - } + @Override + public void removedService(ServiceReference reference, Object service) { + if (ProviderListener.class.isAssignableFrom(service.getClass())) { + bundleListener.removeProviderListener((ProviderListener) service); + } + super.removedService(reference, service); + } + }; + + apiListenerTracker = new ServiceTracker(context, ApiListener.class.getName(), null) { + @Override + public Object addingService(ServiceReference reference) { + Object obj = super.addingService(reference); + if (ApiListener.class.isAssignableFrom(obj.getClass())) { + bundleListener.addApiListenerListener((ApiListener) obj); + } + return obj; + } + + @Override + public void removedService(ServiceReference reference, Object service) { + if (ApiListener.class.isAssignableFrom(service.getClass())) { + bundleListener.removeApiListenerListener((ApiListener) service); + } + super.removedService(reference, service); + } + }; + + providerListenerTracker.open(); + apiListenerTracker.open(); + } + + /** + * Called when this bundle is stopped so the Framework can perform the + * bundle-specific activities necessary to stop the bundle. In general, this + * method should undo the work that the BundleActivator.start + * method started. There should be no active threads that were started by + * this bundle when this bundle returns. A stopped bundle must not call any + * Framework objects. + *

+ *

+ * This method must complete and return to its caller in a timely manner. + * + * @param context The execution context of the bundle being stopped. + * @throws Exception If this method throws an exception, the + * bundle is still marked as stopped, and the Framework will remove + * the bundle's listeners, unregister all services registered by the + * bundle, and release all services used by the bundle. + */ + @Override + public void stop(BundleContext context) throws Exception { + bundleListener.stop(context); + ProviderRegistry.clear(); + ApiRegistry.clear(); + if (apiListenerTracker != null) { + apiListenerTracker.close(); + } + if (providerListenerTracker != null) { + providerListenerTracker.close(); + } + } } diff --git a/core/src/main/java/org/jclouds/osgi/ApiListener.java b/core/src/main/java/org/jclouds/osgi/ApiListener.java new file mode 100644 index 0000000000..4ff0a73e5a --- /dev/null +++ b/core/src/main/java/org/jclouds/osgi/ApiListener.java @@ -0,0 +1,46 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.jclouds.osgi; + +import org.jclouds.apis.ApiMetadata; + +/** + * A listener interface for {@link org.jclouds.apis.ApiMetadata}. + * In OSGi a api can be added or removed dynamically. + * OSGi services using this interface will receive a notification whenever this happens. + */ +public interface ApiListener { + + /** + * Method to be called when an api gets added. + * + * @param api The api that was added. + * @param The {@link org.jclouds.apis.ApiMetadata}. + */ + void added(A api); + + /** + * Method to be called when an api gets removed. + * + * @param api The api that was added. + * @param The {@link ApiMetadata}. + */ + void removed(A api); +} diff --git a/core/src/main/java/org/jclouds/osgi/Bundles.java b/core/src/main/java/org/jclouds/osgi/Bundles.java new file mode 100644 index 0000000000..0400df2eea --- /dev/null +++ b/core/src/main/java/org/jclouds/osgi/Bundles.java @@ -0,0 +1,143 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.jclouds.osgi; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import org.jclouds.util.Strings2; +import org.osgi.framework.Bundle; + +import com.google.common.base.Function; +import com.google.common.base.Predicates; +import com.google.common.base.Splitter; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableSet; + +/** + * Utility functions helpful in working with {@link Bundle bundles}. + * + * @author Adrian Cole + */ +public final class Bundles { + private Bundles() { + + } + + /** + * instantiates the supplied classnames using the bundle classloader, and + * casts to the supplied type. Any errors are silently ignored. + * + * @return instances that could be instantiated without error. + */ + public static ImmutableSet instantiateAvailableClasses(Bundle bundle, Iterable classNames, Class type) { + checkNotNull(bundle, "bundle"); + checkNotNull(classNames, "classNames"); + checkNotNull(type, "type"); + return FluentIterable.from(classNames) + .transform(loadClassIfAssignableFrom(bundle, type)) + .filter(Predicates.notNull()) + .transform(instantiateIfPossible(type)) + .filter(Predicates.notNull()) + .toImmutableSet(); + } + + /** + * A function that loads classes from the bundle, or returns null if the + * class isn't found or assignable by the input parameter + * + * @param bundle + * where to find classes + * @param clazz + * type classes must be assignable from + */ + private static Function> loadClassIfAssignableFrom(final Bundle bundle, final Class clazz) { + checkNotNull(bundle, "bundle"); + checkNotNull(clazz, "clazz"); + return new Function>() { + @Override + public Class apply(String in) { + checkNotNull(in, "classname"); + try { + Class thing = bundle.loadClass(in); + // Classes loaded by other class loaders are not assignable. + if (clazz.isAssignableFrom(thing)) + return thing.asSubclass(clazz); + } catch (ClassNotFoundException e) { + } + return null; + } + }; + } + + /** + * A function that instantiates classes or returns null, if it encounters any + * problems. + * + * @param clazz + * superclass to cast as + */ + private static Function, T> instantiateIfPossible(final Class clazz) { + return new Function, T>() { + @Override + public T apply(Class in) { + checkNotNull(in, "input class"); + try { + return clazz.cast(in.newInstance()); + } catch (InstantiationException e) { + } catch (IllegalAccessException e) { + } + return null; + } + }; + } + + /** + * Reads the resource from a {@link Bundle}. + * + * @param resourcePath + * The path to the resource. + * @param bundle + * The bundle to read from. + * @return strings delimited by newline in the stream or empty set, on any + * exception. + */ + public static ImmutableSet stringsForResorceInBundle(String resourcePath, Bundle bundle) { + checkNotNull(resourcePath, "resourcePath"); + checkNotNull(bundle, "bundle"); + + URL resource = bundle.getEntry(resourcePath); + if (resource == null) + return ImmutableSet.of(); + try { + return ImmutableSet.copyOf(splitOrEmptyAndClose(resource.openStream())); + } catch (IOException e) { + return ImmutableSet.of(); + } catch (RuntimeException ex) { + return ImmutableSet.of(); + } + } + + private static Iterable splitOrEmptyAndClose(InputStream in) throws IOException { + return Splitter.on('\n').omitEmptyStrings().split(Strings2.toStringAndClose(in)); + } +} diff --git a/core/src/main/java/org/jclouds/osgi/MetadataBundleListener.java b/core/src/main/java/org/jclouds/osgi/MetadataBundleListener.java index 2c79713d5c..bf5e222cfb 100644 --- a/core/src/main/java/org/jclouds/osgi/MetadataBundleListener.java +++ b/core/src/main/java/org/jclouds/osgi/MetadataBundleListener.java @@ -18,11 +18,11 @@ */ package org.jclouds.osgi; -import com.google.common.base.Charsets; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Lists; -import com.google.common.collect.Multimap; -import com.google.common.io.Closeables; +import static org.jclouds.osgi.Bundles.instantiateAvailableClasses; +import static org.jclouds.osgi.Bundles.stringsForResorceInBundle; + +import java.util.List; + import org.jclouds.apis.ApiMetadata; import org.jclouds.apis.ApiRegistry; import org.jclouds.providers.ProviderMetadata; @@ -32,91 +32,115 @@ import org.osgi.framework.BundleContext; import org.osgi.framework.BundleEvent; import org.osgi.framework.BundleListener; -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.URL; -import java.util.Collection; -import java.util.List; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; /** * A {@link BundleListener} that listens for {@link BundleEvent} and searches for {@link org.jclouds.providers.ProviderMetadata} and {@link org.jclouds.apis.ApiMetadata} in newly * installed Bundles. This is used as a workaround for OSGi environments where the ServiceLoader cannot cross bundle * boundaries. + * + * @author iocanel */ public class MetadataBundleListener implements BundleListener { - private Multimap providerMetadataMap = ArrayListMultimap.create(); - private Multimap apiMetadataMap = ArrayListMultimap.create(); + private final Multimap providerMetadataMap = ArrayListMultimap.create(); + private final Multimap apiMetadataMap = ArrayListMultimap.create(); + + private final List providerListeners = Lists.newArrayList(); + private final List apiListeners = Lists.newArrayList(); - public void start(BundleContext bundleContext) { + /** + * Starts the listener. + * Checks the bundles that are already active and registers {@link ProviderMetadata} and {@link ApiMetadata} found. + * Registers the itself as a {@link BundleListener}. + * @param bundleContext + */ + public synchronized void start(BundleContext bundleContext) { bundleContext.addBundleListener(this); for (Bundle bundle : bundleContext.getBundles()) { if (bundle.getState() == Bundle.ACTIVE) { - List providerMetadataList = getProviderMetadata(bundle); - List apiMetadataList = getApiMetadata(bundle); + addBundle(bundle); + } + } + bundleContext.addBundleListener(this); + } - for (ProviderMetadata providerMetadata : providerMetadataList) { - if (providerMetadata != null) { - ProviderRegistry.registerProvider(providerMetadata); - providerMetadataMap.put(bundle.getBundleId(), providerMetadata); - } + /** + * Stops the listener. + * Removes itself from the {@link BundleListener}s. + * Clears metadata maps and listeners lists. + * @param bundleContext + */ + public void stop(BundleContext bundleContext) { + bundleContext.removeBundleListener(this); + providerMetadataMap.clear(); + apiMetadataMap.clear(); + apiListeners.clear(); + providerListeners.clear(); + } + + @Override + public synchronized void bundleChanged(BundleEvent event) { + switch (event.getType()) { + case BundleEvent.STARTED: + addBundle(event.getBundle()); + break; + case BundleEvent.STOPPING: + case BundleEvent.STOPPED: + removeBundle(event.getBundle()); + break; + } + } + + /** + * Searches for {@link ProviderMetadata} and {@link ApiMetadata} inside the {@link Bundle}. + * If metadata are found they are registered in the {@link ProviderRegistry} and {@link ApiRegistry}. + * Also the {@link ProviderListener} and {@link ApiListener} are notified. + * @param bundle + */ + private synchronized void addBundle(Bundle bundle) { + for (ProviderMetadata providerMetadata : listProviderMetadata(bundle)) { + if (providerMetadata != null) { + ProviderRegistry.registerProvider(providerMetadata); + providerMetadataMap.put(bundle.getBundleId(), providerMetadata); + for (ProviderListener listener : providerListeners) { + listener.added(providerMetadata); } + } + } - for (ApiMetadata apiMetadata : apiMetadataList) { - if (apiMetadata != null) { - ApiRegistry.registerApi(apiMetadata); - apiMetadataMap.put(bundle.getBundleId(), apiMetadata); - } + for (ApiMetadata apiMetadata : listApiMetadata(bundle)) { + if (apiMetadata != null) { + ApiRegistry.registerApi(apiMetadata); + apiMetadataMap.put(bundle.getBundleId(), apiMetadata); + for (ApiListener listener : apiListeners) { + listener.added(apiMetadata); } } } } - public void stop(BundleContext bundleContext) { - providerMetadataMap.clear(); - apiMetadataMap.clear(); - } - - @Override - public void bundleChanged(BundleEvent event) { - Collection providerMetadataList = null; - Collection apiMetadataList = null; - switch (event.getType()) { - case BundleEvent.STARTED: - providerMetadataList = getProviderMetadata(event.getBundle()); - apiMetadataList = getApiMetadata(event.getBundle()); - for (ProviderMetadata providerMetadata : providerMetadataList) { - if (providerMetadata != null) { - ProviderRegistry.registerProvider(providerMetadata); - providerMetadataMap.put(event.getBundle().getBundleId(), providerMetadata); - } - } - - for (ApiMetadata apiMetadata : apiMetadataList) { - if (apiMetadata != null) { - ApiRegistry.registerApi(apiMetadata); - apiMetadataMap.put(event.getBundle().getBundleId(), apiMetadata); - } - } - break; - case BundleEvent.STOPPING: - case BundleEvent.STOPPED: - providerMetadataList = providerMetadataMap.get(event.getBundle().getBundleId()); - apiMetadataList = apiMetadataMap.get(event.getBundle().getBundleId()); - - if (providerMetadataList != null) { - for (ProviderMetadata providerMetadata : providerMetadataList) { - ProviderRegistry.unregisterProvider(providerMetadata); - } - } - if (apiMetadataList != null) { - for (ApiMetadata apiMetadata : apiMetadataList) { - ApiRegistry.unRegisterApi(apiMetadata); - } - } - break; + /** + * Searches for {@link ProviderMetadata} and {@link ApiMetadata} registered under the {@link Bundle} id. + * If metadata are found they are removed the {@link ProviderRegistry} and {@link ApiRegistry}. + * Also the {@link ProviderListener} and {@link ApiListener} are notified. + * @param bundle + */ + private synchronized void removeBundle(Bundle bundle) { + for (ProviderMetadata providerMetadata : providerMetadataMap.removeAll(bundle.getBundleId())) { + ProviderRegistry.unregisterProvider(providerMetadata); + for (ProviderListener listener : providerListeners) { + listener.removed(providerMetadata); + } + } + for (ApiMetadata apiMetadata : apiMetadataMap.removeAll(bundle.getBundleId())) { + ApiRegistry.unRegisterApi(apiMetadata); + for (ApiListener listener : apiListeners) { + listener.removed(apiMetadata); + } } } @@ -126,28 +150,9 @@ public class MetadataBundleListener implements BundleListener { * @param bundle * @return */ - public List getProviderMetadata(Bundle bundle) { - List metadataList = Lists.newArrayList(); - String classNames = getProviderMetadataClassNames(bundle); - if (classNames != null && !classNames.isEmpty()) { - for (String className : classNames.split("\n")) { - try { - Class providerMetadataClass = bundle.loadClass(className); - //Classes loaded by other class loaders are not assignable. - if (ProviderMetadata.class.isAssignableFrom(providerMetadataClass)) { - ProviderMetadata metadata = providerMetadataClass.newInstance(); - metadataList.add(metadata); - } - } catch (ClassNotFoundException e) { - // ignore - } catch (InstantiationException e) { - // ignore - } catch (IllegalAccessException e) { - // ignore - } - } - } - return metadataList; + public Iterable listProviderMetadata(Bundle bundle) { + Iterable classNames = stringsForResorceInBundle("/META-INF/services/org.jclouds.providers.ProviderMetadata", bundle); + return instantiateAvailableClasses(bundle, classNames, ProviderMetadata.class); } /** @@ -156,73 +161,51 @@ public class MetadataBundleListener implements BundleListener { * @param bundle * @return */ - public List getApiMetadata(Bundle bundle) { - List metadataList = Lists.newArrayList(); - String classNames = getApiMetadataClassNames(bundle); - if (classNames != null && !classNames.isEmpty()) { - for (String className : classNames.split("\n")) { - try { - Class apiMetadataClass = bundle.loadClass(className); - //Classes loaded by other class loaders are not assignable. - if (ApiMetadata.class.isAssignableFrom(apiMetadataClass)) { - ApiMetadata metadata = apiMetadataClass.newInstance(); - metadataList.add(metadata); - } - } catch (ClassNotFoundException e) { - // ignore - } catch (InstantiationException e) { - // ignore - } catch (IllegalAccessException e) { - // ignore - } - } - } - return metadataList; - } - - - public String getMetadataClassNames(Bundle bundle, String pathToMetadata) { - URL resource = bundle.getEntry(pathToMetadata); - InputStream is = null; - InputStreamReader reader = null; - BufferedReader bufferedReader = null; - StringBuilder sb = new StringBuilder(); - - try { - is = resource.openStream(); - reader = new InputStreamReader(is, Charsets.UTF_8); - bufferedReader = new BufferedReader(reader); - String line; - while ((line = bufferedReader.readLine()) != null) { - sb.append(line).append("\n"); - } - } catch (Throwable e) { - } finally { - Closeables.closeQuietly(reader); - Closeables.closeQuietly(bufferedReader); - Closeables.closeQuietly(is); - } - return sb.toString().trim(); + public Iterable listApiMetadata(Bundle bundle) { + Iterable classNames = stringsForResorceInBundle("/META-INF/services/org.jclouds.apis.ApiMetadata", bundle); + return instantiateAvailableClasses(bundle, classNames, ApiMetadata.class); } /** - * Retrieves the {@link ProviderMetadata} class name for the bundle if it exists. + * Adds a {@link ProviderListener} and notifies it of existing {@link ProviderMetadata}. * - * @param bundle - * @return + * @param listener The listener. */ - public String getProviderMetadataClassNames(Bundle bundle) { - return getMetadataClassNames(bundle, "/META-INF/services/org.jclouds.providers.ProviderMetadata"); + public synchronized void addProviderListener(ProviderListener listener) { + providerListeners.add(listener); + for (ProviderMetadata metadata : providerMetadataMap.values()) { + listener.added(metadata); + } } /** - * Retrieves the {@link ProviderMetadata} class name for the bundle if it exists. + * Removes the {@link ProviderListener} * - * @param bundle - * @return + * @param listener The listener */ - public String getApiMetadataClassNames(Bundle bundle) { - return getMetadataClassNames(bundle, "/META-INF/services/org.jclouds.apis.ApiMetadata"); + public synchronized void removeProviderListener(ProviderListener listener) { + providerListeners.remove(listener); } -} \ No newline at end of file + /** + * Adds a {@link ApiListener} and notifies it of existing {@link ApiMetadata}. + * + * @param listener + */ + public synchronized void addApiListenerListener(ApiListener listener) { + apiListeners.add(listener); + for (ApiMetadata metadata : apiMetadataMap.values()) { + listener.added(metadata); + } + } + + /** + * Removes the {@link ApiListener} + * + * @param listener + */ + public synchronized void removeApiListenerListener(ApiListener listener) { + apiListeners.remove(listener); + } + +} diff --git a/core/src/main/java/org/jclouds/osgi/ProviderListener.java b/core/src/main/java/org/jclouds/osgi/ProviderListener.java new file mode 100644 index 0000000000..39e38755df --- /dev/null +++ b/core/src/main/java/org/jclouds/osgi/ProviderListener.java @@ -0,0 +1,47 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.jclouds.osgi; + +import org.jclouds.providers.ProviderMetadata; + +/** + * A listener interface for {@link ProviderMetadata}. + * In OSGi a provider can be added or removed dynamically. + * OSGi services using this interface will receive a notification whenever this happens. + */ +public interface ProviderListener { + + /** + * Method to be called when a Provider gets added. + * + * @param provider The provider that was added. + * @param

The {@link ProviderMetadata}. + */ +

void added(P provider); + + /** + * Method to be called when a Provider gets removed. + * + * @param provider The provider that was added. + * @param

The {@link ProviderMetadata}. + */ +

void removed(P provider); + +} diff --git a/core/src/main/java/org/jclouds/providers/ProviderRegistry.java b/core/src/main/java/org/jclouds/providers/ProviderRegistry.java index b79741e0ec..d9bd784be3 100644 --- a/core/src/main/java/org/jclouds/providers/ProviderRegistry.java +++ b/core/src/main/java/org/jclouds/providers/ProviderRegistry.java @@ -18,15 +18,16 @@ */ package org.jclouds.providers; -import java.util.HashSet; import java.util.Set; +import com.google.common.collect.Sets; + /** * A registry for holding {@link org.jclouds.providers.ProviderMetadata}. */ public class ProviderRegistry { - private static final Set providers = new HashSet(); + private static final Set providers = Sets.newHashSet(); public static void registerProvider(ProviderMetadata provider) { providers.add(provider); diff --git a/core/src/main/java/org/jclouds/providers/Providers.java b/core/src/main/java/org/jclouds/providers/Providers.java index e57fb19daa..c4b79e54ea 100644 --- a/core/src/main/java/org/jclouds/providers/Providers.java +++ b/core/src/main/java/org/jclouds/providers/Providers.java @@ -24,7 +24,7 @@ import static com.google.common.collect.Iterables.find; import java.util.NoSuchElementException; import java.util.ServiceLoader; -import com.google.common.collect.Iterables; +import com.google.common.collect.ImmutableSet; import org.jclouds.Context; import org.jclouds.View; import org.jclouds.apis.ApiMetadata; @@ -83,7 +83,9 @@ public class Providers { * @return all available providers */ public static Iterable all() { - return Iterables.concat(fromServiceLoader(), ProviderRegistry.fromRegistry()); + return ImmutableSet.builder() + .addAll(fromServiceLoader()) + .addAll(ProviderRegistry.fromRegistry()).build(); } /** diff --git a/core/src/test/java/org/jclouds/osgi/BundlesTest.java b/core/src/test/java/org/jclouds/osgi/BundlesTest.java new file mode 100644 index 0000000000..fc4c319295 --- /dev/null +++ b/core/src/test/java/org/jclouds/osgi/BundlesTest.java @@ -0,0 +1,115 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.jclouds.osgi; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.testng.Assert.assertEquals; + +import org.jclouds.apis.JcloudsTestComputeApiMetadata; +import org.jclouds.providers.JcloudsTestBlobStoreProviderMetadata; +import org.jclouds.providers.JcloudsTestComputeProviderMetadata; +import org.jclouds.providers.JcloudsTestYetAnotherComputeProviderMetadata; +import org.jclouds.providers.ProviderMetadata; +import org.osgi.framework.Bundle; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableSet; + +/** + * + * @author Adrian Cole + * + */ +public class BundlesTest { + + @Test + public void testInstantiateAvailableClassesWhenAllAssignable() throws ClassNotFoundException { + Bundle bundle = createMock(Bundle.class); + expect(bundle.loadClass("org.jclouds.providers.JcloudsTestBlobStoreProviderMetadata")) + .andReturn(JcloudsTestBlobStoreProviderMetadata.class); + expect(bundle.loadClass("org.jclouds.providers.JcloudsTestComputeProviderMetadata")) + .andReturn(JcloudsTestComputeProviderMetadata.class); + expect(bundle.loadClass("org.jclouds.providers.JcloudsTestYetAnotherComputeProviderMetadata")) + .andReturn(JcloudsTestYetAnotherComputeProviderMetadata.class); + replay(bundle); + + Iterable providers = Bundles.instantiateAvailableClasses(bundle, ImmutableSet.of( + "org.jclouds.providers.JcloudsTestBlobStoreProviderMetadata", + "org.jclouds.providers.JcloudsTestComputeProviderMetadata", + "org.jclouds.providers.JcloudsTestYetAnotherComputeProviderMetadata"), ProviderMetadata.class); + assertEquals(providers, ImmutableSet.of(new JcloudsTestBlobStoreProviderMetadata(), + new JcloudsTestComputeProviderMetadata(), new JcloudsTestYetAnotherComputeProviderMetadata())); + + verify(bundle); + } + + @Test + public void testInstantiateAvailableClassesWhenNotAllAssignable() throws ClassNotFoundException { + Bundle bundle = createMock(Bundle.class); + expect(bundle.loadClass("org.jclouds.providers.JcloudsTestBlobStoreProviderMetadata")) + .andReturn(JcloudsTestBlobStoreProviderMetadata.class); + expect(bundle.loadClass("org.jclouds.apis.JcloudsTestComputeApiMetadata")) + .andReturn(JcloudsTestComputeApiMetadata.class); + expect(bundle.loadClass("org.jclouds.providers.JcloudsTestYetAnotherComputeProviderMetadata")) + .andReturn(JcloudsTestYetAnotherComputeProviderMetadata.class); + replay(bundle); + + Iterable providers = Bundles.instantiateAvailableClasses(bundle, ImmutableSet.of( + "org.jclouds.providers.JcloudsTestBlobStoreProviderMetadata", + "org.jclouds.apis.JcloudsTestComputeApiMetadata", + "org.jclouds.providers.JcloudsTestYetAnotherComputeProviderMetadata"), ProviderMetadata.class); + assertEquals(providers, ImmutableSet.of(new JcloudsTestBlobStoreProviderMetadata(), + new JcloudsTestYetAnotherComputeProviderMetadata())); + + verify(bundle); + } + + @Test + public void testStringsForResourcesInBundleWhenNoResources() throws Exception { + + Bundle bundle = createMock(Bundle.class); + expect(bundle.getEntry("/META-INF/services/org.jclouds.apis.ApiMetadata")).andReturn(null); + replay(bundle); + + assertEquals(Bundles.stringsForResorceInBundle("/META-INF/services/org.jclouds.apis.ApiMetadata", bundle), + ImmutableSet.of()); + + verify(bundle); + } + + @Test + public void testStringsForResourcesInBundleWhenResourcePresent() throws Exception { + + Bundle bundle = createMock(Bundle.class); + expect(bundle.getEntry("/META-INF/services/org.jclouds.providers.ProviderMetadata")) + .andReturn(getClass().getResource("/META-INF/services/org.jclouds.providers.ProviderMetadata")); + replay(bundle); + + assertEquals(Bundles.stringsForResorceInBundle( + "/META-INF/services/org.jclouds.providers.ProviderMetadata", bundle), ImmutableSet.of( + "org.jclouds.providers.JcloudsTestBlobStoreProviderMetadata", + "org.jclouds.providers.JcloudsTestComputeProviderMetadata", + "org.jclouds.providers.JcloudsTestYetAnotherComputeProviderMetadata")); + + verify(bundle); + } +} diff --git a/core/src/test/java/org/jclouds/osgi/MetadataBundleListenerTest.java b/core/src/test/java/org/jclouds/osgi/MetadataBundleListenerTest.java index 697877491e..7ea1b43621 100644 --- a/core/src/test/java/org/jclouds/osgi/MetadataBundleListenerTest.java +++ b/core/src/test/java/org/jclouds/osgi/MetadataBundleListenerTest.java @@ -18,6 +18,7 @@ */ package org.jclouds.osgi; +import com.google.common.collect.Lists; import org.jclouds.apis.ApiMetadata; import org.jclouds.apis.JcloudsTestBlobStoreApiMetadata; import org.jclouds.apis.JcloudsTestComputeApiMetadata; @@ -27,6 +28,7 @@ import org.jclouds.providers.JcloudsTestComputeProviderMetadata; import org.jclouds.providers.JcloudsTestYetAnotherComputeProviderMetadata; import org.jclouds.providers.ProviderMetadata; import org.osgi.framework.Bundle; +import org.osgi.framework.BundleEvent; import org.testng.annotations.Test; import java.net.MalformedURLException; @@ -34,16 +36,22 @@ import java.net.URL; import java.net.URLClassLoader; import java.util.List; +import static org.easymock.EasyMock.anyObject; import static org.easymock.EasyMock.createMock; import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.verify; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertTrue; import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; - +/** + * + * @author iocanel + * + */ public class MetadataBundleListenerTest { @Test @@ -62,9 +70,8 @@ public class MetadataBundleListenerTest { expect(bundle.loadClass("org.jclouds.providers.JcloudsTestBlobStoreProviderMetadata")).andReturn(JcloudsTestBlobStoreProviderMetadata.class).anyTimes(); expect(bundle.loadClass("org.jclouds.providers.JcloudsTestComputeProviderMetadata")).andReturn(JcloudsTestComputeProviderMetadata.class).anyTimes(); expect(bundle.loadClass("org.jclouds.providers.JcloudsTestYetAnotherComputeProviderMetadata")).andReturn(JcloudsTestYetAnotherComputeProviderMetadata.class).anyTimes(); - replay(bundle); - List providerMetadataList = listener.getProviderMetadata(bundle); + List providerMetadataList = Lists.newArrayList(listener.listProviderMetadata(bundle)); assertNotNull(providerMetadataList); assertEquals(3, providerMetadataList.size()); assertTrue(providerMetadataList.contains(new JcloudsTestBlobStoreProviderMetadata())); @@ -73,6 +80,33 @@ public class MetadataBundleListenerTest { verify(bundle); } + @Test + public void testProviderListener() throws Exception { + MetadataBundleListener listener = new MetadataBundleListener(); + ProviderListener providerListener = createMock(ProviderListener.class); + listener.addProviderListener(providerListener); + + Bundle bundle = createMock(Bundle.class); + expect(bundle.getBundleId()).andReturn(10L).anyTimes(); + expect(bundle.getEntry("/META-INF/services/org.jclouds.providers.ProviderMetadata")).andReturn(getClass().getResource("/META-INF/services/org.jclouds.providers.ProviderMetadata")).anyTimes(); + expect(bundle.getEntry("/META-INF/services/org.jclouds.apis.ApiMetadata")).andReturn(null).anyTimes(); + expect(bundle.loadClass("org.jclouds.providers.JcloudsTestBlobStoreProviderMetadata")).andReturn(JcloudsTestBlobStoreProviderMetadata.class).anyTimes(); + expect(bundle.loadClass("org.jclouds.providers.JcloudsTestComputeProviderMetadata")).andReturn(JcloudsTestComputeProviderMetadata.class).anyTimes(); + expect(bundle.loadClass("org.jclouds.providers.JcloudsTestYetAnotherComputeProviderMetadata")).andReturn(JcloudsTestYetAnotherComputeProviderMetadata.class).anyTimes(); + + providerListener.added(anyObject(JcloudsTestBlobStoreProviderMetadata.class)); + expectLastCall().times(1); + providerListener.added(anyObject(JcloudsTestComputeProviderMetadata.class)); + expectLastCall().times(1); + providerListener.added(anyObject(JcloudsTestYetAnotherComputeProviderMetadata.class)); + expectLastCall().times(1); + replay(bundle, providerListener); + + BundleEvent event = new BundleEvent(BundleEvent.STARTED, bundle); + listener.bundleChanged(event); + verify(bundle, providerListener); + } + @Test public void testGetProviderMetadataFromMultipleClassLoaders() throws Exception { ClassLoader isolatedClassLoader = createIsolatedClassLoader(); @@ -84,7 +118,7 @@ public class MetadataBundleListenerTest { expect(bundle.loadClass("org.jclouds.providers.JcloudsTestYetAnotherComputeProviderMetadata")).andReturn(JcloudsTestYetAnotherComputeProviderMetadata.class).anyTimes(); replay(bundle); - List providerMetadataList = listener.getProviderMetadata(bundle); + List providerMetadataList = Lists.newArrayList(listener.listProviderMetadata(bundle)); assertNotNull(providerMetadataList); assertEquals(2, providerMetadataList.size()); assertFalse(providerMetadataList.contains(new JcloudsTestBlobStoreProviderMetadata())); @@ -103,7 +137,7 @@ public class MetadataBundleListenerTest { expect(bundle.loadClass("org.jclouds.apis.JcloudsTestYetAnotherComputeApiMetadata")).andReturn(JcloudsTestYetAnotherComputeApiMetadata.class).anyTimes(); replay(bundle); - List apiMetadataList = listener.getApiMetadata(bundle); + List apiMetadataList = Lists.newArrayList(listener.listApiMetadata(bundle)); assertNotNull(apiMetadataList); assertEquals(3, apiMetadataList.size()); assertTrue(apiMetadataList.contains(new JcloudsTestBlobStoreApiMetadata())); @@ -112,6 +146,34 @@ public class MetadataBundleListenerTest { verify(bundle); } + @Test + public void testApiListener() throws Exception { + MetadataBundleListener listener = new MetadataBundleListener(); + ApiListener apiListener = createMock(ApiListener.class); + listener.addApiListenerListener(apiListener); + + Bundle bundle = createMock(Bundle.class); + expect(bundle.getBundleId()).andReturn(10L).anyTimes(); + expect(bundle.getEntry("/META-INF/services/org.jclouds.providers.ProviderMetadata")).andReturn(null).anyTimes(); + expect(bundle.getEntry("/META-INF/services/org.jclouds.apis.ApiMetadata")).andReturn(getClass().getResource("/META-INF/services/org.jclouds.apis.ApiMetadata")).anyTimes(); + expect(bundle.loadClass("org.jclouds.apis.JcloudsTestBlobStoreApiMetadata")).andReturn(JcloudsTestBlobStoreApiMetadata.class).anyTimes(); + expect(bundle.loadClass("org.jclouds.apis.JcloudsTestComputeApiMetadata")).andReturn(JcloudsTestComputeApiMetadata.class).anyTimes(); + expect(bundle.loadClass("org.jclouds.apis.JcloudsTestYetAnotherComputeApiMetadata")).andReturn(JcloudsTestYetAnotherComputeApiMetadata.class).anyTimes(); + + + apiListener.added(anyObject(JcloudsTestBlobStoreApiMetadata.class)); + expectLastCall().times(1); + apiListener.added(anyObject(JcloudsTestBlobStoreApiMetadata.class)); + expectLastCall().times(1); + apiListener.added(anyObject(JcloudsTestComputeApiMetadata.class)); + expectLastCall().times(1); + replay(bundle, apiListener); + + BundleEvent event = new BundleEvent(BundleEvent.STARTED, bundle); + listener.bundleChanged(event); + verify(bundle, apiListener); + } + @Test public void testGetApiMetadataFromMultipleClassLoaders() throws Exception { ClassLoader isolatedClassLoader = createIsolatedClassLoader(); @@ -123,7 +185,8 @@ public class MetadataBundleListenerTest { expect(bundle.loadClass("org.jclouds.apis.JcloudsTestYetAnotherComputeApiMetadata")).andReturn(JcloudsTestYetAnotherComputeApiMetadata.class).anyTimes(); replay(bundle); - List apiMetadataList = listener.getApiMetadata(bundle); + + List apiMetadataList = Lists.newArrayList(listener.listApiMetadata(bundle)); assertNotNull(apiMetadataList); assertEquals(2, apiMetadataList.size()); assertFalse(apiMetadataList.contains(new JcloudsTestBlobStoreApiMetadata())); diff --git a/project/pom.xml b/project/pom.xml index 17bf64261f..bcec83f1b7 100644 --- a/project/pom.xml +++ b/project/pom.xml @@ -370,6 +370,7 @@ test.jks CreateInternetService-options-test.xml .gitattributes + OSGI-OPT/bnd.bnd true