--- /dev/null
+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();
+ }
+}