JAL-3954 Create fallback for handling unknown actions
[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.lang.ref.WeakReference;
7 import java.net.URL;
8 import java.util.ArrayList;
9 import java.util.Collection;
10 import java.util.Collections;
11 import java.util.Comparator;
12 import java.util.HashMap;
13 import java.util.HashSet;
14 import java.util.List;
15 import java.util.Map;
16 import java.util.Objects;
17 import java.util.TreeMap;
18 import java.util.concurrent.CompletionStage;
19
20 import javax.swing.JCheckBoxMenuItem;
21 import javax.swing.JLabel;
22 import javax.swing.JMenu;
23 import javax.swing.JMenuItem;
24 import javax.swing.ToolTipManager;
25 import javax.swing.border.EmptyBorder;
26
27 import jalview.bin.Console;
28 import jalview.gui.AlignFrame;
29 import jalview.gui.Desktop;
30 import jalview.gui.JvSwingUtils;
31 import jalview.gui.WsJobParameters;
32 import jalview.util.MessageManager;
33 import jalview.viewmodel.AlignmentViewport;
34 import jalview.ws.params.ArgumentI;
35 import jalview.ws.params.ParamDatastoreI;
36 import jalview.ws.params.WsParamSetI;
37 import jalview.ws2.actions.alignment.AlignmentAction;
38 import jalview.ws2.actions.annotation.AnnotationAction;
39 import jalview.ws2.actions.api.ActionI;
40 import jalview.ws2.actions.api.TaskEventListener;
41 import jalview.ws2.actions.api.TaskI;
42 import jalview.ws2.api.Credentials;
43 import jalview.ws2.api.WebService;
44 import jalview.ws2.client.api.WebServiceProviderI;
45
46 import static java.lang.String.format;
47
48 public class WebServicesMenuManager
49 {
50   private final JMenu menu;
51
52   private final AlignFrame frame;
53
54   private JMenuItem inProgressItem = new JMenuItem("Service discovery in progress");
55
56   private JMenuItem noServicesItem = new JMenuItem("No services available");
57   {
58     inProgressItem.setEnabled(false);
59     inProgressItem.setVisible(false);
60     noServicesItem.setEnabled(false);
61   }
62
63   private Map<String, WeakReference<TaskI<?>>> interactiveTasks = new HashMap<>();
64
65   public WebServicesMenuManager(String name, AlignFrame frame)
66   {
67     this.frame = frame;
68     menu = new JMenu(name);
69     menu.add(inProgressItem);
70     menu.add(noServicesItem);
71   }
72
73   public JMenu getMenu()
74   {
75     return menu;
76   }
77
78   public void setNoServices(boolean noServices)
79   {
80     noServicesItem.setVisible(noServices);
81   }
82
83   public void setInProgress(boolean inProgress)
84   {
85     inProgressItem.setVisible(inProgress);
86   }
87
88   public void setServices(WebServiceProviderI services)
89   {
90     menu.removeAll();
91     // services grouped by their category
92     Map<String, List<WebService<?>>> oneshotServices = new HashMap<>();
93     Map<String, List<WebService<?>>> interactiveServices = new HashMap<>();
94     for (WebService<?> service : services.getServices())
95     {
96       var map = service.isInteractive() ? interactiveServices : oneshotServices;
97       map.computeIfAbsent(service.getCategory(), k -> new ArrayList<>())
98           .add(service);
99     }
100     var allKeysSet = new HashSet<>(oneshotServices.keySet());
101     allKeysSet.addAll(interactiveServices.keySet());
102     var allKeys = new ArrayList<>(allKeysSet);
103     allKeys.sort(Comparator.naturalOrder());
104     for (String category : allKeys)
105     {
106       var categoryMenu = new JMenu(category);
107       var oneshot = oneshotServices.get(category);
108       if (oneshot != null)
109         addOneshotEntries(oneshot, categoryMenu);
110       var interactive = interactiveServices.get(category);
111       if (interactive != null)
112       {
113         if (oneshot != null)
114           categoryMenu.addSeparator();
115         addInteractiveEntries(interactive, categoryMenu);
116       }
117       menu.add(categoryMenu);
118     }
119     menu.add(inProgressItem);
120     menu.add(noServicesItem);
121   }
122
123   private void addOneshotEntries(List<WebService<?>> services, JMenu menu)
124   {
125     // Workaround. Comparator methods not working in j2s
126     services.sort((ws1, ws2) -> {
127       var res = ws1.getUrl().toString().compareTo(ws2.getUrl().toString());
128       if (res == 0)
129         res = ws1.getName().compareTo(ws2.getName());
130       return res;
131     });
132     URL lastHost = null;
133     for (WebService<?> service : services)
134     {
135       // if new host differs from the last one, add entry separating them
136       URL host = service.getUrl();
137       if (!host.equals(lastHost))
138       {
139         if (lastHost != null)
140           menu.addSeparator();
141         var item = new JMenuItem(host.toString());
142         item.setForeground(Color.BLUE);
143         item.addActionListener(e -> Desktop.showUrl(host.toString()));
144         menu.add(item);
145         lastHost = host;
146       }
147       menu.addSeparator();
148       // group actions by their subcategory, sorted
149       var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
150       for (ActionI<?> action : service.getActions())
151       {
152         actionsByCategory
153             .computeIfAbsent(
154                 action.getSubcategory() != null ? action.getSubcategory() : "",
155                 k -> new ArrayList<>())
156             .add(action);
157       }
158       for (var entry : actionsByCategory.entrySet())
159       {
160         var category = entry.getKey();
161         var actions = entry.getValue();
162         // create submenu named {subcategory} with {service} or use root menu
163         var atMenu = category.isEmpty() ? menu : new JMenu(String.format("%s with %s", category, service.getName()));
164         if (atMenu != menu)
165           menu.add(atMenu); // add only if submenu
166         // sort actions by name pulling nulls to the front
167         actions.sort(Comparator.comparing(
168             ActionI::getName, Comparator.nullsFirst(Comparator.naturalOrder())));
169         for (int i = 0; i < actions.size(); i++) {
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         if (serviceItem.getState())
373         {
374           cancelAndRunInteractive(action, frame.getCurrentView(), arguments,
375               Credentials.empty());
376         }
377         else
378         {
379           cancelInteractive(service.getName());
380         }
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                   cancelAndRunInteractive(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             cancelAndRunInteractive(action, frame.getCurrentView(),
417                 preset.getArguments(), Credentials.empty());
418           });
419           presetsMenu.add(item);
420         }
421       }
422
423       cancelAndRunInteractive(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 void cancelInteractive(String wsName)
439   {
440     var taskRef = interactiveTasks.get(wsName);
441     if (taskRef != null && taskRef.get() != null)
442       taskRef.get().cancel();
443     interactiveTasks.put(wsName, null);
444   }
445
446   private void cancelAndRunInteractive(ActionI<?> action,
447       AlignmentViewport viewport, List<ArgumentI> args, Credentials credentials)
448   {
449     var wsName = action.getWebService().getName();
450     cancelInteractive(wsName);
451     var task = runAction(action, viewport, args, credentials);
452     interactiveTasks.put(wsName, new WeakReference<>(task));
453   }
454
455   private TaskI<?> runAction(ActionI<?> action, AlignmentViewport viewport,
456       List<ArgumentI> args, Credentials credentials)
457   {
458     // casting and instance checks can be avoided with some effort,
459     // let them be for now.
460     if (action instanceof AlignmentAction)
461     {
462       // TODO: test if selection contains enough sequences
463       var _action = (AlignmentAction) action;
464       var handler = new AlignmentServiceGuiHandler(_action, frame);
465       return _action.perform(viewport, args, credentials, handler);
466     }
467     if (action instanceof AnnotationAction)
468     {
469       var _action = (AnnotationAction) action;
470       var handler = new AnnotationServiceGuiHandler(_action, frame);
471       return _action.perform(viewport, args, credentials, handler);
472     }
473     Console.warn(String.format(
474             "No known handler for action type %s. All output will be discarded.",
475             action.getClass().getName()));
476     return action.perform(viewport, args, credentials,
477             TaskEventListener.nullListener());
478   }
479
480   private static CompletionStage<List<ArgumentI>> openEditParamsDialog(
481       ParamDatastoreI paramStore, WsParamSetI preset, List<ArgumentI> arguments)
482   {
483     final WsJobParameters jobParams;
484     if (preset == null && arguments != null && arguments.size() > 0)
485       jobParams = new WsJobParameters(paramStore, null, arguments);
486     else
487       jobParams = new WsJobParameters(paramStore, preset, null);
488     if (preset != null)
489       jobParams.setName(MessageManager.getString(
490           "label.adjusting_parameters_for_calculation"));
491     var stage = jobParams.showRunDialog();
492     return stage.thenApply(startJob -> {
493       if (!startJob)
494         return null; // null if cancelled
495       if (jobParams.getPreset() != null)
496         return jobParams.getPreset().getArguments();
497       if (jobParams.isServiceDefaults())
498         return Collections.emptyList();
499       else
500         return jobParams.getJobParams();
501     });
502   }
503 }