JAL-3878 Create alignment service "backend" classes
authorMateusz Warowny <mmzwarowny@dundee.ac.uk>
Thu, 3 Mar 2022 13:57:55 +0000 (14:57 +0100)
committerMateusz Warowny <mmzwarowny@dundee.ac.uk>
Thu, 3 Mar 2022 14:03:02 +0000 (15:03 +0100)
JAL-3878 Implement AlignmentAction#perform method

src/jalview/ws2/actions/alignment/AlignmentAction.java
src/jalview/ws2/actions/alignment/AlignmentJob.java [new file with mode: 0644]
src/jalview/ws2/actions/alignment/AlignmentResult.java
src/jalview/ws2/actions/alignment/AlignmentTask.java [new file with mode: 0644]

index 8c0cd93..d8f5a1d 100644 (file)
@@ -72,12 +72,16 @@ public class AlignmentAction extends BaseAction<AlignmentResult>
       List<ArgumentI> args, Credentials credentials,
       TaskEventListener<AlignmentResult> handler)
   {
-    
+    var msa = viewport.getAlignmentView(true);
+    var task = new AlignmentTask(
+        client, this, args, credentials, msa, viewport, submitGaps, handler);
+    task.start(viewport.getServiceExecutor());
+    return task;
   }
 
   /**
-   * Returns if the action is active for the given viewport.
-   * Alignment services are non-interactive, so the action is never active.
+   * Returns if the action is active for the given viewport. Alignment services
+   * are non-interactive, so the action is never active.
    */
   @Override
   public boolean isActive(AlignmentViewport viewport)
diff --git a/src/jalview/ws2/actions/alignment/AlignmentJob.java b/src/jalview/ws2/actions/alignment/AlignmentJob.java
new file mode 100644 (file)
index 0000000..5ddb64a
--- /dev/null
@@ -0,0 +1,114 @@
+package jalview.ws2.actions.alignment;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import jalview.analysis.AlignSeq;
+import jalview.analysis.SeqsetUtils;
+import jalview.analysis.SeqsetUtils.SequenceInfo;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceI;
+import jalview.util.Comparison;
+import jalview.ws2.actions.BaseJob;
+import jalview.ws2.api.WebServiceJobHandle;
+
+/**
+ * A wrapper class that extends basic job container with data specific to
+ * alignment services. It stores input and empty sequences (with uniquified
+ * names) along the original sequence information. {@link AlignmentJob} objects
+ * are created by {@link AlignmentTask} during a preparation stage.
+ * 
+ * @author mmwarowny
+ *
+ */
+class AlignmentJob extends BaseJob
+{
+  private final List<SequenceI> emptySeqs;
+
+  private final Map<String, SequenceInfo> names;
+
+  private AlignmentI alignmentResult;
+
+  AlignmentJob(List<SequenceI> inputSeqs, List<SequenceI> emptySeqs,
+      Map<String, SequenceInfo> names)
+  {
+    super(Collections.unmodifiableList(inputSeqs));
+    this.emptySeqs = Collections.unmodifiableList(emptySeqs);
+    this.names = Collections.unmodifiableMap(names);
+  }
+
+  public static AlignmentJob create(SequenceI[] seqs, int minlen, boolean keepGaps)
+  {
+    int nseqs = 0;
+    for (int i = 0; i < seqs.length; i++)
+    {
+      if (seqs[i].getEnd() - seqs[i].getStart() >= minlen)
+        nseqs++;
+    }
+    boolean valid = nseqs > 1; // need at least two sequences
+    Map<String, SequenceInfo> names = new LinkedHashMap<>();
+    List<SequenceI> inputSeqs = new ArrayList<>();
+    List<SequenceI> emptySeqs = new ArrayList<>();
+    for (int i = 0; i < seqs.length; i++)
+    {
+      SequenceI seq = seqs[i];
+      String newName = SeqsetUtils.unique_name(i);
+      names.put(newName, SeqsetUtils.SeqCharacterHash(seq));
+      if (valid && seq.getEnd() - seq.getStart() >= minlen)
+      {
+        // make new input sequence
+        String seqString = seq.getSequenceAsString();
+        if (!keepGaps)
+          seqString = AlignSeq.extractGaps(Comparison.GapChars, seqString);
+        inputSeqs.add(new Sequence(newName, seqString));
+      }
+      else
+      {
+        String seqString = "";
+        if (seq.getEnd() >= seq.getStart()) // true if gaps only
+        {
+          seqString = seq.getSequenceAsString();
+          if (!keepGaps)
+            seqString = AlignSeq.extractGaps(Comparison.GapChars, seqString);
+        }
+        emptySeqs.add(new Sequence(newName, seqString));
+      }
+    }
+    return new AlignmentJob(inputSeqs, emptySeqs, names);
+  }
+
+  @Override
+  public boolean isInputValid()
+  {
+    return inputSeqs.size() >= 2;
+  }
+
+  List<SequenceI> getEmptySequences()
+  {
+    return emptySeqs;
+  }
+
+  Map<String, SequenceInfo> getNames()
+  {
+    return names;
+  }
+
+  boolean hasResult()
+  {
+    return alignmentResult != null;
+  }
+
+  AlignmentI getAlignmentResult()
+  {
+    return alignmentResult;
+  }
+
+  void setAlignmentResult(AlignmentI alignment)
+  {
+    this.alignmentResult = alignment;
+  }
+}
index d3ed8fc..9090dd2 100644 (file)
@@ -1,6 +1,48 @@
 package jalview.ws2.actions.alignment;
 
+import java.util.List;
+
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.AlignmentOrder;
+import jalview.datamodel.HiddenColumns;
+import jalview.ws2.actions.api.TaskEventListener;
+
+/**
+ * A data container storing the output of multiple sequence alignment services.
+ * The object is constructed by an {@link AlignmentTask} on completion and
+ * passed to the handler {@link TaskEventListener#taskCompleted(TaskI, Object)}
+ * method as a result.
+ * 
+ * @author mmwarowny
+ */
 public class AlignmentResult
 {
+  final AlignmentI aln;
+
+  final List<AlignmentOrder> alorders;
+
+  final 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;
+  }
 }
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..214cd02
--- /dev/null
@@ -0,0 +1,219 @@
+package jalview.ws2.actions.alignment;
+
+import static java.lang.String.format;
+
+import java.io.IOException;
+import java.util.ArrayList;
+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 = String.join("",
+          Collections.nCopies(width, Character.toString(gapChar)));
+      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;
+          }
+        }
+      }
+    }
+  }
+}