e3560bfac104eb5d1e98de6141342290b369c65c
[jalview.git] / src / jalview / ws2 / operations / AlignmentOperation.java
1 package jalview.ws2.operations;
2
3 import static java.lang.String.format;
4
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.LinkedHashMap;
12 import java.util.List;
13 import java.util.Map;
14 import java.util.Objects;
15 import java.util.concurrent.CompletableFuture;
16 import java.util.concurrent.CompletionStage;
17
18 import javax.swing.JMenu;
19 import javax.swing.JMenuItem;
20 import javax.swing.ToolTipManager;
21
22 import org.codehaus.groovy.ast.GenericsType.GenericsTypeName;
23
24 import jalview.bin.Cache;
25 import jalview.datamodel.AlignmentI;
26 import jalview.datamodel.AlignmentOrder;
27 import jalview.datamodel.AlignmentView;
28 import jalview.datamodel.SequenceI;
29 import jalview.gui.AlignFrame;
30 import jalview.gui.JvSwingUtils;
31 import jalview.gui.WebserviceInfo;
32 import jalview.gui.WsJobParameters;
33 import jalview.util.MathUtils;
34 import jalview.util.MessageManager;
35 import jalview.ws.params.ArgumentI;
36 import jalview.ws.params.WsParamSetI;
37 import jalview.ws2.MenuEntryProviderI;
38 import jalview.ws2.ResultSupplier;
39 import jalview.ws2.WSJob;
40 import jalview.ws2.WSJobStatus;
41 import jalview.ws2.WebServiceExecutor;
42 import jalview.ws2.WebServiceI;
43 import jalview.ws2.WebServiceInfoUpdater;
44 import jalview.ws2.WebServiceWorkerI;
45 import jalview.ws2.utils.WSJobList;
46
47 /**
48  *
49  * @author mmwarowny
50  *
51  */
52 public class AlignmentOperation implements Operation
53 {
54   final WebServiceI service;
55   final ResultSupplier<AlignmentI> supplier;
56
57   public AlignmentOperation(WebServiceI service, ResultSupplier<AlignmentI> supplier) {
58     this.service = service;
59     this.supplier = supplier;
60   }
61
62   @Override public int getMinSequences() { return 2; }
63   @Override public int getMaxSequences() { return Integer.MAX_VALUE; }
64   @Override public boolean isProteinOperation() { return true; }
65   @Override public boolean isNucleotideOperation() { return true; }
66   @Override public boolean canSubmitGaps() {
67     // hack copied from original jabaws code, don't blame me
68     return service.getName().contains("lustal");
69   }
70   @Override public MenuEntryProviderI getMenuBuilder() { return this::buildMenu; }
71
72   protected void buildMenu(JMenu parent, AlignFrame frame) {
73     if (canSubmitGaps()) {
74       var alignSubmenu = new JMenu(service.getName());
75       buildMenu(alignSubmenu, frame, false);
76       parent.add(alignSubmenu);
77       var realignSubmenu = new JMenu(MessageManager.formatMessage(
78               "label.realign_with_params", service.getName()));
79       realignSubmenu.setToolTipText(MessageManager.getString(
80               "label.align_sequences_to_existing_alignment"));
81       buildMenu(realignSubmenu, frame, true);
82       parent.add(realignSubmenu);
83     }
84     else {
85       buildMenu(parent, frame, false);
86     }
87   }
88
89   protected void buildMenu(JMenu parent, AlignFrame frame, boolean submitGaps) {
90     final String action = submitGaps ? "Align" : "Realign";
91     final var calcName = service.getName();
92
93     final AlignmentView msa = frame.gatherSequencesForAlignment();
94     final AlignmentI dataset = frame.getViewport().getAlignment().getDataset();
95     String title = frame.getTitle();
96     WebServiceExecutor executor = frame.getViewport().getWSExecutor();
97     {
98       var item = new JMenuItem(MessageManager.formatMessage(
99           "label.calcname_with_default_settings", calcName));
100       item.setToolTipText(MessageManager.formatMessage(
101           "label.action_with_default_settings", action));
102       item.addActionListener((event) -> {
103         if (msa != null)
104         {
105           WebServiceWorkerI worker = new AlignmentWorker(
106                   msa, Collections.emptyList(), title, submitGaps, true, dataset);
107           executor.submit(worker);
108         }
109       });
110       parent.add(item);
111     }
112
113     if (service.hasParameters())
114     {
115       var item = new JMenuItem(MessageManager.getString("label.edit_settings_and_run"));
116       item.setToolTipText(MessageManager.getString(
117           "label.view_and_change_parameters_before_alignment"));
118       item.addActionListener((event) -> {
119         if (msa != null)
120         {
121           openEditParamsDialog(service, null, null).thenAcceptAsync((arguments) ->{
122             if (arguments != null)
123             {
124               WebServiceWorkerI worker = new AlignmentWorker(
125                       msa, arguments, title, submitGaps, true, dataset);
126               executor.submit(worker);
127             }
128           });
129         }
130       });
131       parent.add(item);
132     }
133
134     var presets = service.getParamStore().getPresets();
135     if (presets != null && presets.size() > 0)
136     {
137       final var presetList = new JMenu(MessageManager.formatMessage(
138           "label.run_with_preset_params", calcName));
139       final var showToolTipFor = ToolTipManager.sharedInstance().getDismissDelay();
140       for (final var preset : presets)
141       {
142         var item = new JMenuItem(preset.getName());
143         final int QUICK_TOOLTIP = 1500;
144         item.addMouseListener(new MouseAdapter()
145         {
146           @Override public void mouseEntered(MouseEvent e)
147           {
148             ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
149           }
150           @Override public void mouseExited(MouseEvent e)
151           {
152             ToolTipManager.sharedInstance().setDismissDelay(showToolTipFor);
153           }
154         });
155         String tooltip = JvSwingUtils.wrapTooltip(true, format(
156             "<strong>%s</strong><br/>%s",
157             MessageManager.getString(preset.isModifiable() ?
158                 "label.user_preset" : "label.service_preset"),
159             preset.getDescription()));
160         item.setToolTipText(tooltip);
161         item.addActionListener((event) -> {
162           if (msa != null)
163           {
164             WebServiceWorkerI worker = new AlignmentWorker(
165                     msa, preset.getArguments(), title, submitGaps, true, dataset);
166             executor.submit(worker);
167           }
168         });
169         presetList.add(item);
170       }
171       parent.add(presetList);
172     }
173   }
174
175
176   private CompletionStage<List<ArgumentI>> openEditParamsDialog(WebServiceI service,
177       WsParamSetI preset, List<ArgumentI> arguments)
178   {
179     WsJobParameters jobParams;
180     if (preset == null && arguments != null && arguments.size() > 0)
181       jobParams = new WsJobParameters(service.getParamStore(), preset, arguments);
182     else
183       jobParams = new WsJobParameters(service.getParamStore(), preset, null);
184     var stage = jobParams.showRunDialog();
185     return stage.thenApply((startJob) -> {
186       if (startJob) {
187         if (jobParams.getPreset() == null) {
188           return jobParams.getJobParams();
189         }
190         else {
191           return jobParams.getPreset().getArguments();
192         }
193       }
194       else {
195         return null;
196       }
197     });
198   }
199
200   /**
201    * Implementation of the web service worker performing multiple sequence
202    * alignment.
203    *
204    * @author mmwarowny
205    *
206    */
207   private class AlignmentWorker implements WebServiceWorkerI {
208
209     private long uid = MathUtils.getUID();
210     private final AlignmentView msa;
211     private final AlignmentI seqdata;
212     private List<ArgumentI> args = Collections.emptyList();
213     private String alnTitle = "";
214     private boolean submitGaps = false;
215     private boolean preserveOrder = false;
216     private WSJobList jobs = new WSJobList();
217     private WebserviceInfo wsInfo;
218     private Map<Long, Integer> exceptionCount = new HashMap<>();
219     private final int MAX_RETRY = 5;
220
221     AlignmentWorker(AlignmentView msa, List<ArgumentI> args, String alnTitle,
222             boolean submitGaps, boolean preserveOrder, AlignmentI seqdata)
223     {
224       this.msa = msa;
225       this.seqdata = seqdata;
226       this.args = args;
227       this.alnTitle = alnTitle;
228       this.submitGaps = submitGaps;
229       this.preserveOrder = preserveOrder;
230
231       String panelInfo = String.format("%s using service hosted at %s%n%s",
232               service.getName(), service.getHostName(),
233               Objects.requireNonNullElse(service.getDescription(), ""));
234       wsInfo = new WebserviceInfo(service.getName(), panelInfo, false);
235     }
236
237
238     @Override
239     public long getUID() { return uid; }
240
241     @Override
242     public List<WSJob> getJobs()
243     {
244       return Collections.unmodifiableList(jobs);
245     }
246
247     @Override
248     public void startJobs() throws IOException
249     {
250       String outputHeader = String.format("%s of %s%nJob details%n",
251               submitGaps ? "Re-alignment" : "Alignment", alnTitle);
252       SequenceI[][] conmsa = msa.getVisibleContigs('-');
253       if (conmsa == null) {
254         return;
255       }
256       WebServiceInfoUpdater updater = new WebServiceInfoUpdater(wsInfo);
257       updater.setOutputHeader(outputHeader);
258       for (int i = 0; i < conmsa.length; i++) {
259         WSJob job = service.submit(List.of(conmsa[i]), args);
260         job.setJobNum(wsInfo.addJobPane());
261         job.addPropertyChangeListener(updater);
262         jobs.add(job);
263         if (conmsa.length > 0) {
264           wsInfo.setProgressName(String.format("region %d", i), job.getJobNum());
265         }
266         wsInfo.setProgressText(job.getJobNum(), outputHeader);
267       }
268     }
269
270     @Override
271     public boolean pollJobs()
272     {
273       boolean done = true;
274       for (WSJob job : getJobs()) {
275         if (!job.getStatus().isDone() ) {
276           try {
277             service.updateProgress(job);
278             exceptionCount.remove(job.getUid());
279           }
280           catch (IOException e) {
281             int count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY);
282             if (--count <= 0) {
283               job.setStatus(WSJobStatus.SERVER_ERROR);
284             }
285             exceptionCount.put(job.getUid(), count);
286           }
287         }
288         done &= job.getStatus().isDone();
289       }
290       updateWSInfoGlobalStatus();
291       return done;
292     }
293
294     private void updateWSInfoGlobalStatus() {
295       if (jobs.countRunning() > 0) {
296         wsInfo.setStatus(WebserviceInfo.STATE_RUNNING);
297       }
298       else if (jobs.countQueuing() > 0 || jobs.countSubmitted() < jobs.size()) {
299         wsInfo.setStatus(WebserviceInfo.STATE_QUEUING);
300       }
301       else {
302         if (jobs.countSuccessful() > 0) {
303           wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_OK);
304         }
305         else if (jobs.countCancelled() > 0) {
306           wsInfo.setStatus(WebserviceInfo.STATE_CANCELLED_OK);
307         }
308         else if (jobs.countFailed() > 0) {
309           wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
310         }
311       }
312     }
313
314     @Override
315     public void done() {
316       long progbarId = MathUtils.getUID();
317       wsInfo.setProgressBar(
318               MessageManager.getString("status.collecting_job_results"),
319               progbarId);
320       Map<Long, AlignmentI> results = new LinkedHashMap<>();
321       for (WSJob job : getJobs()) {
322         try {
323           AlignmentI alignment = supplier.getResult(job);
324           if (alignment != null) {
325             results.put(job.getUid(), alignment);
326           }
327         }
328         catch (Exception e) {
329           if (!service.handleCollectionError(job, e)) {
330             Cache.log.error("Couldn't get alignment for job.", e);
331             // TODO: Increment exception count and retry.
332             job.setStatus(WSJobStatus.SERVER_ERROR);
333           }
334         }
335       }
336       updateWSInfoGlobalStatus();
337       if (results.size() > 0) {
338         wsInfo.showResultsNewFrame.addActionListener(
339                 evt -> displayResults(results));
340         wsInfo.setResultsReady();
341       }
342       else {
343         wsInfo.setFinishedNoResults();
344       }
345       wsInfo.removeProgressBar(progbarId);
346     }
347
348     private void displayResults(Map<Long, AlignmentI> alignments)
349     {
350       List<AlignmentOrder> alorders = new ArrayList<>();
351       SequenceI[][] results = new SequenceI[jobs.size()][];
352       AlignmentOrder[] orders = new AlignmentOrder[jobs.size()];
353       for (int i = 0; i < jobs.size(); i++) {
354         WSJob job = jobs.get(i);
355         AlignmentI aln = alignments.get(job.getUid());
356         if (aln != null) {
357           /*
358            * Get the alignment including any empty sequences in the original
359            * order with original ids.
360            */
361           char gapChar = aln.getGapCharacter();
362           int alSeqLen = aln.getSequences().size();
363           SequenceI[] alSeqs = new SequenceI[alSeqLen];
364           alSeqs = aln.getSequences().toArray(alSeqs);
365         }
366         else {
367           results[i] = null;
368         }
369       }
370     }
371
372     @Override
373     public WebServiceI getWebService()
374     {
375       return service;
376     }
377
378   }
379
380 }