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.bin.Console; 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(SlivkaClient client, SlivkaService service) { this.service = service; this.client = client; } @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.RequestValues(); 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) { Console.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().isEmpty()) 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 jobId = client.submitJob(service, request); return createJobHandle(jobId); } protected WebServiceJobHandle createJobHandle(String jobId) { return new WebServiceJobHandle( getClientName(), service.getName(), client.getUrl().toString(), jobId); } @Override public JobStatus getStatus(WebServiceJobHandle job) throws IOException { return statusMap.getOrDefault(client.fetchJobStatus(job.getJobId()), 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 { for (var f : client.fetchFilesList(job.getJobId())) { if (f.getLabel().equals("log")) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); client.writeFileTo(f, stream); return stream.toString("UTF-8"); } } return ""; } @Override public String getErrorLog(WebServiceJobHandle job) throws IOException { for (var f : client.fetchFilesList(job.getJobId())) { if (f.getLabel().equals("error-log")) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); client.writeFileTo(f, stream); return stream.toString("UTF-8"); } } return ""; } @Override public void cancel(WebServiceJobHandle job) throws IOException, UnsupportedOperationException { Console.warn( "slivka client does not support job cancellation"); } } class SlivkaAlignmentWSClient extends SlivkaWSClient implements AlignmentWebServiceClientI { SlivkaAlignmentWSClient(SlivkaClient client, SlivkaService service) { super(client, service); } @Override public AlignmentI getAlignment(WebServiceJobHandle job) throws IOException { for (var f : client.fetchFilesList(job.getJobId())) { // 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); } Console.warn("No alignment found on the server"); throw new IOException("no alignment found"); } } class SlivkaAnnotationWSClient extends SlivkaWSClient implements AnnotationWebServiceClientI { SlivkaAnnotationWSClient(SlivkaClient client, SlivkaService service) { super(client, service); } @Override public List attachAnnotations(WebServiceJobHandle job, List sequences, Map colours, Map filters) throws IOException { var aln = new Alignment(sequences.toArray(new SequenceI[sequences.size()])); boolean featPresent = false, annotPresent = false; for (var f : client.fetchFilesList(job.getJobId())) { // 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) Console.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) Console.debug(format("loaded features for %s", service.getId())); } } if (!annotPresent) Console.debug(format("no annotations found for %s", service.getId())); if (!featPresent) Console.debug(format("no features found for %s", service.getId())); return aln.getAlignmentAnnotation() != null ? Arrays.asList(aln.getAlignmentAnnotation()) : Collections.emptyList(); } }