271d9ed6edeee8ece2275de26be753f5396b728f
[jalview.git] / src / jalview / ws2 / actions / annotation / AnnotationTask.java
1 package jalview.ws2.actions.annotation;
2
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.Collections;
6 import java.util.HashMap;
7 import java.util.List;
8 import java.util.Map;
9
10 import jalview.analysis.AlignmentAnnotationUtils;
11 import jalview.api.AlignCalcManagerI2;
12 import jalview.api.AlignCalcWorkerI;
13 import jalview.api.AlignViewportI;
14 import jalview.api.FeatureColourI;
15 import jalview.api.PollableAlignCalcWorkerI;
16 import jalview.bin.Cache;
17 import jalview.bin.Console;
18 import jalview.datamodel.AlignmentAnnotation;
19 import jalview.datamodel.AlignmentI;
20 import jalview.datamodel.AnnotatedCollectionI;
21 import jalview.datamodel.Annotation;
22 import jalview.datamodel.ContiguousI;
23 import jalview.datamodel.Mapping;
24 import jalview.datamodel.SequenceI;
25 import jalview.datamodel.features.FeatureMatcherSetI;
26 import jalview.schemes.FeatureSettingsAdapter;
27 import jalview.util.ArrayUtils;
28 import jalview.util.MapList;
29 import jalview.util.MathUtils;
30 import jalview.util.Pair;
31 import jalview.workers.AlignCalcWorker;
32 import jalview.ws.params.ArgumentI;
33 import jalview.ws2.actions.AbstractPollableTask;
34 import jalview.ws2.actions.BaseJob;
35 import jalview.ws2.actions.ServiceInputInvalidException;
36 import jalview.ws2.actions.api.JobI;
37 import jalview.ws2.actions.api.TaskEventListener;
38 import jalview.ws2.actions.api.TaskI;
39 import jalview.ws2.api.Credentials;
40 import jalview.ws2.api.JobStatus;
41 import jalview.ws2.api.WebServiceJobHandle;
42 import jalview.ws2.client.api.AnnotationWebServiceClientI;
43 import jalview.ws2.helpers.DelegateJobEventListener;
44 import jalview.ws2.helpers.TaskEventSupport;
45
46 public class AnnotationTask implements TaskI<AnnotationResult>
47 {
48   private final long uid = MathUtils.getUID();
49
50   private AnnotationWebServiceClientI client;
51
52   private final AnnotationAction action;
53
54   private final List<ArgumentI> args;
55
56   private final Credentials credentials;
57
58   private final AlignViewportI viewport;
59
60   private final TaskEventSupport<AnnotationResult> eventHandler;
61
62   private JobStatus taskStatus = null;
63
64   private AlignCalcWorkerAdapter worker = null;
65
66   private List<AnnotationJob> jobs = Collections.emptyList();
67
68   private AnnotationResult result = null;
69
70   private DelegateJobEventListener<AnnotationResult> jobEventHandler;
71
72   private class AlignCalcWorkerAdapter extends AlignCalcWorker
73       implements PollableAlignCalcWorkerI
74   {
75     private boolean restarting = false;
76
77     AlignCalcWorkerAdapter(AlignCalcManagerI2 calcMan)
78     {
79       super(viewport, null);
80       this.calcMan = calcMan;
81     }
82
83     String getServiceName()
84     {
85       return action.getWebService().getName();
86     }
87
88     @Override
89     public void startUp() throws Throwable
90     {
91       if (alignViewport.isClosed())
92       {
93         stop();
94         throw new IllegalStateException("Starting annotation for closed viewport");
95       }
96       if (restarting)
97         eventHandler.fireTaskRestarted();
98       else
99         restarting = true;
100       jobs = Collections.emptyList();
101       try
102       {
103         jobs = prepare();
104       } catch (ServiceInputInvalidException e)
105       {
106         setStatus(JobStatus.INVALID);
107         eventHandler.fireTaskException(e);
108         throw e;
109       }
110       setStatus(JobStatus.READY);
111       eventHandler.fireTaskStarted(jobs);
112       for (var job : jobs)
113       {
114         job.addPropertyChagneListener(jobEventHandler);
115       }
116       try
117       {
118         startJobs();
119       } catch (IOException e)
120       {
121         eventHandler.fireTaskException(e);
122         cancelJobs();
123         setStatus(JobStatus.SERVER_ERROR);
124         throw e;
125       }
126       setStatus(JobStatus.SUBMITTED);
127     }
128
129     @Override
130     public boolean poll() throws Throwable
131     {
132       boolean done = AnnotationTask.this.poll();
133       updateGlobalStatus();
134       if (done)
135       {
136         retrieveAndProcessResult();
137         eventHandler.fireTaskCompleted(result);
138       }
139       return done;
140     }
141
142     private void retrieveAndProcessResult() throws IOException
143     {
144       result = retrieveResult();
145       updateOurAnnots(result.annotations);
146       if (result.transferFeatures)
147       {
148         final var featureColours = result.featureColours;
149         final var featureFilters = result.featureFilters;
150         viewport.applyFeaturesStyle(new FeatureSettingsAdapter()
151         {
152           @Override
153           public FeatureColourI getFeatureColour(String type)
154           {
155             return featureColours.get(type);
156           }
157
158           @Override
159           public FeatureMatcherSetI getFeatureFilters(String type)
160           {
161             return featureFilters.get(type);
162           }
163
164           @Override
165           public boolean isFeatureDisplayed(String type)
166           {
167             return featureColours.containsKey(type);
168           }
169         });
170       }
171     }
172
173     @Override
174     public void updateAnnotation()
175     {
176       var job = jobs.size() > 0 ? jobs.get(0) : null;
177       if (!calcMan.isWorking(this) && job != null)
178       {
179         var ret = updateResultAnnotation(job, job.returnedAnnotations);
180         updateOurAnnots(ret.get0());
181       }
182     }
183
184     private void updateOurAnnots(List<AlignmentAnnotation> newAnnots)
185     {
186       List<AlignmentAnnotation> oldAnnots = ourAnnots != null ? ourAnnots : Collections.emptyList();
187       ourAnnots = newAnnots;
188       AlignmentI alignment = viewport.getAlignment();
189       for (AlignmentAnnotation an : oldAnnots)
190       {
191         if (!newAnnots.contains(an))
192         {
193           alignment.deleteAnnotation(an);
194         }
195       }
196       oldAnnots.clear();
197       for (AlignmentAnnotation an : ourAnnots)
198       {
199         viewport.getAlignment().validateAnnotation(an);
200       }
201     }
202
203     @Override
204     public void cancel()
205     {
206       cancelJobs();
207     }
208
209     void stop()
210     {
211       calcMan.disableWorker(this);
212       super.abortAndDestroy();
213     }
214
215     @Override
216     public void done()
217     {
218       for (var job : jobs)
219       {
220         if (job.isInputValid() && !job.isCompleted())
221         {
222           /* if done was called but job is not completed then it
223            * must have been stopped by an exception */
224           job.setStatus(JobStatus.SERVER_ERROR);
225         }
226       }
227       updateGlobalStatus();
228       // dispose of unfinished jobs just in case
229       cancelJobs();
230     }
231
232     @Override
233     public String toString()
234     {
235       return AnnotationTask.this.toString() + "$AlignCalcWorker@"
236           + Integer.toHexString(hashCode());
237     }
238   }
239
240   public AnnotationTask(AnnotationWebServiceClientI client,
241       AnnotationAction action, List<ArgumentI> args, Credentials credentials,
242       AlignViewportI viewport,
243       TaskEventListener<AnnotationResult> eventListener)
244   {
245     this.client = client;
246     this.action = action;
247     this.args = args;
248     this.credentials = credentials;
249     this.viewport = viewport;
250     this.eventHandler = new TaskEventSupport<>(this, eventListener);
251     this.jobEventHandler = new DelegateJobEventListener<>(this.eventHandler);
252   }
253
254   @Override
255   public long getUid()
256   {
257     return uid;
258   }
259
260   public void start(AlignCalcManagerI2 calcManager)
261   {
262     if (this.worker != null)
263       throw new IllegalStateException("task already started");
264     this.worker = new AlignCalcWorkerAdapter(calcManager);
265     if (taskStatus != JobStatus.CANCELLED)
266     {
267       List<AlignCalcWorkerI> oldWorkers = calcManager.getWorkersOfClass(
268           AlignCalcWorkerAdapter.class);
269       for (var worker : oldWorkers)
270       {
271         if (action.getWebService().getName().equalsIgnoreCase(
272             ((AlignCalcWorkerAdapter) worker).getServiceName()))
273         {
274           // remove interactive workers for the same service.
275           calcManager.removeWorker(worker);
276           calcManager.cancelWorker(worker);
277         }
278       }
279       if (action.getWebService().isInteractive())
280         calcManager.registerWorker(worker);
281       else
282         calcManager.startWorker(worker);
283     }
284   }
285
286   /*
287    * The following methods are mostly copied from the {@link AbstractPollableTask}
288    * TODO: move common functionality to a base class
289    */
290   @Override
291   public JobStatus getStatus()
292   {
293     return taskStatus;
294   }
295
296   private void setStatus(JobStatus status)
297   {
298     if (this.taskStatus != status)
299     {
300       Console.debug(String.format("%s status change to %s", this, status.name()));
301       this.taskStatus = status;
302       eventHandler.fireTaskStatusChanged(status);
303     }
304   }
305
306   private void updateGlobalStatus()
307   {
308     int precedence = -1;
309     for (BaseJob job : jobs)
310     {
311       JobStatus status = job.getStatus();
312       int jobPrecedence = ArrayUtils.indexOf(JobStatus.statusPrecedence, status);
313       if (precedence < jobPrecedence)
314         precedence = jobPrecedence;
315     }
316     if (precedence >= 0)
317     {
318       setStatus(JobStatus.statusPrecedence[precedence]);
319     }
320   }
321
322   @Override
323   public List<? extends JobI> getSubJobs()
324   {
325     return jobs;
326   }
327
328   /**
329    * Create and return a list of annotation jobs from the current state of the
330    * viewport. Returned job are not started by this method and should be stored
331    * in a field and started separately.
332    * 
333    * @return list of annotation jobs
334    * @throws ServiceInputInvalidException
335    *           input data is not valid
336    */
337   private List<AnnotationJob> prepare() throws ServiceInputInvalidException
338   {
339     AlignmentI alignment = viewport.getAlignment();
340     if (alignment == null || alignment.getWidth() <= 0 ||
341         alignment.getSequences() == null)
342       throw new ServiceInputInvalidException("Alignment does not contain sequences");
343     if (alignment.isNucleotide() && !action.doAllowNucleotide())
344       throw new ServiceInputInvalidException(
345           action.getFullName() + " does not allow nucleotide sequences");
346     if (!alignment.isNucleotide() && !action.doAllowProtein())
347       throw new ServiceInputInvalidException(
348           action.getFullName() + " does not allow protein sequences");
349     boolean bySequence = !action.isAlignmentAnalysis();
350     AnnotatedCollectionI inputSeqs = bySequence ? viewport.getSelectionGroup() : null;
351     if (inputSeqs == null || inputSeqs.getWidth() <= 0 ||
352         inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1)
353       inputSeqs = alignment;
354     boolean submitGaps = action.isAlignmentAnalysis();
355     boolean requireAligned = action.getRequireAlignedSequences();
356     boolean filterSymbols = action.getFilterSymbols();
357     int minSize = action.getMinSequences();
358     AnnotationJob job = AnnotationJob.create(inputSeqs, bySequence,
359         submitGaps, requireAligned, filterSymbols, minSize);
360     if (!job.isInputValid())
361     {
362       job.setStatus(JobStatus.INVALID);
363       throw new ServiceInputInvalidException("Annotation job has invalid input");
364     }
365     job.setStatus(JobStatus.READY);
366     return List.of(job);
367   }
368
369   private void startJobs() throws IOException
370   {
371     for (BaseJob job : jobs)
372     {
373       if (job.isInputValid() && job.getStatus() == JobStatus.READY)
374       {
375         var serverJob = client.submit(job.getInputSequences(),
376             args, credentials);
377         job.setServerJob(serverJob);
378         job.setStatus(JobStatus.SUBMITTED);
379       }
380     }
381   }
382
383   private boolean poll() throws IOException
384   {
385     boolean allDone = true;
386     for (BaseJob job : jobs)
387     {
388       if (job.isInputValid() && !job.getStatus().isDone())
389       {
390         WebServiceJobHandle serverJob = job.getServerJob();
391         job.setStatus(client.getStatus(serverJob));
392         job.setLog(client.getLog(serverJob));
393         job.setErrorLog(client.getErrorLog(serverJob));
394       }
395       allDone &= job.isCompleted();
396     }
397     return allDone;
398   }
399
400   private AnnotationResult retrieveResult() throws IOException
401   {
402     final Map<String, FeatureColourI> featureColours = new HashMap<>();
403     final Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
404     var job = jobs.get(0);
405     List<AlignmentAnnotation> returnedAnnot = client.attachAnnotations(
406         job.getServerJob(), job.getInputSequences(), featureColours,
407         featureFilters);
408     /* TODO
409      * copy over each annotation row returned and also defined on each
410      * sequence, excluding regions not annotated due to gapMap/column
411      * visibility */
412
413     // update calcId if it is not already set on returned annotation
414     for (AlignmentAnnotation annot : returnedAnnot)
415     {
416       if (annot.getCalcId() == null || annot.getCalcId().isEmpty())
417       {
418         annot.setCalcId(action.getFullName());
419       }
420       annot.autoCalculated = action.isAlignmentAnalysis() &&
421           action.getWebService().isInteractive();
422     }
423     job.returnedAnnotations = returnedAnnot;
424     job.featureColours = featureColours;
425     job.featureFilters = featureFilters;
426     var ret = updateResultAnnotation(job, returnedAnnot);
427     var annotations = ret.get0();
428     var transferFeatures = ret.get1();
429     return new AnnotationResult(annotations, transferFeatures, featureColours,
430         featureFilters);
431   }
432
433   private Pair<List<AlignmentAnnotation>, Boolean> updateResultAnnotation(
434       AnnotationJob job, List<AlignmentAnnotation> annotations)
435   {
436     List<AlignmentAnnotation> newAnnots = new ArrayList<>();
437     // update graphGroup for all annotation
438     /* find a graphGroup greater than any existing one, could be moved
439      * to Alignment#getNewGraphGroup() - returns next unused graph group */
440     int graphGroup = 1;
441     if (viewport.getAlignment().getAlignmentAnnotation() != null)
442     {
443       for (var ala : viewport.getAlignment().getAlignmentAnnotation())
444       {
445         graphGroup = Math.max(graphGroup, ala.graphGroup);
446       }
447     }
448     // update graphGroup in the annotation rows returned form service'
449     /* TODO: look at sequence annotation rows and update graph groups in the
450      * case of reference annotation */
451     for (AlignmentAnnotation ala : annotations)
452     {
453       if (ala.graphGroup > 0)
454         ala.graphGroup += graphGroup;
455       SequenceI aseq = null;
456       // transfer sequence refs and adjust gapMap
457       if (ala.sequenceRef != null)
458       {
459         aseq = job.seqNames.get(ala.sequenceRef.getName());
460       }
461       ala.sequenceRef = aseq;
462
463       Annotation[] resAnnot = ala.annotations;
464       boolean[] gapMap = job.gapMap;
465       Annotation[] gappedAnnot = new Annotation[Math.max(
466           viewport.getAlignment().getWidth(), gapMap.length)];
467       for (int p = 0, ap = job.start; ap < gappedAnnot.length; ap++)
468       {
469         if (gapMap.length > ap && !gapMap[ap])
470           gappedAnnot[ap] = new Annotation("", "", ' ', Float.NaN);
471         else if (p < resAnnot.length)
472           gappedAnnot[ap] = resAnnot[p++];
473         // is this loop exhaustive of resAnnot?
474       }
475       ala.annotations = gappedAnnot;
476
477       AlignmentAnnotation newAnnot = viewport.getAlignment()
478           .updateFromOrCopyAnnotation(ala);
479       if (aseq != null)
480       {
481         aseq.addAlignmentAnnotation(newAnnot);
482         newAnnot.adjustForAlignment();
483         AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith(
484             newAnnot, newAnnot.label, newAnnot.getCalcId());
485       }
486       newAnnots.add(newAnnot);
487     }
488
489     boolean transferFeatures = false;
490     for (SequenceI sq : job.getInputSequences())
491     {
492       if (!sq.getFeatures().hasFeatures() &&
493           (sq.getDBRefs() == null || sq.getDBRefs().size() == 0))
494         continue;
495       transferFeatures = true;
496       SequenceI seq = job.seqNames.get(sq.getName());
497       SequenceI dseq;
498       int start = job.start, end = job.end;
499       boolean[] gapMap = job.gapMap;
500       ContiguousI seqRange = seq.findPositions(start, end);
501       while ((dseq = seq).getDatasetSequence() != null)
502       {
503         seq = seq.getDatasetSequence();
504       }
505       List<ContiguousI> sourceRange = new ArrayList<>();
506       if (gapMap.length >= end)
507       {
508         int lastcol = start, col = start;
509         do
510         {
511           if (col == end || !gapMap[col])
512           {
513             if (lastcol <= (col - 1))
514             {
515               seqRange = seq.findPositions(lastcol, col);
516               sourceRange.add(seqRange);
517             }
518             lastcol = col + 1;
519           }
520         } while (col++ < end);
521       }
522       else
523       {
524         sourceRange.add(seq.findPositions(start, end));
525       }
526
527       int i = 0;
528       int sourceStartEnd[] = new int[sourceRange.size() * 2];
529       for (ContiguousI range : sourceRange)
530       {
531         sourceStartEnd[i++] = range.getBegin();
532         sourceStartEnd[i++] = range.getEnd();
533       }
534       Mapping mp = new Mapping(new MapList(
535           sourceStartEnd, new int[]
536           { seq.getStart(), seq.getEnd() }, 1, 1));
537       dseq.transferAnnotation(sq, mp);
538     }
539
540     return new Pair<>(newAnnots, transferFeatures);
541   }
542
543   @Override
544   public AnnotationResult getResult()
545   {
546     return result;
547   }
548
549   @Override
550   public void cancel()
551   {
552     setStatus(JobStatus.CANCELLED);
553     if (worker != null)
554     {
555       worker.stop();
556     }
557     cancelJobs();
558   }
559
560   public void cancelJobs()
561   {
562     for (BaseJob job : jobs)
563     {
564       if (!job.isCompleted())
565       {
566         try
567         {
568           if (job.getServerJob() != null)
569           {
570             client.cancel(job.getServerJob());
571           }
572           job.setStatus(JobStatus.CANCELLED);
573         } catch (IOException e)
574         {
575           Console.error(String.format(
576               "failed to cancel job %s", job.getServerJob()), e);
577         }
578       }
579     }
580   }
581
582   @Override
583   public String toString()
584   {
585     var status = taskStatus != null ? taskStatus.name() : "UNSET";
586     return String.format("%s(%x, %s)", getClass().getSimpleName(), uid, status);
587   }
588 }