All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.core.runtime.ServiceCaller Maven / Gradle / Ivy

/*******************************************************************************
 * 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
 * 
    *
  • Single invocations which happen only once or very rarely. * In this case, maintaining a cache of the service is not worth the overhead.
  • *
  • Multiple invocations that happen often and rapidly. In this case, maintaining * a cache of the service is worth the overhead.
  • *
*

* 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. *

*

* Single invocation example: *

*
 * ServiceCaller.callOnce(MyClass.class, ILog.class, (logger) -> logger.info("All systems go!"));
 * 
*

* 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. *

*

* This allows boilerplate code to be reduced at call sites, which would * otherwise have to do something like: *

*
 * 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);
 * }
 * 
*

* 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: *

*
 * static final ServiceCaller<ILog> log = new ServiceCaller(MyClass.class, ILog.class);
 * static void info(String msg) {
 *   log.call(logger -> logger.info(msg));
 * }
 * 
*

* 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. *

* @param the service type for this caller * @since 3.13 */ public class ServiceCaller { /** * 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. * If the service is found, call the service and return true. *

*

* 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 sneakyThrow * inside a try/catch block: *

*
	 * 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;
	 * }
	 * 
* @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 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 boolean callOnce(Class caller, Class serviceType, Consumer consumer) { return new ServiceCaller<>(caller, serviceType).getCallUnget(consumer); } /** * As {@link #callOnce(Class, Class, Consumer)} with an additional OSGi filter. * @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 filter an OSGi filter to restrict the services found * @param 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 boolean callOnce(Class caller, Class serviceType, String filter, Consumer consumer) { return new ServiceCaller<>(caller, serviceType, filter).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 ref; final Service instance; final int rank; public ReferenceAndService(final BundleContext context, ServiceReference 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 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 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 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 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 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) () -> 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: *
    *
  • The {@link #unget()} method is called.
  • *
  • The service is unregistered.
  • *
  • The service properties change such that this {@code ServiceCaller} filter no longer matches. *
  • The caller bundle is stopped.
  • *
  • The service rankings have changed.
  • *
* * 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 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 current() { return trackCurrent().map((r) -> r.instance); } private Optional 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 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> 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(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy