com.microsoft.tfs.client.eclipse.filemodification.TFSFileModificationValidator.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.tfs.client.eclipse.filemodification.TFSFileModificationValidator.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.eclipse.filemodification;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFileModificationValidator;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;

import com.microsoft.tfs.client.common.commands.vc.EditCommand;
import com.microsoft.tfs.client.common.framework.command.CommandExecutor;
import com.microsoft.tfs.client.common.framework.command.ICommand;
import com.microsoft.tfs.client.common.framework.command.JobCommandAdapter;
import com.microsoft.tfs.client.common.framework.resources.LocationUnavailablePolicy;
import com.microsoft.tfs.client.common.framework.resources.Resources;
import com.microsoft.tfs.client.common.framework.resources.filter.CompositeResourceFilter.Builder;
import com.microsoft.tfs.client.common.framework.resources.filter.CompositeResourceFilter.CompositeResourceFilterType;
import com.microsoft.tfs.client.common.framework.resources.filter.ResourceFilter;
import com.microsoft.tfs.client.common.framework.resources.filter.ResourceFilterResult;
import com.microsoft.tfs.client.common.framework.resources.filter.ResourceFilters;
import com.microsoft.tfs.client.common.repository.TFSRepository;
import com.microsoft.tfs.client.common.util.ExtensionLoader;
import com.microsoft.tfs.client.eclipse.Messages;
import com.microsoft.tfs.client.eclipse.TFSEclipseClientPlugin;
import com.microsoft.tfs.client.eclipse.TFSRepositoryProvider;
import com.microsoft.tfs.client.eclipse.commands.eclipse.IgnoreResourceRefreshesEditCommand;
import com.microsoft.tfs.client.eclipse.project.ProjectRepositoryStatus;
import com.microsoft.tfs.client.eclipse.resource.PluginResourceFilters;
import com.microsoft.tfs.client.eclipse.resourcedata.ResourceDataManager;
import com.microsoft.tfs.core.clients.versioncontrol.GetOptions;
import com.microsoft.tfs.core.clients.versioncontrol.PendChangesOptions;
import com.microsoft.tfs.core.clients.versioncontrol.SupportedFeatures;
import com.microsoft.tfs.core.clients.versioncontrol.VersionControlConstants;
import com.microsoft.tfs.core.clients.versioncontrol.WorkspaceLocation;
import com.microsoft.tfs.core.clients.versioncontrol.path.ServerPath;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.ChangeType;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.LockLevel;
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.specs.ItemSpec;

/**
 * This is the actual file modification validator for the Eclipse plugin. It
 * does NOT implement {@link IFileModificationValidator} or extend
 * {@link org.eclipse.core.resources.team.FileModificationValidator} so that
 * other classes (which do) may proxy to this class safely. (
 * {@link org.eclipse.core.resources.team.FileModificationValidator} is new for
 * Eclipse 3.3.)
 *
 * @threadsafety unknown
 */
public final class TFSFileModificationValidator {
    private final static Log log = LogFactory.getLog(TFSFileModificationValidator.class);

    public static final String ADVISOR_EXTENSION_POINT_ID = "com.microsoft.tfs.client.eclipse.fileModificationAdvisor"; //$NON-NLS-1$

    /**
     * A property to disable the ignoring of events fired from the workspace
     * undo manager.
     */
    private static final String IGNORE_UNDO_MANAGER_PROPERTY_NAME = "com.microsoft.tfs.client.eclipse.filemodification.ignoreUndoManager"; //$NON-NLS-1$

    /**
     * Number of seconds to block waiting for checkouts to finish from the
     * server.
     */
    private final static long CHECKOUT_WAIT_TIME = 30;

    /**
     * Items which match this filter are made writable immediately in response
     * to an edit validation (no echanges are pended). This behavior allows
     * "ignored" resources (from .tpignore or Eclipse's team ignored resources)
     * to be edited by editors or other plug-ins without causing pending
     * changes.
     *
     * The linked resource filter is not part of this filter because linked
     * resources are not {@link IFile}s and won't be passed to
     * {@link #validateEdit(IFile[], boolean, Object)}.
     *
     * {@link PluginResourceFilters#TFS_IGNORE_FILTER} should not be used here
     * because it's only for local workspaces, where files are writable by
     * default and won't be validated by this class.
     */
    private final static ResourceFilter IGNORED_RESOURCES_FILTER = new Builder(
            CompositeResourceFilterType.ALL_MUST_ACCEPT)
                    .addFilter(PluginResourceFilters.TEAM_IGNORED_RESOURCES_FILTER)
                    .addFilter(ResourceFilters.TEAM_PRIVATE_RESOURCES_FILTER)
                    .addFilter(PluginResourceFilters.TPIGNORE_FILTER).build();

    private final TFSRepositoryProvider repositoryProvider;

    /**
     * The {@link backgroundFiles} map contains all files which are currently
     * being checked out on background threads, as well as their modification
     * stamp (before checkout). This allows us to rollback to this modification
     * stamp if the checkout fails.
     */
    private final Map<IFile, TFSFileModificationStatusData> backgroundFiles = new HashMap<IFile, TFSFileModificationStatusData>();

    private final Object advisorLock = new Object();
    private TFSFileModificationAdvisor advisor = null;

    public TFSFileModificationValidator(final TFSRepositoryProvider repositoryProvider) {
        this.repositoryProvider = repositoryProvider;
    }

    /*
     * Validate edit is the commonly called operation for when Eclipse notifies
     * us of resource changes.
     */
    public IStatus validateEdit(final IFile[] files, final boolean attemptUi, final Object shell) {
        final ResourceDataManager resourceDataManager = TFSEclipseClientPlugin.getDefault()
                .getResourceDataManager();
        final TFSRepository repository = repositoryProvider.getRepository();

        if (repositoryProvider.getRepositoryStatus() == ProjectRepositoryStatus.CONNECTING) {
            getStatusReporter(attemptUi, shell).reportError(
                    Messages.getString("TFSFileModificationValidator.ErrorConnectionInProgress"), //$NON-NLS-1$
                    new Status(IStatus.ERROR, TFSEclipseClientPlugin.PLUGIN_ID, 0,
                            Messages.getString("TFSFileModificationValidator.ErrorConnectionInProgressDescription"), //$NON-NLS-1$
                            null));

            return Status.CANCEL_STATUS;
        }

        /*
         * Offline server workspace. Simply mark files as writable and continue.
         */
        if (repository == null) {
            for (int i = 0; i < files.length; i++) {
                log.info(
                        MessageFormat.format("Setting {0} writable, project is offline from TFS server", files[i])); //$NON-NLS-1$

                files[i].setReadOnly(false);
            }

            return Status.OK_STATUS;
        }

        /*
         * Local workspace: ignore this entirely. This method is only called for
         * read-only files. A read only file in a local workspace was not placed
         * by us and we should not set them writable.
         */
        if (WorkspaceLocation.LOCAL.equals(repository.getWorkspace().getLocation())) {
            for (int i = 0; i < files.length; i++) {
                log.info(MessageFormat.format("Ignoring read-only file {0} in local TFS workspace", files[i])); //$NON-NLS-1$
            }

            return Status.OK_STATUS;
        }

        /*
         * HACK: avoid "phantom pending changes" that arise from the undo
         * manager. If we have no undoable context (ie, no Shell), then check to
         * see if we're being called from the undo manager and simply defer this
         * operation.
         *
         * This bug is hard to reproduce, so details are thus far limited. The
         * best guess is that the Eclipse undo manager watches edits using a
         * resource changed listener (for post-change and post-build
         * notifications.) It then realizes that the current contents of a file
         * are identical to a previous undo state, and therefore needs to
         * resynchronize the file history. It then, for some reason, calls
         * validateEdit on that file. This causes the file to be checked out
         * (here.)
         */
        if (attemptUi == false && shell == null) {
            /*
             * Since this is a terrible hack, we may need to have users disable
             * this functionality with a sysprop.
             */
            if ("false".equalsIgnoreCase(System.getProperty(IGNORE_UNDO_MANAGER_PROPERTY_NAME)) == false) //$NON-NLS-1$
            {
                /* Build an exception to get our stack trace. */
                final Exception e = new Exception(""); //$NON-NLS-1$
                e.fillInStackTrace();

                final StackTraceElement[] stackTrace = e.getStackTrace();

                if (stackTrace != null) {
                    for (int i = 0; i < stackTrace.length; i++) {
                        if (stackTrace[i].getClassName()
                                .equals("org.eclipse.ui.internal.ide.undo.WorkspaceUndoMonitor")) //$NON-NLS-1$
                        {
                            log.info("Ignoring file modification request from WorkspaceUndoMonitor"); //$NON-NLS-1$
                            return Status.OK_STATUS;
                        }
                    }
                }
            }
        }

        /* Pend edits for these files. */
        final List<String> pathList = new ArrayList<String>();
        final Set<String> projectSet = new HashSet<String>();

        for (int i = 0; i < files.length; i++) {
            if (IGNORED_RESOURCES_FILTER.filter(files[i]) == ResourceFilterResult.REJECT) {
                log.info(MessageFormat.format("Setting {0} writable, file matches automatic validation filter", //$NON-NLS-1$
                        files[i]));

                files[i].setReadOnly(false);

                continue;
            }

            /* Make sure that this file exists on the server. */
            if (!resourceDataManager.hasResourceData(files[i])
                    && resourceDataManager.hasCompletedRefresh(files[i].getProject())) {
                continue;
            }

            final String path = Resources.getLocation(files[i], LocationUnavailablePolicy.IGNORE_RESOURCE);
            final String serverPath = repository.getWorkspace().getMappedServerPath(path);

            if (path == null) {
                continue;
            }

            final PendingChange pendingChange = repository.getPendingChangeCache()
                    .getPendingChangeByLocalPath(path);

            /* Don't pend changes when there's already an add or edit pended. */
            if (pendingChange != null && (pendingChange.getChangeType().contains(ChangeType.ADD)
                    || pendingChange.getChangeType().contains(ChangeType.EDIT))) {
                log.debug(MessageFormat.format("File {0} has pending change {1}, ignoring", //$NON-NLS-1$
                        files[i], pendingChange.getChangeType().toUIString(true, pendingChange)));

                continue;
            }

            pathList.add(path);

            log.info(MessageFormat.format("File {0} is being modified, checking out", files[i])); //$NON-NLS-1$

            if (serverPath != null) {
                projectSet.add(ServerPath.getTeamProject(serverPath));
            }
        }

        if (pathList.size() == 0) {
            return Status.OK_STATUS;
        }

        LockLevel forcedLockLevel = null;
        boolean forcedGetLatest = false;

        /*
         * Query the server's default checkout lock and get latest on checkout
         * setting
         */
        for (final Iterator<String> i = projectSet.iterator(); i.hasNext();) {
            final String teamProject = i.next();

            final String exclusiveCheckoutAnnotation = repository.getAnnotationCache()
                    .getAnnotationValue(VersionControlConstants.EXCLUSIVE_CHECKOUT_ANNOTATION, teamProject, 0);
            final String getLatestAnnotation = repository.getAnnotationCache()
                    .getAnnotationValue(VersionControlConstants.GET_LATEST_ON_CHECKOUT_ANNOTATION, teamProject, 0);

            if ("true".equalsIgnoreCase(exclusiveCheckoutAnnotation)) //$NON-NLS-1$
            {
                forcedLockLevel = LockLevel.CHECKOUT;
                break;
            }

            /* Server get latest on checkout forces us to work synchronously */
            if ("true".equalsIgnoreCase(getLatestAnnotation)) //$NON-NLS-1$
            {
                forcedGetLatest = true;
            }
        }

        /* Allow UI hooks to handle prompt before checkout. */

        final TFSFileModificationOptions checkoutOptions = getOptions(attemptUi, shell,
                pathList.toArray(new String[pathList.size()]), forcedLockLevel);

        if (!checkoutOptions.getStatus().isOK()) {
            return checkoutOptions.getStatus();
        }

        final String[] paths = checkoutOptions.getFiles();
        final LockLevel lockLevel = checkoutOptions.getLockLevel();
        final boolean getLatest = checkoutOptions.isGetLatest();
        final boolean synchronousCheckout = checkoutOptions.isSynchronous();
        final boolean foregroundCheckout = checkoutOptions.isForeground();

        if (paths.length == 0) {
            return Status.OK_STATUS;
        }

        final ItemSpec[] itemSpecs = new ItemSpec[paths.length];
        for (int i = 0; i < paths.length; i++) {
            itemSpecs[i] = new ItemSpec(paths[i], RecursionType.NONE);
        }

        /*
         * Query get latest on checkout preference (and ensure server supports
         * the feature)
         */
        GetOptions getOptions = GetOptions.NO_DISK_UPDATE;
        PendChangesOptions pendChangesOptions = PendChangesOptions.NONE;

        if (repository.getWorkspace().getClient().getServerSupportedFeatures()
                .contains(SupportedFeatures.GET_LATEST_ON_CHECKOUT) && getLatest) {
            /*
             * If we're doing get latest on checkout, we need add the overwrite
             * flag: we need to set the file writable before this method exits
             * (in order for Eclipse to pick up the change, but we need to do
             * the get in another thread (so that we can clear the resource lock
             * on this file.) Thus we need to set the file writable, then fire a
             * synchronous worker to overwrite it. This is safe as this method
             * will ONLY be called when the file is readonly.
             */
            pendChangesOptions = PendChangesOptions.GET_LATEST_ON_CHECKOUT;
            getOptions = GetOptions.NONE;
        }

        /*
         * Build the checkout command - no need to query conflicts here, the
         * only conflicts that can arise from a pend edit are writable file
         * conflicts (when get latest on checkout is true.) This method is never
         * called for writable files.
         */
        final EditCommand editCommand = new EditCommand(repository, itemSpecs, lockLevel, null, getOptions,
                pendChangesOptions, false);

        /*
         * Pend changes in the foreground if get latest on checkout is
         * requested. A disk update may be required, so we want to block user
         * input.
         */
        if (synchronousCheckout || pendChangesOptions.contains(PendChangesOptions.GET_LATEST_ON_CHECKOUT)
                || forcedGetLatest) {
            /*
             * Wrap this edit command in one that disables the plugin's
             * automatic resource refresh behavior. This is required to avoid
             * deadlocks: the calling thread has taken a resource lock on the
             * resource it wishes to check out - the plugin will also require a
             * resource lock to do the refresh in another thread.
             */
            final ICommand wrappedEditCommand = new IgnoreResourceRefreshesEditCommand(editCommand);

            final IStatus editStatus = getSynchronousCommandExecutor(attemptUi, shell).execute(wrappedEditCommand);

            /* Refresh files on this thread, since it has the resource lock. */
            for (int i = 0; i < files.length; i++) {
                try {
                    files[i].refreshLocal(IResource.DEPTH_ZERO, new NullProgressMonitor());
                } catch (final Throwable e) {
                    log.warn(MessageFormat.format("Could not refresh {0}", files[i].getName()), e); //$NON-NLS-1$
                }
            }

            return editStatus;
        }
        /* Pend changes in the background */
        else {
            synchronized (backgroundFiles) {
                for (int i = 0; i < files.length; i++) {
                    files[i].setReadOnly(false);
                    backgroundFiles.put(files[i], new TFSFileModificationStatusData(files[i]));
                }
            }

            final JobCommandAdapter editJob = new JobCommandAdapter(editCommand);
            editJob.setPriority(Job.INTERACTIVE);
            editJob.setUser(foregroundCheckout);
            editJob.schedule();

            final Thread editThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    IStatus editStatus;

                    try {
                        /*
                         * We don't need to safe-wait with
                         * ExtensionPointAsyncObjectWaiter because we're
                         * guaranteed not on the UI thread.
                         */
                        editJob.join();
                        editStatus = editJob.getResult();
                    } catch (final Exception e) {
                        editStatus = new Status(IStatus.ERROR, TFSEclipseClientPlugin.PLUGIN_ID, 0, null, e);
                    }

                    if (editStatus.isOK()) {
                        synchronized (backgroundFiles) {
                            for (int i = 0; i < files.length; i++) {
                                final TFSFileModificationStatusData statusData = backgroundFiles.remove(files[i]);

                                if (statusData != null) {
                                    log.info(MessageFormat.format("File {0} checked out in {1} seconds", //$NON-NLS-1$
                                            files[i],
                                            (int) ((System.currentTimeMillis() - statusData.getStartTime())
                                                    / 1000)));
                                }
                            }
                        }
                    } else {
                        final List<TFSFileModificationStatusData> statusDataList = new ArrayList<TFSFileModificationStatusData>();

                        synchronized (backgroundFiles) {
                            for (int i = 0; i < files.length; i++) {
                                final TFSFileModificationStatusData statusData = backgroundFiles.remove(files[i]);

                                if (statusData != null) {
                                    log.info(MessageFormat.format("File {0} failed to check out in {1} seconds", //$NON-NLS-1$
                                            files[i],
                                            (int) ((System.currentTimeMillis() - statusData.getStartTime())
                                                    / 1000)));

                                    statusDataList.add(statusData);
                                }
                            }
                        }

                        /*
                         * Unfortunately, we have to roll back ALL FILES when an
                         * edit fails. We could (in theory) be better about this
                         * and use the non fatal listener in EditCommand to give
                         * us the paths that failed, but at the moment, the use
                         * case is only for one file at a time, so this is okay.
                         */
                        final TFSFileModificationStatusData[] statusData = statusDataList
                                .toArray(new TFSFileModificationStatusData[statusDataList.size()]);
                        getStatusReporter(attemptUi, shell).reportStatus(repository, statusData, editStatus);
                    }
                }
            });

            editThread.start();

            return Status.OK_STATUS;
        }
    }

    /*
     * Disallow save events while we're still checking out. With our revert /
     * accept writable conflict logic in the edit hook
     * (TFSFileModificationUiStatusReporter), this is probably unnecessary and
     * should be revisited.
     */
    public IStatus validateSave(final IFile file) {
        final TFSRepository repository = repositoryProvider.getRepository();

        /*
         * Local workspaces: ignore this save. (Modifications will get picked up
         * by the scanner, hinted by file modification validator.)
         */
        if (repository != null && WorkspaceLocation.LOCAL.equals(repository.getWorkspace().getLocation())) {
            return Status.OK_STATUS;
        }

        /*
         * Server workspace: ensure that the background checkout task completes
         * before we allow the save to proceed. (Block on the checkout.)
         */

        boolean inBackground = false;

        synchronized (backgroundFiles) {
            inBackground = (backgroundFiles.get(file) != null);
        }

        /* Wait for the background checkout to complete */
        final long startTime = System.currentTimeMillis();
        long lastNotifyTime = startTime;
        while (inBackground) {
            if (startTime == lastNotifyTime) {
                log.debug(MessageFormat.format(
                        "File {0} still being checked out from TFS server, waiting for checkout to succeed before allowing save event", //$NON-NLS-1$
                        file.getLocation().toOSString()));
            }

            if ((System.currentTimeMillis() - lastNotifyTime) > (CHECKOUT_WAIT_TIME * 1000)) {
                log.warn(MessageFormat.format("File {0} still being checked out from TFS server after {1} seconds.", //$NON-NLS-1$
                        file.getLocation().toOSString(), (int) ((System.currentTimeMillis() - startTime) / 1000)));
                lastNotifyTime = System.currentTimeMillis();
            }

            try {
                Thread.sleep(500);
            } catch (final InterruptedException e) {
                return Status.CANCEL_STATUS;
            }

            synchronized (backgroundFiles) {
                inBackground = (backgroundFiles.get(file) != null);
            }
        }

        return Status.OK_STATUS;
    }

    /**
     * Tests whether the specified file is currenlty being checked out by a
     * background thread.
     *
     * @param file
     *        the file to test (must not be <code>null</code>)
     * @return <code>true</code> if the file is being checked out, false if it
     *         is not
     */
    public boolean isCheckingOutFileInBackground(final IFile file) {
        synchronized (backgroundFiles) {
            return backgroundFiles.get(file) != null;
        }
    }

    /**
     * @return the {@link TFSFileModificationAdvisor} contributed via extension
     *         point, or <code>null</code> if none found
     */
    private final TFSFileModificationAdvisor getFileModificationAdvisor() {
        synchronized (advisorLock) {
            if (advisor == null) {
                try {
                    advisor = (TFSFileModificationAdvisor) ExtensionLoader
                            .loadSingleExtensionClass(ADVISOR_EXTENSION_POINT_ID);
                } catch (final Exception e) {
                    log.error("Could not load file modification advisor for the product", e); //$NON-NLS-1$
                    advisor = null;
                }
            }

            return advisor;
        }
    }

    private final TFSFileModificationOptions getOptions(final boolean attemptUi, final Object shell,
            final String[] files, final LockLevel forcedLockLevel) {
        final TFSFileModificationAdvisor advisor = getFileModificationAdvisor();

        if (advisor != null) {
            final TFSFileModificationOptionsProvider optionsProvider = advisor.getOptionsProvider(attemptUi, shell);

            if (optionsProvider != null) {
                return optionsProvider.getOptions(files, forcedLockLevel);
            }
        }

        log.warn("File modification advisor not present or did not provide options, using default"); //$NON-NLS-1$
        return new TFSFileModificationOptions(Status.OK_STATUS, files, LockLevel.UNCHANGED, true, true, false);
    }

    private final CommandExecutor getSynchronousCommandExecutor(final boolean attemptUi, final Object shell) {
        final TFSFileModificationAdvisor advisor = getFileModificationAdvisor();

        if (advisor != null) {
            final CommandExecutor executor = advisor.getSynchronousCommandExecutor(attemptUi, shell);

            if (executor != null) {
                return executor;
            }
        }

        log.warn("File modification advisor not present or did not provide a command executor, using default"); //$NON-NLS-1$
        return new CommandExecutor();
    }

    private final TFSFileModificationStatusReporter getStatusReporter(final boolean attemptUi, final Object shell) {
        final TFSFileModificationAdvisor advisor = getFileModificationAdvisor();

        if (advisor != null) {
            final TFSFileModificationStatusReporter statusReporter = advisor.getStatusReporter(attemptUi, shell);

            if (statusReporter != null) {
                return statusReporter;
            }
        }

        log.warn("File modification advisor not present or did not provide a status reporter, using default"); //$NON-NLS-1$
        return new TFSFileModificationStatusReporter();
    }
}