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;
18 import javax.swing.JMenu;
19 import javax.swing.JMenuItem;
20 import javax.swing.ToolTipManager;
22 import jalview.analysis.AlignSeq;
23 import jalview.analysis.AlignmentSorter;
24 import jalview.analysis.SeqsetUtils;
25 import jalview.bin.Cache;
26 import jalview.datamodel.AlignedCodonFrame;
27 import jalview.datamodel.Alignment;
28 import jalview.datamodel.AlignmentI;
29 import jalview.datamodel.AlignmentOrder;
30 import jalview.datamodel.AlignmentView;
31 import jalview.datamodel.HiddenColumns;
32 import jalview.datamodel.SequenceI;
33 import jalview.datamodel.Sequence;
34 import jalview.gui.AlignFrame;
35 import jalview.gui.AlignViewport;
36 import jalview.gui.Desktop;
37 import jalview.gui.JvSwingUtils;
38 import jalview.gui.WebserviceInfo;
39 import jalview.gui.WsJobParameters;
40 import jalview.util.MathUtils;
41 import jalview.util.MessageManager;
42 import jalview.ws.params.ArgumentI;
43 import jalview.ws.params.WsParamSetI;
44 import jalview.ws2.MenuEntryProviderI;
45 import jalview.ws2.ResultSupplier;
46 import jalview.ws2.WSJob;
47 import jalview.ws2.WSJobStatus;
48 import jalview.ws2.PollingTaskExecutor;
49 import jalview.ws2.WebServiceI;
50 import jalview.ws2.WebServiceInfoUpdater;
51 import jalview.ws2.WebServiceWorkerI;
52 import jalview.ws2.utils.WSJobList;
59 public class AlignmentOperation implements Operation
61 final WebServiceI service;
63 final ResultSupplier<AlignmentI> supplier;
65 public AlignmentOperation(WebServiceI service,
66 ResultSupplier<AlignmentI> supplier)
68 this.service = service;
69 this.supplier = supplier;
73 public String getName()
75 return service.getName();
79 public String getTypeName()
81 return "Multiple Sequence Alignment";
85 public String getHostName()
87 return service.getHostName();
91 public int getMinSequences()
97 public int getMaxSequences()
99 return Integer.MAX_VALUE;
103 public boolean isProteinOperation()
109 public boolean isNucleotideOperation()
115 public boolean isAlignmentAnalysis()
121 public boolean canSubmitGaps()
123 // hack copied from original jabaws code, don't blame me
124 return service.getName().contains("lustal");
128 public boolean isInteractive()
134 public boolean getFilterNonStandardSymbols()
140 public boolean getNeedsAlignedSequences()
146 public MenuEntryProviderI getMenuBuilder()
148 return this::buildMenu;
151 protected void buildMenu(JMenu parent, AlignFrame frame)
155 var alignSubmenu = new JMenu(service.getName());
156 buildMenu(alignSubmenu, frame, false);
157 parent.add(alignSubmenu);
158 var realignSubmenu = new JMenu(MessageManager.formatMessage(
159 "label.realign_with_params", service.getName()));
160 realignSubmenu.setToolTipText(MessageManager
161 .getString("label.align_sequences_to_existing_alignment"));
162 buildMenu(realignSubmenu, frame, true);
163 parent.add(realignSubmenu);
167 buildMenu(parent, frame, false);
171 protected void buildMenu(JMenu parent, AlignFrame frame,
174 final String action = submitGaps ? "Align" : "Realign";
175 final var calcName = service.getName();
177 String title = frame.getTitle();
178 PollingTaskExecutor executor = frame.getViewport().getWSExecutor();
180 var item = new JMenuItem(MessageManager.formatMessage(
181 "label.calcname_with_default_settings", calcName));
182 item.setToolTipText(MessageManager
183 .formatMessage("label.action_with_default_settings", action));
184 item.addActionListener((event) -> {
185 final AlignmentView msa = frame.gatherSequencesForAlignment();
186 final AlignViewport viewport = frame.getViewport();
187 final AlignmentI alignment = frame.getViewport().getAlignment();
190 WebServiceWorkerI worker = new AlignmentWorker(msa,
191 Collections.emptyList(), title, submitGaps, true,
192 alignment, viewport);
193 executor.submit(worker);
199 if (service.hasParameters())
201 var item = new JMenuItem(
202 MessageManager.getString("label.edit_settings_and_run"));
203 item.setToolTipText(MessageManager.getString(
204 "label.view_and_change_parameters_before_alignment"));
205 item.addActionListener((event) -> {
206 final AlignmentView msa = frame.gatherSequencesForAlignment();
207 final AlignViewport viewport = frame.getViewport();
208 final AlignmentI alignment = frame.getViewport().getAlignment();
211 openEditParamsDialog(service, null, null)
212 .thenAcceptAsync((arguments) -> {
213 if (arguments != null)
215 WebServiceWorkerI worker = new AlignmentWorker(msa,
216 arguments, title, submitGaps, true, alignment,
218 executor.submit(worker);
226 var presets = service.getParamStore().getPresets();
227 if (presets != null && presets.size() > 0)
229 final var presetList = new JMenu(MessageManager
230 .formatMessage("label.run_with_preset_params", calcName));
231 final var showToolTipFor = ToolTipManager.sharedInstance()
233 for (final var preset : presets)
235 var item = new JMenuItem(preset.getName());
236 final int QUICK_TOOLTIP = 1500;
237 item.addMouseListener(new MouseAdapter()
240 public void mouseEntered(MouseEvent e)
242 ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
246 public void mouseExited(MouseEvent e)
248 ToolTipManager.sharedInstance().setDismissDelay(showToolTipFor);
251 String tooltip = JvSwingUtils.wrapTooltip(true,
252 format("<strong>%s</strong><br/>%s",
253 MessageManager.getString(
254 preset.isModifiable() ? "label.user_preset"
255 : "label.service_preset"),
256 preset.getDescription()));
257 item.setToolTipText(tooltip);
258 item.addActionListener((event) -> {
259 final AlignmentView msa = frame.gatherSequencesForAlignment();
260 final AlignViewport viewport = frame.getViewport();
261 final AlignmentI alignment = frame.getViewport().getAlignment();
264 WebServiceWorkerI worker = new AlignmentWorker(msa,
265 preset.getArguments(), title, submitGaps, true,
266 alignment, viewport);
267 executor.submit(worker);
270 presetList.add(item);
272 parent.add(presetList);
276 private CompletionStage<List<ArgumentI>> openEditParamsDialog(
277 WebServiceI service, WsParamSetI preset,
278 List<ArgumentI> arguments)
280 WsJobParameters jobParams;
281 if (preset == null && arguments != null && arguments.size() > 0)
282 jobParams = new WsJobParameters(service.getParamStore(), preset,
285 jobParams = new WsJobParameters(service.getParamStore(), preset,
287 var stage = jobParams.showRunDialog();
288 return stage.thenApply((startJob) -> {
291 if (jobParams.getPreset() == null)
293 return jobParams.getJobParams();
297 return jobParams.getPreset().getArguments();
308 * Implementation of the web service worker performing multiple sequence
314 private class AlignmentWorker implements WebServiceWorkerI
317 private long uid = MathUtils.getUID();
319 private final AlignmentView msa;
321 private final AlignmentI dataset;
323 private final AlignViewport viewport;
325 private final List<AlignedCodonFrame> codonFrame = new ArrayList<>();
327 private List<ArgumentI> args = Collections.emptyList();
329 private String alnTitle = "";
331 private boolean submitGaps = false;
333 private boolean preserveOrder = false;
335 private char gapCharacter;
337 private WSJobList jobs = new WSJobList();
339 private Map<Long, JobInput> inputs = new LinkedHashMap<>();
341 private WebserviceInfo wsInfo;
343 private Map<Long, Integer> exceptionCount = new HashMap<>();
345 private final int MAX_RETRY = 5;
347 AlignmentWorker(AlignmentView msa, List<ArgumentI> args,
348 String alnTitle, boolean submitGaps, boolean preserveOrder,
349 AlignmentI alignment, AlignViewport viewport)
352 this.dataset = alignment.getDataset();
353 List<AlignedCodonFrame> cf = Objects.requireNonNullElse(
354 alignment.getCodonFrames(), Collections.emptyList());
355 this.codonFrame.addAll(cf);
357 this.alnTitle = alnTitle;
358 this.submitGaps = submitGaps;
359 this.preserveOrder = preserveOrder;
360 this.viewport = viewport;
361 this.gapCharacter = viewport.getGapCharacter();
363 String panelInfo = String.format("%s using service hosted at %s%n%s",
364 service.getName(), service.getHostName(),
365 Objects.requireNonNullElse(service.getDescription(), ""));
366 wsInfo = new WebserviceInfo(service.getName(), panelInfo, false);
376 public WebServiceI getWebService()
382 public List<WSJob> getJobs()
384 return Collections.unmodifiableList(jobs);
388 public void start() throws IOException
390 Cache.log.info(format("Starting new %s job.", service.getName()));
391 String outputHeader = String.format("%s of %s%nJob details%n",
392 submitGaps ? "Re-alignment" : "Alignment", alnTitle);
393 SequenceI[][] conmsa = msa.getVisibleContigs('-');
398 WebServiceInfoUpdater updater = new WebServiceInfoUpdater(wsInfo);
399 updater.setOutputHeader(outputHeader);
401 for (int i = 0; i < conmsa.length; i++)
403 JobInput input = JobInput.create(conmsa[i], 2, submitGaps);
404 WSJob job = new WSJob(service.getProviderName(), service.getName(),
405 service.getHostName());
406 job.setJobNum(wsInfo.addJobPane());
407 if (conmsa.length > 1)
409 wsInfo.setProgressName(String.format("region %d", i),
412 wsInfo.setProgressText(job.getJobNum(), outputHeader);
413 job.addPropertyChangeListener(updater);
414 inputs.put(job.getUid(), input);
416 if (input.isInputValid())
422 count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY);
425 jobId = service.submit(input.inputSequences, args);
426 Cache.log.debug((format("Job %s submitted", job)));
427 exceptionCount.remove(job.getUid());
428 } catch (IOException e)
430 exceptionCount.put(job.getUid(), --count);
432 } while (jobId == null && count > 0);
436 job.setStatus(WSJobStatus.SUBMITTED);
441 job.setStatus(WSJobStatus.SERVER_ERROR);
446 job.setStatus(WSJobStatus.INVALID);
448 MessageManager.getString("label.empty_alignment_job"));
453 // wsInfo.setThisService() should happen here
454 wsInfo.setVisible(true);
458 wsInfo.setVisible(false);
459 // TODO show notification dialog.
460 // JvOptionPane.showMessageDialog(frame,
461 // MessageManager.getString("info.invalid_msa_input_mininfo"),
462 // MessageManager.getString("info.invalid_msa_notenough"),
463 // JvOptionPane.INFORMATION_MESSAGE);
468 public boolean poll()
471 for (WSJob job : getJobs())
473 if (!job.getStatus().isDone() && !job.getStatus().isFailed())
475 Cache.log.debug(format("Polling job %s.", job));
478 service.updateProgress(job);
479 exceptionCount.remove(job.getUid());
480 } catch (IOException e)
482 Cache.log.error(format("Polling job %s failed.", job), e);
483 wsInfo.appendProgressText(job.getJobNum(),
484 MessageManager.formatMessage("info.server_exception",
485 service.getName(), e.getMessage()));
486 int count = exceptionCount.getOrDefault(job.getUid(),
490 job.setStatus(WSJobStatus.SERVER_ERROR);
491 Cache.log.warn(format(
492 "Attempts limit exceeded. Droping job %s.", job));
494 exceptionCount.put(job.getUid(), count);
495 } catch (OutOfMemoryError e)
497 job.setStatus(WSJobStatus.BROKEN);
499 format("Out of memory when retrieving job %s", job), e);
502 format("Job %s status is %s", job, job.getStatus()));
504 done &= job.getStatus().isDone() || job.getStatus().isFailed();
506 updateWSInfoGlobalStatus();
510 private void updateWSInfoGlobalStatus()
512 if (jobs.countRunning() > 0)
514 wsInfo.setStatus(WebserviceInfo.STATE_RUNNING);
516 else if (jobs.countQueuing() > 0
517 || jobs.countSubmitted() < jobs.size())
519 wsInfo.setStatus(WebserviceInfo.STATE_QUEUING);
523 if (jobs.countSuccessful() > 0)
525 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_OK);
527 else if (jobs.countCancelled() > 0)
529 wsInfo.setStatus(WebserviceInfo.STATE_CANCELLED_OK);
531 else if (jobs.countFailed() > 0)
533 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
541 long progbarId = MathUtils.getUID();
542 wsInfo.setProgressBar(
543 MessageManager.getString("status.collecting_job_results"),
545 Map<Long, AlignmentI> results = new LinkedHashMap<>();
546 for (WSJob job : getJobs())
548 if (job.getStatus().isFailed())
552 AlignmentI alignment = supplier.getResult(job, dataset.getSequences(), viewport);
553 if (alignment != null)
555 results.put(job.getUid(), alignment);
557 } catch (Exception e)
559 if (!service.handleCollectionError(job, e))
561 Cache.log.error("Couldn't get alignment for job.", e);
562 // TODO: Increment exception count and retry.
563 job.setStatus(WSJobStatus.SERVER_ERROR);
567 updateWSInfoGlobalStatus();
568 if (results.size() > 0)
570 OutputWrapper out = prepareOutput(results);
571 wsInfo.showResultsNewFrame.addActionListener(evt -> displayNewFrame(
572 new Alignment(out.aln), out.alorders, out.hidden));
573 wsInfo.setResultsReady();
577 wsInfo.setFinishedNoResults();
579 wsInfo.removeProgressBar(progbarId);
582 private class OutputWrapper
586 List<AlignmentOrder> alorders;
588 HiddenColumns hidden;
590 OutputWrapper(AlignmentI aln, List<AlignmentOrder> alorders,
591 HiddenColumns hidden)
594 this.alorders = alorders;
595 this.hidden = hidden;
599 private OutputWrapper prepareOutput(Map<Long, AlignmentI> alignments)
601 List<AlignmentOrder> alorders = new ArrayList<>();
602 SequenceI[][] results = new SequenceI[jobs.size()][];
603 AlignmentOrder[] orders = new AlignmentOrder[jobs.size()];
604 for (int i = 0; i < jobs.size(); i++)
606 WSJob job = jobs.get(i);
607 AlignmentI aln = alignments.get(job.getUid());
608 if (aln != null) // equivalent of job.hasResults()
610 /* Get the alignment including any empty sequences in the original
611 * order with original ids. */
612 JobInput input = inputs.get(job.getUid());
613 char gapChar = aln.getGapCharacter();
614 List<SequenceI> emptySeqs = input.emptySequences;
615 List<SequenceI> alnSeqs = aln.getSequences();
616 // find the width of the longest sequence
618 for (var seq : alnSeqs)
619 width = Integer.max(width, seq.getLength());
620 for (var emptySeq : emptySeqs)
621 width = Integer.max(width, emptySeq.getLength());
622 // pad shorter sequences with gaps
623 String gapSeq = String.join("",
624 Collections.nCopies(width, Character.toString(gapChar)));
625 List<SequenceI> seqs = new ArrayList<>(
626 alnSeqs.size() + emptySeqs.size());
627 seqs.addAll(alnSeqs);
628 seqs.addAll(emptySeqs);
631 if (seq.getLength() < width)
632 seq.setSequence(seq.getSequenceAsString()
633 + gapSeq.substring(seq.getLength()));
635 SequenceI[] result = seqs.toArray(new SequenceI[0]);
636 AlignmentOrder msaOrder = new AlignmentOrder(result);
637 AlignmentSorter.recoverOrder(result);
638 // temporary workaround for deuniquify
639 @SuppressWarnings({ "rawtypes", "unchecked" })
640 Hashtable names = new Hashtable(input.sequenceNames);
641 // FIXME first call to deuniquify alters original alignment
642 SeqsetUtils.deuniquify(names, result);
643 alorders.add(msaOrder);
645 orders[i] = msaOrder;
653 Object[] newView = msa.getUpdatedView(results, orders, gapCharacter);
654 // free references to original data
655 for (int i = 0; i < jobs.size(); i++)
660 SequenceI[] alignment = (SequenceI[]) newView[0];
661 HiddenColumns hidden = (HiddenColumns) newView[1];
662 Alignment aln = new Alignment(alignment);
663 aln.setProperty("Alignment Program", service.getName());
665 aln.setDataset(dataset);
667 propagateDatasetMappings(aln);
668 return new OutputWrapper(aln, alorders, hidden);
669 // displayNewFrame(aln, alorders, hidden);
673 * conserves dataset references to sequence objects returned from web
674 * services. propagate codon frame data to alignment.
676 private void propagateDatasetMappings(Alignment aln)
678 if (codonFrame != null)
680 SequenceI[] alignment = aln.getSequencesArray();
681 for (SequenceI seq : alignment)
683 for (AlignedCodonFrame acf : codonFrame)
685 if (acf != null && acf.involvesSequence(seq))
687 aln.addCodonFrame(acf);
695 private void displayNewFrame(AlignmentI aln,
696 List<AlignmentOrder> alorders, HiddenColumns hidden)
698 AlignFrame frame = new AlignFrame(aln, hidden,
699 AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
700 // TODO store feature renderer settings in worker object
701 // frame.getFeatureRenderer().transferSettings(featureSettings);
702 var regions = sortOrders(alorders);
703 if (alorders.size() == 1)
705 frame.addSortByOrderMenuItem(
706 format("%s Ordering", service.getName()), alorders.get(0));
710 for (int i = 0; i < alorders.size(); i++)
713 Iterable<String> iter = () -> regions.get(j).stream()
714 .map(it -> Integer.toString(it)).iterator();
715 var orderName = format("%s Region %s Ordering", service.getName(),
716 String.join(",", iter));
717 frame.addSortByOrderMenuItem(orderName, alorders.get(i));
722 * If alignment was requested from one half of a SplitFrame, show in a
723 * SplitFrame with the other pane similarly aligned.
726 Desktop.addInternalFrame(frame, alnTitle, AlignFrame.DEFAULT_WIDTH,
727 AlignFrame.DEFAULT_HEIGHT);
730 private List<List<Integer>> sortOrders(List<?> alorders)
732 List<List<Integer>> regions = new ArrayList<>();
733 for (int i = 0; i < alorders.size(); i++)
735 List<Integer> regs = new ArrayList<>();
738 while (j < alorders.size())
740 if (alorders.get(i).equals(alorders.get(j)))
756 private static class JobInput
758 final List<SequenceI> inputSequences;
760 final List<SequenceI> emptySequences;
762 @SuppressWarnings("rawtypes")
763 final Map<String, ? extends Map> sequenceNames;
765 private JobInput(int numSequences, List<SequenceI> inputSequences,
766 List<SequenceI> emptySequences,
767 @SuppressWarnings("rawtypes") Map<String, ? extends Map> names)
769 this.inputSequences = Collections.unmodifiableList(inputSequences);
770 this.emptySequences = Collections.unmodifiableList(emptySequences);
771 this.sequenceNames = names;
774 boolean isInputValid()
776 return inputSequences.size() >= 2;
779 static JobInput create(SequenceI[] sequences, int minLength,
782 assert minLength >= 0 : MessageManager.getString(
783 "error.implementation_error_minlen_must_be_greater_zero");
785 for (SequenceI seq : sequences)
787 if (seq.getEnd() - seq.getStart() >= minLength)
793 List<SequenceI> inputSequences = new ArrayList<>();
794 List<SequenceI> emptySequences = new ArrayList<>();
795 @SuppressWarnings("rawtypes")
796 Map<String, Hashtable> names = new LinkedHashMap<>();
797 for (int i = 0; i < sequences.length; i++)
799 SequenceI seq = sequences[i];
800 String newName = SeqsetUtils.unique_name(i);
801 @SuppressWarnings("rawtypes")
802 Hashtable hash = SeqsetUtils.SeqCharacterHash(seq);
803 names.put(newName, hash);
804 if (numSeq > 1 && seq.getEnd() - seq.getStart() >= minLength)
806 String seqString = seq.getSequenceAsString();
809 seqString = AlignSeq.extractGaps(
810 jalview.util.Comparison.GapChars, seqString);
812 inputSequences.add(new Sequence(newName, seqString));
816 String seqString = "";
817 if (seq.getEnd() >= seq.getStart()) // true if gaps only
819 seqString = seq.getSequenceAsString();
822 seqString = AlignSeq.extractGaps(
823 jalview.util.Comparison.GapChars, seqString);
826 emptySequences.add(new Sequence(newName, seqString));
830 return new JobInput(numSeq, inputSequences, emptySequences, names);