org.eclipse.team.internal.ui.synchronize.AbstractSynchronizeModelProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.team.internal.ui.synchronize.AbstractSynchronizeModelProvider.java

Source

/*******************************************************************************
 * Copyright (c) 2000, 2009 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.team.internal.ui.synchronize;

import java.util.*;

import org.eclipse.compare.structuremergeviewer.IDiffElement;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.*;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.events.TreeEvent;
import org.eclipse.swt.events.TreeListener;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.team.core.ITeamStatus;
import org.eclipse.team.core.synchronize.*;
import org.eclipse.team.internal.core.TeamPlugin;
import org.eclipse.team.internal.ui.*;
import org.eclipse.team.ui.synchronize.*;

/**
 * This class is responsible for creating and maintaining a presentation model of
 * {@link SynchronizeModelElement} elements that can be shown in a viewer. The model
 * is based on the synchronization information contained in the provided {@link SyncInfoSet}.
 */
public abstract class AbstractSynchronizeModelProvider
        implements ISynchronizeModelProvider, ISyncInfoSetChangeListener, TreeListener {

    /**
     * Property constant for the expansion state for the elements displayed by the page. The
     * expansion state is a List of resource paths.
     */
    public static final String P_VIEWER_EXPANSION_STATE = TeamUIPlugin.ID + ".P_VIEWER_EXPANSION_STATE"; //$NON-NLS-1$

    /**
     * Property constant for the selection state for the elements displayed by the page. The
     * selection state is a List of resource paths.
     */
    public static final String P_VIEWER_SELECTION_STATE = TeamUIPlugin.ID + ".P_VIEWER_SELECTION_STATE"; //$NON-NLS-1$

    /**
     * Property constant for the checked state for the elements displayed by the page. The
     * checked state is a List of resource paths.
     */
    public static final String P_VIEWER_CHECKED_STATE = TeamUIPlugin.ID + ".P_VIEWER_CHECKED_STATE"; //$NON-NLS-1$

    private ISynchronizeModelElement root;

    private ISynchronizePageConfiguration configuration;

    private SyncInfoSet set;

    private SynchronizeModelUpdateHandler updateHandler;

    private boolean disposed = false;

    private SynchronizePageActionGroup actionGroup;

    private ListenerList listeners;

    private static final boolean DEBUG = false;

    /**
     * Constructor for creating a sub-provider
     * @param parentProvider the parent provider
     * @param parentNode the root node of the model built by this provider
     * @param configuration the sync page configuration
     * @param set the sync info set from which the model is built
     */
    protected AbstractSynchronizeModelProvider(AbstractSynchronizeModelProvider parentProvider,
            ISynchronizeModelElement parentNode, ISynchronizePageConfiguration configuration, SyncInfoSet set) {
        Assert.isNotNull(set);
        Assert.isNotNull(parentNode);
        this.root = parentNode;
        this.set = set;
        this.configuration = configuration;
        if (parentProvider == null) {
            // The update handler will register for sync change events
            // with the sync set when the handler is activated
            updateHandler = new SynchronizeModelUpdateHandler(this);
            getTree().addTreeListener(this);
        } else {
            // We will use the parent's update handler and register for changes with the given set
            updateHandler = parentProvider.updateHandler;
            set.addSyncSetChangedListener(this);
        }
    }

    private Tree getTree() {
        return ((Tree) ((AbstractTreeViewer) getViewer()).getControl());
    }

    /**
    * Constructor for creating a root model provider.
    * @param configuration the sync page configuration
    * @param set the sync info set from which the model is built
    */
    protected AbstractSynchronizeModelProvider(ISynchronizePageConfiguration configuration, SyncInfoSet set) {
        this(null, new UnchangedResourceModelElement(null, ResourcesPlugin.getWorkspace().getRoot()) {
            /*
             * Override to ensure that the diff viewer will appear in CompareEditorInputs
             */
            public boolean hasChildren() {
                return true;
            }
        }, configuration, set);
        // Register the action group for this provider, since it is the root provider
        SynchronizePageActionGroup actionGroup = getActionGroup();
        if (actionGroup != null) {
            configuration.addActionContribution(actionGroup);
        }
    }

    /**
     * Return the action group for this provider or <code>null</code>
      * if there are no actions associated with this provider. The action
      * group will be registered with the configuration if this is
      * the root provider. If this provider is a sub-provider, it
      * is up to the parent provider to register the action group.
      * <p>
      * The action group for a provider is created by calling the
      * <code>createdActionGroup</code> method. If this method returns
      * a non-null group, it is cached so it can be disposed
      * when the provider is disposed.
      * @return the action group for this provider or <code>null</code>
      * if there are no actions associated with this provider
      */
    public final synchronized SynchronizePageActionGroup getActionGroup() {
        if (actionGroup == null) {
            actionGroup = createActionGroup();
        }
        return actionGroup;
    }

    /**
     * Create the action group for this provider. By default,
     * a <code>null</code> is returned. Subclasses may override.
     * @return the action group for this provider or <code>null</code>
     */
    protected SynchronizePageActionGroup createActionGroup() {
        return null;
    }

    /**
    * Return the set that contains the elements this provider is using as
    * a basis for creating a presentation model. This cannot be null.
    * 
    * @return the set that contains the elements this provider is
    * using as a basis for creating a presentation model.
    */
    public SyncInfoSet getSyncInfoSet() {
        return set;
    }

    /**
     * Returns the input created by this provider or <code>null</code> if
     * {@link #prepareInput(IProgressMonitor)} hasn't been called on this object yet.
     * 
     * @return the input created by this provider.
     */
    public ISynchronizeModelElement getModelRoot() {
        return root;
    }

    /**
     * Return the page configuration for this provider.
     * 
     * @return the page configuration for this provider.
     */
    public ISynchronizePageConfiguration getConfiguration() {
        return configuration;
    }

    /**
     * Return the <code>AbstractTreeViewer</code> associated with this
     * provider or <code>null</code> if the viewer is not of the proper type.
     * @return the structured viewer that is displaying the model managed by this provider
     */
    public StructuredViewer getViewer() {
        ISynchronizePage page = configuration.getPage();
        if (page == null)
            return null;
        Viewer viewer = page.getViewer();
        if (viewer instanceof AbstractTreeViewer) {
            return (AbstractTreeViewer) viewer;
        }
        return null;
    }

    /**
     * Builds the viewer model based on the contents of the sync set.
     */
    public ISynchronizeModelElement prepareInput(IProgressMonitor monitor) {
        // Connect to the sync set which will register us as a listener and give us a reset event
        // in a background thread
        if (isRootProvider()) {
            updateHandler.connect(monitor);
        } else {
            getSyncInfoSet().connect(this, monitor);
        }
        return getModelRoot();
    }

    /**
     * Calculate the problem marker that should be shown on the given
     * element. The returned property can be either
     * ISynchronizeModelElement.PROPAGATED_ERROR_MARKER_PROPERTY or
     * ISynchronizeModelElement.PROPAGATED_WARNING_MARKER_PROPERTY.
     * @param element a synchronize model element
     * @return the marker property that should be displayed on the element
     * or <code>null</code> if no marker should be displayed
     */
    public String calculateProblemMarker(ISynchronizeModelElement element) {
        IResource resource = element.getResource();
        String property = null;
        if (resource != null && resource.exists()) {
            try {
                IMarker[] markers = resource.findMarkers(IMarker.PROBLEM, true, getLogicalModelDepth(resource));
                for (int i = 0; i < markers.length; i++) {
                    IMarker marker = markers[i];
                    try {
                        Integer severity = (Integer) marker.getAttribute(IMarker.SEVERITY);
                        if (severity != null) {
                            if (severity.intValue() == IMarker.SEVERITY_ERROR) {
                                property = ISynchronizeModelElement.PROPAGATED_ERROR_MARKER_PROPERTY;
                                break;
                            } else if (severity.intValue() == IMarker.SEVERITY_WARNING) {
                                property = ISynchronizeModelElement.PROPAGATED_WARNING_MARKER_PROPERTY;
                                // Keep going because there may be errors on other resources
                            }
                        }
                    } catch (CoreException e) {
                        if (!resource.exists()) {
                            // The resource was deleted concurrently. Forget any previously found property
                            property = null;
                            break;
                        }
                        // If the marker exists, log the exception and continue.
                        // Otherwise, just ignore the exception and keep going
                        if (marker.exists()) {
                            TeamPlugin.log(e);
                        }
                    }
                }
            } catch (CoreException e) {
                // If the resource exists (is accessible), log the exception and continue.
                // Otherwise, just ignore the exception
                if (resource.isAccessible() && e.getStatus().getCode() != IResourceStatus.RESOURCE_NOT_FOUND
                        && e.getStatus().getCode() != IResourceStatus.PROJECT_NOT_OPEN) {
                    TeamPlugin.log(e);
                }
            }
        } else if (resource == null) {
            // For non-resource elements, show the same propogaqted marker as the children
            IDiffElement[] children = element.getChildren();
            for (int i = 0; i < children.length; i++) {
                IDiffElement child = children[i];
                if (child instanceof ISynchronizeModelElement) {
                    ISynchronizeModelElement childElement = (ISynchronizeModelElement) child;
                    if (childElement.getProperty(ISynchronizeModelElement.PROPAGATED_ERROR_MARKER_PROPERTY)) {
                        property = ISynchronizeModelElement.PROPAGATED_ERROR_MARKER_PROPERTY;
                        break;
                    } else if (childElement
                            .getProperty(ISynchronizeModelElement.PROPAGATED_WARNING_MARKER_PROPERTY)) {
                        property = ISynchronizeModelElement.PROPAGATED_WARNING_MARKER_PROPERTY;
                        // Keep going because there may be errors on other resources
                    }

                }
            }
        }
        return property;
    }

    /**
     * Return the logical model depth used for marker propagation
     * @param resource the resource
     * @return the depth the resources should be traversed
     */
    protected int getLogicalModelDepth(IResource resource) {
        return IResource.DEPTH_INFINITE;
    }

    /**
     * Update the label of the given diff node. The label for nodes queued
     * using this method will not be updated until <code>firePendingLabelUpdates</code>
     * is called.
     * @param diffNode the diff node to be updated
     */
    protected void queueForLabelUpdate(ISynchronizeModelElement diffNode) {
        updateHandler.queueForLabelUpdate(diffNode);
    }

    /**
     * Throw away any old state associated with this provider and
     * rebuild the model from scratch.
     */
    protected void reset() {
        // save expansion state
        if (isRootProvider() && hasViewerState()) {
            saveViewerState();
        }

        // Clear existing model, but keep the root node
        clearModelObjects(getModelRoot());

        // Rebuild the model
        buildModelObjects(getModelRoot());

        // Notify listeners that model has changed
        ISynchronizeModelElement root = getModelRoot();
        if (root instanceof SynchronizeModelElement) {
            ((SynchronizeModelElement) root).fireChanges();
        }

        if (Utils.canUpdateViewer(getViewer())) {
            // If we can update the viewer, that means that the view was updated
            // when the model was rebuilt.
            refreshModelRoot();
        } else {
            // Only refresh the view if there is now background update in
            // progress. If there is, the background update will refresh
            if (!updateHandler.isPerformingBackgroundUpdate()) {
                Utils.asyncExec(new Runnable() {
                    public void run() {
                        refreshModelRoot();
                    }
                }, getViewer());
            }
        }
    }

    private void refreshModelRoot() {
        StructuredViewer viewer = getViewer();
        if (viewer != null && !viewer.getControl().isDisposed()) {
            try {
                viewer.getControl().setRedraw(false);
                if (isRootProvider() || getModelRoot().getParent() == null) {
                    // Refresh the entire view
                    viewer.refresh();
                } else {
                    // Only refresh the model root bu also ensure that
                    // the parents of the model root and the model root
                    // itself are added to the view
                    addToViewer(getModelRoot());
                }
                //   restore expansion state
                if (isRootProvider())
                    restoreViewerState();
            } finally {
                viewer.getControl().setRedraw(true);
            }
        }
    }

    /**
     * For each node create children based on the contents of
     * @param node
     * @return the diff elements
     */
    protected abstract IDiffElement[] buildModelObjects(ISynchronizeModelElement node);

    /**
     * Returns whether the viewer has state to be saved.
      * @return whether the viewer has state to be saved
      */
    protected abstract boolean hasViewerState();

    /*
     * Return all the resources that are expanded in the page.
     * This method should only be called in the UI thread
     * after validating that the viewer is still valid.
     */
    protected IResource[] getExpandedResources() {
        Set expanded = new HashSet();
        IResource[] savedExpansionState = getCachedResources(P_VIEWER_EXPANSION_STATE);
        for (int i = 0; i < savedExpansionState.length; i++) {
            IResource resource = savedExpansionState[i];
            expanded.add(resource);
        }
        StructuredViewer viewer = getViewer();
        Object[] objects = ((AbstractTreeViewer) viewer).getVisibleExpandedElements();
        IResource[] currentExpansionState = getResources(objects);
        for (int i = 0; i < currentExpansionState.length; i++) {
            IResource resource = currentExpansionState[i];
            expanded.add(resource);
        }
        return (IResource[]) expanded.toArray(new IResource[expanded.size()]);
    }

    /*
     * Return all the resources that are selected in the page.
     * This method should only be called in the UI thread
     * after validating that the viewer is still valid.
     */
    protected IResource[] getSelectedResources() {
        StructuredViewer viewer = getViewer();
        return getResources(((IStructuredSelection) viewer.getSelection()).toArray());
    }

    /*
     * Return all the resources that are checked in the page.
     * This method should only be called in the UI thread
     * after validating that the viewer is still valid.
     */
    protected IResource[] getCheckedResources() {
        StructuredViewer viewer = getViewer();
        if (viewer instanceof CheckboxTreeViewer) {
            return getResources(((CheckboxTreeViewer) viewer).getCheckedElements());
        }

        return new IResource[0];
    }

    /*
     * Expand the resources if they appear in the page.
     * This method should only be called in the UI thread
     * after validating that the viewer is still valid.
     */
    protected void expandResources(IResource[] resources) {
        Set expandedElements = new HashSet();
        StructuredViewer viewer = getViewer();
        for (int j = 0; j < resources.length; j++) {
            IResource resource = resources[j];
            ISynchronizeModelElement[] elements = getModelObjects(resource);
            // Only expand when there is one element per resource
            if (elements.length == 1) {
                for (int i = 0; i < elements.length; i++) {
                    ISynchronizeModelElement element = elements[i];
                    // Add all parents of the element to the expansion set
                    while (element != null) {
                        expandedElements.add(element);
                        element = (ISynchronizeModelElement) element.getParent();
                    }
                }
            }
        }
        if (!expandedElements.isEmpty())
            ((AbstractTreeViewer) viewer).setExpandedElements(expandedElements.toArray());
    }

    protected IResource[] getResources(Object[] objects) {
        Set result = new HashSet();
        if (objects.length > 0) {
            for (int i = 0; i < objects.length; i++) {
                if (objects[i] instanceof ISynchronizeModelElement) {
                    IResource resource = ((ISynchronizeModelElement) objects[i]).getResource();
                    if (resource != null)
                        result.add(resource);
                }
            }
        }
        return (IResource[]) result.toArray(new IResource[result.size()]);
    }

    private void clearResourceCache(String configProperty) {
        getConfiguration().setProperty(configProperty, null);
    }

    private void cacheResources(IResource[] resources, String configProperty) {
        if (resources.length > 0) {
            ISynchronizePageConfiguration config = getConfiguration();
            ArrayList paths = new ArrayList();
            for (int i = 0; i < resources.length; i++) {
                IResource resource = resources[i];
                String path = resource.getFullPath().toString();
                if (resource.getType() != IResource.FILE && path.charAt(path.length() - 1) != IPath.SEPARATOR) {
                    // Include a trailing slash on folders and projects.
                    // It is used when recreating cached resources that don't exist locally
                    path += IPath.SEPARATOR;
                }
                paths.add(path);
            }
            config.setProperty(configProperty, paths);
        } else {
            clearResourceCache(configProperty);
        }
    }

    private IResource[] getCachedResources(String configProperty) {
        List paths = (List) getConfiguration().getProperty(configProperty);
        if (paths == null)
            return new IResource[0];
        IContainer container = ResourcesPlugin.getWorkspace().getRoot();
        ArrayList resources = new ArrayList();
        for (Iterator it = paths.iterator(); it.hasNext();) {
            String path = (String) it.next();
            IResource resource = getResourceForPath(container, path);
            if (resource != null) {
                resources.add(resource);
            }
        }
        return (IResource[]) resources.toArray(new IResource[resources.size()]);
    }

    /**
     * Save the viewer state (expansion and selection)
     */
    protected void saveViewerState() {
        //   save visible expanded elements and selection
        final StructuredViewer viewer = getViewer();
        if (viewer != null && !viewer.getControl().isDisposed() && viewer instanceof AbstractTreeViewer) {
            //check to see if we should store the checked states of the tree

            final boolean storeChecks = ((SynchronizePageConfiguration) configuration)
                    .getViewerStyle() == SynchronizePageConfiguration.CHECKBOX;
            final IResource[][] expandedResources = new IResource[1][0];
            final IResource[][] selectedResources = new IResource[1][0];
            final IResource[][] checkedResources = new IResource[1][0];
            viewer.getControl().getDisplay().syncExec(new Runnable() {
                public void run() {
                    if (viewer != null && !viewer.getControl().isDisposed()) {
                        expandedResources[0] = getExpandedResources();
                        selectedResources[0] = getSelectedResources();
                        if (storeChecks)
                            checkedResources[0] = getCheckedResources();
                    }
                }
            });

            // Save expansion and selection
            cacheResources(expandedResources[0], P_VIEWER_EXPANSION_STATE);
            cacheResources(selectedResources[0], P_VIEWER_SELECTION_STATE);
            if (storeChecks)
                cacheResources(checkedResources[0], P_VIEWER_CHECKED_STATE);
        }
    }

    /**
     * Restore the expansion state and selection of the viewer.
     * This method must be invoked from within the UI thread.
     */
    protected void restoreViewerState() {
        // restore expansion state and selection state
        final StructuredViewer viewer = getViewer();
        if (viewer != null && !viewer.getControl().isDisposed() && viewer instanceof AbstractTreeViewer) {
            IResource[] resourcesToExpand = getCachedResources(P_VIEWER_EXPANSION_STATE);
            IResource[] resourcesToSelect = getCachedResources(P_VIEWER_SELECTION_STATE);
            if (((SynchronizePageConfiguration) configuration)
                    .getViewerStyle() == SynchronizePageConfiguration.CHECKBOX) {
                IResource[] resourcesToCheck = getCachedResources(P_VIEWER_CHECKED_STATE);
                checkResources(resourcesToCheck);
            }
            expandResources(resourcesToExpand);
            selectResources(resourcesToSelect);
        }
    }

    /*
     * Select the given resources in the view. This method can
     * only be invoked from the UI thread.
     */
    protected void selectResources(IResource[] resourcesToSelect) {
        StructuredViewer viewer = getViewer();
        final ArrayList selectedElements = new ArrayList();
        for (int i = 0; i < resourcesToSelect.length; i++) {
            IResource resource = resourcesToSelect[i];
            ISynchronizeModelElement[] elements = getModelObjects(resource);
            // Only preserve the selection if there is one element for the resource
            if (elements.length == 1) {
                selectedElements.add(elements[0]);
            }
        }
        if (!selectedElements.isEmpty())
            viewer.setSelection(new StructuredSelection(selectedElements));
    }

    /*
    * Check the given resources in the view. This method can
    * only be invoked from the UI thread.
    */
    protected void checkResources(IResource[] resourcesToCheck) {
        Set checkedElements = new HashSet();
        StructuredViewer viewer = getViewer();
        if (!(viewer instanceof CheckboxTreeViewer))
            return;

        for (int j = 0; j < resourcesToCheck.length; j++) {
            IResource resource = resourcesToCheck[j];
            if (resource.getType() != IResource.FILE)
                continue;

            ISynchronizeModelElement[] elements = getModelObjects(resource);
            // Only expand when there is one element per resource
            if (elements.length == 1) {
                for (int i = 0; i < elements.length; i++) {
                    ISynchronizeModelElement element = elements[i];
                    checkedElements.add(element);
                }
            }
        }
        if (!checkedElements.isEmpty())
            ((CheckboxTreeViewer) viewer).setCheckedElements(checkedElements.toArray());
    }

    /*
     * Convert a path to a resource by first looking in the resource
     * tree and, if that fails, by using the path format to create
     * a handle.
     */
    private IResource getResourceForPath(IContainer container, String path) {
        IResource resource = container.findMember(path, true /* include phantoms */);
        if (resource == null) {
            try {
                // The resource doesn't have an entry on the resources tree
                // but may still appear in the view so try to deduce the type
                // from the path
                if (path.endsWith(Character.toString(IPath.SEPARATOR))) {
                    resource = container.getFolder(new Path(null, path));
                } else {
                    resource = container.getFile(new Path(null, path));
                }
            } catch (IllegalArgumentException e) {
                // Couldn't get a resource handle so ignore
            }
        }
        return resource;
    }

    /* (non-Javadoc)
     * @see org.eclipse.swt.events.TreeListener#treeCollapsed(org.eclipse.swt.events.TreeEvent)
     */
    public void treeCollapsed(TreeEvent e) {
        clearResourceCache(P_VIEWER_EXPANSION_STATE);
    }

    /* (non-Javadoc)
     * @see org.eclipse.swt.events.TreeListener#treeExpanded(org.eclipse.swt.events.TreeEvent)
     */
    public void treeExpanded(TreeEvent e) {
        clearResourceCache(P_VIEWER_EXPANSION_STATE);
    }

    /**
    * Return all the model objects in this provider that represent the given resource
     * @param resource the resource
     * @return the model objects for the resource
     */
    protected abstract ISynchronizeModelElement[] getModelObjects(IResource resource);

    /* (non-Javadoc)
    * @see org.eclipse.team.internal.ui.synchronize.ISynchronizeModelProvider#saveState()
    */
    public void saveState() {
        saveViewerState();
    }

    /**
    * Method invoked when a sync element is added or removed or its state changes.
    * This method can be invoked from the UI thread or a background thread.
    * @param element synchronize element
    * @param clear <code>true</code> if the conflict bit of the element was cleared
    * (i.e. the element has been deleted)
    */
    protected void propogateConflictState(ISynchronizeModelElement element, boolean clear) {
        boolean isConflict = clear ? false : isConflicting(element);
        boolean wasConflict = element.getProperty(ISynchronizeModelElement.PROPAGATED_CONFLICT_PROPERTY);
        // Only propagate and update parent labels if the state of the element has changed
        if (isConflict != wasConflict) {
            element.setPropertyToRoot(ISynchronizeModelElement.PROPAGATED_CONFLICT_PROPERTY, isConflict);
            updateHandler.updateParentLabels(element);
        }
    }

    /**
     * Return whether the given model element represents a conflict.
     * @param element the element being tested
     * @return whether the element is a conflict
     */
    protected boolean isConflicting(ISynchronizeModelElement element) {
        return (element.getKind() & SyncInfo.DIRECTION_MASK) == SyncInfo.CONFLICTING;
    }

    /**
     * Dispose of the provider
     */
    public void dispose() {
        // Only dispose the update handler if it is
        // directly associated with this provider
        if (isRootProvider()) {
            updateHandler.dispose();
            getTree().removeTreeListener(this);
        } else {
            set.removeSyncSetChangedListener(this);
        }
        if (actionGroup != null) {
            Utils.syncExec(new Runnable() {
                public void run() {
                    actionGroup.dispose();
                }
            }, getViewer());
        }
        this.disposed = true;
    }

    private boolean isRootProvider() {
        return updateHandler.getProvider() == this;
    }

    /**
    * Return whether this provide has been disposed.
     * @return whether this provide has been disposed
     */
    public boolean isDisposed() {
        return disposed;
    }

    /**
     * Return the closest parent elements that represents a model element that
     * could contains the given resource. Multiple elements need only be returned
     * if two or more logical views are being shown and each view has an element
     * that could contain the resource.
     * @param resource the resource
     * @return one or more lowest level parents that could contain the resource
     */
    public abstract ISynchronizeModelElement[] getClosestExistingParents(IResource resource);

    /**
     * Handle the changes made to the viewer's <code>SyncInfoSet</code>.
     * This method delegates the changes to the three methods <code>handleResourceChanges(ISyncInfoSetChangeEvent)</code>,
     * <code>handleResourceRemovals(ISyncInfoSetChangeEvent)</code> and
     * <code>handleResourceAdditions(ISyncInfoSetChangeEvent)</code>.
     * @param event
     *            the event containing the changed resources.
     */
    protected void handleChanges(ISyncInfoTreeChangeEvent event, IProgressMonitor monitor) {
        handleResourceChanges(event);
        handleResourceRemovals(event);
        handleResourceAdditions(event);
    }

    /**
    * Update the viewer for the sync set additions in the provided event. This
    * method is invoked by <code>handleChanges(ISyncInfoSetChangeEvent)</code>.
    * Subclasses may override.
    * @param event
    */
    protected abstract void handleResourceAdditions(ISyncInfoTreeChangeEvent event);

    /**
     * Update the viewer for the sync set changes in the provided event. This
     * method is invoked by <code>handleChanges(ISyncInfoSetChangeEvent)</code>.
     * Subclasses may override.
     * @param event
     */
    protected abstract void handleResourceChanges(ISyncInfoTreeChangeEvent event);

    /**
     * Update the viewer for the sync set removals in the provided event. This
     * method is invoked by <code>handleChanges(ISyncInfoSetChangeEvent)</code>.
     * Subclasses may override.
     * @param event
     */
    protected abstract void handleResourceRemovals(ISyncInfoTreeChangeEvent event);

    /* (non-Javadoc)
     * @see org.eclipse.team.core.synchronize.ISyncInfoSetChangeListener#syncInfoChanged(org.eclipse.team.core.synchronize.ISyncInfoSetChangeEvent, org.eclipse.core.runtime.IProgressMonitor)
     */
    public void syncInfoChanged(final ISyncInfoSetChangeEvent event, final IProgressMonitor monitor) {
        if (!(event instanceof ISyncInfoTreeChangeEvent)) {
            reset();
        } else {
            updateHandler.runViewUpdate(new Runnable() {
                public void run() {
                    handleChanges((ISyncInfoTreeChangeEvent) event, monitor);
                }
            }, true /* preserve expansion */);
        }
    }

    /* (non-Javadoc)
     * @see org.eclipse.team.core.synchronize.ISyncInfoSetChangeListener#syncInfoSetErrors(org.eclipse.team.core.synchronize.SyncInfoSet, org.eclipse.team.core.ITeamStatus[], org.eclipse.core.runtime.IProgressMonitor)
     */
    public void syncInfoSetErrors(SyncInfoSet set, ITeamStatus[] errors, IProgressMonitor monitor) {
        // Not handled

    }

    /* (non-Javadoc)
     * @see org.eclipse.team.core.synchronize.ISyncInfoSetChangeListener#syncInfoSetReset(org.eclipse.team.core.synchronize.SyncInfoSet, org.eclipse.core.runtime.IProgressMonitor)
     */
    public void syncInfoSetReset(SyncInfoSet set, IProgressMonitor monitor) {
        reset();
    }

    protected void addToViewer(ISynchronizeModelElement node) {
        if (DEBUG) {
            System.out.println("Adding model element " + node.getName()); //$NON-NLS-1$
        }
        propogateConflictState(node, false);
        // Set the marker property on this node.
        // There is no need to propagate this to the parents
        // as they will be displaying the proper marker already
        String property = calculateProblemMarker(node);
        if (property != null) {
            node.setProperty(property, true);
            // Parent resource nodes would have been properly calculated when they were added.
            // However, non-resource nodes would not so we need to propagate the marker to them
            propogateMarkerPropertyToParent(node, property);
        }
        if (Utils.canUpdateViewer(getViewer())) {
            doAdd((SynchronizeModelElement) node.getParent(), node);
        }
        updateHandler.nodeAdded(node, this);
    }

    /*
      * Propagate the marker property to the parent if it is not already there.
      * Only propagate warnings if the parent isn't an error already.
      */
    private void propogateMarkerPropertyToParent(ISynchronizeModelElement node, String property) {
        ISynchronizeModelElement parent = (ISynchronizeModelElement) node.getParent();
        if (parent != null && !parent.getProperty(property)
                && !parent.getProperty(ISynchronizeModelElement.PROPAGATED_ERROR_MARKER_PROPERTY)) {
            parent.setProperty(property, true);
            propogateMarkerPropertyToParent(parent, property);
        }
    }

    /**
    * Remove any traces of the model element and any of it's descendants in the
    * hierarchy defined by the content provider from the content provider and
    * the viewer it is associated with.
    * @param nodes the model elements to remove
    */
    protected void removeFromViewer(ISynchronizeModelElement[] nodes) {
        List rootsToClear = new ArrayList();
        for (int i = 0; i < nodes.length; i++) {
            ISynchronizeModelElement node = nodes[i];
            if (DEBUG) {
                System.out.println("Removing model element " + node.getName()); //$NON-NLS-1$
            }
            ISynchronizeModelElement rootToClear = getRootToClear(node);
            if (DEBUG) {
                if (rootToClear != node) {
                    System.out.println("Removing parent element " + rootToClear.getName()); //$NON-NLS-1$
                }
            }
            propogateConflictState(rootToClear, true /* clear the conflict */);
            clearModelObjects(rootToClear);
            rootsToClear.add(rootToClear);
        }
        ISynchronizeModelElement[] roots = (ISynchronizeModelElement[]) rootsToClear
                .toArray(new ISynchronizeModelElement[rootsToClear.size()]);
        if (Utils.canUpdateViewer(getViewer())) {
            doRemove(roots);
        }
        for (int i = 0; i < roots.length; i++) {
            ISynchronizeModelElement element = roots[i];
            updateHandler.nodeRemoved(element, this);
        }
    }

    /**
     * Clear the model objects from the diff tree, cleaning up any cached state
     * (such as resource to model object map). This method recurses deeply on
     * the tree to allow the cleanup of any cached state for the children as
     * well.
     * @param node the root node
     */
    protected final void clearModelObjects(ISynchronizeModelElement node) {
        // When clearing model objects, any parents of the node
        // That are not out-of-sync, not the model root and that would
        // be empty as a result of this clear, should also be cleared.
        ISynchronizeModelElement rootToClear = getRootToClear(node);
        // Recursively clear the nodes from the root
        recursiveClearModelObjects(rootToClear);
        if (node == getModelRoot()) {
            IDiffElement[] children = node.getChildren();
            for (int i = 0; i < children.length; i++) {
                IDiffElement element = children[i];
                ((SynchronizeModelElement) node).remove(element);
            }
        } else {
            SynchronizeModelElement parent = ((SynchronizeModelElement) node.getParent());
            if (parent != null)
                parent.remove(node);
        }
    }

    /**
     * Method that subclasses can override when clearing model objects.
      * @param node the node to be cleared recursively
      */
    protected void recursiveClearModelObjects(ISynchronizeModelElement node) {
        // Clear all the children of the node
        IDiffElement[] children = node.getChildren();
        for (int i = 0; i < children.length; i++) {
            IDiffElement element = children[i];
            if (element instanceof ISynchronizeModelElement) {
                ISynchronizeModelElement sme = (ISynchronizeModelElement) element;
                ISynchronizeModelProvider provider = getProvider(sme);
                if (provider != null && provider instanceof AbstractSynchronizeModelProvider) {
                    ((AbstractSynchronizeModelProvider) provider).recursiveClearModelObjects(sme);
                } else {
                    recursiveClearModelObjects(sme);
                }
            }
        }
        // Notify the update handler that the node has been cleared
        if (node != getModelRoot())
            updateHandler.modelObjectCleared(node);
    }

    /*
     * Remove to root should only remove to the root of the provider and not the
     * diff tree.
     */
    private ISynchronizeModelElement getRootToClear(ISynchronizeModelElement node) {
        if (node == getModelRoot())
            return node;
        ISynchronizeModelElement parent = (ISynchronizeModelElement) node.getParent();
        if (parent != null && parent != getModelRoot() && !isOutOfSync(parent)
                && parent.getChildren().length == 1) {
            return getRootToClear(parent);
        }
        return node;
    }

    /*
     * Return whether the node represents an out-of-sync resource.
     */
    protected boolean isOutOfSync(ISynchronizeModelElement node) {
        SyncInfo info = Utils.getSyncInfo(node);
        return (info != null && info.getKind() != SyncInfo.IN_SYNC);
    }

    protected boolean isOutOfSync(IResource resource) {
        SyncInfo info = getSyncInfoSet().getSyncInfo(resource);
        return (info != null && info.getKind() != SyncInfo.IN_SYNC);
    }

    /**
    * Return the provider that created and manages the given
    * model element. The default is to return the receiver.
    * Subclasses may override.
     * @param element the synchronize model element
     * @return the provider that created the element
     */
    protected ISynchronizeModelProvider getProvider(ISynchronizeModelElement element) {
        return this;
    }

    /**
     * Add the element to the viewer.
     * @param parent the parent of the element which is already added to the viewer
     * @param element the element to be added to the viewer
     */
    protected void doAdd(ISynchronizeModelElement parent, ISynchronizeModelElement element) {
        updateHandler.doAdd(parent, element);
    }

    /**
     * Remove the element from the viewer
     * @param elements the elements to be removed
     */
    protected void doRemove(ISynchronizeModelElement[] elements) {
        AbstractTreeViewer viewer = (AbstractTreeViewer) getViewer();
        try {
            viewer.remove(elements);
        } catch (SWTException e) {
            // The remove failed due to an SWT exception. Log it and continue
            TeamUIPlugin.log(IStatus.ERROR, "An error occurred removing elements from the synchronize view", e); //$NON-NLS-1$
        }
        if (DEBUG) {
            for (int i = 0; i < elements.length; i++) {
                ISynchronizeModelElement element = elements[i];
                System.out.println("Removing view item " + element.getName()); //$NON-NLS-1$
            }
        }
    }

    /**
     * This is a callback from the model update handler that gets invoked
     * when a node is added to the viewer. It is only invoked for the
     * root level model provider.
     * @param node
     * @param provider the provider that added the node
     */
    protected void nodeAdded(ISynchronizeModelElement node, AbstractSynchronizeModelProvider provider) {
        // Default is to do nothing
    }

    /**
     * This is a callback from the model update handler that gets invoked
     * when a node is removed from the viewer. It is only invoked for the
     * root level model provider. A removed node may have children for
     * which a <code>nodeRemoved</code> callback is not received (see
     * <code>modelObjectCleared</code>).
     * @param node
     */
    protected void nodeRemoved(ISynchronizeModelElement node, AbstractSynchronizeModelProvider provider) {
        // Default is to do nothing
    }

    /**
    * This is a callback from the model update handler that gets invoked
    * when a node is cleared from the model. It is only invoked for the
    * root level model provider. This callback is deep in the sense that
    * a callback is sent for each node that is cleared.
     * @param node the node that was cleared.
     */
    public void modelObjectCleared(ISynchronizeModelElement node) {
        // Default is to do nothing
    }

    public void addPropertyChangeListener(IPropertyChangeListener listener) {
        synchronized (this) {
            if (listeners == null) {
                listeners = new ListenerList(ListenerList.IDENTITY);
            }
            listeners.add(listener);
        }

    }

    public void removePropertyChangeListener(IPropertyChangeListener listener) {
        if (listeners != null) {
            synchronized (this) {
                listeners.remove(listener);
                if (listeners.isEmpty()) {
                    listeners = null;
                }
            }
        }
    }

    protected void firePropertyChange(String key, Object oldValue, Object newValue) {
        Object[] allListeners;
        synchronized (this) {
            allListeners = listeners.getListeners();
        }
        final PropertyChangeEvent event = new PropertyChangeEvent(this, key, oldValue, newValue);
        for (int i = 0; i < allListeners.length; i++) {
            final IPropertyChangeListener listener = (IPropertyChangeListener) allListeners[i];
            SafeRunner.run(new ISafeRunnable() {
                public void handleException(Throwable exception) {
                    // Error is logged by platform
                }

                public void run() throws Exception {
                    listener.propertyChange(event);
                }
            });
        }
    }

    /**
     * Wait until the provider is done processing any events and
     * the page content are up-to-date.
     * This method is for testing purposes only.
     * @param monitor
     */
    public void waitUntilDone(IProgressMonitor monitor) {
        monitor.worked(1);
        // wait for the event handler to process changes.
        while (updateHandler.getEventHandlerJob().getState() != Job.NONE) {
            monitor.worked(1);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
            }
            Policy.checkCanceled(monitor);
        }
        monitor.worked(1);
    }

    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    public String toString() {
        ISynchronizeModelElement element = getModelRoot();
        String name = getClass().getName();
        int index = name.lastIndexOf("."); //$NON-NLS-1$
        if (index != -1) {
            name = name.substring(index + 1);
        }
        String name2 = element.getName();
        if (name2.length() == 0) {
            name2 = "/"; //$NON-NLS-1$
        }
        return name + ": " + name2; //$NON-NLS-1$
    }

    /**
     * Execute a runnable which performs an update of the model being displayed
     * by this provider. The runnable should be executed in a thread-safe manner
     * which results in the view being updated.
     * @param runnable the runnable which updates the model.
     * @param preserveExpansion whether the expansion of the view should be preserver
     * @param runInUIThread
     */
    public void performUpdate(IWorkspaceRunnable runnable, boolean preserveExpansion, boolean runInUIThread) {
        updateHandler.performUpdate(runnable, preserveExpansion, runInUIThread);
    }
}