blob: 472bcd309de0c2b6a23cbf630af665971f5b0b0d [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.internal.ui.synchronize;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialogWithToggle;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.osgi.util.NLS;
import org.eclipse.team.core.TeamException;
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.registry.SynchronizeParticipantDescriptor;
import org.eclipse.team.internal.ui.registry.SynchronizeParticipantRegistry;
import org.eclipse.team.internal.ui.registry.SynchronizeWizardDescription;
import org.eclipse.team.internal.ui.registry.SynchronizeWizardRegistry;
import org.eclipse.team.ui.synchronize.ISynchronizeManager;
import org.eclipse.team.ui.synchronize.ISynchronizeParticipant;
import org.eclipse.team.ui.synchronize.ISynchronizeParticipantDescriptor;
import org.eclipse.team.ui.synchronize.ISynchronizeParticipantListener;
import org.eclipse.team.ui.synchronize.ISynchronizeParticipantReference;
import org.eclipse.team.ui.synchronize.ISynchronizeView;
import org.eclipse.team.ui.synchronize.ModelSynchronizeParticipant;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.IPerspectiveDescriptor;
import org.eclipse.ui.IPerspectiveRegistry;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.Saveable;
import org.eclipse.ui.WorkbenchException;
import org.eclipse.ui.XMLMemento;
/**
* Manages the registered synchronize participants. It handles notification of
* participant lifecycles, creation of <code>static</code> participants, management
* of dynamic participants, and the re-creation of persisted participants.
* <p>
* A participant is defined in a plugin manifest and can have several properties:
* - static: means that they always exist and don't have to be added to the manager
* - dynamic: will be added to the manager at some later time
*
* Part (title, id, icon, composite) - described in plugin.xml (IPartInstance)
* Can have multiple parts of the same type at runtime -> (IPart)
* - must acquire a part (IPartInstance.createPart())
* - must released to part when done (IPartInstance.releasePart())
* Some parts can added dynamically to the registry and events are fired to listeners. Listeners can create the newly added part via
* the #createPart() method.
* Parts can be persisted/restored with some state
*
*
*
* Lifecycle:
* startup -> registry read and stored in a participant instance
* createParticipant(id) ->
* releaseParticipant(IParticipantDescriptor) ->
* getParticipantRegistry -> return IParticipantDescriptors that describe the participants
* shutdown -> persist all settings
*
* @see ISynchronizeView
* @see ISynchronizeParticipant
* @since 3.0
*/
public class SynchronizeManager implements ISynchronizeManager {
/**
* Synchronize participants listeners
*/
private ListenerList<ISynchronizeParticipantListener> fListeners = null;
/**
* Contains the participant descriptions
*/
private SynchronizeParticipantRegistry participantRegistry = new SynchronizeParticipantRegistry();
/**
* Contains the synchronize wizard descriptions
*/
private SynchronizeWizardRegistry wizardRegistry = new SynchronizeWizardRegistry();
/**
* Contains a table of the state saved between sessions for a participant. The set is keyed
* as such {String key -> ISynchronizeParticipantReference}.
*/
private Map<String, ISynchronizeParticipantReference> participantReferences = Collections.synchronizedMap(new HashMap<>(10));
// change notification constants
private final static int ADDED = 1;
private final static int REMOVED = 2;
// save context constants
private final static String CTX_PARTICIPANTS = "syncparticipants"; //$NON-NLS-1$
private final static String CTX_PARTICIPANT = "participant"; //$NON-NLS-1$
private final static String CTX_ID = "id"; //$NON-NLS-1$
private final static String CTX_SECONDARY_ID = "secondary_id"; //$NON-NLS-1$
private final static String CTX_PARTICIPANT_DISPLAY_NAME = "displayName"; //$NON-NLS-1$
private final static String CTX_PARTICIPANT_DATA = "data"; //$NON-NLS-1$
private final static String FILENAME = "syncParticipants.xml"; //$NON-NLS-1$
/**
* Notifies a participant listeners of additions or removals of participant references.
*/
class SynchronizeViewPageNotifier implements ISafeRunnable {
private ISynchronizeParticipantListener fListener;
private int fType;
private ISynchronizeParticipant[] fChanged;
@Override
public void handleException(Throwable exception) {
TeamUIPlugin.log(IStatus.ERROR, TeamUIMessages.SynchronizeManager_7, exception);
}
@Override
public void run() throws Exception {
switch (fType) {
case ADDED :
fListener.participantsAdded(fChanged);
break;
case REMOVED :
fListener.participantsRemoved(fChanged);
break;
}
}
/**
* Notifies the given listener of the adds/removes
* @param participants the participants that changed
* @param update the type of change
*/
public void notify(ISynchronizeParticipant[] participants, int update) {
if (fListeners == null) {
return;
}
fChanged = participants;
fType = update;
Object[] copiedListeners = fListeners.getListeners();
for (Object copiedListener : copiedListeners) {
fListener = (ISynchronizeParticipantListener) copiedListener;
SafeRunner.run(this);
}
fChanged = null;
fListener = null;
}
}
/**
* Represents a participant instance and allows lazy initialization of the instance
* only when the participant is required.
*/
private class ParticipantInstance implements ISynchronizeParticipantReference {
private Map<String, ISynchronizeParticipant> participants;
private IMemento savedState;
private SynchronizeParticipantDescriptor descriptor;
private String secondaryId;
private String displayName;
private boolean dead;
public ParticipantInstance(SynchronizeParticipantDescriptor descriptor, String secondaryId, String displayName, IMemento savedState) {
this.participants = new HashMap<>();
this.secondaryId = secondaryId;
this.savedState = savedState;
this.descriptor = descriptor;
this.displayName = displayName;
}
public void save(IMemento memento) {
if (dead) return;
String key = Utils.getKey(descriptor.getId(), getSecondaryId());
ISynchronizeParticipant ref = participants.get(key);
if(ref != null) {
ref.saveState(memento);
} else if(savedState != null) {
memento.putMemento(savedState);
}
}
@Override
public boolean equals(Object other) {
if(other == this) return true;
if (! (other instanceof ISynchronizeParticipantReference)) return false;
ISynchronizeParticipantReference otherRef = (ISynchronizeParticipantReference) other;
String otherSecondaryId = otherRef.getSecondaryId();
return otherRef.getId().equals(getId()) && Utils.equalObject(getSecondaryId(), otherSecondaryId);
}
@Override
public String getId() {
return descriptor.getId();
}
@Override
public String getSecondaryId() {
return secondaryId;
}
@Override
public String getDisplayName() {
String key = Utils.getKey(descriptor.getId(), getSecondaryId());
ISynchronizeParticipant participant = participants.get(key);
if(participant != null) {
return participant.getName();
}
return displayName != null ? displayName : descriptor.getName();
}
public boolean isInstantiated() {
String key = Utils.getKey(descriptor.getId(), getSecondaryId());
return participants.get(key) != null;
}
@Override
public ISynchronizeParticipant getParticipant() throws TeamException {
if (dead) return null;
String key = Utils.getKey(descriptor.getId(), getSecondaryId());
try {
ISynchronizeParticipant participant = participants.get(key);
if (participant == null) {
participant = instantiate();
if(participant != null)
participants.put(key, participant);
}
return participant;
} catch (TeamException e) {
TeamUIPlugin.log(e);
participantReferences.remove(key);
throw new TeamException(TeamUIMessages.SynchronizeManager_8, e);
}
}
public void setParticipant(ISynchronizeParticipant participant) {
String key = Utils.getKey(descriptor.getId(), getSecondaryId());
participants.put(key, participant);
}
@Override
public ISynchronizeParticipantDescriptor getDescriptor() {
return descriptor;
}
private ISynchronizeParticipant instantiate() throws TeamException {
try {
ISynchronizeParticipant participant = (ISynchronizeParticipant) TeamUIPlugin.createExtension(descriptor.getConfigurationElement(), SynchronizeParticipantDescriptor.ATT_CLASS);
participant.setInitializationData(descriptor.getConfigurationElement(), null, null);
participant.init(getSecondaryId(), savedState);
savedState = null;
return participant;
} catch (PartInitException e) {
throw new TeamException(NLS.bind(TeamUIMessages.SynchronizeManager_11, new String[] { descriptor.getName() }), e);
} catch (CoreException e) {
throw TeamException.asTeamException(e);
} catch(Exception e) {
throw new TeamException(NLS.bind(TeamUIMessages.SynchronizeManager_11, new String[] { descriptor.getName() }), e);
}
}
/**
* Dispose of the reference
*/
public void dispose() {
try {
ISynchronizeParticipant participant = getParticipant();
if (participant != null)
participant.dispose();
} catch (TeamException e) {
// Ignore since we are disposing anyway;
} finally {
dead = true;
}
}
}
public SynchronizeManager() {
init();
}
@Override
public void addSynchronizeParticipantListener(ISynchronizeParticipantListener listener) {
if (fListeners == null) {
fListeners = new ListenerList<>(ListenerList.IDENTITY);
}
fListeners.add(listener);
}
@Override
public void removeSynchronizeParticipantListener(ISynchronizeParticipantListener listener) {
if (fListeners != null) {
fListeners.remove(listener);
}
}
/**
* Creates a new participant reference with of the provided type. If the secondayId is specified it
* is used as the qualifier for multiple instances of the same type.
* <p>
* The returned participant reference is a light weight handle describing the participant. The plug-in
* defining the participant is not loaded. To instantiate a participant a client must call
* {@link ISynchronizeParticipantReference#createParticipant()} and must call
* {@link ISynchronizeParticipantReference#releaseParticipant()} when finished with the participant.
* </p>
* @param type the type of the participant
* @param secondaryId a unique id for multiple instance support
* @return a reference to a participant
*/
private ParticipantInstance createParticipantReference(String type, String secondaryId, String displayName) throws PartInitException {
SynchronizeParticipantDescriptor desc = participantRegistry.find(type);
// ensure that the view id is valid
if (desc == null)
throw new PartInitException(NLS.bind(TeamUIMessages.SynchronizeManager_19, new String[] { type }));
// ensure that multiple instances are allowed if a secondary id is given
if (secondaryId != null) {
// if (!desc.isMultipleInstances()) {
// throw new PartInitException(Policy.bind("SynchronizeManager.20", type)); //$NON-NLS-1$
// }
}
String key = Utils.getKey(type, secondaryId);
ParticipantInstance ref = (ParticipantInstance) participantReferences.get(key);
if (ref == null) {
ref = new ParticipantInstance(desc, secondaryId, displayName, null);
}
return ref;
}
@Override
public synchronized void addSynchronizeParticipants(ISynchronizeParticipant[] participants) {
// renamed to createSynchronizeParticipant(id)
List<ISynchronizeParticipant> added = new ArrayList<>(participants.length);
for (ISynchronizeParticipant participant : participants) {
String key = Utils.getKey(participant.getId(), participant.getSecondaryId());
if(! participantReferences.containsKey(key)) {
try {
ParticipantInstance ref = createParticipantReference(participant.getId(), participant.getSecondaryId(), participant.getName());
ref.setParticipant(participant);
removeMatchingParticipant(participant.getId());
participantReferences.put(key, ref);
added.add(participant);
} catch (PartInitException e) {
TeamUIPlugin.log(e);
continue;
}
}
}
if (!added.isEmpty()) {
saveState();
fireUpdate(added.toArray(new ISynchronizeParticipant[added.size()]), ADDED);
}
}
private void removeMatchingParticipant(String id) {
ISynchronizeParticipantReference[] refs = get(id);
if (refs.length > 0) {
// Find an un-pinned participant and replace it
for (ISynchronizeParticipantReference reference : refs) {
ISynchronizeParticipant p;
try {
p = reference.getParticipant();
if (!p.isPinned() && !isDirty(p)) {
removeSynchronizeParticipants(new ISynchronizeParticipant[]{p});
break;
}
} catch (TeamException e) {
continue;
}
}
}
}
private boolean isDirty(ISynchronizeParticipant p) {
if (p instanceof ModelSynchronizeParticipant) {
ModelSynchronizeParticipant msp = (ModelSynchronizeParticipant) p;
Saveable s = msp.getActiveSaveable();
if (s != null && s.isDirty()) {
return true;
}
}
return false;
}
@Override
public synchronized void removeSynchronizeParticipants(ISynchronizeParticipant[] participants) {
List<ISynchronizeParticipant> removed = new ArrayList<>(participants.length);
for (ISynchronizeParticipant participant : participants) {
String key = Utils.getKey(participant.getId(), participant.getSecondaryId());
if(participantReferences.containsKey(key)) {
ParticipantInstance ref = (ParticipantInstance)participantReferences.remove(key);
if(ref.isInstantiated()) {
ref.dispose();
}
removed.add(participant);
}
}
if (!removed.isEmpty()) {
saveState();
fireUpdate(removed.toArray(new ISynchronizeParticipant[removed.size()]), REMOVED);
}
}
@Override
public ISynchronizeParticipantReference get(String id, String secondaryId) {
String key = Utils.getKey(id, secondaryId);
return participantReferences.get(key);
}
@Override
public ISynchronizeParticipantReference[] get(String id) {
ISynchronizeParticipantReference[] refs = getSynchronizeParticipants();
ArrayList<ISynchronizeParticipantReference> refsForId = new ArrayList<>();
for (ISynchronizeParticipantReference reference : refs) {
if(reference.getId().equals(id)) {
refsForId.add(reference);
}
}
return refsForId.toArray(new ISynchronizeParticipantReference[refsForId.size()]);
}
@Override
public synchronized ISynchronizeParticipantReference[] getSynchronizeParticipants() {
return participantReferences.values().toArray(new ISynchronizeParticipantReference[participantReferences.values().size()]);
}
@Override
public ISynchronizeView showSynchronizeViewInActivePage() {
IWorkbench workbench = PlatformUI.getWorkbench();
IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
boolean switchPerspectives = promptForPerspectiveSwitch();
IWorkbenchPage activePage = null;
if(switchPerspectives) {
try {
String pId = TeamUIPlugin.getPlugin().getPreferenceStore().getString(IPreferenceIds.SYNCVIEW_DEFAULT_PERSPECTIVE);
activePage = workbench.showPerspective(pId, window);
} catch (WorkbenchException e) {
Utils.handleError(window.getShell(), e, TeamUIMessages.SynchronizeView_14, e.getMessage());
}
}
try {
if (activePage == null) {
activePage = TeamUIPlugin.getActivePage();
if (activePage == null)
return null;
}
//IViewPart part = activePage.showView(ISynchronizeView.VIEW_ID, Long.toString(System.currentTimeMillis()), IWorkbenchPage.VIEW_ACTIVATE);
IViewPart part = activePage.showView(ISynchronizeView.VIEW_ID);
try {
return (ISynchronizeView) part;
} catch (ClassCastException e) {
// Strange that we cannot cast the part (see bug 53671)
TeamUIPlugin.log(IStatus.ERROR, NLS.bind(TeamUIMessages.SynchronizeManager_18, new String[] { part.getClass().getName() }), e);
return null;
}
} catch (PartInitException pe) {
Utils.handleError(window.getShell(), pe, TeamUIMessages.SynchronizeView_16, pe.getMessage());
return null;
}
}
/**
* Decides what action to take when switching perspectives and showing the synchronize view. Basically there are a
* set of user preferences that control how perspective switching.
*/
private boolean promptForPerspectiveSwitch() {
// Decide if a prompt is even required
IPreferenceStore store = TeamUIPlugin.getPlugin().getPreferenceStore();
String option = store.getString(IPreferenceIds.SYNCHRONIZING_COMPLETE_PERSPECTIVE);
if(option.equals(MessageDialogWithToggle.ALWAYS)) {
return true;
} else if(option.equals(MessageDialogWithToggle.NEVER)) {
return false;
}
// Otherwise determine if a prompt is required
IPerspectiveRegistry registry= PlatformUI.getWorkbench().getPerspectiveRegistry();
String defaultSyncPerspectiveId = store.getString(IPreferenceIds.SYNCVIEW_DEFAULT_PERSPECTIVE);
IPerspectiveDescriptor perspectiveDescriptor = registry.findPerspectiveWithId(defaultSyncPerspectiveId);
IWorkbenchPage page = TeamUIPlugin.getActivePage();
if(page != null) {
IPerspectiveDescriptor p = page.getPerspective();
if(p != null && p.getId().equals(defaultSyncPerspectiveId)) {
// currently in default perspective
return false;
}
}
if(perspectiveDescriptor != null) {
String message;;
String desc = perspectiveDescriptor.getDescription();
if (desc == null) {
message = NLS.bind(TeamUIMessages.SynchronizeManager_30, new String[] { perspectiveDescriptor.getLabel() });
} else {
message = NLS.bind(TeamUIMessages.SynchronizeManager_32, new String[] { perspectiveDescriptor.getLabel(), desc });
}
MessageDialogWithToggle m = MessageDialogWithToggle.openYesNoQuestion(Utils.getShell(null),
TeamUIMessages.SynchronizeManager_27,
message,
TeamUIMessages.SynchronizeManager_31,
false /* toggle state */,
store,
IPreferenceIds.SYNCHRONIZING_COMPLETE_PERSPECTIVE);
int result = m.getReturnCode();
switch (result) {
// yes, ok
case IDialogConstants.YES_ID:
case IDialogConstants.OK_ID :
return true;
// no
case IDialogConstants.NO_ID :
return false;
}
}
return false;
}
/**
* Creates the participant registry and restore any saved participants.
* Will also instantiate any static participants.
*/
public void init() {
try {
// Initialize the participant registry - reads all participant extension descriptions.
participantRegistry.readRegistry(Platform.getExtensionRegistry(), TeamUIPlugin.ID, SynchronizeParticipantRegistry.PT_SYNCPARTICIPANTS);
// Initialize the wizard registry
wizardRegistry.readRegistry(Platform.getExtensionRegistry(), TeamUIPlugin.ID, SynchronizeWizardRegistry.PT_SYNCHRONIZE_WIZARDS);
// Instantiate and register any dynamic participants saved from a
// previous session.
restoreSavedParticipants();
} catch (CoreException e) {
TeamUIPlugin.log(new Status(IStatus.ERROR, TeamUIPlugin.ID, 1, TeamUIMessages.SynchronizeManager_8, e));
}
}
/**
* Allow participant instances to clean-up.
*/
public void dispose() {
// save state and settings for existing participants.
saveState();
for (Iterator it = participantReferences.values().iterator(); it.hasNext();) {
ParticipantInstance ref = (ParticipantInstance) it.next();
if((ref).isInstantiated()) {
try {
ref.getParticipant().dispose();
} catch (TeamException e) {
continue;
}
}
}
participantReferences = null;
}
/**
* Restores participants that have been saved between sessions.
*/
private void restoreSavedParticipants() throws CoreException {
File file = getStateFile();
Reader reader;
try {
reader = new BufferedReader(new FileReader(file));
} catch (FileNotFoundException e) {
return;
}
IMemento memento = XMLMemento.createReadRoot(reader);
IMemento[] participantNodes = memento.getChildren(CTX_PARTICIPANT);
for (IMemento memento2 : participantNodes) {
String id = memento2.getString(CTX_ID);
String secondayId = memento2.getString(CTX_SECONDARY_ID);
if (secondayId != null) {
String displayName = memento2.getString(CTX_PARTICIPANT_DISPLAY_NAME);
SynchronizeParticipantDescriptor desc = participantRegistry.find(id);
if (desc != null) {
String key = Utils.getKey(id, secondayId);
participantReferences.put(key, new ParticipantInstance(desc, secondayId, displayName, memento2.getChild(CTX_PARTICIPANT_DATA)));
} else {
TeamUIPlugin.log(new Status(IStatus.ERROR, TeamUIPlugin.ID, 1, NLS.bind(TeamUIMessages.SynchronizeManager_9, new String[] { id }), null));
}
}
}
}
/**
* Saves a file containing the list of participant ids that are registered
* with this manager. Each initialized participant is also given the chance to save
* it's state.
*/
private void saveState() {
XMLMemento xmlMemento = XMLMemento.createWriteRoot(CTX_PARTICIPANTS);
for (Iterator it = participantReferences.values().iterator(); it.hasNext(); ) {
ParticipantInstance ref = (ParticipantInstance) it.next();
// Participants can opt out of being saved between sessions
if(! ref.getDescriptor().isPersistent()) continue;
// Create the state placeholder for a participant
IMemento participantNode = xmlMemento.createChild(CTX_PARTICIPANT);
participantNode.putString(CTX_ID, ref.getId());
String secondaryId = ref.getSecondaryId();
if(secondaryId != null) {
participantNode.putString(CTX_SECONDARY_ID,secondaryId);
}
participantNode.putString(CTX_PARTICIPANT_DISPLAY_NAME, ref.getDisplayName());
IMemento participantData = participantNode.createChild(CTX_PARTICIPANT_DATA);
ref.save(participantData);
}
try {
try (Writer writer = new BufferedWriter(new FileWriter(getStateFile()))) {
xmlMemento.save(writer);
}
} catch (IOException e) {
TeamUIPlugin.log(new Status(IStatus.ERROR, TeamUIPlugin.ID, 1, TeamUIMessages.SynchronizeManager_10, e));
}
}
private File getStateFile() {
IPath pluginStateLocation = TeamUIPlugin.getPlugin().getStateLocation();
return pluginStateLocation.append(FILENAME).toFile();
}
/**
* Fires notification.
*
* @param participants
* participants added/removed
* @param type
* ADDED or REMOVED
* @see SynchronizeManager#ADDED
* @see SynchronizeManager#REMOVED
*/
private void fireUpdate(ISynchronizeParticipant[] participants, int type) {
new SynchronizeViewPageNotifier().notify(participants, type);
}
@Override
public ISynchronizeParticipantDescriptor getParticipantDescriptor(String id) {
return participantRegistry.find(id);
}
public SynchronizeWizardDescription[] getWizardDescriptors() {
return wizardRegistry.getSynchronizeWizards();
}
}