org.eclipse.core.runtime.ServiceCaller Maven / Gradle / Ivy
Show all versions of aspectjtools Show documentation
/*******************************************************************************
* Copyright (c) 2020, 2023 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
* Alexander Fedorov (ArSysOp) - documentation improvements
*******************************************************************************/
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 {@code null}
* @throws IllegalStateException if the bundle associated with the caller class
* cannot be determined
*/
public static boolean callOnce(Class caller, Class serviceType, Consumer consumer) {
return callOnce(caller, serviceType, null, 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 {@code null}
* @throws IllegalStateException if the bundle associated with the caller class
* cannot be determined
*/
public static boolean callOnce(Class caller, Class serviceType, String filter, Consumer consumer) {
return new ServiceCaller<>(caller, serviceType, filter).getCurrent().map(r -> {
try {
consumer.accept(r.instance);
return Boolean.TRUE;
} finally {
r.unget();
}
}).orElse(Boolean.FALSE);
}
private static int getRank(ServiceReference ref) {
Object rank = ref.getProperty(Constants.SERVICE_RANKING);
if (rank instanceof Integer) {
return ((Integer) rank).intValue();
}
return 0;
}
private class ReferenceAndService implements SynchronousBundleListener, ServiceListener {
final BundleContext context;
final ServiceReference ref;
final S instance;
final int rank;
public ReferenceAndService(final BundleContext context, ServiceReference ref, S 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 (requiresUnget(e)) {
unget();
}
}
private boolean requiresUnget(ServiceEvent e) {
if (e.getServiceReference().equals(ref)) {
return (e.getType() == ServiceEvent.UNREGISTERING)
|| (filter != null && e.getType() == ServiceEvent.MODIFIED_ENDMATCH)
|| (e.getType() == ServiceEvent.MODIFIED && getRank(ref) != rank);
// if rank changed: untrack to force a new ReferenceAndService with new rank
}
return e.getType() == ServiceEvent.MODIFIED && getRank(e.getServiceReference()) > rank;
}
// 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
}
}
}
}
private final Bundle bundle;
private final Class serviceType;
private final String filter;
private 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
* @throws NullPointerException if any of the parameters are {@code null}
* @throws IllegalStateException if the bundle associated with the caller class
* cannot be determined
*/
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}.
* @throws NullPointerException if any of the parameters are {@code null}
* @throws IllegalStateException if the bundle associated with the caller class
* cannot be determined
*/
public ServiceCaller(Class caller, Class serviceType, String filter) {
this.serviceType = Objects.requireNonNull(serviceType);
this.bundle = Optional.of(Objects.requireNonNull(caller)).map(FrameworkUtil::getBundle)
.orElseThrow(IllegalStateException::new);
this.filter = filter;
if (filter != null) {
try {
FrameworkUtil.createFilter(filter);
} catch (InvalidSyntaxException e) {
throw new IllegalArgumentException(e);
}
}
}
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 -> {
S 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();
}
}
}