Merge branch 'JAL-3878_ws-overhaul-3' into mmw/Release_2_12_ws_merge
[jalview.git] / src / jalview / ws2 / actions / alignment / AlignmentTask.java
diff --git a/src/jalview/ws2/actions/alignment/AlignmentTask.java b/src/jalview/ws2/actions/alignment/AlignmentTask.java
new file mode 100644 (file)
index 0000000..96e9a12
--- /dev/null
@@ -0,0 +1,223 @@
+package jalview.ws2.actions.alignment;
+
+import static java.lang.String.format;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import jalview.analysis.AlignmentSorter;
+import jalview.analysis.SeqsetUtils;
+import jalview.analysis.SeqsetUtils.SequenceInfo;
+import jalview.api.AlignViewportI;
+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.ws.params.ArgumentI;
+import jalview.ws2.actions.AbstractPollableTask;
+import jalview.ws2.actions.ServiceInputInvalidException;
+import jalview.ws2.actions.api.TaskEventListener;
+import jalview.ws2.api.Credentials;
+import jalview.ws2.api.JobStatus;
+import jalview.ws2.client.api.AlignmentWebServiceClientI;
+
+/**
+ * Implementation of an abstract pollable task used by alignment service
+ * actions.
+ * 
+ * @author mmwarowny
+ *
+ */
+class AlignmentTask extends AbstractPollableTask<AlignmentJob, AlignmentResult>
+{
+  /* task parameters set in the constructor */
+  private final AlignmentWebServiceClientI client;
+
+  private final AlignmentAction action;
+
+  private final AlignmentView msa; // a.k.a. input
+
+  private final AlignViewportI viewport;
+
+  private final boolean submitGaps;
+
+  private final AlignmentI currentView;
+
+  private final AlignmentI dataset;
+
+  private final char gapChar;
+
+  private final List<AlignedCodonFrame> codonFrame = new ArrayList<>();
+
+  AlignmentTask(AlignmentWebServiceClientI client, AlignmentAction action,
+      List<ArgumentI> args, Credentials credentials,
+      AlignmentView msa, AlignViewportI viewport, boolean submitGaps,
+      TaskEventListener<AlignmentResult> eventListener)
+  {
+    super(client, args, credentials, eventListener);
+    this.client = client;
+    this.action = action;
+    this.msa = msa;
+    this.viewport = viewport;
+    this.submitGaps = submitGaps;
+    this.currentView = viewport.getAlignment();
+    this.dataset = viewport.getAlignment().getDataset();
+    this.gapChar = viewport.getGapCharacter();
+    List<AlignedCodonFrame> cf = viewport.getAlignment().getCodonFrames();
+    if (cf != null)
+      this.codonFrame.addAll(cf);
+  }
+  
+  @Override
+  protected List<AlignmentJob> prepare() throws ServiceInputInvalidException
+  { 
+    Cache.log.info(format("starting alignment service %s:%s",
+        client.getClientName(), action.getName()));
+    SequenceI[][] conmsa = msa.getVisibleContigs(gapChar);
+    if (conmsa == null)
+    {
+      throw new ServiceInputInvalidException("no visible contigs for alignment");
+    }
+    List<AlignmentJob> jobs = new ArrayList<>(conmsa.length);
+    boolean validInput = false;
+    for (int i = 0; i < conmsa.length; i++)
+    {
+      AlignmentJob job = AlignmentJob.create(conmsa[i], 2, submitGaps);
+      validInput |= job.isInputValid();  // at least one input is valid
+      job.setStatus(job.isInputValid() ? JobStatus.READY : JobStatus.INVALID);
+      jobs.add(job);
+    }
+    this.jobs = jobs;
+    if (!validInput)
+    {
+      throw new ServiceInputInvalidException("no valid sequences for alignment");
+    }
+    return jobs;
+  }
+
+  @Override
+  protected AlignmentResult done() throws IOException
+  {
+    IOException lastIOE = null;
+    for (AlignmentJob job : jobs)
+    {
+      if (job.isInputValid() && job.getStatus() == JobStatus.COMPLETED &&
+          !job.hasResult())
+      {
+        try
+        {
+          job.setAlignmentResult(client.getAlignment(job.getServerJob()));
+        } catch (IOException e)
+        {
+          lastIOE = e;
+        }
+      }
+    }
+    if (lastIOE != null)
+      throw lastIOE;  // do not proceed unless all results has been retrieved
+    
+    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++)
+    {
+      /* alternative implementation of MsaWSJob#getAlignment */
+      AlignmentJob job = jobs.get(i);
+      if (!job.hasResult())
+        continue;
+      AlignmentI alignment = job.getAlignmentResult();
+      int alnSize = alignment.getSequences().size();
+      char gapChar = alnSize > 0 ? alignment.getGapCharacter() : '-';
+      List<SequenceI> emptySeqs = job.getEmptySequences();
+      List<SequenceI> alnSeqs = new ArrayList<>(alnSize);
+      // create copies of all sequences involved
+      for (SequenceI seq : alignment.getSequences())
+      {
+        alnSeqs.add(new Sequence(seq));
+      }
+      for (SequenceI seq : emptySeqs)
+      {
+        alnSeqs.add(new Sequence(seq));
+      }
+      // find the width of the longest sequence
+      int width = 0;
+      for (var seq: alnSeqs)
+        width = Integer.max(width, seq.getLength());
+      // make a sequence of gaps only to cut/paste
+      String gapSeq;
+      {
+        char[] gaps = new char[width];
+        Arrays.fill(gaps, gapChar);
+        gapSeq = new String(gaps);
+      }
+      for (var seq: alnSeqs)
+      {
+        if (seq.getLength() < width)
+        {
+          // pad sequences shorter than the target width with gaps
+          seq.setSequence(seq.getSequenceAsString()
+              + gapSeq.substring(seq.getLength()));
+        }
+      }
+      SequenceI[] result = alnSeqs.toArray(new SequenceI[0]);
+      AlignmentOrder msaOrder = new AlignmentOrder(result);
+      AlignmentSorter.recoverOrder(result);
+      Map<String, SequenceInfo> names = new HashMap<>(job.getNames());
+      SeqsetUtils.deuniquify(names, result);
+      
+      alorders.add(msaOrder);
+      results[i] = result;
+      orders[i] = msaOrder;
+    }
+    Object[] newView = msa.getUpdatedView(results, orders, gapChar);
+    // 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", action.getName());
+    if (dataset != null)
+      aln.setDataset(dataset);
+    
+    propagateDatasetMappings(aln);
+    return new AlignmentResult(aln, alorders, hidden);
+  }
+  
+  /**
+   * Conserve dataset references to sequence objects returned from web services.
+   * Propagate AlignedCodonFrame data from {@code codonFrame} to {@code aln}.
+   * TODO: Refactor to datamodel
+   */
+  private void propagateDatasetMappings(AlignmentI aln)
+  {
+    if (codonFrame != null)
+    {
+      SequenceI[] alignment = aln.getSequencesArray();
+      for (final SequenceI seq : alignment)
+      {
+        for (AlignedCodonFrame acf : codonFrame)
+        {
+          if (acf != null && acf.involvesSequence(seq))
+          {
+            aln.addCodonFrame(acf);
+            break;
+          }
+        }
+      }
+    }
+  }
+}