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