rabbit.tracking.internal.trackers.JavaTracker.java Source code

Java tutorial

Introduction

Here is the source code for rabbit.tracking.internal.trackers.JavaTracker.java

Source

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