JAL-3878 Create abstract ws discoverer providing common skeleton
authorMateusz Warowny <mmzwarowny@dundee.ac.uk>
Fri, 18 Mar 2022 16:05:36 +0000 (17:05 +0100)
committerMateusz Warowny <mmzwarowny@dundee.ac.uk>
Fri, 18 Mar 2022 16:05:36 +0000 (17:05 +0100)
src/jalview/ws2/client/api/AbstractWebServiceDiscoverer.java [new file with mode: 0644]
src/jalview/ws2/client/api/WebServiceDiscovererI.java

diff --git a/src/jalview/ws2/client/api/AbstractWebServiceDiscoverer.java b/src/jalview/ws2/client/api/AbstractWebServiceDiscoverer.java
new file mode 100644 (file)
index 0000000..a526e72
--- /dev/null
@@ -0,0 +1,217 @@
+package jalview.ws2.client.api;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import jalview.bin.Cache;
+import jalview.ws2.actions.api.ActionI;
+import jalview.ws2.api.WebService;
+
+public abstract class AbstractWebServiceDiscoverer implements WebServiceDiscovererI
+{
+  // TODO: we can use linked hash map to group and retrieve services by type.
+  protected List<WebService<?>> services = List.of();
+
+  @Override
+  public List<WebService<?>> getServices()
+  {
+    return services;
+  }
+
+  @Override
+  public <A extends ActionI<?>> List<WebService<A>> getServices(Class<A> type)
+  {
+    List<WebService<A>> list = new ArrayList<>();
+    for (WebService<?> service : services)
+    {
+      if (service.getActionClass().equals(type))
+      {
+        @SuppressWarnings("unchecked")
+        WebService<A> _service = (WebService<A>) service;
+        list.add(_service);
+      }
+    }
+    return list;
+  }
+
+  @Override
+  public List<URL> getUrls()
+  {
+    String key = getUrlsPropertyKey();
+    if (key == null)
+      // unmodifiable urls list, return default
+      return List.of(getDefaultUrl());
+    String surls = Cache.getProperty(key);
+    if (surls == null)
+      return List.of(getDefaultUrl());
+    String[] urls = surls.split(",");
+    ArrayList<URL> valid = new ArrayList<>(urls.length);
+    for (String url : urls)
+    {
+      try
+      {
+        valid.add(new URL(url));
+      } catch (MalformedURLException e)
+      {
+        Cache.log.warn(String.format(
+            "Problem whilst trying to make a URL from '%s'. " +
+                "This was probably due to malformed comma-separated-list " +
+                "in the %s entry of ${HOME}/.jalview-properties",
+            Objects.toString(url, "<null>"), key));
+        Cache.log.debug("Exception occurred while reading url list", e);
+      }
+    }
+    return valid;
+  }
+
+  @Override
+  public void setUrls(List<URL> wsUrls)
+  {
+    String key = getUrlsPropertyKey();
+    if (key == null)
+      throw new UnsupportedOperationException("setting urls not supported");
+    if (wsUrls != null && !wsUrls.isEmpty())
+    {
+      String[] surls = new String[wsUrls.size()];
+      var iter = wsUrls.iterator(); 
+      for (int i = 0; iter.hasNext(); i++)
+        surls[i] = iter.next().toString();
+      Cache.setProperty(key, String.join(",", surls));
+    }
+    else
+    {
+      Cache.removeProperty(key);
+    }
+  }
+
+  /**
+   * Get the key in jalview property file where the urls for this discoverer are
+   * stored. Return null if modifying urls is not supported.
+   * 
+   * @return urls entry key
+   */
+  protected abstract String getUrlsPropertyKey();
+
+  /**
+   * Get the default url for web service discovery for this discoverer.
+   * 
+   * @return default discovery url
+   */
+  protected abstract URL getDefaultUrl();
+
+  @Override
+  public boolean hasServices()
+  {
+    return !isRunning() && services.size() > 0;
+  }
+
+  private static final int END = 0x01;
+  private static final int BEGIN = 0x02;
+  private static final int AGAIN = 0x04;
+  private final AtomicInteger state = new AtomicInteger(END);
+  private CompletableFuture<List<WebService<?>>> discoveryTask = new CompletableFuture<>();
+  
+  @Override
+  public boolean isRunning()
+  {
+    return (state.get() & (BEGIN | AGAIN)) != 0;
+  }
+
+  @Override
+  public boolean isDone()
+  {
+    return state.get() == END && discoveryTask.isDone();
+  }
+
+  @Override
+  public synchronized final CompletableFuture<List<WebService<?>>> startDiscoverer()
+  {
+    while (true)
+    {
+      if (state.get() == AGAIN)
+      {
+        return discoveryTask;
+      }
+      if (state.compareAndSet(END, BEGIN) || state.compareAndSet(BEGIN, AGAIN))
+      {
+        final var oldTask = discoveryTask;
+        CompletableFuture<List<WebService<?>>> task = oldTask
+            .handleAsync((_r, _e) -> {
+              Cache.log.info("Reloading services for " + this);
+              fireServicesChanged(Collections.emptyList());
+              var allServices = new ArrayList<WebService<?>>();
+              for (var url : getUrls())
+              {
+                Cache.log.info("Fetching list of services from " + url);
+                try
+                {
+                  allServices.addAll(getServices(url));
+                }
+                catch (IOException e)
+                {
+                  Cache.log.error("Failed to get services from " + url, e);
+                }
+              }
+              return services = allServices;
+            });
+        task.thenAccept(services -> {
+          while (true)
+          {
+            if (state.get() == END)
+              // should never happen, throw exception to break the loop just in case
+              throw new AssertionError();
+            if (state.compareAndSet(BEGIN, END) || state.compareAndSet(AGAIN, BEGIN))
+              break;
+          }
+          fireServicesChanged(services);
+        });
+        oldTask.cancel(false);
+        return discoveryTask = task;
+      }
+    }
+  }
+  
+  protected abstract List<WebService<?>> getServices(URL url) throws IOException;
+  
+  private List<ServicesChangeListener> listeners = new ArrayList<>();
+  
+  private void fireServicesChanged(List<WebService<?>> services)
+  {
+    for (var listener : listeners)
+    {
+      try
+      {
+        listener.servicesChanged(this, services);
+      }
+      catch (Exception e)
+      {
+        Cache.log.warn(e);
+      }
+    }
+  }
+
+  @Override
+  public final void addServicesChangeListener(ServicesChangeListener listener)
+  {
+    listeners.add(listener);
+  }
+
+  @Override
+  public final void removeServicesChangeListener(ServicesChangeListener listener)
+  {
+    listeners.remove(listener);
+  }
+
+  @Override
+  public String toString()
+  {
+    return getClass().getName();
+  }
+}
index 951c86f..4a09ef7 100644 (file)
@@ -79,7 +79,7 @@ public interface WebServiceDiscovererI extends WebServiceProviderI
    * 
    * @return services list future result
    */
-  CompletableFuture<List<WebService>> startDiscoverer();
+  CompletableFuture<List<WebService<?>>> startDiscoverer();
 
   /**
    * An interface for the services list observers.
@@ -99,7 +99,7 @@ public interface WebServiceDiscovererI extends WebServiceProviderI
      * @param list
      */
     public void servicesChanged(WebServiceDiscovererI discoverer,
-        List<WebService> services);
+        List<WebService<?>> services);
   }
 
   /**