--- /dev/null
+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;
+ }
+}
--- /dev/null
+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;
+ }
+ }
+ }
+ }
+ }
+}