JAL-4131 Replace usages of requireNonNullElse
[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     services.sort(Comparator
124         .<WebService<?>, String> comparing(s -> s.getUrl().toString())
125         .thenComparing(WebService::getName));
126     URL lastHost = null;
127     for (WebService<?> service : services)
128     {
129       // if new host differs from the last one, add entry separating them
130       URL host = service.getUrl();
131       if (!host.equals(lastHost))
132       {
133         if (lastHost != null)
134           menu.addSeparator();
135         var item = new JMenuItem(host.toString());
136         item.setForeground(Color.BLUE);
137         item.addActionListener(e -> Desktop.showUrl(host.toString()));
138         menu.add(item);
139         lastHost = host;
140       }
141       menu.addSeparator();
142       // group actions by their subcategory, sorted
143       var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
144       for (ActionI<?> action : service.getActions())
145       {
146         actionsByCategory
147             .computeIfAbsent(
148                 action.getSubcategory() != null ? action.getSubcategory() : "",
149                 k -> new ArrayList<>())
150             .add(action);
151       }
152       actionsByCategory.forEach((k, v) -> {
153         // create submenu named {subcategory} with {service} or use root menu
154         var atMenu = k.isEmpty() ? menu : new JMenu(String.format("%s with %s", k, service.getName()));
155         if (atMenu != menu)
156           menu.add(atMenu); // add only if submenu
157         // sort actions by name pulling nulls to the front
158         v.sort(Comparator.comparing(
159             ActionI::getName, Comparator.nullsFirst(Comparator.naturalOrder())));
160         for (int i = 0; i < v.size(); i++) {
161           addEntriesForAction(v.get(i), atMenu, atMenu == menu);
162         }
163       });
164     }
165   }
166
167   private void addEntriesForAction(ActionI<?> action, JMenu menu, boolean isTopLevel)
168   {
169     var service = action.getWebService();
170     String itemName;
171     if (isTopLevel)
172     {
173       itemName = service.getName();
174       if (action.getName() != null && !action.getName().isEmpty())
175         itemName += " " + action.getName();
176     }
177     else
178     {
179       if (action.getName() == null || action.getName().isEmpty())
180         itemName = "Run";
181       else
182         itemName = action.getName();
183     }
184     var datastore = service.getParamDatastore();
185     {
186       String text = itemName;
187       if (datastore.hasParameters() || datastore.hasPresets())
188         text += " with defaults";
189       JMenuItem item = new JMenuItem(text);
190       item.addActionListener(e -> {
191         runAction(action, frame.getCurrentView(), Collections.emptyList(),
192             Credentials.empty());
193       });
194       menu.add(item);
195     }
196     if (datastore.hasParameters())
197     {
198       JMenuItem item = new JMenuItem("Edit settings and run...");
199       item.addActionListener(e -> {
200         openEditParamsDialog(datastore, null, null).thenAccept(args -> {
201           if (args != null)
202             runAction(action, frame.getCurrentView(), args, Credentials.empty());
203         });
204       });
205       menu.add(item);
206     }
207     var presets = datastore.getPresets();
208     if (presets != null && presets.size() > 0)
209     {
210       final var presetsMenu = new JMenu(MessageManager.formatMessage(
211           "label.run_with_preset_params", service.getName()));
212       final int dismissDelay = ToolTipManager.sharedInstance()
213           .getDismissDelay();
214       final int QUICK_TOOLTIP = 1500;
215       for (var preset : presets)
216       {
217         var item = new JMenuItem(preset.getName());
218         item.addMouseListener(new MouseAdapter()
219         {
220           @Override
221           public void mouseEntered(MouseEvent evt)
222           {
223             ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
224           }
225
226           @Override
227           public void mouseExited(MouseEvent evt)
228           {
229             ToolTipManager.sharedInstance().setDismissDelay(dismissDelay);
230           }
231         });
232         String tooltipTitle = MessageManager.getString(
233             preset.isModifiable() ? "label.user_preset" : "label.service_preset");
234         String tooltip = String.format("<strong>%s</strong><br/>%s",
235             tooltipTitle, preset.getDescription());
236         tooltip = JvSwingUtils.wrapTooltip(true, tooltip);
237         item.setToolTipText(tooltip);
238         item.addActionListener(event -> {
239           runAction(action, frame.getCurrentView(), preset.getArguments(),
240               Credentials.empty());
241         });
242         presetsMenu.add(item);
243       }
244       menu.add(presetsMenu);
245     }
246   }
247
248   private void addInteractiveEntries(List<WebService<?>> services, JMenu menu)
249   {
250     Map<String, List<WebService<?>>> byServiceName = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
251     for (var service : services)
252     {
253       byServiceName.computeIfAbsent(service.getName(), k -> new ArrayList<>())
254           .add(service);
255     }
256     for (var entry : byServiceName.entrySet())
257     {
258       var group = new InteractiveServiceEntryGroup(entry.getKey(), entry.getValue());
259       group.appendTo(menu);
260     }
261   }
262
263   private class InteractiveServiceEntryGroup
264   {
265     JLabel serviceLabel;
266
267     JMenuItem urlItem = new JMenuItem();
268
269     JCheckBoxMenuItem serviceItem = new JCheckBoxMenuItem();
270
271     JMenuItem editParamsItem = new JMenuItem("Edit parameters...");
272
273     JMenu presetsMenu = new JMenu("Change preset");
274
275     JMenu alternativesMenu = new JMenu("Choose action");
276     {
277       urlItem.setForeground(Color.BLUE);
278       urlItem.setVisible(false);
279       serviceItem.setVisible(false);
280       editParamsItem.setVisible(false);
281       presetsMenu.setVisible(false);
282     }
283
284     InteractiveServiceEntryGroup(String name, List<WebService<?>> services)
285     {
286       serviceLabel = new JLabel(name);
287       serviceLabel.setBorder(new EmptyBorder(0, 6, 0, 6));
288       buildAlternativesMenu(services);
289     }
290
291     private void buildAlternativesMenu(List<WebService<?>> services)
292     {
293       var menu = alternativesMenu;
294       services.sort(Comparator
295           .<WebService<?>, String> comparing(s -> s.getUrl().toString())
296           .thenComparing(s -> s.getName()));
297       URL lastHost = null;
298       for (var service : services)
299       {
300         // Adding url "separator" before each group
301         URL host = service.getUrl();
302         if (!host.equals(lastHost))
303         {
304           if (lastHost != null)
305             menu.addSeparator();
306           var item = new JMenuItem(host.toString());
307           item.setForeground(Color.BLUE);
308           item.addActionListener(e -> Desktop.showUrl(host.toString()));
309           menu.add(item);
310           lastHost = host;
311         }
312         menu.addSeparator();
313         var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
314         for (ActionI<?> action : service.getActions())
315         {
316           actionsByCategory
317               .computeIfAbsent(
318                   action.getSubcategory() != null ? action.getSubcategory() : "",
319                   k -> new ArrayList<>())
320               .add(action);
321         }
322         actionsByCategory.forEach((key, actions) -> {
323           var atMenu = key.isEmpty() ? menu : new JMenu(key + " with " + service.getName());
324           boolean topLevel = atMenu == menu;
325           if (!topLevel)
326             menu.add(atMenu);
327           actions.sort(Comparator.comparing(
328               a -> a.getName(),
329               Comparator.nullsFirst(Comparator.naturalOrder())));
330           for (ActionI<?> action : actions)
331           {
332             var item = new JMenuItem(action.getFullName());
333             item.addActionListener(e -> setAlternative(action));
334             atMenu.add(item);
335           }
336         });
337       }
338     }
339
340     private void setAlternative(ActionI<?> action)
341     {
342       final var arguments = new ArrayList<ArgumentI>();
343       final WsParamSetI[] lastPreset = { null };
344
345       // update selected url menu item
346       String url = action.getWebService().getUrl().toString();
347       urlItem.setText(url);
348       urlItem.setVisible(true);
349       for (var l : urlItem.getActionListeners())
350         urlItem.removeActionListener(l);
351       urlItem.addActionListener(e -> Desktop.showUrl(url));
352
353       // update selected service menu item
354       serviceItem.setText(action.getFullName());
355       serviceItem.setVisible(true);
356       for (var l : serviceItem.getActionListeners())
357         serviceItem.removeActionListener(l);
358       WebService<?> service = action.getWebService();
359       serviceItem.addActionListener(e -> {
360         if (serviceItem.getState())
361         {
362           cancelAndRunInteractive(action, frame.getCurrentView(), arguments,
363               Credentials.empty());
364         }
365         else
366         {
367           cancelInteractive(service.getName());
368         }
369       });
370       serviceItem.setSelected(true);
371
372       // update edit parameters menu item
373       var datastore = service.getParamDatastore();
374       editParamsItem.setVisible(datastore.hasParameters());
375       for (var l : editParamsItem.getActionListeners())
376         editParamsItem.removeActionListener(l);
377       if (datastore.hasParameters())
378       {
379         editParamsItem.addActionListener(e -> {
380           openEditParamsDialog(service.getParamDatastore(), lastPreset[0], arguments)
381               .thenAccept(args -> {
382                 if (args != null)
383                 {
384                   lastPreset[0] = null;
385                   arguments.clear();
386                   arguments.addAll(args);
387                   cancelAndRunInteractive(action, frame.getCurrentView(),
388                       arguments, Credentials.empty());
389                 }
390               });
391         });
392       }
393
394       // update presets menu
395       presetsMenu.removeAll();
396       presetsMenu.setEnabled(datastore.hasPresets());
397       if (datastore.hasPresets())
398       {
399         for (WsParamSetI preset : datastore.getPresets())
400         {
401           var item = new JMenuItem(preset.getName());
402           item.addActionListener(e -> {
403             lastPreset[0] = preset;
404             cancelAndRunInteractive(action, frame.getCurrentView(),
405                 preset.getArguments(), Credentials.empty());
406           });
407           presetsMenu.add(item);
408         }
409       }
410
411       cancelAndRunInteractive(action, frame.getCurrentView(), arguments,
412           Credentials.empty());
413     }
414
415     void appendTo(JMenu menu)
416     {
417       menu.add(serviceLabel);
418       menu.add(urlItem);
419       menu.add(serviceItem);
420       menu.add(editParamsItem);
421       menu.add(presetsMenu);
422       menu.add(alternativesMenu);
423     }
424   }
425
426   private void cancelInteractive(String wsName)
427   {
428     var taskRef = interactiveTasks.get(wsName);
429     if (taskRef != null && taskRef.get() != null)
430       taskRef.get().cancel();
431     interactiveTasks.put(wsName, null);
432   }
433
434   private void cancelAndRunInteractive(ActionI<?> action,
435       AlignmentViewport viewport, List<ArgumentI> args, Credentials credentials)
436   {
437     var wsName = action.getWebService().getName();
438     cancelInteractive(wsName);
439     var task = runAction(action, viewport, args, credentials);
440     interactiveTasks.put(wsName, new WeakReference<>(task));
441   }
442
443   private TaskI<?> runAction(ActionI<?> action, AlignmentViewport viewport,
444       List<ArgumentI> args, Credentials credentials)
445   {
446     // casting and instance checks can be avoided with some effort,
447     // let them be for now.
448     if (action instanceof AlignmentAction)
449     {
450       // TODO: test if selection contains enough sequences
451       var _action = (AlignmentAction) action;
452       var handler = new AlignmentServiceGuiHandler(_action, frame);
453       return _action.perform(viewport, args, credentials, handler);
454     }
455     if (action instanceof AnnotationAction)
456     {
457       var _action = (AnnotationAction) action;
458       var handler = new AnnotationServiceGuiHandler(_action, frame);
459       return _action.perform(viewport, args, credentials, handler);
460     }
461     throw new IllegalArgumentException(
462         String.format("Illegal action type %s", action.getClass().getName()));
463   }
464
465   private static CompletionStage<List<ArgumentI>> openEditParamsDialog(
466       ParamDatastoreI paramStore, WsParamSetI preset, List<ArgumentI> arguments)
467   {
468     final WsJobParameters jobParams;
469     if (preset == null && arguments != null && arguments.size() > 0)
470       jobParams = new WsJobParameters(paramStore, null, arguments);
471     else
472       jobParams = new WsJobParameters(paramStore, preset, null);
473     if (preset != null)
474       jobParams.setName(MessageManager.getString(
475           "label.adjusting_parameters_for_calculation"));
476     var stage = jobParams.showRunDialog();
477     return stage.thenApply(startJob -> {
478       if (!startJob)
479         return null; // null if cancelled
480       if (jobParams.getPreset() != null)
481         return jobParams.getPreset().getArguments();
482       if (jobParams.isServiceDefaults())
483         return Collections.emptyList();
484       else
485         return jobParams.getJobParams();
486     });
487   }
488 }