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