From c85714a15361197d5d4f841f8f8b2e0fd20746f1 Mon Sep 17 00:00:00 2001 From: Mateusz Warowny Date: Thu, 3 Mar 2022 14:57:55 +0100 Subject: [PATCH] JAL-3878 Create alignment service "backend" classes JAL-3878 Implement AlignmentAction#perform method --- .../ws2/actions/alignment/AlignmentAction.java | 10 +- .../ws2/actions/alignment/AlignmentJob.java | 114 ++++++++++ .../ws2/actions/alignment/AlignmentResult.java | 42 ++++ .../ws2/actions/alignment/AlignmentTask.java | 219 ++++++++++++++++++++ 4 files changed, 382 insertions(+), 3 deletions(-) create mode 100644 src/jalview/ws2/actions/alignment/AlignmentJob.java create mode 100644 src/jalview/ws2/actions/alignment/AlignmentTask.java diff --git a/src/jalview/ws2/actions/alignment/AlignmentAction.java b/src/jalview/ws2/actions/alignment/AlignmentAction.java index 8c0cd93..d8f5a1d 100644 --- a/src/jalview/ws2/actions/alignment/AlignmentAction.java +++ b/src/jalview/ws2/actions/alignment/AlignmentAction.java @@ -72,12 +72,16 @@ public class AlignmentAction extends BaseAction List args, Credentials credentials, TaskEventListener 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 index 0000000..5ddb64a --- /dev/null +++ b/src/jalview/ws2/actions/alignment/AlignmentJob.java @@ -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 emptySeqs; + + private final Map names; + + private AlignmentI alignmentResult; + + AlignmentJob(List inputSeqs, List emptySeqs, + Map 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 names = new LinkedHashMap<>(); + List inputSeqs = new ArrayList<>(); + List 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 getEmptySequences() + { + return emptySeqs; + } + + Map getNames() + { + return names; + } + + boolean hasResult() + { + return alignmentResult != null; + } + + AlignmentI getAlignmentResult() + { + return alignmentResult; + } + + void setAlignmentResult(AlignmentI alignment) + { + this.alignmentResult = alignment; + } +} diff --git a/src/jalview/ws2/actions/alignment/AlignmentResult.java b/src/jalview/ws2/actions/alignment/AlignmentResult.java index d3ed8fc..9090dd2 100644 --- a/src/jalview/ws2/actions/alignment/AlignmentResult.java +++ b/src/jalview/ws2/actions/alignment/AlignmentResult.java @@ -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 alorders; + + final HiddenColumns hidden; + + AlignmentResult(AlignmentI aln, List alorders, + HiddenColumns hidden) + { + this.aln = aln; + this.alorders = alorders; + this.hidden = hidden; + } + + public AlignmentI getAlignment() + { + return aln; + } + + public List 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 index 0000000..214cd02 --- /dev/null +++ b/src/jalview/ws2/actions/alignment/AlignmentTask.java @@ -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 +{ + /* 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 codonFrame = new ArrayList<>(); + + AlignmentTask(AlignmentWebServiceClientI client, AlignmentAction action, + List args, Credentials credentials, + AlignmentView msa, AlignViewportI viewport, boolean submitGaps, + TaskEventListener 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 cf = viewport.getAlignment().getCodonFrames(); + if (cf != null) + this.codonFrame.addAll(cf); + } + + @Override + protected List 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 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 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 emptySeqs = job.getEmptySequences(); + List 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 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; + } + } + } + } + } +} -- 1.7.10.2