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