com.microsoft.tfs.core.checkinpolicies.PolicyEvaluator.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.tfs.core.checkinpolicies.PolicyEvaluator.java

Source

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

package com.microsoft.tfs.core.checkinpolicies;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.microsoft.tfs.core.Messages;
import com.microsoft.tfs.core.checkinpolicies.events.PolicyEvaluatorStateChangedEvent;
import com.microsoft.tfs.core.checkinpolicies.events.PolicyEvaluatorStateChangedListener;
import com.microsoft.tfs.core.checkinpolicies.events.PolicyLoadErrorEvent;
import com.microsoft.tfs.core.checkinpolicies.events.PolicyLoadErrorListener;
import com.microsoft.tfs.core.checkinpolicies.events.PolicyStateChangedEvent;
import com.microsoft.tfs.core.checkinpolicies.events.PolicyStateChangedListener;
import com.microsoft.tfs.core.checkinpolicies.internal.LoadErrorPolicy;
import com.microsoft.tfs.core.checkinpolicies.internal.PolicyEvaluationStatusComparator;
import com.microsoft.tfs.core.clients.CoreClientEvent;
import com.microsoft.tfs.core.clients.versioncontrol.VersionControlClient;
import com.microsoft.tfs.core.clients.versioncontrol.events.EventSource;
import com.microsoft.tfs.core.exceptions.TECoreException;
import com.microsoft.tfs.core.pendingcheckin.PendingCheckin;
import com.microsoft.tfs.core.pendingcheckin.events.AffectedTeamProjectsChangedEvent;
import com.microsoft.tfs.core.pendingcheckin.events.AffectedTeamProjectsChangedListener;
import com.microsoft.tfs.core.pendingcheckin.events.CheckedPendingChangesChangedEvent;
import com.microsoft.tfs.core.pendingcheckin.events.CheckedPendingChangesChangedListener;
import com.microsoft.tfs.core.pendingcheckin.events.CheckedWorkItemsChangedEvent;
import com.microsoft.tfs.core.pendingcheckin.events.CheckedWorkItemsChangedListener;
import com.microsoft.tfs.core.pendingcheckin.events.CommentChangedEvent;
import com.microsoft.tfs.core.pendingcheckin.events.CommentChangedListener;
import com.microsoft.tfs.util.Check;
import com.microsoft.tfs.util.Closable;
import com.microsoft.tfs.util.listeners.ListenerList;
import com.microsoft.tfs.util.listeners.ListenerRunnable;
import com.microsoft.tfs.util.listeners.StandardListenerList;
import com.microsoft.tfs.util.tasks.TaskMonitor;
import com.microsoft.tfs.util.tasks.TaskMonitorService;

/**
 * <p>
 * A long-lived object that manages evaluation of check-in policies. Most UI
 * controls will use a {@link PolicyEvaluator} to drive policy evaluation (and
 * user-initiated re-evaluation). Clients like the command-line client will use
 * one (for a short time) for evaluation.
 * </p>
 * <p>
 * After a {@link PolicyEvaluator} is constructed, it is initialized with a
 * pending checkin via {@link #setPendingCheckin(PendingCheckin)}.
 * </p>
 * <p>
 * Then, evaluation is done via the public {@link #evaluate(PolicyContext)}
 * method, or {@link #reloadAndEvaluate(PolicyContext)} if the policy
 * definitions and implementations should be re-loaded from the server. Policy
 * failures can be retrieved after evaluation with {@link #getFailures()}, or by
 * attaching listeners with
 * {@link #addPolicyStateChangedListener(PolicyStateChangedListener)}.
 * </p>
 * <p>
 * The general status of the object can be watched through events (
 * {@link #addPolicyEvaluatorStateChangedListener(PolicyEvaluatorStateChangedListener)}
 * ) or with {@link #getPolicyEvaluatorState()}. See
 * {@link PolicyEvaluatorState} for information on possible states.
 * </p>
 * <p>
 * A new pending check-in can be assigned at any time with
 * {@link #setPendingCheckin(PendingCheckin)}. The evaluator resets its state,
 * and is ready to evaluate again.
 * </p>
 * <p>
 * Call the {@link #close()} method when done with the evaluator, so it can
 * close its policy implementations (which may have large objects to release or
 * events to unhook).
 * </p>
 * <p>
 * <b>Context Notes</b>
 * </p>
 * <p>
 * Callers don't need to supply a {@link TaskMonitor} in the policy context they
 * pass to {@link #reloadAndEvaluate(PolicyContext)} or
 * {@link #evaluate(PolicyContext)}. The {@link PolicyEvaluator} will put one in
 * the context when it runs the policy instances.
 * </p>
 * <p>
 * <b>Handling Load Errors</b>
 * </p>
 * <p>
 * The evaluator doesn't normally throw exceptions during
 * {@link #evaluate(PolicyContext)} or {@link #reloadAndEvaluate(PolicyContext)}
 * if a configured policy could not be loaded from disk. Instead, the
 * evaluator's state is set to {@link PolicyEvaluatorState#POLICIES_LOAD_ERROR}
 * and the caller can test for this state with
 * {@link #getPolicyEvaluatorState()}. If the caller is interested in the load
 * error details (including exception data), it can register an event listener
 * via {@link #addPolicyLoadErrorListener(PolicyLoadErrorListener)} which is
 * fired as the load error happens.
 * </p>
 *
 * @see PolicyEvaluatorState
 * @since TEE-SDK-10.1
 * @threadsafety thread-safe
 */
public class PolicyEvaluator implements Closable {
    private static final Log log = LogFactory.getLog(PolicyEvaluator.class);

    private static final PolicyEvaluationStatusComparator policyStatusComparator = new PolicyEvaluationStatusComparator();

    /**
     * Contains {@link PolicyEvaluationStatus} objects, which contain the actual
     * instances we evaluate.
     */
    private List<PolicyEvaluationStatus> policyEvaluationStatuses = new ArrayList<PolicyEvaluationStatus>();

    private final VersionControlClient client;
    private final PolicyLoader policyLoader;
    private final ListenerList evaluatorStateChangedEventListeners = new StandardListenerList();
    private final ListenerList policyStateChangedEventListeners = new StandardListenerList();
    private final ListenerList policyLoadErrorEventListeners = new StandardListenerList();

    private PendingCheckin pendingCheckin;

    private final Object pendingCheckinLock = new Object();

    private final Object evaluatorLock = new Object();

    /**
     * Invoked when a policy status object we have changes its state.
     */
    private final PolicyStateChangedListener savedPolicyStateChangedEventListener = new PolicyStateChangedListener() {
        @Override
        public void onPolicyStateChanged(final PolicyStateChangedEvent e) {
            PolicyEvaluator.this.onPolicyStateChanged(e);
        }
    };

    /**
     * Invoked when the checked pending changes in our pending checkin changes.
     */
    private final CheckedPendingChangesChangedListener savedCheckedPendingChangesChangedListener = new CheckedPendingChangesChangedListener() {
        @Override
        public void onCheckedPendingChangesChanged(final CheckedPendingChangesChangedEvent e) {
            PolicyEvaluator.this.onPolicyEvaluatorStateChanged(e);
        }
    };

    /**
     * Invoked when a change to the pending checkin we watch causes the set of
     * affected pending changes to change.
     */
    private final AffectedTeamProjectsChangedListener savedAffectedTeamProjectsChangedListener = new AffectedTeamProjectsChangedListener() {
        @Override
        public void onAffectedTeamProjectsChanged(final AffectedTeamProjectsChangedEvent e) {
            PolicyEvaluator.this.onPolicyEvaluatorStateChanged(e);
        }
    };

    /**
     * Invoked when the check-in comment changes.
     */
    private final CommentChangedListener savedCommentChangedListener = new CommentChangedListener() {
        @Override
        public void onCommentChanged(final CommentChangedEvent e) {
            PolicyEvaluator.this.onPolicyEvaluatorStateChanged(e);
        }
    };

    private final CheckedWorkItemsChangedListener savedWorkItemsChagnedListener = new CheckedWorkItemsChangedListener() {
        @Override
        public void onCheckedWorkItemsChangesChanged(final CheckedWorkItemsChangedEvent e) {
            PolicyEvaluator.this.onPolicyEvaluatorStateChanged(e);
        }
    };

    /**
     * State of this evaluator.
     */
    private PolicyEvaluatorState evaluatorState = PolicyEvaluatorState.UNEVALUATED;

    /**
     * Builds a policy evaluator for the given {@link VersionControlClient}. No
     * checkin policies are automatically loaded or evaluated during
     * construction.
     */
    public PolicyEvaluator(final VersionControlClient client, final PolicyLoader policyLoader) {
        Check.notNull(client, "client"); //$NON-NLS-1$
        Check.notNull(policyLoader, "policyLoader"); //$NON-NLS-1$

        this.client = client;
        this.policyLoader = policyLoader;
    }

    /**
     * Add a listener for the event fired when the state of this evaluator
     * changes (perhaps because it has been given a new pending checkin or team
     * project to run policies for).
     *
     * @param listener
     *        the listener to add (must not be <code>null</code>)
     */
    public void addPolicyEvaluatorStateChangedListener(final PolicyEvaluatorStateChangedListener listener) {
        evaluatorStateChangedEventListeners.addListener(listener);
    }

    /**
     * Remove a listener for the event fired when the state of this evaluator
     * changes (perhaps because it has been given a new pending checkin or team
     * project to run policies for).
     *
     * @param listener
     *        the listener to remove (must not be <code>null</code>)
     */
    public void removePolicyEvaluatorStateChangedListener(final PolicyEvaluatorStateChangedListener listener) {
        evaluatorStateChangedEventListeners.removeListener(listener);
    }

    /**
     * Fires the given {@link PolicyEvaluatorStateChangedEvent}, which is
     * constructed automatically.
     */
    private void firePolicyEvaluatorStateChangedEvent() {
        final PolicyEvaluator evaluator = this;

        evaluatorStateChangedEventListeners.foreachListener(new ListenerRunnable() {
            @Override
            public boolean run(final Object listener) throws Exception {
                ((PolicyEvaluatorStateChangedListener) listener).onPolicyEvaluatorStateChanged(
                        new PolicyEvaluatorStateChangedEvent(EventSource.newFromHere(), evaluator));
                return true;
            }
        });
    }

    /**
     * @see PolicyInstance#addPolicyStateChangedListener(PolicyStateChangedListener)
     */
    public void addPolicyStateChangedListener(final PolicyStateChangedListener listener) {
        policyStateChangedEventListeners.addListener(listener);
    }

    /**
     * @see PolicyInstance#removePolicyStateChangedListener(PolicyStateChangedListener)
     */
    public void removePolicyStateChangedListener(final PolicyStateChangedListener listener) {
        policyStateChangedEventListeners.removeListener(listener);
    }

    /**
     * Fires the given {@link PolicyStateChangedEvent}, which must be
     * constructed manually.
     * <p>
     * {@link #firePolicyStateChangedEvent(PolicyFailure[])} fills in some
     * fields automatically, and is preferred to this method.
     *
     * @param event
     *        the event to fire (must not be <code>null</code>)
     * @see #firePolicyStateChangedEvent(PolicyFailure[])
     */
    private void firePolicyStateChangedEvent(final PolicyStateChangedEvent event) {
        Check.notNull(event, "event"); //$NON-NLS-1$

        policyStateChangedEventListeners.foreachListener(new ListenerRunnable() {
            @Override
            public boolean run(final Object listener) throws Exception {
                ((PolicyStateChangedListener) listener).onPolicyStateChanged(event);
                return true;
            }
        });
    }

    /**
     * Adds a listener for the {@link PolicyLoadErrorEvent}, which is fired when
     * a policy implementation fails to load during an
     * {@link #evaluate(PolicyContext)} or
     * {@link #reloadAndEvaluate(PolicyContext)} call.
     *
     * @param listener
     *        the listener to add (must not be <code>null</code>)
     */
    public void addPolicyLoadErrorListener(final PolicyLoadErrorListener listener) {
        policyLoadErrorEventListeners.addListener(listener);
    }

    /**
     * Removes a listener for the {@link PolicyLoadErrorEvent}, which is fired
     * when a policy implementation fails to load during an
     * {@link #evaluate(PolicyContext)} or
     * {@link #reloadAndEvaluate(PolicyContext)} call.
     *
     * @param listener
     *        the listener to remove (must not be <code>null</code>)
     */
    public void removePolicyLoadErrorListener(final PolicyLoadErrorListener listener) {
        policyLoadErrorEventListeners.removeListener(listener);
    }

    /**
     * Fires the given {@link PolicyLoadErrorEvent}, which must be constructed
     * manually.
     *
     * @param event
     *        the event to fire (must not be <code>null</code>)
     */
    private void firePolicyLoadErrorEvent(final PolicyLoadErrorEvent event) {
        Check.notNull(event, "event"); //$NON-NLS-1$

        policyLoadErrorEventListeners.foreachListener(new ListenerRunnable() {
            @Override
            public boolean run(final Object listener) throws Exception {
                ((PolicyLoadErrorListener) listener).onPolicyLoadError(event);
                return true;
            }
        });
    }

    /**
     * Sets the pending checkin used for policy evaluations. Resets the state of
     * the evaluator to {@link PolicyEvaluatorState#UNEVALUATED}.
     *
     * @param pendingCheckin
     *        the pending checkin used for policy evaluations (must not be
     *        <code>null</code>)
     */
    public void setPendingCheckin(final PendingCheckin pendingCheckin) {
        Check.notNull(pendingCheckin, "pendingCheckin"); //$NON-NLS-1$

        synchronized (pendingCheckinLock) {
            /*
             * Remove any listeners on the existing pending checkin (if there is
             * one).
             */
            removePendingCheckinEventListeners();

            this.pendingCheckin = pendingCheckin;

            /*
             * Add the new listeners. Removed on another call to this method, or
             * to close().
             */
            this.pendingCheckin.getPendingChanges()
                    .addAffectedTeamProjectsChangedListener(savedAffectedTeamProjectsChangedListener);

            this.pendingCheckin.getPendingChanges()
                    .addCheckedPendingChangesChangedListener(savedCheckedPendingChangesChangedListener);

            this.pendingCheckin.getPendingChanges().addCommentChangedListener(savedCommentChangedListener);

            this.pendingCheckin.getWorkItems().addCheckedWorkItemsChangedListener(savedWorkItemsChagnedListener);
        }

        synchronized (evaluatorLock) {
            evaluatorState = PolicyEvaluatorState.UNEVALUATED;
        }

        /*
         * Must happen outside synchronized to prevent deadlock.
         */
        firePolicyEvaluatorStateChangedEvent();
    }

    /**
     * Loads the policy definitions from the server appropriate for the pending
     * checkin previously passed to {@link #setPendingCheckin(PendingCheckin)}.
     * If the pending checkin that was supplied was null, this evaluator's
     * loaded policies are cleared.
     * <p>
     * Policy <b>implementations</b> are not necessarily reloaded when this
     * method is run. An implementation for a given policy type ID may be reused
     * between definition loads.
     * <p>
     * If some policies fail to load, the state of the evaluator will become
     * {@link PolicyEvaluatorState#POLICIES_LOAD_ERROR}. Details about the
     * problems can be retrieved as failures. This method tries hard to capture
     * all policy-related problems (including exceptions) and report them as
     * failures, so they can be presented to the user in the usual way and are
     * available to be sent to the server with an override comment.
     * <p>
     * <b>Event Warning</b>
     * <p>
     * This method does not invoke
     * {@link #firePolicyEvaluatorStateChangedEvent()} even though it changes
     * the evaluator state. The caller must fire the event after it invokes this
     * method.
     *
     * @param policyContext
     *        contextual settings that may include information about the user
     *        interface, etc. (must not be <code>null</code>)
     */
    private void loadPolicies(final PolicyContext policyContext) {
        Check.notNull(policyContext, "policyContext"); //$NON-NLS-1$

        final PendingCheckin currentPendingCheckin;

        synchronized (pendingCheckinLock) {
            currentPendingCheckin = pendingCheckin;
        }

        /*
         * Make a copy of the old policies so they can be disposed.
         */
        final List<PolicyEvaluationStatus> oldPolicies = policyEvaluationStatuses;

        /*
         * Use a new list for this evaluator's statuses.
         */
        policyEvaluationStatuses = new ArrayList<PolicyEvaluationStatus>();

        evaluatorState = PolicyEvaluatorState.UNEVALUATED;

        if (currentPendingCheckin == null) {
            return;
        }

        try {
            final String[] affectedTeamProjectServerPaths = currentPendingCheckin.getPendingChanges()
                    .getAffectedTeamProjectPaths();
            if (affectedTeamProjectServerPaths.length > 0) {
                // Get the definitions for all the paths from the server (TFS
                // annotation).
                final PolicyDefinition[] definitions = client
                        .getCheckinPoliciesForServerPaths(affectedTeamProjectServerPaths);

                // For each definition...
                for (int i = 0; i < definitions.length; i++) {
                    final PolicyDefinition definition = definitions[i];

                    if (definition.isEnabled()) {
                        /*
                         * See if we previously loaded a policy (from
                         * oldPolicies) loaded for the given type, and if so,
                         * don't reload it (but do set new priority, etc.). Skip
                         * implementations that are LoadErrorPolicy because
                         * they're generated each time this method runs.
                         */
                        final int oldPoliciesOriginalSize = oldPolicies.size();
                        int j;
                        for (j = 0; j < oldPolicies.size(); j++) {
                            final PolicyEvaluationStatus oldStatus = oldPolicies.get(j);

                            if ((oldStatus.getPolicy() instanceof LoadErrorPolicy == false)
                                    && oldStatus.getPolicyType().equals(definition.getType())) {
                                // Copy to the list of keepers.
                                policyEvaluationStatuses.add(oldStatus);

                                /*
                                 * Remove the policy from the old list so it
                                 * doesn't get disposed.
                                 */
                                oldPolicies.remove(j);

                                /*
                                 * Make sure to update the priority with our new
                                 * configuration.
                                 */
                                oldStatus.update(definition.getPriority(), definition.getScopeExpressions(),
                                        definition.getConfigurationMemento());

                                break;
                            }
                        }

                        /*
                         * If we searched the whole list and didn't find an old
                         * one, load a new implementation.
                         */
                        if (j == oldPoliciesOriginalSize) {
                            final PolicyLoader loader = getPolicyLoader();
                            PolicyInstance instance = null;

                            try {
                                /*
                                 * The loader can signal errors in two ways:
                                 * PolicyLoaderException, and a null return
                                 * value. A null return simply means the policy
                                 * wasn't found. Other (bigger) problems are
                                 * exceptions.
                                 */
                                instance = loader.load(definition.getType().getID());

                                if (instance == null) {
                                    /*
                                     * Add a surrogate policy implementation so
                                     * the failure appears in the UI for
                                     * failures and can also be sent to TFS
                                     * (which requires a failure when an
                                     * override comment is supplied).
                                     */
                                    log.warn(MessageFormat.format(
                                            Messages.getString("PolicyEvaluator.CouldNotLoadImplementationFormat"), //$NON-NLS-1$
                                            definition.getType().toString()));

                                    instance = new LoadErrorPolicy(MessageFormat.format(
                                            Messages.getString("PolicyEvaluator.NoImplementationFoundFormat"), //$NON-NLS-1$
                                            definition.getType().getID()), definition.getType());

                                    /*
                                     * Turning on the load error lets users of
                                     * this PolicyEvaluator know to raise
                                     * warnings, prevent check-ins, etc.
                                     */
                                    evaluatorState = PolicyEvaluatorState.POLICIES_LOAD_ERROR;
                                }
                            } catch (final PolicyLoaderException e) {
                                log.warn(MessageFormat.format("Exception loading check-in policy {0}", //$NON-NLS-1$
                                        definition.toString()), e);
                                evaluatorState = PolicyEvaluatorState.POLICIES_LOAD_ERROR;

                                instance = new LoadErrorPolicy(
                                        MessageFormat.format(
                                                Messages.getString("PolicyEvaluator.ExceptionLoadingPolicyFormat"), //$NON-NLS-1$
                                                definition.getType().getID(), e.getLocalizedMessage()),
                                        definition.getType());
                            }

                            /*
                             * We must configure the instance with the
                             * definition.
                             */
                            instance.loadConfiguration(definition.getConfigurationMemento());

                            /*
                             * Put a reference into the new list.
                             */
                            policyEvaluationStatuses.add(new PolicyEvaluationStatus(instance,
                                    definition.getPriority(), definition.getScopeExpressions()));
                        }

                    }
                }

                /*
                 * Sort the list by their internal priorities and save the new
                 * list.
                 */
                Collections.sort(policyEvaluationStatuses, policyStatusComparator);

                /*
                 * Initialize each policy. This happens to all loaded policies
                 * (even old ones) every time through this method.
                 *
                 * If initialize() throws (probably an implementation error),
                 * the status is replaced with a new one holding a
                 * LoadErrorPolicy (to better report the errors).
                 */
                for (int i = 0; i < policyEvaluationStatuses.size(); i++) {
                    final PolicyEvaluationStatus status = policyEvaluationStatuses.get(i);

                    try {
                        status.initialize(currentPendingCheckin, policyContext);
                        status.addPolicyStateChangedEventListener(savedPolicyStateChangedEventListener);
                    } catch (final Exception e) {
                        log.warn(MessageFormat.format("Exception initializing check-in policy {0}", //$NON-NLS-1$
                                status.getPolicyType().getName()), e);

                        evaluatorState = PolicyEvaluatorState.POLICIES_LOAD_ERROR;

                        policyEvaluationStatuses.set(i, new PolicyEvaluationStatus(
                                new LoadErrorPolicy(
                                        MessageFormat.format(
                                                Messages.getString(
                                                        "PolicyEvaluator.ExceptionInitializingPolicyFormat"), //$NON-NLS-1$
                                                status.getPolicyType().getID(), e.getLocalizedMessage()),
                                        status.getPolicyType()),
                                status.getPriority(), status.getScopeExpressions()));

                        // Call close last so we can use methods on status to
                        // build the replacement.
                        status.close();
                    }
                }

            }
        } catch (final Throwable t) {
            log.error("Generic error loading policies", t); //$NON-NLS-1$
            evaluatorState = PolicyEvaluatorState.POLICIES_LOAD_ERROR;

            /*
             * Clear our all our new statuses that we may have loaded before the
             * failure. The finally block will get the old ones.
             */
            closePolicyStatuses(
                    policyEvaluationStatuses.toArray(new PolicyEvaluationStatus[policyEvaluationStatuses.size()]));
            policyEvaluationStatuses = new ArrayList<PolicyEvaluationStatus>();

            /*
             * Rethrow the exception because this is probably a programming
             * error.
             */
            throw new TECoreException(Messages.getString("PolicyEvaluator.ErrorLoadingCheckinPolicies"), t); //$NON-NLS-1$
        } finally {
            /*
             * Close out the old statuses we no longer use.
             */
            closePolicyStatuses(oldPolicies.toArray(new PolicyEvaluationStatus[oldPolicies.size()]));
        }

        /*
         * A quick performance check for no policies found.
         */
        if (evaluatorState == PolicyEvaluatorState.UNEVALUATED && policyEvaluationStatuses.size() == 0) {
            evaluatorState = PolicyEvaluatorState.EVALUATED;
        }
    }

    /**
     * Reloads all policy definitions from the server and evaluates all policies
     * via {@link #evaluate(PolicyContext)}.
     *
     * @return the value returned by {@link #evaluate(PolicyContext)};
     * @throws PolicyEvaluationCancelledException
     *         if the user cancelled the policy evaluation.
     */
    public PolicyFailure[] reloadAndEvaluate(final PolicyContext policyContext)
            throws PolicyEvaluationCancelledException {
        Check.notNull(policyContext, "policyContext"); //$NON-NLS-1$

        synchronized (evaluatorLock) {
            // This triggers a reload of the definitions when evaluate()
            // runs. It will always fire the state changed event for us.
            evaluatorState = PolicyEvaluatorState.UNEVALUATED;

            return evaluate(policyContext);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void close() {
        synchronized (pendingCheckinLock) {
            removePendingCheckinEventListeners();
        }

        synchronized (evaluatorLock) {
            evaluatorStateChangedEventListeners.clear();

            closePolicyStatuses(
                    policyEvaluationStatuses.toArray(new PolicyEvaluationStatus[policyEvaluationStatuses.size()]));

            policyEvaluationStatuses.clear();
        }
    }

    /**
     * Closes the given statuses, removing this class's saved policy state
     * changed even listener in the process.
     */
    private void closePolicyStatuses(final PolicyEvaluationStatus[] statuses) {
        Check.notNull(statuses, "statuses"); //$NON-NLS-1$

        for (int i = 0; i < statuses.length; i++) {
            final PolicyEvaluationStatus s = statuses[i];

            if (s != null) {
                log.trace(MessageFormat.format("closing status for no-longer-needed policy type {0}", //$NON-NLS-1$
                        s.getPolicyType().getID()));

                /*
                 * Stop listening to events from the status, because policies
                 * can fire events at will (from timers, other threads, etc.).
                 */
                s.removePolicyStateChangedEventListener(savedPolicyStateChangedEventListener);

                /*
                 * Let the policy status do its own clean-up.
                 */
                try {
                    s.close();
                } catch (final Exception e) {
                    /*
                     * Log and continue.
                     */

                    log.error("Error closing policy status, continuing closing others", e); //$NON-NLS-1$
                }
            }
        }
    }

    /**
     * Removes the listeners we added to the pending checkin during
     * {@link #setPendingCheckin(PendingCheckin)}. Shared by
     * {@link #setPendingCheckin(PendingCheckin)} and {@link #close()}.
     */
    private void removePendingCheckinEventListeners() {
        if (pendingCheckin != null && pendingCheckin.getPendingChanges() != null) {
            pendingCheckin.getPendingChanges()
                    .removeAffectedTeamProjectsChangedListener(savedAffectedTeamProjectsChangedListener);

            pendingCheckin.getPendingChanges()
                    .removeCheckedPendingChangesChangedListener(savedCheckedPendingChangesChangedListener);
        }
    }

    /**
     * Evaluates checkin policies.
     *
     * @param policyContext
     *        contextual settings that may include information about the user
     *        interface, etc. (must not be <code>null</code>)
     * @return any failures detected.
     * @throws PolicyEvaluationCancelledException
     *         if the user canceled the policy evaluation.
     */
    public PolicyFailure[] evaluate(final PolicyContext policyContext) throws PolicyEvaluationCancelledException {
        Check.notNull(policyContext, "policyContext"); //$NON-NLS-1$

        log.trace("evaluate called"); //$NON-NLS-1$

        PolicyFailure[] failures = new PolicyFailure[0];

        final TaskMonitor taskMonitor = TaskMonitorService.getTaskMonitor();

        /*
         * Holds a loader exception so we can fire an event later (outside the
         * synchronized block).
         */
        Throwable loaderThrowable = null;

        try {
            synchronized (evaluatorLock) {
                try {
                    /*
                     * Load or reload policies if needed. We must be very
                     * careful not to fire the evaluator state changed until we
                     * have set the correct status, otherwise the event may tell
                     * a control to re-evaluate (which calls this method, which
                     * fires the event, etc.). If the state is correctly set,
                     * the control can know not to evaluate.
                     */
                    if (evaluatorState == PolicyEvaluatorState.UNEVALUATED
                            || evaluatorState == PolicyEvaluatorState.POLICIES_LOAD_ERROR
                            || evaluatorState == PolicyEvaluatorState.CANCELLED) {
                        try {
                            loadPolicies(policyContext);
                        } catch (final Throwable t) {
                            loaderThrowable = t;
                        }
                    }

                    /*
                     * The load may have failed, but the failures are converted
                     * to statuses each with a failure message. However, we must
                     * evaluate these load failure implementations to calculate
                     * their messages (and store the failures in the statuses).
                     */
                    boolean preserveLoadErrorState = false;
                    if (evaluatorState == PolicyEvaluatorState.POLICIES_LOAD_ERROR) {
                        preserveLoadErrorState = true;
                    }

                    /*
                     * Allocate one work unit for each policy status. 1 is big
                     * enough, since policy instances can begin new subtasks
                     * with as many work units as they desire, and those units
                     * will be scaled into 1 of these work units.
                     */
                    taskMonitor.begin(Messages.getString("PolicyEvaluator.EvaluatingCheckinPolicies"), //$NON-NLS-1$
                            policyEvaluationStatuses.size());

                    if (policyEvaluationStatuses.size() == 0) {
                        /*
                         * If there are no statuses (no policies configured for
                         * the current pending checkin), then become evaluated.
                         * Of course, don't set to evaluated if we're preserving
                         * an error.
                         */
                        if (preserveLoadErrorState == false) {
                            evaluatorState = PolicyEvaluatorState.EVALUATED;
                        }
                    } else {
                        /*
                         * Evaluate all the statuses.
                         */

                        for (int i = 0; i < policyEvaluationStatuses.size(); i++) {
                            if (taskMonitor.isCanceled()) {
                                throw new PolicyEvaluationCancelledException();
                            }

                            final PolicyEvaluationStatus status = policyEvaluationStatuses.get(i);

                            taskMonitor.setCurrentWorkDescription(
                                    MessageFormat.format(Messages.getString("PolicyEvaluator.EvaluatingFormat"), //$NON-NLS-1$
                                            status.getPolicyType().getName()));

                            TaskMonitor subTaskMonitor = null;

                            try {
                                subTaskMonitor = taskMonitor.newSubTaskMonitor(1);
                                policyContext.addProperty(PolicyContextKeys.TASK_MONITOR, subTaskMonitor);
                                status.evaluate(policyContext);
                            } finally {
                                if (subTaskMonitor != null) {
                                    subTaskMonitor.done();
                                }
                            }

                            /*
                             * Only change state to Evaluated if there wasn't a
                             * load error state before the evaluation that we
                             * must preserve.
                             */
                            if (preserveLoadErrorState == false) {
                                evaluatorState = PolicyEvaluatorState.EVALUATED;
                            }
                        }

                        /*
                         * Get all the failures from all of the statuses we
                         * evaluated.
                         */
                        failures = getFailures();
                    }
                } catch (final PolicyEvaluationCancelledException e) {
                    evaluatorState = PolicyEvaluatorState.CANCELLED;
                    throw e;
                } catch (final Exception e) {
                    /*
                     * These are probably programming errors, since load
                     * exceptions are handled in loadPolicies(), and evaluation
                     * failures are handled as failures.
                     */
                    log.error("Unhandled policy evaluation exception", e); //$NON-NLS-1$
                    evaluatorState = PolicyEvaluatorState.POLICIES_LOAD_ERROR;
                    failures = new PolicyFailure[0];

                    firePolicyLoadErrorEvent(new PolicyLoadErrorEvent(EventSource.newFromHere(), this, e));
                }
            }
        } finally {
            taskMonitor.done();

            /*
             * Fire an event for any exception we encountered.
             */
            if (loaderThrowable != null) {
                firePolicyLoadErrorEvent(
                        new PolicyLoadErrorEvent(EventSource.newFromHere(), this, loaderThrowable));
            }

            /*
             * Also fire an event for any failures that were load error
             * failures. This pattern lets us report multiple failures whereas
             * we could only ever handle one exception.
             */
            for (int i = 0; i < failures.length; i++) {
                final PolicyFailure policyFailure = failures[i];

                if (policyFailure.getPolicy() instanceof LoadErrorPolicy) {
                    firePolicyLoadErrorEvent(
                            new PolicyLoadErrorEvent(EventSource.newFromHere(), this, new PolicyLoaderException(
                                    policyFailure.getMessage(), policyFailure.getPolicy().getPolicyType())));
                }
            }

            /*
             * Fire once for all the conditions that change state. This must
             * happen outside the synchronized block to prevent deadlock (event
             * handlers will often call back into this class).
             */
            firePolicyEvaluatorStateChangedEvent();
        }

        return failures;
    }

    /**
     * @return the current state of this evaluator
     */
    public PolicyEvaluatorState getPolicyEvaluatorState() {
        synchronized (evaluatorLock) {
            return evaluatorState;
        }
    }

    /**
     * @return the count of policy definitions loaded. Can be 0 if
     *         {@link #loadPolicies(PolicyContext)} and
     *         {@link #setPendingCheckin(PendingCheckin)} were not called, or if
     *         no policies were defined or loaded for the current pending
     *         checkin.
     */
    public int getPolicyCount() {
        synchronized (evaluatorLock) {
            return policyEvaluationStatuses.size();
        }
    }

    /**
     * @return shallow copy of all the failures from all the policies evaluated
     *         by this evaluator. Do not modify the objects returned.
     */
    public PolicyFailure[] getFailures() {
        /*
         * TODO Introduce a cache here if needed. It can be updated in the
         * onPolicyStateChanged handler with the new failures.
         */

        final List<PolicyFailure> failures = new ArrayList<PolicyFailure>();
        synchronized (evaluatorLock) {
            for (final Iterator<PolicyEvaluationStatus> i = policyEvaluationStatuses.iterator(); i.hasNext();) {
                final PolicyEvaluationStatus status = i.next();

                final PolicyFailure[] theseFailures = status.getFailures();

                if (theseFailures != null) {
                    for (int j = 0; j < theseFailures.length; j++) {
                        failures.add(theseFailures[j]);
                    }
                }
            }
        }

        return failures.toArray(new PolicyFailure[failures.size()]);
    }

    /**
     * @return the {@link PolicyLoader} in use by this evaluator.
     */
    public PolicyLoader getPolicyLoader() {
        return policyLoader;
    }

    /**
     * Handles generic policy evaluator state events by settings the state to
     * {@link PolicyEvaluatorState#UNEVALUATED} and firing the event.
     */
    private void onPolicyEvaluatorStateChanged(final CoreClientEvent e) {
        synchronized (evaluatorLock) {
            evaluatorState = PolicyEvaluatorState.UNEVALUATED;
        }

        firePolicyEvaluatorStateChangedEvent();
    }

    /**
     * @see PolicyStateChangedListener
     */
    private void onPolicyStateChanged(final PolicyStateChangedEvent e) {
        /*
         * This method is invoked when one of our loaded PolicyEvaluationStatus
         * object fires its policy state changed event, which is actually
         * reflected up from the status's PolicyInstance object.
         */

        firePolicyStateChangedEvent(e);
    }

    /**
     * @return the pending checkin that was previously set via
     *         {@link #setPendingCheckin(PendingCheckin)}.
     */
    public PendingCheckin getPendingCheckin() {
        synchronized (pendingCheckinLock) {
            return pendingCheckin;
        }
    }

    /**
     * Returns a text error message (with newlines) suitable for printing in a
     * console window, log file, or other text area that describes a problem
     * loading a check-in policy implementation so the user can fix the problem.
     * Things like policy type ID, installation instructions, and sometimes
     * stack traces are formatted into the message.
     *
     * @param throwable
     *        the problem that caused the load failure, usually these are
     *        {@link PolicyLoaderException}, but they can be any kind of
     *        {@link Throwable} and the error message will be as descriptive as
     *        possible (must not be <code>null</code>)
     * @return the formatted error text.
     */
    public static String makeTextErrorForLoadException(final Throwable throwable) {
        Check.notNull(throwable, "throwable"); //$NON-NLS-1$

        final StringBuffer sb = new StringBuffer();

        if (throwable instanceof PolicyLoaderException
                && ((PolicyLoaderException) throwable).getPolicyType() != null) {
            /*
             * Additional details for policy loader exceptions with policy type
             * information.
             */

            sb.append(Messages.getString("PolicyEvaluator.RequiredCheckinPolicyFailedToLoad")); //$NON-NLS-1$

            final PolicyLoaderException ple = (PolicyLoaderException) throwable;
            sb.append(Messages.getString("PolicyEvaluator.NameColon") + ple.getPolicyType().getName() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$
            sb.append(Messages.getString("PolicyEvaluator.IDColon") + ple.getPolicyType().getID() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$
            sb.append(Messages.getString("PolicyEvaluator.InstallationInstructionsColon") //$NON-NLS-1$
                    + ple.getPolicyType().getInstallationInstructions() + "\n"); //$NON-NLS-1$

            sb.append(Messages.getString("PolicyEvaluator.ErrorColon") + throwable.getLocalizedMessage() + "\n\n"); //$NON-NLS-1$ //$NON-NLS-2$
        } else {
            /*
             * Run-time errors and other problems not handled during policy
             * loading are wrapped in TECoreException, but some other exception
             * types may come through (very rare).
             */

            sb.append(Messages.getString("PolicyEvaluator.AnErrorOccurredInThePolicyFramework")); //$NON-NLS-1$

            final StringWriter sw = new StringWriter();
            final PrintWriter pw = new PrintWriter(sw, true);
            throwable.printStackTrace(pw);
            pw.flush();
            sw.flush();

            sb.append(Messages.getString("PolicyEvaluator.ErrorColon") + sw.toString() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$
        }

        sb.append(Messages.getString("PolicyEvaluator.MoreDetailsMayBeAvailableInPlatformLogs")); //$NON-NLS-1$

        return sb.toString();
    }
}