com.microsoft.tfs.client.common.ui.teamexplorer.internal.pendingchanges.PendingChangesViewModel.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.tfs.client.common.ui.teamexplorer.internal.pendingchanges.PendingChangesViewModel.java

Source

// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See License.txt in the repository root.

package com.microsoft.tfs.client.common.ui.teamexplorer.internal.pendingchanges;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.swt.widgets.Shell;

import com.microsoft.tfs.checkinpolicies.ExtensionPointPolicyLoader;
import com.microsoft.tfs.client.common.commands.QueryLocalWorkspacesCommand;
import com.microsoft.tfs.client.common.commands.wit.QueryWorkItemsCommand;
import com.microsoft.tfs.client.common.framework.command.CommandExecutor;
import com.microsoft.tfs.client.common.framework.command.ICommandExecutor;
import com.microsoft.tfs.client.common.repository.TFSRepository;
import com.microsoft.tfs.client.common.repository.cache.pendingchange.PendingChangeCacheAdapter;
import com.microsoft.tfs.client.common.repository.cache.pendingchange.PendingChangeCacheEvent;
import com.microsoft.tfs.client.common.server.ServerManagerEvent;
import com.microsoft.tfs.client.common.server.ServerManagerListener;
import com.microsoft.tfs.client.common.server.TFSServer;
import com.microsoft.tfs.client.common.ui.TFSCommonUIClientPlugin;
import com.microsoft.tfs.client.common.ui.controls.vc.checkinpolicies.PolicyFailureData;
import com.microsoft.tfs.client.common.ui.framework.command.UICommandExecutorFactory;
import com.microsoft.tfs.client.common.ui.productplugin.TFSProductPlugin;
import com.microsoft.tfs.core.checkinpolicies.PolicyContext;
import com.microsoft.tfs.core.checkinpolicies.PolicyContextKeys;
import com.microsoft.tfs.core.checkinpolicies.PolicyEvaluationCancelledException;
import com.microsoft.tfs.core.checkinpolicies.PolicyEvaluator;
import com.microsoft.tfs.core.checkinpolicies.PolicyEvaluatorState;
import com.microsoft.tfs.core.checkinpolicies.PolicyFailure;
import com.microsoft.tfs.core.clients.versioncontrol.VersionControlClient;
import com.microsoft.tfs.core.clients.versioncontrol.WorkspaceLocation;
import com.microsoft.tfs.core.clients.versioncontrol.events.LocalWorkspaceScanListener;
import com.microsoft.tfs.core.clients.versioncontrol.events.PendingChangeCandidatesChangedListener;
import com.microsoft.tfs.core.clients.versioncontrol.events.PendingChangesChangedListener;
import com.microsoft.tfs.core.clients.versioncontrol.events.PendingChangesCountChangedListener;
import com.microsoft.tfs.core.clients.versioncontrol.events.VersionControlEventEngine;
import com.microsoft.tfs.core.clients.versioncontrol.events.WorkspaceCreatedListener;
import com.microsoft.tfs.core.clients.versioncontrol.events.WorkspaceDeletedListener;
import com.microsoft.tfs.core.clients.versioncontrol.events.WorkspaceEvent;
import com.microsoft.tfs.core.clients.versioncontrol.events.WorkspaceEvent.WorkspaceEventSource;
import com.microsoft.tfs.core.clients.versioncontrol.events.WorkspaceUpdatedEvent;
import com.microsoft.tfs.core.clients.versioncontrol.events.WorkspaceUpdatedListener;
import com.microsoft.tfs.core.clients.versioncontrol.path.LocalPath;
import com.microsoft.tfs.core.clients.versioncontrol.path.ServerPath;
import com.microsoft.tfs.core.clients.versioncontrol.path.Wildcard;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.CheckinNote;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.CheckinNoteFieldDefinition;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.CheckinNoteFieldValue;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.PendingChange;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.RecursionType;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.WorkItemCheckinInfo;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.Workspace;
import com.microsoft.tfs.core.clients.versioncontrol.specs.ItemSpec;
import com.microsoft.tfs.core.clients.versioncontrol.workspacecache.SavedCheckin;
import com.microsoft.tfs.core.clients.versioncontrol.workspacecache.WorkItemCheckedInfo;
import com.microsoft.tfs.core.clients.workitem.CheckinWorkItemAction;
import com.microsoft.tfs.core.clients.workitem.CoreFieldReferenceNames;
import com.microsoft.tfs.core.clients.workitem.WorkItem;
import com.microsoft.tfs.core.clients.workitem.WorkItemClient;
import com.microsoft.tfs.core.clients.workitem.events.WorkItemEventEngine;
import com.microsoft.tfs.core.clients.workitem.events.WorkItemSaveEvent;
import com.microsoft.tfs.core.clients.workitem.events.WorkItemSaveListener;
import com.microsoft.tfs.core.clients.workitem.query.BatchReadParameter;
import com.microsoft.tfs.core.clients.workitem.query.BatchReadParameterCollection;
import com.microsoft.tfs.core.clients.workitem.query.Query;
import com.microsoft.tfs.core.pendingcheckin.AffectedTeamProjects;
import com.microsoft.tfs.core.pendingcheckin.PendingCheckin;
import com.microsoft.tfs.util.Check;
import com.microsoft.tfs.util.FileHelpers;
import com.microsoft.tfs.util.listeners.SingleListenerFacade;

public class PendingChangesViewModel {
    private final static Log log = LogFactory.getLog(PendingChangesViewModel.class);

    private final Shell workbenchShell;

    // These fields updated when the server changes, null when offline
    private TFSServer server;
    private TFSRepository repository;
    private Workspace workspace;
    private Workspace[] allWorkspaces;

    private PendingChange[] allPendingChanges = new PendingChange[0];
    private PendingChange[] allCandidates = new PendingChange[0];

    private List<PendingChange> filteredIncludedChanges = new ArrayList<PendingChange>();
    private List<PendingChange> filteredExcludedChanges = new ArrayList<PendingChange>();

    private int numIncludedUnfilteredChanges;
    private int numExcludedUnfilteredChanges;

    private final Map<Integer, WorkItemCheckinInfo> associatedWorkItems = new HashMap<Integer, WorkItemCheckinInfo>();

    private final AffectedTeamProjects affectedTeamProjects = new AffectedTeamProjects();

    private boolean includedFilterEnabled = false;
    private boolean excludedFilterEnabled = false;

    private String includedFilterText;
    private String excludedFilterText;

    private String comment = ""; //$NON-NLS-1$
    private String uncommittedComment = ""; //$NON-NLS-1$

    private CheckinNoteFieldDefinition[] checkinNoteFieldDefinitions = new CheckinNoteFieldDefinition[0];
    private final Map<String, CheckinNoteFieldValue> checkinNoteValues = new HashMap<String, CheckinNoteFieldValue>();

    // Evaluator and context are null when offline
    private PolicyEvaluator policyEvaluator;
    private PolicyContext policyContext;
    private PolicyFailureData[] policyWarnings = new PolicyFailureData[0];

    private final Set<PendingChange> excludedPendingChanges = new HashSet<PendingChange>();

    private final ServerManagerListener serverListener = new ServerListener();
    private final WorkspaceListener workspaceListener = new WorkspaceListener();

    private final CacheListener pendingChangeCacheListener = new CacheListener();
    private final CorePendingChangeListener corePendingChangeListener = new CorePendingChangeListener();
    private final CoreCandidateListener coreCandidateListener = new CoreCandidateListener();
    private final CoreLocalWorkspaceScanListener coreLocalWorkspaceScanListener = new CoreLocalWorkspaceScanListener();
    private final UIWorkItemSaveListener workItemSaveListener = new UIWorkItemSaveListener();

    private final SingleListenerFacade includedPendingChangesListeners = new SingleListenerFacade(
            PendingChangesChangedListener.class);
    private final SingleListenerFacade excludedPendingChangesListeners = new SingleListenerFacade(
            PendingChangesChangedListener.class);
    private final SingleListenerFacade includedChangesCountListeners = new SingleListenerFacade(
            PendingChangesCountChangedListener.class);
    private final SingleListenerFacade excludedChangesCountListeners = new SingleListenerFacade(
            PendingChangesCountChangedListener.class);
    private final SingleListenerFacade candidateListeners = new SingleListenerFacade(
            PendingChangeCandidatesChangedListener.class);
    private final SingleListenerFacade checkinNoteFieldDefinitionsChangedListeners = new SingleListenerFacade(
            CheckinNoteFieldDefinitionsChangedListener.class);
    private final SingleListenerFacade checkinNoteFieldValuesChangedListeners = new SingleListenerFacade(
            CheckinNoteFieldValuesChangedListener.class);
    private final SingleListenerFacade associatedWorkItemsListeners = new SingleListenerFacade(
            AssociatedWorkItemsChangedListener.class);
    private final SingleListenerFacade checkinCommentChangedListeners = new SingleListenerFacade(
            CheckinCommentChangedListener.class);

    private final SingleListenerFacade policyWarningsChangedListeners = new SingleListenerFacade(
            PolicyWarningsChangedListener.class);

    public PendingChangesViewModel(final Shell workbenchShell) {
        this.workbenchShell = workbenchShell;
        final TFSProductPlugin plugin = TFSCommonUIClientPlugin.getDefault().getProductPlugin();

        plugin.getServerManager().addListener(serverListener);

        initialize();
    }

    /**
     * Sets the {@link PendingChangesViewModel}'s connection-oriented data from
     * the UI client plugin's current default server and repository.
     * <p>
     * Call whenever the default connection information changes, including when
     * offline.
     */
    public void initialize() {
        final TFSProductPlugin plugin = TFSCommonUIClientPlugin.getDefault().getProductPlugin();

        final TFSServer newServer = plugin.getServerManager().getDefaultServer();
        final TFSRepository newRepository = plugin.getRepositoryManager().getDefaultRepository();
        final Workspace newWorkspace = newRepository == null ? null : newRepository.getWorkspace();
        allWorkspaces = null;

        if (connectionObjectEqual(server, newServer) && connectionObjectEqual(repository, newRepository)
                && connectionObjectEqual(workspace, newWorkspace)) {
            return;
        }

        // Remove listeners from old connection
        removeListeners();

        // The default server, repository, and workspace may be null when
        // offline in a server workspace
        server = newServer;
        repository = newRepository;
        workspace = newWorkspace;

        // Clear the filters.
        includedFilterEnabled = false;
        excludedFilterEnabled = false;
        includedFilterText = ""; //$NON-NLS-1$
        excludedFilterText = ""; //$NON-NLS-1$

        policyEvaluator = null;
        policyContext = null;

        if (repository != null) {
            policyEvaluator = new PolicyEvaluator(repository.getVersionControlClient(),
                    new ExtensionPointPolicyLoader());

            policyContext = new PolicyContext();
            policyContext.addProperty(PolicyContextKeys.SWT_SHELL, workbenchShell);
            policyContext.addProperty(PolicyContextKeys.TFS_TEAM_PROJECT_COLLECTION, server.getConnection());
            policyContext.addProperty(PolicyContextKeys.RUNNING_PRODUCT_ECLIPSE_PLUGIN, new Object());
        }

        comment = ""; //$NON-NLS-1$
        associatedWorkItems.clear();
        excludedPendingChanges.clear();

        affectedTeamProjects.set(new PendingChange[0]);
        checkinNoteFieldDefinitions = new CheckinNoteFieldDefinition[0];
        checkinNoteValues.clear();

        // Retrieve all pending changes and candidates
        loadPendingChangesAndCandidates();

        // Get the last save check-in state.
        loadSavedCheckin();

        // Listen to the new connection
        addListeners();
    }

    /**
     * Removes listeners this class has hooked into lower-level plugins and
     * core. Safe to call when not connected.
     */
    private void removeListeners() {
        if (server != null) {
            final VersionControlEventEngine vcEventEngine = server.getConnection().getVersionControlClient()
                    .getEventEngine();
            vcEventEngine.removeWorkspaceCreatedListener(workspaceListener);
            vcEventEngine.removeWorkspaceDeletedListener(workspaceListener);
            vcEventEngine.removeWorkspaceUpdatedListener(workspaceListener);

            final WorkItemEventEngine wiEventEngine = server.getConnection().getWorkItemClient().getEventEngine();
            wiEventEngine.removeWorkItemSaveListener(workItemSaveListener);
        }

        if (repository != null) {
            repository.getPendingChangeCache().removeListener(pendingChangeCacheListener);
        }

        if (workspace != null) {
            final VersionControlEventEngine vcEventEngine = workspace.getClient().getEventEngine();
            vcEventEngine.removePendingChangesChangedListener(corePendingChangeListener);
            vcEventEngine.removePendingChangeCandidatesChangedListener(coreCandidateListener);
            vcEventEngine.removeLocalWorkspaceScanListener(coreLocalWorkspaceScanListener);
        }
    }

    /**
     * Adds listeners to events fired by lower-level plugins and core. Safe to
     * call when not connected.
     */
    private void addListeners() {
        if (server != null) {
            final VersionControlEventEngine vcEventEngine = server.getConnection().getVersionControlClient()
                    .getEventEngine();
            vcEventEngine.addWorkspaceCreatedListener(workspaceListener);
            vcEventEngine.addWorkspaceDeletedListener(workspaceListener);
            vcEventEngine.addWorkspaceUpdatedListener(workspaceListener);

            final WorkItemEventEngine wiEventEngine = server.getConnection().getWorkItemClient().getEventEngine();
            wiEventEngine.addWorkItemSaveListener(workItemSaveListener);
        }

        // Pending change cache provides much more efficient notification of the
        // fine-grained events than core
        if (repository != null) {
            repository.getPendingChangeCache().addListener(pendingChangeCacheListener);
        }

        // Listen to coarse-grained pending change events from core.
        if (workspace != null) {
            final VersionControlEventEngine vcEventEngine = workspace.getClient().getEventEngine();
            vcEventEngine.addPendingChangesChangedListener(corePendingChangeListener);
            vcEventEngine.addPendingChangeCandidatesChangedListener(coreCandidateListener);
            vcEventEngine.addLocalWorkspaceScanListener(coreLocalWorkspaceScanListener);
        }
    }

    /**
     * @return the repository or <code>null</code> if offline
     */
    public TFSRepository getRepository() {
        return repository;
    }

    /**
     * @return the workspace or <code>null</code> if offline
     */
    public Workspace getWorkspace() {
        return workspace;
    }

    public Workspace[] getWorkspaces() {
        if (allWorkspaces == null) {
            final QueryLocalWorkspacesCommand queryCommand = new QueryLocalWorkspacesCommand(
                    server.getConnection());
            final IStatus queryStatus = new CommandExecutor().execute(queryCommand);

            if (queryStatus.isOK()) {
                allWorkspaces = queryCommand.getWorkspaces();

                Arrays.sort(allWorkspaces, new Comparator<Workspace>() {
                    @Override
                    public int compare(final Workspace workspace0, final Workspace workspace1) {
                        return String.CASE_INSENSITIVE_ORDER.compare(workspace0.getName(), workspace1.getName());
                    }
                });
            }
        }

        return allWorkspaces;
    }

    public PolicyEvaluatorState evaluateCheckinPolicies(final PendingCheckin pendingCheckin) {
        return evaluateCheckinPolicies(pendingCheckin, null);
    }

    public PolicyEvaluatorState evaluateCheckinPolicies(final PendingCheckin pendingCheckin,
            final AtomicReference<PolicyFailure[]> outFailures) {
        if (policyEvaluator == null) {
            return PolicyEvaluatorState.UNEVALUATED;
        }

        Check.notNull(policyContext, "policyContext"); //$NON-NLS-1$

        PolicyFailure[] failures;

        try {
            policyEvaluator.setPendingCheckin(pendingCheckin);
            failures = policyEvaluator.evaluate(policyContext);
        } catch (final PolicyEvaluationCancelledException e) {
            return PolicyEvaluatorState.CANCELLED;
        }
        policyWarnings = PolicyFailureData.fromPolicyFailures(failures, policyContext);
        firePolicyWarningsChangedEvent();

        if (outFailures != null) {
            outFailures.set(failures);
        }

        return policyEvaluator.getPolicyEvaluatorState();
    }

    public CheckinNote getCheckinNote() {
        final List<CheckinNoteFieldValue> values = new ArrayList<CheckinNoteFieldValue>();

        for (final CheckinNoteFieldDefinition definition : checkinNoteFieldDefinitions) {
            if (checkinNoteValues.containsKey(definition.getName())) {
                values.add(checkinNoteValues.get(definition.getName()));
            }
        }

        return new CheckinNote(values.toArray(new CheckinNoteFieldValue[values.size()]));
    }

    public void setCheckinNoteFieldValues(final CheckinNoteFieldValue[] values) {
        checkinNoteValues.clear();

        if (values != null) {
            for (final CheckinNoteFieldValue value : values) {
                checkinNoteValues.put(value.getName(), value);
            }
        }

        updateLastSavedCheckin();
        fireCheckinNoteFieldValuesChangedEvent();
    }

    public void clearCheckinNotes() {
        checkinNoteValues.clear();
        updateLastSavedCheckin();
        fireCheckinNoteFieldValuesChangedEvent();
    }

    public void clearPolicyWarnings() {
        if (policyWarnings != null && policyWarnings.length > 0) {
            policyWarnings = new PolicyFailureData[0];
            firePolicyWarningsChangedEvent();
        }
    }

    public int getCheckinNoteFieldDefinitionCount() {
        return checkinNoteFieldDefinitions.length;
    }

    public CheckinNoteFieldDefinition[] getCheckinNoteFieldDefinitions() {
        return checkinNoteFieldDefinitions;
    }

    public String getCheckinNoteFieldValue(final String fieldName) {
        final CheckinNoteFieldValue fieldValue = checkinNoteValues.get(fieldName);
        return fieldValue == null ? null : fieldValue.getValue();
    }

    public int getPolicyWarningsCount() {
        return (policyWarnings == null) ? 0 : policyWarnings.length;
    }

    public PolicyFailureData[] getPolicyWarnings() {
        return policyWarnings;
    }

    public void clearComment() {
        if (comment.length() > 0) {
            setComment(""); //$NON-NLS-1$
        }
    }

    /*
     * Set the comment but don't commit it to the local cache and don't raise
     * comment changed events. The value of this comment will be automatically
     * committed on an attempt to read the cached comment or cleared if a value
     * is explicitly committed.
     */
    public void setUncommittedComment(final String value) {
        uncommittedComment = value;
    }

    public void setComment(String value) {
        if (value == null) {
            value = ""; //$NON-NLS-1$
        }

        // If the comment has changed, write it to the local cache and fire the
        // comment changed event.
        if (!comment.equals(value)) {
            comment = value;
            updateLastSavedCheckin();
            fireCheckinCommentChangedEvent(value);
        }

        // Clear the uncommitted comment.
        uncommittedComment = ""; //$NON-NLS-1$
    }

    public String getComment() {
        // If there is a pending uncommitted comment we commit it now. An
        // uncommitted comment may have been set as a light weight way of
        // storing the current comment without incurring the expensive
        // processing of writing it to the local disk cache and firing the
        // comment changed events.
        if (uncommittedComment.length() > 0) {
            setComment(uncommittedComment);
            uncommittedComment = ""; //$NON-NLS-1$
        }

        // Return the committed comment.
        return comment == null ? "" : comment; //$NON-NLS-1$
    }

    public void setCheckinNoteFieldValue(final String name, final String text) {
        if (checkinNoteValues.containsKey(name)) {
            final CheckinNoteFieldValue currentFieldValue = checkinNoteValues.get(name);
            if (!currentFieldValue.getValue().equals(text)) {
                checkinNoteValues.get(name).setValue(text);
                updateLastSavedCheckin();
                fireCheckinNoteFieldValuesChangedEvent();
            }
        } else {
            final CheckinNoteFieldValue value = new CheckinNoteFieldValue(name, text);
            checkinNoteValues.put(name, value);
            updateLastSavedCheckin();
            fireCheckinNoteFieldValuesChangedEvent();
        }

    }

    public WorkItemCheckinInfo[] getAssociatedWorkItems() {
        return associatedWorkItems.values().toArray(new WorkItemCheckinInfo[associatedWorkItems.size()]);
    }

    public int getAssociatedWorkItemCount() {
        return associatedWorkItems.size();
    }

    /**
     * Associates the specified work item. The default action is used. If the
     * work item was already associated, its check-in action is not changed.
     *
     * @param workItem
     *        the work item to associate (must not be <code>null</code>)
     */
    public void associateWorkItem(final WorkItem workItem) {
        associateWorkItems(new WorkItem[] { workItem });
    }

    /**
     * Associates the specified work items. The default action is used. If some
     * work items were already associated, their check-in actions are not
     * changed.
     *
     * @param workItems
     *        the work items to associate (must not be <code>null</code>)
     */
    public void associateWorkItems(final WorkItem[] workItems) {
        boolean added = false;

        for (final WorkItem workItem : workItems) {
            if (!associatedWorkItems.containsKey(workItem.getID())) {
                final WorkItemCheckinInfo checkinInfo = new WorkItemCheckinInfo(workItem);
                checkinInfo.setActionToDefault();
                associatedWorkItems.put(workItem.getID(), checkinInfo);
                added = true;
            }
        }

        if (added) {
            fireAssociatedWorkItemsChangedEvent();
            updateLastSavedCheckin();
        }
    }

    /**
     * Associates the specified work items. If some work items were already
     * associated, their check-in actions are <b>replaced</b> by the actions in
     * the specified work items (this is normal unshelve behavior).
     *
     * @param workItemInfos
     *        the work items to associate (must not be <code>null</code>)
     */
    public void associateWorkItems(final WorkItemCheckinInfo[] workItemInfos) {
        boolean added = false;

        for (final WorkItemCheckinInfo info : workItemInfos) {
            associatedWorkItems.put(info.getWorkItem().getID(), info);
            added = true;
        }

        if (added) {
            fireAssociatedWorkItemsChangedEvent();
            updateLastSavedCheckin();
        }
    }

    public void dissociateAllWorkItems() {
        if (associatedWorkItems.size() > 0) {
            final Collection<WorkItemCheckinInfo> values = associatedWorkItems.values();
            final WorkItemCheckinInfo[] workItemCheckinInfos = values
                    .toArray(new WorkItemCheckinInfo[values.size()]);
            dissociateWorkItems(workItemCheckinInfos);
        }
    }

    public void dissociateWorkItems(final WorkItemCheckinInfo[] workItemCheckinInfos) {
        boolean removed = false;

        for (final WorkItemCheckinInfo workItemCheckinInfo : workItemCheckinInfos) {
            if (associatedWorkItems.containsKey(workItemCheckinInfo.getWorkItem().getID())) {
                final WorkItem workItem = workItemCheckinInfo.getWorkItem();
                associatedWorkItems.remove(workItem.getID());
                removed = true;
            }
        }

        if (removed) {
            fireAssociatedWorkItemsChangedEvent();
            updateLastSavedCheckin();
        }
    }

    public boolean isLocalWorkspace() {
        return workspace != null && workspace.getLocation() == WorkspaceLocation.LOCAL;
    }

    public void addAssociatedWorkItemsChangedListener(final AssociatedWorkItemsChangedListener listener) {
        associatedWorkItemsListeners.addListener(listener);
    }

    public void removeAssociatedWorkItemsChangedListener(final AssociatedWorkItemsChangedListener listener) {
        associatedWorkItemsListeners.removeListener(listener);
    }

    public void addCheckinCommentChangedListener(final CheckinCommentChangedListener listener) {
        checkinCommentChangedListeners.addListener(listener);
    }

    public void removeCheckinCommentChangedListener(final CheckinCommentChangedListener listener) {
        checkinCommentChangedListeners.removeListener(listener);
    }

    public void addPolicyWarningsChangedListener(final PolicyWarningsChangedListener listener) {
        policyWarningsChangedListeners.addListener(listener);
    }

    public void removePolicyWarningsChangedListener(final PolicyWarningsChangedListener listener) {
        policyWarningsChangedListeners.removeListener(listener);
    }

    public void addCheckinNoteFieldValuesChangedListener(final CheckinNoteFieldValuesChangedListener listener) {
        checkinNoteFieldValuesChangedListeners.addListener(listener);
    }

    public void removeCheckinNoteFieldValuesChangedListener(final CheckinNoteFieldValuesChangedListener listener) {
        checkinNoteFieldValuesChangedListeners.removeListener(listener);
    }

    public void addIncludedPendingChangesChangedListener(final PendingChangesChangedListener listener) {
        includedPendingChangesListeners.addListener(listener);
    }

    public void removeIncludedPendingChangesChangedListener(final PendingChangesChangedListener listener) {
        includedPendingChangesListeners.removeListener(listener);
    }

    public void addExcludedPendingChangesChangedListener(final PendingChangesChangedListener listener) {
        excludedPendingChangesListeners.addListener(listener);
    }

    public void removeExcludedPendingChangesChangedListener(final PendingChangesChangedListener listener) {
        excludedPendingChangesListeners.removeListener(listener);
    }

    public void addIncludedPendingChangesCountChangedListener(final PendingChangesCountChangedListener listener) {
        includedChangesCountListeners.addListener(listener);
    }

    public void removeIncludedPendingChangesCountChangedListener(
            final PendingChangesCountChangedListener listener) {
        includedChangesCountListeners.removeListener(listener);
    }

    public void addExcludedPendingChangesCountChangedListener(final PendingChangesCountChangedListener listener) {
        excludedChangesCountListeners.addListener(listener);
    }

    public void removeExcludedPendingChangesCountChangedListener(
            final PendingChangesCountChangedListener listener) {
        excludedChangesCountListeners.removeListener(listener);
    }

    public void addPendingChangeCandidatesChangedListener(final PendingChangeCandidatesChangedListener listener) {
        candidateListeners.addListener(listener);
    }

    public void removePendingChangeCandidatesChangedListener(
            final PendingChangeCandidatesChangedListener listener) {
        candidateListeners.removeListener(listener);
    }

    public void addCheckinNoteFieldDefinitionsChangedListener(
            final CheckinNoteFieldDefinitionsChangedListener listener) {
        checkinNoteFieldDefinitionsChangedListeners.addListener(listener);
    }

    public void removeCheckinNoteFieldDefinitionsChangedListener(
            final CheckinNoteFieldDefinitionsChangedListener listener) {
        checkinNoteFieldDefinitionsChangedListeners.removeListener(listener);
    }

    public boolean getIncludedFilterEnabled() {
        return includedFilterEnabled;
    }

    public boolean getExcludedFilterEnabled() {
        return excludedFilterEnabled;
    }

    public void setIncludedFilterEnabled(final boolean enabled) {
        if (includedFilterEnabled != enabled) {
            includedFilterEnabled = enabled;
            if (includedFilterText != null && includedFilterText.length() > 0) {
                updateFilteredIncludedChanges();
            }
        }
    }

    public void setExcludedFilterVisible(final boolean enabled) {
        if (excludedFilterEnabled != enabled) {
            excludedFilterEnabled = enabled;
            if (excludedFilterText != null && excludedFilterText.length() > 0) {
                updateFilteredExcludedChanges();
            }
        }
    }

    public String getIncludedFilterText() {
        return includedFilterText;
    }

    public String getExcludedFilterText() {
        return excludedFilterText;
    }

    public void setIncludedFilterText(final String filterText) {
        includedFilterText = filterText;
        updateFilteredIncludedChanges();
    }

    public void setExcludedFilterText(final String filterText) {
        excludedFilterText = filterText;
        updateFilteredExcludedChanges();
    }

    private void loadPendingChangesAndCandidates() {
        if (workspace == null) {
            allPendingChanges = new PendingChange[0];
            updateCandidates(new PendingChange[0]);
            // Also updates checkin note field definitions
            updateIncludedExcludedAndAffected();
            return;
        }

        final long start = System.currentTimeMillis();

        final ItemSpec[] itemSpecs = new ItemSpec[1];
        itemSpecs[0] = new ItemSpec(ServerPath.ROOT, RecursionType.FULL);
        final AtomicReference<PendingChange[]> outCandidateChanges = new AtomicReference<PendingChange[]>();

        allPendingChanges = workspace.getPendingChangesWithCandidates(itemSpecs, false, outCandidateChanges);
        updateCandidates(outCandidateChanges.get());

        updateIncludedExcludedAndAffected();

        log.debug(MessageFormat.format("Load changes including candidates took {0} ms", //$NON-NLS-1$
                (System.currentTimeMillis() - start)));
    }

    private void loadSavedCheckin() {
        if (workspace == null) {
            updateIncludedExcludedAndAffected();
            dissociateAllWorkItems();
            clearCheckinNotes();
            return;
        }

        final SavedCheckin savedCheckin = workspace.getLastSavedCheckin();

        if (savedCheckin != null) {
            // Set the comment from the last saved check-in.
            comment = savedCheckin.getComment();
            if (comment == null) {
                comment = ""; //$NON-NLS-1$
            }
            fireCheckinCommentChangedEvent(comment);

            // Set the excluded pending changes.
            final String[] excludedPaths = savedCheckin.getExcludedServerPaths();

            if (excludedPaths != null && excludedPaths.length > 0) {
                final Set<String> set = new HashSet<String>(Arrays.asList(excludedPaths));

                for (final PendingChange pendingChange : allPendingChanges) {
                    if (set.contains(pendingChange.getServerItem())) {
                        excludedPendingChanges.add(pendingChange);
                    }
                }

                // Update the include, exclude, and effected lists.
                updateIncludedExcludedAndAffected();
            }

            // Set the associated work items
            initializeAssociatedWorkItems(savedCheckin.getWorkItemsCheckedInfo());

            // Set the check-in notes.
            initializeCheckinNote(savedCheckin.getCheckinNotes());
        }
    }

    private void initializeAssociatedWorkItems(final WorkItemCheckedInfo[] workItemInfos) {
        if (workItemInfos == null || workItemInfos.length == 0) {
            return;
        }

        final BatchReadParameterCollection batchReadParams = new BatchReadParameterCollection();
        final Map<Integer, CheckinWorkItemAction> map = new HashMap<Integer, CheckinWorkItemAction>();

        for (final WorkItemCheckedInfo workItemInfo : workItemInfos) {
            if (workItemInfo.isChecked()) {
                map.put(workItemInfo.getID(), workItemInfo.getCheckinAction());
                batchReadParams.add(new BatchReadParameter(workItemInfo.getID()));
            }
        }

        final WorkItemClient client = server.getConnection().getWorkItemClient();

        final String[] workItemFieldNames = new String[] { CoreFieldReferenceNames.ID,
                CoreFieldReferenceNames.TITLE, CoreFieldReferenceNames.STATE, CoreFieldReferenceNames.CREATED_BY,
                CoreFieldReferenceNames.CREATED_DATE, CoreFieldReferenceNames.ASSIGNED_TO,
                CoreFieldReferenceNames.CHANGED_DATE, CoreFieldReferenceNames.AREA_PATH,
                CoreFieldReferenceNames.ITERATION_PATH, };

        final StringBuilder sb = new StringBuilder();
        sb.append("select "); //$NON-NLS-1$

        boolean needComma = false;
        for (final String workItemFieldName : workItemFieldNames) {
            sb.append(needComma ? ",[" : "["); //$NON-NLS-1$ //$NON-NLS-2$
            sb.append(workItemFieldName);
            sb.append("]"); //$NON-NLS-1$
            needComma = true;
        }

        sb.append(" from workitems"); //$NON-NLS-1$
        final Query query = client.createQuery(sb.toString(), batchReadParams);

        final QueryWorkItemsCommand command = new QueryWorkItemsCommand(query);
        final ICommandExecutor executor = UICommandExecutorFactory.newUICommandExecutor(workbenchShell);
        final IStatus status = executor.execute(command);

        if (status.isOK() && command.getWorkItems() != null) {
            final WorkItem[] workItems = command.getWorkItems();

            if (workItems != null) {
                for (final WorkItem workItem : workItems) {
                    final WorkItemCheckinInfo info = new WorkItemCheckinInfo(workItem, map.get(workItem.getID()));
                    associatedWorkItems.put(workItem.getID(), info);
                }
            }
        }
    }

    private void initializeCheckinNote(final CheckinNote checkinNote) {
        setCheckinNoteFieldValues(checkinNote == null ? null : checkinNote.getValues());
    }

    private void updateIncludedExcludedAndAffected() {
        updateFilteredExcludedChanges();
        updateFilteredIncludedChanges();
        updateAffectedProjects();
    }

    private void updateCheckinNoteFieldDefinitions(final String[] teamProjectPaths) {
        final CheckinNoteFieldDefinition[] definitions;

        if ((workspace != null && workspace.getLocation() == WorkspaceLocation.LOCAL
                && workspace.getClient().getConnection().getConnectivityFailureOnLastWebServiceCall())
                || teamProjectPaths == null || teamProjectPaths.length == 0) {
            definitions = new CheckinNoteFieldDefinition[0];
        } else {
            final VersionControlClient client = repository.getVersionControlClient();
            final SortedSet<CheckinNoteFieldDefinition> set = client
                    .queryCheckinNoteFieldDefinitionsForServerPaths(teamProjectPaths);

            definitions = set.toArray(new CheckinNoteFieldDefinition[set.size()]);
        }

        if (checkinNotesDiffer(checkinNoteFieldDefinitions, definitions)) {
            checkinNoteFieldDefinitions = definitions;
            fireCheckinNoteFieldDefinitionsChangedEvent();
        }
    }

    public void updateFilteredIncludedChanges() {
        final List<PendingChange> changes = getPendingChanges(false);
        final int numChanges = changes.size();

        if (includedFilterEnabled) {
            filterPendingChanges(changes, includedFilterText);
        }

        if (changesDiffer(changes, filteredIncludedChanges)) {
            filteredIncludedChanges = changes;
            fireIncludedChangesChangedEvent();
        }

        if (numChanges != numIncludedUnfilteredChanges) {
            numIncludedUnfilteredChanges = numChanges;
            fireIncludedPendingChangesCountChangedEvent();
        }
    }

    private boolean checkinNotesDiffer(final CheckinNoteFieldDefinition[] notes1,
            final CheckinNoteFieldDefinition[] notes2) {
        Check.notNull(notes1, "notes1"); //$NON-NLS-1$
        Check.notNull(notes2, "notes2"); //$NON-NLS-1$

        if (notes1.length != notes2.length) {
            return true;
        }

        final Set<CheckinNoteFieldDefinition> set = new HashSet<CheckinNoteFieldDefinition>();

        for (final CheckinNoteFieldDefinition note : notes1) {
            set.add(note);
        }

        for (final CheckinNoteFieldDefinition note : notes2) {
            if (!set.contains(note)) {
                return true;
            }
        }

        return false;
    }

    private boolean changesDiffer(final List<PendingChange> changes1, final List<PendingChange> changes2) {
        Check.notNull(changes1, "changes1"); //$NON-NLS-1$
        Check.notNull(changes2, "changes2"); //$NON-NLS-1$

        if (changes1.size() != changes2.size()) {
            return true;
        }

        return changesDiffer(changes1.toArray(new PendingChange[changes1.size()]),
                changes2.toArray(new PendingChange[changes2.size()]));
    }

    private boolean changesDiffer(final PendingChange[] changes1, final PendingChange[] changes2) {
        Check.notNull(changes1, "changes1"); //$NON-NLS-1$
        Check.notNull(changes2, "changes2"); //$NON-NLS-1$

        if (changes1.length != changes2.length) {
            return true;
        }

        for (final PendingChange change2 : changes2) {
            boolean found = false;
            for (final PendingChange change1 : changes1) {
                if (change2.equalsIgnoringLockLevelAndVersion(change1)) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                return true;
            }
        }

        return false;
    }

    private void updateFilteredExcludedChanges() {
        final List<PendingChange> changes = getPendingChanges(true);
        final int numChanges = changes.size();

        if (excludedFilterEnabled) {
            filterPendingChanges(changes, excludedFilterText);
        }

        if (changesDiffer(changes, filteredExcludedChanges)) {
            filteredExcludedChanges = changes;
            fireExcludedChangesChangedEvent();
        }

        if (numChanges != numExcludedUnfilteredChanges) {
            numExcludedUnfilteredChanges = numChanges;
            fireExcludedPendingChangesCountChangedEvent();
        }
    }

    private void updateAffectedProjects() {
        if (affectedTeamProjects.set(getIncludedPendingChanges())) {
            updateCheckinNoteFieldDefinitions(affectedTeamProjects.getTeamProjectPaths());
        }
    }

    private void updateCandidates(final PendingChange[] newCandidates) {
        if (changesDiffer(newCandidates, allCandidates)) {
            allCandidates = newCandidates;
            firePendingChangesCandidatesChangedEvent();
        }
    }

    private void fireAssociatedWorkItemsChangedEvent() {
        ((AssociatedWorkItemsChangedListener) associatedWorkItemsListeners.getListener())
                .onAssociatedWorkItemsChanged();
    }

    private void fireIncludedChangesChangedEvent() {
        ((PendingChangesChangedListener) includedPendingChangesListeners.getListener())
                .onPendingChangesChanged(null);
    }

    private void fireExcludedChangesChangedEvent() {
        ((PendingChangesChangedListener) excludedPendingChangesListeners.getListener())
                .onPendingChangesChanged(null);
    }

    private void fireIncludedPendingChangesCountChangedEvent() {
        ((PendingChangesCountChangedListener) includedChangesCountListeners.getListener())
                .onPendingChangesCountChanged(null);
    }

    private void fireExcludedPendingChangesCountChangedEvent() {
        ((PendingChangesCountChangedListener) excludedChangesCountListeners.getListener())
                .onPendingChangesCountChanged(null);
    }

    private void firePendingChangesCandidatesChangedEvent() {
        ((PendingChangeCandidatesChangedListener) candidateListeners.getListener())
                .onPendingChangeCandidatesChanged(null);
    }

    private void fireCheckinNoteFieldDefinitionsChangedEvent() {
        ((CheckinNoteFieldDefinitionsChangedListener) checkinNoteFieldDefinitionsChangedListeners.getListener())
                .onCheckinNoteFieldDefinitionsChanged();
    }

    private void fireCheckinCommentChangedEvent(final String comment) {
        ((CheckinCommentChangedListener) checkinCommentChangedListeners.getListener())
                .onCheckinCommentChanged(comment);
    }

    private void fireCheckinNoteFieldValuesChangedEvent() {
        ((CheckinNoteFieldValuesChangedListener) checkinNoteFieldValuesChangedListeners.getListener())
                .onCheckinNoteFieldValuesChanged();
    }

    private void firePolicyWarningsChangedEvent() {
        ((PolicyWarningsChangedListener) policyWarningsChangedListeners.getListener()).onPolicyWarningsChanged();
    }

    public int getCandidateChangeCount() {
        return allCandidates.length;
    }

    public PendingChange[] getAllPendingChanges() {
        return allPendingChanges;
    }

    public PendingChange[] getIncludedPendingChanges() {
        return listToArray(filteredIncludedChanges);
    }

    public PendingChange[] getExcludedPendingChanges() {
        return listToArray(filteredExcludedChanges);
    }

    public PendingChange[] getIncludedUnfilteredPendingChanges() {
        return listToArray(getPendingChanges(false));
    }

    public int getIncludedUnfilteredChangeCount() {
        return numIncludedUnfilteredChanges;
    }

    public int getExcludedUnfilteredChangeCount() {
        return numExcludedUnfilteredChanges;
    }

    public int getIncludedFilteredChangeCount() {
        return filteredIncludedChanges.size();
    }

    public int getExcludedFilteredChangeCount() {
        return filteredExcludedChanges.size();
    }

    public PendingChange[] getCandidatePendingChanges() {
        return allCandidates;
    }

    public void includeAllPendingChanges() {
        includePendingChanges(allPendingChanges);
    }

    public void excludeAllPendingChanges() {
        excludePendingChanges(allPendingChanges);
    }

    public void includePendingChanges(final PendingChange[] changesToInclude) {
        if (changesToInclude == null || changesToInclude.length == 0) {
            return;
        }

        for (final PendingChange change : changesToInclude) {
            excludedPendingChanges.remove(change);
        }

        updateIncludedExcludedAndAffected();
        updateLastSavedCheckin();
    }

    public void excludePendingChanges(final PendingChange[] changesToExclude) {
        if (changesToExclude == null) {
            return;
        }

        for (final PendingChange change : changesToExclude) {
            excludedPendingChanges.add(change);
        }

        updateIncludedExcludedAndAffected();
        updateLastSavedCheckin();
    }

    public void updateLastSavedCheckin() {
        if (workspace != null) {
            final SavedCheckin newSavedCheckin = new SavedCheckin(comment,
                    excludedPendingChanges.toArray(new PendingChange[excludedPendingChanges.size()]),
                    getCheckinNote(), getWorkItemCheckedInfos(), null);

            workspace.setLastSavedCheckin(newSavedCheckin);
        }
    }

    private List<PendingChange> getPendingChanges(final boolean excludedChanges) {
        final List<PendingChange> list = new ArrayList<PendingChange>();
        for (final PendingChange pendingChange : allPendingChanges) {
            final boolean isExcluded = excludedPendingChanges.contains(pendingChange);

            if (excludedChanges && isExcluded || !excludedChanges && !isExcluded) {
                list.add(pendingChange);
            }
        }
        return list;
    }

    private void filterPendingChanges(final List<PendingChange> list, String filterText) {
        if (filterText == null || filterText.length() == 0) {
            return;
        }

        filterText = filterText.trim();
        filterText = filterText.replace("\"", ""); //$NON-NLS-1$//$NON-NLS-2$
        final boolean isWildCard = Wildcard.isWildcard(filterText);

        Pattern caseInsensitivePattern = null;
        if (isWildCard) {
            /*
             * Our wildcard matcher, FileHelpers.filenameMatches, considers
             * backslashes in the pattern to be escape characters, so double
             * them up.
             */
            filterText = filterText.replace("\\", "\\\\"); //$NON-NLS-1$//$NON-NLS-2$
        } else {
            /*
             * For non-wildcards, use a regular expression to get
             * case-insensitive substring search.
             */
            caseInsensitivePattern = Pattern.compile(Pattern.quote(filterText), Pattern.CASE_INSENSITIVE);
        }

        for (int i = list.size() - 1; i >= 0; i--) {
            final PendingChange change = list.get(i);

            String item = change.getLocalItem();
            if (item == null) {
                item = change.getSourceLocalItem();
            }
            if (item == null) {
                item = change.getServerItem();
            }

            if (isWildCard) {
                /*
                 * Wildcards are anchored at the beginning and end of the string
                 * (it's not a substring search). If we don't find a match
                 * against the full path, try matching just the file name.
                 */

                // Match the whole path
                boolean isMatch = FileHelpers.filenameMatches(item, filterText, true);

                // Match the file name
                if (!isMatch) {
                    String fileName;
                    if (ServerPath.isServerPath(item)) {
                        fileName = ServerPath.getFileName(item);
                    } else {
                        fileName = LocalPath.getFileName(item);
                    }

                    isMatch = FileHelpers.filenameMatches(fileName, filterText, true);
                }

                if (!isMatch) {
                    list.remove(i);
                }
            } else {
                // Use the case-insensitive expression pattern.
                if (!caseInsensitivePattern.matcher(item).find()) {
                    list.remove(i);
                }
            }
        }
    }

    private WorkItemCheckedInfo[] getWorkItemCheckedInfos() {
        final List<WorkItemCheckedInfo> list = new ArrayList<WorkItemCheckedInfo>();

        for (final WorkItemCheckinInfo checkinInfo : associatedWorkItems.values()) {
            list.add(new WorkItemCheckedInfo(checkinInfo.getWorkItem().getID(), true, checkinInfo.getAction()));
        }

        return list.toArray(new WorkItemCheckedInfo[list.size()]);
    }

    private static PendingChange[] listToArray(final List<PendingChange> list) {
        return list == null ? new PendingChange[0] : list.toArray(new PendingChange[list.size()]);
    }

    private boolean connectionObjectEqual(final Object a, final Object b) {
        if (a == b) {
            return true;
        } else if (a == null || b == null) {
            return false;
        }

        return a.equals(b);
    }

    private final class WorkspaceListener
            implements WorkspaceCreatedListener, WorkspaceUpdatedListener, WorkspaceDeletedListener {
        @Override
        public void onWorkspaceDeleted(final WorkspaceEvent e) {
            allWorkspaces = null;
        }

        @Override
        public void onWorkspaceUpdated(final WorkspaceUpdatedEvent e) {
            allWorkspaces = null;
        }

        @Override
        public void onWorkspaceCreated(final WorkspaceEvent e) {
            allWorkspaces = null;
        }
    }

    private final class ServerListener implements ServerManagerListener {
        @Override
        public void onServerAdded(final ServerManagerEvent event) {
            final TFSServer server = event.getServer();
            final VersionControlEventEngine eventEngine = server.getConnection().getVersionControlClient()
                    .getEventEngine();
            eventEngine.addWorkspaceCreatedListener(workspaceListener);
            eventEngine.addWorkspaceDeletedListener(workspaceListener);
            eventEngine.addWorkspaceUpdatedListener(workspaceListener);
        }

        @Override
        public void onServerRemoved(final ServerManagerEvent event) {
            final TFSServer server = event.getServer();
            final VersionControlEventEngine eventEngine = server.getConnection().getVersionControlClient()
                    .getEventEngine();
            eventEngine.removeWorkspaceCreatedListener(workspaceListener);
            eventEngine.removeWorkspaceDeletedListener(workspaceListener);
            eventEngine.removeWorkspaceUpdatedListener(workspaceListener);
        }

        @Override
        public void onDefaultServerChanged(final ServerManagerEvent event) {
            allWorkspaces = null;
        }
    }

    private class CacheListener extends PendingChangeCacheAdapter {
        @Override
        public void onAfterUpdatePendingChanges(final PendingChangeCacheEvent event,
                final boolean modifiedDuringOperation) {
            if (modifiedDuringOperation) {
                loadPendingChangesAndCandidates();
            }
        }
    }

    private class CorePendingChangeListener implements PendingChangesChangedListener {
        @Override
        public void onPendingChangesChanged(final WorkspaceEvent e) {
            if (e.getWorkspaceSource() == WorkspaceEventSource.EXTERNAL) {
                loadPendingChangesAndCandidates();
            }
        }
    }

    private class CoreCandidateListener implements PendingChangeCandidatesChangedListener {
        @Override
        public void onPendingChangeCandidatesChanged(final WorkspaceEvent e) {
            loadPendingChangesAndCandidates();
            firePendingChangesCandidatesChangedEvent();
        }
    }

    private class CoreLocalWorkspaceScanListener implements LocalWorkspaceScanListener {
        @Override
        public void onLocalWorkspaceScan(final WorkspaceEvent e) {
            if (e.getWorkspaceSource() == WorkspaceEventSource.EXTERNAL) {
                loadPendingChangesAndCandidates();
                firePendingChangesCandidatesChangedEvent();
            }
        }
    }

    private class UIWorkItemSaveListener implements WorkItemSaveListener {
        @Override
        public void onWorkItemSave(final WorkItemSaveEvent e) {
            final int id = e.getWorkItem().getID();

            final WorkItemCheckinInfo oldCheckinInfo = associatedWorkItems.get(id);
            if (oldCheckinInfo != null) {
                // Replace the association with a new checkin info that contains
                // the updated work item

                final WorkItemCheckinInfo newCheckinInfo = new WorkItemCheckinInfo(e.getWorkItem(),
                        oldCheckinInfo.getAction());

                associatedWorkItems.put(id, newCheckinInfo);

                fireAssociatedWorkItemsChangedEvent();
            }
        }
    }
}