JAL-3878 update branch from 2.12 merge from 2.11.2
[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   /**
96    * Get the key in jalview property file where the urls for this discoverer are
97    * stored. Return null if modifying urls is not supported.
98    * 
99    * @return urls entry key
100    */
101   protected abstract String getUrlsPropertyKey();
102
103   /**
104    * Get the default url for web service discovery for this discoverer.
105    * 
106    * @return default discovery url
107    */
108   protected abstract URL getDefaultUrl();
109
110   @Override
111   public boolean hasServices()
112   {
113     return !isRunning() && services.size() > 0;
114   }
115
116   private static final int END = 0x01;
117   private static final int BEGIN = 0x02;
118   private static final int AGAIN = 0x04;
119   private final AtomicInteger state = new AtomicInteger(END);
120   private CompletableFuture<List<WebService<?>>> discoveryTask = new CompletableFuture<>();
121   
122   @Override
123   public boolean isRunning()
124   {
125     return (state.get() & (BEGIN | AGAIN)) != 0;
126   }
127
128   @Override
129   public boolean isDone()
130   {
131     return state.get() == END && discoveryTask.isDone();
132   }
133
134   @Override
135   public synchronized final CompletableFuture<List<WebService<?>>> startDiscoverer()
136   {
137     Console.debug("Requesting service discovery");
138     while (true)
139     {
140       if (state.get() == AGAIN)
141       {
142         return discoveryTask;
143       }
144       if (state.compareAndSet(END, BEGIN) || state.compareAndSet(BEGIN, AGAIN))
145       {
146         Console.debug("State changed to " + state.get());
147         final var oldTask = discoveryTask;
148         CompletableFuture<List<WebService<?>>> task = oldTask
149             .handleAsync((_r, _e) -> {
150               Console.info("Reloading services for " + this);
151               fireServicesChanged(services = Collections.emptyList());
152               var allServices = new ArrayList<WebService<?>>();
153               for (var url : getUrls())
154               {
155                 Console.info("Fetching list of services from " + url);
156                 try
157                 {
158                   allServices.addAll(fetchServices(url));
159                 }
160                 catch (IOException e)
161                 {
162                   Console.error("Failed to get services from " + url, e);
163                 }
164               }
165               return services = allServices;
166             });
167         task.<Void>handle((services, exception) -> {
168           while (true)
169           {
170             if (state.get() == END)
171               // should never happen, throw exception to break the loop just in case
172               throw new AssertionError();
173             if (state.compareAndSet(BEGIN, END) || state.compareAndSet(AGAIN, BEGIN))
174               Console.debug("Discovery ended, state is " + state.get());
175               break;
176           }
177           if (services != null)
178             fireServicesChanged(services);
179           return null;
180         });
181         Console.debug("Spawned task " + task);
182         Console.debug("Killing task " + oldTask);
183         oldTask.cancel(false);
184         return discoveryTask = task;
185       }
186     }
187   }
188   
189   protected abstract List<WebService<?>> fetchServices(URL url) throws IOException;
190   
191   private List<ServicesChangeListener> listeners = new ArrayList<>();
192   
193   private void fireServicesChanged(List<WebService<?>> services)
194   {
195     for (var listener : listeners)
196     {
197       try
198       {
199         listener.servicesChanged(this, services);
200       }
201       catch (Exception e)
202       {
203         Console.warn("Services Changed event raised an exception",e);
204       }
205     }
206   }
207
208   @Override
209   public final void addServicesChangeListener(ServicesChangeListener listener)
210   {
211     listeners.add(listener);
212   }
213
214   @Override
215   public final void removeServicesChangeListener(ServicesChangeListener listener)
216   {
217     listeners.remove(listener);
218   }
219
220   @Override
221   public String toString()
222   {
223     return getClass().getName();
224   }
225 }