JAL-4199 Replace AbstractPollingTask with BaseTask
[jalview.git] / src / jalview / ws2 / actions / annotation / AnnotationTask.java
index 271d9ed..238439a 100644 (file)
@@ -30,8 +30,8 @@ import jalview.util.MathUtils;
 import jalview.util.Pair;
 import jalview.workers.AlignCalcWorker;
 import jalview.ws.params.ArgumentI;
-import jalview.ws2.actions.AbstractPollableTask;
 import jalview.ws2.actions.BaseJob;
+import jalview.ws2.actions.BaseTask;
 import jalview.ws2.actions.ServiceInputInvalidException;
 import jalview.ws2.actions.api.JobI;
 import jalview.ws2.actions.api.TaskEventListener;
@@ -43,287 +43,55 @@ import jalview.ws2.client.api.AnnotationWebServiceClientI;
 import jalview.ws2.helpers.DelegateJobEventListener;
 import jalview.ws2.helpers.TaskEventSupport;
 
-public class AnnotationTask implements TaskI<AnnotationResult>
+public class AnnotationTask extends BaseTask<AnnotationJob, AnnotationResult>
 {
-  private final long uid = MathUtils.getUID();
-
   private AnnotationWebServiceClientI client;
 
   private final AnnotationAction action;
 
-  private final List<ArgumentI> args;
-
-  private final Credentials credentials;
-
   private final AlignViewportI viewport;
 
-  private final TaskEventSupport<AnnotationResult> eventHandler;
-
   private JobStatus taskStatus = null;
 
   private AlignCalcWorkerAdapter worker = null;
 
-  private List<AnnotationJob> jobs = Collections.emptyList();
-
-  private AnnotationResult result = null;
-
   private DelegateJobEventListener<AnnotationResult> jobEventHandler;
 
-  private class AlignCalcWorkerAdapter extends AlignCalcWorker
-      implements PollableAlignCalcWorkerI
-  {
-    private boolean restarting = false;
-
-    AlignCalcWorkerAdapter(AlignCalcManagerI2 calcMan)
-    {
-      super(viewport, null);
-      this.calcMan = calcMan;
-    }
-
-    String getServiceName()
-    {
-      return action.getWebService().getName();
-    }
-
-    @Override
-    public void startUp() throws Throwable
-    {
-      if (alignViewport.isClosed())
-      {
-        stop();
-        throw new IllegalStateException("Starting annotation for closed viewport");
-      }
-      if (restarting)
-        eventHandler.fireTaskRestarted();
-      else
-        restarting = true;
-      jobs = Collections.emptyList();
-      try
-      {
-        jobs = prepare();
-      } catch (ServiceInputInvalidException e)
-      {
-        setStatus(JobStatus.INVALID);
-        eventHandler.fireTaskException(e);
-        throw e;
-      }
-      setStatus(JobStatus.READY);
-      eventHandler.fireTaskStarted(jobs);
-      for (var job : jobs)
-      {
-        job.addPropertyChagneListener(jobEventHandler);
-      }
-      try
-      {
-        startJobs();
-      } catch (IOException e)
-      {
-        eventHandler.fireTaskException(e);
-        cancelJobs();
-        setStatus(JobStatus.SERVER_ERROR);
-        throw e;
-      }
-      setStatus(JobStatus.SUBMITTED);
-    }
-
-    @Override
-    public boolean poll() throws Throwable
-    {
-      boolean done = AnnotationTask.this.poll();
-      updateGlobalStatus();
-      if (done)
-      {
-        retrieveAndProcessResult();
-        eventHandler.fireTaskCompleted(result);
-      }
-      return done;
-    }
-
-    private void retrieveAndProcessResult() throws IOException
-    {
-      result = retrieveResult();
-      updateOurAnnots(result.annotations);
-      if (result.transferFeatures)
-      {
-        final var featureColours = result.featureColours;
-        final var featureFilters = result.featureFilters;
-        viewport.applyFeaturesStyle(new FeatureSettingsAdapter()
-        {
-          @Override
-          public FeatureColourI getFeatureColour(String type)
-          {
-            return featureColours.get(type);
-          }
-
-          @Override
-          public FeatureMatcherSetI getFeatureFilters(String type)
-          {
-            return featureFilters.get(type);
-          }
-
-          @Override
-          public boolean isFeatureDisplayed(String type)
-          {
-            return featureColours.containsKey(type);
-          }
-        });
-      }
-    }
-
-    @Override
-    public void updateAnnotation()
-    {
-      var job = jobs.size() > 0 ? jobs.get(0) : null;
-      if (!calcMan.isWorking(this) && job != null)
-      {
-        var ret = updateResultAnnotation(job, job.returnedAnnotations);
-        updateOurAnnots(ret.get0());
-      }
-    }
-
-    private void updateOurAnnots(List<AlignmentAnnotation> newAnnots)
-    {
-      List<AlignmentAnnotation> oldAnnots = ourAnnots != null ? ourAnnots : Collections.emptyList();
-      ourAnnots = newAnnots;
-      AlignmentI alignment = viewport.getAlignment();
-      for (AlignmentAnnotation an : oldAnnots)
-      {
-        if (!newAnnots.contains(an))
-        {
-          alignment.deleteAnnotation(an);
-        }
-      }
-      oldAnnots.clear();
-      for (AlignmentAnnotation an : ourAnnots)
-      {
-        viewport.getAlignment().validateAnnotation(an);
-      }
-    }
-
-    @Override
-    public void cancel()
-    {
-      cancelJobs();
-    }
-
-    void stop()
-    {
-      calcMan.disableWorker(this);
-      super.abortAndDestroy();
-    }
-
-    @Override
-    public void done()
-    {
-      for (var job : jobs)
-      {
-        if (job.isInputValid() && !job.isCompleted())
-        {
-          /* if done was called but job is not completed then it
-           * must have been stopped by an exception */
-          job.setStatus(JobStatus.SERVER_ERROR);
-        }
-      }
-      updateGlobalStatus();
-      // dispose of unfinished jobs just in case
-      cancelJobs();
-    }
-
-    @Override
-    public String toString()
-    {
-      return AnnotationTask.this.toString() + "$AlignCalcWorker@"
-          + Integer.toHexString(hashCode());
-    }
-  }
-
   public AnnotationTask(AnnotationWebServiceClientI client,
       AnnotationAction action, List<ArgumentI> args, Credentials credentials,
-      AlignViewportI viewport,
-      TaskEventListener<AnnotationResult> eventListener)
+      AlignViewportI viewport)
   {
+    super(client, args, credentials);
     this.client = client;
     this.action = action;
-    this.args = args;
-    this.credentials = credentials;
     this.viewport = viewport;
-    this.eventHandler = new TaskEventSupport<>(this, eventListener);
-    this.jobEventHandler = new DelegateJobEventListener<>(this.eventHandler);
-  }
-
-  @Override
-  public long getUid()
-  {
-    return uid;
   }
 
-  public void start(AlignCalcManagerI2 calcManager)
-  {
-    if (this.worker != null)
-      throw new IllegalStateException("task already started");
-    this.worker = new AlignCalcWorkerAdapter(calcManager);
-    if (taskStatus != JobStatus.CANCELLED)
-    {
-      List<AlignCalcWorkerI> oldWorkers = calcManager.getWorkersOfClass(
-          AlignCalcWorkerAdapter.class);
-      for (var worker : oldWorkers)
-      {
-        if (action.getWebService().getName().equalsIgnoreCase(
-            ((AlignCalcWorkerAdapter) worker).getServiceName()))
-        {
-          // remove interactive workers for the same service.
-          calcManager.removeWorker(worker);
-          calcManager.cancelWorker(worker);
-        }
-      }
-      if (action.getWebService().isInteractive())
-        calcManager.registerWorker(worker);
-      else
-        calcManager.startWorker(worker);
-    }
-  }
-
-  /*
-   * The following methods are mostly copied from the {@link AbstractPollableTask}
-   * TODO: move common functionality to a base class
-   */
-  @Override
-  public JobStatus getStatus()
-  {
-    return taskStatus;
-  }
-
-  private void setStatus(JobStatus status)
-  {
-    if (this.taskStatus != status)
-    {
-      Console.debug(String.format("%s status change to %s", this, status.name()));
-      this.taskStatus = status;
-      eventHandler.fireTaskStatusChanged(status);
-    }
-  }
-
-  private void updateGlobalStatus()
-  {
-    int precedence = -1;
-    for (BaseJob job : jobs)
-    {
-      JobStatus status = job.getStatus();
-      int jobPrecedence = ArrayUtils.indexOf(JobStatus.statusPrecedence, status);
-      if (precedence < jobPrecedence)
-        precedence = jobPrecedence;
-    }
-    if (precedence >= 0)
-    {
-      setStatus(JobStatus.statusPrecedence[precedence]);
-    }
-  }
-
-  @Override
-  public List<? extends JobI> getSubJobs()
-  {
-    return jobs;
-  }
+  // public void start(AlignCalcManagerI2 calcManager)
+  // {
+  // if (this.worker != null)
+  // throw new IllegalStateException("task already started");
+  // this.worker = new AlignCalcWorkerAdapter(calcManager);
+  // if (taskStatus != JobStatus.CANCELLED)
+  // {
+  // List<AlignCalcWorkerI> oldWorkers = calcManager.getWorkersOfClass(
+  // AlignCalcWorkerAdapter.class);
+  // for (var worker : oldWorkers)
+  // {
+  // if (action.getWebService().getName().equalsIgnoreCase(
+  // ((AlignCalcWorkerAdapter) worker).getServiceName()))
+  // {
+  // // remove interactive workers for the same service.
+  // calcManager.removeWorker(worker);
+  // calcManager.cancelWorker(worker);
+  // }
+  // }
+  // if (action.getWebService().isInteractive())
+  // calcManager.registerWorker(worker);
+  // else
+  // calcManager.startWorker(worker);
+  // }
+  // }
 
   /**
    * Create and return a list of annotation jobs from the current state of the
@@ -334,7 +102,8 @@ public class AnnotationTask implements TaskI<AnnotationResult>
    * @throws ServiceInputInvalidException
    *           input data is not valid
    */
-  private List<AnnotationJob> prepare() throws ServiceInputInvalidException
+  @Override
+  public List<AnnotationJob> prepareJobs() throws ServiceInputInvalidException
   {
     AlignmentI alignment = viewport.getAlignment();
     if (alignment == null || alignment.getWidth() <= 0 ||
@@ -366,38 +135,8 @@ public class AnnotationTask implements TaskI<AnnotationResult>
     return List.of(job);
   }
 
-  private void startJobs() throws IOException
-  {
-    for (BaseJob job : jobs)
-    {
-      if (job.isInputValid() && job.getStatus() == JobStatus.READY)
-      {
-        var serverJob = client.submit(job.getInputSequences(),
-            args, credentials);
-        job.setServerJob(serverJob);
-        job.setStatus(JobStatus.SUBMITTED);
-      }
-    }
-  }
-
-  private boolean poll() throws IOException
-  {
-    boolean allDone = true;
-    for (BaseJob job : jobs)
-    {
-      if (job.isInputValid() && !job.getStatus().isDone())
-      {
-        WebServiceJobHandle serverJob = job.getServerJob();
-        job.setStatus(client.getStatus(serverJob));
-        job.setLog(client.getLog(serverJob));
-        job.setErrorLog(client.getErrorLog(serverJob));
-      }
-      allDone &= job.isCompleted();
-    }
-    return allDone;
-  }
-
-  private AnnotationResult retrieveResult() throws IOException
+  @Override
+  protected AnnotationResult collectResult(List<AnnotationJob> jobs) throws IOException
   {
     final Map<String, FeatureColourI> featureColours = new HashMap<>();
     final Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
@@ -410,173 +149,129 @@ public class AnnotationTask implements TaskI<AnnotationResult>
      * sequence, excluding regions not annotated due to gapMap/column
      * visibility */
 
-    // update calcId if it is not already set on returned annotation
-    for (AlignmentAnnotation annot : returnedAnnot)
+    udpateCalcId(returnedAnnot);
+    int graphGroup = getNextGraphGroup(viewport.getAlignment());
+    shiftGraphGroup(returnedAnnot, graphGroup);
+    List<AlignmentAnnotation> annotations = new ArrayList<>();
+    for (AlignmentAnnotation ala : returnedAnnot)
     {
-      if (annot.getCalcId() == null || annot.getCalcId().isEmpty())
-      {
-        annot.setCalcId(action.getFullName());
-      }
-      annot.autoCalculated = action.isAlignmentAnalysis() &&
-          action.getWebService().isInteractive();
-    }
-    job.returnedAnnotations = returnedAnnot;
-    job.featureColours = featureColours;
-    job.featureFilters = featureFilters;
-    var ret = updateResultAnnotation(job, returnedAnnot);
-    var annotations = ret.get0();
-    var transferFeatures = ret.get1();
-    return new AnnotationResult(annotations, transferFeatures, featureColours,
-        featureFilters);
-  }
-
-  private Pair<List<AlignmentAnnotation>, Boolean> updateResultAnnotation(
-      AnnotationJob job, List<AlignmentAnnotation> annotations)
-  {
-    List<AlignmentAnnotation> newAnnots = new ArrayList<>();
-    // update graphGroup for all annotation
-    /* find a graphGroup greater than any existing one, could be moved
-     * to Alignment#getNewGraphGroup() - returns next unused graph group */
-    int graphGroup = 1;
-    if (viewport.getAlignment().getAlignmentAnnotation() != null)
-    {
-      for (var ala : viewport.getAlignment().getAlignmentAnnotation())
-      {
-        graphGroup = Math.max(graphGroup, ala.graphGroup);
-      }
-    }
-    // update graphGroup in the annotation rows returned form service'
-    /* TODO: look at sequence annotation rows and update graph groups in the
-     * case of reference annotation */
-    for (AlignmentAnnotation ala : annotations)
-    {
-      if (ala.graphGroup > 0)
-        ala.graphGroup += graphGroup;
-      SequenceI aseq = null;
-      // transfer sequence refs and adjust gapMap
-      if (ala.sequenceRef != null)
-      {
-        aseq = job.seqNames.get(ala.sequenceRef.getName());
-      }
+      SequenceI seq = job.seqNames.get(ala.sequenceRef.getName());
+      SequenceI aseq = getRootDatasetSequence(seq);
+      Annotation[] gappedAnnots = createGappedAnnotations(ala.annotations, job.start, job.gapMap);
       ala.sequenceRef = aseq;
-
-      Annotation[] resAnnot = ala.annotations;
-      boolean[] gapMap = job.gapMap;
-      Annotation[] gappedAnnot = new Annotation[Math.max(
-          viewport.getAlignment().getWidth(), gapMap.length)];
-      for (int p = 0, ap = job.start; ap < gappedAnnot.length; ap++)
-      {
-        if (gapMap.length > ap && !gapMap[ap])
-          gappedAnnot[ap] = new Annotation("", "", ' ', Float.NaN);
-        else if (p < resAnnot.length)
-          gappedAnnot[ap] = resAnnot[p++];
-        // is this loop exhaustive of resAnnot?
-      }
-      ala.annotations = gappedAnnot;
+      ala.annotations = gappedAnnots;
 
       AlignmentAnnotation newAnnot = viewport.getAlignment()
           .updateFromOrCopyAnnotation(ala);
-      if (aseq != null)
+      if (aseq != null) // I suspect it's always true
       {
         aseq.addAlignmentAnnotation(newAnnot);
         newAnnot.adjustForAlignment();
         AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith(
             newAnnot, newAnnot.label, newAnnot.getCalcId());
       }
-      newAnnots.add(newAnnot);
+      annotations.add(newAnnot);
     }
 
-    boolean transferFeatures = false;
+    boolean hasFeatures = false;
     for (SequenceI sq : job.getInputSequences())
     {
-      if (!sq.getFeatures().hasFeatures() &&
-          (sq.getDBRefs() == null || sq.getDBRefs().size() == 0))
+      if (!sq.getFeatures().hasFeatures() && (sq.getDBRefs() == null || sq.getDBRefs().isEmpty()))
         continue;
-      transferFeatures = true;
+      hasFeatures = true;
       SequenceI seq = job.seqNames.get(sq.getName());
-      SequenceI dseq;
-      int start = job.start, end = job.end;
-      boolean[] gapMap = job.gapMap;
-      ContiguousI seqRange = seq.findPositions(start, end);
-      while ((dseq = seq).getDatasetSequence() != null)
-      {
-        seq = seq.getDatasetSequence();
-      }
-      List<ContiguousI> sourceRange = new ArrayList<>();
-      if (gapMap.length >= end)
-      {
-        int lastcol = start, col = start;
-        do
-        {
-          if (col == end || !gapMap[col])
-          {
-            if (lastcol <= (col - 1))
-            {
-              seqRange = seq.findPositions(lastcol, col);
-              sourceRange.add(seqRange);
-            }
-            lastcol = col + 1;
-          }
-        } while (col++ < end);
-      }
-      else
-      {
-        sourceRange.add(seq.findPositions(start, end));
-      }
+      SequenceI datasetSeq = getRootDatasetSequence(seq);
+      List<ContiguousI> sourceRange = findContiguousRanges(datasetSeq, job.gapMap, job.start, job.end);
+      int[] sourceStartEnd = ContiguousI.toStartEndArray(sourceRange);
+      Mapping mp = new Mapping(new MapList(
+          sourceStartEnd, new int[]
+          { datasetSeq.getStart(), datasetSeq.getEnd() }, 1, 1));
+      datasetSeq.transferAnnotation(sq, mp);
+    }
+
+    return new AnnotationResult(annotations, hasFeatures, featureColours, featureFilters);
+  }
 
-      int i = 0;
-      int sourceStartEnd[] = new int[sourceRange.size() * 2];
-      for (ContiguousI range : sourceRange)
+  /**
+   * Updates calcId on provided annotations if not already set.
+   */
+  public void udpateCalcId(Iterable<AlignmentAnnotation> annotations)
+  {
+    for (var annotation : annotations)
+    {
+      if (annotation.getCalcId() == null || annotation.getCalcId().isEmpty())
       {
-        sourceStartEnd[i++] = range.getBegin();
-        sourceStartEnd[i++] = range.getEnd();
+        annotation.setCalcId(action.getFullName());
       }
-      Mapping mp = new Mapping(new MapList(
-          sourceStartEnd, new int[]
-          { seq.getStart(), seq.getEnd() }, 1, 1));
-      dseq.transferAnnotation(sq, mp);
+      annotation.autoCalculated = action.isAlignmentAnalysis() &&
+          action.getWebService().isInteractive();
     }
+  }
 
-    return new Pair<>(newAnnots, transferFeatures);
+  private static int getNextGraphGroup(AlignmentI alignment)
+  {
+    if (alignment == null || alignment.getAlignmentAnnotation() == null)
+      return 1;
+    int graphGroup = 1;
+    for (AlignmentAnnotation ala : alignment.getAlignmentAnnotation())
+      graphGroup = Math.max(graphGroup, ala.graphGroup);
+    return graphGroup;
   }
 
-  @Override
-  public AnnotationResult getResult()
+  private static void shiftGraphGroup(Iterable<AlignmentAnnotation> annotations, int shift)
   {
-    return result;
+    for (AlignmentAnnotation ala : annotations)
+    {
+      if (ala.graphGroup > 0)
+      {
+        ala.graphGroup += shift;
+      }
+    }
   }
 
-  @Override
-  public void cancel()
+  private static SequenceI getRootDatasetSequence(SequenceI sequence)
   {
-    setStatus(JobStatus.CANCELLED);
-    if (worker != null)
+    while (sequence.getDatasetSequence() != null)
     {
-      worker.stop();
+      sequence = sequence.getDatasetSequence();
     }
-    cancelJobs();
+    return sequence;
   }
 
-  public void cancelJobs()
+  private Annotation[] createGappedAnnotations(Annotation[] annotations, int start, boolean[] gapMap)
   {
-    for (BaseJob job : jobs)
+    var size = Math.max(viewport.getAlignment().getWidth(), gapMap.length);
+    Annotation[] gappedAnnotations = new Annotation[size];
+    for (int p = 0, ap = start; ap < size; ap++)
     {
-      if (!job.isCompleted())
+      if (gapMap != null && gapMap.length > ap && !gapMap[ap])
       {
-        try
-        {
-          if (job.getServerJob() != null)
-          {
-            client.cancel(job.getServerJob());
-          }
-          job.setStatus(JobStatus.CANCELLED);
-        } catch (IOException e)
-        {
-          Console.error(String.format(
-              "failed to cancel job %s", job.getServerJob()), e);
-        }
+        gappedAnnotations[ap] = new Annotation("", "", ' ', Float.NaN);
+      }
+      else if (p < annotations.length)
+      {
+        gappedAnnotations[ap] = annotations[p++];
       }
     }
+    return gappedAnnotations;
+  }
+
+  private List<ContiguousI> findContiguousRanges(SequenceI seq, boolean[] gapMap, int start, int end)
+  {
+    if (gapMap == null || gapMap.length < end)
+      return List.of(seq.findPositions(start, end));
+    List<ContiguousI> ranges = new ArrayList<>();
+    int lastcol = start, col = start;
+    do
+    {
+      if (col == end || !gapMap[col])
+      {
+        if (lastcol < col)
+          ranges.add(seq.findPositions(lastcol, col));
+        lastcol = col + 1;
+      }
+    } while (++col <= end);
+    return ranges;
   }
 
   @Override