/*******************************************************************************
* Copyright (C) 2007,2010 Robin Rosenberg <robin.rosenberg@dewire.com>
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
* Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
* Copyright (C) 2012, Matthias Sohn <matthias.sohn@sap.com>
* Copyright (C) 2015, Philipp Bumann <bumannp@gmail.com>
* Copyright (C) 2016, Dani Megert <daniel_megert@ch.ibm.com>
*
* 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
*******************************************************************************/
package org.eclipse.egit.ui;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.egit.core.RepositoryCache;
import org.eclipse.egit.core.RepositoryUtil;
import org.eclipse.egit.core.project.RepositoryMapping;
import org.eclipse.egit.ui.internal.ConfigurationChecker;
import org.eclipse.egit.ui.internal.KnownHosts;
import org.eclipse.egit.ui.internal.RepositoryCacheRule;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.credentials.EGitCredentialsProvider;
import org.eclipse.egit.ui.internal.trace.GitTraceLocation;
import org.eclipse.egit.ui.internal.variables.GitTemplateVariableResolver;
import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.resource.LocalResourceManager;
import org.eclipse.jface.resource.ResourceManager;
import org.eclipse.jface.text.templates.ContextTypeRegistry;
import org.eclipse.jface.text.templates.TemplateContextType;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jgit.events.IndexChangedEvent;
import org.eclipse.jgit.events.IndexChangedListener;
import org.eclipse.jgit.events.ListenerHandle;
import org.eclipse.jgit.events.RepositoryEvent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.osgi.service.debug.DebugOptions;
import org.eclipse.osgi.service.debug.DebugOptionsListener;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IWindowListener;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.eclipse.ui.statushandlers.StatusManager;
import org.eclipse.ui.themes.ITheme;
import org.osgi.framework.BundleContext;
/**
* This is a plugin singleton mostly controlling logging.
*/
public class Activator extends AbstractUIPlugin implements DebugOptionsListener {
/**
* The one and only instance
*/
private static Activator plugin;
/**
* Property listeners for plugin specific events
*/
private static List<IPropertyChangeListener> propertyChangeListeners =
new ArrayList<>(5);
/**
* Property constant indicating the decorator configuration has changed.
*/
public static final String DECORATORS_CHANGED = "org.eclipse.egit.ui.DECORATORS_CHANGED"; //$NON-NLS-1$
/**
* @return the {@link Activator} singleton.
*/
public static Activator getDefault() {
return plugin;
}
/**
* @return the id of the egit ui plugin
*/
public static String getPluginId() {
return getDefault().getBundle().getSymbolicName();
}
/**
* Creates an {@link IStatus} from the parameters. If the throwable is an
* {@link InvocationTargetException}, the status is created from the first
* exception that is either not an InvocationTargetException or that has a
* message. If the message passed is empty, tries to supply a message from
* that exception.
*
* @param severity
* of the {@link IStatus}
* @param message
* for the status
* @param throwable
* that caused the status, may be {@code null}
* @return the status
*/
private static IStatus toStatus(int severity, String message,
Throwable throwable) {
Throwable exc = throwable;
while (exc instanceof InvocationTargetException) {
String msg = exc.getLocalizedMessage();
if (msg != null && !msg.isEmpty()) {
break;
}
Throwable cause = exc.getCause();
if (cause == null) {
break;
}
exc = cause;
}
if (exc != null && (message == null || message.isEmpty())) {
message = exc.getLocalizedMessage();
}
return new Status(severity, getPluginId(), message, exc);
}
/**
* Handle an error. The error is logged. If <code>show</code> is
* <code>true</code> the error is shown to the user.
*
* @param message
* a localized message
* @param throwable
* @param show
*/
public static void handleError(String message, Throwable throwable,
boolean show) {
handleIssue(IStatus.ERROR, message, throwable, show);
}
/**
* Handle an issue. The issue is logged. If <code>show</code> is
* <code>true</code> the issue is shown to the user.
*
* @param severity
* status severity, use constants defined in {@link IStatus}
* @param message
* a localized message
* @param throwable
* @param show
* @since 2.2
*/
public static void handleIssue(int severity, String message, Throwable throwable,
boolean show) {
IStatus status = toStatus(severity, message, throwable);
int style = StatusManager.LOG;
if (show)
style |= StatusManager.SHOW;
StatusManager.getManager().handle(status, style);
}
/**
* Shows an error. The error is NOT logged.
*
* @param message
* a localized message
* @param throwable
*/
public static void showError(String message, Throwable throwable) {
IStatus status = toStatus(IStatus.ERROR, message, throwable);
StatusManager.getManager().handle(status, StatusManager.SHOW);
}
/**
* Shows an error. The error is NOT logged.
*
* @param message
* a localized message
* @param status
*/
public static void showErrorStatus(String message, IStatus status) {
StatusManager.getManager().handle(status, StatusManager.SHOW);
}
/**
* @param message
* @param e
*/
public static void logError(String message, Throwable e) {
handleError(message, e, false);
}
/**
* @param message
* @param e
*/
public static void error(String message, Throwable e) {
handleError(message, e, false);
}
/**
* Creates an error status
*
* @param message
* a localized message
* @param throwable
* @return a new Status object
*/
public static IStatus createErrorStatus(String message,
Throwable throwable) {
return toStatus(IStatus.ERROR, message, throwable);
}
/**
* Creates an error status
*
* @param message
* a localized message
* @return a new Status object
*/
public static IStatus createErrorStatus(String message) {
return toStatus(IStatus.ERROR, message, null);
}
/**
* Get the theme used by this plugin.
*
* @return our theme.
*/
public static ITheme getTheme() {
return plugin.getWorkbench().getThemeManager().getCurrentTheme();
}
/**
* Get a font known to this plugin.
*
* @param id
* one of our THEME_* font preference ids (see
* {@link UIPreferences});
* @return the configured font, borrowed from the registry.
*/
public static Font getFont(final String id) {
return getTheme().getFontRegistry().get(id);
}
/**
* Get a font known to this plugin, but with bold style applied over top.
*
* @param id
* one of our THEME_* font preference ids (see
* {@link UIPreferences});
* @return the configured font, borrowed from the registry.
*/
public static Font getBoldFont(final String id) {
return getTheme().getFontRegistry().getBold(id);
}
private ResourceManager resourceManager;
private RepositoryChangeScanner rcs;
private ResourceRefreshJob refreshJob;
private ListenerHandle refreshHandle;
private DebugOptions debugOptions;
private volatile boolean uiIsActive;
private IWindowListener focusListener;
/**
* Construct the {@link Activator} egit ui plugin singleton instance
*/
public Activator() {
Activator.setActivator(this);
}
private static void setActivator(Activator a) {
plugin = a;
}
@Override
public void start(final BundleContext context) throws Exception {
super.start(context);
// we want to be notified about debug options changes
Dictionary<String, String> props = new Hashtable<>(4);
props.put(DebugOptions.LISTENER_SYMBOLICNAME, context.getBundle()
.getSymbolicName());
context.registerService(DebugOptionsListener.class.getName(), this,
props);
setupRepoChangeScanner();
setupRepoIndexRefresh();
setupFocusHandling();
setupCredentialsProvider();
ConfigurationChecker.checkConfiguration();
registerTemplateVariableResolvers();
}
private void setupCredentialsProvider() {
CredentialsProvider.setDefault(new EGitCredentialsProvider());
}
private void registerTemplateVariableResolvers() {
if (hasJavaPlugin()) {
final ContextTypeRegistry codeTemplateContextRegistry = JavaPlugin
.getDefault().getCodeTemplateContextRegistry();
final Iterator<?> ctIter = codeTemplateContextRegistry
.contextTypes();
while (ctIter.hasNext()) {
final TemplateContextType contextType = (TemplateContextType) ctIter
.next();
contextType
.addResolver(new GitTemplateVariableResolver(
"git_config", //$NON-NLS-1$
UIText.GitTemplateVariableResolver_GitConfigDescription));
}
}
}
/**
* @return true if at least one Eclipse window is active
*/
static boolean isActive() {
return getDefault().uiIsActive;
}
private void setupFocusHandling() {
focusListener = new IWindowListener() {
private void updateUiState() {
Display.getCurrent().asyncExec(new Runnable() {
@Override
public void run() {
boolean wasActive = uiIsActive;
uiIsActive = Display.getCurrent().getActiveShell() != null;
if (uiIsActive != wasActive
&& GitTraceLocation.REPOSITORYCHANGESCANNER
.isActive())
traceUiIsActive();
}
private void traceUiIsActive() {
StringBuilder message = new StringBuilder(
"workbench is "); //$NON-NLS-1$
message.append(uiIsActive ? "active" : "inactive"); //$NON-NLS-1$//$NON-NLS-2$
GitTraceLocation.getTrace().trace(
GitTraceLocation.REPOSITORYCHANGESCANNER
.getLocation(), message.toString());
}
});
}
@Override
public void windowOpened(IWorkbenchWindow window) {
updateUiState();
}
@Override
public void windowDeactivated(IWorkbenchWindow window) {
updateUiState();
}
@Override
public void windowClosed(IWorkbenchWindow window) {
updateUiState();
}
@Override
public void windowActivated(IWorkbenchWindow window) {
updateUiState();
// 500: give the UI task a chance to update the active state
rcs.schedule(500);
refreshJob.triggerRefresh();
}
};
Job job = new Job(UIText.Activator_setupFocusListener) {
@Override
protected IStatus run(IProgressMonitor monitor) {
if (PlatformUI.isWorkbenchRunning())
PlatformUI.getWorkbench().addWindowListener(focusListener);
else
schedule(1000L);
return Status.OK_STATUS;
}
};
job.schedule();
}
@Override
public void optionsChanged(DebugOptions options) {
// initialize the trace stuff
debugOptions = options;
GitTraceLocation.initializeFromOptions(options, isDebugging());
}
/**
* @return the {@link DebugOptions}
*/
public DebugOptions getDebugOptions() {
return debugOptions;
}
private void setupRepoIndexRefresh() {
refreshJob = new ResourceRefreshJob();
refreshHandle = Repository.getGlobalListenerList()
.addIndexChangedListener(refreshJob);
}
/**
* Register for changes made to Team properties.
*
* @param listener
* The listener to register
*/
public static synchronized void addPropertyChangeListener(
IPropertyChangeListener listener) {
propertyChangeListeners.add(listener);
}
/**
* Remove a Team property changes.
*
* @param listener
* The listener to remove
*/
public static synchronized void removePropertyChangeListener(
IPropertyChangeListener listener) {
propertyChangeListeners.remove(listener);
}
/**
* Broadcast a Team property change.
*
* @param event
* The event to broadcast
*/
public static synchronized void broadcastPropertyChange(PropertyChangeEvent event) {
for (IPropertyChangeListener listener : propertyChangeListeners)
listener.propertyChange(event);
}
/**
* Refresh projects in repositories that we suspect may have resource
* changes.
*/
static class ResourceRefreshJob extends Job implements
IndexChangedListener {
ResourceRefreshJob() {
super(UIText.Activator_refreshJobName);
setUser(false);
setSystem(true);
}
private Set<Repository> repositoriesChanged = new LinkedHashSet<>();
@Override
public IStatus run(IProgressMonitor monitor) {
Set<Repository> repos;
synchronized (repositoriesChanged) {
if (repositoriesChanged.isEmpty()) {
return Status.OK_STATUS;
}
repos = new LinkedHashSet<>(repositoriesChanged);
repositoriesChanged.clear();
}
IWorkspace workspace = ResourcesPlugin.getWorkspace();
IProject[] projects = workspace.getRoot().getProjects();
final Set<IProject> toRefresh = new LinkedHashSet<>();
for (IProject p : projects) {
if (!p.isAccessible()) {
continue;
}
RepositoryMapping mapping = RepositoryMapping.getMapping(p);
if (mapping != null
&& repos.contains(mapping.getRepository())) {
toRefresh.add(p);
}
}
if (toRefresh.isEmpty()) {
return Status.OK_STATUS;
}
try {
workspace.run(new IWorkspaceRunnable() {
@Override
public void run(IProgressMonitor m) throws CoreException {
SubMonitor subMonitor = SubMonitor.convert(m,
UIText.Activator_refreshingProjects,
toRefresh.size());
for (IProject p : toRefresh) {
if (subMonitor.isCanceled()) {
return;
}
ISchedulingRule rule = p.getWorkspace().getRuleFactory().refreshRule(p);
try {
getJobManager().beginRule(rule, subMonitor);
// handle missing projects after branch switch
if (p.isAccessible()) {
p.refreshLocal(IResource.DEPTH_INFINITE,
subMonitor.newChild(1));
}
} catch (CoreException e) {
handleError(UIText.Activator_refreshFailed, e, false);
} finally {
getJobManager().endRule(rule);
}
}
}
}, workspace.getRuleFactory().refreshRule(workspace.getRoot()),
IWorkspace.AVOID_UPDATE, monitor);
} catch (CoreException e) {
handleError(UIText.Activator_refreshFailed, e, false);
return new Status(IStatus.ERROR, getPluginId(), e.getMessage());
}
if (!monitor.isCanceled()) {
// re-schedule if we got some changes in the meantime
synchronized (repositoriesChanged) {
if (!repositoriesChanged.isEmpty()) {
schedule(100);
}
}
}
monitor.done();
return Status.OK_STATUS;
}
@Override
public void onIndexChanged(IndexChangedEvent e) {
if (Activator.getDefault().getPreferenceStore()
.getBoolean(UIPreferences.REFESH_ON_INDEX_CHANGE)) {
mayTriggerRefresh(e);
}
}
/**
* Record which projects have changes. Initiate a resource refresh job
* if the user settings allow it.
*
* @param e
* The {@link RepositoryEvent} that triggered this refresh
*/
private void mayTriggerRefresh(RepositoryEvent e) {
synchronized (repositoriesChanged) {
repositoriesChanged.add(e.getRepository());
}
if (!Activator.getDefault().getPreferenceStore()
.getBoolean(UIPreferences.REFESH_ONLY_WHEN_ACTIVE)
|| isActive()) {
triggerRefresh();
}
}
/**
* Figure which projects belong to a repository, add them to a set of
* project to refresh and schedule the refresh as a job.
*/
void triggerRefresh() {
if (GitTraceLocation.REPOSITORYCHANGESCANNER.isActive()) {
GitTraceLocation.getTrace().trace(
GitTraceLocation.REPOSITORYCHANGESCANNER.getLocation(),
"Triggered refresh"); //$NON-NLS-1$
}
schedule();
}
}
/**
* A Job that looks at the repository meta data and triggers a refresh of
* the resources in the affected projects.
*/
private static class RepositoryChangeScanner extends Job
implements IPropertyChangeListener {
// volatile in order to ensure thread synchronization
private volatile boolean doReschedule;
private int interval;
private final RepositoryCache repositoryCache;
RepositoryChangeScanner() {
super(UIText.Activator_repoScanJobName);
setRule(new RepositoryCacheRule());
setSystem(true);
setUser(false);
repositoryCache = org.eclipse.egit.core.Activator.getDefault()
.getRepositoryCache();
updateRefreshInterval();
}
@Override
public boolean shouldSchedule() {
return doReschedule;
}
@Override
public boolean shouldRun() {
return doReschedule;
}
void setReschedule(boolean reschedule){
doReschedule = reschedule;
}
@Override
protected IStatus run(IProgressMonitor monitor) {
// When people use Git from the command line a lot of changes
// may happen. Don't scan when inactive depending on the user's
// choice.
if (getDefault().getPreferenceStore()
.getBoolean(UIPreferences.REFESH_ONLY_WHEN_ACTIVE)) {
if (!isActive()) {
monitor.done();
return Status.OK_STATUS;
}
}
Repository[] repos = repositoryCache.getAllRepositories();
if (repos.length == 0) {
return Status.OK_STATUS;
}
monitor.beginTask(UIText.Activator_scanningRepositories,
repos.length);
try {
for (Repository repo : repos) {
if (monitor.isCanceled()) {
break;
}
if (GitTraceLocation.REPOSITORYCHANGESCANNER.isActive()) {
GitTraceLocation.getTrace().trace(
GitTraceLocation.REPOSITORYCHANGESCANNER
.getLocation(),
"Scanning " + repo + " for changes"); //$NON-NLS-1$ //$NON-NLS-2$
}
repo.scanForRepoChanges();
monitor.worked(1);
}
} catch (IOException e) {
if (GitTraceLocation.REPOSITORYCHANGESCANNER.isActive()) {
GitTraceLocation.getTrace().trace(
GitTraceLocation.REPOSITORYCHANGESCANNER
.getLocation(),
"Stopped rescheduling " + getName() + "job"); //$NON-NLS-1$ //$NON-NLS-2$
}
return createErrorStatus(UIText.Activator_scanError, e);
} finally {
monitor.done();
}
if (GitTraceLocation.REPOSITORYCHANGESCANNER.isActive()) {
GitTraceLocation.getTrace().trace(
GitTraceLocation.REPOSITORYCHANGESCANNER.getLocation(),
"Rescheduling " + getName() + " job"); //$NON-NLS-1$ //$NON-NLS-2$
}
schedule(interval);
return Status.OK_STATUS;
}
@Override
public void propertyChange(PropertyChangeEvent event) {
if (!UIPreferences.REFESH_INDEX_INTERVAL
.equals(event.getProperty())) {
return;
}
updateRefreshInterval();
}
private void updateRefreshInterval() {
interval = getRefreshIndexInterval();
setReschedule(interval > 0);
cancel();
schedule(interval);
}
/**
* @return interval in milliseconds for automatic index check, 0 is if
* check should be disabled
*/
private static int getRefreshIndexInterval() {
return 1000 * getDefault().getPreferenceStore()
.getInt(UIPreferences.REFESH_INDEX_INTERVAL);
}
}
private void setupRepoChangeScanner() {
rcs = new RepositoryChangeScanner();
getPreferenceStore().addPropertyChangeListener(rcs);
}
@Override
public void stop(final BundleContext context) throws Exception {
if (refreshHandle != null) {
refreshHandle.remove();
refreshHandle = null;
}
if (focusListener != null) {
if (PlatformUI.isWorkbenchRunning()) {
PlatformUI.getWorkbench().removeWindowListener(focusListener);
}
focusListener = null;
}
if (GitTraceLocation.REPOSITORYCHANGESCANNER.isActive()) {
GitTraceLocation.getTrace().trace(
GitTraceLocation.REPOSITORYCHANGESCANNER.getLocation(),
"Trying to cancel " + rcs.getName() + " job"); //$NON-NLS-1$ //$NON-NLS-2$
}
getPreferenceStore().removePropertyChangeListener(rcs);
rcs.setReschedule(false);
rcs.cancel();
if (GitTraceLocation.REPOSITORYCHANGESCANNER.isActive()) {
GitTraceLocation.getTrace().trace(
GitTraceLocation.REPOSITORYCHANGESCANNER.getLocation(),
"Trying to cancel " + refreshJob.getName() + " job"); //$NON-NLS-1$ //$NON-NLS-2$
}
refreshJob.cancel();
rcs.join();
refreshJob.join();
if (GitTraceLocation.REPOSITORYCHANGESCANNER.isActive()) {
GitTraceLocation.getTrace().trace(
GitTraceLocation.REPOSITORYCHANGESCANNER.getLocation(),
"Jobs terminated"); //$NON-NLS-1$
}
if (resourceManager != null) {
resourceManager.dispose();
resourceManager = null;
}
super.stop(context);
plugin = null;
}
@Override
protected void saveDialogSettings() {
KnownHosts.store();
super.saveDialogSettings();
}
/**
* @return the {@link RepositoryUtil} instance
*/
public RepositoryUtil getRepositoryUtil() {
return org.eclipse.egit.core.Activator.getDefault().getRepositoryUtil();
}
/**
* Gets this plugin's {@link ResourceManager}.
*
* @return the {@link ResourceManager} of this plugin
*/
public synchronized ResourceManager getResourceManager() {
if (resourceManager == null) {
Display display = PlatformUI.getWorkbench().getDisplay();
if (display == null) {
// Workbench already closed?
throw new IllegalStateException();
}
resourceManager = new LocalResourceManager(JFaceResources
.getResources(display));
}
return resourceManager;
}
/**
* @return true if the Java Plugin is loaded
*/
public static final boolean hasJavaPlugin() {
try {
return Class.forName("org.eclipse.jdt.internal.ui.JavaPlugin") != null; //$NON-NLS-1$
} catch (ClassNotFoundException e) {
return false;
}
}
}