JAL-3878 Connect alignment services with gui
authorMateusz Warowny <mmzwarowny@dundee.ac.uk>
Tue, 15 Mar 2022 14:48:30 +0000 (15:48 +0100)
committerMateusz Warowny <mmzwarowny@dundee.ac.uk>
Tue, 15 Mar 2022 14:48:30 +0000 (15:48 +0100)
src/jalview/ws2/gui/AlignmentServiceGuiHandler.java [new file with mode: 0644]
src/jalview/ws2/helpers/WSClientTaskWrapper.java [new file with mode: 0644]

diff --git a/src/jalview/ws2/gui/AlignmentServiceGuiHandler.java b/src/jalview/ws2/gui/AlignmentServiceGuiHandler.java
new file mode 100644 (file)
index 0000000..2540dd1
--- /dev/null
@@ -0,0 +1,298 @@
+package jalview.ws2.gui;
+
+import static java.util.Objects.requireNonNullElse;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.swing.JInternalFrame;
+
+import jalview.bin.Cache;
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.AlignmentOrder;
+import jalview.datamodel.HiddenColumns;
+import jalview.gui.AlignFrame;
+import jalview.gui.Desktop;
+import jalview.gui.JvOptionPane;
+import jalview.gui.SplitFrame;
+import jalview.gui.WebserviceInfo;
+import jalview.util.ArrayUtils;
+import jalview.util.MessageManager;
+import jalview.util.Pair;
+import jalview.ws2.actions.alignment.AlignmentAction;
+import jalview.ws2.actions.alignment.AlignmentResult;
+import jalview.ws2.actions.api.JobI;
+import jalview.ws2.actions.api.TaskEventListener;
+import jalview.ws2.actions.api.TaskI;
+import jalview.ws2.api.JobStatus;
+import jalview.ws2.api.WebService;
+import jalview.ws2.helpers.WSClientTaskWrapper;
+
+class AlignmentServiceGuiHandler
+    implements TaskEventListener<AlignmentResult>
+{
+  private final WebService<?> service;
+
+  private final AlignFrame frame;
+
+  private WebserviceInfo infoPanel;
+
+  private String alnTitle; // title of the alignment used in new window
+
+  private JobI[] jobs = new JobI[0];
+
+  private int[] tabs = new int[0];
+
+  private int[] logOffset = new int[0];
+
+  private int[] errLogOffset = new int[0];
+
+  public AlignmentServiceGuiHandler(AlignmentAction action, AlignFrame frame)
+  {
+    this.service = action.getWebService();
+    this.frame = frame;
+    String panelInfo = String.format("%s using service hosted at %s%n%s",
+        service.getName(), service.getUrl(), service.getDescription());
+    infoPanel = new WebserviceInfo(service.getName(), panelInfo, false);
+    String actionName = requireNonNullElse(action.getName(), "Alignment");
+    alnTitle = String.format("%s %s of %s", service.getName(), actionName,
+        frame.getTitle());
+  }
+
+  @Override
+  public void taskStatusChanged(TaskI<AlignmentResult> source, JobStatus status)
+  {
+    switch (status)
+    {
+    case INVALID:
+      infoPanel.setVisible(false);
+      JvOptionPane.showMessageDialog(frame,
+          MessageManager.getString("info.invalid_msa_input_mininfo"),
+          MessageManager.getString("info.invalid_msa_notenough"),
+          JvOptionPane.INFORMATION_MESSAGE);
+      break;
+    case READY:
+      infoPanel.setthisService(new WSClientTaskWrapper(source));
+      infoPanel.setVisible(true);
+      // intentional no break
+    case SUBMITTED:
+    case QUEUED:
+      infoPanel.setStatus(WebserviceInfo.STATE_QUEUING);
+      break;
+    case RUNNING:
+    case UNKNOWN: // unsure what to do with unknown
+      infoPanel.setStatus(WebserviceInfo.STATE_RUNNING);
+      break;
+    case COMPLETED:
+      infoPanel.setProgressBar(
+          MessageManager.getString("status.collecting_job_results"),
+          jobs[0].getInternalId());
+      infoPanel.setStatus(WebserviceInfo.STATE_STOPPED_OK);
+      break;
+    case FAILED:
+      infoPanel.removeProgressBar(jobs[0].getInternalId());
+      infoPanel.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
+      break;
+    case CANCELLED:
+      infoPanel.setStatus(WebserviceInfo.STATE_CANCELLED_OK);
+      break;
+    case SERVER_ERROR:
+      infoPanel.removeProgressBar(jobs[0].getInternalId());
+      infoPanel.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR);
+      break;
+    }
+  }
+
+  @Override
+  public void taskStarted(TaskI<AlignmentResult> source, List<? extends JobI> subJobs)
+  {
+    jobs = subJobs.toArray(new JobI[0]);
+    tabs = new int[subJobs.size()];
+    logOffset = new int[subJobs.size()];
+    errLogOffset = new int[subJobs.size()];
+    for (int i = 0; i < subJobs.size(); i++)
+    {
+      JobI job = jobs[i];
+      int tabIndex = infoPanel.addJobPane();
+      tabs[i] = tabIndex;
+      infoPanel.setProgressName(String.format("region %d", i), tabIndex);
+      infoPanel.setProgressText(tabIndex, alnTitle + "\nJob details\n");
+      // jobs should not have states other than invalid or ready at this point
+      if (job.getStatus() == JobStatus.INVALID)
+        infoPanel.setStatus(tabIndex, WebserviceInfo.STATE_STOPPED_OK);
+      else if (job.getStatus() == JobStatus.READY)
+        infoPanel.setStatus(tabIndex, WebserviceInfo.STATE_QUEUING);
+    }
+  }
+
+  @Override
+  public void taskCompleted(TaskI<AlignmentResult> source, AlignmentResult result)
+  {
+    if (result == null)
+    {
+      infoPanel.setFinishedNoResults();
+      return;
+    }
+    infoPanel.showResultsNewFrame.addActionListener(evt -> {
+      var aln = result.getAlignment();
+      // copy alignment for each frame to have its own isntance
+      var alnCpy = new Alignment(aln);
+      alnCpy.setGapCharacter(aln.getGapCharacter());
+      alnCpy.setDataset(aln.getDataset());
+      displayResultsNewFrame(alnCpy, result.getAlignmentOrders(),
+          result.getHiddenColumns());
+    });
+    infoPanel.setResultsReady();
+  }
+
+  private void displayResultsNewFrame(Alignment aln,
+      List<AlignmentOrder> alorders, HiddenColumns hidden)
+  {
+    AlignFrame newFrame = new AlignFrame(aln, hidden, AlignFrame.DEFAULT_WIDTH,
+        AlignFrame.DEFAULT_HEIGHT);
+    newFrame.getFeatureRenderer().transferSettings(
+        frame.getFeatureRenderer().getSettings());
+    if (alorders.size() > 0)
+    {
+      addSortByMenuItems(newFrame, alorders);
+    }
+
+    var requestingFrame = frame;
+    var splitContainer = requestingFrame.getSplitViewContainer();
+    if (splitContainer != null && splitContainer.getComplement(requestingFrame) != null)
+    {
+      AlignmentI complement = splitContainer.getComplement(requestingFrame);
+      String complementTitle = splitContainer.getComplementTitle(requestingFrame);
+      Alignment copyComplement = new Alignment(complement);
+      copyComplement.setGapCharacter(complement.getGapCharacter());
+      copyComplement.setDataset(complement.getDataset());
+      copyComplement.alignAs(aln);
+      if (copyComplement.getHeight() > 0)
+      {
+        newFrame.setTitle(alnTitle);
+        AlignFrame newFrame2 = new AlignFrame(copyComplement,
+            AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
+        newFrame2.setTitle(complementTitle);
+        String linkedTitle = MessageManager.getString("label.linked_view_title");
+        JInternalFrame splitFrame = new SplitFrame(
+            aln.isNucleotide() ? newFrame : newFrame2,
+            aln.isNucleotide() ? newFrame2 : newFrame);
+        Desktop.addInternalFrame(splitFrame, linkedTitle, -1, -1);
+        return;
+      }
+    }
+    // no split frame or failed to create complementary alignment
+    Desktop.addInternalFrame(frame, alnTitle, AlignFrame.DEFAULT_WIDTH,
+        AlignFrame.DEFAULT_HEIGHT);
+  }
+  
+  private void addSortByMenuItems(AlignFrame frame, List<AlignmentOrder> alorders)
+  {
+    if (alorders.size() == 1)
+    {
+      frame.addSortByOrderMenuItem(service.getName() + " Ordering",
+          alorders.get(0));
+      return;
+    }
+    BitSet collected = new BitSet(alorders.size());
+    for (int i = 0, N = alorders.size(); i < N; i++)
+    {
+      if (collected.get(i))
+        continue;
+      var regions = new ArrayList<String>();
+      var order = alorders.get(i);
+      for (int j = i; j < N; j++)
+      {
+        if (!collected.get(j) && alorders.get(j).equals(order))
+        {
+          regions.add(Integer.toString(j + 1));
+          collected.set(j);
+        }
+      }
+      var orderName = String.format("%s Region %s Ordering",
+          service.getName(), String.join(",", regions));
+      frame.addSortByOrderMenuItem(orderName, order);
+    }
+  }
+
+  @Override
+  public void taskException(TaskI<AlignmentResult> source, Exception e)
+  {
+    Cache.log.error(String.format("Service %s raised an exception.", service.getName()), e);
+    infoPanel.appendProgressText(e.getMessage());
+  }
+
+  @Override
+  public void taskRestarted(TaskI<AlignmentResult> source)
+  {
+    // alignment services are not restartable
+  }
+
+  @Override
+  public void subJobStatusChanged(TaskI<AlignmentResult> source, JobI job, JobStatus status)
+  {
+    int i = ArrayUtils.indexOf(jobs, job);
+    assert i >= 0 : "job does not exist";
+    if (i < 0)
+      // safeguard that should not happen irl
+      return;
+    int wsStatus;
+    switch (status)
+    {
+    case INVALID:
+    case COMPLETED:
+      wsStatus = WebserviceInfo.STATE_STOPPED_OK;
+      break;
+    case READY:
+    case SUBMITTED:
+    case QUEUED:
+      wsStatus = WebserviceInfo.STATE_QUEUING;
+      break;
+    case RUNNING:
+    case UNKNOWN:
+      wsStatus = WebserviceInfo.STATE_RUNNING;
+      break;
+    case FAILED:
+      wsStatus = WebserviceInfo.STATE_STOPPED_ERROR;
+      break;
+    case CANCELLED:
+      wsStatus = WebserviceInfo.STATE_CANCELLED_OK;
+      break;
+    case SERVER_ERROR:
+      wsStatus = WebserviceInfo.STATE_STOPPED_SERVERERROR;
+      break;
+    default:
+      throw new AssertionError("Non-exhaustive switch statement");
+    }
+    infoPanel.setStatus(tabs[i], wsStatus);
+  }
+
+  @Override
+  public void subJobLogChanged(TaskI<AlignmentResult> source, JobI job, String log)
+  {
+    int i = ArrayUtils.indexOf(jobs, job);
+    assert i >= 0 : "job does not exist";
+    if (i < 0)
+      // safeguard that should never happen
+      return;
+    infoPanel.appendProgressText(tabs[i], log.substring(logOffset[i]));
+  }
+
+  @Override
+  public void subJobErrorLogChanged(TaskI<AlignmentResult> source, JobI job, String log)
+  {
+    int i = ArrayUtils.indexOf(jobs, job);
+    assert i >= 0 : "job does not exist";
+    if (i < 0)
+      // safeguard that should never happen
+      return;
+    infoPanel.appendProgressText(tabs[i], log.substring(errLogOffset[i]));
+  }
+
+}
diff --git a/src/jalview/ws2/helpers/WSClientTaskWrapper.java b/src/jalview/ws2/helpers/WSClientTaskWrapper.java
new file mode 100644 (file)
index 0000000..c9032f6
--- /dev/null
@@ -0,0 +1,52 @@
+package jalview.ws2.helpers;
+
+import jalview.gui.WebserviceInfo;
+import jalview.ws.WSClientI;
+import jalview.ws2.actions.api.TaskI;
+
+/**
+ * A simple wrapper around the {@link TaskI} implementing {@link WSClientI}. Its
+ * main purpose is to delegate the call to {@link #cancelJob} to the underlying
+ * task.
+ * 
+ * @author mmwarowny
+ */
+public class WSClientTaskWrapper implements WSClientI
+{
+  private TaskI<?> delegate;
+
+  private boolean cancellable;
+
+  private boolean canMerge;
+
+  public WSClientTaskWrapper(TaskI<?> task, boolean cancellable, boolean canMerge)
+  {
+    this.delegate = task;
+    this.cancellable = cancellable;
+    this.canMerge = canMerge;
+  }
+
+  public WSClientTaskWrapper(TaskI<?> task)
+  {
+    this(task, true, false);
+  }
+
+  @Override
+  public boolean isCancellable()
+  {
+    return cancellable;
+  }
+
+  @Override
+  public boolean canMergeResults()
+  {
+    return canMerge;
+  }
+
+  @Override
+  public void cancelJob()
+  {
+    delegate.cancel();
+  }
+
+}