| /******************************************************************************* |
| * Copyright (c) 2007 The Eclipse Foundation. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * The Eclipse Foundation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.epp.usagedata.internal.gathering.services; |
| |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.concurrent.LinkedBlockingQueue; |
| |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IConfigurationElement; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.ListenerList; |
| import org.eclipse.core.runtime.Platform; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.adaptor.EclipseStarter; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.epp.usagedata.internal.gathering.UsageDataCaptureActivator; |
| import org.eclipse.epp.usagedata.internal.gathering.events.UsageDataEvent; |
| import org.eclipse.epp.usagedata.internal.gathering.events.UsageDataEventListener; |
| import org.eclipse.epp.usagedata.internal.gathering.monitors.UsageMonitor; |
| |
| /** |
| * The {@link UsageDataService} class is registered as an OSGi service by the |
| * bundle activator on startup. It is responsible for installing monitors |
| * registered via the {@value #MONITORS_EXTENSION_POINT} extension point. These |
| * monitors all feed information back into the instance which is responsible for |
| * dispatching those events to event listeners registered via the |
| * {@link #addUsageDataEventListener(UsageDataEventListener)} method. |
| * <p> |
| * The instance starts monitoring activities immediately after it is started, |
| * but does not dispatch the resulting events until after the workbench is |
| * running (as reported by the {@link EclipseStarter#isRunning()} method. |
| * </p> |
| * <p> |
| * Efforts have been taken to try and keep the impact on the user |
| * experience—performance in particular—to a minimum. Any decision |
| * balancing absolute correctness of data capture and user experience is made in |
| * favour of preserving positive user experience and reducing any negative |
| * impact on performance. In that regard, for example, cancel really means |
| * cancel to the {@link #eventConsumerJob} and may leave some events |
| * undispatched to the listeners. |
| * |
| * @author Wayne Beaton |
| */ |
| @SuppressWarnings("restriction") |
| public class UsageDataService { |
| private static final String MONITORS_EXTENSION_POINT = UsageDataCaptureActivator.PLUGIN_ID + ".monitors"; //$NON-NLS-1$ |
| |
| private boolean monitoring = false; |
| |
| /** |
| * The list of monitors hooked into various parts of the system listening to |
| * what the user is up to. The objects in this list are of type |
| * {@link UsageMonitor}. Strictly speaking this is not a list of |
| * "listeners", but {@link ListenerList} provides some convenient management |
| * functionality. |
| */ |
| private ListenerList monitors = new ListenerList(); |
| |
| /** |
| * The list of objects of type {@link UsageDataEventListener} listening to events |
| * generated by this service. |
| */ |
| private ListenerList eventListeners = new ListenerList(); |
| |
| /** |
| * The thread that figures out what to do with events provided by the |
| * various monitors. This functionality is separated into a separate thread |
| * in anticipation of performance issues (see {@link #startEventConsumerJob()} |
| * for discussion. |
| */ |
| Job eventConsumerJob; |
| |
| /** |
| * A temporary home for events as they are generated. As they are created, |
| * events are dropped into the queue by the source thread. Events are consumed |
| * from the queue by the {@link #eventConsumerJob}. |
| * @see #startEventConsumerJob() |
| */ |
| protected LinkedBlockingQueue<UsageDataEvent> events = new LinkedBlockingQueue<UsageDataEvent>(); |
| |
| /** |
| * This field maps the symbolic name of bundles to the last loaded version. |
| * This information is handy for filling in missing bundle version information |
| * for singleton bundles. |
| * @see #registerBundleVersion(UsageDataEvent) |
| */ |
| private Map<String, String> bundleVersionMap = new HashMap<String, String>(); |
| |
| /** |
| * This method starts the monitoring process. If the service has already been |
| * "started" when this method is called, nothing happens (i.e. multiple calls |
| * to this method are tolerated). |
| */ |
| public void startMonitoring() { |
| if (isMonitoring()) |
| return; |
| |
| startMonitors(); |
| startEventConsumerJob(); |
| |
| monitoring = true; |
| |
| } |
| |
| /** |
| * This method stops the monitoring process. If the service is already stopped |
| * when this method is called, nothing happens (i.e. multiple calls |
| * to this method are tolerated). |
| */ |
| public synchronized void stopMonitoring() { |
| if (!isMonitoring()) |
| return; |
| |
| stopMonitors(); |
| stopEventConsumerJob(); |
| |
| monitoring = false; |
| |
| } |
| |
| |
| public boolean isMonitoring() { |
| return monitoring; |
| } |
| |
| /** |
| * Start the {@link #eventConsumerJob}. Various monitors add events to the |
| * {@link #events} queue. In order to avoid degrading system performance any |
| * more than necessary, events are added to the queue by the monitors. The |
| * {@link #eventConsumerJob} then consumes the events from the queue and |
| * dispatches them to the various {@link UsageDataEventListener}s. Since |
| * the event listeners will do expensive things like open and write to |
| * files, it is anticipated that this architecture will allow the necessary |
| * activities to happen without significantly impacting the user's |
| * experience. |
| */ |
| protected void startEventConsumerJob() { |
| // TODO Decide if the job is more trouble than it's worth. |
| if (eventConsumerJob != null) return; |
| |
| eventConsumerJob = new Job("Usage Data Event consumer") { //$NON-NLS-1$ |
| public IStatus run(IProgressMonitor monitor) { |
| waitForWorkbenchToFinishStarting(); |
| while (!monitor.isCanceled()) { |
| UsageDataEvent event = getQueuedEvent(); |
| dispatchEvent(event); |
| } |
| return Status.OK_STATUS; |
| } |
| }; |
| eventConsumerJob.setSystem(true); |
| eventConsumerJob.setPriority(Job.LONG); |
| eventConsumerJob.schedule(1000); // Wait a few minutes before scheduling the job. |
| } |
| |
| /** |
| * This method pauses the current thread until the workbench has |
| * finished starting. This should provide enough time for bundles |
| * that are installing usage data event listeners to complete before |
| * events are dispatched. |
| */ |
| protected void waitForWorkbenchToFinishStarting() { |
| /* |
| * We want the job to pause until after all the bundles that are |
| * loaded at startup have finished loading. This will give |
| * bundles that listen to usage data events time to load and |
| * install listeners before events are fired off (which should |
| * mean that events won't get lost). |
| * |
| * I had originally tried using Display.syncExec(Runnable) (with |
| * an "do nothing" Runnable, but this caused some weird classloading |
| * issues similar to those referenced in Bug 88109. |
| */ |
| while (!EclipseStarter.isRunning()) { |
| try { |
| // It probably doesn't matter too much if we wait too long here. |
| // TODO Is 1 second too long? |
| Thread.sleep(1000); |
| } catch (InterruptedException e) { |
| // Ignore and loop again! |
| } |
| } |
| } |
| |
| protected void stopEventConsumerJob() { |
| eventConsumerJob.cancel(); |
| // Interrupt the thread to make sure that the |
| // job gets the chance to terminate gracefully. Then join |
| // the thread to make sure that it gets enough time to |
| // properly shutdown. See Bug 306449. |
| Thread thread = eventConsumerJob.getThread(); |
| if (thread != null) |
| thread.interrupt(); |
| try { |
| eventConsumerJob.join(); |
| } catch (InterruptedException e) { |
| // Oh well, we tried... |
| } |
| eventConsumerJob = null; |
| } |
| |
| /** |
| * This method returns the next available event. If no event is available, |
| * the current thread is suspended until an event is added. This method will |
| * return <code>null</null> if it is called with an empty event queue after |
| * monitoring is turned off or if the thread is interrupted. |
| * |
| * @return an instance of {@link UsageDataEvent} or <code>null</code>. |
| */ |
| private UsageDataEvent getQueuedEvent() { |
| try { |
| return events.take(); |
| } catch (InterruptedException e) { |
| return null; |
| } |
| } |
| |
| /** |
| * This method queues an event containing the given information for |
| * processing. |
| * |
| * @param what |
| * what happened? was it an activation, started, clicked, ... ? |
| * @param kind |
| * what kind of thing caused it? view, editor, bundle, ... ? |
| * @param description |
| * information about the event. e.g. name of the command, view, |
| * editor, ... |
| * @param bundleId |
| * symbolic name of the bundle that owns the thing that caused |
| * the event. |
| */ |
| public void recordEvent(String what, String kind, String description, |
| String bundleId) { |
| recordEvent(what, kind, description, bundleId, null); |
| } |
| |
| /** |
| * <p> |
| * This method queues an event containing the given information for |
| * processing. |
| * </p> |
| * |
| * @param what |
| * what happened? was it an activation, started, clicked, ... ? |
| * @param kind |
| * what kind of thing caused it? view, editor, bundle, ... ? |
| * @param description |
| * information about the event. e.g. name of the command, view, |
| * editor, ... |
| * @param bundleId |
| * symbolic name of the bundle that owns the thing that caused |
| * the event. |
| * @param bundleVersion |
| * the version of the bundle that owns the thing that caused the |
| * event. |
| */ |
| public void recordEvent(String what, String kind, String description, |
| String bundleId, String bundleVersion) { |
| UsageDataEvent event = new UsageDataEvent(what, kind, description, bundleId, |
| bundleVersion, System.currentTimeMillis()); |
| recordEvent(event); |
| |
| } |
| |
| private void recordEvent(UsageDataEvent event) { |
| /* |
| * Multiple thread access to #events is managed the LinkedBlockingQueue |
| * implementation. |
| */ |
| events.add(event); |
| } |
| |
| /** |
| * This method dispatches <code>event</code> to the registered event |
| * listeners. |
| * |
| * @param event |
| * the {@link UsageDataEvent} to dispatch. |
| */ |
| private void dispatchEvent(UsageDataEvent event) { |
| if (event == null) return; |
| registerBundleVersion(event); |
| if (event.bundleVersion == null) event.bundleVersion = getBundleVersion(event.bundleId); |
| Object[] listeners = eventListeners.getListeners(); |
| for (int index = 0; index < listeners.length; index++) { |
| UsageDataEventListener listener = (UsageDataEventListener) listeners[index]; |
| dispatchEvent(event, listener); |
| } |
| } |
| |
| /** |
| * This method does the actual dispatching of the event to a single listener. If |
| * an exception occurs in the execution of the listener, an exception is logged. |
| * |
| * @param event |
| * @param listener |
| */ |
| private void dispatchEvent(UsageDataEvent event, UsageDataEventListener listener) { |
| try { |
| listener.accept(event); |
| } catch (Throwable e) { |
| // TODO Add some logic to remove repeat offenders. |
| UsageDataCaptureActivator.getDefault().logException("The listener (" + listener.getClass() + ") threw an exception", e); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| } |
| |
| /** |
| * If the event represents a bundle activation, record a mapping between the |
| * bundleId and bundleVersion. This information is used to fill in missing |
| * information when an event comes in with just a bundleId and no version |
| * information. This assumes that the bundle is a singleton. That is, there |
| * is no provision here for dealing with multiple versions of bundles. If |
| * the event is a result of something that may come from a non-singleton |
| * bundle, then it is the responsibility of the event source to determine |
| * the appropriate version. |
| * |
| * @param event |
| * instance of {@link UsageDataEvent}. |
| */ |
| private void registerBundleVersion(UsageDataEvent event) { |
| /* |
| * This is a bit of a hack since we're using inside knowledge about a |
| * particular type of event (that we're pretty well decoupled |
| * from--though this knowledge does constitute a relatively tight |
| * form of coupling). If the event tells us that a bundle has been |
| * started, we'll move on; otherwise, we bail out. We're not interested |
| * in other bundle events (like stops, etc.), since these |
| * events will be relatively rare for the kinds of bundles we actually |
| * care about. |
| */ |
| if (!("bundle".equals(event.kind))) return; //$NON-NLS-1$ |
| if (!("started".equals(event.what))) return; //$NON-NLS-1$ |
| |
| synchronized (bundleVersionMap) { |
| bundleVersionMap.put(event.bundleId, event.bundleVersion); |
| } |
| } |
| |
| /** |
| * This method returns the version of the bundle with id bundleId. This |
| * assumes that the bundle is a singleton. That is, there is no provision |
| * here for dealing with multiple versions of bundles. If the event is a |
| * result of something that may come from a non-singleton bundle, then it is |
| * the responsibility of the event source to determine the appropriate |
| * version. |
| * |
| * @param bundleId |
| * the symbolic name of a bundle. |
| * |
| * @return the id of the last bundle started with the given id. |
| */ |
| private String getBundleVersion(String bundleId) { |
| if (bundleId == null) return null; |
| synchronized (bundleVersionMap) { |
| return bundleVersionMap.get(bundleId); |
| } |
| } |
| |
| protected void startMonitors() { |
| IConfigurationElement[] elements = Platform.getExtensionRegistry() |
| .getConfigurationElementsFor( |
| MONITORS_EXTENSION_POINT); |
| for (IConfigurationElement element : elements) { |
| if ("monitor".equals(element.getName())) { //$NON-NLS-1$ |
| |
| try { |
| Object monitor = element.createExecutableExtension("class"); //$NON-NLS-1$ |
| if (monitor instanceof UsageMonitor) { |
| startMonitor((UsageMonitor) monitor); |
| } |
| } catch (CoreException e) { |
| // TODO Auto-generated catch block |
| e.printStackTrace(); |
| } |
| } |
| } |
| } |
| |
| private void startMonitor(UsageMonitor monitor) { |
| monitor.startMonitoring(this); |
| monitors.add(monitor); |
| } |
| |
| protected void stopMonitors() { |
| for (Object monitor : monitors.getListeners()) { |
| stopMonitor((UsageMonitor) monitor); |
| } |
| } |
| |
| private void stopMonitor(UsageMonitor monitor) { |
| monitor.stopMonitoring(); |
| monitors.remove(monitor); |
| } |
| |
| public void addUsageDataEventListener(UsageDataEventListener listener) { |
| eventListeners.add(listener); |
| } |
| |
| public void removeUsageDataEventListener(UsageDataEventListener listener) { |
| eventListeners.remove(listener); |
| } |
| |
| } |