com.microsoft.tfs.client.common.ui.commands.search.WorkItemSearchCommand.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.tfs.client.common.ui.commands.search.WorkItemSearchCommand.java

Source

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

package com.microsoft.tfs.client.common.ui.commands.search;

import java.text.DateFormat;
import java.text.MessageFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

import com.microsoft.tfs.client.common.commands.TFSCommand;
import com.microsoft.tfs.client.common.repository.TFSRepository;
import com.microsoft.tfs.client.common.server.TFSServer;
import com.microsoft.tfs.client.common.ui.Messages;
import com.microsoft.tfs.client.common.ui.TFSCommonUIClientPlugin;
import com.microsoft.tfs.client.common.ui.framework.helper.UIHelpers;
import com.microsoft.tfs.client.common.ui.helpers.WorkItemEditorHelper;
import com.microsoft.tfs.client.common.ui.wit.results.QueryResultsEditor;
import com.microsoft.tfs.core.clients.webservices.PropertyValidation;
import com.microsoft.tfs.core.clients.workitem.CoreCategoryReferenceNames;
import com.microsoft.tfs.core.clients.workitem.CoreFieldReferenceNames;
import com.microsoft.tfs.core.clients.workitem.NonCoreFieldsReferenceNames;
import com.microsoft.tfs.core.clients.workitem.SupportedFeatures;
import com.microsoft.tfs.core.clients.workitem.WorkItemClient;
import com.microsoft.tfs.core.clients.workitem.fields.FieldDefinition;
import com.microsoft.tfs.core.clients.workitem.fields.FieldType;
import com.microsoft.tfs.core.clients.workitem.internal.fields.FieldDefinitionImpl;
import com.microsoft.tfs.core.clients.workitem.internal.type.WITypeConverter;
import com.microsoft.tfs.core.clients.workitem.internal.type.WITypeConverterException;
import com.microsoft.tfs.core.clients.workitem.internal.type.WIValueSource;
import com.microsoft.tfs.core.clients.workitem.project.Project;
import com.microsoft.tfs.core.clients.workitem.query.QueryDocument;
import com.microsoft.tfs.core.clients.workitem.query.QueryScope;
import com.microsoft.tfs.core.clients.workitem.query.qe.WIQLHelpers;
import com.microsoft.tfs.core.clients.workitem.query.qe.WIQLOperators;
import com.microsoft.tfs.core.exceptions.TECoreException;
import com.microsoft.tfs.core.search.IVSSearchFilterToken;
import com.microsoft.tfs.core.search.IVSSearchQuery;
import com.microsoft.tfs.core.search.IVSSearchQueryParser;
import com.microsoft.tfs.core.search.IVSSearchToken;
import com.microsoft.tfs.core.search.VSSearchFilterTokenType;
import com.microsoft.tfs.core.search.VSSearchUtils;
import com.microsoft.tfs.util.Check;
import com.microsoft.tfs.util.GUID;
import com.microsoft.tfs.util.LocaleInvariantStringHelpers;
import com.microsoft.tfs.util.LocaleUtil;
import com.microsoft.tfs.util.StringUtil;
import com.microsoft.tfs.util.datetime.LenientDateTimeParser;

@SuppressWarnings("restriction")
public class WorkItemSearchCommand extends TFSCommand {
    private static final Log log = LogFactory.getLog(WorkItemSearchCommand.class);

    private static final int SEARCH_TERM_LIMIT = 50;
    private static final int SEARCH_CHAR_LIMIT = 2000;

    private final TFSServer server;
    private final TFSRepository repository;
    private final Project project;
    private final String searchString;

    private WorkItemClient workItemClient;
    private final WorkItemSearchCriteria criteria;

    private final LenientDateTimeParser lenientDateTimeParser = new LenientDateTimeParser();
    private final DateFormat wiqlDateFormatter = DateFormat.getDateInstance(DateFormat.SHORT);
    private final DateFormat rangeErrorDateFormatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
            DateFormat.MEDIUM);

    public WorkItemSearchCommand(final TFSServer server, final TFSRepository repository, final Project project,
            final String searchString) {
        super();

        Check.notNull(server, "server"); //$NON-NLS-1$
        Check.notNull(repository, "repository"); //$NON-NLS-1$
        Check.notNull(project, "project"); //$NON-NLS-1$
        Check.notNull(searchString, "searchString"); //$NON-NLS-1$

        this.server = server;
        this.repository = repository;
        this.project = project;
        this.searchString = searchString;

        criteria = new WorkItemSearchCriteria();

        setCancellable(true);
    }

    @Override
    public String getName() {
        return Messages.getString("WorkItemSearchCommand.CommandText"); //$NON-NLS-1$
    }

    @Override
    public String getErrorDescription() {
        return Messages.getString("WorkItemSearchCommand.CommandErrorText"); //$NON-NLS-1$
    }

    @Override
    public String getLoggingDescription() {
        return Messages.getString("WorkItemSearchCommand.CommandText", LocaleUtil.ROOT); //$NON-NLS-1$
    }

    @Override
    protected IStatus doRun(final IProgressMonitor progressMonitor) throws Exception {
        this.workItemClient = repository.getVersionControlClient().getConnection().getWorkItemClient();

        if (searchString.length() > SEARCH_CHAR_LIMIT) {
            return new Status(Status.ERROR, TFSCommonUIClientPlugin.PLUGIN_ID,
                    MessageFormat.format(
                            Messages.getString("WorkItemSearchCommand.SearchPageExceededCharacterLimitFormat"), //$NON-NLS-1$
                            SEARCH_CHAR_LIMIT));
        }

        // It might just be a simple ID
        if (trySearchAsWorkItemID(searchString)) {
            return Status.OK_STATUS;
        }

        final IVSSearchQueryParser parser = VSSearchUtils.createSearchQueryParser();
        final IVSSearchQuery query = parser.parse(searchString);

        if (query.getSearchString() == null || query.getSearchString().length() == 0) {
            return new Status(Status.ERROR, TFSCommonUIClientPlugin.PLUGIN_ID,
                    "Cannot start a search with a query for a string that is null, empty or consisting only of white-space characters."); //$NON-NLS-1$
        }

        /*
         * TODO Maybe check parsedQuery.getParseSerror() at this point,
         * returning an error status in some cases? VS doesn't appear to do
         * this, instead just checking each token later.
         */

        int numTokens = query.getTokens(0, null);
        final IVSSearchToken[] tokens = new IVSSearchToken[numTokens];
        numTokens = query.getTokens(numTokens, tokens);

        if (numTokens > SEARCH_TERM_LIMIT) {
            return new Status(Status.ERROR, TFSCommonUIClientPlugin.PLUGIN_ID,
                    MessageFormat.format(
                            Messages.getString("WorkItemSearchCommand.SearchPageExceededTermLimitFormat"), //$NON-NLS-1$
                            SEARCH_TERM_LIMIT));
        }

        final String wiql = buildWIQL(tokens, true);

        if (progressMonitor.isCanceled()) {
            return Status.CANCEL_STATUS;
        }

        final QueryDocument queryDocument = server.getQueryDocumentService().createNewQueryDocument(wiql,
                project.getName(), QueryScope.PRIVATE);

        if (progressMonitor.isCanceled()) {
            return Status.CANCEL_STATUS;
        }

        UIHelpers.runOnUIThread(true, new Runnable() {
            @Override
            public void run() {
                QueryResultsEditor.openEditor(server, queryDocument);
            }
        });

        return Status.OK_STATUS;
    }

    private boolean trySearchAsWorkItemID(final String text) {
        try {
            final int id = Integer.parseInt(text);
            if (id > 0) {
                UIHelpers.runOnUIThread(true, new Runnable() {
                    @Override
                    public void run() {
                        WorkItemEditorHelper.openEditor(server, id);
                    }
                });
                return true;
            }
        } catch (final NumberFormatException e) {
        }

        return false;
    }

    private String buildWIQL(final IVSSearchToken[] tokens, final boolean includePreviewFields) {
        final StringBuilder wiql = new StringBuilder();

        wiql.append(buildSelect(includePreviewFields));
        wiql.append(buildWhere(tokens));
        wiql.append(buildOrder());

        return wiql.toString();
    }

    private String buildSelect(final boolean includePreviewFields) {
        final StringBuilder wiql = new StringBuilder();

        // Basic fields
        wiql.append("SELECT System.Id, "); //$NON-NLS-1$
        if (includePreviewFields) {
            // Hover preview fields
            wiql.append(getRequiredFieldsWiqlFragment());
        } else {
            wiql.append(StringUtil
                    .join(new String[] { CoreFieldReferenceNames.WORK_ITEM_TYPE, CoreFieldReferenceNames.TITLE,
                            CoreFieldReferenceNames.STATE, CoreFieldReferenceNames.ASSIGNED_TO }, ",")); //$NON-NLS-1$
        }

        wiql.append(" FROM workitems"); //$NON-NLS-1$

        return wiql.toString();
    }

    private String buildWhere(final IVSSearchToken[] tokens) {
        final StringBuilder wiql = new StringBuilder();

        wiql.append(" WHERE System.TeamProject = @project"); //$NON-NLS-1$
        for (final IVSSearchToken token : tokens) {
            wiql.append(" " + WIQLOperators.AND + " "); //$NON-NLS-1$//$NON-NLS-2$

            if (token instanceof IVSSearchFilterToken) {
                appendFilterCondition((IVSSearchFilterToken) token, wiql);
            } else {
                appendBasicCondition(token, wiql);
            }
        }

        final Project p = workItemClient.getProjects().get(project.getName());
        if (p != null
                && p.getWITContext().getServerInfo().isSupported(SupportedFeatures.WORK_ITEM_TYPE_CATEGORY_MEMBERS)
                && p.getCategories().contains(CoreCategoryReferenceNames.CODE_REVIEW_RESPONSE)) {
            // Exclude the code review response category
            wiql.append(" "); //$NON-NLS-1$
            wiql.append(StringUtil.join(
                    new String[] { WIQLOperators.AND,
                            WIQLHelpers.getEnclosedField(CoreFieldReferenceNames.WORK_ITEM_TYPE),
                            WIQLOperators.NOT_IN_GROUP,
                            WIQLHelpers.getSingleQuotedValue(CoreCategoryReferenceNames.CODE_REVIEW_RESPONSE) },
                    " ")); //$NON-NLS-1$
            wiql.append(" "); //$NON-NLS-1$
        }

        return wiql.toString();
    }

    private String buildOrder() {
        final StringBuilder wiql = new StringBuilder();

        // ORDER BY
        wiql.append(" "); //$NON-NLS-1$
        wiql.append(getOrderByDescendingCreatedDate());

        return wiql.toString();
    }

    private void appendFilterCondition(final IVSSearchFilterToken filterToken, final StringBuilder wiql) {
        final FieldDefinition field = workItemClient.getFieldDefinitions()
                .get(getFieldReferenceName(filterToken.getFilterField()));
        wiql.append(field.getReferenceName());

        wiql.append(" "); //$NON-NLS-1$

        final String operation = getOperator(field, filterToken.getFilterTokenType());
        wiql.append(operation);

        wiql.append(" "); //$NON-NLS-1$

        wiql.append(getInvariantFieldValue(field, filterToken.getFilterValue()));

        // need to do separate filtering for criteria builder.
        String fieldValue = filterToken.getFilterValue();
        final String macroCandidate = getInvariantMacro(field, filterToken.getFilterValue());
        if (macroCandidate != null && macroCandidate.length() > 0) {
            fieldValue = evaluateInvariantMacro(field, macroCandidate);
        }

        // add the criterion to the store of raw criteria information.
        criteria.addCriterion(field.getName(), operation, fieldValue);
    }

    /**
     * Return a WIQL operator based on the search operator and field.
     *
     * @param field
     *        The field the operator is used with.
     * @param filterOp
     *        VS search operator flags
     * @return WIQL operator
     */
    private String getOperator(final FieldDefinition field, final int filterOp) {
        // Determine which search token flags have been set.
        final boolean exactMatch = (filterOp & VSSearchFilterTokenType.EXACT_MATCH) != 0;
        final boolean exclude = (filterOp & VSSearchFilterTokenType.EXCLUDE) != 0;

        // Determine the correct WIQL operator.
        final FieldType fieldType = field.getFieldType();

        if (fieldType == FieldType.BOOLEAN || fieldType == FieldType.DOUBLE || fieldType == FieldType.GUID
                || fieldType == FieldType.INTEGER || fieldType == FieldType.INTERNAL
                || fieldType == FieldType.DATETIME) {
            // FIELDS SUPPORTING (NOT)EQUALS ONLY
            // (For these fields, both ":" and "=" are interpreted as "=")
            if (exclude) {
                return WIQLOperators.NOT_EQUAL_TO;
            }
            return WIQLOperators.EQUAL_TO;

        } else if (fieldType == FieldType.HISTORY || fieldType == FieldType.HTML
                || fieldType == FieldType.PLAINTEXT) {
            // FIELDS SUPPORTING (NOT)CONTAINS ONLY
            // (For these fields, both ":" and "=" are interpreted as ":")
            if (exclude) {
                return WIQLOperators.NOT_CONTAINS;
            }
            return WIQLOperators.CONTAINS;
        } else if (fieldType == FieldType.STRING) {
            // FIELDS SUPPORTING BOTH (NOT)EQUALS AND (NOT)CONTAINS
            if (exactMatch) {
                if (exclude) {
                    return WIQLOperators.NOT_EQUAL_TO;
                }
                return WIQLOperators.EQUAL_TO;
            } else {
                if (exclude) {
                    return WIQLOperators.NOT_CONTAINS;
                }
                return WIQLOperators.CONTAINS;
            }
        } else if (fieldType == FieldType.TREEPATH) {
            // SPECIAL CASE: TREE PATH
            if (exactMatch) {
                if (exclude) {
                    return WIQLOperators.NOT_EQUAL_TO;
                }
                return WIQLOperators.EQUAL_TO;
            } else {
                if (exclude) {
                    return WIQLOperators.NOT_UNDER;
                }
                return WIQLOperators.UNDER;
            }
        } else {
            // DEFAULT
            log.warn(
                    "Defaulting to equals / not equals operator; this situation should have been covered by one of the above cases."); //$NON-NLS-1$
            if (exclude) {
                return WIQLOperators.NOT_EQUAL_TO;
            }
            return WIQLOperators.EQUAL_TO;
        }
    }

    private String getInvariantMacro(final FieldDefinition field, final String localizedValue) {
        // is it a macro, replace with invariant macro
        // TODO: This implementation differs a fair bit from similar method in
        // FilterGridAdapter...
        // If subset behavior is intentional, this should be called out,
        // otherwise fixed.

        // today is special in that it can have (@today - x)
        if (LocaleInvariantStringHelpers.caseInsensitiveStartsWith(localizedValue,
                WIQLOperators.getLocalizedOperator(WIQLOperators.MACRO_TODAY))) {
            return WIQLOperators.getInvariantTodayMinusMacro(localizedValue);
        } else if (LocaleInvariantStringHelpers.caseInsensitiveEquals(localizedValue,
                WIQLOperators.getLocalizedOperator(WIQLOperators.MACRO_ME))) {
            return WIQLOperators.MACRO_ME;
        } else if (LocaleInvariantStringHelpers.caseInsensitiveEquals(localizedValue,
                WIQLOperators.getLocalizedOperator(WIQLOperators.MACRO_PROJECT))) {
            return WIQLOperators.MACRO_PROJECT;
        } else if (LocaleInvariantStringHelpers.caseInsensitiveEquals(localizedValue,
                WIQLOperators.getLocalizedOperator(WIQLOperators.MACRO_CURRENT_ITERATION))) {
            return WIQLOperators.MACRO_CURRENT_ITERATION;
        }
        return null;
    }

    /**
     * Note: This culture conversion is modelled on
     * GetInvariantFieldValueFromString() method in Query builder
     * implementation.
     *
     * \vset\SCM\workitemtracking\Controls\WinForms\QueryBuilder\
     * FilterGridAdapter.cs
     *
     * The two have similar functional requirements, but their current contracts
     * differ a fair bit, making code sharing impractical in current form.
     */
    private String getInvariantFieldValue(final FieldDefinition field, final String localValue) {
        String invariantResult = localValue;
        try {
            final String macroCandidate = getInvariantMacro(field, localValue);

            final boolean isEmpty = localValue.length() == 0;

            if (macroCandidate != null) {
                // Convert macros as you would operators
                invariantResult = macroCandidate;
            } else if (field.getSystemType().equals(Date.class) && !isEmpty) {
                // Convert date time values to invariant format using
                // LenientDateTimeParser, which is much more flexible than
                // WITypeConverter.
                final Date d = lenientDateTimeParser.parse(localValue, true, true).getTime();
                throwIfDateOutOfRange(d);
                invariantResult = wiqlDateFormatter.format(d);
                invariantResult = WIQLHelpers.getEscapedSingleQuotedValue(invariantResult);
            } else if (!isEmpty && field.getSystemType().equals(Integer.class)
                    || field.getSystemType().equals(Double.class) || field.getSystemType().equals(Boolean.class)) {
                // Generic object conversion.
                final WITypeConverter typeConverter = ((FieldDefinitionImpl) field).getTypeConverter();
                final Object ob = typeConverter.translate(localValue, WIValueSource.LOCAL);
                invariantResult = ob.toString();
            } else {
                final boolean isString = field.getSystemType().equals(String.class);
                final boolean isGuid = field.getSystemType().equals(GUID.class);
                if (isGuid || isString || isEmpty) {
                    invariantResult = WIQLHelpers.getEscapedSingleQuotedValue(localValue);
                }
            }
        } catch (final Exception e) {
            log.warn(MessageFormat.format("Error parsing field value {0}", localValue), e); //$NON-NLS-1$

            throw new RuntimeException(MessageFormat.format(
                    Messages.getString("WorkItemSearchCommand.SearchPageFormatExceptionFormat"), //$NON-NLS-1$
                    localValue, field.getName(), field.getFieldType().getDisplayName()));
        }

        return invariantResult;
    }

    private String getFieldReferenceName(final String name) {
        if (name.equalsIgnoreCase("A")) //$NON-NLS-1$
        {
            return CoreFieldReferenceNames.ASSIGNED_TO;
        } else if (name.equalsIgnoreCase("C")) //$NON-NLS-1$
        {
            return CoreFieldReferenceNames.CREATED_BY;
        } else if (name.equalsIgnoreCase("S")) //$NON-NLS-1$
        {
            return CoreFieldReferenceNames.STATE;
        } else if (name.equalsIgnoreCase("T")) //$NON-NLS-1$
        {
            return CoreFieldReferenceNames.WORK_ITEM_TYPE;
        } else {
            if (workItemClient.getFieldDefinitions().contains(name)) {
                return workItemClient.getFieldDefinitions().get(name).getReferenceName();
            }

            final String format = Messages.getString("WorkItemSearchCommand.ErrorSearchInvalidFieldNameFormat"); //$NON-NLS-1$
            throw new TECoreException(MessageFormat.format(format, name));
        }
    }

    private void appendBasicCondition(final IVSSearchToken token, final StringBuilder wiql) {
        final FieldDefinition titleField = workItemClient.getFieldDefinitions().get(CoreFieldReferenceNames.TITLE);
        final FieldDefinition descriptionField = workItemClient.getFieldDefinitions()
                .get(CoreFieldReferenceNames.DESCRIPTION);
        final FieldDefinition reproStepsField = workItemClient.getFieldDefinitions()
                .contains(NonCoreFieldsReferenceNames.REPRO_STEPS)
                        ? workItemClient.getFieldDefinitions().get(NonCoreFieldsReferenceNames.REPRO_STEPS)
                        : null;

        boolean exclude = false;
        String parsedValue = token.getParsedTokenText();

        // Only 'exclude' if the very first character is a '-' from a
        // non-literal (quoted) token.
        if (parsedValue.length() > 1 && parsedValue.charAt(0) == '-' && !isQuoted(token.getOriginalTokenText())) {
            exclude = true;
            parsedValue = parsedValue.substring(1);
        }

        final String clauseOperator = exclude ? WIQLOperators.AND : WIQLOperators.OR;

        final String escapedValue = WIQLHelpers.getEscapedSingleQuotedValue(parsedValue);

        // Use text search for all fields if available, for more efficient
        // search. For simplicity, we'll show "contains words" in the
        // description if Title supports text search, since Description and
        // ReproSteps are text fields and will as well then.
        final boolean useTextSearchIfAvailable = true;
        final boolean showContainsWords = useTextSearchIfAvailable && titleField.supportsTextQuery();

        wiql.append("("); //$NON-NLS-1$
        wiql.append(getContainsClause(titleField, escapedValue, exclude, useTextSearchIfAvailable));

        wiql.append(" " + clauseOperator + " "); //$NON-NLS-1$ //$NON-NLS-2$
        wiql.append(getContainsClause(descriptionField, escapedValue, exclude, useTextSearchIfAvailable));
        if (reproStepsField != null) {
            wiql.append(" " + clauseOperator + " "); //$NON-NLS-1$ //$NON-NLS-2$
            wiql.append(getContainsClause(reproStepsField, escapedValue, exclude, useTextSearchIfAvailable));
        }
        wiql.append(")"); //$NON-NLS-1$

        criteria.addCriterion(getKeywordSearchHelpText(exclude, titleField, descriptionField, reproStepsField),
                (exclude ? (showContainsWords ? WorkItemSearchCriteriaHelper.INVARIANT_KEYWORD_NOT_CONTAINS_WORDS
                        : WorkItemSearchCriteriaHelper.INVARIANT_KEYWORD_NOT_CONTAINS)
                        : (showContainsWords ? WorkItemSearchCriteriaHelper.INVARIANT_KEYWORD_CONTAINS_WORDS
                                : WorkItemSearchCriteriaHelper.INVARIANT_KEYWORD_CONTAINS)),
                parsedValue);
    }

    /*
     * TODO Move this method to some other place if/when we do work item
     * previews in Team Explorer.
     */
    /**
     * Fields (defined in WIQL format) used by the work item preview control.
     * These fields must be directly retrieved by the query to avoid paging work
     * item field data on the UI thread when opening the Work Item Preview.
     * <p>
     * This string is in the format
     * "System.FieldX, System.FieldY, System.FieldZ" (no ending comma) and can
     * be be treated as a self-contained fragment in the SELECT clause when
     * inserting into WIQL.
     * <p>
     * For example:
     * <code>String.Format("SELECT X, {0}, Y, Z FROM ...", WorkItemPreview.GetRequiredFieldsWiqlFragment(store))</code>
     *
     * @param store
     * @return
     */
    private String getRequiredFieldsWiqlFragment() {
        String retval = "System.Title, System.CreatedBy, System.CreatedDate, System.WorkItemType, System.State, System.AssignedTo, System.ChangedDate, System.AreaPath, System.IterationPath, System.Description"; //$NON-NLS-1$
        final boolean containsRepro = workItemClient.getFieldDefinitions()
                .contains(NonCoreFieldsReferenceNames.REPRO_STEPS);

        if (containsRepro) {
            retval += ", " + NonCoreFieldsReferenceNames.REPRO_STEPS; //$NON-NLS-1$
        }

        return retval;
    }

    /**
     * Returns a WIQL 'ORDER BY' clause (including 'ORDER BY') that sorts by
     * created date in descending order.
     */
    public static String getOrderByDescendingCreatedDate() {
        final StringBuilder wiql = new StringBuilder();
        wiql.append("ORDER BY ["); //$NON-NLS-1$
        wiql.append(CoreFieldReferenceNames.CREATED_DATE);
        wiql.append("] desc"); //$NON-NLS-1$
        return wiql.toString();
    }

    /**
     * Returns <code>true</code> if the specified string is contained in double
     * quotes, <code>false</code> otherwise
     */
    private boolean isQuoted(String text) {
        text = text.trim();
        return text.length() > 1 && text.charAt(0) == '"' && text.charAt(text.length() - 1) == '"';
    }

    /**
     * If the user is searching a bare term, we want to tell them what fields
     * are being searched (title, description and/or repro steps).
     *
     *
     * @param exclude
     *        Whether the search term is negated (used to determine whether the
     *        last separator is an "and" or "or").
     * @param titleField
     *        The title field.
     * @param descriptionField
     *        The description field.
     * @param reproStepsField
     *        The repro steps field, null if not present in this team project.
     * @return A user-friendly representation of the keyword fields being
     *         searched, in localized form - for example,
     *         "Title, Description, and[/or] Repro Steps" or
     *         "Title and[/or] Description".
     */
    private static String getKeywordSearchHelpText(final boolean exclude, final FieldDefinition titleField,
            final FieldDefinition descriptionField, final FieldDefinition reproStepsField) {
        String keywordSearchHelpText = ""; //$NON-NLS-1$
        if (reproStepsField != null) {
            final String format = exclude
                    ? Messages.getString("WorkItemSearchCommand.SearchPageKeywordNotHelpTextWithReproFormat") //$NON-NLS-1$
                    : Messages.getString("WorkItemSearchCommand.SearchPageKeywordHelpTextWithReproFormat"); //$NON-NLS-1$

            keywordSearchHelpText = MessageFormat.format(format, titleField.getName(), descriptionField.getName(),
                    reproStepsField.getName());
        } else {
            final String format = exclude
                    ? Messages.getString("WorkItemSearchCommand.SearchPageKeywordNotHelpTextWithoutReproFormat") //$NON-NLS-1$
                    : Messages.getString("WorkItemSearchCommand.SearchPageKeywordHelpTextWithoutReproFormat"); //$NON-NLS-1$

            keywordSearchHelpText = MessageFormat.format(format, titleField.getName(), descriptionField.getName());
        }
        return keywordSearchHelpText;
    }

    private String getContainsClause(final FieldDefinition field, final String value, final boolean exclude,
            final boolean useTextSearchIfAvailable) {
        // Use "ContainsWords" on String fields if available. "Contains" already
        // does text search for Text fields if available, and works with old
        // clients.
        final boolean useContainsWords = useTextSearchIfAvailable && !field.isLongText()
                && field.supportsTextQuery();
        final String op = exclude
                ? (useContainsWords ? WIQLOperators.NOT_CONTAINS_WORDS : WIQLOperators.NOT_CONTAINS)
                : (useContainsWords ? WIQLOperators.CONTAINS_WORDS : WIQLOperators.CONTAINS);

        return field.getReferenceName() + " " + op + " " + value; //$NON-NLS-1$ //$NON-NLS-2$
    }

    private String evaluateInvariantMacro(final FieldDefinition field, final String invariantMacro) {
        // is it a macro, replace with actual macro value
        if (field.getFieldType() == FieldType.DATETIME && LocaleInvariantStringHelpers
                .caseInsensitiveStartsWith(invariantMacro.trim(), WIQLOperators.MACRO_TODAY)) {
            // if it is only @today with no @today - n, then just return the
            // date
            if (invariantMacro.trim().equalsIgnoreCase(WIQLOperators.MACRO_TODAY)) {
                return wiqlDateFormatter.format(Calendar.getInstance().getTime());
            }

            // if the macro contains a '-' sign that isn't at the end of
            // the string
            boolean subtract = true;
            String[] macroTokens = invariantMacro.split(Pattern.quote("-")); //$NON-NLS-1$

            if (macroTokens.length != 2) {
                // try for the '+' sign
                macroTokens = invariantMacro.split(Pattern.quote("+")); //$NON-NLS-1$
                subtract = false;
            }

            // if there aren't two tokens, one on either side of the minus or
            // plus sign, the macro is ill-formed and we will not parse it.
            if (macroTokens.length != 2) {
                return invariantMacro;
            }

            int n;
            try {
                n = Integer.parseInt(macroTokens[1].trim());
            } catch (final NumberFormatException e) {
                return invariantMacro;
            }

            if (subtract) {
                n *= -1;
            }

            // return the date corresponding to today + (-n)
            final Calendar calendar = Calendar.getInstance();
            calendar.add(Calendar.DATE, n);
            throwIfDateOutOfRange(calendar.getTime());
            return wiqlDateFormatter.format(calendar.getTime());
        } else if (invariantMacro.equalsIgnoreCase(WIQLOperators.MACRO_ME)) {
            return workItemClient.getUserDisplayName();
        } else if (invariantMacro.equalsIgnoreCase(WIQLOperators.MACRO_PROJECT)) {
            return project.getName();
        }

        return invariantMacro;
    }

    /**
     * Checks that the {@link Date} is within the supported range of dates for
     * work item queries. This method is present because the messages from the
     * client-side WIQL parser and query builder are hard to understand in
     * overflow or underflow cases.
     *
     * @param date
     *        the date to check
     * @throws WITypeConverterException
     *         for consistency with core validation methods
     */
    private void throwIfDateOutOfRange(final Date date) throws WITypeConverterException {
        final Calendar cal = Calendar.getInstance();
        cal.setTime(date);

        if (cal.before(PropertyValidation.MIN_ALLOWED_DATE_TIME)
                || cal.after(PropertyValidation.MAX_ALLOWED_DATE_TIME)) {
            throw new WITypeConverterException(
                    MessageFormat.format(Messages.getString("WorkItemSearchCommand.DateOutOfRangeFormat"), //$NON-NLS-1$
                            rangeErrorDateFormatter.format(date),
                            rangeErrorDateFormatter.format(PropertyValidation.MIN_ALLOWED_DATE_TIME.getTime()),
                            rangeErrorDateFormatter.format(PropertyValidation.MAX_ALLOWED_DATE_TIME.getTime())));
        }
    }
}