JAL-3878 Create web service menu manager.
[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.net.URL;
7 import java.util.ArrayList;
8 import java.util.Collection;
9 import java.util.Collections;
10 import java.util.Comparator;
11 import java.util.HashMap;
12 import java.util.HashSet;
13 import java.util.List;
14 import java.util.Map;
15 import java.util.Objects;
16 import java.util.TreeMap;
17 import java.util.concurrent.CompletionStage;
18
19 import javax.swing.JMenu;
20 import javax.swing.JMenuItem;
21 import javax.swing.ToolTipManager;
22
23 import jalview.gui.AlignFrame;
24 import jalview.gui.Desktop;
25 import jalview.gui.JvSwingUtils;
26 import jalview.gui.WsJobParameters;
27 import jalview.util.MessageManager;
28 import jalview.viewmodel.AlignmentViewport;
29 import jalview.ws.params.ArgumentI;
30 import jalview.ws.params.ParamDatastoreI;
31 import jalview.ws.params.WsParamSetI;
32 import jalview.ws2.actions.alignment.AlignmentAction;
33 import jalview.ws2.actions.api.ActionI;
34 import jalview.ws2.actions.api.TaskI;
35 import jalview.ws2.api.Credentials;
36 import jalview.ws2.api.WebService;
37 import jalview.ws2.client.api.WebServiceProviderI;
38
39 public class WebServicesMenuManager
40 {
41   private final JMenu menu;
42
43   private final AlignFrame frame;
44
45   private JMenuItem inProgressItem = new JMenuItem("Service discovery in progress");
46
47   private JMenuItem noServicesItem = new JMenuItem("No services available");
48   {
49     inProgressItem.setEnabled(false);
50     inProgressItem.setVisible(false);
51     noServicesItem.setEnabled(false);
52   }
53
54   public WebServicesMenuManager(String name, AlignFrame frame)
55   {
56     this.frame = frame;
57     menu = new JMenu(name);
58     menu.add(inProgressItem);
59     menu.add(noServicesItem);
60   }
61
62   public JMenu getMenu()
63   {
64     return menu;
65   }
66
67   public void setNoServices(boolean noServices)
68   {
69     noServicesItem.setVisible(noServices);
70   }
71
72   public void setInProgress(boolean inProgress)
73   {
74     inProgressItem.setVisible(inProgress);
75   }
76
77   public void setServices(WebServiceProviderI services)
78   {
79     menu.removeAll();
80     // services grouped by their category
81     Map<String, List<WebService<?>>> oneshotServices = new HashMap<>();
82     Map<String, List<WebService<?>>> interactiveServices = new HashMap<>();
83     for (WebService<?> service : services.getServices())
84     {
85       var map = service.isInteractive() ? interactiveServices : oneshotServices;
86       map.computeIfAbsent(service.getCategory(), k -> new ArrayList<>())
87           .add(service);
88     }
89     var allKeysSet = new HashSet<>(oneshotServices.keySet());
90     allKeysSet.addAll(interactiveServices.keySet());
91     var allKeys = new ArrayList<>(allKeysSet);
92     allKeys.sort(Comparator.naturalOrder());
93     for (String category : allKeys)
94     {
95       var categoryMenu = new JMenu(category);
96       var oneshot = oneshotServices.get(category);
97       if (oneshot != null)
98         addOneshotEntries(oneshot, categoryMenu);
99       var interactive = interactiveServices.get(category);
100       if (interactive != null)
101       {
102         if (oneshot != null)
103           categoryMenu.addSeparator();
104         // addInteractiveEntries(interactive, categoryMenu);
105       }
106       menu.add(categoryMenu);
107     }
108     menu.add(inProgressItem);
109     menu.add(noServicesItem);
110   }
111
112   private void addOneshotEntries(List<WebService<?>> services, JMenu menu)
113   {
114     services.sort(Comparator
115         .<WebService<?>, String> comparing(s -> s.getUrl().toString())
116         .thenComparing(WebService::getName));
117     URL lastHost = null;
118     for (WebService<?> service : services)
119     {
120       // if new host differs from the last one, add entry separating them
121       URL host = service.getUrl();
122       if (!host.equals(lastHost))
123       {
124         if (lastHost != null)
125           menu.addSeparator();
126         var item = new JMenuItem(host.toString());
127         item.setForeground(Color.BLUE);
128         item.addActionListener(e -> Desktop.showUrl(host.toString()));
129         menu.add(item);
130         lastHost = host;
131       }
132       menu.addSeparator();
133       // group actions by their subcategory, sorted
134       var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
135       for (ActionI<?> action : service.getActions())
136       {
137         actionsByCategory
138             .computeIfAbsent(
139                 Objects.requireNonNullElse(action.getSubcategory(), ""),
140                 k -> new ArrayList<>())
141             .add(action);
142       }
143       actionsByCategory.forEach((k, v) -> {
144         // create submenu named {subcategory} with {service} or use root menu
145         var atMenu = k.isEmpty() ? menu : new JMenu(String.format("%s with %s", k, service.getName()));
146         if (atMenu != menu)
147           menu.add(atMenu); // add only if submenu
148         // sort actions by name pulling nulls to the front
149         v.sort(Comparator.comparing(
150             ActionI::getName, Comparator.nullsFirst(Comparator.naturalOrder())));
151         for (ActionI<?> action : v)
152         {
153           addEntriesForAction(action, atMenu, atMenu == menu);
154         }
155       });
156     }
157   }
158
159   private void addEntriesForAction(ActionI<?> action, JMenu menu, boolean isTopLevel)
160   {
161     var service = action.getWebService();
162     String itemName;
163     if (isTopLevel)
164     {
165       itemName = service.getName();
166       if (action.getName() != null && !action.getName().isEmpty())
167         itemName += " " + action.getName();
168     }
169     else
170     {
171       if (action.getName() == null || action.getName().isEmpty())
172         itemName = "Run";
173       else
174         itemName = action.getName();
175     }
176     var datastore = service.getParamDatastore();
177     {
178       String text = itemName;
179       if (datastore.hasParameters() || datastore.hasPresets())
180         text += "with defaults";
181       JMenuItem item = new JMenuItem(text);
182       item.addActionListener(e -> {
183         runAction(action, frame.getCurrentView(), Collections.emptyList(),
184             Credentials.empty());
185       });
186       menu.add(item);
187     }
188     if (datastore.hasParameters())
189     {
190       JMenuItem item = new JMenuItem("Edit settings and run...");
191       item.addActionListener(e -> {
192         openEditParamsDialog(datastore, null, null).thenAccept(args -> {
193           if (args != null)
194             runAction(action, frame.getCurrentView(), args, Credentials.empty());
195         });
196       });
197       menu.add(item);
198     }
199     var presets = datastore.getPresets();
200     if (presets != null && presets.size() > 0)
201     {
202       final var presetsMenu = new JMenu(MessageManager.formatMessage(
203           "label.run_with_preset_params", service.getName()));
204       final int dismissDelay = ToolTipManager.sharedInstance()
205           .getDismissDelay();
206       final int QUICK_TOOLTIP = 1500;
207       for (var preset : presets)
208       {
209         var item = new JMenuItem(preset.getName());
210         item.addMouseListener(new MouseAdapter()
211         {
212           @Override
213           public void mouseEntered(MouseEvent evt)
214           {
215             ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
216           }
217
218           @Override
219           public void mouseExited(MouseEvent evt)
220           {
221             ToolTipManager.sharedInstance().setDismissDelay(dismissDelay);
222           }
223         });
224         String tooltipTitle = MessageManager.getString(
225             preset.isModifiable() ? "label.user_preset" : "label.service_preset");
226         String tooltip = String.format("<strong>%s</strong><br/>%s",
227             tooltipTitle, preset.getDescription());
228         tooltip = JvSwingUtils.wrapTooltip(true, tooltip);
229         item.setToolTipText(tooltip);
230         item.addActionListener(event -> {
231           runAction(action, frame.getCurrentView(), preset.getArguments(),
232               Credentials.empty());
233         });
234         presetsMenu.add(item);
235       }
236       menu.add(presetsMenu);
237     }
238   }
239
240   private TaskI<?> runAction(ActionI<?> action, AlignmentViewport viewport,
241       List<ArgumentI> args, Credentials credentials)
242   {
243     // casting and instance checks can be avoided with some effort,
244     // let them be for now.
245     if (action instanceof AlignmentAction)
246     {
247       // TODO: test if selection contains enough sequences
248       var _action = (AlignmentAction) action;
249       var handler = new AlignmentServiceGuiHandler(_action, frame);
250       return _action.perform(viewport, args, credentials, handler);
251     }
252     throw new IllegalArgumentException(
253         String.format("Illegal action type %s", action.getClass().getName()));
254   }
255
256   private static CompletionStage<List<ArgumentI>> openEditParamsDialog(
257       ParamDatastoreI paramStore, WsParamSetI preset, List<ArgumentI> arguments)
258   {
259     final WsJobParameters jobParams;
260     if (preset == null && arguments != null && arguments.size() > 0)
261       jobParams = new WsJobParameters(paramStore, preset, arguments);
262     else
263       jobParams = new WsJobParameters(paramStore, preset, null);
264     if (preset != null)
265       jobParams.setName(MessageManager.getString(
266           "label.adjusting_parameters_for_calculation"));
267     var stage = jobParams.showRunDialog();
268     return stage.thenApply(startJob -> {
269       if (!startJob)
270         return null; // null if cancelled
271       if (jobParams.getPreset() != null)
272         return jobParams.getPreset().getArguments();
273       if (jobParams.isServiceDefaults())
274         return Collections.emptyList();
275       else
276         return jobParams.getJobParams();
277     });
278   }
279 }