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;
66 public class AlignmentOperation implements Operation
68 private final WebServiceI service;
70 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);
180 * Implementation of the web service worker performing multiple sequence
186 public class AlignmentWorker implements WebServiceWorkerI
189 private long uid = MathUtils.getUID();
191 private final AlignmentView msa;
193 private final AlignmentI dataset;
195 private final AlignViewport viewport;
197 private final List<AlignedCodonFrame> codonFrame = new ArrayList<>();
199 private List<ArgumentI> args = Collections.emptyList();
201 private String alnTitle = "";
203 private boolean submitGaps = false;
205 private boolean preserveOrder = false;
207 private char gapCharacter;
209 private WSJobList jobs = new WSJobList();
211 private Map<Long, JobInput> inputs = new LinkedHashMap<>();
213 private Map<Long, Integer> exceptionCount = new HashMap<>();
215 private final int MAX_RETRY = 5;
217 public AlignmentWorker(AlignmentView msa, List<ArgumentI> args,
218 String alnTitle, boolean submitGaps, boolean preserveOrder,
219 AlignViewport viewport)
222 this.dataset = viewport.getAlignment().getDataset();
223 List<AlignedCodonFrame> cf = Objects.requireNonNullElse(
224 viewport.getAlignment().getCodonFrames(), Collections.emptyList());
225 this.codonFrame.addAll(cf);
227 this.alnTitle = alnTitle;
228 this.submitGaps = submitGaps;
229 this.preserveOrder = preserveOrder;
230 this.viewport = viewport;
231 this.gapCharacter = viewport.getGapCharacter();
241 public WebServiceI getWebService()
247 public WSJobList getJobs()
253 public void start() throws IOException
255 Cache.log.info(format("Starting new %s job.", service.getName()));
256 SequenceI[][] conmsa = msa.getVisibleContigs('-');
262 for (int i = 0; i < conmsa.length; i++)
264 JobInput input = JobInput.create(conmsa[i], 2, submitGaps);
265 WSJob job = new WSJob(service.getProviderName(), service.getName(),
266 service.getHostName());
268 inputs.put(job.getUid(), input);
270 listeners.fireJobCreated(job);
271 if (input.isInputValid())
277 count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY);
280 jobId = service.submit(input.inputSequences, args);
281 Cache.log.debug((format("Job %s submitted", job)));
282 exceptionCount.remove(job.getUid());
283 } catch (IOException e)
285 exceptionCount.put(job.getUid(), --count);
287 } while (jobId == null && count > 0);
291 job.setStatus(WSJobStatus.SUBMITTED);
296 job.setStatus(WSJobStatus.SERVER_ERROR);
301 job.setStatus(WSJobStatus.INVALID);
303 MessageManager.getString("label.empty_alignment_job"));
308 listeners.fireWorkerStarted();
312 listeners.fireWorkerNotStarted();
317 public boolean poll()
320 for (WSJob job : getJobs())
322 if (!job.getStatus().isDone() && !job.getStatus().isFailed())
324 Cache.log.debug(format("Polling job %s.", job));
327 service.updateProgress(job);
328 exceptionCount.remove(job.getUid());
329 } catch (IOException e)
331 Cache.log.error(format("Polling job %s failed.", job), e);
332 listeners.firePollException(job, e);
333 int count = exceptionCount.getOrDefault(job.getUid(),
337 job.setStatus(WSJobStatus.SERVER_ERROR);
338 Cache.log.warn(format(
339 "Attempts limit exceeded. Droping job %s.", job));
341 exceptionCount.put(job.getUid(), count);
342 } catch (OutOfMemoryError e)
344 job.setStatus(WSJobStatus.BROKEN);
346 format("Out of memory when retrieving job %s", job), e);
349 format("Job %s status is %s", job, job.getStatus()));
351 done &= job.getStatus().isDone() || job.getStatus().isFailed();
360 listeners.fireWorkerCompleting();
361 Map<Long, AlignmentI> results = new LinkedHashMap<>();
362 for (WSJob job : getJobs())
364 if (job.getStatus().isFailed())
368 AlignmentI alignment = supplier.getResult(job,
369 dataset.getSequences(), viewport);
370 if (alignment != null)
372 results.put(job.getUid(), alignment);
374 } catch (Exception e)
376 if (!service.handleCollectionError(job, e))
378 Cache.log.error("Couldn't get alignment for job.", e);
379 // TODO: Increment exception count and retry.
380 job.setStatus(WSJobStatus.SERVER_ERROR);
384 if (results.size() > 0)
386 AlignmentResult out = prepareResult(results);
387 resultConsumer.accept(out);
391 resultConsumer.accept(null);
393 listeners.fireWorkerCompleted();
396 private AlignmentResult prepareResult(Map<Long, AlignmentI> alignments)
398 List<AlignmentOrder> alorders = new ArrayList<>();
399 SequenceI[][] results = new SequenceI[jobs.size()][];
400 AlignmentOrder[] orders = new AlignmentOrder[jobs.size()];
401 for (int i = 0; i < jobs.size(); i++)
403 WSJob job = jobs.get(i);
404 AlignmentI aln = alignments.get(job.getUid());
405 if (aln != null) // equivalent of job.hasResults()
407 /* Get the alignment including any empty sequences in the original
408 * order with original ids. */
409 JobInput input = inputs.get(job.getUid());
410 char gapChar = aln.getGapCharacter();
411 List<SequenceI> emptySeqs = input.emptySequences;
412 List<SequenceI> alnSeqs = aln.getSequences();
413 // find the width of the longest sequence
415 for (var seq : alnSeqs)
416 width = Integer.max(width, seq.getLength());
417 for (var emptySeq : emptySeqs)
418 width = Integer.max(width, emptySeq.getLength());
419 // pad shorter sequences with gaps
420 String gapSeq = String.join("",
421 Collections.nCopies(width, Character.toString(gapChar)));
422 List<SequenceI> seqs = new ArrayList<>(
423 alnSeqs.size() + emptySeqs.size());
424 seqs.addAll(alnSeqs);
425 seqs.addAll(emptySeqs);
428 if (seq.getLength() < width)
429 seq.setSequence(seq.getSequenceAsString()
430 + gapSeq.substring(seq.getLength()));
432 SequenceI[] result = seqs.toArray(new SequenceI[0]);
433 AlignmentOrder msaOrder = new AlignmentOrder(result);
434 AlignmentSorter.recoverOrder(result);
435 // temporary workaround for deuniquify
436 @SuppressWarnings({ "rawtypes", "unchecked" })
437 Hashtable names = new Hashtable(input.sequenceNames);
438 // FIXME first call to deuniquify alters original alignment
439 SeqsetUtils.deuniquify(names, result);
440 alorders.add(msaOrder);
442 orders[i] = msaOrder;
450 Object[] newView = msa.getUpdatedView(results, orders, gapCharacter);
451 // free references to original data
452 for (int i = 0; i < jobs.size(); i++)
457 SequenceI[] alignment = (SequenceI[]) newView[0];
458 HiddenColumns hidden = (HiddenColumns) newView[1];
459 Alignment aln = new Alignment(alignment);
460 aln.setProperty("Alignment Program", service.getName());
462 aln.setDataset(dataset);
464 propagateDatasetMappings(aln);
465 return new AlignmentResult(aln, alorders, hidden);
466 // displayNewFrame(aln, alorders, hidden);
470 * conserves dataset references to sequence objects returned from web
471 * services. propagate codon frame data to alignment.
473 private void propagateDatasetMappings(Alignment aln)
475 if (codonFrame != null)
477 SequenceI[] alignment = aln.getSequencesArray();
478 for (SequenceI seq : alignment)
480 for (AlignedCodonFrame acf : codonFrame)
482 if (acf != null && acf.involvesSequence(seq))
484 aln.addCodonFrame(acf);
492 private Consumer<AlignmentResult> resultConsumer;
494 public void setResultConsumer(Consumer<AlignmentResult> consumer)
496 this.resultConsumer = consumer;
499 private WebServiceWorkerListenersList listeners =
500 new WebServiceWorkerListenersList(this);
503 public void addListener(WebServiceWorkerListener listener)
505 listeners.addListener(listener);
509 public class AlignmentResult
513 List<AlignmentOrder> alorders;
515 HiddenColumns hidden;
517 AlignmentResult(AlignmentI aln, List<AlignmentOrder> alorders,
518 HiddenColumns hidden)
521 this.alorders = alorders;
522 this.hidden = hidden;
525 public AlignmentI getAln()
530 public List<AlignmentOrder> getAlorders()
535 public HiddenColumns getHidden()
541 private static class JobInput
543 final List<SequenceI> inputSequences;
545 final List<SequenceI> emptySequences;
547 @SuppressWarnings("rawtypes")
548 final Map<String, ? extends Map> sequenceNames;
550 private JobInput(int numSequences, List<SequenceI> inputSequences,
551 List<SequenceI> emptySequences,
552 @SuppressWarnings("rawtypes") Map<String, ? extends Map> names)
554 this.inputSequences = Collections.unmodifiableList(inputSequences);
555 this.emptySequences = Collections.unmodifiableList(emptySequences);
556 this.sequenceNames = names;
559 boolean isInputValid()
561 return inputSequences.size() >= 2;
564 static JobInput create(SequenceI[] sequences, int minLength,
567 assert minLength >= 0 : MessageManager.getString(
568 "error.implementation_error_minlen_must_be_greater_zero");
570 for (SequenceI seq : sequences)
572 if (seq.getEnd() - seq.getStart() >= minLength)
578 List<SequenceI> inputSequences = new ArrayList<>();
579 List<SequenceI> emptySequences = new ArrayList<>();
580 @SuppressWarnings("rawtypes")
581 Map<String, Hashtable> names = new LinkedHashMap<>();
582 for (int i = 0; i < sequences.length; i++)
584 SequenceI seq = sequences[i];
585 String newName = SeqsetUtils.unique_name(i);
586 @SuppressWarnings("rawtypes")
587 Hashtable hash = SeqsetUtils.SeqCharacterHash(seq);
588 names.put(newName, hash);
589 if (numSeq > 1 && seq.getEnd() - seq.getStart() >= minLength)
591 String seqString = seq.getSequenceAsString();
594 seqString = AlignSeq.extractGaps(
595 jalview.util.Comparison.GapChars, seqString);
597 inputSequences.add(new Sequence(newName, seqString));
601 String seqString = "";
602 if (seq.getEnd() >= seq.getStart()) // true if gaps only
604 seqString = seq.getSequenceAsString();
607 seqString = AlignSeq.extractGaps(
608 jalview.util.Comparison.GapChars, seqString);
611 emptySequences.add(new Sequence(newName, seqString));
615 return new JobInput(numSeq, inputSequences, emptySequences, names);