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.WebServiceExecutor;
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 int getMinSequences()
91 public int getMaxSequences()
93 return Integer.MAX_VALUE;
97 public boolean isProteinOperation()
103 public boolean isNucleotideOperation()
109 public boolean canSubmitGaps()
111 // hack copied from original jabaws code, don't blame me
112 return service.getName().contains("lustal");
116 public boolean isInteractive()
122 public MenuEntryProviderI getMenuBuilder()
124 return this::buildMenu;
127 protected void buildMenu(JMenu parent, AlignFrame frame)
131 var alignSubmenu = new JMenu(service.getName());
132 buildMenu(alignSubmenu, frame, false);
133 parent.add(alignSubmenu);
134 var realignSubmenu = new JMenu(MessageManager.formatMessage(
135 "label.realign_with_params", service.getName()));
136 realignSubmenu.setToolTipText(MessageManager
137 .getString("label.align_sequences_to_existing_alignment"));
138 buildMenu(realignSubmenu, frame, true);
139 parent.add(realignSubmenu);
143 buildMenu(parent, frame, false);
147 protected void buildMenu(JMenu parent, AlignFrame frame,
150 final String action = submitGaps ? "Align" : "Realign";
151 final var calcName = service.getName();
153 final AlignmentView msa = frame.gatherSequencesForAlignment();
154 final AlignViewport viewport = frame.getViewport();
155 final AlignmentI alignment = frame.getViewport().getAlignment();
156 String title = frame.getTitle();
157 WebServiceExecutor executor = frame.getViewport().getWSExecutor();
159 var item = new JMenuItem(MessageManager.formatMessage(
160 "label.calcname_with_default_settings", calcName));
161 item.setToolTipText(MessageManager
162 .formatMessage("label.action_with_default_settings", action));
163 item.addActionListener((event) -> {
166 WebServiceWorkerI worker = new AlignmentWorker(msa,
167 Collections.emptyList(), title, submitGaps, true,
168 alignment, viewport);
169 executor.submit(worker);
175 if (service.hasParameters())
177 var item = new JMenuItem(
178 MessageManager.getString("label.edit_settings_and_run"));
179 item.setToolTipText(MessageManager.getString(
180 "label.view_and_change_parameters_before_alignment"));
181 item.addActionListener((event) -> {
184 openEditParamsDialog(service, null, null)
185 .thenAcceptAsync((arguments) -> {
186 if (arguments != null)
188 WebServiceWorkerI worker = new AlignmentWorker(msa,
189 arguments, title, submitGaps, true, alignment,
191 executor.submit(worker);
199 var presets = service.getParamStore().getPresets();
200 if (presets != null && presets.size() > 0)
202 final var presetList = new JMenu(MessageManager
203 .formatMessage("label.run_with_preset_params", calcName));
204 final var showToolTipFor = ToolTipManager.sharedInstance()
206 for (final var preset : presets)
208 var item = new JMenuItem(preset.getName());
209 final int QUICK_TOOLTIP = 1500;
210 item.addMouseListener(new MouseAdapter()
213 public void mouseEntered(MouseEvent e)
215 ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
219 public void mouseExited(MouseEvent e)
221 ToolTipManager.sharedInstance().setDismissDelay(showToolTipFor);
224 String tooltip = JvSwingUtils.wrapTooltip(true,
225 format("<strong>%s</strong><br/>%s",
226 MessageManager.getString(
227 preset.isModifiable() ? "label.user_preset"
228 : "label.service_preset"),
229 preset.getDescription()));
230 item.setToolTipText(tooltip);
231 item.addActionListener((event) -> {
234 WebServiceWorkerI worker = new AlignmentWorker(msa,
235 preset.getArguments(), title, submitGaps, true,
236 alignment, viewport);
237 executor.submit(worker);
240 presetList.add(item);
242 parent.add(presetList);
246 private CompletionStage<List<ArgumentI>> openEditParamsDialog(
247 WebServiceI service, WsParamSetI preset,
248 List<ArgumentI> arguments)
250 WsJobParameters jobParams;
251 if (preset == null && arguments != null && arguments.size() > 0)
252 jobParams = new WsJobParameters(service.getParamStore(), preset,
255 jobParams = new WsJobParameters(service.getParamStore(), preset,
257 var stage = jobParams.showRunDialog();
258 return stage.thenApply((startJob) -> {
261 if (jobParams.getPreset() == null)
263 return jobParams.getJobParams();
267 return jobParams.getPreset().getArguments();
278 * Implementation of the web service worker performing multiple sequence
284 private class AlignmentWorker implements WebServiceWorkerI
287 private long uid = MathUtils.getUID();
289 private final AlignmentView msa;
291 private final AlignmentI dataset;
293 private final List<AlignedCodonFrame> codonFrame = new ArrayList<>();
295 private List<ArgumentI> args = Collections.emptyList();
297 private String alnTitle = "";
299 private boolean submitGaps = false;
301 private boolean preserveOrder = false;
303 private char gapCharacter;
305 private WSJobList jobs = new WSJobList();
307 private Map<Long, JobInput> inputs = new LinkedHashMap<>();
309 private WebserviceInfo wsInfo;
311 private Map<Long, Integer> exceptionCount = new HashMap<>();
313 private final int MAX_RETRY = 5;
315 AlignmentWorker(AlignmentView msa, List<ArgumentI> args,
316 String alnTitle, boolean submitGaps, boolean preserveOrder,
317 AlignmentI alignment, AlignViewport viewport)
320 this.dataset = alignment.getDataset();
321 List<AlignedCodonFrame> cf = Objects.requireNonNullElse(
322 alignment.getCodonFrames(), Collections.emptyList());
323 this.codonFrame.addAll(cf);
325 this.alnTitle = alnTitle;
326 this.submitGaps = submitGaps;
327 this.preserveOrder = preserveOrder;
328 this.gapCharacter = viewport.getGapCharacter();
330 String panelInfo = String.format("%s using service hosted at %s%n%s",
331 service.getName(), service.getHostName(),
332 Objects.requireNonNullElse(service.getDescription(), ""));
333 wsInfo = new WebserviceInfo(service.getName(), panelInfo, false);
343 public WebServiceI getWebService()
349 public List<WSJob> getJobs()
351 return Collections.unmodifiableList(jobs);
355 public void startJobs() throws IOException
357 String outputHeader = String.format("%s of %s%nJob details%n",
358 submitGaps ? "Re-alignment" : "Alignment", alnTitle);
359 SequenceI[][] conmsa = msa.getVisibleContigs('-');
364 WebServiceInfoUpdater updater = new WebServiceInfoUpdater(wsInfo);
365 updater.setOutputHeader(outputHeader);
367 for (int i = 0; i < conmsa.length; i++)
369 JobInput input = JobInput.create(conmsa[i], 2, submitGaps);
370 WSJob job = new WSJob(service.getProviderName(), service.getName(),
371 service.getHostName());
372 job.setJobNum(wsInfo.addJobPane());
373 if (conmsa.length > 0)
375 wsInfo.setProgressName(String.format("region %d", i),
378 wsInfo.setProgressText(job.getJobNum(), outputHeader);
379 job.addPropertyChangeListener(updater);
380 inputs.put(job.getUid(), input);
382 if (input.isInputValid())
388 count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY);
391 jobId = service.submit(input.inputSequences, args);
392 Cache.log.debug((format("Job %s submitted", job)));
393 exceptionCount.remove(job.getUid());
394 } catch (IOException e)
396 exceptionCount.put(job.getUid(), --count);
398 } while (jobId == null && count > 0);
402 job.setStatus(WSJobStatus.SUBMITTED);
407 job.setStatus(WSJobStatus.SERVER_ERROR);
412 job.setStatus(WSJobStatus.INVALID);
414 MessageManager.getString("label.empty_alignment_job"));
419 // wsInfo.setThisService() should happen here
420 wsInfo.setVisible(true);
424 wsInfo.setVisible(false);
425 // TODO show notification dialog.
426 // JvOptionPane.showMessageDialog(frame,
427 // MessageManager.getString("info.invalid_msa_input_mininfo"),
428 // MessageManager.getString("info.invalid_msa_notenough"),
429 // JvOptionPane.INFORMATION_MESSAGE);
434 public boolean pollJobs()
437 for (WSJob job : getJobs())
439 if (!job.getStatus().isDone())
441 Cache.log.debug(format("Polling job %s.", job));
444 service.updateProgress(job);
445 exceptionCount.remove(job.getUid());
446 } catch (IOException e)
448 Cache.log.error(format("Polling job %s failed.", job), e);
449 wsInfo.appendProgressText(job.getJobNum(),
450 MessageManager.formatMessage("info.server_exception",
451 service.getName(), e.getMessage()));
452 int count = exceptionCount.getOrDefault(job.getUid(),
456 job.setStatus(WSJobStatus.SERVER_ERROR);
457 Cache.log.warn(format(
458 "Attempts limit exceeded. Droping job %s.", job));
460 exceptionCount.put(job.getUid(), count);
461 } catch (OutOfMemoryError e)
463 job.setStatus(WSJobStatus.BROKEN);
465 format("Out of memory when retrieving job %s", job), e);
468 format("Job %s status is %s", job, job.getStatus()));
470 done &= job.getStatus().isDone();
472 updateWSInfoGlobalStatus();
476 private void updateWSInfoGlobalStatus()
478 if (jobs.countRunning() > 0)
480 wsInfo.setStatus(WebserviceInfo.STATE_RUNNING);
482 else if (jobs.countQueuing() > 0
483 || jobs.countSubmitted() < jobs.size())
485 wsInfo.setStatus(WebserviceInfo.STATE_QUEUING);
489 if (jobs.countSuccessful() > 0)
491 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_OK);
493 else if (jobs.countCancelled() > 0)
495 wsInfo.setStatus(WebserviceInfo.STATE_CANCELLED_OK);
497 else if (jobs.countFailed() > 0)
499 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
507 long progbarId = MathUtils.getUID();
508 wsInfo.setProgressBar(
509 MessageManager.getString("status.collecting_job_results"),
511 Map<Long, AlignmentI> results = new LinkedHashMap<>();
512 for (WSJob job : getJobs())
516 AlignmentI alignment = supplier.getResult(job);
517 if (alignment != null)
519 results.put(job.getUid(), alignment);
521 } catch (Exception e)
523 if (!service.handleCollectionError(job, e))
525 Cache.log.error("Couldn't get alignment for job.", e);
526 // TODO: Increment exception count and retry.
527 job.setStatus(WSJobStatus.SERVER_ERROR);
531 updateWSInfoGlobalStatus();
532 if (results.size() > 0)
534 OutputWrapper out = prepareOutput(results);
535 wsInfo.showResultsNewFrame.addActionListener(evt -> displayNewFrame(
536 new Alignment(out.aln), out.alorders, out.hidden));
537 wsInfo.setResultsReady();
541 wsInfo.setFinishedNoResults();
543 wsInfo.removeProgressBar(progbarId);
546 private class OutputWrapper
550 List<AlignmentOrder> alorders;
552 HiddenColumns hidden;
554 OutputWrapper(AlignmentI aln, List<AlignmentOrder> alorders,
555 HiddenColumns hidden)
558 this.alorders = alorders;
559 this.hidden = hidden;
563 private OutputWrapper prepareOutput(Map<Long, AlignmentI> alignments)
565 List<AlignmentOrder> alorders = new ArrayList<>();
566 SequenceI[][] results = new SequenceI[jobs.size()][];
567 AlignmentOrder[] orders = new AlignmentOrder[jobs.size()];
568 for (int i = 0; i < jobs.size(); i++)
570 WSJob job = jobs.get(i);
571 AlignmentI aln = alignments.get(job.getUid());
572 if (aln != null) // equivalent of job.hasResults()
574 /* Get the alignment including any empty sequences in the original
575 * order with original ids. */
576 JobInput input = inputs.get(job.getUid());
577 char gapChar = aln.getGapCharacter();
578 List<SequenceI> emptySeqs = input.emptySequences;
579 List<SequenceI> alnSeqs = aln.getSequences();
580 // find the width of the longest sequence
582 for (var seq : alnSeqs)
583 width = Integer.max(width, seq.getLength());
584 for (var emptySeq : emptySeqs)
585 width = Integer.max(width, emptySeq.getLength());
586 // pad shorter sequences with gaps
587 String gapSeq = String.join("",
588 Collections.nCopies(width, Character.toString(gapChar)));
589 List<SequenceI> seqs = new ArrayList<>(
590 alnSeqs.size() + emptySeqs.size());
591 seqs.addAll(alnSeqs);
592 seqs.addAll(emptySeqs);
595 if (seq.getLength() < width)
596 seq.setSequence(seq.getSequenceAsString()
597 + gapSeq.substring(seq.getLength()));
599 SequenceI[] result = seqs.toArray(new SequenceI[0]);
600 AlignmentOrder msaOrder = new AlignmentOrder(result);
601 AlignmentSorter.recoverOrder(result);
602 // temporary workaround for deuniquify
603 @SuppressWarnings({ "rawtypes", "unchecked" })
604 Hashtable names = new Hashtable(input.sequenceNames);
605 // FIXME first call to deuniquify alters original alignment
606 SeqsetUtils.deuniquify(names, result);
607 alorders.add(msaOrder);
609 orders[i] = msaOrder;
617 Object[] newView = msa.getUpdatedView(results, orders, gapCharacter);
618 // free references to original data
619 for (int i = 0; i < jobs.size(); i++)
624 SequenceI[] alignment = (SequenceI[]) newView[0];
625 HiddenColumns hidden = (HiddenColumns) newView[1];
626 Alignment aln = new Alignment(alignment);
627 aln.setProperty("Alignment Program", service.getName());
629 aln.setDataset(dataset);
631 propagateDatasetMappings(aln);
632 return new OutputWrapper(aln, alorders, hidden);
633 // displayNewFrame(aln, alorders, hidden);
637 * conserves dataset references to sequence objects returned from web
638 * services. propagate codon frame data to alignment.
640 private void propagateDatasetMappings(Alignment aln)
642 if (codonFrame != null)
644 SequenceI[] alignment = aln.getSequencesArray();
645 for (SequenceI seq : alignment)
647 for (AlignedCodonFrame acf : codonFrame)
649 if (acf != null && acf.involvesSequence(seq))
651 aln.addCodonFrame(acf);
659 private void displayNewFrame(AlignmentI aln,
660 List<AlignmentOrder> alorders, HiddenColumns hidden)
662 AlignFrame frame = new AlignFrame(aln, hidden,
663 AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
664 // TODO store feature renderer settings in worker object
665 // frame.getFeatureRenderer().transferSettings(featureSettings);
666 var regions = sortOrders(alorders);
667 if (alorders.size() == 1)
669 frame.addSortByOrderMenuItem(
670 format("%s Ordering", service.getName()), alorders.get(0));
674 for (int i = 0; i < alorders.size(); i++)
677 Iterable<String> iter = () -> regions.get(j).stream()
678 .map(it -> Integer.toString(it)).iterator();
679 var orderName = format("%s Region %s Ordering", service.getName(),
680 String.join(",", iter));
681 frame.addSortByOrderMenuItem(orderName, alorders.get(i));
686 * If alignment was requested from one half of a SplitFrame, show in a
687 * SplitFrame with the other pane similarly aligned.
690 Desktop.addInternalFrame(frame, alnTitle, AlignFrame.DEFAULT_WIDTH,
691 AlignFrame.DEFAULT_HEIGHT);
694 private List<List<Integer>> sortOrders(List<?> alorders)
696 List<List<Integer>> regions = new ArrayList<>();
697 for (int i = 0; i < alorders.size(); i++)
699 List<Integer> regs = new ArrayList<>();
702 while (j < alorders.size())
704 if (alorders.get(i).equals(alorders.get(j)))
720 private static class JobInput
722 final List<SequenceI> inputSequences;
724 final List<SequenceI> emptySequences;
726 @SuppressWarnings("rawtypes")
727 final Map<String, ? extends Map> sequenceNames;
729 private JobInput(int numSequences, List<SequenceI> inputSequences,
730 List<SequenceI> emptySequences,
731 @SuppressWarnings("rawtypes") Map<String, ? extends Map> names)
733 this.inputSequences = Collections.unmodifiableList(inputSequences);
734 this.emptySequences = Collections.unmodifiableList(emptySequences);
735 this.sequenceNames = names;
738 boolean isInputValid()
740 return inputSequences.size() >= 2;
743 static JobInput create(SequenceI[] sequences, int minLength,
746 assert minLength >= 0 : MessageManager.getString(
747 "error.implementation_error_minlen_must_be_greater_zero");
749 for (SequenceI seq : sequences)
751 if (seq.getEnd() - seq.getStart() >= minLength)
757 List<SequenceI> inputSequences = new ArrayList<>();
758 List<SequenceI> emptySequences = new ArrayList<>();
759 @SuppressWarnings("rawtypes")
760 Map<String, Hashtable> names = new LinkedHashMap<>();
761 for (int i = 0; i < sequences.length; i++)
763 SequenceI seq = sequences[i];
764 String newName = SeqsetUtils.unique_name(i);
765 @SuppressWarnings("rawtypes")
766 Hashtable hash = SeqsetUtils.SeqCharacterHash(seq);
767 names.put(newName, hash);
768 if (numSeq > 1 && seq.getEnd() - seq.getStart() >= minLength)
770 String seqString = seq.getSequenceAsString();
773 seqString = AlignSeq.extractGaps(
774 jalview.util.Comparison.GapChars, seqString);
776 inputSequences.add(new Sequence(newName, seqString));
780 String seqString = null;
781 if (seq.getEnd() >= seq.getStart()) // is it ever false?
783 seqString = seq.getSequenceAsString();
786 seqString = AlignSeq.extractGaps(
787 jalview.util.Comparison.GapChars, seqString);
790 emptySequences.add(new Sequence(newName, seqString));
794 return new JobInput(numSeq, inputSequences, emptySequences, names);