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.bin.Console; 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> services = List.of(); @Override public List> getServices() { return services; } @Override public > List> getServices(Class type) { List> list = new ArrayList<>(); for (WebService service : services) { if (service.getActionClass().equals(type)) { @SuppressWarnings("unchecked") WebService _service = (WebService) service; list.add(_service); } } return list; } @Override public List 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 valid = new ArrayList<>(urls.length); for (String url : urls) { try { valid.add(new URL(url)); } catch (MalformedURLException e) { Console.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, ""), key)); Console.debug("Exception occurred while reading url list", e); } } return valid; } @Override public void setUrls(List 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>> 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>> startDiscoverer() { Console.debug("Requesting service discovery"); while (true) { if (state.get() == AGAIN) { return discoveryTask; } if (state.compareAndSet(END, BEGIN) || state.compareAndSet(BEGIN, AGAIN)) { Console.debug("State changed to " + state.get()); final var oldTask = discoveryTask; CompletableFuture>> task = oldTask .handleAsync((_r, _e) -> { Console.info("Reloading services for " + this); fireServicesChanged(services = Collections.emptyList()); var allServices = new ArrayList>(); for (var url : getUrls()) { Console.info("Fetching list of services from " + url); try { allServices.addAll(fetchServices(url)); } catch (IOException e) { Console.error("Failed to get services from " + url, e); } } return services = allServices; }); task.handle((services, exception) -> { 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)) Console.debug("Discovery ended, state is " + state.get()); break; } if (services != null) fireServicesChanged(services); return null; }); Console.debug("Spawned task " + task); Console.debug("Killing task " + oldTask); oldTask.cancel(false); return discoveryTask = task; } } } protected abstract List> fetchServices(URL url) throws IOException; private List listeners = new ArrayList<>(); private void fireServicesChanged(List> services) { for (var listener : listeners) { try { listener.servicesChanged(this, services); } catch (Exception e) { Console.warn(e.toString(), 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(); } }