JAL3878 Implement slivka service discoverer
authorMateusz Warowny <mmzwarowny@dundee.ac.uk>
Fri, 25 Mar 2022 14:49:40 +0000 (15:49 +0100)
committerMateusz Warowny <mmzwarowny@dundee.ac.uk>
Fri, 25 Mar 2022 14:49:40 +0000 (15:49 +0100)
src/jalview/ws2/client/api/AbstractWebServiceDiscoverer.java
src/jalview/ws2/client/slivka/SlivkaParamStoreFactory.java [new file with mode: 0644]
src/jalview/ws2/client/slivka/SlivkaWSDiscoverer.java [new file with mode: 0644]

index a526e72..965abca 100644 (file)
@@ -152,7 +152,7 @@ public abstract class AbstractWebServiceDiscoverer implements WebServiceDiscover
                 Cache.log.info("Fetching list of services from " + url);
                 try
                 {
-                  allServices.addAll(getServices(url));
+                  allServices.addAll(fetchServices(url));
                 }
                 catch (IOException e)
                 {
@@ -178,7 +178,7 @@ public abstract class AbstractWebServiceDiscoverer implements WebServiceDiscover
     }
   }
   
-  protected abstract List<WebService<?>> getServices(URL url) throws IOException;
+  protected abstract List<WebService<?>> fetchServices(URL url) throws IOException;
   
   private List<ServicesChangeListener> listeners = new ArrayList<>();
   
diff --git a/src/jalview/ws2/client/slivka/SlivkaParamStoreFactory.java b/src/jalview/ws2/client/slivka/SlivkaParamStoreFactory.java
new file mode 100644 (file)
index 0000000..6ac21df
--- /dev/null
@@ -0,0 +1,224 @@
+package jalview.ws2.client.slivka;
+
+import static java.util.Objects.requireNonNullElse;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import com.stevesoft.pat.NotImplementedError;
+
+import jalview.bin.Cache;
+import jalview.ws.params.ArgumentI;
+import jalview.ws.params.ParamDatastoreI;
+import jalview.ws.params.ParamManager;
+import jalview.ws.params.WsParamSetI;
+import jalview.ws.params.simple.BooleanOption;
+import jalview.ws.params.simple.DoubleParameter;
+import jalview.ws.params.simple.IntegerParameter;
+import jalview.ws.params.simple.Option;
+import jalview.ws.params.simple.StringParameter;
+import jalview.ws2.params.SimpleParamDatastore;
+import jalview.ws2.params.SimpleParamSet;
+import uk.ac.dundee.compbio.slivkaclient.Parameter;
+import uk.ac.dundee.compbio.slivkaclient.SlivkaService;
+
+class SlivkaParamStoreFactory
+{
+  private final SlivkaService service;
+  private final ParamManager manager;
+
+  SlivkaParamStoreFactory(SlivkaService service, ParamManager manager)
+  {
+    this.service = service;
+    this.manager = manager;
+  }
+  
+  ParamDatastoreI createParamDatastore()
+  {
+    URL url = null;
+    try
+    {
+      url = service.getUrl().toURL();
+    } catch (MalformedURLException e)
+    {
+      Cache.log.warn("Invalid service url " + service.getUrl(), e);
+    }
+    List<WsParamSetI> presets = new ArrayList<>(service.getPresets().size());
+    for (var preset : service.getPresets())
+    {
+      presets.add(createPreset(preset));
+    }
+    List<ArgumentI> arguments = createPresetArguments(Collections.emptyMap());
+    return new SimpleParamDatastore(url, arguments, presets, manager);
+  }
+  
+  WsParamSetI createPreset(SlivkaService.Preset preset)
+  {
+    var builder = SimpleParamSet.newBuilder();
+    builder.name(preset.name);
+    builder.description(preset.description);
+    builder.url(service.getUrl().toString());
+    builder.modifiable(false);
+    builder.arguments(createPresetArguments(preset.values));
+    return builder.build();
+  }
+
+  List<ArgumentI> createPresetArguments(Map<String, Object> values)
+  {
+    var args = new ArrayList<ArgumentI>();
+    for (Parameter param : service.getParameters())
+    {
+      if (param instanceof Parameter.IntegerParameter)
+      {
+        args.add(createOption((Parameter.IntegerParameter) param,
+            (Integer) values.get(param.getId())));
+      }
+      else if (param instanceof Parameter.DecimalParameter)
+      {
+        args.add(createOption((Parameter.DecimalParameter) param,
+            (Double) values.get(param.getId())));
+      }
+      else if (param instanceof Parameter.TextParameter)
+      {
+        args.add(createOption((Parameter.TextParameter) param,
+            (String) values.get(param.getId())));
+      }
+      else if (param instanceof Parameter.FlagParameter)
+      {
+        args.add(createOption((Parameter.FlagParameter) param,
+            (Boolean) values.get(param.getId())));
+      }
+      else if (param instanceof Parameter.ChoiceParameter)
+      {
+        Object ovalue = values.get(param.getId());
+        List<String> lvalue = param.isArray() ? (List<String>) ovalue : List.of((String) ovalue);
+        args.addAll(createChoiceOptions((Parameter.ChoiceParameter) param, lvalue));
+      }
+      else if (param instanceof Parameter.FileParameter)
+      {
+        // args.add(createOption((Parameter.FileParameter) param, null));
+      }
+      else
+      {
+        args.add(createOption(param, values.get(param.getId())));
+      }
+    }
+    return args;
+  }
+
+  private Option createOption(Parameter.IntegerParameter param, Integer value)
+  {
+    var builder = IntegerParameter.newBuilder();
+    setCommonProperties(param, builder);
+    builder.setDefaultValue((Integer) param.getDefault());
+    builder.setValue(value);
+    builder.setBounds(param.getMin(), param.getMax());
+    return builder.build();
+  }
+
+  private Option createOption(Parameter.DecimalParameter param, Double value)
+  {
+    var builder = DoubleParameter.newBuilder();
+    setCommonProperties(param, builder);
+    builder.setDefaultValue((Double) param.getDefault());
+    builder.setValue(value);
+    builder.setBounds(param.getMin(), param.getMax());
+    return builder.build();
+  }
+
+  private Option createOption(Parameter.TextParameter param, String value)
+  {
+    var builder = StringParameter.newBuilder();
+    setCommonProperties(param, builder);
+    builder.setDefaultValue((String) param.getDefault());
+    builder.setValue(value);
+    return builder.build();
+  }
+
+  private Option createOption(Parameter.FlagParameter param, Boolean value)
+  {
+    var builder = BooleanOption.newBuilder();
+    setCommonProperties(param, builder);
+    builder.setDefaultValue((Boolean) param.getDefault());
+    builder.setValue(value);
+    return builder.build();
+  }
+
+  private List<Option> createChoiceOptions(Parameter.ChoiceParameter param, List<String> value)
+  {
+    value = requireNonNullElse(value, Collections.emptyList());
+    if (param.isArray())
+    {
+      /*
+       * Array parameter means that multiple values can be provided.
+       * Use multiple boolean checkboxes to represent the value.
+       */
+      List<Option> options = new ArrayList<>();
+      List<?> selected = requireNonNullElse(
+          (List<?>) param.getDefault(), Collections.emptyList());
+      int i = 0;
+      var builder = BooleanOption.newBuilder();
+      setCommonProperties(param, builder);
+      for (String choice : param.getChoices())
+      {
+        builder.setName(String.format("%s$%d", param.getId(), i++));
+        builder.setLabel(choice);
+        builder.setDefaultValue(selected.contains(choice));
+        builder.setValue(value.contains(choice));
+        builder.setReprValue(choice);
+        options.add(builder.build());
+      }
+      return options;
+    }
+    else
+    {
+      /*
+       * Single value parameter means a single string with limited possible
+       * values can be used.
+       */
+      var builder = StringParameter.newBuilder();
+      setCommonProperties(param, builder);
+      builder.setDefaultValue((String) param.getDefault());
+      if (value.size() > 0)
+        builder.setValue(value.get(0));
+      builder.setPossibleValues(param.getChoices());
+      return List.of(builder.build());
+    }
+  }
+
+  private Option createOption(Parameter.FileParameter param, File value)
+  {
+    throw new NotImplementedError("file paramters are not implemented for slivka");
+  }
+
+  private Option createOption(Parameter param, Object value)
+  {
+    var builder = StringParameter.newBuilder();
+    setCommonProperties(param, builder);
+    if (param.getDefault() != null)
+      builder.setDefaultValue(param.getDefault().toString());
+    if (value != null)
+      builder.setValue(value.toString());
+    return builder.build();
+  }
+
+  private void setCommonProperties(Parameter param, Option.Builder builder)
+  {
+    builder.setName(param.getId());
+    builder.setLabel(param.getName());
+    builder.setDescription(param.getDescription());
+    builder.setRequired(param.isRequired());
+    try
+    {
+      builder.setDetailsUrl(service.getUrl().toURL());
+    } catch (MalformedURLException e)
+    {
+      Cache.log.warn("invalid service url " + service.getUrl(), e);
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/jalview/ws2/client/slivka/SlivkaWSDiscoverer.java b/src/jalview/ws2/client/slivka/SlivkaWSDiscoverer.java
new file mode 100644 (file)
index 0000000..a5f51ab
--- /dev/null
@@ -0,0 +1,190 @@
+package jalview.ws2.client.slivka;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+import jalview.bin.Cache;
+import jalview.ws.params.ParamManager;
+import jalview.ws2.actions.alignment.AlignmentAction;
+import jalview.ws2.api.WebService;
+import jalview.ws2.client.api.AbstractWebServiceDiscoverer;
+import uk.ac.dundee.compbio.slivkaclient.SlivkaClient;
+import uk.ac.dundee.compbio.slivkaclient.SlivkaService;
+
+public class SlivkaWSDiscoverer extends AbstractWebServiceDiscoverer
+{
+  private static final String SLIVKA_HOST_URLS = "SLIVKAHOSTURLS";
+
+  private static final URL DEFAULT_URL;
+  static
+  {
+    try
+    {
+      DEFAULT_URL = new URL("https://www.compbio.dundee.ac.uk/slivka/");
+    } catch (MalformedURLException e)
+    {
+      throw new AssertionError(e);
+    }
+  }
+
+  private static SlivkaWSDiscoverer instance = null;
+
+  private static ParamManager paramManager = null;
+
+  private SlivkaWSDiscoverer()
+  {
+  }
+
+  public static SlivkaWSDiscoverer getInstance()
+  {
+    if (instance == null)
+      instance = new SlivkaWSDiscoverer();
+    return instance;
+  }
+
+  public static void setParamManager(ParamManager manager)
+  {
+    paramManager = manager;
+  }
+
+  @Override
+  public int getStatusForUrl(URL url)
+  {
+    try
+    {
+      List<?> services = new SlivkaClient(url.toString()).getServices();
+      return services.isEmpty() ? STATUS_NO_SERVICES : STATUS_OK;
+    } catch (IOException e)
+    {
+      Cache.log.error("slivka could not retrieve services from " + url, e);
+      return STATUS_INVALID;
+    }
+  }
+
+  @Override
+  protected String getUrlsPropertyKey()
+  {
+    return SLIVKA_HOST_URLS;
+  }
+
+  @Override
+  protected URL getDefaultUrl()
+  {
+    return DEFAULT_URL;
+  }
+
+  @Override
+  protected List<WebService<?>> fetchServices(URL url) throws IOException
+  {
+    ArrayList<WebService<?>> allServices = new ArrayList<>();
+    SlivkaClient slivkaClient;
+    try
+    {
+      slivkaClient = new SlivkaClient(url.toURI());
+    } catch (URISyntaxException e)
+    {
+      throw new MalformedURLException(e.getMessage());
+    }
+    for (var slivkaService : slivkaClient.getServices())
+    {
+      int serviceClass = getServiceClass(slivkaService);
+      if (serviceClass == SERVICE_CLASS_MSA)
+      {
+        var wsb = WebService.<AlignmentAction> newBuilder();
+        initServiceBuilder(slivkaService, wsb);
+        wsb.category("Alignment");
+        wsb.interactive(false);
+        wsb.actionClass(AlignmentAction.class);
+        var msaService = wsb.build();
+
+        boolean canRealign = msaService.getName().endsWith("lustal");
+        var client = new SlivkaAlignmentWSClient(slivkaService);
+        var actionBuilder = AlignmentAction.newBuilder(client);
+        actionBuilder.name("Alignment");
+        actionBuilder.webService(msaService);
+        if (canRealign)
+          actionBuilder.subcategory("Align");
+        actionBuilder.minSequences(2);
+        msaService.addAction(actionBuilder.build());
+        if (canRealign)
+        {
+          actionBuilder.name("Re-alignment");
+          actionBuilder.subcategory("Realign");
+          actionBuilder.submitGaps(true);
+          msaService.addAction(actionBuilder.build());
+        }
+        allServices.add(msaService);
+      }
+      else
+      {
+        continue;
+      }
+    }
+    return allServices;
+  }
+
+  private void initServiceBuilder(SlivkaService service, WebService.Builder<?> wsBuilder)
+  {
+    try
+    {
+      wsBuilder.url(service.getUrl().toURL());
+    } catch (MalformedURLException e)
+    {
+      e.printStackTrace();
+    }
+    wsBuilder.clientName("slivka");
+    wsBuilder.name(service.getName());
+    wsBuilder.description(service.getDescription());
+    var storeBuilder = new SlivkaParamStoreFactory(service, paramManager);
+    wsBuilder.paramDatastore(storeBuilder.createParamDatastore());
+  }
+
+  static final int SERVICE_CLASS_UNSUPPORTED = -1;
+
+  static final int SERVICE_CLASS_MSA = 1;
+
+  static final int SERVICE_CLASS_RNA_SEC_STR_PRED = 2;
+
+  static final int SERVICE_CLASS_CONSERVATION = 3;
+
+  static final int SERVICE_CLASS_PROT_SEQ_ANALYSIS = 4;
+
+  static final int SERVICE_CLASS_PROT_SEC_STR_PRED = 5;
+
+  /**
+   * Scan service classifiers starting with operation :: analysis to decide the
+   * operation class.
+   * 
+   * @return service class flag
+   */
+  private static int getServiceClass(SlivkaService service)
+  {
+    for (String classifier : service.getClassifiers())
+    {
+      String[] path = classifier.split("\\s*::\\s*");
+      if (path.length < 3 || !path[0].equalsIgnoreCase("operation") ||
+          !path[1].equalsIgnoreCase("analysis"))
+        continue;
+      // classifier is operation :: analysis :: *
+      var tail = path[path.length - 1].toLowerCase();
+      switch (tail)
+      {
+      case "multiple sequence alignment":
+        return SERVICE_CLASS_MSA;
+      case "rna secondary structure prediction":
+        return SERVICE_CLASS_RNA_SEC_STR_PRED;
+      case "sequence alignment analysis (conservation)":
+        return SERVICE_CLASS_CONSERVATION;
+      case "protein sequence analysis":
+        return SERVICE_CLASS_PROT_SEQ_ANALYSIS;
+      case "protein secondary structure prediction":
+        return SERVICE_CLASS_PROT_SEC_STR_PRED;
+      }
+    }
+    return SERVICE_CLASS_UNSUPPORTED;
+  }
+}