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