com.vectrace.MercurialEclipse.MercurialEclipsePlugin.java Source code

Java tutorial

Introduction

Here is the source code for com.vectrace.MercurialEclipse.MercurialEclipsePlugin.java

Source

/*******************************************************************************
 * Copyright (c) 2005-2008 VecTrace (Zingo Andersen) and others.
 * 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:
 *     VecTrace (Zingo Andersen) - implementation
 *     Stefan Groschupf          - logError
 *     Jrme Ngre              - some fixes
 *     Stefan C                  - Code cleanup
 *     Andrei Loskutov           - bug fixes
 *     Zsolt Koppany (Intland)
 *     Adam Berkes (Intland)     - default encoding
 *     Philip Graf               - proxy support
 *     Bastian Doetsch           - bug fixes and implementation
 *******************************************************************************/

package com.vectrace.MercurialEclipse;

import java.lang.reflect.InvocationTargetException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.net.proxy.IProxyService;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.IJobManager;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.dialogs.MessageDialogWithToggle;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.DecorationOverlayIcon;
import org.eclipse.jface.viewers.IDecoration;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.eclipse.ui.statushandlers.StatusManager;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Version;
import org.osgi.util.tracker.ServiceTracker;

import com.vectrace.MercurialEclipse.commands.AbstractShellCommand;
import com.vectrace.MercurialEclipse.commands.HgClients;
import com.vectrace.MercurialEclipse.commands.HgDebugInstallClient;
import com.vectrace.MercurialEclipse.commands.RootlessHgCommand;
import com.vectrace.MercurialEclipse.exception.HgException;
import com.vectrace.MercurialEclipse.model.HgRoot;
import com.vectrace.MercurialEclipse.preferences.MercurialPreferenceConstants;
import com.vectrace.MercurialEclipse.storage.HgCommitMessageManager;
import com.vectrace.MercurialEclipse.storage.HgRepositoryLocationManager;
import com.vectrace.MercurialEclipse.team.MercurialUtilities;
import com.vectrace.MercurialEclipse.team.cache.CommandServerCache;
import com.vectrace.MercurialEclipse.utils.StringUtils;
import com.vectrace.MercurialEclipse.views.console.HgConsoleHolder;

/**
 * The main plugin class to be used in the desktop.
 */
public class MercurialEclipsePlugin extends AbstractUIPlugin {

    public static final String ID = "com.vectrace.MercurialEclipse"; //$NON-NLS-1$

    public static final String ID_CHANGELOG_VIEW = "com.vectrace.MercurialEclipse.views.ChangeLogView"; //$NON-NLS-1$

    public static final String BUNDLE_FILE_PREFIX = "bundlefile"; //$NON-NLS-1$

    // The shared instance.
    private static MercurialEclipsePlugin plugin;

    private static final String HGENCODING;

    static {
        // next in line is HGENCODING in environment
        String enc = System.getProperty("HGENCODING");

        // next is platform encoding as available in JDK
        if (!StringUtils.isEmpty(enc) && Charset.isSupported(enc)) {
            HGENCODING = enc;
        } else {
            if (Charset.isSupported("UTF-8")) {
                HGENCODING = Charset.forName("UTF-8").name();
            } else {
                HGENCODING = Charset.defaultCharset().name();
            }
        }
    }

    // the repository manager
    private static HgRepositoryLocationManager repoManager = new HgRepositoryLocationManager();

    // the commit message manager
    private static HgCommitMessageManager commitMessageManager = new HgCommitMessageManager();

    private boolean hgUsable = true;

    private ServiceTracker proxyServiceTracker;

    /** Observed hg version */
    public /*final*/ Version hgVersion = Version.emptyVersion;

    /**
     * should not be used by any code except initialization of hg
     *
     * @see MercurialEclipsePlugin#checkHgInstallation()
     */
    public static final CountDownLatch startSignal = new CountDownLatch(1);

    private static final Pattern VERSION_PATTERN = Pattern.compile(".*version\\s+(\\d(\\.\\d)+)+.*", //$NON-NLS-1$
            Pattern.CASE_INSENSITIVE);

    public MercurialEclipsePlugin() {
        // should NOT do anything until started by OSGI
    }

    /**
     * @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext)
     */
    @Override
    public void start(BundleContext context) throws Exception {
        super.start(context);
        plugin = this;
        DefaultConfiguration cfg = new DefaultConfiguration();
        HgClients.initialize(cfg, cfg, cfg);
        proxyServiceTracker = new ServiceTracker(context, IProxyService.class.getName(), null);
        proxyServiceTracker.open();

        final Job job = new Job(Messages.getString("MercurialEclipsePlugin.startingMercurialEclipse")) { //$NON-NLS-1$
            @Override
            protected IStatus run(IProgressMonitor monitor) {
                try {
                    monitor.beginTask(Messages.getString("MercurialEclipsePlugin.startingMercurialEclipse"), 3); //$NON-NLS-1$
                    monitor.subTask(Messages.getString("MercurialEclipsePlugin.checkingMercurialInstallation")); //$NON-NLS-1$
                    checkHgInstallation();
                    monitor.worked(1);
                    // read known repositories
                    monitor.subTask(Messages.getString("MercurialEclipsePlugin.loadingKnownMercurialRepositories")); //$NON-NLS-1$
                    repoManager.start();
                    monitor.worked(1);
                    // read in commit messages from disk
                    monitor.subTask(Messages.getString("MercurialEclipsePlugin.startingCommitMessageManager")); //$NON-NLS-1$
                    commitMessageManager.start();
                    monitor.worked(1);
                    monitor.done();
                    return new Status(IStatus.OK, ID,
                            Messages.getString("MercurialEclipsePlugin.startedSuccessfully")); //$NON-NLS-1$
                } catch (Throwable e) {
                    hgUsable = false;
                    logError(Messages.getString("MercurialEclipsePlugin.unableToStart"), e); //$NON-NLS-1$
                    return new Status(IStatus.ERROR, ID, e.getLocalizedMessage(), e);
                } finally {
                    // show console on startup if configured
                    if (getPreferenceStore()
                            .getBoolean(MercurialPreferenceConstants.PREF_CONSOLE_SHOW_ON_STARTUP)) {
                        // open console in SWT GUI Thread
                        new SafeUiJob(Messages.getString("MercurialEclipsePlugin.openingMercurialConsole")) { //$NON-NLS-1$
                            @Override
                            protected IStatus runSafe(IProgressMonitor monitor2) {
                                HgConsoleHolder.getInstance().showConsole(true);
                                return super.runSafe(monitor2);
                            }
                        }.schedule();
                    }

                    new SafeUiJob("Initializing UI resources") { //$NON-NLS-1$
                        @Override
                        protected IStatus runSafe(IProgressMonitor monitor2) {
                            PlatformUI.getWorkbench().getProgressService().registerIconForFamily(
                                    getImageDescriptor("mercurialeclipse.png"), AbstractShellCommand.class);
                            return super.runSafe(monitor2);
                        }
                    }.schedule();
                }
            }
        };

        job.setPriority(Job.SHORT);
        job.schedule();
    }

    /**
     * Checks if Mercurial is configured properly by issuing the hg debuginstall command.
     */
    public void checkHgInstallation() {
        try {
            hgUsable = true;
            MercurialUtilities.getHGExecutable(true);
            String result = HgDebugInstallClient.debugInstall();
            hgVersion = checkHgVersion();
            if (!result.endsWith("o problems detected")) { //$NON-NLS-1$
                logInfo(result, null);
            }
        } catch (Throwable e) {
            hgUsable = false;
            MercurialEclipsePlugin.logError(e);
            MercurialEclipsePlugin.showError(e);
            hgVersion = Version.emptyVersion;
        } finally {
            CommandServerCache.getInstance().invalidateAll();
            startSignal.countDown();
            if (isDebugging()) {
                System.out.println(HgFeatures.printSummary());
            }
        }
    }

    /**
     * Plugin depends on native mercurial installation, which has to be checked at plugin startup.
     * Note that the right plugin state will be set only some time after plugin startup, so that in
     * a short time between plugin activation and
     * {@link MercurialEclipsePlugin#checkHgInstallation()} call plugin state might be not yet
     * initialized properly.
     *
     * @return true if we have already tried to identify mercurial version (independently if the
     *         check fails or not), false if plugin is still not yet initialized properly
     */
    public static boolean isVersionCheckDone() {
        MercurialEclipsePlugin mep = MercurialEclipsePlugin.getDefault();
        return !mep.isHgUsable() || !mep.getHgVersion().equals(Version.emptyVersion);
    }

    private Version checkHgVersion() throws HgException {
        AbstractShellCommand command = new RootlessHgCommand("version", "Checking for required version");

        command.setInitialCommand(true);

        Version preferredVersion = HgFeatures.getPreferredVersion();
        String version = new String(command.executeToBytes(Integer.MAX_VALUE)).trim();
        String[] split = version.split("\\n"); //$NON-NLS-1$
        version = split.length > 0 ? split[0] : ""; //$NON-NLS-1$
        Matcher matcher = VERSION_PATTERN.matcher(version);
        boolean failedToParse = !matcher.matches() || matcher.groupCount() <= 0
                || (version = matcher.group(1)) == null;
        if (failedToParse) {
            HgFeatures.setToVersion(preferredVersion);
            HgFeatures.applyAllTo(getPreferenceStore());
            logWarning("Can't uderstand Mercurial version string: '" + version + "'. Assume that at least "
                    + preferredVersion + " is available.", null);
            return preferredVersion;
        }
        Version detectedVersion = new Version(version);
        HgFeatures.setToVersion(detectedVersion);
        HgFeatures.applyAllTo(getPreferenceStore());
        if (!HgFeatures.isSupported(detectedVersion)) {
            throw new HgException(Messages.getString("MercurialEclipsePlugin.unsupportedHgVersion") //$NON-NLS-1$
                    + version + Messages.getString("MercurialEclipsePlugin.expectedAtLeast") //$NON-NLS-1$
                    + HgFeatures.getLowestWorkingVersion() + "."); //$NON-NLS-1$
        }
        if (!HgFeatures.isHappyWith(detectedVersion)) {
            logWarning("Can not use some of the new Mercurial features, " + "hg version greater equals "
                    + preferredVersion + " required, but " + detectedVersion + " found. Features state:\n"
                    + HgFeatures.printSummary() + ".", null);
        }
        return detectedVersion;
    }

    /**
     * @return the observer hg version, never null. Returns {@link Version#emptyVersion} in case the
     *         hg version is either not detected yet or can't be parsed properly
     */
    public Version getHgVersion() {
        return hgVersion;
    }

    /**
     * Gets the repository manager
     */
    public static HgRepositoryLocationManager getRepoManager() {
        return repoManager;
    }

    public static HgCommitMessageManager getCommitMessageManager() {
        return commitMessageManager;
    }

    @Override
    public void stop(BundleContext context) throws Exception {
        try {
            repoManager.stop();
            // save commit messages to disk
            commitMessageManager.stop();
            proxyServiceTracker.close();
            MercurialUtilities.disposeColorsAndFonts();
        } finally {
            super.stop(context);
        }
    }

    /**
     * Returns the shared instance.
     */
    public static MercurialEclipsePlugin getDefault() {
        return plugin;
    }

    /**
     * Returns an image descriptor for the image file at the given plug-in relative path.
     *
     * @param path
     *            the path
     * @return the image descriptor
     */
    public static ImageDescriptor getImageDescriptor(String path) {
        ImageDescriptor descriptor = getDefault().getImageRegistry().getDescriptor(path);
        if (descriptor == null) {
            descriptor = AbstractUIPlugin.imageDescriptorFromPlugin(ID, "icons/" + path); //$NON-NLS-1$
            getDefault().getImageRegistry().put(path, descriptor);
        }
        return descriptor;
    }

    /**
     * Returns an image at the given plug-in relative path.
     *
     * @param path
     *            the path
     * @return the image
     */
    public static Image getImage(String path) {
        // make sure descriptor is created
        getImageDescriptor(path);
        return getDefault().getImageRegistry().get(path);
    }

    /**
     * Returns an image with overlay at given place at the given plug-in relative path.
     *
     * @param basePath
     *            the base image plug-in relative path.
     * @param overlayPath
     *            the overlay image plug-in relative path.
     * @param quadrant
     *            the quadrant (one of {@link IDecoration} ({@link IDecoration#TOP_LEFT},
     *            {@link IDecoration#TOP_RIGHT}, {@link IDecoration#BOTTOM_LEFT},
     *            {@link IDecoration#BOTTOM_RIGHT} or {@link IDecoration#UNDERLAY})
     * @return the image
     */
    public static Image getImage(String basePath, String overlayPath, int quadrant) {
        getImageDescriptor(basePath, overlayPath, quadrant);
        return getDefault().getImageRegistry().get(basePath + overlayPath + quadrant);
    }

    /**
     * Returns an image with overlay at given place at the given plug-in relative path.
     *
     * @param basePath
     *            the base image plug-in relative path.
     * @param overlayPath
     *            the overlay image plug-in relative path.
     * @param quadrant
     *            the quadrant (one of {@link IDecoration} ({@link IDecoration#TOP_LEFT},
     *            {@link IDecoration#TOP_RIGHT}, {@link IDecoration#BOTTOM_LEFT},
     *            {@link IDecoration#BOTTOM_RIGHT} or {@link IDecoration#UNDERLAY})
     * @return the image
     */
    public static ImageDescriptor getImageDescriptor(String basePath, String overlayPath, int quadrant) {
        String key = basePath + overlayPath + quadrant;
        ImageDescriptor descriptor = getDefault().getImageRegistry().getDescriptor(key);
        if (descriptor == null) {
            Image base = getImage(basePath);
            ImageDescriptor overlay = getImageDescriptor(overlayPath);
            descriptor = new DecorationOverlayIcon(base, overlay, quadrant);
            getDefault().getImageRegistry().put(key, descriptor);
        }
        return descriptor;
    }

    public static final void logError(String message, Throwable error) {
        getDefault().getLog().log(createStatus(message, 0, IStatus.ERROR, error));
    }

    public static void showError(final Throwable error) {
        new ErrorJob(error).schedule(100);
    }

    public static final void logWarning(String message, Throwable error) {
        getDefault().getLog().log(createStatus(message, 0, IStatus.WARNING, error));
    }

    public static final void logInfo(String message, Throwable error) {
        getDefault().getLog().log(createStatus(message, 0, IStatus.INFO, error));
    }

    public static IStatus createStatus(String msg, int code, int severity, Throwable ex) {
        return new Status(severity, ID, code, msg, ex);
    }

    public static final void logError(Throwable ex) {
        logError(ex.getMessage(), ex);
    }

    /**
     * Creates a busy cursor and runs the specified runnable. May be called from a non-UI thread.
     *
     * @param parent
     *            the parent Shell for the dialog
     * @param cancelable
     *            if true, the dialog will support cancelation
     * @param runnable
     *            the runnable
     *
     * @exception InvocationTargetException
     *                when an exception is thrown from the runnable
     * @exception InterruptedException
     *                when the progress monitor is cancelled
     */
    public static void runWithProgress(Shell parent, boolean cancelable, final IRunnableWithProgress runnable)
            throws InvocationTargetException, InterruptedException {

        boolean createdShell = false;
        Shell myParent = parent;
        try {
            if (myParent == null || myParent.isDisposed()) {
                Display display = Display.getCurrent();
                if (display == null) {
                    // cannot provide progress (not in UI thread)
                    runnable.run(new NullProgressMonitor());
                    return;
                }
                // get the active shell or a suitable top-level shell
                myParent = display.getActiveShell();
                if (myParent == null) {
                    myParent = new Shell(display);
                    createdShell = true;
                }
            }
            // pop up progress dialog after a short delay
            final Exception[] holder = new Exception[1];
            BusyIndicator.showWhile(myParent.getDisplay(), new Runnable() {
                public void run() {
                    try {
                        runnable.run(new NullProgressMonitor());
                    } catch (InvocationTargetException e) {
                        holder[0] = e;
                    } catch (InterruptedException e) {
                        holder[0] = e;
                    }
                }
            });
            if (holder[0] != null) {
                if (holder[0] instanceof InvocationTargetException) {
                    throw (InvocationTargetException) holder[0];
                }
                throw (InterruptedException) holder[0];

            }
            // new TimeoutProgressMonitorDialog(parent, TIMEOUT).run(true
            // /*fork*/, cancelable, runnable);
        } finally {
            if (createdShell) {
                parent.dispose();
            }
        }
    }

    /**
     * Convenience method to get the currently active workbench page. Note that the active page may
     * not be the one that the usr perceives as active in some situations so this method of
     * obtaining the activae page should only be used if no other method is available.
     *
     * @return the active workbench page
     */
    public static IWorkbenchPage getActivePage() {
        return getActiveWindow().getActivePage();
    }

    /**
     * Convenience method to get the currently active shell.
     *
     * @return the active workbench shell. Never null, if there is at least one window open.
     */
    public static Shell getActiveShell() {
        return getActiveWindow().getShell();
    }

    /**
     * Convenience method to get the currently active workbench window.
     *
     * @return the active workbench window. Never null, if there is at least one window open.
     */
    public static IWorkbenchWindow getActiveWindow() {
        IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
        if (window != null) {
            return window;
        }
        return PlatformUI.getWorkbench().getWorkbenchWindows()[0];
    }

    public boolean isHgUsable() {
        return hgUsable;
    }

    public static Display getStandardDisplay() {
        return PlatformUI.getWorkbench().getDisplay();
    }

    public IProxyService getProxyService() {
        return (IProxyService) proxyServiceTracker.getService();
    }

    /**
     * Show a dialog only if the user hasn't selected "don't show again" for it.
     *
     * @param title The title
     * @param message The message
     * @param type The type, for example MessageDialog.CONFIRM
     * @param key The preference key
     * @param shell The shell to use
     * @return True of ok was pressed
     */
    public static boolean showDontShowAgainConfirmDialog(final String title, final String message, int type,
            String key, Shell shell) {
        IPreferenceStore store = getDefault().getPreferenceStore();
        String pref = store.getString(key);
        if (MessageDialogWithToggle.PROMPT.equals(pref)) {
            String toggleMessage = Messages.getString("Dialogs.DontShowAgain");
            MessageDialogWithToggle confirmDialog = MessageDialogWithToggle.open(type, shell, title, message,
                    toggleMessage, false, store, key, SWT.NONE);
            int returnCode = confirmDialog.getReturnCode();
            return returnCode == Window.OK;
        }
        return true;
    }

    /**
     * The default encoding which is used by the current environment.
     * <p>
     * <b>Note</b>: you probably want use {@link HgRoot#getEncoding()} instead, as each repository
     * may use it's own encoding
     * <p>
     * <b>Note</b>: Python's encoding isn't 1-1 with Charset.name() so do not store
     * {@link java.nio.charset.Charset}.
     *
     * @return a valid encoding name, never null.
     */
    public static String getDefaultEncoding() {
        return HGENCODING;
    }

    /**
     * Job to show error dialogs. Avoids to show hunderts of dialogs by ussing an exclusive rule.
     *
     * @author Andrei
     */
    private static final class ErrorJob extends SafeUiJob {

        static class ExclusiveRule implements ISchedulingRule {
            public boolean isConflicting(ISchedulingRule rule) {
                return contains(rule);
            }

            public boolean contains(ISchedulingRule rule) {
                return rule instanceof ExclusiveRule;
            }
        }

        final IStatus status;

        private ErrorJob(Throwable error) {
            super(Messages.getString("MercurialEclipsePlugin.showError")); //$NON-NLS-1$
            if (error instanceof CoreException) {
                status = ((CoreException) error).getStatus();
            } else {
                status = createStatus(error.getMessage(), 0, IStatus.ERROR, error);
            }
            setRule(new ExclusiveRule());
        }

        @Override
        protected IStatus runSafe(IProgressMonitor monitor) {

            IJobManager jobManager = Job.getJobManager();
            String title;
            IStatus errStatus;
            if (jobManager.find(plugin).length == 1) {
                // it's me alone there
                errStatus = status;
            } else {
                // o-ho, we have multiple errors waiting to be displayed...
                title = Messages.getString("MercurialEclipsePlugin.unexpectedErrors"); //$NON-NLS-1$
                String message = Messages.getString("MercurialEclipsePlugin.unexpectedErrorsOccured"); //$NON-NLS-1$
                // get the latest state
                Job[] jobs = jobManager.find(plugin);
                // discard all waiting now (we are not affected)
                jobManager.cancel(plugin);
                List<IStatus> stati = new ArrayList<IStatus>();
                for (Job job : jobs) {
                    if (job instanceof ErrorJob) {
                        ErrorJob errorJob = (ErrorJob) job;
                        stati.add(errorJob.status);
                    }
                }
                IStatus[] array = stati.toArray(new IStatus[stati.size()]);
                errStatus = new MultiStatus(title, 0, array, message, null);
            }
            StatusManager.getManager().handle(errStatus, StatusManager.SHOW);
            return super.runSafe(monitor);
        }

        @Override
        public boolean belongsTo(Object family) {
            return plugin == family;
        }
    }

    /**
     * Find the object associated with the given object that is adapted to
     * the provided class.
     *
     * @param anyObject might be null
     * @param clazz class to get the adapter for
     * @return adapted object or null if no adapter provided or the given object is null
     */
    public static <V> V getAdapter(Object anyObject, Class<V> clazz) {
        if (clazz.isInstance(anyObject)) {
            return clazz.cast(anyObject);
        }
        if (anyObject instanceof IAdaptable) {
            IAdaptable a = (IAdaptable) anyObject;
            return clazz.cast(a.getAdapter(clazz));
        }
        return null;
    }

    /**
     * Unwrap and throw as a CoreException. Note: Never returns
     * @param e The exception to use
     * @throws CoreException
     */
    public static void rethrow(Throwable e) throws CoreException {
        if (e instanceof CoreException) {
            throw (CoreException) e;
        } else if (e instanceof InvocationTargetException) {
            rethrow(((InvocationTargetException) e).getTargetException());
        }

        throw new HgException(e.getLocalizedMessage(), e);
    }

    /**
     * Block the current thread until initial configuration of the hg executable is done.
     */
    public static void waitForHgInitDone() {
        try {
            MercurialEclipsePlugin.startSignal.await();
        } catch (InterruptedException e1) {
            MercurialEclipsePlugin.logError(e1);
        }
    }
}