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