/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.gui.workflow.view.list;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
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 org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.ui.part.ViewPart;
import de.rcenvironment.core.communication.common.InstanceNodeSessionId;
import de.rcenvironment.core.communication.management.WorkflowHostService;
import de.rcenvironment.core.communication.management.WorkflowHostSetListener;
import de.rcenvironment.core.component.execution.api.ExecutionControllerException;
import de.rcenvironment.core.component.workflow.api.WorkflowConstants;
import de.rcenvironment.core.component.workflow.execution.api.WorkflowExecutionInformation;
import de.rcenvironment.core.component.workflow.execution.api.WorkflowExecutionService;
import de.rcenvironment.core.component.workflow.execution.api.WorkflowState;
import de.rcenvironment.core.component.workflow.execution.api.WorkflowStateNotificationSubscriber;
import de.rcenvironment.core.component.workflow.execution.spi.MultipleWorkflowsStateChangeListener;
import de.rcenvironment.core.gui.resources.api.ImageManager;
import de.rcenvironment.core.gui.resources.api.StandardImages;
import de.rcenvironment.core.gui.workflow.Activator;
import de.rcenvironment.core.gui.workflow.view.WorkflowRunEditorAction;
import de.rcenvironment.core.notification.SimpleNotificationService;
import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils;
import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.core.utils.common.rpc.RemoteOperationException;
import de.rcenvironment.core.utils.incubator.ServiceRegistry;
import de.rcenvironment.core.utils.incubator.ServiceRegistryAccess;
import de.rcenvironment.core.utils.incubator.ServiceRegistryPublisherAccess;
import de.rcenvironment.toolkit.modules.concurrency.api.AsyncExceptionListener;
import de.rcenvironment.toolkit.modules.concurrency.api.BatchAggregator;
import de.rcenvironment.toolkit.modules.concurrency.api.BatchProcessor;
import de.rcenvironment.toolkit.modules.concurrency.api.CallablesGroup;
import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription;
/**
* This view shows all running workflows.
*
* @author Heinrich Wendel
* @author Robert Mischke
* @author Doreen Seider
*/
public class WorkflowListView extends ViewPart implements MultipleWorkflowsStateChangeListener {
// the maximum number of ConsoleRows to aggregate to a single batch
// NOTE: arbitrary value; adjust when useful/necessary
private static final int MAX_BATCH_SIZE = 500;
// the maximum time a ConsoleRow may be delayed by batch aggregation
// NOTE: arbitrary value; adjust when useful/necessary
private static final long MAX_BATCH_LATENCY_MSEC = 500;
// guarded by synchronization on itself
private final List<String> idsOfRemoteWorkflowsSubscribedFor = new ArrayList<String>();
// guarded by synchronization on itself
private final Set<InstanceNodeSessionId> nodesSubscribedForNewWorkflows = new HashSet<InstanceNodeSessionId>();
private final WorkflowStateNotificationSubscriber workflowStateChangeListener =
new WorkflowStateNotificationSubscriber(this);
private final SimpleNotificationService sns = new SimpleNotificationService();
private TableViewer viewer;
private Table table;
private WorkflowInformationColumnSorter columnSorter;
private Action pauseAction;
private Action resumeAction;
private Action cancelAction;
private Action disposeAction;
private Display display;
private ServiceRegistryPublisherAccess serviceRegistryPublisherAccess;
private WorkflowExecutionService workflowExecutionService;
private Object syncUpdateLock = new Object();
private final BatchAggregator<Set<WorkflowExecutionInformation>> batchAggregator;
public WorkflowListView() {
ServiceRegistryAccess serviceRegistryAccess = ServiceRegistry.createAccessFor(this);
workflowExecutionService = serviceRegistryAccess.getService(WorkflowExecutionService.class);
serviceRegistryPublisherAccess = ServiceRegistry.createPublisherAccessFor(this);
BatchProcessor<Set<WorkflowExecutionInformation>> batchProcessor = new BatchProcessor<Set<WorkflowExecutionInformation>>() {
@Override
public void processBatch(final List<Set<WorkflowExecutionInformation>> batch) {
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
refresh(batch.get(batch.size() - 1));
}
});
}
};
batchAggregator = ConcurrencyUtils.getFactory().createBatchAggregator(MAX_BATCH_SIZE, MAX_BATCH_LATENCY_MSEC, batchProcessor);
}
/**
* Registers an event listener for network changes as an OSGi service (whiteboard pattern).
*
* @param display
*/
private void registerWorkflowHostSetListener() {
serviceRegistryPublisherAccess.registerService(WorkflowHostSetListener.class, new WorkflowHostSetListener() {
@Override
public void onReachableWorkflowHostsChanged(Set<InstanceNodeSessionId> reachableWfHosts,
Set<InstanceNodeSessionId> addedWfHosts, Set<InstanceNodeSessionId> removedWfHosts) {
updateSubscriptionsForNewlyCreatedWorkflows();
synchronized (syncUpdateLock) {
final Set<WorkflowExecutionInformation> wis = updateWorkflowInformations();
if (!table.isDisposed()) {
batchAggregator.enqueue(wis);
}
}
}
});
}
@Override
public void createPartControl(Composite parent) {
display = parent.getShell().getDisplay();
viewer = new TableViewer(parent, SWT.MULTI | SWT.FULL_SELECTION);
viewer.getControl().addKeyListener(new KeyListener() {
@Override
public void keyReleased(KeyEvent event) {}
@Override
public void keyPressed(KeyEvent event) {
if (event.stateMask == SWT.CTRL && event.keyCode == 'a') {
viewer.getTable().selectAll();
getSite().getSelectionProvider().setSelection(viewer.getSelection());
updateSelectedWorkflowState();
}
if (event.keyCode == SWT.DEL) {
if (disposeAction.isEnabled()) {
disposeAction.run();
}
}
}
});
table = viewer.getTable();
table.setLinesVisible(true);
table.setHeaderVisible(true);
columnSorter = new WorkflowInformationColumnSorter();
viewer.setSorter(columnSorter);
String[] titles = {
Messages.name, Messages.status, "Controller", "Start", "Started From", "Comment" };
final int width = 150;
for (int i = 0; i < titles.length; i++) {
final int index = i;
final TableViewerColumn viewerColumn = new TableViewerColumn(viewer, SWT.NONE);
final TableColumn column = viewerColumn.getColumn();
column.setText(titles[i]);
column.setWidth(width);
column.setResizable(true);
column.setMoveable(true);
column.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
columnSorter.setColumn(index);
int direction = viewer.getTable().getSortDirection();
if (viewer.getTable().getSortColumn() == column) {
if (direction == SWT.UP) {
direction = SWT.DOWN;
} else {
direction = SWT.UP;
}
} else {
direction = SWT.UP;
}
viewer.getTable().setSortDirection(direction);
viewer.getTable().setSortColumn(column);
viewer.refresh();
}
});
}
// add toolbar actions (right top of view)
for (Action action : createToolbarActions()) {
action.setEnabled(false);
getViewSite().getActionBars().getToolBarManager().add(action);
}
table.addMouseListener(new MouseAdapter() {
@Override
public void mouseDoubleClick(MouseEvent e) {
WorkflowExecutionInformation wi =
(WorkflowExecutionInformation) ((IStructuredSelection) viewer.getSelection()).getFirstElement();
if (wi == null) {
return;
}
new WorkflowRunEditorAction(wi).run();
}
});
table.addSelectionListener(new SelectionListener() {
@Override
public void widgetSelected(SelectionEvent eve) {
widgetDefaultSelected(eve);
}
@Override
public void widgetDefaultSelected(SelectionEvent eve) {
updateSelectedWorkflowState();
}
});
Job job = new Job(Messages.workflows) {
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
monitor.beginTask(Messages.fetchingWorkflows, 7);
// subscribe to all state notifications of the local node
try {
sns.subscribe(WorkflowConstants.STATE_NOTIFICATION_ID + ".*", workflowStateChangeListener, null);
} catch (RemoteOperationException e) {
LogFactory.getLog(getClass()).error(
"Failed to set up remote subscriptions; the workflow list will not update properly: " + e.getMessage());
// TODO review: anything else that can be done except continuing with broken updates? - misc_ro, April 2015
}
monitor.worked(2);
// subscribe to get informed about new workflows created to refresh and fetch them
updateSubscriptionsForNewlyCreatedWorkflows();
monitor.worked(3);
monitor.worked(2);
final Set<WorkflowExecutionInformation> wis = updateWorkflowInformations();
display.asyncExec(new Runnable() {
@Override
public void run() {
refresh(wis);
}
});
return Status.OK_STATUS;
} finally {
monitor.done();
}
};
};
job.setUser(true);
job.schedule();
hookContextMenu();
registerWorkflowHostSetListener();
}
private void hookContextMenu() {
MenuManager menuManager = new MenuManager();
menuManager.add(pauseAction);
menuManager.add(resumeAction);
menuManager.add(cancelAction);
menuManager.add(disposeAction);
Menu menu = menuManager.createContextMenu(viewer.getTable());
viewer.getTable().setMenu(menu);
getSite().registerContextMenu(menuManager, viewer);
getSite().setSelectionProvider(viewer);
}
/**
* Refresh the contents of the table viewer.
*
* @param wis workflow informations to consider
*/
public void refresh(Set<WorkflowExecutionInformation> wis) {
// ignore refresh request in case the table widget is already disposed
if (table.isDisposed()) {
return;
}
viewer.setContentProvider(new WorkflowInformationContentProvider());
viewer.setLabelProvider(new WorkflowInformationLabelProvider());
TableItem[] selectedItems = viewer.getTable().getSelection();
String selWiId = null;
if (selectedItems.length == 1) {
selWiId = ((WorkflowExecutionInformation) selectedItems[0].getData()).getExecutionIdentifier();
}
viewer.setInput(wis);
if (selWiId != null) {
for (TableItem i : viewer.getTable().getItems()) {
WorkflowExecutionInformation wei = (WorkflowExecutionInformation) i.getData();
if (selWiId.equals(wei.getExecutionIdentifier())) {
viewer.getTable().setSelection(i);
break;
}
}
}
updateSelectedWorkflowState();
}
/**
* @return workflow informations viewer must consider
*/
public Set<WorkflowExecutionInformation> updateWorkflowInformations() {
// synchronize to avoid needless/duplicate subscriptions
synchronized (idsOfRemoteWorkflowsSubscribedFor) {
Set<WorkflowExecutionInformation> wis = workflowExecutionService.getWorkflowExecutionInformations(true);
// subscribe to all new remote ones in parallel and fetch their current states
CallablesGroup<Void> callablesGroup = ConcurrencyUtils.getFactory().createCallablesGroup(Void.class);
List<String> alreadySubscribedWiIds = new ArrayList<String>(idsOfRemoteWorkflowsSubscribedFor);
idsOfRemoteWorkflowsSubscribedFor.clear();
for (final WorkflowExecutionInformation wi : wis) {
final String executionId = wi.getExecutionIdentifier();
idsOfRemoteWorkflowsSubscribedFor.add(executionId);
if (!alreadySubscribedWiIds.contains(executionId)) {
callablesGroup.add(new Callable<Void>() {
@Override
@TaskDescription("Subscribe to new workflow")
public Void call() throws Exception {
sns.subscribe(WorkflowConstants.STATE_NOTIFICATION_ID + executionId,
workflowStateChangeListener, wi.getNodeId());
WorkflowStateModel.getInstance().setState(wi.getExecutionIdentifier(), wi.getWorkflowState());
return null;
}
});
}
}
callablesGroup.executeParallel(new AsyncExceptionListener() {
@Override
public void onAsyncException(Exception e) {
LogFactory.getLog(getClass()).warn("Asynchronous exception while subscribing to a new workflow");
}
});
return wis;
}
}
@Override
public void setFocus() {
table.setFocus();
updateSelectedWorkflowState();
}
private Action[] createToolbarActions() {
pauseAction = new WorflowLifeCycleAction(Messages.pause, Activator.getInstance().getImageRegistry()
.getDescriptor(WorkflowState.PAUSED.name())) {
@Override
protected void performAction(WorkflowExecutionInformation wfExeInfo) throws ExecutionControllerException,
RemoteOperationException {
workflowExecutionService.pause(wfExeInfo.getExecutionIdentifier(), wfExeInfo.getNodeId());
}
@Override
protected String getActionAsString() {
return "Pausing";
}
};
resumeAction = new WorflowLifeCycleAction(Messages.resume, Activator.getInstance().getImageRegistry()
.getDescriptor(WorkflowState.RESUMING.name())) {
@Override
protected void performAction(WorkflowExecutionInformation wfExeInfo) throws ExecutionControllerException,
RemoteOperationException {
workflowExecutionService.resume(wfExeInfo.getExecutionIdentifier(), wfExeInfo.getNodeId());
}
@Override
protected String getActionAsString() {
return "Resuming";
}
};
cancelAction = new WorflowLifeCycleAction(Messages.cancel, Activator.getInstance().getImageRegistry()
.getDescriptor(WorkflowState.CANCELLED.name())) {
@Override
protected void performAction(WorkflowExecutionInformation wfExeInfo) throws ExecutionControllerException,
RemoteOperationException {
workflowExecutionService.cancel(wfExeInfo.getExecutionIdentifier(), wfExeInfo.getNodeId());
}
@Override
protected String getActionAsString() {
return "Cancelling";
}
};
disposeAction = new WorflowLifeCycleAction(Messages.dispose, ImageDescriptor.createFromImage(
ImageManager.getInstance().getSharedImage(StandardImages.REMOVE_16))) {
@Override
protected void performAction(WorkflowExecutionInformation wfExeInfo) throws ExecutionControllerException,
RemoteOperationException {
workflowExecutionService.dispose(wfExeInfo.getExecutionIdentifier(), wfExeInfo.getNodeId());
}
@Override
protected String getActionAsString() {
return "Disposing";
}
};
return new Action[] { pauseAction, resumeAction, cancelAction, disposeAction };
}
/**
* Abstract class for lifecylce actions.
*
* @author Doreen Seider
*/
private abstract class WorflowLifeCycleAction extends Action {
protected WorflowLifeCycleAction(String text, ImageDescriptor image) {
super(text, image);
}
@Override
public void run() {
@SuppressWarnings("unchecked") final List<Object> selection = ((StructuredSelection) viewer.getSelection()).toList();
Job job = new Job(getActionAsString() + " workflow(s)") {
@Override
protected IStatus run(final IProgressMonitor monitor) {
CallablesGroup<Void> callablesGroup = ConcurrencyUtils.getFactory().createCallablesGroup(Void.class);
for (Object o : selection) {
WorkflowExecutionInformation wfExeInfo = (WorkflowExecutionInformation) o;
try {
performAction(wfExeInfo);
} catch (ExecutionControllerException | RemoteOperationException e) {
LogFactory.getLog(WorkflowListView.class).error(
StringUtils.format("%s workflow failed", getActionAsString()), e);
}
}
callablesGroup.executeParallel(new AsyncExceptionListener() {
@Override
public void onAsyncException(Exception e) {
LogFactory.getLog(WorkflowListView.class).error(
StringUtils.format("%s workflow failed", getActionAsString()), e);
}
});
final Set<WorkflowExecutionInformation> wfExeInfos = updateWorkflowInformations();
try {
if (!table.isDisposed()) {
table.getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
refresh(wfExeInfos);
}
});
}
} finally {
monitor.done();
}
return Status.OK_STATUS;
};
};
job.setUser(false);
job.schedule();
}
protected abstract void performAction(WorkflowExecutionInformation wfExeInfo) throws ExecutionControllerException,
RemoteOperationException;
protected abstract String getActionAsString();
}
private void updateSubscriptionsForNewlyCreatedWorkflows() {
synchronized (nodesSubscribedForNewWorkflows) {
CallablesGroup<InstanceNodeSessionId> callablesGroup =
ConcurrencyUtils.getFactory().createCallablesGroup(InstanceNodeSessionId.class);
ServiceRegistryAccess registryAccess = ServiceRegistry.createAccessFor(this);
Set<InstanceNodeSessionId> nodes = registryAccess.getService(WorkflowHostService.class).getWorkflowHostNodesAndSelf();
for (final InstanceNodeSessionId node : nodes) {
if (!nodesSubscribedForNewWorkflows.contains(node)) {
nodesSubscribedForNewWorkflows.add(node);
callablesGroup.add(new Callable<InstanceNodeSessionId>() {
@Override
@TaskDescription("Distributed subscriptions for newly created workflow notifications")
public InstanceNodeSessionId call() throws Exception {
sns.subscribe(WorkflowConstants.NEW_WORKFLOW_NOTIFICATION_ID, workflowStateChangeListener, node);
return node;
}
});
}
}
List<InstanceNodeSessionId> nodesAdded = callablesGroup.executeParallel(new AsyncExceptionListener() {
@Override
public void onAsyncException(Exception e) {
final Log log = LogFactory.getLog(getClass());
if (e.getCause() == null) {
// log a compressed message; this includes RemoteOperationExceptions, which (by design) never have a "cause"
log.warn("Asynchronous exception during parallel subscriptions for newly created workflow notifications: "
+ e.toString());
} else {
// on unexpected errors, log the full stacktrace
log.warn("Asynchronous exception during parallel subscriptions for newly created workflow notifications", e);
}
}
});
nodesSubscribedForNewWorkflows.retainAll(nodesAdded);
}
}
private void updateSelectedWorkflowState() {
setAllIconsEnabled(true);
if (viewer.getSelection() != null) {
if (((IStructuredSelection) viewer.getSelection()).size() > 0) {
for (Object o : ((IStructuredSelection) viewer.getSelection()).toList()) {
WorkflowExecutionInformation wi = (WorkflowExecutionInformation) o;
final WorkflowState workflowState = WorkflowStateModel.getInstance().getState(wi.getExecutionIdentifier());
if (workflowState == WorkflowState.RUNNING || workflowState == WorkflowState.PREPARING) {
resumeAction.setEnabled(false);
disposeAction.setEnabled(false);
} else if (workflowState == WorkflowState.PAUSED) {
pauseAction.setEnabled(false);
disposeAction.setEnabled(false);
} else if (WorkflowConstants.FINAL_WORKFLOW_STATES.contains(workflowState)) {
pauseAction.setEnabled(false);
resumeAction.setEnabled(false);
cancelAction.setEnabled(false);
} else {
setAllIconsEnabled(false);
}
}
} else {
setAllIconsEnabled(false);
}
}
}
private void setAllIconsEnabled(boolean enabled) {
pauseAction.setEnabled(enabled);
resumeAction.setEnabled(enabled);
cancelAction.setEnabled(enabled);
disposeAction.setEnabled(enabled);
}
@Override
public void onWorkflowStateChanged(String wfExecutionId, WorkflowState newState) {
if (newState != null) {
WorkflowStateModel.getInstance().setState(wfExecutionId, newState);
}
synchronized (syncUpdateLock) {
final Set<WorkflowExecutionInformation> wis = updateWorkflowInformations();
batchAggregator.enqueue(wis);
}
}
}