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