blob: 76971be88886a6fa369ca8bb3eb37cc66866cd70 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2015 Wind River Systems 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:
* Wind River Systems - initial API and implementation
* Jonah Graham (Kichwa Coders) - Bug 317173 - cleanup warnings
*******************************************************************************/
package org.eclipse.cdt.dsf.service;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.RejectedExecutionException;
import org.eclipse.cdt.dsf.concurrent.ConfinedToDsfExecutor;
import org.eclipse.cdt.dsf.concurrent.DsfRunnable;
import org.eclipse.cdt.dsf.concurrent.ThreadSafe;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
/**
* Convenience class to help track DSF services that a given
* client needs to use. This class is similar to the standard OSGI
* org.osgi.util.tracker.ServiceTracker class, with a few differences:
* <br>1. This class is assumed to be accessed by a single thread hence it
* has no synchronization built in, while OSGI ServiceTracker synchronized
* access to its data.
* <br>2. This class is primarily designed to track multiple services of
* different type (class), while OSGI ServiceTracker is designed to work with
* single class type, with optional filtering options.
* <br>3. This class uses knowledge of DSF sessions to help narrow down
* service references.
* <br>4. OSGI Service tracker explicitly listens to OSGI service
* startup/shutdown events and it will clear a reference to a service as
* soon as it's shut down.
* Since version 2.0, this class listens to service unregister events
* as an indication of service shutdown. In the case of an unregister event,
* this class will clear the reference to that service.
* <p>
* That said, it might be more convenient for certain types of clients to use
* OSGI Service tracker for the additional features it provides.
*
* @see org.osgi.util.tracker.ServiceTracker
*
* @since 1.0
*/
@ConfinedToDsfExecutor("DsfSession.getSession(sessionId).getExecutor()")
public class DsfServicesTracker {
private static String getServiceFilter(String sessionId) {
return ("(" + IDsfService.PROP_SESSION_ID + "=" + sessionId + ")").intern(); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
}
final private static class ServiceKey {
private final String fClassName;
private final String fFilter;
private final int fHashCode;
private final String fHashString;
public ServiceKey(Class<?> clazz, String filter) {
fClassName = clazz != null ? clazz.getName() : null;
fFilter = filter;
fHashString = 'C' + (fClassName == null ? "" : fClassName) + //$NON-NLS-1$
'F' + (fFilter == null ? "" : fFilter); //$NON-NLS-1$
fHashCode = fHashString.hashCode();
}
@Override
public boolean equals(Object other) {
// hashcodes are not guaranteed to be unique, but objects that are equal must have the same hashcode
// thus we can optimize by first comparing hashcodes
return other instanceof ServiceKey && ((((ServiceKey) other).fHashCode == this.fHashCode)
&& (((ServiceKey) other).fHashString.equals(this.fHashString)));
}
@Override
public int hashCode() {
return fHashCode;
}
}
private final String fSessionId;
private volatile boolean fDisposed = false;
private final BundleContext fBundleContext;
private final Map<ServiceKey, ServiceReference<?>> fServiceReferences = new HashMap<>();
private final Map<ServiceReference<?>, Object> fServices = new HashMap<>();
private final String fServiceFilter;
private final ServiceListener fListener = new ServiceListener() {
@Override
public void serviceChanged(final ServiceEvent event) {
// Only listen to unregister events.
if (event.getType() != ServiceEvent.UNREGISTERING) {
return;
}
// If session is not active anymore, just exit. The tracker should
// soon be disposed.
DsfSession session = DsfSession.getSession(fSessionId);
if (session == null) {
return;
}
if (session.getExecutor().isInExecutorThread()) {
handleUnregisterEvent(event);
} else {
try {
session.getExecutor().execute(new DsfRunnable() {
@Override
public void run() {
handleUnregisterEvent(event);
}
});
} catch (RejectedExecutionException e) {
// Same situation as when the session is not active
}
}
}
};
private void handleUnregisterEvent(ServiceEvent event) {
for (Iterator<Map.Entry<ServiceKey, ServiceReference<?>>> itr = fServiceReferences.entrySet().iterator(); itr
.hasNext();) {
Map.Entry<ServiceKey, ServiceReference<?>> entry = itr.next();
if (entry.getValue().equals(event.getServiceReference())) {
itr.remove();
}
}
if (fServices.remove(event.getServiceReference()) != null) {
fBundleContext.ungetService(event.getServiceReference());
}
}
/**
* Only constructor.
* @param bundleContext Context of the plugin that the client lives in.
* @param sessionId The DSF session that this tracker will be used for.
*/
@ThreadSafe
public DsfServicesTracker(BundleContext bundleContext, String sessionId) {
fSessionId = sessionId;
fBundleContext = bundleContext;
fServiceFilter = getServiceFilter(sessionId);
try {
fBundleContext.addServiceListener(fListener, fServiceFilter);
} catch (InvalidSyntaxException e) {
assert false : "Invalid session ID syntax"; //$NON-NLS-1$
}
}
/**
* Retrieves a service reference for given service class and optional filter.
* Filter should be used if there are multiple instances of the desired service
* running within the same session.
* @param serviceClass class of the desired service
* @param custom filter to use when searching for the service, this filter will
* be used instead of the standard filter so it should also specify the desired
* session-ID
* @return OSGI service reference object to the desired service, null if not found
*/
public <V> ServiceReference<V> getServiceReference(Class<V> serviceClass, String filter) {
if (fDisposed) {
return null;
}
// If the session is not active, all of its services are gone.
DsfSession session = DsfSession.getSession(fSessionId);
if (session == null) {
return null;
}
assert session.getExecutor().isInExecutorThread();
ServiceKey key = new ServiceKey(serviceClass, filter != null ? filter : fServiceFilter);
if (fServiceReferences.containsKey(key)) {
@SuppressWarnings("unchecked")
ServiceReference<V> ref = (ServiceReference<V>) fServiceReferences.get(key);
return ref;
}
try {
Collection<ServiceReference<V>> references = fBundleContext.getServiceReferences(serviceClass, key.fFilter);
assert references == null || references.size() <= 1;
if (references == null || references.isEmpty()) {
return null;
} else {
ServiceReference<V> ref = references.iterator().next();
fServiceReferences.put(key, ref);
return ref;
}
} catch (InvalidSyntaxException e) {
assert false : "Invalid session ID syntax"; //$NON-NLS-1$
} catch (IllegalStateException e) {
// Can occur when plugin is shutting down.
}
return null;
}
/**
* Convenience class to retrieve a service based on class name only.
* @param serviceClass class of the desired service
* @return instance of the desired service, null if not found
*/
public <V> V getService(Class<V> serviceClass) {
return getService(serviceClass, null);
}
/**
* Retrieves the service given service class and optional filter.
* Filter should be used if there are multiple instances of the desired service
* running within the same session.
* @param serviceClass class of the desired service
* @param custom filter to use when searching for the service, this filter will
* be used instead of the standard filter so it should also specify the desired
* session-ID
* @return instance of the desired service, null if not found
*/
public <V> V getService(Class<V> serviceClass, String filter) {
ServiceReference<V> serviceRef = getServiceReference(serviceClass, filter);
if (serviceRef == null) {
return null;
}
@SuppressWarnings("unchecked")
V service = (V) fServices.get(serviceRef);
if (service == null) {
// Check to see if 'null' means we never fetched the
// service, or if there simply is none.
// If we never fetched it, do so now and store the result.
if (!fServices.containsKey(serviceRef)) {
service = fBundleContext.getService(serviceRef);
fServices.put(serviceRef, service);
}
}
return service;
}
/**
* Un-gets all the references held by this tracker. Must be called
* to avoid leaking OSGI service references.
*/
@ThreadSafe
public void dispose() {
assert !fDisposed;
fDisposed = true;
DsfSession session = DsfSession.getSession(fSessionId);
if (session != null) {
try {
if (!session.getExecutor().isInExecutorThread()) {
session.getExecutor().execute(this::doDispose);
return;
}
} catch (RejectedExecutionException e) {
}
}
// We should get to this point if
// 1) we're in session's executor thread
// 2) session is disposed already
// 3) executor rejected our runnable
// In all cases dispose the tracker in current thread.
doDispose();
}
private void doDispose() {
try {
fBundleContext.removeServiceListener(fListener);
for (ServiceReference<?> serviceRef : fServices.keySet()) {
fBundleContext.ungetService(serviceRef);
}
} catch (IllegalStateException e) {
// May be thrown during shutdown (bug 293049).
}
fServices.clear();
fServiceReferences.clear();
}
@Override
protected void finalize() throws Throwable {
assert fDisposed;
super.finalize();
}
}