1 package jalview.ws2.operations;
3 import static java.lang.String.format;
5 import java.awt.event.MouseAdapter;
6 import java.awt.event.MouseEvent;
7 import java.io.IOException;
8 import java.util.ArrayList;
9 import java.util.Collections;
10 import java.util.HashMap;
11 import java.util.Hashtable;
12 import java.util.LinkedHashMap;
13 import java.util.List;
15 import java.util.Objects;
16 import java.util.concurrent.CompletionStage;
17 import java.util.function.Consumer;
19 import javax.swing.JMenu;
20 import javax.swing.JMenuItem;
21 import javax.swing.ToolTipManager;
23 import org.eclipse.jetty.http.HttpGenerator.Result;
25 import jalview.analysis.AlignSeq;
26 import jalview.analysis.AlignmentSorter;
27 import jalview.analysis.SeqsetUtils;
28 import jalview.bin.Cache;
29 import jalview.datamodel.AlignedCodonFrame;
30 import jalview.datamodel.Alignment;
31 import jalview.datamodel.AlignmentI;
32 import jalview.datamodel.AlignmentOrder;
33 import jalview.datamodel.AlignmentView;
34 import jalview.datamodel.HiddenColumns;
35 import jalview.datamodel.SequenceI;
36 import jalview.datamodel.Sequence;
37 import jalview.gui.AlignFrame;
38 import jalview.gui.AlignViewport;
39 import jalview.gui.Desktop;
40 import jalview.gui.JvSwingUtils;
41 import jalview.gui.WebserviceInfo;
42 import jalview.gui.WsJobParameters;
43 import jalview.util.MathUtils;
44 import jalview.util.MessageManager;
45 import jalview.ws.params.ArgumentI;
46 import jalview.ws.params.ParamDatastoreI;
47 import jalview.ws.params.WsParamSetI;
48 import jalview.ws2.MenuEntryProviderI;
49 import jalview.ws2.ResultSupplier;
50 import jalview.ws2.WSJob;
51 import jalview.ws2.WSJobStatus;
52 import jalview.ws2.PollingTaskExecutor;
53 import jalview.ws2.WebServiceI;
54 import jalview.ws2.WebServiceInfoUpdater;
55 import jalview.ws2.WebServiceWorkerI;
56 import jalview.ws2.WebServiceWorkerListener;
57 import jalview.ws2.WebServiceWorkerListenersList;
58 import jalview.ws2.gui.AlignmentMenuBuilder;
59 import jalview.ws2.utils.WSJobList;
62 * Implementation of the {@link Operation} for multiple sequence alignment jobs.
67 public class AlignmentOperation implements Operation
69 private final WebServiceI service;
71 private final ResultSupplier<AlignmentI> supplier;
73 public AlignmentOperation(
75 ResultSupplier<AlignmentI> supplier)
77 this.service = service;
78 this.supplier = supplier;
82 public String getName()
84 return service.getName();
88 public String getDescription()
90 return service.getDescription();
94 public String getTypeName()
96 return "Multiple Sequence Alignment";
100 public String getHostName()
102 return service.getHostName();
106 public boolean hasParameters()
108 return service.hasParameters();
112 public ParamDatastoreI getParamStore()
114 return service.getParamStore();
118 public int getMinSequences()
124 public int getMaxSequences()
126 return Integer.MAX_VALUE;
130 public boolean isProteinOperation()
136 public boolean isNucleotideOperation()
142 public boolean isAlignmentAnalysis()
148 public boolean canSubmitGaps()
150 // hack copied from original jabaws code, don't blame me
151 return getName().contains("lustal");
155 public boolean isInteractive()
161 public boolean getFilterNonStandardSymbols()
167 public boolean getNeedsAlignedSequences()
173 public MenuEntryProviderI getMenuBuilder()
175 return new AlignmentMenuBuilder(this);
179 * Implementation of the web service worker performing multiple sequence
185 public class AlignmentWorker implements WebServiceWorkerI
188 private long uid = MathUtils.getUID();
190 private final AlignmentView msa;
192 private final AlignmentI dataset;
194 private final AlignViewport viewport;
196 private final List<AlignedCodonFrame> codonFrame = new ArrayList<>();
198 private List<ArgumentI> args = Collections.emptyList();
200 private String alnTitle = "";
202 private boolean submitGaps = false;
204 private boolean preserveOrder = false;
206 private char gapCharacter;
208 private WSJobList jobs = new WSJobList();
210 private Map<Long, JobInput> inputs = new LinkedHashMap<>();
212 private Map<Long, Integer> exceptionCount = new HashMap<>();
214 private final int MAX_RETRY = 5;
216 public AlignmentWorker(AlignmentView msa, List<ArgumentI> args,
217 String alnTitle, boolean submitGaps, boolean preserveOrder,
218 AlignViewport viewport)
221 this.dataset = viewport.getAlignment().getDataset();
222 List<AlignedCodonFrame> cf = Objects.requireNonNullElse(
223 viewport.getAlignment().getCodonFrames(), Collections.emptyList());
224 this.codonFrame.addAll(cf);
226 this.alnTitle = alnTitle;
227 this.submitGaps = submitGaps;
228 this.preserveOrder = preserveOrder;
229 this.viewport = viewport;
230 this.gapCharacter = viewport.getGapCharacter();
240 public WebServiceI getWebService()
246 public WSJobList getJobs()
252 public void start() throws IOException
254 Cache.log.info(format("Starting new %s job.", service.getName()));
255 SequenceI[][] conmsa = msa.getVisibleContigs('-');
261 for (int i = 0; i < conmsa.length; i++)
263 JobInput input = JobInput.create(conmsa[i], 2, submitGaps);
264 WSJob job = new WSJob(service.getProviderName(), service.getName(),
265 service.getHostName());
267 inputs.put(job.getUid(), input);
269 listeners.fireJobCreated(job);
270 if (input.isInputValid())
276 count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY);
279 jobId = service.submit(input.inputSequences, args);
280 Cache.log.debug((format("Job %s submitted", job)));
281 exceptionCount.remove(job.getUid());
282 } catch (IOException e)
284 exceptionCount.put(job.getUid(), --count);
286 } while (jobId == null && count > 0);
290 job.setStatus(WSJobStatus.SUBMITTED);
295 job.setStatus(WSJobStatus.SERVER_ERROR);
300 job.setStatus(WSJobStatus.INVALID);
302 MessageManager.getString("label.empty_alignment_job"));
307 listeners.fireWorkerStarted();
311 listeners.fireWorkerNotStarted();
316 public boolean poll()
319 for (WSJob job : getJobs())
321 if (!job.getStatus().isDone() && !job.getStatus().isFailed())
323 Cache.log.debug(format("Polling job %s.", job));
326 service.updateProgress(job);
327 exceptionCount.remove(job.getUid());
328 } catch (IOException e)
330 Cache.log.error(format("Polling job %s failed.", job), e);
331 listeners.firePollException(job, e);
332 int count = exceptionCount.getOrDefault(job.getUid(),
336 job.setStatus(WSJobStatus.SERVER_ERROR);
337 Cache.log.warn(format(
338 "Attempts limit exceeded. Droping job %s.", job));
340 exceptionCount.put(job.getUid(), count);
341 } catch (OutOfMemoryError e)
343 job.setStatus(WSJobStatus.BROKEN);
345 format("Out of memory when retrieving job %s", job), e);
348 format("Job %s status is %s", job, job.getStatus()));
350 done &= job.getStatus().isDone() || job.getStatus().isFailed();
358 listeners.fireWorkerCompleting();
359 Map<Long, AlignmentI> results = new LinkedHashMap<>();
360 for (WSJob job : getJobs())
362 if (job.getStatus().isFailed())
366 AlignmentI alignment = supplier.getResult(job,
367 dataset.getSequences(), viewport);
368 if (alignment != null)
370 results.put(job.getUid(), alignment);
372 } catch (Exception e)
374 if (!service.handleCollectionError(job, e))
376 Cache.log.error("Couldn't get alignment for job.", e);
377 // TODO: Increment exception count and retry.
378 job.setStatus(WSJobStatus.SERVER_ERROR);
382 if (results.size() > 0)
384 AlignmentResult out = prepareResult(results);
385 resultConsumer.accept(out);
389 resultConsumer.accept(null);
391 listeners.fireWorkerCompleted();
394 private AlignmentResult prepareResult(Map<Long, AlignmentI> alignments)
396 List<AlignmentOrder> alorders = new ArrayList<>();
397 SequenceI[][] results = new SequenceI[jobs.size()][];
398 AlignmentOrder[] orders = new AlignmentOrder[jobs.size()];
399 for (int i = 0; i < jobs.size(); i++)
401 WSJob job = jobs.get(i);
402 AlignmentI aln = alignments.get(job.getUid());
403 if (aln != null) // equivalent of job.hasResults()
405 /* Get the alignment including any empty sequences in the original
406 * order with original ids. */
407 JobInput input = inputs.get(job.getUid());
408 char gapChar = aln.getGapCharacter();
409 List<SequenceI> emptySeqs = input.emptySequences;
410 List<SequenceI> alnSeqs = aln.getSequences();
411 // find the width of the longest sequence
413 for (var seq : alnSeqs)
414 width = Integer.max(width, seq.getLength());
415 for (var emptySeq : emptySeqs)
416 width = Integer.max(width, emptySeq.getLength());
417 // pad shorter sequences with gaps
418 String gapSeq = String.join("",
419 Collections.nCopies(width, Character.toString(gapChar)));
420 List<SequenceI> seqs = new ArrayList<>(
421 alnSeqs.size() + emptySeqs.size());
422 seqs.addAll(alnSeqs);
423 seqs.addAll(emptySeqs);
426 if (seq.getLength() < width)
427 seq.setSequence(seq.getSequenceAsString()
428 + gapSeq.substring(seq.getLength()));
430 SequenceI[] result = seqs.toArray(new SequenceI[0]);
431 AlignmentOrder msaOrder = new AlignmentOrder(result);
432 AlignmentSorter.recoverOrder(result);
433 // temporary workaround for deuniquify
434 @SuppressWarnings({ "rawtypes", "unchecked" })
435 Hashtable names = new Hashtable(input.sequenceNames);
436 // FIXME first call to deuniquify alters original alignment
437 SeqsetUtils.deuniquify(names, result);
438 alorders.add(msaOrder);
440 orders[i] = msaOrder;
448 Object[] newView = msa.getUpdatedView(results, orders, gapCharacter);
449 // free references to original data
450 for (int i = 0; i < jobs.size(); i++)
455 SequenceI[] alignment = (SequenceI[]) newView[0];
456 HiddenColumns hidden = (HiddenColumns) newView[1];
457 Alignment aln = new Alignment(alignment);
458 aln.setProperty("Alignment Program", service.getName());
460 aln.setDataset(dataset);
462 propagateDatasetMappings(aln);
463 return new AlignmentResult(aln, alorders, hidden);
464 // displayNewFrame(aln, alorders, hidden);
468 * conserves dataset references to sequence objects returned from web
469 * services. propagate codon frame data to alignment.
471 private void propagateDatasetMappings(Alignment aln)
473 if (codonFrame != null)
475 SequenceI[] alignment = aln.getSequencesArray();
476 for (SequenceI seq : alignment)
478 for (AlignedCodonFrame acf : codonFrame)
480 if (acf != null && acf.involvesSequence(seq))
482 aln.addCodonFrame(acf);
490 private Consumer<AlignmentResult> resultConsumer;
492 public void setResultConsumer(Consumer<AlignmentResult> consumer)
494 this.resultConsumer = consumer;
497 private WebServiceWorkerListenersList listeners = new WebServiceWorkerListenersList(this);
500 public void addListener(WebServiceWorkerListener listener)
502 listeners.addListener(listener);
506 public class AlignmentResult
510 List<AlignmentOrder> alorders;
512 HiddenColumns hidden;
514 AlignmentResult(AlignmentI aln, List<AlignmentOrder> alorders,
515 HiddenColumns hidden)
518 this.alorders = alorders;
519 this.hidden = hidden;
522 public AlignmentI getAln()
527 public List<AlignmentOrder> getAlorders()
532 public HiddenColumns getHidden()
538 private static class JobInput
540 final List<SequenceI> inputSequences;
542 final List<SequenceI> emptySequences;
544 @SuppressWarnings("rawtypes")
545 final Map<String, ? extends Map> sequenceNames;
547 private JobInput(int numSequences, List<SequenceI> inputSequences,
548 List<SequenceI> emptySequences,
549 @SuppressWarnings("rawtypes") Map<String, ? extends Map> names)
551 this.inputSequences = Collections.unmodifiableList(inputSequences);
552 this.emptySequences = Collections.unmodifiableList(emptySequences);
553 this.sequenceNames = names;
556 boolean isInputValid()
558 return inputSequences.size() >= 2;
561 static JobInput create(SequenceI[] sequences, int minLength,
564 assert minLength >= 0 : MessageManager.getString(
565 "error.implementation_error_minlen_must_be_greater_zero");
567 for (SequenceI seq : sequences)
569 if (seq.getEnd() - seq.getStart() >= minLength)
575 List<SequenceI> inputSequences = new ArrayList<>();
576 List<SequenceI> emptySequences = new ArrayList<>();
577 @SuppressWarnings("rawtypes")
578 Map<String, Hashtable> names = new LinkedHashMap<>();
579 for (int i = 0; i < sequences.length; i++)
581 SequenceI seq = sequences[i];
582 String newName = SeqsetUtils.unique_name(i);
583 @SuppressWarnings("rawtypes")
584 Hashtable hash = SeqsetUtils.SeqCharacterHash(seq);
585 names.put(newName, hash);
586 if (numSeq > 1 && seq.getEnd() - seq.getStart() >= minLength)
588 String seqString = seq.getSequenceAsString();
591 seqString = AlignSeq.extractGaps(
592 jalview.util.Comparison.GapChars, seqString);
594 inputSequences.add(new Sequence(newName, seqString));
598 String seqString = "";
599 if (seq.getEnd() >= seq.getStart()) // true if gaps only
601 seqString = seq.getSequenceAsString();
604 seqString = AlignSeq.extractGaps(
605 jalview.util.Comparison.GapChars, seqString);
608 emptySequences.add(new Sequence(newName, seqString));
612 return new JobInput(numSeq, inputSequences, emptySequences, names);