package jalview.ws2.operations; import static java.lang.String.format; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Hashtable; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletionStage; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.ToolTipManager; import jalview.analysis.AlignSeq; import jalview.analysis.AlignmentSorter; import jalview.analysis.SeqsetUtils; import jalview.bin.Cache; import jalview.datamodel.AlignedCodonFrame; import jalview.datamodel.Alignment; import jalview.datamodel.AlignmentI; import jalview.datamodel.AlignmentOrder; import jalview.datamodel.AlignmentView; import jalview.datamodel.HiddenColumns; import jalview.datamodel.SequenceI; import jalview.datamodel.Sequence; import jalview.gui.AlignFrame; import jalview.gui.AlignViewport; import jalview.gui.Desktop; import jalview.gui.JvSwingUtils; import jalview.gui.WebserviceInfo; import jalview.gui.WsJobParameters; import jalview.util.MathUtils; import jalview.util.MessageManager; import jalview.ws.params.ArgumentI; import jalview.ws.params.WsParamSetI; import jalview.ws2.MenuEntryProviderI; import jalview.ws2.ResultSupplier; import jalview.ws2.WSJob; import jalview.ws2.WSJobStatus; import jalview.ws2.PollingTaskExecutor; import jalview.ws2.WebServiceI; import jalview.ws2.WebServiceInfoUpdater; import jalview.ws2.WebServiceWorkerI; import jalview.ws2.utils.WSJobList; /** * * @author mmwarowny * */ public class AlignmentOperation implements Operation { final WebServiceI service; final ResultSupplier supplier; public AlignmentOperation(WebServiceI service, ResultSupplier supplier) { this.service = service; this.supplier = supplier; } @Override public String getName() { return service.getName(); } @Override public String getTypeName() { return "Multiple Sequence Alignment"; } @Override public String getHostName() { return service.getHostName(); } @Override public int getMinSequences() { return 2; } @Override public int getMaxSequences() { return Integer.MAX_VALUE; } @Override public boolean isProteinOperation() { return true; } @Override public boolean isNucleotideOperation() { return true; } @Override public boolean canSubmitGaps() { // hack copied from original jabaws code, don't blame me return service.getName().contains("lustal"); } @Override public boolean isInteractive() { return false; } @Override public MenuEntryProviderI getMenuBuilder() { return this::buildMenu; } protected void buildMenu(JMenu parent, AlignFrame frame) { if (canSubmitGaps()) { var alignSubmenu = new JMenu(service.getName()); buildMenu(alignSubmenu, frame, false); parent.add(alignSubmenu); var realignSubmenu = new JMenu(MessageManager.formatMessage( "label.realign_with_params", service.getName())); realignSubmenu.setToolTipText(MessageManager .getString("label.align_sequences_to_existing_alignment")); buildMenu(realignSubmenu, frame, true); parent.add(realignSubmenu); } else { buildMenu(parent, frame, false); } } protected void buildMenu(JMenu parent, AlignFrame frame, boolean submitGaps) { final String action = submitGaps ? "Align" : "Realign"; final var calcName = service.getName(); String title = frame.getTitle(); PollingTaskExecutor executor = frame.getViewport().getWSExecutor(); { var item = new JMenuItem(MessageManager.formatMessage( "label.calcname_with_default_settings", calcName)); item.setToolTipText(MessageManager .formatMessage("label.action_with_default_settings", action)); item.addActionListener((event) -> { final AlignmentView msa = frame.gatherSequencesForAlignment(); final AlignViewport viewport = frame.getViewport(); final AlignmentI alignment = frame.getViewport().getAlignment(); if (msa != null) { WebServiceWorkerI worker = new AlignmentWorker(msa, Collections.emptyList(), title, submitGaps, true, alignment, viewport); executor.submit(worker); } }); parent.add(item); } if (service.hasParameters()) { var item = new JMenuItem( MessageManager.getString("label.edit_settings_and_run")); item.setToolTipText(MessageManager.getString( "label.view_and_change_parameters_before_alignment")); item.addActionListener((event) -> { final AlignmentView msa = frame.gatherSequencesForAlignment(); final AlignViewport viewport = frame.getViewport(); final AlignmentI alignment = frame.getViewport().getAlignment(); if (msa != null) { openEditParamsDialog(service, null, null) .thenAcceptAsync((arguments) -> { if (arguments != null) { WebServiceWorkerI worker = new AlignmentWorker(msa, arguments, title, submitGaps, true, alignment, viewport); executor.submit(worker); } }); } }); parent.add(item); } var presets = service.getParamStore().getPresets(); if (presets != null && presets.size() > 0) { final var presetList = new JMenu(MessageManager .formatMessage("label.run_with_preset_params", calcName)); final var showToolTipFor = ToolTipManager.sharedInstance() .getDismissDelay(); for (final var preset : presets) { var item = new JMenuItem(preset.getName()); final int QUICK_TOOLTIP = 1500; item.addMouseListener(new MouseAdapter() { @Override public void mouseEntered(MouseEvent e) { ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP); } @Override public void mouseExited(MouseEvent e) { ToolTipManager.sharedInstance().setDismissDelay(showToolTipFor); } }); String tooltip = JvSwingUtils.wrapTooltip(true, format("%s
%s", MessageManager.getString( preset.isModifiable() ? "label.user_preset" : "label.service_preset"), preset.getDescription())); item.setToolTipText(tooltip); item.addActionListener((event) -> { final AlignmentView msa = frame.gatherSequencesForAlignment(); final AlignViewport viewport = frame.getViewport(); final AlignmentI alignment = frame.getViewport().getAlignment(); if (msa != null) { WebServiceWorkerI worker = new AlignmentWorker(msa, preset.getArguments(), title, submitGaps, true, alignment, viewport); executor.submit(worker); } }); presetList.add(item); } parent.add(presetList); } } private CompletionStage> openEditParamsDialog( WebServiceI service, WsParamSetI preset, List arguments) { WsJobParameters jobParams; if (preset == null && arguments != null && arguments.size() > 0) jobParams = new WsJobParameters(service.getParamStore(), preset, arguments); else jobParams = new WsJobParameters(service.getParamStore(), preset, null); var stage = jobParams.showRunDialog(); return stage.thenApply((startJob) -> { if (startJob) { if (jobParams.getPreset() == null) { return jobParams.getJobParams(); } else { return jobParams.getPreset().getArguments(); } } else { return null; } }); } /** * Implementation of the web service worker performing multiple sequence * alignment. * * @author mmwarowny * */ private class AlignmentWorker implements WebServiceWorkerI { private long uid = MathUtils.getUID(); private final AlignmentView msa; private final AlignmentI dataset; private final AlignViewport viewport; private final List codonFrame = new ArrayList<>(); private List args = Collections.emptyList(); private String alnTitle = ""; private boolean submitGaps = false; private boolean preserveOrder = false; private char gapCharacter; private WSJobList jobs = new WSJobList(); private Map inputs = new LinkedHashMap<>(); private WebserviceInfo wsInfo; private Map exceptionCount = new HashMap<>(); private final int MAX_RETRY = 5; AlignmentWorker(AlignmentView msa, List args, String alnTitle, boolean submitGaps, boolean preserveOrder, AlignmentI alignment, AlignViewport viewport) { this.msa = msa; this.dataset = alignment.getDataset(); List cf = Objects.requireNonNullElse( alignment.getCodonFrames(), Collections.emptyList()); this.codonFrame.addAll(cf); this.args = args; this.alnTitle = alnTitle; this.submitGaps = submitGaps; this.preserveOrder = preserveOrder; this.viewport = viewport; this.gapCharacter = viewport.getGapCharacter(); String panelInfo = String.format("%s using service hosted at %s%n%s", service.getName(), service.getHostName(), Objects.requireNonNullElse(service.getDescription(), "")); wsInfo = new WebserviceInfo(service.getName(), panelInfo, false); } @Override public long getUID() { return uid; } @Override public WebServiceI getWebService() { return service; } @Override public List getJobs() { return Collections.unmodifiableList(jobs); } @Override public void start() throws IOException { Cache.log.info(format("Starting new %s job.", service.getName())); String outputHeader = String.format("%s of %s%nJob details%n", submitGaps ? "Re-alignment" : "Alignment", alnTitle); SequenceI[][] conmsa = msa.getVisibleContigs('-'); if (conmsa == null) { return; } WebServiceInfoUpdater updater = new WebServiceInfoUpdater(wsInfo); updater.setOutputHeader(outputHeader); int numValid = 0; for (int i = 0; i < conmsa.length; i++) { JobInput input = JobInput.create(conmsa[i], 2, submitGaps); WSJob job = new WSJob(service.getProviderName(), service.getName(), service.getHostName()); job.setJobNum(wsInfo.addJobPane()); if (conmsa.length > 1) { wsInfo.setProgressName(String.format("region %d", i), job.getJobNum()); } wsInfo.setProgressText(job.getJobNum(), outputHeader); job.addPropertyChangeListener(updater); inputs.put(job.getUid(), input); jobs.add(job); if (input.isInputValid()) { int count; String jobId = null; do { count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY); try { jobId = service.submit(input.inputSequences, args); Cache.log.debug((format("Job %s submitted", job))); exceptionCount.remove(job.getUid()); } catch (IOException e) { exceptionCount.put(job.getUid(), --count); } } while (jobId == null && count > 0); if (jobId != null) { job.setJobId(jobId); job.setStatus(WSJobStatus.SUBMITTED); numValid++; } else { job.setStatus(WSJobStatus.SERVER_ERROR); } } else { job.setStatus(WSJobStatus.INVALID); job.setErrorLog( MessageManager.getString("label.empty_alignment_job")); } } if (numValid > 0) { // wsInfo.setThisService() should happen here wsInfo.setVisible(true); } else { wsInfo.setVisible(false); // TODO show notification dialog. // JvOptionPane.showMessageDialog(frame, // MessageManager.getString("info.invalid_msa_input_mininfo"), // MessageManager.getString("info.invalid_msa_notenough"), // JvOptionPane.INFORMATION_MESSAGE); } } @Override public boolean poll() { boolean done = true; for (WSJob job : getJobs()) { if (!job.getStatus().isDone() && !job.getStatus().isFailed()) { Cache.log.debug(format("Polling job %s.", job)); try { service.updateProgress(job); exceptionCount.remove(job.getUid()); } catch (IOException e) { Cache.log.error(format("Polling job %s failed.", job), e); wsInfo.appendProgressText(job.getJobNum(), MessageManager.formatMessage("info.server_exception", service.getName(), e.getMessage())); int count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY); if (--count <= 0) { job.setStatus(WSJobStatus.SERVER_ERROR); Cache.log.warn(format( "Attempts limit exceeded. Droping job %s.", job)); } exceptionCount.put(job.getUid(), count); } catch (OutOfMemoryError e) { job.setStatus(WSJobStatus.BROKEN); Cache.log.error( format("Out of memory when retrieving job %s", job), e); } Cache.log.debug( format("Job %s status is %s", job, job.getStatus())); } done &= job.getStatus().isDone() || job.getStatus().isFailed(); } updateWSInfoGlobalStatus(); return done; } private void updateWSInfoGlobalStatus() { if (jobs.countRunning() > 0) { wsInfo.setStatus(WebserviceInfo.STATE_RUNNING); } else if (jobs.countQueuing() > 0 || jobs.countSubmitted() < jobs.size()) { wsInfo.setStatus(WebserviceInfo.STATE_QUEUING); } else { if (jobs.countSuccessful() > 0) { wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_OK); } else if (jobs.countCancelled() > 0) { wsInfo.setStatus(WebserviceInfo.STATE_CANCELLED_OK); } else if (jobs.countFailed() > 0) { wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR); } } } @Override public void done() { long progbarId = MathUtils.getUID(); wsInfo.setProgressBar( MessageManager.getString("status.collecting_job_results"), progbarId); Map results = new LinkedHashMap<>(); for (WSJob job : getJobs()) { if (job.getStatus().isFailed()) continue; try { AlignmentI alignment = supplier.getResult(job, dataset.getSequences(), viewport); if (alignment != null) { results.put(job.getUid(), alignment); } } catch (Exception e) { if (!service.handleCollectionError(job, e)) { Cache.log.error("Couldn't get alignment for job.", e); // TODO: Increment exception count and retry. job.setStatus(WSJobStatus.SERVER_ERROR); } } } updateWSInfoGlobalStatus(); if (results.size() > 0) { OutputWrapper out = prepareOutput(results); wsInfo.showResultsNewFrame.addActionListener(evt -> displayNewFrame( new Alignment(out.aln), out.alorders, out.hidden)); wsInfo.setResultsReady(); } else { wsInfo.setFinishedNoResults(); } wsInfo.removeProgressBar(progbarId); } private class OutputWrapper { AlignmentI aln; List alorders; HiddenColumns hidden; OutputWrapper(AlignmentI aln, List alorders, HiddenColumns hidden) { this.aln = aln; this.alorders = alorders; this.hidden = hidden; } } private OutputWrapper prepareOutput(Map alignments) { List alorders = new ArrayList<>(); SequenceI[][] results = new SequenceI[jobs.size()][]; AlignmentOrder[] orders = new AlignmentOrder[jobs.size()]; for (int i = 0; i < jobs.size(); i++) { WSJob job = jobs.get(i); AlignmentI aln = alignments.get(job.getUid()); if (aln != null) // equivalent of job.hasResults() { /* Get the alignment including any empty sequences in the original * order with original ids. */ JobInput input = inputs.get(job.getUid()); char gapChar = aln.getGapCharacter(); List emptySeqs = input.emptySequences; List alnSeqs = aln.getSequences(); // find the width of the longest sequence int width = 0; for (var seq : alnSeqs) width = Integer.max(width, seq.getLength()); for (var emptySeq : emptySeqs) width = Integer.max(width, emptySeq.getLength()); // pad shorter sequences with gaps String gapSeq = String.join("", Collections.nCopies(width, Character.toString(gapChar))); List seqs = new ArrayList<>( alnSeqs.size() + emptySeqs.size()); seqs.addAll(alnSeqs); seqs.addAll(emptySeqs); for (var seq : seqs) { if (seq.getLength() < width) seq.setSequence(seq.getSequenceAsString() + gapSeq.substring(seq.getLength())); } SequenceI[] result = seqs.toArray(new SequenceI[0]); AlignmentOrder msaOrder = new AlignmentOrder(result); AlignmentSorter.recoverOrder(result); // temporary workaround for deuniquify @SuppressWarnings({ "rawtypes", "unchecked" }) Hashtable names = new Hashtable(input.sequenceNames); // FIXME first call to deuniquify alters original alignment SeqsetUtils.deuniquify(names, result); alorders.add(msaOrder); results[i] = result; orders[i] = msaOrder; } else { results[i] = null; } } Object[] newView = msa.getUpdatedView(results, orders, gapCharacter); // free references to original data for (int i = 0; i < jobs.size(); i++) { results[i] = null; orders[i] = null; } SequenceI[] alignment = (SequenceI[]) newView[0]; HiddenColumns hidden = (HiddenColumns) newView[1]; Alignment aln = new Alignment(alignment); aln.setProperty("Alignment Program", service.getName()); if (dataset != null) aln.setDataset(dataset); propagateDatasetMappings(aln); return new OutputWrapper(aln, alorders, hidden); // displayNewFrame(aln, alorders, hidden); } /* * conserves dataset references to sequence objects returned from web * services. propagate codon frame data to alignment. */ private void propagateDatasetMappings(Alignment aln) { if (codonFrame != null) { SequenceI[] alignment = aln.getSequencesArray(); for (SequenceI seq : alignment) { for (AlignedCodonFrame acf : codonFrame) { if (acf != null && acf.involvesSequence(seq)) { aln.addCodonFrame(acf); break; } } } } } private void displayNewFrame(AlignmentI aln, List alorders, HiddenColumns hidden) { AlignFrame frame = new AlignFrame(aln, hidden, AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT); // TODO store feature renderer settings in worker object // frame.getFeatureRenderer().transferSettings(featureSettings); var regions = sortOrders(alorders); if (alorders.size() == 1) { frame.addSortByOrderMenuItem( format("%s Ordering", service.getName()), alorders.get(0)); } else { for (int i = 0; i < alorders.size(); i++) { final int j = i; Iterable iter = () -> regions.get(j).stream() .map(it -> Integer.toString(it)).iterator(); var orderName = format("%s Region %s Ordering", service.getName(), String.join(",", iter)); frame.addSortByOrderMenuItem(orderName, alorders.get(i)); } } /* TODO * If alignment was requested from one half of a SplitFrame, show in a * SplitFrame with the other pane similarly aligned. */ Desktop.addInternalFrame(frame, alnTitle, AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT); } private List> sortOrders(List alorders) { List> regions = new ArrayList<>(); for (int i = 0; i < alorders.size(); i++) { List regs = new ArrayList<>(); regs.add(i); int j = i + 1; while (j < alorders.size()) { if (alorders.get(i).equals(alorders.get(j))) { alorders.remove(j); regs.add(j); } else { j++; } } regions.add(regs); } return regions; } } private static class JobInput { final List inputSequences; final List emptySequences; @SuppressWarnings("rawtypes") final Map sequenceNames; private JobInput(int numSequences, List inputSequences, List emptySequences, @SuppressWarnings("rawtypes") Map names) { this.inputSequences = Collections.unmodifiableList(inputSequences); this.emptySequences = Collections.unmodifiableList(emptySequences); this.sequenceNames = names; } boolean isInputValid() { return inputSequences.size() >= 2; } static JobInput create(SequenceI[] sequences, int minLength, boolean submitGaps) { assert minLength >= 0 : MessageManager.getString( "error.implementation_error_minlen_must_be_greater_zero"); int numSeq = 0; for (SequenceI seq : sequences) { if (seq.getEnd() - seq.getStart() >= minLength) { numSeq++; } } List inputSequences = new ArrayList<>(); List emptySequences = new ArrayList<>(); @SuppressWarnings("rawtypes") Map names = new LinkedHashMap<>(); for (int i = 0; i < sequences.length; i++) { SequenceI seq = sequences[i]; String newName = SeqsetUtils.unique_name(i); @SuppressWarnings("rawtypes") Hashtable hash = SeqsetUtils.SeqCharacterHash(seq); names.put(newName, hash); if (numSeq > 1 && seq.getEnd() - seq.getStart() >= minLength) { String seqString = seq.getSequenceAsString(); if (!submitGaps) { seqString = AlignSeq.extractGaps( jalview.util.Comparison.GapChars, seqString); } inputSequences.add(new Sequence(newName, seqString)); } else { String seqString = ""; if (seq.getEnd() >= seq.getStart()) // true if gaps only { seqString = seq.getSequenceAsString(); if (!submitGaps) { seqString = AlignSeq.extractGaps( jalview.util.Comparison.GapChars, seqString); } } emptySequences.add(new Sequence(newName, seqString)); } } return new JobInput(numSeq, inputSequences, emptySequences, names); } } }