public IStatus validateEdit()

in source/com.microsoft.tfs.client.eclipse/src/com/microsoft/tfs/client/eclipse/filemodification/TFSFileModificationValidator.java [127:463]


    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", files[i])); //$NON-NLS-1$

                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;
        }
    }