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