JAL-3878 Create SlivkaAnnotationWSClient
[jalview.git] / src / jalview / ws2 / client / slivka / SlivkaWSClient.java
1 package jalview.ws2.client.slivka;
2
3 import java.io.ByteArrayInputStream;
4 import java.io.ByteArrayOutputStream;
5 import java.io.File;
6 import java.io.IOException;
7 import java.io.InputStream;
8 import java.nio.charset.StandardCharsets;
9 import java.util.Arrays;
10 import java.util.Collections;
11 import java.util.EnumMap;
12 import java.util.List;
13 import java.util.Map;
14 import java.util.regex.Pattern;
15
16 import jalview.api.FeatureColourI;
17 import jalview.bin.Cache;
18 import jalview.datamodel.Alignment;
19 import jalview.datamodel.AlignmentAnnotation;
20 import jalview.datamodel.AlignmentI;
21 import jalview.datamodel.SequenceI;
22 import jalview.datamodel.features.FeatureMatcherSetI;
23 import jalview.io.AnnotationFile;
24 import jalview.io.DataSourceType;
25 import jalview.io.FeaturesFile;
26 import jalview.io.FileFormat;
27 import jalview.io.FormatAdapter;
28 import jalview.ws.params.ArgumentI;
29 import jalview.ws2.api.Credentials;
30 import jalview.ws2.api.JobStatus;
31 import jalview.ws2.api.WebServiceJobHandle;
32 import jalview.ws2.client.api.AlignmentWebServiceClientI;
33 import jalview.ws2.client.api.AnnotationWebServiceClientI;
34 import jalview.ws2.client.api.WebServiceClientI;
35 import uk.ac.dundee.compbio.slivkaclient.Job;
36 import uk.ac.dundee.compbio.slivkaclient.Parameter;
37 import uk.ac.dundee.compbio.slivkaclient.SlivkaClient;
38 import uk.ac.dundee.compbio.slivkaclient.SlivkaService;
39
40 import static java.lang.String.format;
41
42 public class SlivkaWSClient implements WebServiceClientI
43 {
44   final SlivkaService service;
45
46   final SlivkaClient client;
47
48   SlivkaWSClient(SlivkaService service)
49   {
50     this.service = service;
51     this.client = service.getClient();
52   }
53
54   @Override
55   public String getUrl()
56   {
57     return client.getUrl().toString();
58   }
59
60   @Override
61   public String getClientName()
62   {
63     return "slivka";
64   }
65
66   // pattern for matching media types
67   static final Pattern mediaTypePattern = Pattern.compile(
68       "(?:text|application)\\/(?:x-)?([\\w-]+)");
69
70   @Override
71   public WebServiceJobHandle submit(List<SequenceI> sequences,
72       List<ArgumentI> args, Credentials credentials) throws IOException
73   {
74     var request = new uk.ac.dundee.compbio.slivkaclient.JobRequest();
75     for (Parameter param : service.getParameters())
76     {
77       // TODO: restrict input sequences parameter name to "sequences"
78       if (param instanceof Parameter.FileParameter)
79       {
80         Parameter.FileParameter fileParam = (Parameter.FileParameter) param;
81         FileFormat format = null;
82         var match = mediaTypePattern.matcher(fileParam.getMediaType());
83         if (match.find())
84         {
85           String fmt = match.group(1);
86           if (fmt.equalsIgnoreCase("pfam"))
87             format = FileFormat.Pfam;
88           else if (fmt.equalsIgnoreCase("stockholm"))
89             format = FileFormat.Stockholm;
90           else if (fmt.equalsIgnoreCase("clustal"))
91             format = FileFormat.Clustal;
92           else if (fmt.equalsIgnoreCase("fasta"))
93             format = FileFormat.Fasta;
94         }
95         if (format == null)
96         {
97           Cache.log.warn(String.format(
98               "Unknown input format %s, assuming fasta.",
99               fileParam.getMediaType()));
100           format = FileFormat.Fasta;
101         }
102         InputStream stream = new ByteArrayInputStream(format.getWriter(null)
103             .print(sequences.toArray(new SequenceI[0]), false)
104             .getBytes());
105         request.addFile(param.getId(), stream);
106       }
107     }
108     if (args != null)
109     {
110       for (ArgumentI arg : args)
111       {
112         // multiple choice field names are name$number to avoid duplications
113         // the number is stripped here
114         String paramId = arg.getName().split("\\$", 2)[0];
115         Parameter param = service.getParameter(paramId);
116         if (param instanceof Parameter.FlagParameter)
117         {
118           if (arg.getValue() != null && !arg.getValue().isBlank())
119             request.addData(paramId, true);
120           else
121             request.addData(paramId, false);
122         }
123         else if (param instanceof Parameter.FileParameter)
124         {
125           request.addFile(paramId, new File(arg.getValue()));
126         }
127         else
128         {
129           request.addData(paramId, arg.getValue());
130         }
131       }
132     }
133     var job = service.submitJob(request);
134     return createJobHandle(job.getId());
135   }
136
137   protected WebServiceJobHandle createJobHandle(String jobId)
138   {
139     return new WebServiceJobHandle(
140         getClientName(), service.getName(), client.getUrl().toString(),
141         jobId);
142   }
143
144   @Override
145   public JobStatus getStatus(WebServiceJobHandle job) throws IOException
146   {
147     var slivkaJob = client.getJob(job.getJobId());
148     return statusMap.getOrDefault(slivkaJob.getStatus(), JobStatus.UNKNOWN);
149   }
150
151   protected static final EnumMap<Job.Status, JobStatus> statusMap = new EnumMap<>(Job.Status.class);
152   static
153   {
154     statusMap.put(Job.Status.PENDING, JobStatus.SUBMITTED);
155     statusMap.put(Job.Status.REJECTED, JobStatus.INVALID);
156     statusMap.put(Job.Status.ACCEPTED, JobStatus.SUBMITTED);
157     statusMap.put(Job.Status.QUEUED, JobStatus.QUEUED);
158     statusMap.put(Job.Status.RUNNING, JobStatus.RUNNING);
159     statusMap.put(Job.Status.COMPLETED, JobStatus.COMPLETED);
160     statusMap.put(Job.Status.INTERRUPTED, JobStatus.CANCELLED);
161     statusMap.put(Job.Status.DELETED, JobStatus.CANCELLED);
162     statusMap.put(Job.Status.FAILED, JobStatus.FAILED);
163     statusMap.put(Job.Status.ERROR, JobStatus.SERVER_ERROR);
164     statusMap.put(Job.Status.UNKNOWN, JobStatus.UNKNOWN);
165   }
166
167   @Override
168   public String getLog(WebServiceJobHandle job) throws IOException
169   {
170     var slivkaJob = client.getJob(job.getJobId());
171     for (var f : slivkaJob.getResults())
172     {
173       if (f.getLabel().equals("log"))
174       {
175         ByteArrayOutputStream stream = new ByteArrayOutputStream();
176         f.writeTo(stream);
177         return stream.toString(StandardCharsets.UTF_8);
178       }
179     }
180     return "";
181   }
182
183   @Override
184   public String getErrorLog(WebServiceJobHandle job) throws IOException
185   {
186     var slivkaJob = client.getJob(job.getJobId());
187     for (var f : slivkaJob.getResults())
188     {
189       if (f.getLabel().equals("error-log"))
190       {
191         ByteArrayOutputStream stream = new ByteArrayOutputStream();
192         f.writeTo(stream);
193         return stream.toString(StandardCharsets.UTF_8);
194       }
195     }
196     return "";
197   }
198
199   @Override
200   public void cancel(WebServiceJobHandle job)
201       throws IOException, UnsupportedOperationException
202   {
203     throw new UnsupportedOperationException(
204         "slivka client does not support job cancellation");
205   }
206 }
207
208 class SlivkaAlignmentWSClient extends SlivkaWSClient
209     implements AlignmentWebServiceClientI
210 {
211
212   SlivkaAlignmentWSClient(SlivkaService service)
213   {
214     super(service);
215   }
216
217   @Override
218   public AlignmentI getAlignment(WebServiceJobHandle job) throws IOException
219   {
220     var slivkaJob = client.getJob(job.getJobId());
221     for (var f : slivkaJob.getResults())
222     {
223       // TODO: restrict result file label to "alignment"
224       FileFormat format;
225       var match = mediaTypePattern.matcher(f.getMediaType());
226       if (!match.find())
227         continue;
228       String fmt = match.group(1);
229       if (fmt.equalsIgnoreCase("clustal"))
230         format = FileFormat.Clustal;
231       else if (fmt.equalsIgnoreCase("fasta"))
232         format = FileFormat.Fasta;
233       else
234         continue;
235       return new FormatAdapter().readFile(f.getContentUrl().toString(),
236           DataSourceType.URL, format);
237     }
238     Cache.log.warn("No alignment found on the server");
239     throw new IOException("no alignment found");
240   }
241
242 }
243
244 class SlivkaAnnotationWSClient extends SlivkaWSClient
245     implements AnnotationWebServiceClientI
246 {
247   SlivkaAnnotationWSClient(SlivkaService service)
248   {
249     super(service);
250   }
251
252   @Override
253   public List<AlignmentAnnotation> attachAnnotations(WebServiceJobHandle job,
254       List<SequenceI> sequences, Map<String, FeatureColourI> colours,
255       Map<String, FeatureMatcherSetI> filters) throws IOException
256   {
257     var slivkaJob = client.getJob(job.getJobId());
258     var aln = new Alignment(sequences.toArray(new SequenceI[sequences.size()]));
259     boolean featPresent = false, annotPresent = false;
260     for (var f : slivkaJob.getResults())
261     {
262       // TODO: restrict file label to "annotations" or "features"
263       var match = mediaTypePattern.matcher(f.getMediaType());
264       if (!match.find())
265         continue;
266       String fmt = match.group(1);
267       if (fmt.equalsIgnoreCase("jalview-annotations"))
268       {
269         annotPresent = new AnnotationFile().readAnnotationFileWithCalcId(
270             aln, service.getId(), f.getContentUrl().toString(),
271             DataSourceType.URL);
272         if (annotPresent)
273           Cache.log.debug(format("loaded annotations for %s", service.getId()));
274       }
275       else if (fmt.equalsIgnoreCase("jalview-features"))
276       {
277         FeaturesFile ff = new FeaturesFile(f.getContentUrl().toString(),
278             DataSourceType.URL);
279         featPresent = ff.parse(aln, colours, true);
280         if (featPresent)
281           Cache.log.debug(format("loaded features for %s", service.getId()));
282       }
283     }
284     if (!annotPresent)
285       Cache.log.debug(format("no annotations found for %s", service.getId()));
286     if (!featPresent)
287       Cache.log.debug(format("no features found for %s", service.getId()));
288     return aln.getAlignmentAnnotation() != null ? Arrays.asList(aln.getAlignmentAnnotation())
289         : Collections.emptyList();
290   }
291 }