| /******************************************************************************* |
| * Copyright (c) 2020 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Alex Blewitt - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.core.runtime; |
| |
| import java.security.AccessController; |
| import java.security.PrivilegedAction; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.function.Consumer; |
| import org.osgi.framework.*; |
| import org.osgi.util.tracker.ServiceTracker; |
| |
| /** |
| * {@code ServiceCaller} provides functional methods for invoking OSGi services |
| * in two different ways |
| * <ul> |
| * <li> Single invocations which happen only once or very rarely. |
| * In this case, maintaining a cache of the service is not worth the overhead.</li> |
| * <li> Multiple invocations that happen often and rapidly. In this case, maintaining |
| * a cache of the service is worth the overhead.</li> |
| * </ul> |
| * <p> |
| * For single invocations of a service the static method |
| * {@link ServiceCaller#callOnce(Class, Class, Consumer)} can be used. |
| * This method will wrap a call to the consumer of the service with |
| * the necessary OSGi service registry calls to ensure the service |
| * exists and will do the proper get and release service operations |
| * surround the calls to the service. By wrapping a call around the |
| * service we can ensure that it is correctly released after use. |
| * </p> |
| * <p> |
| * Single invocation example: |
| * </p> |
| * <pre> |
| * ServiceCaller.callOnce(MyClass.class, ILog.class, (logger) -> logger.info("All systems go!")); |
| * </pre> |
| * <p> |
| * Note that it is generally more efficient to use a long-running service |
| * utility, such as {@link ServiceTracker} or declarative services, but there |
| * are cases where a single one-shot lookup is preferable, especially if the |
| * service is not required after use. Examples might include logging unlikely |
| * conditions or processing debug options that are only read once. |
| * </p> |
| * <p> |
| * This allows boilerplate code to be reduced at call sites, which would |
| * otherwise have to do something like: |
| * </p> |
| * <pre> |
| * Bundle bundle = FrameworkUtil.getBundle(BadExample.class); |
| * BundleContext context = bundle == null ? null : bundle.getBundleContext(); |
| * ServiceReference<Service> reference = context == null ? null : context.getServiceReference(serviceType); |
| * try { |
| * Service service = reference == null ? null : context.getService(reference); |
| * if (service != null) |
| * consumer.accept(service); |
| * } finally { |
| * context.ungetService(reference); |
| * } |
| * </pre> |
| * <p> |
| * For cases where a service is used much more often a {@code ServiceCaller} instance |
| * can be used to cache and track the available service. This may be useful for cases |
| * that cannot use declarative services and that want to avoid using something like |
| * a {@link ServiceTracker} that does not easily allow for lazy instantiation of the service |
| * instance. For example, if logging is used more often then something like the following |
| * could be used: |
| * </p> |
| * <pre> |
| * static final ServiceCaller<ILog> log = new ServiceCaller(MyClass.class, ILog.class); |
| * static void info(String msg) { |
| * log.call(logger -> logger.info(msg)); |
| * } |
| * </pre> |
| * <p> |
| * Note that this class is intended for simple service usage patterns only. More advanced cases |
| * should use other mechanisms such as the {@link ServiceTracker} or declarative services. |
| * </p> |
| * @param <Service> the service type for this caller |
| * @since 3.13 |
| */ |
| public class ServiceCaller<Service> { |
| /** |
| * Calls an OSGi service by dynamically looking it up and passing it to the given consumer. |
| * <p> |
| * If not running under OSGi, the caller bundle is not active or the service is not available, return false. |
| * If the service is found, call the service and return true. |
| * </p> |
| * <p> |
| * Any runtime exception thrown by the consumer is rethrown by this method. |
| * If the consumer throws a checked exception, it can be propagated using a <em>sneakyThrow</em> |
| * inside a try/catch block: |
| * </p> |
| * <pre> |
| * callOnce(MyClass.class, Callable.class, (callable) -> { |
| * try { |
| * callable.call(); |
| * } catch (Exception e) { |
| * sneakyThrow(e); |
| * } |
| * }); |
| * ... |
| * {@literal @}SuppressWarnings("unchecked") |
| * static <E extends Throwable> void sneakyThrow(Throwable e) throws E { |
| * throw (E) e; |
| * } |
| * </pre> |
| * @param caller a class from the bundle that will use service |
| * @param serviceType the OSGi service type to look up |
| * @param consumer the consumer of the OSGi service |
| * @param <Service> the OSGi service type to look up |
| * @return true if the OSGi service was located and called successfully, false otherwise |
| * @throws NullPointerException if any of the parameters are null |
| */ |
| public static <Service> boolean callOnce(Class<?> caller, Class<Service> serviceType, Consumer<Service> consumer) { |
| return new ServiceCaller<>(caller, serviceType).getCallUnget(consumer); |
| } |
| |
| static int getRank(ServiceReference<?> ref) { |
| Object rank = ref.getProperty(Constants.SERVICE_RANKING); |
| if (rank instanceof Integer) { |
| return ((Integer) rank).intValue(); |
| } |
| return 0; |
| } |
| |
| class ReferenceAndService implements SynchronousBundleListener, ServiceListener { |
| final BundleContext context; |
| final ServiceReference<Service> ref; |
| final Service instance; |
| final int rank; |
| |
| public ReferenceAndService(final BundleContext context, ServiceReference<Service> ref, Service instance) { |
| this.context = context; |
| this.ref = ref; |
| this.instance = instance; |
| this.rank = getRank(ref); |
| } |
| |
| void unget() { |
| untrack(); |
| try { |
| context.ungetService(ref); |
| } catch (IllegalStateException e) { |
| // ignore; just trying to cleanup but context is not valid now |
| } |
| } |
| |
| @Override |
| public void bundleChanged(BundleEvent e) { |
| if (bundle.equals(e.getBundle()) && e.getType() == BundleEvent.STOPPING) { |
| unget(); |
| } |
| } |
| |
| @Override |
| public void serviceChanged(ServiceEvent e) { |
| if (e.getServiceReference().equals(ref)) { |
| if (e.getType() == ServiceEvent.UNREGISTERING) { |
| unget(); |
| } else if (filter != null && e.getType() == ServiceEvent.MODIFIED_ENDMATCH) { |
| unget(); |
| } else if (e.getType() == ServiceEvent.MODIFIED) { |
| if (getRank(ref) != rank) { |
| // rank changed; untrack to force a new ReferenceAndService with new rank |
| unget(); |
| } |
| } |
| } else if (e.getType() == ServiceEvent.MODIFIED) { |
| if (getRank(e.getServiceReference()) > rank) { |
| // Another service with higher rank is available |
| unget(); |
| } |
| } |
| } |
| |
| // must hold monitor on ServiceCaller.this when calling track |
| Optional<ReferenceAndService> track() { |
| try { |
| ServiceCaller.this.service = this; |
| context.addServiceListener(this, "(&" //$NON-NLS-1$ |
| + "(objectClass=" + serviceType.getName() + ")" // //$NON-NLS-1$ //$NON-NLS-2$ |
| + (filter == null ? "" : filter) // //$NON-NLS-1$ |
| + ")"); //$NON-NLS-1$ |
| context.addBundleListener(this); |
| if ((ref.getBundle() == null || context.getBundle() == null) && ServiceCaller.this.service == this) { |
| // service should have been untracked but we may have missed the event |
| // before we could added the listeners |
| unget(); |
| } |
| if (getRank(ref) != rank) { |
| // ranking has changed; unget to force reget in case the ranking is not the highest |
| unget(); |
| } |
| } catch (InvalidSyntaxException e) { |
| // really should never happen with our own filter above. |
| ServiceCaller.this.service = null; |
| throw new IllegalStateException(e); |
| } catch (IllegalStateException e) { |
| // bundle was stopped before we could get listeners added/removed |
| ServiceCaller.this.service = null; |
| } |
| // Note that we always return this ReferenceAndService |
| // even for cases where the instance was unget |
| // It is way complicated to try again and |
| // even if we did the returned value can become |
| // stale right after return. |
| return Optional.of(this); |
| } |
| |
| void untrack() { |
| synchronized (ServiceCaller.this) { |
| if (ServiceCaller.this.service == this) { |
| ServiceCaller.this.service = null; |
| } |
| try { |
| context.removeServiceListener(this); |
| context.removeBundleListener(this); |
| } catch (IllegalStateException e) { |
| // context is invalid; |
| // ignore - the listeners already got cleaned up |
| } |
| } |
| } |
| } |
| |
| final Bundle bundle; |
| final Class<Service> serviceType; |
| final String filter; |
| volatile ReferenceAndService service = null; |
| |
| /** |
| * Creates a {@code ServiceCaller} instance for invoking an OSGi |
| * service many times with a consumer function. |
| * @param caller a class from the bundle that will consume the service |
| * @param serviceType the OSGi service type to look up |
| */ |
| public ServiceCaller(Class<?> caller, Class<Service> serviceType) { |
| this(caller, serviceType, null); |
| } |
| |
| /** |
| * Creates a {@code ServiceCaller} instance for invoking an OSGi |
| * service many times with a consumer function. |
| * @param caller a class from the bundle that will consume the service |
| * @param serviceType the OSGi service type to look up |
| * @param filter the service filter used to look up the service. May be {@code null}. |
| */ |
| public ServiceCaller(Class<?> caller, Class<Service> serviceType, String filter) { |
| this.serviceType = Objects.requireNonNull(serviceType); |
| this.bundle = Objects.requireNonNull(FrameworkUtil.getBundle(Objects.requireNonNull(caller))); |
| this.filter = filter; |
| if (filter != null) { |
| try { |
| FrameworkUtil.createFilter(filter); |
| } catch (InvalidSyntaxException e) { |
| throw new IllegalArgumentException(e); |
| } |
| } |
| } |
| |
| private boolean getCallUnget(Consumer<Service> consumer) { |
| return getCurrent().map((r) -> { |
| try { |
| consumer.accept(r.instance); |
| return Boolean.TRUE; |
| } finally { |
| r.unget(); |
| } |
| }).orElse(Boolean.FALSE); |
| } |
| |
| private BundleContext getContext() { |
| if (System.getSecurityManager() != null) { |
| return AccessController.doPrivileged((PrivilegedAction<BundleContext>) () -> bundle.getBundleContext()); |
| } |
| return bundle.getBundleContext(); |
| } |
| |
| /** |
| * Calls an OSGi service by dynamically looking it up and passing it to the given consumer. |
| * If not running under OSGi, the caller bundle is not active or the service is not available, return false. |
| * Any runtime exception thrown by the consumer is rethrown by this method. |
| * (For handling checked exceptions, see {@link #callOnce(Class, Class, Consumer)} for a solution.) |
| * Subsequent calls to this method will attempt to reuse the previously acquired service instance until one |
| * of the following occurs: |
| * <ul> |
| * <li>The {@link #unget()} method is called.</li> |
| * <li>The service is unregistered.</li> |
| * <li>The service properties change such that this {@code ServiceCaller} filter no longer matches. |
| * <li>The caller bundle is stopped.</li> |
| * <li>The service rankings have changed.</li> |
| * </ul> |
| * |
| * After one of these conditions occur subsequent calls to this method will try to acquire the |
| * another service instance. |
| * @param consumer the consumer of the OSGi service |
| * @return true if the OSGi service was located and called successfully, false otherwise |
| */ |
| public boolean call(Consumer<Service> consumer) { |
| return trackCurrent().map((r) -> { |
| consumer.accept(r.instance); |
| return Boolean.TRUE; |
| }).orElse(Boolean.FALSE); |
| } |
| |
| /** |
| * Return the currently available service. |
| * @return the currently available service or empty if the service cannot be found. |
| */ |
| public Optional<Service> current() { |
| return trackCurrent().map((r) -> r.instance); |
| } |
| |
| private Optional<ReferenceAndService> trackCurrent() { |
| ReferenceAndService current = service; |
| if (current != null) { |
| return Optional.of(current); |
| } |
| return getCurrent().flatMap((r) -> { |
| synchronized (ServiceCaller.this) { |
| if (service != null) { |
| // another thread beat us |
| // unget this instance and return existing |
| r.unget(); |
| return Optional.of(service); |
| } |
| return r.track(); |
| } |
| }); |
| |
| } |
| |
| private Optional<ReferenceAndService> getCurrent() { |
| BundleContext context = getContext(); |
| return getServiceReference(context).map((r) -> { |
| Service current = context.getService(r); |
| return current == null ? null : new ReferenceAndService(context, r, current); |
| }); |
| } |
| |
| private Optional<ServiceReference<Service>> getServiceReference(BundleContext context) { |
| if (context == null) { |
| return Optional.empty(); |
| } |
| if (filter == null) { |
| return Optional.ofNullable(context.getServiceReference(serviceType)); |
| } |
| try { |
| return context.getServiceReferences(serviceType, filter).stream().findFirst(); |
| } catch (InvalidSyntaxException e) { |
| // should not happen; filter was checked at construction |
| return Optional.empty(); |
| } |
| } |
| |
| /** |
| * Releases the cached service object, if it exists. |
| * Another invocation of {@link #call(Consumer)} will |
| * lazily get the service instance again and cache the new |
| * instance if found. |
| */ |
| public void unget() { |
| ReferenceAndService current = service; |
| if (current != null) { |
| current.unget(); |
| } |
| } |
| } |