/*
* Copyright 2010 The Rabbit Eclipse Plug-in Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package rabbit.tracking.internal.trackers;
import rabbit.data.handler.DataHandler;
import rabbit.data.store.IStorer;
import rabbit.data.store.model.JavaEvent;
import rabbit.tracking.internal.IdleDetector;
import rabbit.tracking.internal.TrackingPlugin;
import rabbit.tracking.internal.util.Recorder;
import rabbit.tracking.internal.util.WorkbenchUtil;
import com.google.common.collect.Sets;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.ui.actions.SelectionConverter;
import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IPartListener;
import org.eclipse.ui.IWindowListener;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.joda.time.Interval;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Tracks time spent on Java elements such as classes, methods.
*/
@SuppressWarnings("restriction")
public class JavaTracker extends AbstractTracker<JavaEvent> {
/*
* Note that a lot of elements may be tracked by this tracker, and many of
* them are of no interest to us, for example, invalid elements, anonymous
* classes (their have no unique identifier) etc. Therefore we should perform
* filtering on the data before saving. Filtering the data does not remove the
* elements we don't want, instead we replace the element with a parent which
* is of our interest. For example, a data node before filter may be
* "the user spent 2 minutes on elementA", and after filter it may be
* "the user spent 2 minutes on the parent of elementA", where "elementA" is
* of no interest to us, but the parent of the element does.
*
* The following element types are of interest to us:
*
* 1) Type elements (classes, interfaces etc) that are not anonymous. 2)
* Methods (includes constructors) that are not enclosed in anonymous types.
* 3) Static initializers.
*
*
* Other elements will be converted.
*
* (A secrete note: doing so also reduces the size of the data files on disk,
* shhhh!)
*/
/**
* A set of all text widgets that are currently being listened to. This set is
* not synchronised.
*/
private final Set<StyledText> registeredWidgets;
/**
* Recorder for recording time duration.
*/
private final Recorder<IJavaElement> recorder = new Recorder<IJavaElement>();
/**
* A part listener listening for Java editor events.
*/
private final IPartListener partListener = new IPartListener() {
@Override
public void partActivated(IWorkbenchPart part) {
checkStart(part);
}
@Override
public void partBroughtToTop(IWorkbenchPart part) {
// Do nothing.
}
@Override
public void partClosed(IWorkbenchPart part) {
if (part instanceof JavaEditor) {
deregister((JavaEditor) part);
}
}
@Override
public void partDeactivated(IWorkbenchPart part) {
if (part instanceof JavaEditor) {
recorder.stop();
}
}
@Override
public void partOpened(IWorkbenchPart part) {
if (part instanceof JavaEditor) {
register((JavaEditor) part);
}
}
};
/**
* A window listener listening to window focus.
*/
private final IWindowListener winListener = new IWindowListener() {
@Override
public void windowActivated(IWorkbenchWindow window) {
checkStart(window.getPartService().getActivePart());
}
@Override
public void windowClosed(IWorkbenchWindow window) {
recorder.stop();
deregister(window);
}
@Override
public void windowDeactivated(IWorkbenchWindow window) {
recorder.stop();
}
@Override
public void windowOpened(IWorkbenchWindow window) {
register(window);
if (window.getWorkbench().getActiveWorkbenchWindow() == window) {
checkStart(window.getPartService().getActivePart());
}
}
};
/**
* An observer observing on the {@link #recorder} and use activeness.
*/
private final Observer observer = new Observer() {
@Override
public void update(Observable o, Object arg) {
if (!isEnabled()) {
return;
}
if (o == TrackingPlugin.getDefault().getIdleDetector()) {
if (((IdleDetector) o).isUserActive()) {
IWorkbenchWindow win = WorkbenchUtil.getActiveWindow();
if (win != null && WorkbenchUtil.isActiveShell(win)) {
checkStart(win.getPartService().getActivePart());
}
} else {
recorder.stop();
}
} else if (o == recorder) {
long start = recorder.getLastRecord().getStartTimeMillis();
long end = recorder.getLastRecord().getEndTimeMillis();
IJavaElement element = recorder.getLastRecord().getUserData();
if (element != null) {
addData(new JavaEvent(new Interval(start, end), element));
}
}
}
};
/**
* Listener to listen to keyboard input and mouse input on text widgets of
* editors.
*/
private final Listener listener = new Listener() {
// This listener used to provide compatibility with Eclipse 3.4, otherwise
// org.eclipse.swt.custom.CaretListener might be a better option (Eclipse
// * 3.5+).
@Override
public void handleEvent(Event event) {
checkStart();
}
};
/**
* Constructor.
*/
public JavaTracker() {
super();
registeredWidgets = Sets.newHashSet();
recorder.addObserver(observer);
}
@Override
public void saveData() {
filterData();
super.saveData();
}
@Override
protected IStorer<JavaEvent> createDataStorer() {
return DataHandler.getStorer(JavaEvent.class);
}
@Override
protected void doDisable() {
recorder.stop();
TrackingPlugin.getDefault().getIdleDetector().deleteObserver(observer);
IWorkbench workbench = PlatformUI.getWorkbench();
workbench.removeWindowListener(winListener);
for (IWorkbenchWindow window : workbench.getWorkbenchWindows()) {
deregister(window);
}
}
@Override
protected void doEnable() {
IWorkbench workbench = PlatformUI.getWorkbench();
workbench.addWindowListener(winListener);
for (IWorkbenchWindow window : workbench.getWorkbenchWindows()) {
register(window);
}
TrackingPlugin.getDefault().getIdleDetector().addObserver(observer);
// If there is an Java editor already active, start tracking:
checkStart();
}
/**
* Tries to start a tracking session, if the current element is not change,
* will do nothing, otherwise ends a session if there is one running, then if
* the currently selected element in Eclipse's active editor is not null,
* starts a new session.
*/
private void checkStart() {
IWorkbenchWindow win = WorkbenchUtil.getActiveWindow();
if (WorkbenchUtil.isActiveShell(win)) {
checkStart(win.getPartService().getActivePart());
}
}
/**
* Tries to start a tracking session, if the current element is not change,
* will do nothing, otherwise ends a session if there is one running, then if
* the currently selected element in Eclipse's active editor is not null,
* starts a new session.
*
* @param activePart The currently active part of the workbench, may be null.
*/
private void checkStart(final IWorkbenchPart activePart) {
if (!(activePart instanceof JavaEditor)) {
return;
}
PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable() {
@Override
public void run() {
IJavaElement element = null;
try {
element = SelectionConverter.getElementAtOffset((JavaEditor) activePart);
if (element != null) {
recorder.start(element);
}
} catch (JavaModelException e) {
// Nothing we can do.
System.err.println(getClass().getSimpleName() + " - checkStart: "
+ e.getMessage());
}
}
});
}
/**
* Removes the workbench window so that it's no longer being tracked.
*
* @param window The workbench window.
*/
private void deregister(IWorkbenchWindow window) {
window.getPartService().removePartListener(partListener);
for (IWorkbenchPage page : window.getPages()) {
for (IEditorReference ref : page.getEditorReferences()) {
IEditorPart editor = ref.getEditor(false);
if (editor instanceof JavaEditor) {
deregister((JavaEditor) editor);
}
}
}
}
/**
* Removes the editor no that it's no longer being tracked.
*
* @param editor The editor.
*/
private synchronized void deregister(JavaEditor editor) {
final StyledText widget = editor.getViewer().getTextWidget();
if (registeredWidgets.contains(widget)) {
PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable() {
@Override
public void run() {
widget.removeListener(SWT.KeyDown, listener);
widget.removeListener(SWT.MouseDown, listener);
}
});
registeredWidgets.remove(widget);
}
}
/**
* Performs filtering of the data before saving.
* <p>
* NOTE: Then a user starts to type a new java element, like a method, he/she
* knows what the name he/she is going to type for the method, but we have no
* way of knowing that, so lots of events may be recorded before he/she
* finishes typing the name. For example, if the user want to type "hello" as
* the method name, there will be events recorded about the java element
* "hel", or "hell", or "hello", we only need one of them ("hello") but we
* also want to keep the time about the invalid ones, so before we save the
* data, we check for non-existent java elements, and instead of saving the
* data under those elements, we save the data under the first existing parent
* of the elements, if all parents are missing (e.g. deletes the file), we
* save it under the file parent, like "File.java".
* </p>
*/
private void filterData() {
Set<JavaEvent> filteredData = Sets.newLinkedHashSet();
for (JavaEvent event : getData()) {
IJavaElement e = event.getElement();
// ITypeRoot represents the file, xxx.java. Everything above that is not
// modifiable in a JavaEditor, so no need to check them:
if (!e.exists()) {
for (; !e.exists() && !(e instanceof ITypeRoot); e = e.getParent());
filteredData.add(new JavaEvent(event.getInterval(), e));
} else {
IJavaElement actual = null;
try {
actual = filterElement(e);
} catch (JavaModelException ex) {
actual = null;
ex.printStackTrace();
}
if (actual == null) {
filteredData.add(event);
} else {
filteredData.add(new JavaEvent(event.getInterval(), actual));
}
}
}
// Replace the old data with the filtered:
flushData();
for (JavaEvent event : filteredData) {
addData(event);
}
}
/**
* Gets the actual element that we want before saving. One of the following
* types is returned:
*
* <ul>
* <li>A type that is not anonymous.</li>
* <li>A method that is not enclosed in an anonymous type.</li>
* <li>An initializer.</li>
* <li>A compilation unit.</li>
* <li>A class file.</li>
* <li>Null</li>
* </ul>
*
* @param element The element to filter.
* @return A filtered element, or null if not found.
* @throws JavaModelException If this element does not exist or if an
* exception occurs while accessing its corresponding resource.
*/
private IJavaElement filterElement(@Nullable IJavaElement element)
throws JavaModelException {
if (element == null) {
return null;
}
switch (element.getElementType()) {
case IJavaElement.TYPE:
if (((IType) element).isAnonymous()) {
return filterElement(element.getParent());
}
return element;
case IJavaElement.METHOD:
if (((IType) element.getParent()).isAnonymous()) {
return filterElement(element.getParent());
}
return element;
case IJavaElement.INITIALIZER:
case IJavaElement.COMPILATION_UNIT:
case IJavaElement.CLASS_FILE:
return element;
default:
return filterElement(element.getParent());
}
}
/**
* Registers the given workbench window to be tracked.
*
* @param window The workbench window.
*/
private void register(IWorkbenchWindow window) {
window.getPartService().addPartListener(partListener);
for (IWorkbenchPage page : window.getPages()) {
for (IEditorReference ref : page.getEditorReferences()) {
IEditorPart editor = ref.getEditor(false);
if (editor instanceof JavaEditor) {
register((JavaEditor) editor);
}
}
}
}
/**
* Registers the given editor to be tracked. Has no effect if the editor is
* already registered.
*
* @param editor The editor.
*/
private synchronized void register(JavaEditor editor) {
final StyledText widget = editor.getViewer().getTextWidget();
if (!registeredWidgets.contains(widget)) {
PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable() {
@Override
public void run() {
widget.addListener(SWT.KeyDown, listener);
widget.addListener(SWT.MouseDown, listener);
}
});
registeredWidgets.add(widget);
}
}
}