Merge branch 'mmw/JAL-4199-task-execution-update' into development/Release_2_12_Branch
[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 service = action.getWebService();
179     String itemName;
180     if (isTopLevel)
181     {
182       itemName = service.getName();
183       if (action.getName() != null && !action.getName().isEmpty())
184         itemName += " " + action.getName();
185     }
186     else
187     {
188       if (action.getName() == null || action.getName().isEmpty())
189         itemName = "Run";
190       else
191         itemName = action.getName();
192     }
193     var datastore = service.getParamDatastore();
194     {
195       String text = itemName;
196       if (datastore.hasParameters() || datastore.hasPresets())
197         text += " with defaults";
198       JMenuItem item = new JMenuItem(text);
199       item.addActionListener(e -> {
200         runAction(action, frame.getCurrentView(), Collections.emptyList(),
201             Credentials.empty());
202       });
203       menu.add(item);
204     }
205     if (datastore.hasParameters())
206     {
207       JMenuItem item = new JMenuItem("Edit settings and run...");
208       item.addActionListener(e -> {
209         openEditParamsDialog(datastore, null, null).thenAccept(args -> {
210           if (args != null)
211             runAction(action, frame.getCurrentView(), args, Credentials.empty());
212         });
213       });
214       menu.add(item);
215     }
216     var presets = datastore.getPresets();
217     if (presets != null && presets.size() > 0)
218     {
219       final var presetsMenu = new JMenu(MessageManager.formatMessage(
220           "label.run_with_preset_params", service.getName()));
221       final int dismissDelay = ToolTipManager.sharedInstance()
222           .getDismissDelay();
223       final int QUICK_TOOLTIP = 1500;
224       for (var preset : presets)
225       {
226         var item = new JMenuItem(preset.getName());
227         item.addMouseListener(new MouseAdapter()
228         {
229           @Override
230           public void mouseEntered(MouseEvent evt)
231           {
232             ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
233           }
234
235           @Override
236           public void mouseExited(MouseEvent evt)
237           {
238             ToolTipManager.sharedInstance().setDismissDelay(dismissDelay);
239           }
240         });
241         String tooltipTitle = MessageManager.getString(
242             preset.isModifiable() ? "label.user_preset" : "label.service_preset");
243         String tooltip = String.format("<strong>%s</strong><br/>%s",
244             tooltipTitle, preset.getDescription());
245         tooltip = JvSwingUtils.wrapTooltip(true, tooltip);
246         item.setToolTipText(tooltip);
247         item.addActionListener(event -> {
248           runAction(action, frame.getCurrentView(), preset.getArguments(),
249               Credentials.empty());
250         });
251         presetsMenu.add(item);
252       }
253       menu.add(presetsMenu);
254     }
255   }
256
257   private void addInteractiveEntries(List<WebService<?>> services, JMenu menu)
258   {
259     Map<String, List<WebService<?>>> byServiceName = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
260     for (var service : services)
261     {
262       byServiceName.computeIfAbsent(service.getName(), k -> new ArrayList<>())
263           .add(service);
264     }
265     for (var entry : byServiceName.entrySet())
266     {
267       var group = new InteractiveServiceEntryGroup(entry.getKey(), entry.getValue());
268       group.appendTo(menu);
269     }
270   }
271
272   private class InteractiveServiceEntryGroup
273   {
274     JLabel serviceLabel;
275
276     JMenuItem urlItem = new JMenuItem();
277
278     JCheckBoxMenuItem serviceItem = new JCheckBoxMenuItem();
279
280     JMenuItem editParamsItem = new JMenuItem("Edit parameters...");
281
282     JMenu presetsMenu = new JMenu("Change preset");
283
284     JMenu alternativesMenu = new JMenu("Choose action");
285     {
286       urlItem.setForeground(Color.BLUE);
287       urlItem.setVisible(false);
288       serviceItem.setVisible(false);
289       editParamsItem.setVisible(false);
290       presetsMenu.setVisible(false);
291     }
292
293     InteractiveServiceEntryGroup(String name, List<WebService<?>> services)
294     {
295       serviceLabel = new JLabel(name);
296       serviceLabel.setBorder(new EmptyBorder(0, 6, 0, 6));
297       buildAlternativesMenu(services);
298     }
299
300     private void buildAlternativesMenu(List<WebService<?>> services)
301     {
302       var menu = alternativesMenu;
303       services.sort((ws1, ws2) -> {
304         var res = ws1.getUrl().toString().compareTo(ws2.getUrl().toString());
305         if (res == 0)
306           res = ws1.getName().compareTo(ws2.getName());
307         return res;
308       });
309       URL lastHost = null;
310       for (var service : services)
311       {
312         // Adding url "separator" before each group
313         URL host = service.getUrl();
314         if (!host.equals(lastHost))
315         {
316           if (lastHost != null)
317             menu.addSeparator();
318           var item = new JMenuItem(host.toString());
319           item.setForeground(Color.BLUE);
320           item.addActionListener(e -> Desktop.showUrl(host.toString()));
321           menu.add(item);
322           lastHost = host;
323         }
324         menu.addSeparator();
325         var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
326         for (ActionI<?> action : service.getActions())
327         {
328           actionsByCategory
329               .computeIfAbsent(
330                   action.getSubcategory() != null ? action.getSubcategory() : "",
331                   k -> new ArrayList<>())
332               .add(action);
333         }
334         actionsByCategory.forEach((key, actions) -> {
335           var atMenu = key.isEmpty() ? menu : new JMenu(key + " with " + service.getName());
336           boolean topLevel = atMenu == menu;
337           if (!topLevel)
338             menu.add(atMenu);
339           actions.sort(Comparator.comparing(
340               a -> a.getName(),
341               Comparator.nullsFirst(Comparator.naturalOrder())));
342           for (ActionI<?> action : actions)
343           {
344             var item = new JMenuItem(action.getFullName());
345             item.addActionListener(e -> setAlternative(action));
346             atMenu.add(item);
347           }
348         });
349       }
350     }
351
352     private void setAlternative(ActionI<?> action)
353     {
354       final var arguments = new ArrayList<ArgumentI>();
355       final WsParamSetI[] lastPreset = { null };
356
357       // update selected url menu item
358       String url = action.getWebService().getUrl().toString();
359       urlItem.setText(url);
360       urlItem.setVisible(true);
361       for (var l : urlItem.getActionListeners())
362         urlItem.removeActionListener(l);
363       urlItem.addActionListener(e -> Desktop.showUrl(url));
364
365       // update selected service menu item
366       serviceItem.setText(action.getFullName());
367       serviceItem.setVisible(true);
368       for (var l : serviceItem.getActionListeners())
369         serviceItem.removeActionListener(l);
370       WebService<?> service = action.getWebService();
371       serviceItem.addActionListener(e -> {
372         runAction(action, frame.getCurrentView(), arguments,
373             Credentials.empty());
374       });
375       serviceItem.setSelected(true);
376
377       // update edit parameters menu item
378       var datastore = service.getParamDatastore();
379       editParamsItem.setVisible(datastore.hasParameters());
380       for (var l : editParamsItem.getActionListeners())
381         editParamsItem.removeActionListener(l);
382       if (datastore.hasParameters())
383       {
384         editParamsItem.addActionListener(e -> {
385           openEditParamsDialog(service.getParamDatastore(), lastPreset[0], arguments)
386               .thenAccept(args -> {
387                 if (args != null)
388                 {
389                   lastPreset[0] = null;
390                   arguments.clear();
391                   arguments.addAll(args);
392                   runAction(action, frame.getCurrentView(),
393                       arguments, Credentials.empty());
394                 }
395               });
396         });
397       }
398
399       // update presets menu
400       presetsMenu.removeAll();
401       presetsMenu.setEnabled(datastore.hasPresets());
402       if (datastore.hasPresets())
403       {
404         for (WsParamSetI preset : datastore.getPresets())
405         {
406           var item = new JMenuItem(preset.getName());
407           item.addActionListener(e -> {
408             lastPreset[0] = preset;
409             runAction(action, frame.getCurrentView(),
410                 preset.getArguments(), Credentials.empty());
411           });
412           presetsMenu.add(item);
413         }
414       }
415
416       runAction(action, frame.getCurrentView(), arguments,
417           Credentials.empty());
418     }
419
420     void appendTo(JMenu menu)
421     {
422       menu.add(serviceLabel);
423       menu.add(urlItem);
424       menu.add(serviceItem);
425       menu.add(editParamsItem);
426       menu.add(presetsMenu);
427       menu.add(alternativesMenu);
428     }
429   }
430
431
432   private void runAction(ActionI<?> action, AlignmentViewport viewport,
433       List<ArgumentI> args, Credentials credentials)
434   {
435     // casting and instance checks can be avoided with some effort,
436     // let them be for now.
437     if (action instanceof AlignmentAction)
438     {
439       // TODO: test if selection contains enough sequences
440       var _action = (AlignmentAction) action;
441       var handler = new AlignmentServiceGuiHandler(_action, frame);
442       BaseTask<?, AlignmentResult> task = _action.createTask(viewport, args, credentials);
443       var executor = PollingTaskExecutor.fromPool(viewport.getServiceExecutor());
444       task.addTaskEventListener(handler);
445       var future = executor.submit(task);
446       task.setCancelAction(() -> { future.cancel(true); });
447       return;
448     }
449     if (action instanceof AnnotationAction)
450     {
451       var calcManager = viewport.getCalcManager();
452
453       var _action = (AnnotationAction) action;
454       var worker = new AlignCalcWorkerAdapter(viewport, frame.alignPanel,
455           _action, args, credentials);
456       var handler = new AnnotationServiceGuiHandler(_action, frame);
457       worker.setWorkerListener(handler);
458       for (var w : calcManager.getWorkers())
459       {
460         if (worker.getCalcName() != null && worker.getCalcName().equals(w.getCalcName()))
461         {
462           calcManager.cancelWorker(w);
463           calcManager.removeWorker(w);
464         }
465       }
466       if (action.getWebService().isInteractive())
467         calcManager.registerWorker(worker);
468       else
469         calcManager.startWorker(worker);
470       return;
471     }
472     if (action instanceof PhmmerAction)
473     {
474       var _action = (PhmmerAction) action;
475       var handler = new SearchServiceGuiHandler(_action, frame);
476       TaskI<AlignmentI> task = _action.createTask(viewport, args, credentials);
477       var executor = PollingTaskExecutor.fromPool(viewport.getServiceExecutor());
478       task.addTaskEventListener(handler);
479       _action.perform(viewport, args, credentials, handler);
480       return;
481     }
482     Console.warn(String.format(
483             "No known handler for action type %s. All output will be discarded.",
484             action.getClass().getName()));
485     var task = action.createTask(viewport, args, credentials);
486     task.addTaskEventListener(TaskEventListener.nullListener());
487     PollingTaskExecutor.fromPool(viewport.getServiceExecutor())
488       .submit(task);
489   }
490
491   private static CompletionStage<List<ArgumentI>> openEditParamsDialog(
492       ParamDatastoreI paramStore, WsParamSetI preset, List<ArgumentI> arguments)
493   {
494     final WsJobParameters jobParams;
495     if (preset == null && arguments != null && arguments.size() > 0)
496       jobParams = new WsJobParameters(paramStore, null, arguments);
497     else
498       jobParams = new WsJobParameters(paramStore, preset, null);
499     if (preset != null)
500       jobParams.setName(MessageManager.getString(
501           "label.adjusting_parameters_for_calculation"));
502     var stage = jobParams.showRunDialog();
503     return stage.thenApply(startJob -> {
504       if (!startJob)
505         return null; // null if cancelled
506       if (jobParams.getPreset() != null)
507         return jobParams.getPreset().getArguments();
508       if (jobParams.isServiceDefaults())
509         return Collections.emptyList();
510       else
511         return jobParams.getJobParams();
512     });
513   }
514 }