blob: 8e898417a36a02a785008d40fd11984b1bcd2c9f [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2017 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.team.ui.synchronize;
import java.util.Arrays;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.IBasicPropertyConstants;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.team.core.TeamException;
import org.eclipse.team.core.subscribers.Subscriber;
import org.eclipse.team.core.synchronize.SyncInfoFilter;
import org.eclipse.team.core.synchronize.SyncInfoTree;
import org.eclipse.team.internal.core.subscribers.SubscriberSyncInfoCollector;
import org.eclipse.team.internal.ui.IPreferenceIds;
import org.eclipse.team.internal.ui.TeamUIMessages;
import org.eclipse.team.internal.ui.TeamUIPlugin;
import org.eclipse.team.internal.ui.Utils;
import org.eclipse.team.internal.ui.synchronize.IRefreshSubscriberListener;
import org.eclipse.team.internal.ui.synchronize.IRefreshable;
import org.eclipse.team.internal.ui.synchronize.RefreshParticipantJob;
import org.eclipse.team.internal.ui.synchronize.RefreshSubscriberParticipantJob;
import org.eclipse.team.internal.ui.synchronize.RefreshUserNotificationPolicy;
import org.eclipse.team.internal.ui.synchronize.RefreshUserNotificationPolicyInModalDialog;
import org.eclipse.team.internal.ui.synchronize.SubscriberParticipantPage;
import org.eclipse.team.internal.ui.synchronize.SubscriberRefreshSchedule;
import org.eclipse.team.internal.ui.synchronize.SynchronizePageConfiguration;
import org.eclipse.team.ui.TeamUI;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchSite;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.part.IPageBookViewPage;
import org.eclipse.ui.progress.IProgressConstants2;
/**
* A synchronize participant that displays synchronization information for local resources that are
* managed via a {@link Subscriber}. It maintains a dynamic collection of all out-of-sync resources
* by listening to workspace resource changes and remote changes thus creating a live view of
* changes in the workspace.
* <p>
* The subscriber can be configured to be synchronized in the background based on a schedule. This
* effectively refreshes the subscriber and updates the dynamic sync set.
* </p><p>
* Subclasses will typically want to override the following methods:</p>
* <ul>
* <li>initializeConfiguration: participants can add toolbar actions, configure the context menu, decorator.
* <li>saveState and init: persist settings between sessions.
* </ul>
* This class is intended to be subclassed.
* <br><br>
* @since 3.0
*/
public abstract class SubscriberParticipant extends AbstractSynchronizeParticipant implements IPropertyChangeListener {
/*
* Collects and maintains set of all out-of-sync resources of the subscriber
*/
private SubscriberSyncInfoCollector collector;
/*
* Controls the automatic synchronization of this participant
*/
private SubscriberRefreshSchedule refreshSchedule;
/*
* Provides the resource scope for this participant
*/
private ISynchronizeScope scope;
/*
* Key for settings in memento
*/
private static final String CTX_SUBSCRIBER_PARTICIPANT_SETTINGS = TeamUIPlugin.ID + ".TEAMSUBSRCIBERSETTINGS"; //$NON-NLS-1$
/*
* Key for schedule in memento
*/
private static final String CTX_SUBSCRIBER_SCHEDULE_SETTINGS = TeamUIPlugin.ID + ".TEAMSUBSRCIBER_REFRESHSCHEDULE"; //$NON-NLS-1$
/**
* Constructor initializes the schedule. Subclasses must call this method.
*/
public SubscriberParticipant() {
refreshSchedule = new SubscriberRefreshSchedule(createRefreshable());
}
private IRefreshable createRefreshable() {
return new IRefreshable() {
@Override
public RefreshParticipantJob createJob(String interval) {
return new RefreshSubscriberParticipantJob(SubscriberParticipant.this,
TeamUIMessages.RefreshSchedule_14,
NLS.bind(TeamUIMessages.RefreshSchedule_15, new String[] { SubscriberParticipant.this.getName(), interval }), getResources(),
new RefreshUserNotificationPolicy(SubscriberParticipant.this));
}
@Override
public ISynchronizeParticipant getParticipant() {
return SubscriberParticipant.this;
}
@Override
public void setRefreshSchedule(SubscriberRefreshSchedule schedule) {
SubscriberParticipant.this.setRefreshSchedule(schedule);
}
@Override
public SubscriberRefreshSchedule getRefreshSchedule() {
return SubscriberParticipant.this.getRefreshSchedule();
}
};
}
/**
* Constructor which should be called when creating a participant whose resources
* are to be scoped.
*
* @param scope a synchronize scope
*/
public SubscriberParticipant(ISynchronizeScope scope) {
this();
this.scope = scope;
scope.addPropertyChangeListener(this);
}
@Override
public final IPageBookViewPage createPage(ISynchronizePageConfiguration configuration) {
validateConfiguration(configuration);
return new SubscriberParticipantPage(configuration, getSubscriberSyncInfoCollector());
}
/**
* Returns the resources supervised by this participant. It will
* either be the roots of the subscriber or the resource scope
* provided when the subscriber was set.
*
* @return the resources supervised by this participant.
*/
public IResource[] getResources() {
return collector.getRoots();
}
/*
* Set the resources supervised by this participant. If <code>null</code>,
* the participant will include all roots of its subscriber
*
* @param roots the root resources to consider or <code>null</code>
* to consider all roots of the subscriber
*/
private void setResources(IResource[] roots) {
collector.setRoots(roots);
}
/**
* Refresh this participants synchronization state and displays the result in a model dialog.
*
* @param shell parent shell for the dialog
* @param resources the resources to be refreshed.
* @param jobName the job name or <code>null</code>
* @param taskName the task name to be shown to the user
* @param configuration the configuration
* @param site the site in which to run the refresh
*/
public final void refreshInDialog(Shell shell, IResource[] resources, String jobName, String taskName, ISynchronizePageConfiguration configuration, IWorkbenchSite site) {
IRefreshSubscriberListener listener = new RefreshUserNotificationPolicyInModalDialog(shell, taskName, configuration, this);
internalRefresh(resources, jobName, taskName, site, listener);
}
/**
* Refresh a participant in the background the result of the refresh are shown in the progress view. Refreshing
* can also be considered synchronizing, or refreshing the synchronization state. Basically this is a long
* running operation that will update the participants sync info sets with new changes detected on the
* server. Either or both of the <code>shortTaskName</code> and <code>longTaskName</code> can be <code>null</code>
* in which case, the default values for these are returned by the methods <code>getShortTaskName()</code> and
* <code>getLongTaskName(IResource[])</code> will be used.
*
* @param resources the resources to be refreshed.
* @param shortTaskName the taskName of the background job that will run the synchronize or <code>null</code>
* if the default job name is desired.
* @param longTaskName the taskName of the progress monitor running the synchronize or <code>null</code>
* if the default job name is desired.
* @param site the workbench site the synchronize is running from. This can be used to notify the site
* that a job is running.
*/
public final void refresh(IResource[] resources, String shortTaskName, String longTaskName, IWorkbenchSite site) {
IRefreshSubscriberListener listener = new RefreshUserNotificationPolicy(this);
internalRefresh(resources, shortTaskName, longTaskName, site, listener);
}
/**
* Refresh a participant. The returned status describes the result of the refresh.
*
* @param resources the resources to be refreshed.
* @param taskName the task name to be shown to the user
* @param monitor monitor for job progress
* @return a status
*/
public final IStatus refreshNow(IResource[] resources, String taskName, IProgressMonitor monitor) {
Job.getJobManager().cancel(this);
RefreshParticipantJob job = new RefreshSubscriberParticipantJob(this, taskName, taskName, resources, null);
return job.run(monitor);
}
@Override
public void dispose() {
Job.getJobManager().cancel(this);
refreshSchedule.dispose();
TeamUI.removePropertyChangeListener(this);
collector.dispose();
scope.dispose();
}
@Override
public String getName() {
String name = super.getName();
return NLS.bind(TeamUIMessages.SubscriberParticipant_namePattern, new String[] { name, scope.getName() });
}
/**
* Return the name of the participant as specified in the plugin manifest file.
* This method is provided to give access to this name since it is masked by
* the <code>getName()</code> method defined in this class.
* @return the name of the participant as specified in the plugin manifest file
* @since 3.1
*/
protected final String getShortName() {
return super.getName();
}
/**
* Returns the <code>SyncInfoTree</code> for this participant. This set
* contains the out-of-sync resources supervised by this participant.
*
* @return the sync info set that contains the out-of-sync resources
* for this participant.
*/
public SyncInfoTree getSyncInfoSet() {
return getSubscriberSyncInfoCollector().getSyncInfoSet();
}
/**
* Return the <code>Subscriber</code> associated with this this participant. This
* method will only return <code>null</code> if the participant has not been initialized
* yet.
*
* @return the <code>Subscriber</code> associated with this this participant.
*/
public Subscriber getSubscriber() {
if (collector == null) return null;
return collector.getSubscriber();
}
/**
* Returns a participant that matches the given resource scoping
*
* @param ID the type id of participants to match
* @param resources the resources to match in the scope
* @return a participant that matches the given resource scoping
*/
public static SubscriberParticipant getMatchingParticipant(String ID, IResource[] resources) {
ISynchronizeParticipantReference[] refs = TeamUI.getSynchronizeManager().getSynchronizeParticipants();
for (ISynchronizeParticipantReference reference : refs) {
if(reference.getId().equals(ID)) {
SubscriberParticipant p;
try {
p = (SubscriberParticipant)reference.getParticipant();
} catch (TeamException e) {
continue;
}
IResource[] roots = p.getResources();
Arrays.sort(resources, Utils.resourceComparator);
Arrays.sort(roots, Utils.resourceComparator);
if (Arrays.equals(resources, roots)) {
return p;
}
}
}
return null;
}
@Override
public void propertyChange(PropertyChangeEvent event) {
if (event.getProperty().equals(TeamUI.GLOBAL_IGNORES_CHANGED)) {
collector.reset();
}
if (event.getProperty().equals(ISynchronizeScope.ROOTS)) {
setResources(scope.getRoots());
}
if (event.getProperty().equals(ISynchronizeScope.NAME)) {
// Force a name change event, which will cause this classes getName to be called
// and updated with the correct working set name.
firePropertyChange(this, IBasicPropertyConstants.P_TEXT, null, getName());
}
}
@Override
public void init(String secondaryId, IMemento memento) throws PartInitException {
super.init(secondaryId, memento);
if(memento != null) {
IMemento settings = memento.getChild(CTX_SUBSCRIBER_PARTICIPANT_SETTINGS);
if(settings != null) {
SubscriberRefreshSchedule schedule = SubscriberRefreshSchedule.init(settings.getChild(CTX_SUBSCRIBER_SCHEDULE_SETTINGS), createRefreshable());
setRefreshSchedule(schedule);
this.scope = AbstractSynchronizeScope.createScope(settings);
scope.addPropertyChangeListener(this);
}
}
}
@Override
public void saveState(IMemento memento) {
super.saveState(memento);
IMemento settings = memento.createChild(CTX_SUBSCRIBER_PARTICIPANT_SETTINGS);
refreshSchedule.saveState(settings.createChild(CTX_SUBSCRIBER_SCHEDULE_SETTINGS));
AbstractSynchronizeScope.saveScope(scope, settings);
}
/**
* Reset the sync set of the participant by repopulating it from scratch.
*/
public void reset() {
getSubscriberSyncInfoCollector().reset();
}
/**
* Return the <code>SubscriberSyncInfoCollector</code> for the participant.
* This collector maintains the set of all out-of-sync resources for the
* subscriber.
*
* @return the <code>SubscriberSyncInfoCollector</code> for this participant
* @noreference This method is not intended to be referenced by clients.
* @nooverride This method is not intended to be re-implemented or extended
* by clients.
*/
public SubscriberSyncInfoCollector getSubscriberSyncInfoCollector() {
return collector;
}
/**
* @noreference This method is not intended to be referenced by clients.
* @nooverride This method is not intended to be re-implemented or extended by clients.
*/
public void setRefreshSchedule(SubscriberRefreshSchedule schedule) {
if (refreshSchedule != schedule) {
if (refreshSchedule != null) {
refreshSchedule.dispose();
}
this.refreshSchedule = schedule;
}
// Always fir the event since the schedule may have been changed
firePropertyChange(this, AbstractSynchronizeParticipant.P_SCHEDULED, schedule, schedule);
}
/**
* @noreference This method is not intended to be referenced by clients.
* @nooverride This method is not intended to be re-implemented or extended by clients.
*/
public SubscriberRefreshSchedule getRefreshSchedule() {
return refreshSchedule;
}
@Override
protected void initializeConfiguration(ISynchronizePageConfiguration configuration) {
configuration.setProperty(SynchronizePageConfiguration.P_PARTICIPANT_SYNC_INFO_SET, collector.getSyncInfoSet());
}
@Override
public void run(IWorkbenchPart part) {
refresh(getResources(), null, null, part != null ? part.getSite() : null);
}
/**
* Returns the short task name (e.g. no more than 25 characters) to describe the behavior of the
* refresh operation to the user. This is typically shown in the status line when this subscriber is refreshed
* in the background. When refreshed in the foreground, only the long task name is shown.
*
* @return the short task name to show in the status line.
*/
protected String getShortTaskName() {
return TeamUIMessages.Participant_synchronizing;
}
/**
* Returns the long task name to describe the behavior of the
* refresh operation to the user. This is typically shown in the status line when this subscriber is refreshed
* in the background.
*
* @return the long task name
* @deprecated use <code>getLongTaskName(IResource[]) instead</code>
*/
@Deprecated
protected String getLongTaskName() {
return TeamUIMessages.Participant_synchronizing;
}
/**
* Returns the long task name to describe the behavior of the
* refresh operation to the user. This is typically shown in the status line when this subscriber is refreshed
* in the background.
* @param resources resources we are working on to get meaningful task name from
* @return the long task name
* @since 3.1
*/
protected String getLongTaskName(IResource[] resources) {
int resourceCount = 0;
if (getResources().length == resources.length) {
// Assume that the resources are the same as the roots.
// If we are wrong, the message may no mention the specific resources which is OK
ISynchronizeScope scope = getScope();
if (scope instanceof ResourceScope) {
resourceCount = scope.getRoots().length;
}
} else {
resourceCount = resources.length;
}
if (resourceCount == 1) {
return NLS.bind(TeamUIMessages.Participant_synchronizingMoreDetails, new String[] { getShortName(), resources[0].getFullPath().toString() });
} else if (resourceCount > 1) {
return NLS.bind(TeamUIMessages.Participant_synchronizingResources, new String[] { getShortName(), Integer.toString(resourceCount) });
}
// A resource count of zero means that it is a non-resource scope so we can print the scope name
return NLS.bind(TeamUIMessages.Participant_synchronizingDetails, new String[] { getName() });
}
/**
* This method is invoked before the given configuration is used to
* create the page (see <code>createPage(ISynchronizePageConfiguration)</code>).
* The configuration would have been initialized by
* <code>initializeConfiguration(ISynchronizePageConfiguration)</code>
* but may have also been tailored further. This method gives the participant
* a chance to validate those changes before the page is created.
*
* @param configuration the page configuration that is about to be used to create a page.
*/
protected void validateConfiguration(ISynchronizePageConfiguration configuration) {
// Do nothing by default
}
/**
* Subclasses must call this method to initialize the participant. Typically this
* method is called in {@link #init(String, IMemento)}. This method will initialize
* the sync info collector.
*
* @param subscriber the subscriber to associate with this participant.
*/
protected void setSubscriber(Subscriber subscriber) {
if (scope == null) {
scope = new WorkspaceScope();
}
collector = new SubscriberSyncInfoCollector(subscriber, scope.getRoots());
// listen for global ignore changes
TeamUI.addPropertyChangeListener(this);
// Start collecting changes
collector.start();
// Start the refresh now that a subscriber has been added
SubscriberRefreshSchedule schedule = getRefreshSchedule();
if(schedule.isEnabled()) {
getRefreshSchedule().startJob();
}
}
/**
* Provide a filter that is used to filter the contents of the sync info set for the participant. Normally, all out-of-sync
* resources from the subscriber will be included in the participant's set. However, a filter can be used to exclude
* some of these out-of-sync resources, if desired.
* <p>
* Subclasses can invoke this method any time after <code>setSubscriber</code> has been invoked.
* </p>
* @param filter a sync info filter
*/
protected void setSyncInfoFilter(SyncInfoFilter filter) {
collector.setFilter(filter);
}
/*
* Create and schedule a subscriber refresh job.
*
* @param resources resources to be synchronized
* @param taskName the task name to be shown to the user
* @param site the site in which to run the refresh
* @param listener the listener to handle the refresh workflow
*/
private void internalRefresh(IResource[] resources, String jobName, String taskName, IWorkbenchSite site, IRefreshSubscriberListener listener) {
if (jobName == null)
jobName = getShortTaskName();
if (taskName == null)
taskName = getLongTaskName(resources);
Job.getJobManager().cancel(this);
RefreshParticipantJob job = new RefreshSubscriberParticipantJob(this, jobName, taskName, resources, listener);
job.setUser(true);
job.setProperty(IProgressConstants2.SHOW_IN_TASKBAR_ICON_PROPERTY, Boolean.TRUE);
Utils.schedule(job, site);
// Remember the last participant synchronized
TeamUIPlugin.getPlugin().getPreferenceStore().setValue(IPreferenceIds.SYNCHRONIZING_DEFAULT_PARTICIPANT, getId());
TeamUIPlugin.getPlugin().getPreferenceStore().setValue(IPreferenceIds.SYNCHRONIZING_DEFAULT_PARTICIPANT_SEC_ID, getSecondaryId());
}
/**
* Return the scope that defines the resources displayed by this participant.
*
* @return Returns the scope.
*/
public ISynchronizeScope getScope() {
return scope;
}
@SuppressWarnings("unchecked")
@Override
public <T> T getAdapter(Class<T> adapter) {
if (adapter == IRefreshable.class && refreshSchedule != null) {
return (T) refreshSchedule.getRefreshable();
}
return super.getAdapter(adapter);
}
}