com.microsoft.tfs.client.common.ui.wit.form.WorkItemLinksControl.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.tfs.client.common.ui.wit.form.WorkItemLinksControl.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.wit.form;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;

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.MessageBoxHelpers;
import com.microsoft.tfs.client.common.ui.framework.helper.TableViewerSorter;
import com.microsoft.tfs.client.common.ui.framework.helper.UIHelpers;
import com.microsoft.tfs.client.common.ui.framework.image.ImageHelper;
import com.microsoft.tfs.client.common.ui.framework.tooltip.IToolTipProvider;
import com.microsoft.tfs.client.common.ui.helpers.WorkItemEditorHelper;
import com.microsoft.tfs.client.common.ui.teamexplorer.helpers.ArtifactLinkHelpers;
import com.microsoft.tfs.client.common.ui.wit.OpenWorkItemWithAction;
import com.microsoft.tfs.client.common.ui.wit.form.link.LinkDialog;
import com.microsoft.tfs.client.common.ui.wit.form.link.LinkUIRegistry;
import com.microsoft.tfs.client.common.ui.wit.form.link.NewLinkedWorkItemAction;
import com.microsoft.tfs.core.artifact.ArtifactID;
import com.microsoft.tfs.core.clients.workitem.CoreFieldReferenceNames;
import com.microsoft.tfs.core.clients.workitem.WorkItem;
import com.microsoft.tfs.core.clients.workitem.WorkItemClient;
import com.microsoft.tfs.core.clients.workitem.WorkItemStateAdapter;
import com.microsoft.tfs.core.clients.workitem.WorkItemStateListener;
import com.microsoft.tfs.core.clients.workitem.exceptions.WorkItemLinkValidationException;
import com.microsoft.tfs.core.clients.workitem.fields.Field;
import com.microsoft.tfs.core.clients.workitem.fields.FieldDefinition;
import com.microsoft.tfs.core.clients.workitem.fields.FieldDefinitionCollection;
import com.microsoft.tfs.core.clients.workitem.fields.FieldType;
import com.microsoft.tfs.core.clients.workitem.fields.FieldUsages;
import com.microsoft.tfs.core.clients.workitem.form.WIFormLinkColumn;
import com.microsoft.tfs.core.clients.workitem.form.WIFormLinksControlExternalLinkFilters;
import com.microsoft.tfs.core.clients.workitem.form.WIFormLinksControlOptions;
import com.microsoft.tfs.core.clients.workitem.form.WIFormLinksControlWILinkFilters;
import com.microsoft.tfs.core.clients.workitem.form.WIFormLinksControlWITypeFilterEnum;
import com.microsoft.tfs.core.clients.workitem.form.WIFormLinksControlWITypeFilters;
import com.microsoft.tfs.core.clients.workitem.internal.link.LinkCollectionChangedListener;
import com.microsoft.tfs.core.clients.workitem.internal.link.LinkCollectionImpl;
import com.microsoft.tfs.core.clients.workitem.link.ExternalLink;
import com.microsoft.tfs.core.clients.workitem.link.Hyperlink;
import com.microsoft.tfs.core.clients.workitem.link.Link;
import com.microsoft.tfs.core.clients.workitem.link.LinkCollection;
import com.microsoft.tfs.core.clients.workitem.link.RegisteredLinkTypeNames;
import com.microsoft.tfs.core.clients.workitem.link.RelatedLink;
import com.microsoft.tfs.core.clients.workitem.link.WorkItemLinkTypeCollection;
import com.microsoft.tfs.core.clients.workitem.query.WorkItemCollection;
import com.microsoft.tfs.util.Check;

public class WorkItemLinksControl extends BaseWITComponentControl {
    private static final Log log = LogFactory.getLog(WorkItemLinksControl.class);

    private LinkUIRegistry linkUiRegistry;

    private IAction openAction;
    private IAction[] openWithActions;
    private IAction addAction;
    private IAction deleteAction;
    private IAction copyDescriptionToClipboardAction;
    private IAction copyURIToClipboardAction;

    private ColumnMetadata[] columnMetadatas;
    private final WIFormLinksControlOptions linksControlOptions;
    private LinkCollectionChangedListener linkCollectionChangedListener;
    private WorkItemStateListener workItemStateListener;

    private Button openButton;
    private Button newButton;
    private Button addButton;
    private Button deleteButton;

    private static String LINKATTRIBUTE_REFNAME_LINKTYPE = "System.Links.LinkType"; //$NON-NLS-1$
    private static String LINKATTRIBUTE_REFNAME_DESCRIPTION = "System.Links.Description"; //$NON-NLS-1$
    private static String LINKATTRIBUTE_REFNAME_COMMENT = "System.Links.Comment"; //$NON-NLS-1$

    private static String LINKATTRIBUTE_DISPNAME_LINKTYPE = Messages
            .getString("WorkItemLinksControl.ColumnNameLinkType"); //$NON-NLS-1$
    private static String LINKATTRIBUTE_DISPNAME_DESCRIPTION = Messages
            .getString("WorkItemLinksControl.ColumnNameDescription"); //$NON-NLS-1$
    private static String LINKATTRIBUTE_DISPNAME_COMMENT = Messages
            .getString("WorkItemLinksControl.ColumnNameComment"); //$NON-NLS-1$

    private final ImageHelper imageHelper = new ImageHelper(TFSCommonUIClientPlugin.PLUGIN_ID);

    public WorkItemLinksControl(final Composite parent, final int style, final TFSServer server,
            final WorkItem workItemInput, final WIFormLinksControlOptions options) {
        super(parent, style | SWT.MULTI, server, workItemInput);
        linksControlOptions = options;

        addDisposeListener(new DisposeListener() {
            @Override
            public void widgetDisposed(final DisposeEvent e) {
                imageHelper.dispose();
            }
        });
    }

    @Override
    protected void hookInit() {
        // Create and wire the actions for this control.
        setupActions();

        linkUiRegistry = new LinkUIRegistry(getServer(), getWorkItem(),
                (linksControlOptions == null) ? null : linksControlOptions.getWorkItemTypeFilters());

        // handle initial validation and populate table with the work item.
        bindWorkItemToTable();

        final WorkItem workItem = getWorkItem();

        // Create a listener for work item save events.
        workItemStateListener = new WorkItemStateAdapter() {
            @Override
            public void saved(final WorkItem workItem) {
                UIHelpers.runOnUIThread(getDisplay(), true, new Runnable() {
                    @Override
                    public void run() {
                        if (isDisposed()) {
                            return;
                        }

                        newButton.setEnabled(true);
                    }
                });
            }

            @Override
            public void synchedToLatest(final WorkItem workItem) {
                UIHelpers.runOnUIThread(getDisplay(), true, new Runnable() {
                    @Override
                    public void run() {
                        refresh();
                    }
                });
            }
        };

        // Add a listener for work item save events
        workItem.addWorkItemStateListener(workItemStateListener);

        // Create a listener for changes to the work items link collection.
        linkCollectionChangedListener = new LinkCollectionChangedListener() {
            @Override
            public void linkAdded(final Link link, final LinkCollection collection) {
                UIHelpers.runOnUIThread(getDisplay(), true, new Runnable() {
                    @Override
                    public void run() {
                        refresh();
                    }
                });
            }

            @Override
            public void linkRemoved(final Link link, final LinkCollection collection) {
                UIHelpers.runOnUIThread(getDisplay(), true, new Runnable() {
                    @Override
                    public void run() {
                        refresh();
                    }
                });
            }

            @Override
            public void linkTargetsUpdated(final LinkCollection collection) {
                UIHelpers.runOnUIThread(getDisplay(), true, new Runnable() {
                    @Override
                    public void run() {
                        refresh();
                    }
                });
            }
        };

        // Get access to the internal methods of the LinkCollection
        final LinkCollectionImpl linkCollectionImpl = (LinkCollectionImpl) workItem.getLinks();

        // Add a listener for changes to the link collection.
        linkCollectionImpl.addLinkCollectionChangedListener(linkCollectionChangedListener);

        // Remove the link collection and work item state listeners when this
        // link control is disposed.
        addDisposeListener(new DisposeListener() {
            @Override
            public void widgetDisposed(final DisposeEvent e) {
                linkCollectionImpl.removeLinkCollectionChangedListener(linkCollectionChangedListener);
                workItem.removeWorkItemStateListener(workItemStateListener);
            }
        });
    }

    private void performOpen() {
        final Link link = (Link) getSelectedItem();

        if (link instanceof Hyperlink) {
            ArtifactLinkHelpers.openHyperlinkLink(getShell(), ((Hyperlink) link).getLocation());
        } else if (link instanceof RelatedLink) {
            final RelatedLink relatedLink = (RelatedLink) link;
            WorkItem workItem;
            try {
                workItem = getServer().getConnection().getWorkItemClient()
                        .getWorkItemByID(relatedLink.getTargetWorkItemID());
            } catch (final Exception e) {
                throw new RuntimeException(e);
            }
            WorkItemEditorHelper.openEditor(getServer(), workItem);
        } else if (link instanceof ExternalLink) {
            final ArtifactID artifactID = ((ExternalLink) link).getArtifactID();
            ArtifactLinkHelpers.openArtifact(getShell(), artifactID);
        }
    }

    /**
     * Performs the action to open a related work item in a specific editor.
     *
     *
     * @param editorID
     *        The workbench ID of the editor.
     */
    private void performOpenRelatedWorkItemWith(final String editorID) {
        final Object selectedItem = getSelectedItem();
        Check.isTrue(selectedItem instanceof RelatedLink, "selectedItem instanceof RelatedLink"); //$NON-NLS-1$

        final RelatedLink relatedLink = (RelatedLink) selectedItem;
        WorkItem workItem;

        try {
            final int targetWorkItemID = relatedLink.getTargetWorkItemID();
            workItem = getServer().getConnection().getWorkItemClient().getWorkItemByID(targetWorkItemID);
        } catch (final Exception e) {
            throw new RuntimeException(e);
        }
        WorkItemEditorHelper.openEditor(getServer(), workItem, editorID);
    }

    private void performNew() {
        new NewLinkedWorkItemAction(getShell(), getServer(), getWorkItem(), linksControlOptions).run();
    }

    private void performAdd() {
        try {
            final LinkDialog dialog = new LinkDialog(getShell(), getWorkItem(), linkUiRegistry,
                    linksControlOptions);
            if (dialog.open() == IDialogConstants.OK_ID) {
                final Link[] links = dialog.getLinks();
                for (int i = 0; i < links.length; i++) {
                    getWorkItem().getLinks().add(links[i]);
                }
                getWorkItemForm().updateWorkItemLinkTargetsColumns();
            }
        } catch (final WorkItemLinkValidationException e) {
            MessageDialog.openError(getShell(), Messages.getString("WorkItemLinksControl.ErrorDialogTitle"), //$NON-NLS-1$
                    e.getLocalizedMessage());
        }
    }

    private void performDelete() {
        if (!MessageBoxHelpers.dialogConfirmPrompt(getShell(),
                Messages.getString("WorkItemLinksControl.ConfirmDialogTitle"), //$NON-NLS-1$
                Messages.getString("WorkItemLinksControl.ConfirmDialogText"))) //$NON-NLS-1$
        {
            return;
        }

        final Object[] selectedLinks = getSelectedItems();
        for (int i = 0; i < selectedLinks.length; i++) {
            getWorkItem().getLinks().remove((Link) selectedLinks[i]);
        }
    }

    private void performCopyDescriptionToClipboard() {
        UIHelpers.copyToClipboard(((Link) getSelectedItem()).getDescription());
    }

    private void copyURIToClipboard() {
        UIHelpers.copyToClipboard(((ExternalLink) getSelectedItem()).getURI());
    }

    /**
     * Returns the containing WorkItemForm for this control. Follows the parent
     * chain until the WorkItemForm is found. This method will never return null
     * and throws if a WorkItemForm is not found in the parent chain.
     *
     *
     * @return The WorkItemForm from the parent chain of this control.
     */
    private WorkItemForm getWorkItemForm() {
        Composite parent = getParent();
        while (parent != null && !(parent instanceof WorkItemForm)) {
            parent = parent.getParent();
        }

        if (parent != null) {
            return (WorkItemForm) parent;
        }

        throw new IllegalStateException(Messages.getString("WorkItemLinksControl.CantFindParent")); //$NON-NLS-1$
    }

    @Override
    protected void handleSelectionChanged(final Object[] selectedItems) {
        final boolean oneLinkSelected = (selectedItems.length == 1);
        final boolean openable = oneLinkSelected && isSelectedLinkOpenable();
        final boolean externalLink = oneLinkSelected && (getSelectedItem() instanceof ExternalLink);

        boolean deleteAllowed = (selectedItems.length > 0);
        for (int i = 0; i < selectedItems.length; i++) {
            final Link link = (Link) selectedItems[i];
            if (link.isReadOnly()) {
                deleteAllowed = false;
                break;
            }
        }

        openAction.setEnabled(openable);
        addAction.setEnabled(true);
        deleteAction.setEnabled(deleteAllowed);

        copyDescriptionToClipboardAction.setEnabled(oneLinkSelected);
        copyURIToClipboardAction.setEnabled(externalLink);

        openButton.setEnabled(openable);
        deleteButton.setEnabled(deleteAllowed);
    }

    private void setupActions() {
        openAction = new Action() {
            @Override
            public void run() {
                performOpen();
            }
        };
        openAction.setText(Messages.getString("WorkItemLinksControl.OpenActionText")); //$NON-NLS-1$

        final List<WorkItemEditorInfo> editors = WorkItemEditorHelper.getWorkItemEditors();
        if (editors != null && editors.size() > 0) {
            int count = 0;
            openWithActions = new OpenWorkItemWithAction[editors.size()];

            for (final WorkItemEditorInfo editor : editors) {
                openWithActions[count++] = new OpenWorkItemWithAction(editor.getDisplayName(),
                        editor.getEditorID()) {
                    @Override
                    public void run() {
                        performOpenRelatedWorkItemWith(this.getEditorID());
                    }
                };
            }
        }

        addAction = new Action() {
            @Override
            public void run() {
                performAdd();
            }
        };
        addAction.setText(Messages.getString("WorkItemLinksControl.AddActionText")); //$NON-NLS-1$

        deleteAction = new Action() {
            @Override
            public void run() {
                performDelete();
            }
        };
        deleteAction.setText(Messages.getString("WorkItemLinksControl.DeleteActionText")); //$NON-NLS-1$

        copyDescriptionToClipboardAction = new Action() {
            @Override
            public void run() {
                performCopyDescriptionToClipboard();
            }
        };
        copyDescriptionToClipboardAction
                .setText(Messages.getString("WorkItemLinksControl.CopyDescriptionActionText")); //$NON-NLS-1$

        copyURIToClipboardAction = new Action() {
            @Override
            public void run() {
                copyURIToClipboard();
            }
        };
        copyURIToClipboardAction.setText(Messages.getString("WorkItemLinksControl.CopyUriActionText")); //$NON-NLS-1$
    }

    @Override
    protected IToolTipProvider getToolTipProvider() {
        return new IToolTipProvider() {
            @Override
            public String getToolTipText(final Object element) {
                final Link link = (Link) element;

                if (link.isReadOnly()) {
                    return Messages.getString("WorkItemLinksControl.LockedLinkTooltipText"); //$NON-NLS-1$
                }

                final StringBuffer buffer = new StringBuffer();

                String messageFormat;
                String message;

                messageFormat = Messages.getString("WorkItemLinksControl.LinkTypeTooltipFormat"); //$NON-NLS-1$
                message = MessageFormat.format(messageFormat, link.getLinkType().getName());
                buffer.append(message);
                buffer.append(NEWLINE);

                messageFormat = Messages.getString("WorkItemLinksControl.CommentTooltipFormat"); //$NON-NLS-1$
                message = MessageFormat.format(messageFormat, (link.getComment() != null ? link.getComment() : "")); //$NON-NLS-1$
                buffer.append(message);
                buffer.append(NEWLINE);

                if (link instanceof RelatedLink) {
                    final int workItemId = ((RelatedLink) link).getTargetWorkItemID();
                    messageFormat = Messages.getString("WorkItemLinksControl.RelatedItemTooltipFormat"); //$NON-NLS-1$
                    message = MessageFormat.format(messageFormat, Integer.toString(workItemId));
                    buffer.append(message);
                } else if (link instanceof Hyperlink) {
                    final String location = ((Hyperlink) link).getLocation();
                    messageFormat = Messages.getString("WorkItemLinksControl.LocationTooltipFormat"); //$NON-NLS-1$
                    message = MessageFormat.format(messageFormat, location);
                    buffer.append(message);
                } else {
                    final String uri = ((ExternalLink) link).getURI();
                    messageFormat = Messages.getString("WorkItemLinksControl.UriTooltipFormat"); //$NON-NLS-1$
                    message = MessageFormat.format(messageFormat, uri);
                    buffer.append(message);
                }

                return buffer.toString();
            }
        };
    }

    @Override
    protected void fillMenuBeforeShow(final IMenuManager manager) {
        manager.add(openAction);

        if (openWithActions != null && selectionIsSingleRelatedLink()) {
            final IMenuManager subMenu = new MenuManager(
                    Messages.getString("QueryResultsControl.OpenWithCommandText")); //$NON-NLS-1$
            manager.add(subMenu);
            manager.add(new Separator());

            for (final IAction action : openWithActions) {
                subMenu.add(action);
            }
        }

        manager.add(addAction);
        manager.add(deleteAction);

        manager.add(new Separator());

        manager.add(copyDescriptionToClipboardAction);
        manager.add(copyURIToClipboardAction);
    }

    @Override
    protected Object[] getItemsFromWorkItem(final WorkItem workItem) {
        // If no options were supplied there is no filtering to perform.
        if (linksControlOptions == null) {
            final ArrayList<Link> links = new ArrayList<Link>();
            for (final Link link : workItem.getLinks()) {
                if (!link.isPendingDelete()) {
                    links.add(link);
                }
            }
            return links.toArray();
        }

        // Filter the links based on the specified options.
        final WorkItemLinkTypeCollection linkTypes = workItem.getClient().getLinkTypes();
        final WIFormLinksControlWILinkFilters wiLinkFilters = linksControlOptions.getWorkItemLinkFilters();
        final WIFormLinksControlWITypeFilters wiTypeFilters = linksControlOptions.getWorkItemTypeFilters();
        final WIFormLinksControlExternalLinkFilters externalLinkFilters = linksControlOptions
                .getExternalLinkFilters();

        // Filter the full list by link type. Track the IDs of work item links.
        final ArrayList<Link> filteredByLinkType = new ArrayList<Link>();
        final ArrayList<Link> workItemLinks = new ArrayList<Link>();

        for (final Link link : workItem.getLinks()) {
            if (link.isPendingDelete()) {
                continue;
            }

            if (link instanceof ExternalLink || link instanceof Hyperlink) {
                if (externalLinkFilters == null || externalLinkFilters.includes(link.getLinkType().getName())) {
                    filteredByLinkType.add(link);
                }
            } else if (link instanceof RelatedLink) {
                final int linkTypeId = ((RelatedLink) link).getWorkItemLinkTypeID();
                final String linkReferenceName = linkTypes.getReferenceName(linkTypeId);
                final boolean isForward = linkTypes.isForwardLink(linkTypeId);
                final boolean isReverse = linkTypes.isReverseLink(linkTypeId);

                if (wiLinkFilters == null || wiLinkFilters.includes(linkReferenceName, isForward, isReverse)) {
                    filteredByLinkType.add(link);
                    workItemLinks.add(link);
                }
            }
        }

        // If there are no work-item type filters, we are done.
        if (wiTypeFilters == null || workItemLinks.size() == 0) {
            return filteredByLinkType.toArray();
        }

        // If the work-item type filter says INCLUDEALL we are done.
        final WIFormLinksControlWITypeFilterEnum filterType = wiTypeFilters.getFilter();
        if (filterType == WIFormLinksControlWITypeFilterEnum.INCLUDEALL) {
            return filteredByLinkType.toArray();
        }

        // Create a SQL query to further filter the list by work item type.
        final int[] candidateWorkItemIds = new int[workItemLinks.size()];
        for (int i = 0; i < workItemLinks.size(); i++) {
            candidateWorkItemIds[i] = ((RelatedLink) workItemLinks.get(i)).getTargetWorkItemID();
        }

        final String projectName = workItem.getType().getProject().getName();
        final String wiql = wiTypeFilters.createFilterWIQLQuery(candidateWorkItemIds, projectName);

        // TODO: try/catch here?
        final WorkItemCollection workItems = workItem.getClient().query(wiql);

        // The workItem IDs returned from the query are the work item links to
        // keep.
        final HashSet<Integer> mapWorkItemIds = new HashSet<Integer>();
        final int[] workItemIds = workItems.getIDs();
        for (int i = 0; i < workItemIds.length; i++) {
            mapWorkItemIds.add(new Integer(workItemIds[i]));
        }

        final ArrayList<Link> filteredByWorkItemType = new ArrayList<Link>();
        for (int i = 0; i < filteredByLinkType.size(); i++) {
            final Link link = filteredByLinkType.get(i);

            if (link instanceof RelatedLink) {
                final Integer boxedId = new Integer(((RelatedLink) link).getTargetWorkItemID());
                if (mapWorkItemIds.contains(boxedId)) {
                    filteredByWorkItemType.add(link);
                }
            } else {
                filteredByWorkItemType.add(link);
            }
        }

        return filteredByWorkItemType.toArray();
    }

    /**
     * Returns the display names for each column in the result grid. For columns
     * which reference a work item field, the display name comes from the set of
     * work item field definitions maintained by the work item client. For
     * column which reference a link attribute, the display name is a hard coded
     * value.
     *
     * In addition to building the list of display names, this method
     * initializes the columnMetadatas member of this link control. The column
     * metadata is used to help identify the correct property or field being
     * represented by this column when text is requested for the cell in the
     * grid.
     */
    @Override
    protected String[] getTableColumnNames() {
        final ArrayList<ColumnMetadata> listColumnMetadata = new ArrayList<ColumnMetadata>();
        final ArrayList<String> listColumnWorkItemReferences = new ArrayList<String>();

        // Default to link properties if no columns specified, or created
        // columns defined in options.
        if (linksControlOptions == null || linksControlOptions.getLinkColumns() == null) {
            // No columns specified. Default to the link properties.
            listColumnMetadata
                    .add(new ColumnMetadata(LINKATTRIBUTE_REFNAME_LINKTYPE, LINKATTRIBUTE_DISPNAME_LINKTYPE, true));

            listColumnMetadata.add(new ColumnMetadata(LINKATTRIBUTE_REFNAME_DESCRIPTION,
                    LINKATTRIBUTE_DISPNAME_DESCRIPTION, true));

            listColumnMetadata
                    .add(new ColumnMetadata(LINKATTRIBUTE_REFNAME_COMMENT, LINKATTRIBUTE_DISPNAME_COMMENT, true));
        } else {
            // Column definitions appear in the link control options. Define the
            // column metadatas.
            final WIFormLinkColumn[] columns = linksControlOptions.getLinkColumns().getLinkColumns();
            final FieldDefinitionCollection fieldDefinitions = getWorkItem().getClient().getFieldDefinitions();

            // Always place the link type column first since we don't do
            // grouping.
            listColumnMetadata.add(0,
                    new ColumnMetadata(LINKATTRIBUTE_REFNAME_LINKTYPE, LINKATTRIBUTE_DISPNAME_LINKTYPE, true));

            for (int i = 0; i < columns.length; i++) {
                final WIFormLinkColumn column = columns[i];
                if (column.getLinkAttribute() != null) {
                    // Column definition is for a link property.
                    final String linkAttribute = column.getLinkAttribute();

                    // Skip the link type column if encountered since we always
                    // add it as the first column.
                    if (!linkAttribute.equalsIgnoreCase(LINKATTRIBUTE_REFNAME_LINKTYPE)) {
                        final String displayName = linkAttributeToDisplayName(linkAttribute);
                        if (displayName != null) {
                            listColumnMetadata.add(new ColumnMetadata(linkAttribute, displayName, true));
                        }
                    }
                } else if (column.getRefName() != null) {
                    // Column definition is for a work item field.
                    final String wiRefName = column.getRefName();
                    final FieldDefinition fieldDef = fieldDefinitions.get(wiRefName);

                    // Exclude this column if the reference name is not intended
                    // for use with work items. Any column that we accept here
                    // ultimately appears as a column name in a SELECT statement
                    // for the WORK ITEMS table which retrieve the data to
                    // display
                    // in this column. The EMC SCRUM template includes a column
                    // with a reference name for LinkType which is not a WIT
                    // field and should be excluded. The LinkType column is
                    // always
                    // included with TEE, so the EMC template still works as
                    // intended.
                    if (fieldDef != null && fieldDef.getUsage() == FieldUsages.WORK_ITEM) {
                        Comparator<Link> comparator = null;
                        final FieldType type = fieldDef.getFieldType();

                        // Instantiate a sorting comparator for this column if
                        // it is a WIT field with type INTEGER, DOUBLE, or
                        // DATETIME. All other field types are compared as
                        // strings.
                        if (type == FieldType.INTEGER || type == FieldType.DOUBLE || type == FieldType.DATETIME) {
                            comparator = new RelatedLinkFieldComparator(wiRefName);
                        }

                        listColumnMetadata
                                .add(new ColumnMetadata(wiRefName, fieldDef.getName(), false, comparator));
                        listColumnWorkItemReferences.add(wiRefName);
                    }
                }
            }
        }

        // Initialize the column metadata and work item field reference members
        columnMetadatas = listColumnMetadata.toArray(new ColumnMetadata[listColumnMetadata.size()]);

        // Merge the field reference names defined for columns on this control
        // into the set
        // of all WIT columns maintained by the LinksCollection across all
        // LinksControls on
        // the WIT form.
        if (listColumnWorkItemReferences.size() > 0) {
            final String[] fieldReferences = listColumnWorkItemReferences
                    .toArray(new String[listColumnWorkItemReferences.size()]);

            ((LinkCollectionImpl) getWorkItem().getLinks()).mergeColumnFieldReferences(fieldReferences);
        }

        // Get the display names.
        final String[] columnNames = new String[columnMetadatas.length];
        for (int i = 0; i < columnNames.length; i++) {
            columnNames[i] = columnMetadatas[i].getDisplayName();
        }

        // Return the display names for each column.
        return columnNames;
    }

    /**
     * Override addSorting to supply sort comparators for columns that require
     * non-string comparisons.
     */
    @Override
    protected void addSorting(final TableViewer tableViewer) {
        final TableViewerSorter sorter = new TableViewerSorter(tableViewer);

        for (int i = 0; i < columnMetadatas.length; i++) {
            final ColumnMetadata columnMetadata = columnMetadatas[i];
            final Comparator<Link> comparator = columnMetadata.getComparator();

            if (comparator != null) {
                sorter.setComparator(i, comparator);
            }
        }

        tableViewer.setSorter(sorter);
    }

    /**
     * Returns the image to be displayed in the specified column of the grid for
     * the specified row.
     *
     * @param element
     *        The object associated with the row in the grid (a link).
     *
     * @param columnIndex
     *        The index of the column we want to retrieve an image for.
     *
     * @returns Returns the image for the cell in teh grid for the given
     *          row/column. Returns null if there is no image for the cell.
     */
    @Override
    protected Image getImageForColumn(final Object element, final int columnIndex) {
        if (columnIndex == 0) {
            final Link link = (Link) element;

            if (link.isReadOnly()) {
                return imageHelper.getImage("/images/wit/LockedLink.gif"); //$NON-NLS-1$
            }
        }

        return null;
    }

    /**
     * Returns the text to be displayed in the specified column of the grid for
     * the specified row. The returned text comes from either a field on the
     * linked work item (when the link is a work item link) or it comes from a
     * property on the link itself. It is valid for there to be no data
     * available for the specified row/column. For example, the column which
     * displays a field from a work item link will always be empty when the link
     * is not a releatd link.
     *
     * @param element
     *        The object associated with a row in the grid (a link).
     *
     * @param columnIndex
     *        The index of the column we want to retrieve text for.
     *
     * @returns Returns the display text for the cell in the grid for the given
     *          row/column. Returns null if there is no data available for the
     *          cell.
     */
    @Override
    protected String getTextForColumn(final Object element, final int columnIndex) {
        // Get the objects represented by the specified row/column arguments.
        final Link link = (Link) element;
        final ColumnMetadata columnData = columnMetadatas[columnIndex];

        if (columnData.isWorkItemField()) {
            // Work item columns only show data for work item links.
            if (link instanceof RelatedLink) {
                // Get the target work item and referenced field name.
                final RelatedLink relatedLink = (RelatedLink) link;
                final WorkItem linkedWorkItem = relatedLink.getTargetWorkItem();
                final String wiFieldRefName = columnData.getReferenceName();

                // Nothing is returned we the linked work item has not been set.
                if (linkedWorkItem != null) {
                    // Retrieve the field referenced by this column. It's valid
                    // for
                    // the field not to exist for a particular work item type.
                    if (linkedWorkItem.getFields().contains(wiFieldRefName)) {
                        // Return the value of the field from the work item.
                        final Object fieldValue = linkedWorkItem.getFields().getField(wiFieldRefName).getValue();
                        if (fieldValue != null) {
                            return fieldValue.toString();
                        }
                    }
                }
            } else {
                // Show the description in the Title field for external links.
                if (columnData.getReferenceName().equalsIgnoreCase(CoreFieldReferenceNames.TITLE)) {
                    return link.getDescription();
                }
            }
        } else {
            // The column represents a property from the link itself.
            final String linkRefName = columnData.getReferenceName();
            if (linkRefName.equalsIgnoreCase(LINKATTRIBUTE_REFNAME_LINKTYPE)) {
                if (link instanceof RelatedLink) {
                    final RelatedLink relatedLink = (RelatedLink) link;
                    final WorkItemClient client = getWorkItem().getClient();

                    if (client.supportsWorkItemLinkTypes()) {
                        return client.getLinkTypes().getDisplayName(relatedLink.getWorkItemLinkTypeID());
                    } else {
                        return LinkUIRegistry.getDisplayName(link.getLinkType().getName());
                    }
                } else {
                    return LinkUIRegistry.getDisplayName(link.getLinkType().getName());
                }
            } else if (linkRefName.equalsIgnoreCase(LINKATTRIBUTE_REFNAME_DESCRIPTION)) {
                return link.getDescription();
            } else if (linkRefName.equalsIgnoreCase(LINKATTRIBUTE_REFNAME_COMMENT)) {
                return link.getComment();
            }
        }

        // There was no data available for this row/column.
        return null;
    }

    @Override
    protected void handleItemDoubleClick(final Object selectedItem) {
        if (isSelectedLinkOpenable()) {
            performOpen();
        }
    }

    /**
     * @return Returns true if the selection is a single item and the selected
     *         item is a related link.
     */
    private boolean selectionIsSingleRelatedLink() {
        final Object[] selectedItems = getSelectedItems();
        if (selectedItems != null && selectedItems.length == 1) {
            return selectedItems[0] instanceof RelatedLink;
        }

        return false;
    }

    private boolean isSelectedLinkOpenable() {
        final Link link = (Link) getSelectedItem();

        if (link instanceof Hyperlink || link instanceof RelatedLink) {
            return true;
        }

        if (link instanceof ExternalLink && (link.getLinkType().getName().equals(RegisteredLinkTypeNames.CHANGESET)
                || link.getLinkType().getName().equals(RegisteredLinkTypeNames.VERSIONED_ITEM)
                || link.getLinkType().getName().equals(RegisteredLinkTypeNames.STORYBOARD)
                || link.getLinkType().getName().equals(RegisteredLinkTypeNames.COMMIT))) {
            return true;
        }

        return false;
    }

    @Override
    protected void createButtons(final Composite parent) {
        newButton = createButton(parent, Messages.getString("WorkItemLinksControl.NewButtonText"), //$NON-NLS-1$
                new SelectionAdapter() {
                    @Override
                    public void widgetSelected(final SelectionEvent e) {
                        performNew();
                    }
                });
        newButton.setToolTipText(Messages.getString("WorkItemLinksControl.NewLinkedItemTooltipText")); //$NON-NLS-1$
        newButton.setEnabled(getWorkItem().getID() != 0);

        addButton = createButton(parent, Messages.getString("WorkItemLinksControl.AddLinkButtonText"), //$NON-NLS-1$
                new SelectionAdapter() {
                    @Override
                    public void widgetSelected(final SelectionEvent e) {
                        performAdd();
                    }
                });
        addButton.setToolTipText(Messages.getString("WorkItemLinksControl.AddLinkButtonTooltip")); //$NON-NLS-1$

        openButton = createButton(parent, Messages.getString("WorkItemLinksControl.OpenButtonText"), //$NON-NLS-1$
                new SelectionAdapter() {
                    @Override
                    public void widgetSelected(final SelectionEvent e) {
                        performOpen();
                    }
                });
        openButton.setToolTipText(Messages.getString("WorkItemLinksControl.OpenLinkedItemButtonTooltip")); //$NON-NLS-1$

        deleteButton = createButton(parent, Messages.getString("WorkItemLinksControl.DeleteButtonText"), //$NON-NLS-1$
                new SelectionAdapter() {
                    @Override
                    public void widgetSelected(final SelectionEvent e) {
                        performDelete();
                    }
                });
        deleteButton.setToolTipText(Messages.getString("WorkItemLinksControl.DeleteButtonTooltip")); //$NON-NLS-1$
    }

    @Override
    protected int getNumberOfButtons() {
        return 4;
    }

    private static String linkAttributeToDisplayName(final String linkAttribute) {
        if (linkAttribute.equalsIgnoreCase(LINKATTRIBUTE_REFNAME_LINKTYPE)) {
            return LINKATTRIBUTE_DISPNAME_LINKTYPE;
        } else if (linkAttribute.equalsIgnoreCase(LINKATTRIBUTE_REFNAME_DESCRIPTION)) {
            return LINKATTRIBUTE_DISPNAME_DESCRIPTION;
        } else if (linkAttribute.equalsIgnoreCase(LINKATTRIBUTE_REFNAME_COMMENT)) {
            return LINKATTRIBUTE_DISPNAME_COMMENT;
        } else {
            final String messageFormat = Messages.getString("WorkItemLinksControl.UnknownAttributeFormat"); //$NON-NLS-1$
            final String message = MessageFormat.format(messageFormat, linkAttribute);
            throw new IllegalArgumentException(message);
        }
    }

    /**
     * A class to compare a field from the target of two work item link. Numeric
     * comparisons are computed for INTEGER and DOUBLE field types. A date
     * comparison is compuated for a DATETIME field. All other field types are
     * compared as stinrgs.
     *
     * An instance of this class is associated with a table column in a
     * TableViewerSorter class.
     *
     *
     */
    private class RelatedLinkFieldComparator implements Comparator<Link> {
        private final String fieldReferenceName;

        public RelatedLinkFieldComparator(final String fieldReferenceName) {
            this.fieldReferenceName = fieldReferenceName;
        }

        @Override
        public int compare(final Link link0, final Link link1) {
            RelatedLink relatedLink0 = null;
            RelatedLink relatedLink1 = null;

            if (link0 instanceof RelatedLink) {
                relatedLink0 = (RelatedLink) link0;
            }

            if (link1 instanceof RelatedLink) {
                relatedLink1 = (RelatedLink) link1;
            }

            if (relatedLink0 == null || relatedLink1 == null) {
                return compareWithNull(relatedLink0, relatedLink1);
            }

            final WorkItem workItem0 = relatedLink0.getTargetWorkItem();
            final WorkItem workItem1 = relatedLink1.getTargetWorkItem();

            if (workItem0 == null || workItem1 == null) {
                return compareWithNull(workItem0, workItem1);
            }

            final Field field0 = workItem0.getFields().getField(fieldReferenceName);
            final Field field1 = workItem1.getFields().getField(fieldReferenceName);

            if (field0 == null || field1 == null) {
                return compareWithNull(field0, field1);
            }

            final Object value0 = field0.getValue();
            final Object value1 = field1.getValue();

            if (value0 == null || value1 == null) {
                return compareWithNull(value0, value1);
            }

            if (field0.getFieldDefinition().getFieldType() == FieldType.INTEGER) {
                return ((Integer) value0).compareTo(((Integer) value1));
            } else if (field0.getFieldDefinition().getFieldType() == FieldType.DOUBLE) {
                return ((Double) value0).compareTo(((Double) value1));
            } else if (field0.getFieldDefinition().getFieldType() == FieldType.DATETIME) {
                return ((Date) value0).compareTo(((Date) value1));
            } else {
                return ((String) value0).compareTo(((String) value1));
            }
        }

        private int compareWithNull(final Object obj0, final Object obj1) {
            Check.isTrue(obj0 == null || obj1 == null, "obj0 == null || obj1 == null"); //$NON-NLS-1$

            if (obj0 == null && obj1 == null) {
                return 0;
            } else if (obj0 == null) {
                return -1;
            } else {
                return 1;
            }
        }
    }

    private class ColumnMetadata {
        private final String referenceName;
        private final String displayName;
        private final boolean isLinkAttribute;
        private final Comparator<Link> comparator;

        public ColumnMetadata(final String referenceName, final String displayName, final boolean isLinkAttribute) {
            this(referenceName, displayName, isLinkAttribute, null);
        }

        public ColumnMetadata(final String referenceName, final String displayName, final boolean isLinkAttribute,
                final Comparator<Link> comparator) {
            this.referenceName = referenceName;
            this.displayName = displayName;
            this.isLinkAttribute = isLinkAttribute;
            this.comparator = comparator;
        }

        public String getReferenceName() {
            return referenceName;
        }

        public String getDisplayName() {
            return displayName;
        }

        public Comparator<Link> getComparator() {
            return comparator;
        }

        public boolean isWorkItemField() {
            return !isLinkAttribute;
        }
    }
}