/** * */ package org.nightlabs.jfire.issuetracking.ui.issue; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.jdo.FetchPlan; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.ListenerList; import org.eclipse.core.runtime.Status; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.nightlabs.base.ui.job.Job; import org.nightlabs.base.ui.layout.WeightedTableLayout; import org.nightlabs.base.ui.table.AbstractTableComposite; import org.nightlabs.base.ui.table.TableContentProvider; import org.nightlabs.base.ui.table.TableLabelProvider; import org.nightlabs.eclipse.extension.EPProcessorException; import org.nightlabs.jdo.NLJDOHelper; import org.nightlabs.jdo.ObjectID; import org.nightlabs.jdo.ObjectIDUtil; import org.nightlabs.jfire.base.jdo.JDOObjectID2PCClassMap; import org.nightlabs.jfire.issue.Issue; import org.nightlabs.jfire.issue.IssueLink; import org.nightlabs.jfire.issue.IssueLinkType; import org.nightlabs.jfire.issue.dao.IssueDAO; import org.nightlabs.jfire.issue.id.IssueID; import org.nightlabs.jfire.issuetracking.ui.issuelink.IssueLinkHandler; import org.nightlabs.jfire.issuetracking.ui.issuelink.IssueLinkHandlerFactory; import org.nightlabs.jfire.issuetracking.ui.issuelink.IssueLinkHandlerFactoryRegistry; import org.nightlabs.jfire.issuetracking.ui.issuelink.IssueLinkItemChangeEvent; import org.nightlabs.jfire.issuetracking.ui.resource.Messages; import org.nightlabs.progress.ProgressMonitor; import org.nightlabs.progress.SubProgressMonitor; /** * The composite that has a table for listing {@link IssueLink}. * @author Chairat Kongarayawetchakun - chairat[at]nightlabs[dot]de * */ public class IssueLinkTable extends AbstractTableComposite<IssueLinkTableItem> { private class LabelProvider extends TableLabelProvider { @Override public Image getColumnImage(Object element, int columnIndex) { if (columnIndex == 1) { if (element instanceof IssueLinkTableItem) { IssueLinkTableItem issueLinkTableItem = (IssueLinkTableItem) element; IssueLinkHandler<ObjectID, Object> handler = getIssueLinkHandler(issueLinkTableItem.getLinkedObjectID()); IssueLink issueLink = issueLinkTableItem.getIssueLink(); if (issueLink == null) return null; // TODO we should return an image symbolising that currently data is loaded. issueLinkTableItem.getIssueLink() is only null, if there is currently a Jab running in the background loading data for a newly created IssueLink. Object linkedObject = issueLink2LinkedObjectMap.get(issueLink); return handler.getLinkedObjectImage(issueLink, linkedObject); } } return null; } public String getColumnText(Object element, int columnIndex) { if (element instanceof IssueLinkTableItem) { IssueLinkTableItem issueLinkTableItem = (IssueLinkTableItem) element; IssueLinkHandler<ObjectID, Object> handler = getIssueLinkHandler(issueLinkTableItem.getLinkedObjectID()); IssueLink issueLink = issueLinkTableItem.getIssueLink(); Object linkedObject = issueLink2LinkedObjectMap.get(issueLink); if (columnIndex == 0) { if (issueLink == null) return Messages.getString("org.nightlabs.jfire.issuetracking.ui.issue.IssueLinkTable.table.loading.text"); //$NON-NLS-1$ return issueLink.getIssueLinkType().getName().getText(); // IssueLinkType issueLinkType = IssueLinkTypeDAO.sharedInstance().getIssueLinkTypesByLinkClass(Object.class, // new String[]{IssueLinkType.FETCH_GROUP_THIS_ISSUE_LINK_TYPE, FetchPlan.DEFAULT}, // NLJDOHelper.MAX_FETCH_DEPTH_NO_LIMIT, // new NullProgressMonitor()).get(0); // return issueLinkTableItem.getIssueLinkType() == null ? issueLinkType.getName().getText() : issueLinkTableItem.getIssueLinkType().getName().getText(); } if (issueLink == null) return ""; //$NON-NLS-1$ if (columnIndex == 1) { return handler.getLinkedObjectName(issueLink, linkedObject); } } return ""; //$NON-NLS-1$ } } public IssueLinkTable(Composite parent, int style) { super(parent, style); } /** * Set the issue to work with. This method is used by editor(page)s in order to have the data-loading-work * to be done by the editor's page-controller. If you don't have a page-controller, but are working in * another situation, you better use {@link #setIssueID(IssueID)}. * * @param issue An instance of <code>Issue</code> which must have at least those fields detached that are referenced by the following fetch-groups: * <ul> * <li>{@link FetchPlan#DEFAULT}</li> * <li>{@link Issue#FETCH_GROUP_ISSUE_LINKS}</li> * <li>{@link IssueLink#FETCH_GROUP_ISSUE_LINK_TYPE}</li> * <li>{@link IssueLinkType#FETCH_GROUP_NAME}</li> * <li>{@link IssueLink#FETCH_GROUP_LINKED_OBJECT_CLASS}</li> * </ul> */ public void setIssue(final Issue issue) { setIssue( IssueID.create(issue.getOrganisationID(), issue.getIssueID()), issue ); } /** * Set the issue to work with. This method should normally used. It causes this table to load all its * data itself, so you don't need to care about fetch-groups. The IssueID * * @param issueID - the issue's {@link IssueID} used in the table */ public void setIssueID(final IssueID issueID) { setIssue(issueID, null); } private void setIssue(final IssueID $issueID, final Issue $issue) { Job job = new Job(Messages.getString("org.nightlabs.jfire.issuetracking.ui.issue.IssueLinkTable.job.loadingIssueLinks.text")) { //$NON-NLS-1$ @Override protected IStatus run(ProgressMonitor monitor) throws Exception { Display.getDefault().asyncExec(new Runnable() { public void run() { issue = null; issueLink2LinkedObjectMap = null; setLoadingMessage(Messages.getString("org.nightlabs.jfire.issuetracking.ui.issue.IssueLinkTable.job.loadingIssueLinks.loadingMessage.text")); //$NON-NLS-1$ } }); int monitorTicksLeft = 100; monitor.beginTask(Messages.getString("org.nightlabs.jfire.issuetracking.ui.issue.IssueLinkTable.monitor.loadingIssueLinks.text"), monitorTicksLeft); //$NON-NLS-1$ // (1) load IssueLinks without linkedObjects but with linkedObjectClasses and linkedObjectIDs final Issue _issue; // When we use this table in an editor, the Issue instance is loaded by the page-controller // and we must not load it again. In this case, $issue will be initialised - otherwise, it's null. if ($issue != null) { _issue = $issue; monitor.worked(40); } else { _issue = IssueDAO.sharedInstance().getIssue( $issueID, new String[] { FetchPlan.DEFAULT, Issue.FETCH_GROUP_ISSUE_LINKS, IssueLinkType.FETCH_GROUP_NAME, IssueLink.FETCH_GROUP_LINKED_OBJECT_CLASS }, NLJDOHelper.MAX_FETCH_DEPTH_NO_LIMIT, new SubProgressMonitor(monitor, 40)); } monitorTicksLeft -= 40; // (2) resolve IssueLinkHandlers and group IssueLinks by IssueLinkHandler // => Map<IssueLinkHandler, Set<IssueLink>> Map<IssueLinkHandler<?, ?>, Set<IssueLink>> handler2issueLinks = new HashMap<IssueLinkHandler<?, ?>, Set<IssueLink>>(); for (IssueLink il : _issue.getIssueLinks()) { IssueLinkHandler<?, ?> handler = getIssueLinkHandler(il.getLinkedObjectClass()); Set<IssueLink> issueLinks = handler2issueLinks.get(handler); if (issueLinks == null) { issueLinks = new HashSet<IssueLink>(); handler2issueLinks.put(handler, issueLinks); } issueLinks.add(il); } // (3) obtain linked objects via their IssueLinkHandlers final Map<IssueLink, Object> _issueLink2LinkedObjectMap = new HashMap<IssueLink, Object>(); if (handler2issueLinks.isEmpty()) monitor.worked(monitorTicksLeft); else { int monitorTick = monitorTicksLeft / handler2issueLinks.keySet().size(); for (Map.Entry<IssueLinkHandler<?, ?>, Set<IssueLink>> me : handler2issueLinks.entrySet()) { IssueLinkHandler<?, ?> handler = me.getKey(); Set<IssueLink> issueLinks = me.getValue(); Map<IssueLink, ?> il2loMap = handler.getLinkedObjects(issueLinks, new SubProgressMonitor(monitor, monitorTick)); _issueLink2LinkedObjectMap.putAll(il2loMap); } } // display data Display.getDefault().asyncExec(new Runnable() { public void run() { if (isDisposed()) return; issue = _issue; issueLink2LinkedObjectMap = _issueLink2LinkedObjectMap; issueLinkTableItems.clear(); for (Map.Entry<IssueLink, Object> me : _issueLink2LinkedObjectMap.entrySet()) { IssueLink issueLink = me.getKey(); IssueLinkTableItem linkItem = new IssueLinkTableItem(issueLink.getLinkedObjectID(), issueLink.getIssueLinkType()); linkItem.initIssueLink(issueLink); issueLinkTableItems.add(linkItem); } setInput(issueLinkTableItems); if (issueLinkTableItems.size() > 0) { select(0); } } }); return Status.OK_STATUS; } }; job.schedule(); } // only modified on the SWT UI thread! private Issue issue; // only modified on the SWT UI thread! private Set<IssueLinkTableItem> issueLinkTableItems = new HashSet<IssueLinkTableItem>(); // only modified on the SWT UI thread! private Map<IssueLink, Object> issueLink2LinkedObjectMap; @Override protected void createTableColumns(TableViewer tableViewer, Table table) { TableColumn tableColumn; tableColumn = new TableColumn(table, SWT.NONE); tableColumn.setMoveable(true); tableColumn.setText(Messages.getString("org.nightlabs.jfire.issuetracking.ui.issue.IssueLinkTable.tableColumn.relation.text")); //$NON-NLS-1$ tableColumn = new TableColumn(table, SWT.NONE); tableColumn.setMoveable(true); tableColumn.setText(Messages.getString("org.nightlabs.jfire.issuetracking.ui.issue.IssueLinkTable.tableColumn.linkObject.text")); //$NON-NLS-1$ WeightedTableLayout layout = new WeightedTableLayout( new int[]{24, 76} ); table.setLayout(layout); } @Override protected void setTableProvider(TableViewer tableViewer) { tableViewer.setLabelProvider(new LabelProvider()); tableViewer.setContentProvider(new TableContentProvider()); } public IssueLinkHandler<ObjectID, Object> getIssueLinkHandler(String idStr) { return getIssueLinkHandler(ObjectIDUtil.createObjectID(idStr)); } public IssueLinkHandler<ObjectID, Object> getIssueLinkHandler(ObjectID objectID) { Class<?> pcClass = JDOObjectID2PCClassMap.sharedInstance().getPersistenceCapableClass(objectID); return getIssueLinkHandler(pcClass); } private Map<Class<?>, IssueLinkHandler<ObjectID, Object>> class2IssueLinkHandler = new HashMap<Class<?>, IssueLinkHandler<ObjectID, Object>>(); public synchronized IssueLinkHandler<ObjectID, Object> getIssueLinkHandler(Class<?> linkedObjectClass) { IssueLinkHandler<ObjectID, Object> handler = class2IssueLinkHandler.get(linkedObjectClass); if (handler == null) { IssueLinkHandlerFactory<ObjectID, Object> factory = null; try { factory = IssueLinkHandlerFactoryRegistry.sharedInstance().getIssueLinkHandlerFactory(linkedObjectClass); } catch (EPProcessorException e) { throw new RuntimeException(e); } handler = factory.createIssueLinkHandler(); class2IssueLinkHandler.put(linkedObjectClass, handler); } return handler; } private ListenerList tableItemChangeListeners = new ListenerList(); public void addIssueLinkTableItemChangeListener(IssueLinkTableItemChangeListener listener) { tableItemChangeListeners.add(listener); } public void removeIssueLinkTableItemChangeListener(IssueLinkTableItemChangeListener listener) { tableItemChangeListeners.remove(listener); } protected void notifyIssueLinkTableItemChangeListeners(IssueLinkItemChangeEvent.ChangeType changeType, Collection<IssueLinkTableItem> items) { Object[] listeners = tableItemChangeListeners.getListeners(); IssueLinkItemChangeEvent evt = new IssueLinkItemChangeEvent(this, changeType, items); for (Object l : listeners) { if (l instanceof IssueLinkTableItemChangeListener) { ((IssueLinkTableItemChangeListener) l) .issueLinkItemChanged(evt); } } } public void addIssueLinkTableItem(Issue issue, IssueLinkTableItem item) { this.issue = issue; Set<IssueLinkTableItem> items = new HashSet<IssueLinkTableItem>(); items.add(item); addIssueLinkTableItems(items); } /** * Add new instances of {@link IssueLinkTableItem} representing new {@link IssueLink}s that must be created. * Therefore, the {@link IssueLinkTableItem#getIssueLink()} property is not yet assigned (i.e. still returning <code>null</code>). * * @param items the new <code>IssueLinkTableItem</code> */ public void addIssueLinkTableItems(final Collection<IssueLinkTableItem> items) { if (Display.getCurrent() == null) throw new IllegalStateException("This method must be called on the SWT UI thread!"); //$NON-NLS-1$ if (issue == null) throw new IllegalStateException("Not yet initialised! Cannot add issue links before this table has loaded its data!"); //$NON-NLS-1$ if (items.isEmpty()) return; // check whether the new items are really new and have no IssueLink assigned yet for (IssueLinkTableItem issueLinkTableItem : items) { if (issueLinkTableItem.getIssueLink() != null) throw new IllegalArgumentException("issueLinkTableItem.getIssueLink() != null !!! The IssueLink instance must not yet exist!"); //$NON-NLS-1$ } // add them to the table (the LabelProvider will show "Loading data..." as long as there is no IssueLink assigned) issueLinkTableItems.addAll(items); this.refresh(); // create IssueLink instances - since the Issue is not thread-safe, we call this method on the UI thread before spawning a job. final Map<IssueLinkTableItem, IssueLink> issueLinkTableItem2issueLinkMap = new HashMap<IssueLinkTableItem, IssueLink>(); for (IssueLinkTableItem issueLinkTableItem : items) { IssueLink issueLink = issue.createIssueLink(issueLinkTableItem.getIssueLinkType(), issueLinkTableItem.getLinkedObjectID(), JDOObjectID2PCClassMap.sharedInstance().getPersistenceCapableClass(issueLinkTableItem.getLinkedObjectID())); issueLinkTableItem2issueLinkMap.put(issueLinkTableItem, issueLink); } Job job = new Job(Messages.getString("org.nightlabs.jfire.issuetracking.ui.issue.IssueLinkTable.job.loadingLinkedObject.text")) { //$NON-NLS-1$ @Override protected IStatus run(ProgressMonitor monitor) throws Exception { int monitorTicksLeft = 100; monitor.beginTask(Messages.getString("org.nightlabs.jfire.issuetracking.ui.issue.IssueLinkTable.monitor.loadingLinkedObjects.text"), monitorTicksLeft); //$NON-NLS-1$ // resolve IssueLinkHandlers and group IssueLinks by IssueLinkHandler // => Map<IssueLinkHandler, Set<IssueLink>> Map<IssueLinkHandler<?, ?>, Set<IssueLink>> handler2issueLinks = new HashMap<IssueLinkHandler<?, ?>, Set<IssueLink>>(); for (Map.Entry<IssueLinkTableItem, IssueLink> me : issueLinkTableItem2issueLinkMap.entrySet()) { IssueLinkHandler<?, ?> handler = getIssueLinkHandler(me.getKey().getLinkedObjectID()); Set<IssueLink> issueLinks = handler2issueLinks.get(handler); if (issueLinks == null) { issueLinks = new HashSet<IssueLink>(); handler2issueLinks.put(handler, issueLinks); } issueLinks.add(me.getValue()); } monitor.worked(5); monitorTicksLeft -= 5; // obtain linked objects via their IssueLinkHandlers final Map<IssueLink, Object> _issueLink2LinkedObjectMap = new HashMap<IssueLink, Object>(); int monitorTick = monitorTicksLeft / handler2issueLinks.keySet().size(); for (Map.Entry<IssueLinkHandler<?, ?>, Set<IssueLink>> me : handler2issueLinks.entrySet()) { IssueLinkHandler<?, ?> handler = me.getKey(); Set<IssueLink> issueLinks = me.getValue(); Map<IssueLink, ?> il2loMap = handler.getLinkedObjects(issueLinks, new SubProgressMonitor(monitor, monitorTick)); _issueLink2LinkedObjectMap.putAll(il2loMap); } Display.getDefault().asyncExec(new Runnable() { public void run() { if (issue == null) return; // silently return, if setIssueID(...) has been called in the meantime issueLink2LinkedObjectMap.putAll(_issueLink2LinkedObjectMap); for (Map.Entry<IssueLinkTableItem, IssueLink> me : issueLinkTableItem2issueLinkMap.entrySet()) { me.getKey().initIssueLink(me.getValue()); } refresh(); notifyIssueLinkTableItemChangeListeners(IssueLinkItemChangeEvent.ChangeType.add, items); } }); return Status.OK_STATUS; } }; job.setUser(true); job.schedule(); } public void removeIssueLinkTableItems(Collection<IssueLinkTableItem> items) { if (Display.getCurrent() == null) throw new IllegalStateException("This method must be called on the SWT UI thread!"); //$NON-NLS-1$ if (issue == null) throw new IllegalStateException("Not yet initialised! Cannot add issue links before this table has loaded its data!"); //$NON-NLS-1$ Collection<IssueLinkTableItem> removedItems = new HashSet<IssueLinkTableItem>(items.size()); for (IssueLinkTableItem issueLinkTableItem : items) { IssueLink issueLink = issueLinkTableItem.getIssueLink(); if (issueLink != null) { if (issueLinkTableItems.remove(issueLinkTableItem)) { issue.removeIssueLink(issueLink); issueLink2LinkedObjectMap.remove(issueLink); removedItems.add(issueLinkTableItem); } } } refresh(); notifyIssueLinkTableItemChangeListeners(IssueLinkItemChangeEvent.ChangeType.remove, items); } public Set<IssueLinkTableItem> getIssueLinkTableItems() { return issueLinkTableItems; } public Issue getIssue() { return issue; } }