JAL-4232 Disable menu items for RNA services for proteins.
[jalview.git] / src / jalview / ws2 / gui / WebServicesMenuManager.java
1 package jalview.ws2.gui;
2
3 import java.awt.Color;
4 import java.awt.event.MouseAdapter;
5 import java.awt.event.MouseEvent;
6 import java.net.URL;
7 import java.util.ArrayList;
8 import java.util.Collections;
9 import java.util.Comparator;
10 import java.util.HashMap;
11 import java.util.HashSet;
12 import java.util.List;
13 import java.util.Map;
14 import java.util.TreeMap;
15 import java.util.concurrent.CompletionStage;
16
17 import javax.swing.JCheckBoxMenuItem;
18 import javax.swing.JLabel;
19 import javax.swing.JMenu;
20 import javax.swing.JMenuItem;
21 import javax.swing.ToolTipManager;
22 import javax.swing.border.EmptyBorder;
23
24 import jalview.bin.Console;
25 import jalview.datamodel.AlignmentI;
26 import jalview.gui.AlignFrame;
27 import jalview.gui.Desktop;
28 import jalview.gui.JvSwingUtils;
29 import jalview.gui.WsJobParameters;
30 import jalview.util.MessageManager;
31 import jalview.viewmodel.AlignmentViewport;
32 import jalview.ws.params.ArgumentI;
33 import jalview.ws.params.ParamDatastoreI;
34 import jalview.ws.params.WsParamSetI;
35 import jalview.ws2.actions.BaseTask;
36 import jalview.ws2.actions.PollingTaskExecutor;
37 import jalview.ws2.actions.alignment.AlignmentAction;
38 import jalview.ws2.actions.alignment.AlignmentResult;
39 import jalview.ws2.actions.annotation.AlignCalcWorkerAdapter;
40 import jalview.ws2.actions.annotation.AnnotationAction;
41 import jalview.ws2.actions.api.ActionI;
42 import jalview.ws2.actions.api.TaskEventListener;
43 import jalview.ws2.actions.api.TaskI;
44 import jalview.ws2.actions.hmmer.PhmmerAction;
45 import jalview.ws2.api.Credentials;
46 import jalview.ws2.api.WebService;
47 import jalview.ws2.client.api.WebServiceProviderI;
48
49 public class WebServicesMenuManager
50 {
51   private final JMenu menu;
52
53   private final AlignFrame frame;
54
55   private JMenuItem inProgressItem = new JMenuItem("Service discovery in progress");
56
57   private JMenuItem noServicesItem = new JMenuItem("No services available");
58   {
59     inProgressItem.setEnabled(false);
60     inProgressItem.setVisible(false);
61     noServicesItem.setEnabled(false);
62   }
63
64   public WebServicesMenuManager(String name, AlignFrame frame)
65   {
66     this.frame = frame;
67     menu = new JMenu(name);
68     menu.add(inProgressItem);
69     menu.add(noServicesItem);
70   }
71
72   public JMenu getMenu()
73   {
74     return menu;
75   }
76
77   public void setNoServices(boolean noServices)
78   {
79     noServicesItem.setVisible(noServices);
80   }
81
82   public void setInProgress(boolean inProgress)
83   {
84     inProgressItem.setVisible(inProgress);
85   }
86
87   public void setServices(WebServiceProviderI services)
88   {
89     menu.removeAll();
90     // services grouped by their category
91     Map<String, List<WebService<?>>> oneshotServices = new HashMap<>();
92     Map<String, List<WebService<?>>> interactiveServices = new HashMap<>();
93     for (WebService<?> service : services.getServices())
94     {
95       var map = service.isInteractive() ? interactiveServices : oneshotServices;
96       map.computeIfAbsent(service.getCategory(), k -> new ArrayList<>())
97           .add(service);
98     }
99     var allKeysSet = new HashSet<>(oneshotServices.keySet());
100     allKeysSet.addAll(interactiveServices.keySet());
101     var allKeys = new ArrayList<>(allKeysSet);
102     allKeys.sort(Comparator.naturalOrder());
103     for (String category : allKeys)
104     {
105       var categoryMenu = new JMenu(category);
106       var oneshot = oneshotServices.get(category);
107       if (oneshot != null)
108         addOneshotEntries(oneshot, categoryMenu);
109       var interactive = interactiveServices.get(category);
110       if (interactive != null)
111       {
112         if (oneshot != null)
113           categoryMenu.addSeparator();
114         addInteractiveEntries(interactive, categoryMenu);
115       }
116       menu.add(categoryMenu);
117     }
118     menu.add(inProgressItem);
119     menu.add(noServicesItem);
120   }
121
122   private void addOneshotEntries(List<WebService<?>> services, JMenu menu)
123   {
124     // Workaround. Comparator methods not working in j2s
125     services.sort((ws1, ws2) -> {
126       var res = ws1.getUrl().toString().compareTo(ws2.getUrl().toString());
127       if (res == 0)
128         res = ws1.getName().compareTo(ws2.getName());
129       return res;
130     });
131     URL lastHost = null;
132     for (WebService<?> service : services)
133     {
134       // if new host differs from the last one, add entry separating them
135       URL host = service.getUrl();
136       if (!host.equals(lastHost))
137       {
138         if (lastHost != null)
139           menu.addSeparator();
140         var item = new JMenuItem(host.toString());
141         item.setForeground(Color.BLUE);
142         item.addActionListener(e -> Desktop.showUrl(host.toString()));
143         menu.add(item);
144         lastHost = host;
145       }
146       menu.addSeparator();
147       // group actions by their subcategory, sorted
148       var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
149       for (ActionI<?> action : service.getActions())
150       {
151         actionsByCategory
152             .computeIfAbsent(
153                 action.getSubcategory() != null ? action.getSubcategory() : "",
154                 k -> new ArrayList<>())
155             .add(action);
156       }
157       for (var entry : actionsByCategory.entrySet())
158       {
159         var category = entry.getKey();
160         var actions = entry.getValue();
161         // create submenu named {subcategory} with {service} or use root menu
162         var atMenu = category.isEmpty() ? menu : new JMenu(String.format("%s with %s", category, service.getName()));
163         if (atMenu != menu)
164           menu.add(atMenu); // add only if submenu
165         // sort actions by name pulling nulls to the front
166         actions.sort(Comparator.comparing(
167             ActionI::getName, Comparator.nullsFirst(Comparator.naturalOrder())));
168         for (int i = 0; i < actions.size(); i++)
169         {
170           addEntriesForAction(actions.get(i), atMenu, atMenu == menu);
171         }
172       }
173     }
174   }
175
176   private void addEntriesForAction(ActionI<?> action, JMenu menu, boolean isTopLevel)
177   {
178     var enabled = isActionEnabled(action);
179     var service = action.getWebService();
180     String itemName;
181     if (isTopLevel)
182     {
183       itemName = service.getName();
184       if (action.getName() != null && !action.getName().isEmpty())
185         itemName += " " + action.getName();
186     }
187     else
188     {
189       if (action.getName() == null || action.getName().isEmpty())
190         itemName = "Run";
191       else
192         itemName = action.getName();
193     }
194     var datastore = service.getParamDatastore();
195     {
196       String text = itemName;
197       if (datastore.hasParameters() || datastore.hasPresets())
198         text += " with defaults";
199       JMenuItem item = new JMenuItem(text);
200       item.setEnabled(enabled);
201       item.addActionListener(e -> {
202         runAction(action, frame.getCurrentView(), Collections.emptyList(),
203             Credentials.empty());
204       });
205       menu.add(item);
206     }
207     if (datastore.hasParameters())
208     {
209       JMenuItem item = new JMenuItem("Edit settings and run...");
210       item.setEnabled(enabled);
211       item.addActionListener(e -> {
212         openEditParamsDialog(datastore, null, null).thenAccept(args -> {
213           if (args != null)
214             runAction(action, frame.getCurrentView(), args, Credentials.empty());
215         });
216       });
217       menu.add(item);
218     }
219     var presets = datastore.getPresets();
220     if (presets != null && presets.size() > 0)
221     {
222       final var presetsMenu = new JMenu(MessageManager.formatMessage(
223           "label.run_with_preset_params", service.getName()));
224       presetsMenu.setEnabled(enabled);
225       final int dismissDelay = ToolTipManager.sharedInstance()
226           .getDismissDelay();
227       final int QUICK_TOOLTIP = 1500;
228       for (var preset : presets)
229       {
230         var item = new JMenuItem(preset.getName());
231         item.addMouseListener(new MouseAdapter()
232         {
233           @Override
234           public void mouseEntered(MouseEvent evt)
235           {
236             ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
237           }
238
239           @Override
240           public void mouseExited(MouseEvent evt)
241           {
242             ToolTipManager.sharedInstance().setDismissDelay(dismissDelay);
243           }
244         });
245         String tooltipTitle = MessageManager.getString(
246             preset.isModifiable() ? "label.user_preset" : "label.service_preset");
247         String tooltip = String.format("<strong>%s</strong><br/>%s",
248             tooltipTitle, preset.getDescription());
249         tooltip = JvSwingUtils.wrapTooltip(true, tooltip);
250         item.setToolTipText(tooltip);
251         item.addActionListener(event -> {
252           runAction(action, frame.getCurrentView(), preset.getArguments(),
253               Credentials.empty());
254         });
255         presetsMenu.add(item);
256       }
257       menu.add(presetsMenu);
258     }
259   }
260
261   private void addInteractiveEntries(List<WebService<?>> services, JMenu menu)
262   {
263     Map<String, List<WebService<?>>> byServiceName = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
264     for (var service : services)
265     {
266       byServiceName.computeIfAbsent(service.getName(), k -> new ArrayList<>())
267           .add(service);
268     }
269     for (var entry : byServiceName.entrySet())
270     {
271       var group = new InteractiveServiceEntryGroup(entry.getKey(), entry.getValue());
272       group.appendTo(menu);
273     }
274   }
275
276   private class InteractiveServiceEntryGroup
277   {
278     JLabel serviceLabel;
279
280     JMenuItem urlItem = new JMenuItem();
281
282     JCheckBoxMenuItem serviceItem = new JCheckBoxMenuItem();
283
284     JMenuItem editParamsItem = new JMenuItem("Edit parameters...");
285
286     JMenu presetsMenu = new JMenu("Change preset");
287
288     JMenu alternativesMenu = new JMenu("Choose action");
289     {
290       urlItem.setForeground(Color.BLUE);
291       urlItem.setVisible(false);
292       serviceItem.setVisible(false);
293       editParamsItem.setVisible(false);
294       presetsMenu.setVisible(false);
295     }
296
297     InteractiveServiceEntryGroup(String name, List<WebService<?>> services)
298     {
299       serviceLabel = new JLabel(name);
300       serviceLabel.setBorder(new EmptyBorder(0, 6, 0, 6));
301       buildAlternativesMenu(services);
302     }
303
304     private void buildAlternativesMenu(List<WebService<?>> services)
305     {
306       var menu = alternativesMenu;
307       services.sort((ws1, ws2) -> {
308         var res = ws1.getUrl().toString().compareTo(ws2.getUrl().toString());
309         if (res == 0)
310           res = ws1.getName().compareTo(ws2.getName());
311         return res;
312       });
313       URL lastHost = null;
314       for (var service : services)
315       {
316         // Adding url "separator" before each group
317         URL host = service.getUrl();
318         if (!host.equals(lastHost))
319         {
320           if (lastHost != null)
321             menu.addSeparator();
322           var item = new JMenuItem(host.toString());
323           item.setForeground(Color.BLUE);
324           item.addActionListener(e -> Desktop.showUrl(host.toString()));
325           menu.add(item);
326           lastHost = host;
327         }
328         menu.addSeparator();
329         var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
330         for (ActionI<?> action : service.getActions())
331         {
332           actionsByCategory
333               .computeIfAbsent(
334                   action.getSubcategory() != null ? action.getSubcategory() : "",
335                   k -> new ArrayList<>())
336               .add(action);
337         }
338         actionsByCategory.forEach((key, actions) -> {
339           var atMenu = key.isEmpty() ? menu : new JMenu(key + " with " + service.getName());
340           boolean topLevel = atMenu == menu;
341           if (!topLevel)
342             menu.add(atMenu);
343           actions.sort(Comparator.comparing(
344               a -> a.getName(),
345               Comparator.nullsFirst(Comparator.naturalOrder())));
346           for (ActionI<?> action : actions)
347           {
348             var item = new JMenuItem(action.getFullName());
349             item.setEnabled(isActionEnabled(action));
350             item.addActionListener(e -> setAlternative(action));
351             atMenu.add(item);
352           }
353         });
354       }
355     }
356
357     private void setAlternative(ActionI<?> action)
358     {
359       final var arguments = new ArrayList<ArgumentI>();
360       final WsParamSetI[] lastPreset = { null };
361
362       // update selected url menu item
363       String url = action.getWebService().getUrl().toString();
364       urlItem.setText(url);
365       urlItem.setVisible(true);
366       for (var l : urlItem.getActionListeners())
367         urlItem.removeActionListener(l);
368       urlItem.addActionListener(e -> Desktop.showUrl(url));
369
370       // update selected service menu item
371       serviceItem.setText(action.getFullName());
372       serviceItem.setVisible(true);
373       for (var l : serviceItem.getActionListeners())
374         serviceItem.removeActionListener(l);
375       WebService<?> service = action.getWebService();
376       serviceItem.addActionListener(e -> {
377         runAction(action, frame.getCurrentView(), arguments,
378             Credentials.empty());
379       });
380       serviceItem.setSelected(true);
381
382       // update edit parameters menu item
383       var datastore = service.getParamDatastore();
384       editParamsItem.setVisible(datastore.hasParameters());
385       for (var l : editParamsItem.getActionListeners())
386         editParamsItem.removeActionListener(l);
387       if (datastore.hasParameters())
388       {
389         editParamsItem.addActionListener(e -> {
390           openEditParamsDialog(service.getParamDatastore(), lastPreset[0], arguments)
391               .thenAccept(args -> {
392                 if (args != null)
393                 {
394                   lastPreset[0] = null;
395                   arguments.clear();
396                   arguments.addAll(args);
397                   runAction(action, frame.getCurrentView(),
398                       arguments, Credentials.empty());
399                 }
400               });
401         });
402       }
403
404       // update presets menu
405       presetsMenu.removeAll();
406       presetsMenu.setEnabled(datastore.hasPresets());
407       if (datastore.hasPresets())
408       {
409         for (WsParamSetI preset : datastore.getPresets())
410         {
411           var item = new JMenuItem(preset.getName());
412           item.addActionListener(e -> {
413             lastPreset[0] = preset;
414             runAction(action, frame.getCurrentView(),
415                 preset.getArguments(), Credentials.empty());
416           });
417           presetsMenu.add(item);
418         }
419       }
420
421       runAction(action, frame.getCurrentView(), arguments,
422           Credentials.empty());
423     }
424
425     void appendTo(JMenu menu)
426     {
427       menu.add(serviceLabel);
428       menu.add(urlItem);
429       menu.add(serviceItem);
430       menu.add(editParamsItem);
431       menu.add(presetsMenu);
432       menu.add(alternativesMenu);
433     }
434   }
435
436   private boolean isActionEnabled(ActionI<?> action)
437   {
438     var isNa = frame.getViewport().getAlignment().isNucleotide();
439     return ((isNa && action.doAllowNucleotide()) ||
440         (!isNa && action.doAllowProtein()));
441   }
442
443   private void runAction(ActionI<?> action, AlignmentViewport viewport,
444       List<ArgumentI> args, Credentials credentials)
445   {
446     // casting and instance checks can be avoided with some effort,
447     // let them be for now.
448     if (action instanceof AlignmentAction)
449     {
450       // TODO: test if selection contains enough sequences
451       var _action = (AlignmentAction) action;
452       var handler = new AlignmentServiceGuiHandler(_action, frame);
453       BaseTask<?, AlignmentResult> task = _action.createTask(viewport, args, credentials);
454       var executor = PollingTaskExecutor.fromPool(viewport.getServiceExecutor());
455       task.addTaskEventListener(handler);
456       var future = executor.submit(task);
457       task.setCancelAction(() -> { future.cancel(true); });
458       return;
459     }
460     if (action instanceof AnnotationAction)
461     {
462       var calcManager = viewport.getCalcManager();
463
464       var _action = (AnnotationAction) action;
465       var worker = new AlignCalcWorkerAdapter(viewport, frame.alignPanel,
466           _action, args, credentials);
467       var handler = new AnnotationServiceGuiHandler(_action, frame);
468       worker.setWorkerListener(handler);
469       for (var w : calcManager.getWorkers())
470       {
471         if (worker.getCalcName() != null && worker.getCalcName().equals(w.getCalcName()))
472         {
473           calcManager.cancelWorker(w);
474           calcManager.removeWorker(w);
475         }
476       }
477       if (action.getWebService().isInteractive())
478         calcManager.registerWorker(worker);
479       else
480         calcManager.startWorker(worker);
481       return;
482     }
483     if (action instanceof PhmmerAction)
484     {
485       var _action = (PhmmerAction) action;
486       var handler = new SearchServiceGuiHandler(_action, frame);
487       TaskI<AlignmentI> task = _action.createTask(viewport, args, credentials);
488       var executor = PollingTaskExecutor.fromPool(viewport.getServiceExecutor());
489       task.addTaskEventListener(handler);
490       _action.perform(viewport, args, credentials, handler);
491       return;
492     }
493     Console.warn(String.format(
494             "No known handler for action type %s. All output will be discarded.",
495             action.getClass().getName()));
496     var task = action.createTask(viewport, args, credentials);
497     task.addTaskEventListener(TaskEventListener.nullListener());
498     PollingTaskExecutor.fromPool(viewport.getServiceExecutor())
499       .submit(task);
500   }
501
502   private static CompletionStage<List<ArgumentI>> openEditParamsDialog(
503       ParamDatastoreI paramStore, WsParamSetI preset, List<ArgumentI> arguments)
504   {
505     final WsJobParameters jobParams;
506     if (preset == null && arguments != null && arguments.size() > 0)
507       jobParams = new WsJobParameters(paramStore, null, arguments);
508     else
509       jobParams = new WsJobParameters(paramStore, preset, null);
510     if (preset != null)
511       jobParams.setName(MessageManager.getString(
512           "label.adjusting_parameters_for_calculation"));
513     var stage = jobParams.showRunDialog();
514     return stage.thenApply(startJob -> {
515       if (!startJob)
516         return null; // null if cancelled
517       if (jobParams.getPreset() != null)
518         return jobParams.getPreset().getArguments();
519       if (jobParams.isServiceDefaults())
520         return Collections.emptyList();
521       else
522         return jobParams.getJobParams();
523     });
524   }
525 }