JAL-4389 Disable url editor if urls are not modifiable
[jalview.git] / src / jalview / ws2 / client / api / AbstractWebServiceDiscoverer.java
1 package jalview.ws2.client.api;
2
3 import java.io.IOException;
4 import java.net.MalformedURLException;
5 import java.net.URL;
6 import java.util.ArrayList;
7 import java.util.Collections;
8 import java.util.List;
9 import java.util.Objects;
10 import java.util.concurrent.CompletableFuture;
11 import java.util.concurrent.atomic.AtomicInteger;
12
13 import jalview.bin.Cache;
14 import jalview.bin.Console;
15 import jalview.ws2.actions.api.ActionI;
16 import jalview.ws2.api.WebService;
17
18 public abstract class AbstractWebServiceDiscoverer implements WebServiceDiscovererI
19 {  
20   // TODO: we can use linked hash map to group and retrieve services by type.
21   protected List<WebService<?>> services = List.of();
22
23   @Override
24   public List<WebService<?>> getServices()
25   {
26     return services;
27   }
28
29   @Override
30   public <A extends ActionI<?>> List<WebService<A>> getServices(Class<A> type)
31   {
32     List<WebService<A>> list = new ArrayList<>();
33     for (WebService<?> service : services)
34     {
35       if (service.getActionClass().equals(type))
36       {
37         @SuppressWarnings("unchecked")
38         WebService<A> _service = (WebService<A>) service;
39         list.add(_service);
40       }
41     }
42     return list;
43   }
44
45   @Override
46   public List<URL> getUrls()
47   {
48     String key = getUrlsPropertyKey();
49     if (key == null)
50       // unmodifiable urls list, return default
51       return List.of(getDefaultUrl());
52     String surls = Cache.getProperty(key);
53     if (surls == null)
54       return List.of(getDefaultUrl());
55     String[] urls = surls.split(",");
56     ArrayList<URL> valid = new ArrayList<>(urls.length);
57     for (String url : urls)
58     {
59       try
60       {
61         valid.add(new URL(url));
62       } catch (MalformedURLException e)
63       {
64         Console.warn(String.format(
65             "Problem whilst trying to make a URL from '%s'. " +
66                 "This was probably due to malformed comma-separated-list " +
67                 "in the %s entry of ${HOME}/.jalview-properties",
68             Objects.toString(url, "<null>"), key));
69         Console.debug("Exception occurred while reading url list", e);
70       }
71     }
72     return valid;
73   }
74
75   @Override
76   public void setUrls(List<URL> wsUrls)
77   {
78     String key = getUrlsPropertyKey();
79     if (key == null)
80       throw new UnsupportedOperationException("setting urls not supported");
81     if (wsUrls != null && !wsUrls.isEmpty())
82     {
83       String[] surls = new String[wsUrls.size()];
84       var iter = wsUrls.iterator(); 
85       for (int i = 0; iter.hasNext(); i++)
86         surls[i] = iter.next().toString();
87       Cache.setProperty(key, String.join(",", surls));
88     }
89     else
90     {
91       Cache.removeProperty(key);
92     }
93   }
94
95   @Override
96   public boolean isUrlsModifiable()
97   {
98     return getUrlsPropertyKey() != null;
99   }
100
101   /**
102    * Get the key in jalview property file where the urls for this discoverer are
103    * stored. Return null if modifying urls is not supported.
104    * 
105    * @return urls entry key
106    */
107   protected abstract String getUrlsPropertyKey();
108
109   /**
110    * Get the default url for web service discovery for this discoverer.
111    * 
112    * @return default discovery url
113    */
114   protected abstract URL getDefaultUrl();
115
116   @Override
117   public boolean hasServices()
118   {
119     return !isRunning() && services.size() > 0;
120   }
121
122   private static final int END = 0x01;
123   private static final int BEGIN = 0x02;
124   private static final int AGAIN = 0x04;
125   private final AtomicInteger state = new AtomicInteger(END);
126   private CompletableFuture<List<WebService<?>>> discoveryTask = new CompletableFuture<>();
127   
128   @Override
129   public boolean isRunning()
130   {
131     return (state.get() & (BEGIN | AGAIN)) != 0;
132   }
133
134   @Override
135   public boolean isDone()
136   {
137     return state.get() == END && discoveryTask.isDone();
138   }
139
140   @Override
141   public synchronized final CompletableFuture<List<WebService<?>>> startDiscoverer()
142   {
143     Console.debug("Requesting service discovery");
144     while (true)
145     {
146       if (state.get() == AGAIN)
147       {
148         return discoveryTask;
149       }
150       if (state.compareAndSet(END, BEGIN) || state.compareAndSet(BEGIN, AGAIN))
151       {
152         Console.debug("State changed to " + state.get());
153         final var oldTask = discoveryTask;
154         CompletableFuture<List<WebService<?>>> task = oldTask
155             .handleAsync((_r, _e) -> {
156               Console.info("Reloading services for " + this);
157               fireServicesChanged(services = Collections.emptyList());
158               var allServices = new ArrayList<WebService<?>>();
159               for (var url : getUrls())
160               {
161                 Console.info("Fetching list of services from " + url);
162                 try
163                 {
164                   allServices.addAll(fetchServices(url));
165                 }
166                 catch (IOException e)
167                 {
168                   Console.error("Failed to get services from " + url, e);
169                 }
170               }
171               return services = allServices;
172             });
173         task.<Void>handle((services, exception) -> {
174           while (true)
175           {
176             if (state.get() == END)
177               // should never happen, throw exception to break the loop just in case
178               throw new AssertionError();
179             if (state.compareAndSet(BEGIN, END) || state.compareAndSet(AGAIN, BEGIN))
180               Console.debug("Discovery ended, state is " + state.get());
181               break;
182           }
183           if (services != null)
184             fireServicesChanged(services);
185           return null;
186         });
187         Console.debug("Spawned task " + task);
188         Console.debug("Killing task " + oldTask);
189         oldTask.cancel(false);
190         return discoveryTask = task;
191       }
192     }
193   }
194   
195   protected abstract List<WebService<?>> fetchServices(URL url) throws IOException;
196   
197   private List<ServicesChangeListener> listeners = new ArrayList<>();
198   
199   private void fireServicesChanged(List<WebService<?>> services)
200   {
201     for (var listener : listeners)
202     {
203       try
204       {
205         listener.servicesChanged(this, services);
206       }
207       catch (Exception e)
208       {
209         Console.warn(e.toString(), e);
210       }
211     }
212   }
213
214   @Override
215   public final void addServicesChangeListener(ServicesChangeListener listener)
216   {
217     listeners.add(listener);
218   }
219
220   @Override
221   public final void removeServicesChangeListener(ServicesChangeListener listener)
222   {
223     listeners.remove(listener);
224   }
225
226   @Override
227   public String toString()
228   {
229     return getClass().getName();
230   }
231 }