package jalview.ws2.client.slivka; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import jalview.api.FeatureColourI; import jalview.bin.Cache; import jalview.datamodel.Alignment; import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceI; import jalview.datamodel.features.FeatureMatcherSetI; import jalview.io.AnnotationFile; import jalview.io.DataSourceType; import jalview.io.FeaturesFile; import jalview.io.FileFormat; import jalview.io.FormatAdapter; import jalview.ws.params.ArgumentI; import jalview.ws2.api.Credentials; import jalview.ws2.api.JobStatus; import jalview.ws2.api.WebServiceJobHandle; import jalview.ws2.client.api.AlignmentWebServiceClientI; import jalview.ws2.client.api.AnnotationWebServiceClientI; import jalview.ws2.client.api.WebServiceClientI; import uk.ac.dundee.compbio.slivkaclient.Job; import uk.ac.dundee.compbio.slivkaclient.Parameter; import uk.ac.dundee.compbio.slivkaclient.SlivkaClient; import uk.ac.dundee.compbio.slivkaclient.SlivkaService; import static java.lang.String.format; public class SlivkaWSClient implements WebServiceClientI { final SlivkaService service; final SlivkaClient client; SlivkaWSClient(SlivkaService service) { this.service = service; this.client = service.getClient(); } @Override public String getUrl() { return client.getUrl().toString(); } @Override public String getClientName() { return "slivka"; } // pattern for matching media types static final Pattern mediaTypePattern = Pattern.compile( "(?:text|application)\\/(?:x-)?([\\w-]+)"); @Override public WebServiceJobHandle submit(List sequences, List args, Credentials credentials) throws IOException { var request = new uk.ac.dundee.compbio.slivkaclient.JobRequest(); for (Parameter param : service.getParameters()) { // TODO: restrict input sequences parameter name to "sequences" if (param instanceof Parameter.FileParameter) { Parameter.FileParameter fileParam = (Parameter.FileParameter) param; FileFormat format = null; var match = mediaTypePattern.matcher(fileParam.getMediaType()); if (match.find()) { String fmt = match.group(1); if (fmt.equalsIgnoreCase("pfam")) format = FileFormat.Pfam; else if (fmt.equalsIgnoreCase("stockholm")) format = FileFormat.Stockholm; else if (fmt.equalsIgnoreCase("clustal")) format = FileFormat.Clustal; else if (fmt.equalsIgnoreCase("fasta")) format = FileFormat.Fasta; } if (format == null) { Cache.log.warn(String.format( "Unknown input format %s, assuming fasta.", fileParam.getMediaType())); format = FileFormat.Fasta; } InputStream stream = new ByteArrayInputStream(format.getWriter(null) .print(sequences.toArray(new SequenceI[0]), false) .getBytes()); request.addFile(param.getId(), stream); } } if (args != null) { for (ArgumentI arg : args) { // multiple choice field names are name$number to avoid duplications // the number is stripped here String paramId = arg.getName().split("\\$", 2)[0]; Parameter param = service.getParameter(paramId); if (param instanceof Parameter.FlagParameter) { if (arg.getValue() != null && !arg.getValue().isBlank()) request.addData(paramId, true); else request.addData(paramId, false); } else if (param instanceof Parameter.FileParameter) { request.addFile(paramId, new File(arg.getValue())); } else { request.addData(paramId, arg.getValue()); } } } var job = service.submitJob(request); return createJobHandle(job.getId()); } protected WebServiceJobHandle createJobHandle(String jobId) { return new WebServiceJobHandle( getClientName(), service.getName(), client.getUrl().toString(), jobId); } @Override public JobStatus getStatus(WebServiceJobHandle job) throws IOException { var slivkaJob = client.getJob(job.getJobId()); return statusMap.getOrDefault(slivkaJob.getStatus(), JobStatus.UNKNOWN); } protected static final EnumMap statusMap = new EnumMap<>(Job.Status.class); static { statusMap.put(Job.Status.PENDING, JobStatus.SUBMITTED); statusMap.put(Job.Status.REJECTED, JobStatus.INVALID); statusMap.put(Job.Status.ACCEPTED, JobStatus.SUBMITTED); statusMap.put(Job.Status.QUEUED, JobStatus.QUEUED); statusMap.put(Job.Status.RUNNING, JobStatus.RUNNING); statusMap.put(Job.Status.COMPLETED, JobStatus.COMPLETED); statusMap.put(Job.Status.INTERRUPTED, JobStatus.CANCELLED); statusMap.put(Job.Status.DELETED, JobStatus.CANCELLED); statusMap.put(Job.Status.FAILED, JobStatus.FAILED); statusMap.put(Job.Status.ERROR, JobStatus.SERVER_ERROR); statusMap.put(Job.Status.UNKNOWN, JobStatus.UNKNOWN); } @Override public String getLog(WebServiceJobHandle job) throws IOException { var slivkaJob = client.getJob(job.getJobId()); for (var f : slivkaJob.getResults()) { if (f.getLabel().equals("log")) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); f.writeTo(stream); return stream.toString(StandardCharsets.UTF_8); } } return ""; } @Override public String getErrorLog(WebServiceJobHandle job) throws IOException { var slivkaJob = client.getJob(job.getJobId()); for (var f : slivkaJob.getResults()) { if (f.getLabel().equals("error-log")) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); f.writeTo(stream); return stream.toString(StandardCharsets.UTF_8); } } return ""; } @Override public void cancel(WebServiceJobHandle job) throws IOException, UnsupportedOperationException { Cache.log.warn( "slivka client does not support job cancellation"); } } class SlivkaAlignmentWSClient extends SlivkaWSClient implements AlignmentWebServiceClientI { SlivkaAlignmentWSClient(SlivkaService service) { super(service); } @Override public AlignmentI getAlignment(WebServiceJobHandle job) throws IOException { var slivkaJob = client.getJob(job.getJobId()); for (var f : slivkaJob.getResults()) { // TODO: restrict result file label to "alignment" FileFormat format; var match = mediaTypePattern.matcher(f.getMediaType()); if (!match.find()) continue; String fmt = match.group(1); if (fmt.equalsIgnoreCase("clustal")) format = FileFormat.Clustal; else if (fmt.equalsIgnoreCase("fasta")) format = FileFormat.Fasta; else continue; return new FormatAdapter().readFile(f.getContentUrl().toString(), DataSourceType.URL, format); } Cache.log.warn("No alignment found on the server"); throw new IOException("no alignment found"); } } class SlivkaAnnotationWSClient extends SlivkaWSClient implements AnnotationWebServiceClientI { SlivkaAnnotationWSClient(SlivkaService service) { super(service); } @Override public List attachAnnotations(WebServiceJobHandle job, List sequences, Map colours, Map filters) throws IOException { var slivkaJob = client.getJob(job.getJobId()); var aln = new Alignment(sequences.toArray(new SequenceI[sequences.size()])); boolean featPresent = false, annotPresent = false; for (var f : slivkaJob.getResults()) { // TODO: restrict file label to "annotations" or "features" var match = mediaTypePattern.matcher(f.getMediaType()); if (!match.find()) continue; String fmt = match.group(1); if (fmt.equalsIgnoreCase("jalview-annotations")) { annotPresent = new AnnotationFile().readAnnotationFileWithCalcId( aln, service.getId(), f.getContentUrl().toString(), DataSourceType.URL); if (annotPresent) Cache.log.debug(format("loaded annotations for %s", service.getId())); } else if (fmt.equalsIgnoreCase("jalview-features")) { FeaturesFile ff = new FeaturesFile(f.getContentUrl().toString(), DataSourceType.URL); featPresent = ff.parse(aln, colours, true); if (featPresent) Cache.log.debug(format("loaded features for %s", service.getId())); } } if (!annotPresent) Cache.log.debug(format("no annotations found for %s", service.getId())); if (!featPresent) Cache.log.debug(format("no features found for %s", service.getId())); return aln.getAlignmentAnnotation() != null ? Arrays.asList(aln.getAlignmentAnnotation()) : Collections.emptyList(); } }