1 package jalview.ws2.operations;
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.Collections;
6 import java.util.HashMap;
7 import java.util.LinkedHashMap;
10 import java.util.Objects;
11 import java.util.function.Consumer;
13 import jalview.analysis.AlignSeq;
14 import jalview.analysis.AlignmentSorter;
15 import jalview.analysis.SeqsetUtils;
16 import jalview.analysis.SeqsetUtils.SequenceInfo;
17 import jalview.api.AlignViewportI;
18 import jalview.bin.Cache;
19 import jalview.datamodel.AlignedCodonFrame;
20 import jalview.datamodel.Alignment;
21 import jalview.datamodel.AlignmentI;
22 import jalview.datamodel.AlignmentOrder;
23 import jalview.datamodel.AlignmentView;
24 import jalview.datamodel.HiddenColumns;
25 import jalview.datamodel.Sequence;
26 import jalview.datamodel.SequenceI;
27 import jalview.gui.AlignViewport;
28 import jalview.util.MessageManager;
29 import jalview.ws.params.ArgumentI;
30 import jalview.ws2.WSJob;
31 import jalview.ws2.WSJobStatus;
33 public class AlignmentWorker extends AbstractPollableWorker
35 private AlignmentOperation operation;
37 private Consumer<AlignmentResult> resultConsumer;
39 private AlignmentView msa;
41 private AlignmentI dataset;
43 private AlignViewportI viewport;
45 private List<AlignedCodonFrame> codonFrame = new ArrayList<>();
47 private List<ArgumentI> args = Collections.emptyList();
49 private boolean submitGaps = false;
51 private boolean preserveOrder = false;
53 private char gapCharacter;
55 private WSJobList<AlignmentJob> jobs = new WSJobList<>();
57 private Map<Long, Integer> exceptionCount = new HashMap<>();
59 private static final int MAX_RETRY = 5;
61 private static class JobInput
63 final List<SequenceI> inputSequences;
65 final List<SequenceI> emptySequences;
67 final Map<String, SequenceInfo> sequenceNames;
69 private JobInput(List<SequenceI> inputSequences,
70 List<SequenceI> emptySequences,
71 Map<String, SequenceInfo> names)
73 this.inputSequences = Collections.unmodifiableList(inputSequences);
74 this.emptySequences = Collections.unmodifiableList(emptySequences);
75 this.sequenceNames = names;
78 boolean isInputValid()
80 return inputSequences.size() >= 2;
84 public class AlignmentJob extends WSJob
86 private List<SequenceI> inputSequences;
87 private List<SequenceI> emptySequences;
88 private Map<String, SequenceInfo> sequenceNames;
90 private AlignmentJob(String serviceProvider, String serviceName,
93 super(serviceProvider, serviceName, hostName);
96 private void setInput(JobInput input) {
97 inputSequences = input.inputSequences;
98 emptySequences = input.emptySequences;
99 sequenceNames = input.sequenceNames;
103 public class AlignmentResult
107 List<AlignmentOrder> alorders;
109 HiddenColumns hidden;
111 AlignmentResult(AlignmentI aln, List<AlignmentOrder> alorders,
112 HiddenColumns hidden)
115 this.alorders = alorders;
116 this.hidden = hidden;
119 public AlignmentI getAlignment()
124 public List<AlignmentOrder> getAlignmentOrders()
129 public HiddenColumns getHiddenColumns()
135 public AlignmentWorker(AlignmentOperation operation,
136 AlignmentView msa, List<ArgumentI> args,
137 boolean submitGaps, boolean preserveOrder, AlignViewportI viewport)
139 this.operation = operation;
141 this.dataset = viewport.getAlignment().getDataset();
142 List<AlignedCodonFrame> cf = Objects.requireNonNullElse(
143 viewport.getAlignment().getCodonFrames(), Collections.emptyList());
144 this.codonFrame.addAll(cf);
146 this.submitGaps = submitGaps;
147 this.preserveOrder = preserveOrder;
148 this.viewport = viewport;
149 this.gapCharacter = viewport.getGapCharacter();
153 public Operation getOperation()
159 public WSJobList<? extends WSJob> getJobs()
164 public void setResultConsumer(Consumer<AlignmentResult> consumer)
166 this.resultConsumer = consumer;
170 public void start() throws IOException
172 Cache.log.info(String.format("Starting new %s job.", operation.getName()));
173 SequenceI[][] conmsa = msa.getVisibleContigs('-');
179 for (int i = 0; i < conmsa.length; i++)
181 JobInput input = prepareInputData(conmsa[i], 2, submitGaps);
182 AlignmentJob job = new AlignmentJob(operation.service.getProviderName(),
183 operation.getName(), operation.getHostName());
187 listeners.fireJobCreated(job);
188 if (input.isInputValid())
194 count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY);
197 jobId = operation.service.submit(input.inputSequences, args);
198 Cache.log.debug((String.format("Job %s submitted", job)));
199 exceptionCount.remove(job.getUid());
200 } catch (IOException e)
202 exceptionCount.put(job.getUid(), --count);
204 } while (jobId == null && count > 0);
208 job.setStatus(WSJobStatus.SUBMITTED);
213 job.setStatus(WSJobStatus.SERVER_ERROR);
218 job.setStatus(WSJobStatus.INVALID);
220 MessageManager.getString("label.empty_alignment_job"));
225 listeners.fireWorkerStarted();
229 listeners.fireWorkerNotStarted();
234 private static JobInput prepareInputData(SequenceI[] sequences,
235 int minLength, boolean submitGaps)
237 assert minLength >= 0 : MessageManager.getString(
238 "error.implementation_error_minlen_must_be_greater_zero");
240 for (SequenceI seq : sequences)
242 if (seq.getEnd() - seq.getStart() >= minLength)
248 List<SequenceI> inputSequences = new ArrayList<>();
249 List<SequenceI> emptySequences = new ArrayList<>();
250 Map<String, SequenceInfo> names = new LinkedHashMap<>();
251 for (int i = 0; i < sequences.length; i++)
253 SequenceI seq = sequences[i];
254 String newName = SeqsetUtils.unique_name(i);
255 var hash = SeqsetUtils.SeqCharacterHash(seq);
256 names.put(newName, hash);
257 if (numSeq > 1 && seq.getEnd() - seq.getStart() >= minLength)
259 String seqString = seq.getSequenceAsString();
262 seqString = AlignSeq.extractGaps(
263 jalview.util.Comparison.GapChars, seqString);
265 inputSequences.add(new Sequence(newName, seqString));
269 String seqString = "";
270 if (seq.getEnd() >= seq.getStart()) // true if gaps only
272 seqString = seq.getSequenceAsString();
275 seqString = AlignSeq.extractGaps(
276 jalview.util.Comparison.GapChars, seqString);
279 emptySequences.add(new Sequence(newName, seqString));
283 return new JobInput(inputSequences, emptySequences, names);
289 listeners.fireWorkerCompleting();
290 Map<Long, AlignmentI> results = new LinkedHashMap<>();
291 for (WSJob job : getJobs())
293 if (job.getStatus().isFailed())
297 AlignmentI alignment = operation.getAlignmentSupplier().getAlignment(job);
298 if (alignment != null)
300 results.put(job.getUid(), alignment);
302 } catch (Exception e)
304 if (!operation.getWebService().handleCollectionError(job, e))
306 Cache.log.error("Couldn't get alignment for job.", e);
307 // TODO: Increment exception count and retry.
308 job.setStatus(WSJobStatus.SERVER_ERROR);
312 if (results.size() > 0)
314 AlignmentResult out = prepareResult(results);
315 resultConsumer.accept(out);
319 resultConsumer.accept(null);
321 listeners.fireWorkerCompleted();
324 private AlignmentResult prepareResult(Map<Long, AlignmentI> alignments)
326 List<AlignmentOrder> alorders = new ArrayList<>();
327 SequenceI[][] results = new SequenceI[jobs.size()][];
328 AlignmentOrder[] orders = new AlignmentOrder[jobs.size()];
329 for (int i = 0; i < jobs.size(); i++)
331 AlignmentJob job = jobs.get(i);
332 AlignmentI aln = alignments.get(job.getUid());
333 if (aln != null) // equivalent of job.hasResults()
335 /* Get the alignment including any empty sequences in the original
336 * order with original ids. */
337 char gapChar = aln.getGapCharacter();
338 List<SequenceI> emptySeqs = job.emptySequences;
339 List<SequenceI> alnSeqs = aln.getSequences();
340 // find the width of the longest sequence
342 for (var seq : alnSeqs)
343 width = Integer.max(width, seq.getLength());
344 for (var emptySeq : emptySeqs)
345 width = Integer.max(width, emptySeq.getLength());
346 // pad shorter sequences with gaps
347 String gapSeq = String.join("",
348 Collections.nCopies(width, Character.toString(gapChar)));
349 List<SequenceI> seqs = new ArrayList<>(
350 alnSeqs.size() + emptySeqs.size());
351 seqs.addAll(alnSeqs);
352 seqs.addAll(emptySeqs);
355 if (seq.getLength() < width)
356 seq.setSequence(seq.getSequenceAsString()
357 + gapSeq.substring(seq.getLength()));
359 SequenceI[] result = seqs.toArray(new SequenceI[0]);
360 AlignmentOrder msaOrder = new AlignmentOrder(result);
361 AlignmentSorter.recoverOrder(result);
362 Map<String, SequenceInfo> names = new HashMap<>(job.sequenceNames);
363 // FIXME first call to deuniquify alters original alignment
364 SeqsetUtils.deuniquify(names, result);
365 alorders.add(msaOrder);
367 orders[i] = msaOrder;
375 Object[] newView = msa.getUpdatedView(results, orders, gapCharacter);
376 // free references to original data
377 for (int i = 0; i < jobs.size(); i++)
382 SequenceI[] alignment = (SequenceI[]) newView[0];
383 HiddenColumns hidden = (HiddenColumns) newView[1];
384 Alignment aln = new Alignment(alignment);
385 aln.setProperty("Alignment Program", operation.getName());
387 aln.setDataset(dataset);
389 propagateDatasetMappings(aln);
390 return new AlignmentResult(aln, alorders, hidden);
391 // displayNewFrame(aln, alorders, hidden);
395 * conserves dataset references to sequence objects returned from web
396 * services. propagate codon frame data to alignment.
398 private void propagateDatasetMappings(Alignment aln)
400 if (codonFrame != null)
402 SequenceI[] alignment = aln.getSequencesArray();
403 for (SequenceI seq : alignment)
405 for (AlignedCodonFrame acf : codonFrame)
407 if (acf != null && acf.involvesSequence(seq))
409 aln.addCodonFrame(acf);