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.JvSwingUtils;
37 import jalview.gui.WebserviceInfo;
38 import jalview.gui.WsJobParameters;
39 import jalview.util.MathUtils;
40 import jalview.util.MessageManager;
41 import jalview.ws.params.ArgumentI;
42 import jalview.ws.params.WsParamSetI;
43 import jalview.ws2.MenuEntryProviderI;
44 import jalview.ws2.ResultSupplier;
45 import jalview.ws2.WSJob;
46 import jalview.ws2.WSJobStatus;
47 import jalview.ws2.WebServiceExecutor;
48 import jalview.ws2.WebServiceI;
49 import jalview.ws2.WebServiceInfoUpdater;
50 import jalview.ws2.WebServiceWorkerI;
51 import jalview.ws2.utils.WSJobList;
58 public class AlignmentOperation implements Operation
60 final WebServiceI service;
62 final ResultSupplier<AlignmentI> supplier;
64 public AlignmentOperation(WebServiceI service,
65 ResultSupplier<AlignmentI> supplier)
67 this.service = service;
68 this.supplier = supplier;
72 public String getTypeName()
74 return "Multiple Sequence Alignment";
78 public int getMinSequences()
84 public int getMaxSequences()
86 return Integer.MAX_VALUE;
90 public boolean isProteinOperation()
96 public boolean isNucleotideOperation()
102 public boolean canSubmitGaps()
104 // hack copied from original jabaws code, don't blame me
105 return service.getName().contains("lustal");
109 public MenuEntryProviderI getMenuBuilder()
111 return this::buildMenu;
114 protected void buildMenu(JMenu parent, AlignFrame frame)
118 var alignSubmenu = new JMenu(service.getName());
119 buildMenu(alignSubmenu, frame, false);
120 parent.add(alignSubmenu);
121 var realignSubmenu = new JMenu(MessageManager.formatMessage(
122 "label.realign_with_params", service.getName()));
123 realignSubmenu.setToolTipText(MessageManager
124 .getString("label.align_sequences_to_existing_alignment"));
125 buildMenu(realignSubmenu, frame, true);
126 parent.add(realignSubmenu);
130 buildMenu(parent, frame, false);
134 protected void buildMenu(JMenu parent, AlignFrame frame,
137 final String action = submitGaps ? "Align" : "Realign";
138 final var calcName = service.getName();
140 final AlignmentView msa = frame.gatherSequencesForAlignment();
141 final AlignViewport viewport = frame.getViewport();
142 final AlignmentI alignment = frame.getViewport().getAlignment();
143 String title = frame.getTitle();
144 WebServiceExecutor executor = frame.getViewport().getWSExecutor();
146 var item = new JMenuItem(MessageManager.formatMessage(
147 "label.calcname_with_default_settings", calcName));
148 item.setToolTipText(MessageManager
149 .formatMessage("label.action_with_default_settings", action));
150 item.addActionListener((event) -> {
153 WebServiceWorkerI worker = new AlignmentWorker(msa,
154 Collections.emptyList(), title, submitGaps, true,
155 alignment, viewport);
156 executor.submit(worker);
162 if (service.hasParameters())
164 var item = new JMenuItem(
165 MessageManager.getString("label.edit_settings_and_run"));
166 item.setToolTipText(MessageManager.getString(
167 "label.view_and_change_parameters_before_alignment"));
168 item.addActionListener((event) -> {
171 openEditParamsDialog(service, null, null)
172 .thenAcceptAsync((arguments) -> {
173 if (arguments != null)
175 WebServiceWorkerI worker = new AlignmentWorker(msa,
176 arguments, title, submitGaps, true, alignment,
178 executor.submit(worker);
186 var presets = service.getParamStore().getPresets();
187 if (presets != null && presets.size() > 0)
189 final var presetList = new JMenu(MessageManager
190 .formatMessage("label.run_with_preset_params", calcName));
191 final var showToolTipFor = ToolTipManager.sharedInstance()
193 for (final var preset : presets)
195 var item = new JMenuItem(preset.getName());
196 final int QUICK_TOOLTIP = 1500;
197 item.addMouseListener(new MouseAdapter()
200 public void mouseEntered(MouseEvent e)
202 ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
206 public void mouseExited(MouseEvent e)
208 ToolTipManager.sharedInstance().setDismissDelay(showToolTipFor);
211 String tooltip = JvSwingUtils.wrapTooltip(true,
212 format("<strong>%s</strong><br/>%s",
213 MessageManager.getString(
214 preset.isModifiable() ? "label.user_preset"
215 : "label.service_preset"),
216 preset.getDescription()));
217 item.setToolTipText(tooltip);
218 item.addActionListener((event) -> {
221 WebServiceWorkerI worker = new AlignmentWorker(msa,
222 preset.getArguments(), title, submitGaps, true,
223 alignment, viewport);
224 executor.submit(worker);
227 presetList.add(item);
229 parent.add(presetList);
233 private CompletionStage<List<ArgumentI>> openEditParamsDialog(
234 WebServiceI service, WsParamSetI preset,
235 List<ArgumentI> arguments)
237 WsJobParameters jobParams;
238 if (preset == null && arguments != null && arguments.size() > 0)
239 jobParams = new WsJobParameters(service.getParamStore(), preset,
242 jobParams = new WsJobParameters(service.getParamStore(), preset,
244 var stage = jobParams.showRunDialog();
245 return stage.thenApply((startJob) -> {
248 if (jobParams.getPreset() == null)
250 return jobParams.getJobParams();
254 return jobParams.getPreset().getArguments();
265 * Implementation of the web service worker performing multiple sequence
271 private class AlignmentWorker implements WebServiceWorkerI
274 private long uid = MathUtils.getUID();
276 private final AlignmentView msa;
278 private final AlignmentI dataset;
280 private final List<AlignedCodonFrame> codonFrame = new ArrayList<>();
282 private List<ArgumentI> args = Collections.emptyList();
284 private String alnTitle = "";
286 private boolean submitGaps = false;
288 private boolean preserveOrder = false;
290 private char gapCharacter;
292 private WSJobList jobs = new WSJobList();
294 private Map<Long, JobInput> inputs = new LinkedHashMap<>();
296 private WebserviceInfo wsInfo;
298 private Map<Long, Integer> exceptionCount = new HashMap<>();
300 private final int MAX_RETRY = 5;
302 AlignmentWorker(AlignmentView msa, List<ArgumentI> args,
303 String alnTitle, boolean submitGaps, boolean preserveOrder,
304 AlignmentI alignment, AlignViewport viewport)
307 this.dataset = alignment.getDataset();
308 List<AlignedCodonFrame> cf = Objects.requireNonNullElse(
309 alignment.getCodonFrames(), Collections.emptyList());
310 this.codonFrame.addAll(cf);
312 this.alnTitle = alnTitle;
313 this.submitGaps = submitGaps;
314 this.preserveOrder = preserveOrder;
315 this.gapCharacter = viewport.getGapCharacter();
317 String panelInfo = String.format("%s using service hosted at %s%n%s",
318 service.getName(), service.getHostName(),
319 Objects.requireNonNullElse(service.getDescription(), ""));
320 wsInfo = new WebserviceInfo(service.getName(), panelInfo, false);
330 public WebServiceI getWebService()
336 public List<WSJob> getJobs()
338 return Collections.unmodifiableList(jobs);
342 public void startJobs() throws IOException
344 String outputHeader = String.format("%s of %s%nJob details%n",
345 submitGaps ? "Re-alignment" : "Alignment", alnTitle);
346 SequenceI[][] conmsa = msa.getVisibleContigs('-');
351 WebServiceInfoUpdater updater = new WebServiceInfoUpdater(wsInfo);
352 updater.setOutputHeader(outputHeader);
354 for (int i = 0; i < conmsa.length; i++)
356 JobInput input = JobInput.create(conmsa[i], 2, submitGaps);
357 WSJob job = new WSJob(service.getProviderName(), service.getName(),
358 service.getHostName());
359 job.setJobNum(wsInfo.addJobPane());
360 if (conmsa.length > 0)
362 wsInfo.setProgressName(String.format("region %d", i),
365 wsInfo.setProgressText(job.getJobNum(), outputHeader);
366 job.addPropertyChangeListener(updater);
367 inputs.put(job.getUid(), input);
369 if (input.isInputValid())
375 count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY);
378 jobId = service.submit(input.inputSequences, args);
379 Cache.log.debug((format("Job %s submitted", job)));
380 exceptionCount.remove(job.getUid());
381 } catch (IOException e)
383 exceptionCount.put(job.getUid(), --count);
385 } while (jobId == null && count > 0);
389 job.setStatus(WSJobStatus.SUBMITTED);
394 job.setStatus(WSJobStatus.SERVER_ERROR);
399 job.setStatus(WSJobStatus.INVALID);
401 MessageManager.getString("label.empty_alignment_job"));
406 // wsInfo.setThisService() should happen here
407 wsInfo.setVisible(true);
411 wsInfo.setVisible(false);
412 // TODO show notification dialog.
413 // JvOptionPane.showMessageDialog(frame,
414 // MessageManager.getString("info.invalid_msa_input_mininfo"),
415 // MessageManager.getString("info.invalid_msa_notenough"),
416 // JvOptionPane.INFORMATION_MESSAGE);
421 public boolean pollJobs()
424 for (WSJob job : getJobs())
426 if (!job.getStatus().isDone())
428 Cache.log.debug(format("Polling job %s.", job));
431 service.updateProgress(job);
432 exceptionCount.remove(job.getUid());
433 } catch (IOException e)
435 Cache.log.error(format("Polling job %s failed.", job), e);
436 wsInfo.appendProgressText(job.getJobNum(),
437 MessageManager.formatMessage("info.server_exception",
438 service.getName(), e.getMessage()));
439 int count = exceptionCount.getOrDefault(job.getUid(),
443 job.setStatus(WSJobStatus.SERVER_ERROR);
444 Cache.log.warn(format(
445 "Attempts limit exceeded. Droping job %s.", job));
447 exceptionCount.put(job.getUid(), count);
448 } catch (OutOfMemoryError e)
450 job.setStatus(WSJobStatus.BROKEN);
452 format("Out of memory when retrieving job %s", job), e);
455 format("Job %s status is %s", job, job.getStatus()));
457 done &= job.getStatus().isDone();
459 updateWSInfoGlobalStatus();
463 private void updateWSInfoGlobalStatus()
465 if (jobs.countRunning() > 0)
467 wsInfo.setStatus(WebserviceInfo.STATE_RUNNING);
469 else if (jobs.countQueuing() > 0
470 || jobs.countSubmitted() < jobs.size())
472 wsInfo.setStatus(WebserviceInfo.STATE_QUEUING);
476 if (jobs.countSuccessful() > 0)
478 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_OK);
480 else if (jobs.countCancelled() > 0)
482 wsInfo.setStatus(WebserviceInfo.STATE_CANCELLED_OK);
484 else if (jobs.countFailed() > 0)
486 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
494 long progbarId = MathUtils.getUID();
495 wsInfo.setProgressBar(
496 MessageManager.getString("status.collecting_job_results"),
498 Map<Long, AlignmentI> results = new LinkedHashMap<>();
499 for (WSJob job : getJobs())
503 AlignmentI alignment = supplier.getResult(job);
504 if (alignment != null)
506 results.put(job.getUid(), alignment);
508 } catch (Exception e)
510 if (!service.handleCollectionError(job, e))
512 Cache.log.error("Couldn't get alignment for job.", e);
513 // TODO: Increment exception count and retry.
514 job.setStatus(WSJobStatus.SERVER_ERROR);
518 updateWSInfoGlobalStatus();
519 if (results.size() > 0)
521 wsInfo.showResultsNewFrame
522 .addActionListener(evt -> displayResults(results));
523 wsInfo.setResultsReady();
527 wsInfo.setFinishedNoResults();
529 wsInfo.removeProgressBar(progbarId);
532 private void displayResults(Map<Long, AlignmentI> alignments)
534 List<AlignmentOrder> alorders = new ArrayList<>();
535 SequenceI[][] results = new SequenceI[jobs.size()][];
536 AlignmentOrder[] orders = new AlignmentOrder[jobs.size()];
537 for (int i = 0; i < jobs.size(); i++)
539 WSJob job = jobs.get(i);
540 AlignmentI aln = alignments.get(job.getUid());
542 { // equivalent of job.hasResults()
543 /* Get the alignment including any empty sequences in the original
544 * order with original ids. */
545 JobInput input = inputs.get(job.getUid());
546 char gapChar = aln.getGapCharacter();
547 List<SequenceI> emptySeqs = input.emptySequences;
548 List<SequenceI> alnSeqs = aln.getSequences();
549 // find the width of the longest sequence
551 for (var seq : alnSeqs)
552 width = Integer.max(width, seq.getLength());
553 for (var emptySeq : emptySeqs)
554 width = Integer.max(width, emptySeq.getLength());
555 // pad shorter sequences with gaps
556 String gapSeq = String.join("",
557 Collections.nCopies(width, Character.toString(gapChar)));
558 List<SequenceI> seqs = new ArrayList<>(
559 alnSeqs.size() + emptySeqs.size());
560 seqs.addAll(alnSeqs);
561 seqs.addAll(emptySeqs);
564 if (seq.getLength() < width)
565 seq.setSequence(seq.getSequenceAsString()
566 + gapSeq.substring(seq.getLength()));
568 SequenceI[] result = seqs.toArray(new SequenceI[0]);
569 AlignmentOrder msaOrder = new AlignmentOrder(result);
570 AlignmentSorter.recoverOrder(result);
571 // temporary workaround for deuniquify
572 @SuppressWarnings({ "rawtypes", "unchecked" })
573 Hashtable names = new Hashtable(input.sequenceNames);
574 SeqsetUtils.deuniquify(names, result);
575 alorders.add(msaOrder);
577 orders[i] = msaOrder;
585 Object[] newView = msa.getUpdatedView(results, orders, gapCharacter);
586 // free references to original data
587 for (int i = 0; i < jobs.size(); i++)
592 SequenceI[] alignment = (SequenceI[]) newView[0];
593 HiddenColumns hidden = (HiddenColumns) newView[1];
594 Alignment aln = new Alignment(alignment);
595 aln.setProperty("Alignment Program", service.getName());
597 aln.setDataset(dataset);
599 propagateDatasetMappings(aln);
601 displayNewFrame(aln, alorders, hidden);
605 * conserves dataset references to sequence objects returned from web
606 * services. propagate codon frame data to alignment.
608 private void propagateDatasetMappings(Alignment aln)
610 if (codonFrame != null)
612 SequenceI[] alignment = aln.getSequencesArray();
613 for (SequenceI seq : alignment)
615 for (AlignedCodonFrame acf : codonFrame)
617 if (acf != null && acf.involvesSequence(seq))
619 aln.addCodonFrame(acf);
627 private void displayNewFrame(AlignmentI aln,
628 List<AlignmentOrder> alorders, HiddenColumns hidden)
630 AlignFrame frame = new AlignFrame(aln, hidden,
631 AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
632 // TODO store feature renderer settings in worker object
633 // frame.getFeatureRenderer().transferSettings(featureSettings);
634 var regions = sortOrders(alorders);
635 if (alorders.size() == 1)
637 frame.addSortByOrderMenuItem(
638 format("%s Ordering", service.getName()), alorders.get(0));
642 for (int i = 0; i < alorders.size(); i++)
645 Iterable<String> iter = () -> regions.get(j).stream()
646 .map(it -> Integer.toString(it)).iterator();
647 var orderName = format("%s Region %s Ordering", service.getName(),
648 String.join(",", iter));
649 frame.addSortByOrderMenuItem(orderName, alorders.get(i));
654 * If alignment was requested from one half of a SplitFrame, show in a
655 * SplitFrame with the other pane similarly aligned.
659 private List<List<Integer>> sortOrders(List<?> alorders)
661 List<List<Integer>> regions = new ArrayList<>();
662 for (int i = 0; i < alorders.size(); i++)
664 List<Integer> regs = new ArrayList<>();
667 while (j < alorders.size())
669 if (alorders.get(i).equals(alorders.get(j)))
685 private static class JobInput
687 final List<SequenceI> inputSequences;
689 final List<SequenceI> emptySequences;
691 @SuppressWarnings("rawtypes")
692 final Map<String, ? extends Map> sequenceNames;
694 private JobInput(int numSequences, List<SequenceI> inputSequences,
695 List<SequenceI> emptySequences,
696 @SuppressWarnings("rawtypes") Map<String, ? extends Map> names)
698 this.inputSequences = Collections.unmodifiableList(inputSequences);
699 this.emptySequences = Collections.unmodifiableList(emptySequences);
700 this.sequenceNames = names;
703 boolean isInputValid()
705 return inputSequences.size() >= 2;
708 static JobInput create(SequenceI[] sequences, int minLength,
711 assert minLength >= 0 : MessageManager.getString(
712 "error.implementation_error_minlen_must_be_greater_zero");
714 for (SequenceI seq : sequences)
716 if (seq.getEnd() - seq.getStart() >= minLength)
722 List<SequenceI> inputSequences = new ArrayList<>();
723 List<SequenceI> emptySequences = new ArrayList<>();
724 @SuppressWarnings("rawtypes")
725 Map<String, Hashtable> names = new LinkedHashMap<>();
726 for (int i = 0; i < sequences.length; i++)
728 SequenceI seq = sequences[i];
729 String newName = SeqsetUtils.unique_name(i);
730 @SuppressWarnings("rawtypes")
731 Hashtable hash = SeqsetUtils.SeqCharacterHash(seq);
732 names.put(newName, hash);
733 if (numSeq > 1 && seq.getEnd() - seq.getStart() >= minLength)
735 String seqString = seq.getSequenceAsString();
738 seqString = AlignSeq.extractGaps(
739 jalview.util.Comparison.GapChars, seqString);
741 inputSequences.add(new Sequence(newName, seqString));
745 String seqString = null;
746 if (seq.getEnd() >= seq.getStart()) // is it ever false?
748 seqString = seq.getSequenceAsString();
751 seqString = AlignSeq.extractGaps(
752 jalview.util.Comparison.GapChars, seqString);
755 emptySequences.add(new Sequence(newName, seqString));
759 return new JobInput(numSeq, inputSequences, emptySequences, names);