JAL-3878 Add multiple sequence operation.
authorMateusz Warowny <mmzwarowny@dundee.ac.uk>
Mon, 22 Nov 2021 13:58:34 +0000 (14:58 +0100)
committerMateusz Warowny <mmzwarowny@dundee.ac.uk>
Mon, 22 Nov 2021 14:45:14 +0000 (15:45 +0100)
src/jalview/ws2/gui/AlignmentMenuBuilder.java [new file with mode: 0644]
src/jalview/ws2/gui/AlignmentProgressUpdater.java [new file with mode: 0644]
src/jalview/ws2/gui/WebServiceInfoUpdater.java [new file with mode: 0644]
src/jalview/ws2/operations/AlignmentOperation.java [new file with mode: 0644]
src/jalview/ws2/operations/AlignmentWorker.java [new file with mode: 0644]
src/jalview/ws2/operations/WebServiceWorkerI.java
src/jalview/ws2/slivka/SlivkaWSDiscoverer.java
src/jalview/ws2/slivka/SlivkaWebService.java

diff --git a/src/jalview/ws2/gui/AlignmentMenuBuilder.java b/src/jalview/ws2/gui/AlignmentMenuBuilder.java
new file mode 100644 (file)
index 0000000..b29bde5
--- /dev/null
@@ -0,0 +1,206 @@
+package jalview.ws2.gui;
+
+import static java.lang.String.format;
+
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletionStage;
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+import javax.swing.ToolTipManager;
+
+import jalview.datamodel.AlignmentView;
+import jalview.gui.AlignFrame;
+import jalview.gui.AlignViewport;
+import jalview.gui.JvSwingUtils;
+import jalview.gui.WebserviceInfo;
+import jalview.gui.WsJobParameters;
+import jalview.util.MessageManager;
+import jalview.ws.params.ArgumentI;
+import jalview.ws.params.ParamDatastoreI;
+import jalview.ws.params.WsParamSetI;
+import jalview.ws2.PollingTaskExecutor;
+import jalview.ws2.operations.AlignmentOperation;
+import jalview.ws2.operations.AlignmentWorker;
+
+public class AlignmentMenuBuilder implements MenuEntryProviderI
+{
+  AlignmentOperation operation;
+
+  public AlignmentMenuBuilder(AlignmentOperation operation)
+  {
+    this.operation = operation;
+  }
+
+  public void buildMenu(JMenu parent, AlignFrame frame)
+  {
+    if (operation.canSubmitGaps())
+    {
+      var alignSubmenu = new JMenu(operation.getName());
+      buildMenu(alignSubmenu, frame, false);
+      parent.add(alignSubmenu);
+      var realignSubmenu = new JMenu(MessageManager.formatMessage(
+          "label.realign_with_params", operation.getName()));
+      realignSubmenu.setToolTipText(MessageManager
+          .getString("label.align_sequences_to_existing_alignment"));
+      buildMenu(realignSubmenu, frame, true);
+      parent.add(realignSubmenu);
+    }
+    else
+    {
+      buildMenu(parent, frame, false);
+    }
+  }
+
+  protected void buildMenu(JMenu parent, AlignFrame frame,
+      boolean submitGaps)
+  {
+    final String action = submitGaps ? "Align" : "Realign";
+    final var calcName = operation.getName();
+
+    {
+      var item = new JMenuItem(MessageManager.formatMessage(
+          "label.calcname_with_default_settings", calcName));
+      item.setToolTipText(MessageManager
+          .formatMessage("label.action_with_default_settings", action));
+      item.addActionListener((event) -> {
+        final AlignmentView msa = frame.gatherSequencesForAlignment();
+        if (msa != null)
+        {
+          startWorker(frame, msa, Collections.emptyList(), submitGaps);
+        }
+      });
+      parent.add(item);
+    }
+
+    if (operation.hasParameters())
+    {
+      var item = new JMenuItem(
+          MessageManager.getString("label.edit_settings_and_run"));
+      item.setToolTipText(MessageManager.getString(
+          "label.view_and_change_parameters_before_alignment"));
+      item.addActionListener((event) -> {
+        AlignmentView msa = frame.gatherSequencesForAlignment();
+        if (msa != null)
+        {
+          openEditParamsDialog(operation.getParamStore(), null, null)
+              .thenAcceptAsync((arguments) -> {
+                if (arguments != null)
+                {
+                  startWorker(frame, msa, arguments, submitGaps);
+                }
+              });
+        }
+      });
+      parent.add(item);
+    }
+
+    var presets = operation.getParamStore().getPresets();
+    if (presets != null && presets.size() > 0)
+    {
+      final var presetList = new JMenu(MessageManager
+          .formatMessage("label.run_with_preset_params", calcName));
+      final var showToolTipFor = ToolTipManager.sharedInstance()
+          .getDismissDelay();
+      for (final var preset : presets)
+      {
+        var item = new JMenuItem(preset.getName());
+        final int QUICK_TOOLTIP = 1500;
+        item.addMouseListener(new MouseAdapter()
+        {
+          @Override
+          public void mouseEntered(MouseEvent e)
+          {
+            ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
+          }
+
+          @Override
+          public void mouseExited(MouseEvent e)
+          {
+            ToolTipManager.sharedInstance().setDismissDelay(showToolTipFor);
+          }
+        });
+        String tooltip = JvSwingUtils.wrapTooltip(true,
+            format("<strong>%s</strong><br/>%s",
+                MessageManager.getString(
+                    preset.isModifiable() ? "label.user_preset"
+                        : "label.service_preset"),
+                preset.getDescription()));
+        item.setToolTipText(tooltip);
+        item.addActionListener((event) -> {
+          AlignmentView msa = frame.gatherSequencesForAlignment();
+          startWorker(frame, msa, preset.getArguments(), submitGaps);
+        });
+        presetList.add(item);
+      }
+      parent.add(presetList);
+    }
+  }
+
+  private CompletionStage<List<ArgumentI>> openEditParamsDialog(
+      ParamDatastoreI paramStore, WsParamSetI preset,
+      List<ArgumentI> arguments)
+  {
+    WsJobParameters jobParams;
+    if (preset == null && arguments != null && arguments.size() > 0)
+      jobParams = new WsJobParameters(paramStore, preset, arguments);
+    else
+      jobParams = new WsJobParameters(paramStore, preset, null);
+    if (preset != null)
+    {
+      jobParams.setName(MessageManager.getString(
+          "label.adjusting_parameters_for_calculation"));
+    }
+    var stage = jobParams.showRunDialog();
+    return stage.thenApply((startJob) -> {
+      if (startJob)
+      {
+        if (jobParams.getPreset() == null)
+        {
+          return jobParams.getJobParams();
+        }
+        else
+        {
+          return jobParams.getPreset().getArguments();
+        }
+      }
+      else
+      {
+        return null;
+      }
+    });
+  }
+
+  private void startWorker(AlignFrame frame, AlignmentView msa,
+      List<ArgumentI> arguments, boolean submitGaps)
+  {
+    AlignViewport viewport = frame.getViewport();
+    PollingTaskExecutor executor = frame.getViewport().getWSExecutor();
+    if (msa != null)
+    {
+
+      String panelInfo = String.format("%s using service hosted at %s%n%s",
+          operation.getName(), operation.getHostName(),
+          Objects.requireNonNullElse(operation.getDescription(), ""));
+      var wsInfo = new WebserviceInfo(operation.getName(), panelInfo, false);
+
+      final String alnTitle = frame.getTitle();
+      AlignmentWorker worker = new AlignmentWorker(operation, msa,
+          arguments, submitGaps, true, viewport);
+      String outputHeader = String.format("%s of %s%nJob details%n",
+          submitGaps ? "Re-alignment" : "Alignment", alnTitle);
+
+      var awl = new AlignmentProgressUpdater(worker, wsInfo, frame,
+          outputHeader);
+      worker.setResultConsumer(awl);
+      worker.addListener(awl);
+
+      executor.submit(worker);
+    }
+
+  }
+
+}
diff --git a/src/jalview/ws2/gui/AlignmentProgressUpdater.java b/src/jalview/ws2/gui/AlignmentProgressUpdater.java
new file mode 100644 (file)
index 0000000..9704432
--- /dev/null
@@ -0,0 +1,170 @@
+package jalview.ws2.gui;
+
+import static java.lang.String.format;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import jalview.ws2.operations.WebServiceWorkerListener;
+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.WebserviceInfo;
+import jalview.util.MessageManager;
+import jalview.ws2.operations.AlignmentWorker.AlignmentResult;
+import jalview.ws2.WSJob;
+import jalview.ws2.operations.WebServiceWorkerI;
+
+public class AlignmentProgressUpdater
+    implements WebServiceWorkerListener, Consumer<AlignmentResult>
+{
+  WebServiceWorkerI worker;
+
+  WebserviceInfo wsInfo;
+
+  AlignFrame frame;
+
+  private final WebServiceInfoUpdater wsInfoUpdater;
+
+  String outputHeader;
+
+  AlignmentProgressUpdater(WebServiceWorkerI worker,
+      WebserviceInfo wsInfo, AlignFrame frame, String header)
+  {
+    this.worker = worker;
+    this.wsInfo = wsInfo;
+    this.outputHeader = header;
+    this.wsInfoUpdater = new WebServiceInfoUpdater(worker, wsInfo);
+    wsInfoUpdater.setOutputHeader(header);
+  }
+
+  @Override
+  public void workerStarted(WebServiceWorkerI source)
+  {
+    wsInfo.setVisible(true);
+  }
+
+  @Override
+  public void workerNotStarted(WebServiceWorkerI source)
+  {
+    wsInfo.setVisible(false);
+    // TODO show notification dialog.
+    JvOptionPane.showMessageDialog(frame,
+        MessageManager.getString("info.invalid_msa_input_mininfo"),
+        MessageManager.getString("info.invalid_msa_notenough"),
+        JvOptionPane.INFORMATION_MESSAGE);
+  }
+
+  @Override
+  public void jobCreated(WebServiceWorkerI source, WSJob job)
+  {
+    int tabIndex = wsInfo.addJobPane();
+    wsInfo.setProgressName(format("region %d", job.getJobNum()), tabIndex);
+    wsInfo.setProgressText(tabIndex, outputHeader);
+    job.addPropertyChangeListener(wsInfoUpdater);
+  }
+
+  @Override
+  public void pollException(WebServiceWorkerI source, WSJob job, Exception e)
+  {
+    wsInfo.appendProgressText(job.getJobNum(),
+        MessageManager.formatMessage("info.server_exception",
+            source.getOperation().getName(), e.getMessage()));
+  }
+
+  @Override
+  public void workerCompleting(WebServiceWorkerI source)
+  {
+    wsInfo.setProgressBar(
+        MessageManager.getString("status.collecting_job_results"),
+        worker.getUID());
+  }
+
+  @Override
+  public void workerCompleted(WebServiceWorkerI source)
+  {
+    wsInfo.removeProgressBar(worker.getUID());
+  }
+
+  @Override
+  public void accept(AlignmentResult out)
+  {
+    if (out != null)
+    {
+      wsInfo.showResultsNewFrame.addActionListener(evt -> displayNewFrame(
+          new Alignment(out.getAlignment()), out.getAlignmentOrders(),
+          out.getHiddenColumns()));
+      wsInfo.setResultsReady();
+    }
+    else
+    {
+      wsInfo.setFinishedNoResults();
+    }
+  }
+
+  private void displayNewFrame(AlignmentI aln,
+      List<AlignmentOrder> alorders, HiddenColumns hidden)
+  {
+    AlignFrame frame = new AlignFrame(aln, hidden,
+        AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
+    // TODO store feature renderer settings in worker object
+    // frame.getFeatureRenderer().transferSettings(featureSettings);
+    var regions = sortOrders(alorders);
+    if (alorders.size() == 1)
+    {
+      frame.addSortByOrderMenuItem(format("%s Ordering",
+          worker.getOperation().getName()), alorders.get(0));
+    }
+    else
+    {
+      for (int i = 0; i < alorders.size(); i++)
+      {
+        final int j = i;
+        Iterable<String> iter = () -> regions.get(j).stream()
+            .map(it -> Integer.toString(it)).iterator();
+        var orderName = format("%s Region %s Ordering",
+            worker.getOperation().getName(), String.join(",", iter));
+        frame.addSortByOrderMenuItem(orderName, alorders.get(i));
+      }
+    }
+
+    /* TODO
+     * If alignment was requested from one half of a SplitFrame, show in a
+     * SplitFrame with the other pane similarly aligned.
+     */
+
+    Desktop.addInternalFrame(frame, frame.getTitle(), AlignFrame.DEFAULT_WIDTH,
+        AlignFrame.DEFAULT_HEIGHT);
+  }
+
+  private List<List<Integer>> sortOrders(List<?> alorders)
+  {
+    List<List<Integer>> regions = new ArrayList<>();
+    for (int i = 0; i < alorders.size(); i++)
+    {
+      List<Integer> regs = new ArrayList<>();
+      regs.add(i);
+      int j = i + 1;
+      while (j < alorders.size())
+      {
+        if (alorders.get(i).equals(alorders.get(j)))
+        {
+          alorders.remove(j);
+          regs.add(j);
+        }
+        else
+        {
+          j++;
+        }
+      }
+      regions.add(regs);
+    }
+    return regions;
+  }
+
+}
diff --git a/src/jalview/ws2/gui/WebServiceInfoUpdater.java b/src/jalview/ws2/gui/WebServiceInfoUpdater.java
new file mode 100644 (file)
index 0000000..f674a9c
--- /dev/null
@@ -0,0 +1,145 @@
+package jalview.ws2.gui;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.Objects;
+
+import jalview.gui.WebserviceInfo;
+import jalview.ws2.WSJob;
+import jalview.ws2.WSJobStatus;
+import jalview.ws2.operations.WebServiceWorkerI;
+
+/**
+ * A helper class that can be attached as a listener to the {@link WSJob}
+ * object. It updates the job status in the {@link jalview.gui.WebServiceInfo}
+ * window according to the state changes of the job object.
+ * 
+ * The {@link WebServiceInfoUpdater} object allows to decouple GUI updates
+ * from the web service worker logic.
+ * 
+ * @author mmwarowny
+ *
+ */
+public class WebServiceInfoUpdater implements PropertyChangeListener
+{
+  private final WebServiceWorkerI worker;
+  private final WebserviceInfo wsInfo;
+
+  private String outputHeader = "";
+
+  public WebServiceInfoUpdater(WebServiceWorkerI worker, WebserviceInfo wsInfo)
+  {
+    this.worker = worker;
+    this.wsInfo = wsInfo;
+  }
+
+  public String getOutputHeader()
+  {
+    return outputHeader;
+  }
+
+  public void setOutputHeader(String header)
+  {
+    this.outputHeader = header;
+  }
+
+  @Override
+  public void propertyChange(PropertyChangeEvent evt)
+  {
+    switch (evt.getPropertyName())
+    {
+    case "status":
+      statusChanged(evt);
+      break;
+    case "log":
+      logChanged(evt);
+      break;
+    case "errorLog":
+      errorLogChanged(evt);
+      break;
+    }
+  }
+
+  private void statusChanged(PropertyChangeEvent evt)
+  {
+    WSJob job = (WSJob) evt.getSource();
+    WSJobStatus status = (WSJobStatus) evt.getNewValue();
+    int wsInfoStatus = 0;
+    switch (status)
+    {
+    case READY:
+    case SUBMITTED:
+    case QUEUED:
+      wsInfoStatus = WebserviceInfo.STATE_QUEUING;
+      break;
+    case RUNNING:
+      wsInfoStatus = WebserviceInfo.STATE_RUNNING;
+      break;
+    case FINISHED:
+      wsInfoStatus = WebserviceInfo.STATE_STOPPED_OK;
+      break;
+    case CANCELLED:
+      wsInfoStatus = WebserviceInfo.STATE_CANCELLED_OK;
+      break;
+    case INVALID:
+    case BROKEN:
+    case FAILED:
+    case UNKNOWN:
+      wsInfoStatus = WebserviceInfo.STATE_STOPPED_ERROR;
+      break;
+    case SERVER_ERROR:
+      wsInfoStatus = WebserviceInfo.STATE_STOPPED_SERVERERROR;
+      break;
+    }
+    wsInfo.setStatus(job.getJobNum(), wsInfoStatus);
+    updateWSInfoGlobalStatus();
+  }
+
+  private void logChanged(PropertyChangeEvent evt)
+  {
+    WSJob job = (WSJob) evt.getSource();
+    String oldLog = (String) evt.getOldValue();
+    String newLog = (String) evt.getNewValue();
+    wsInfo.appendProgressText(job.getJobNum(),
+            newLog.substring(oldLog.length()));
+  }
+
+  private void errorLogChanged(PropertyChangeEvent evt)
+  {
+    WSJob job = (WSJob) evt.getSource();
+    String oldLog = (String) evt.getOldValue();
+    String newLog = (String) evt.getNewValue();
+    wsInfo.appendProgressText(job.getJobNum(),
+            newLog.substring(oldLog.length()));
+  }
+
+
+  private void updateWSInfoGlobalStatus()
+  {
+    var jobs = worker.getJobs();
+    if (jobs.countRunning() > 0)
+    {
+      wsInfo.setStatus(WebserviceInfo.STATE_RUNNING);
+    }
+    else if (jobs.countQueuing() > 0
+            || jobs.countSubmitted() < jobs.size())
+    {
+      wsInfo.setStatus(WebserviceInfo.STATE_QUEUING);
+    }
+    else
+    {
+      if (jobs.countSuccessful() > 0)
+      {
+        wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_OK);
+      }
+      else if (jobs.countCancelled() > 0)
+      {
+        wsInfo.setStatus(WebserviceInfo.STATE_CANCELLED_OK);
+      }
+      else if (jobs.countFailed() > 0)
+      {
+        wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
+      }
+    }
+  }
+}
diff --git a/src/jalview/ws2/operations/AlignmentOperation.java b/src/jalview/ws2/operations/AlignmentOperation.java
new file mode 100644 (file)
index 0000000..2b510e9
--- /dev/null
@@ -0,0 +1,35 @@
+package jalview.ws2.operations;
+
+import java.io.IOException;
+
+import jalview.datamodel.AlignmentI;
+import jalview.ws2.WSJob;
+import jalview.ws2.WebServiceI;
+import jalview.ws2.gui.AlignmentMenuBuilder;
+import jalview.ws2.gui.MenuEntryProviderI;
+
+public class AlignmentOperation extends AbstractOperation
+{
+  public static interface AlignmentResultSupplier {
+    public AlignmentI getAlignment(WSJob job) throws IOException;
+  }
+  
+  private AlignmentResultSupplier alignmentSupplier;
+
+  public AlignmentOperation(WebServiceI service, AlignmentResultSupplier alignmentSupplier)
+  {
+    super(service, "Alignment");
+    this.alignmentSupplier = alignmentSupplier;
+  }
+
+  @Override
+  public MenuEntryProviderI getMenuBuilder()
+  {
+    return new AlignmentMenuBuilder(this);
+  }
+  
+  public AlignmentResultSupplier getAlignmentSupplier()
+  {
+    return alignmentSupplier;
+  }
+}
diff --git a/src/jalview/ws2/operations/AlignmentWorker.java b/src/jalview/ws2/operations/AlignmentWorker.java
new file mode 100644 (file)
index 0000000..79ef527
--- /dev/null
@@ -0,0 +1,415 @@
+package jalview.ws2.operations;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+import jalview.analysis.AlignSeq;
+import jalview.analysis.AlignmentSorter;
+import jalview.analysis.SeqsetUtils;
+import jalview.analysis.SeqsetUtils.SequenceInfo;
+import jalview.bin.Cache;
+import jalview.datamodel.AlignedCodonFrame;
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.AlignmentOrder;
+import jalview.datamodel.AlignmentView;
+import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignViewport;
+import jalview.util.MessageManager;
+import jalview.ws.params.ArgumentI;
+import jalview.ws2.WSJob;
+import jalview.ws2.WSJobStatus;
+
+public class AlignmentWorker extends AbstractPollableWorker
+{
+  private AlignmentOperation operation;
+  
+  private Consumer<AlignmentResult> resultConsumer;
+  
+  private AlignmentView msa;
+
+  private AlignmentI dataset;
+
+  private AlignViewport viewport;
+
+  private List<AlignedCodonFrame> codonFrame = new ArrayList<>();
+
+  private List<ArgumentI> args = Collections.emptyList();
+
+  private boolean submitGaps = false;
+
+  private boolean preserveOrder = false;
+
+  private char gapCharacter;
+
+  private WSJobList<AlignmentJob> jobs = new WSJobList<>();
+
+  private Map<Long, Integer> exceptionCount = new HashMap<>();
+
+  private static final int MAX_RETRY = 5;
+
+  private static class JobInput
+  {
+    final List<SequenceI> inputSequences;
+
+    final List<SequenceI> emptySequences;
+
+    final Map<String, SequenceInfo> sequenceNames;
+
+    private JobInput(List<SequenceI> inputSequences,
+        List<SequenceI> emptySequences,
+        Map<String, SequenceInfo> names)
+    {
+      this.inputSequences = Collections.unmodifiableList(inputSequences);
+      this.emptySequences = Collections.unmodifiableList(emptySequences);
+      this.sequenceNames = names;
+    }
+
+    boolean isInputValid()
+    {
+      return inputSequences.size() >= 2;
+    }
+  }
+  
+  public class AlignmentJob extends WSJob
+  {
+    private List<SequenceI> inputSequences;
+    private List<SequenceI> emptySequences;
+    private Map<String, SequenceInfo> sequenceNames;
+    
+    private AlignmentJob(String serviceProvider, String serviceName, 
+        String hostName)
+    {
+      super(serviceProvider, serviceName, hostName);
+    }
+    
+    private void setInput(JobInput input) {
+      inputSequences = input.inputSequences;
+      emptySequences = input.emptySequences;
+      sequenceNames = input.sequenceNames;
+    }
+  }
+
+  public class AlignmentResult
+  {
+    AlignmentI aln;
+
+    List<AlignmentOrder> alorders;
+
+    HiddenColumns hidden;
+
+    AlignmentResult(AlignmentI aln, List<AlignmentOrder> alorders,
+        HiddenColumns hidden)
+    {
+      this.aln = aln;
+      this.alorders = alorders;
+      this.hidden = hidden;
+    }
+
+    public AlignmentI getAlignment()
+    {
+      return aln;
+    }
+
+    public List<AlignmentOrder> getAlignmentOrders()
+    {
+      return alorders;
+    }
+
+    public HiddenColumns getHiddenColumns()
+    {
+      return hidden;
+    }
+  }
+
+  public AlignmentWorker(AlignmentOperation operation,
+      AlignmentView msa, List<ArgumentI> args,
+      boolean submitGaps, boolean preserveOrder, AlignViewport viewport)
+  {
+    this.operation = operation;
+    this.msa = msa;
+    this.dataset = viewport.getAlignment().getDataset();
+    List<AlignedCodonFrame> cf = Objects.requireNonNullElse(
+        viewport.getAlignment().getCodonFrames(), Collections.emptyList());
+    this.codonFrame.addAll(cf);
+    this.args = args;
+    this.submitGaps = submitGaps;
+    this.preserveOrder = preserveOrder;
+    this.viewport = viewport;
+    this.gapCharacter = viewport.getGapCharacter();
+  }
+
+  @Override
+  public Operation getOperation()
+  {
+    return operation;
+  }
+
+  @Override
+  public WSJobList<? extends WSJob> getJobs()
+  {
+    return jobs;
+  }
+  
+  public void setResultConsumer(Consumer<AlignmentResult> consumer)
+  {
+    this.resultConsumer = consumer;
+  }
+  
+  @Override
+  public void start() throws IOException
+  {
+    Cache.log.info(String.format("Starting new %s job.", operation.getName()));
+    SequenceI[][] conmsa = msa.getVisibleContigs('-');
+    if (conmsa == null)
+    {
+      return;
+    }
+    int numValid = 0;
+    for (int i =  0; i < conmsa.length; i++)
+    {
+      JobInput input = prepareInputData(conmsa[i], 2, submitGaps);
+      AlignmentJob job = new AlignmentJob(operation.service.getProviderName(),
+          operation.getName(), operation.getHostName());
+      job.setJobNum(i);
+      job.setInput(input);
+      jobs.add(job);
+      listeners.fireJobCreated(job);
+      if (input.isInputValid())
+      {
+        int count;
+        String jobId = null;
+        do
+        {
+          count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY);
+          try
+          {
+            jobId = operation.service.submit(input.inputSequences, args);
+            Cache.log.debug((String.format("Job %s submitted", job)));
+            exceptionCount.remove(job.getUid());
+          } catch (IOException e)
+          {
+            exceptionCount.put(job.getUid(), --count);
+          }
+        } while (jobId == null && count > 0);
+        if (jobId != null)
+        {
+          job.setJobId(jobId);
+          job.setStatus(WSJobStatus.SUBMITTED);
+          numValid++;
+        }
+        else
+        {
+          job.setStatus(WSJobStatus.SERVER_ERROR);
+        }
+      }
+      else
+      {
+        job.setStatus(WSJobStatus.INVALID);
+        job.setErrorLog(
+            MessageManager.getString("label.empty_alignment_job"));
+      }
+    }
+    if (numValid > 0)
+    {
+      listeners.fireWorkerStarted();
+    }
+    else
+    {
+      listeners.fireWorkerNotStarted();
+    }
+  }
+  
+
+  private static JobInput prepareInputData(SequenceI[] sequences,
+      int minLength, boolean submitGaps)
+  {
+    assert minLength >= 0 : MessageManager.getString(
+        "error.implementation_error_minlen_must_be_greater_zero");
+    int numSeq = 0;
+    for (SequenceI seq : sequences)
+    {
+      if (seq.getEnd() - seq.getStart() >= minLength)
+      {
+        numSeq++;
+      }
+    }
+
+    List<SequenceI> inputSequences = new ArrayList<>();
+    List<SequenceI> emptySequences = new ArrayList<>();
+    Map<String, SequenceInfo> names = new LinkedHashMap<>();
+    for (int i = 0; i < sequences.length; i++)
+    {
+      SequenceI seq = sequences[i];
+      String newName = SeqsetUtils.unique_name(i);
+      var hash = SeqsetUtils.SeqCharacterHash(seq);
+      names.put(newName, hash);
+      if (numSeq > 1 && seq.getEnd() - seq.getStart() >= minLength)
+      {
+        String seqString = seq.getSequenceAsString();
+        if (!submitGaps)
+        {
+          seqString = AlignSeq.extractGaps(
+              jalview.util.Comparison.GapChars, seqString);
+        }
+        inputSequences.add(new Sequence(newName, seqString));
+      }
+      else
+      {
+        String seqString = "";
+        if (seq.getEnd() >= seq.getStart()) // true if gaps only
+        {
+          seqString = seq.getSequenceAsString();
+          if (!submitGaps)
+          {
+            seqString = AlignSeq.extractGaps(
+                jalview.util.Comparison.GapChars, seqString);
+          }
+        }
+        emptySequences.add(new Sequence(newName, seqString));
+      }
+    }
+
+    return new JobInput(inputSequences, emptySequences, names);
+  }
+
+  @Override
+  public void done()
+  {
+    listeners.fireWorkerCompleting();
+    Map<Long, AlignmentI> results = new LinkedHashMap<>();
+    for (WSJob job : getJobs())
+    {
+      if (job.getStatus().isFailed())
+        continue;
+      try
+      {
+        AlignmentI alignment = operation.getAlignmentSupplier().getAlignment(job);
+        if (alignment != null)
+        {
+          results.put(job.getUid(), alignment);
+        }
+      } catch (Exception e)
+      {
+        if (!operation.getWebService().handleCollectionError(job, e))
+        {
+          Cache.log.error("Couldn't get alignment for job.", e);
+          // TODO: Increment exception count and retry.
+          job.setStatus(WSJobStatus.SERVER_ERROR);
+        }
+      }
+    }
+    if (results.size() > 0)
+    {
+      AlignmentResult out = prepareResult(results);
+      resultConsumer.accept(out);
+    }
+    else
+    {
+      resultConsumer.accept(null);
+    }
+    listeners.fireWorkerCompleted();
+  }
+
+  private AlignmentResult prepareResult(Map<Long, AlignmentI> alignments)
+  {
+    List<AlignmentOrder> alorders = new ArrayList<>();
+    SequenceI[][] results = new SequenceI[jobs.size()][];
+    AlignmentOrder[] orders = new AlignmentOrder[jobs.size()];
+    for (int i = 0; i < jobs.size(); i++)
+    {
+      AlignmentJob job = jobs.get(i);
+      AlignmentI aln = alignments.get(job.getUid());
+      if (aln != null) // equivalent of job.hasResults()
+      {
+        /* Get the alignment including any empty sequences in the original
+         * order with original ids. */
+        char gapChar = aln.getGapCharacter();
+        List<SequenceI> emptySeqs = job.emptySequences;
+        List<SequenceI> alnSeqs = aln.getSequences();
+        // find the width of the longest sequence
+        int width = 0;
+        for (var seq : alnSeqs)
+          width = Integer.max(width, seq.getLength());
+        for (var emptySeq : emptySeqs)
+          width = Integer.max(width, emptySeq.getLength());
+        // pad shorter sequences with gaps
+        String gapSeq = String.join("",
+            Collections.nCopies(width, Character.toString(gapChar)));
+        List<SequenceI> seqs = new ArrayList<>(
+            alnSeqs.size() + emptySeqs.size());
+        seqs.addAll(alnSeqs);
+        seqs.addAll(emptySeqs);
+        for (var seq : seqs)
+        {
+          if (seq.getLength() < width)
+            seq.setSequence(seq.getSequenceAsString()
+                + gapSeq.substring(seq.getLength()));
+        }
+        SequenceI[] result = seqs.toArray(new SequenceI[0]);
+        AlignmentOrder msaOrder = new AlignmentOrder(result);
+        AlignmentSorter.recoverOrder(result);
+        Map<String, SequenceInfo> names = new HashMap<>(job.sequenceNames);
+        // FIXME first call to deuniquify alters original alignment
+        SeqsetUtils.deuniquify(names, result);
+        alorders.add(msaOrder);
+        results[i] = result;
+        orders[i] = msaOrder;
+      }
+      else
+      {
+        results[i] = null;
+      }
+    }
+
+    Object[] newView = msa.getUpdatedView(results, orders, gapCharacter);
+    // free references to original data
+    for (int i = 0; i < jobs.size(); i++)
+    {
+      results[i] = null;
+      orders[i] = null;
+    }
+    SequenceI[] alignment = (SequenceI[]) newView[0];
+    HiddenColumns hidden = (HiddenColumns) newView[1];
+    Alignment aln = new Alignment(alignment);
+    aln.setProperty("Alignment Program", operation.getName());
+    if (dataset != null)
+      aln.setDataset(dataset);
+
+    propagateDatasetMappings(aln);
+    return new AlignmentResult(aln, alorders, hidden);
+    // displayNewFrame(aln, alorders, hidden);
+  }
+
+  /*
+   * conserves dataset references to sequence objects returned from web
+   * services. propagate codon frame data to alignment.
+   */
+  private void propagateDatasetMappings(Alignment aln)
+  {
+    if (codonFrame != null)
+    {
+      SequenceI[] alignment = aln.getSequencesArray();
+      for (SequenceI seq : alignment)
+      {
+        for (AlignedCodonFrame acf : codonFrame)
+        {
+          if (acf != null && acf.involvesSequence(seq))
+          {
+            aln.addCodonFrame(acf);
+            break;
+          }
+        }
+      }
+    }
+  }
+}
index 5ec7e97..e81dfcb 100644 (file)
@@ -4,6 +4,8 @@ import jalview.ws2.WSJob;
 
 public interface WebServiceWorkerI
 {
+  long getUID();
+
   Operation getOperation();
 
   WSJobList<? extends WSJob> getJobs();
index 802254b..b6004fc 100644 (file)
@@ -182,7 +182,7 @@ public class SlivkaWSDiscoverer implements WebServiceDiscovererI
               op = new OperationStub(webService, "Protein Disorder");
               break;
             case "multiple sequence alignment":
-              op = new OperationStub(webService, "Alignment");
+              op = new AlignmentOperation(webService, webService::getAlignment);
               break;
             }
             if (op != null)
index a2d7f20..bf61ca6 100644 (file)
@@ -218,8 +218,7 @@ public class SlivkaWebService implements WebServiceI
     return false;
   }
 
-  public AlignmentI getAlignment(WSJob job, List<SequenceI> dataset,
-      AlignViewportI viewport) throws IOException
+  public AlignmentI getAlignment(WSJob job) throws IOException
   {
     Collection<RemoteFile> files;
     var slivkaJob = client.getJob(job.getJobId());