JAL-3070 rough-and-ready refactor of JABA SequenceAnnotation style services - needs...
[jalview.git] / src / jalview / ws / jws2 / AbstractJabaCalcWorker.java
index 1a361c4..422b4ef 100644 (file)
@@ -24,17 +24,24 @@ import jalview.analysis.AlignSeq;
 import jalview.analysis.SeqsetUtils;
 import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
+import jalview.bin.Cache;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.AnnotatedCollectionI;
 import jalview.datamodel.SequenceI;
 import jalview.gui.AlignFrame;
+import jalview.gui.Desktop;
 import jalview.gui.IProgressIndicator;
 import jalview.gui.IProgressIndicatorHandler;
+import jalview.gui.JvOptionPane;
 import jalview.schemes.ResidueProperties;
+import jalview.util.MessageManager;
 import jalview.workers.AlignCalcWorker;
+import jalview.ws.api.CancellableI;
+import jalview.ws.api.JobId;
+import jalview.ws.api.WSAnnotationCalcManagerI;
+import jalview.ws.gui.AnnotationWsJob;
 import jalview.ws.jws2.dm.AAConSettings;
-import jalview.ws.jws2.dm.JabaWsParamSet;
 import jalview.ws.jws2.jabaws2.Jws2Instance;
 import jalview.ws.params.ArgumentI;
 import jalview.ws.params.WsParamSetI;
@@ -44,14 +51,8 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-import compbio.data.sequence.FastaSequence;
-import compbio.metadata.ChunkHolder;
-import compbio.metadata.JobStatus;
-import compbio.metadata.JobSubmissionException;
-import compbio.metadata.Option;
-import compbio.metadata.ResultNotAvailableException;
-
-public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
+public class AbstractJabaCalcWorker extends AlignCalcWorker
+        implements WSAnnotationCalcManagerI
 {
 
   protected Jws2Instance service;
@@ -88,7 +89,11 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
    * @return null or a string used to recover all annotation generated by this
    *         worker
    */
-  public abstract String getCalcId();
+  public String getCalcId()
+  {
+    return service.getAlignAnalysisUI() == null ? null
+            : service.getAlignAnalysisUI().getCalcId();
+  }
 
   public WsParamSetI getPreset()
   {
@@ -116,27 +121,6 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
     calcMan.startWorker(this);
     initViewportParams();
   }
-
-  public List<Option> getJabaArguments()
-  {
-    List<Option> newargs = new ArrayList<>();
-    if (preset != null && preset instanceof JabaWsParamSet)
-    {
-      newargs.addAll(((JabaWsParamSet) preset).getjabaArguments());
-    }
-    if (arguments != null && arguments.size() > 0)
-    {
-      for (Object rg : JabaParamStore.getJabafromJwsArgs(arguments))
-      {
-        if (Option.class.isAssignableFrom(rg.getClass()))
-        {
-          newargs.add((Option) rg);
-        }
-      }
-    }
-    return newargs;
-  }
-
   protected boolean alignedSeqs = true;
 
   protected boolean nucleotidesAllowed = false;
@@ -150,6 +134,7 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
 
   protected Map<String, SequenceI> seqNames;
 
+  // TODO: convert to bitset
   protected boolean[] gapMap;
 
   int realw;
@@ -158,6 +143,13 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
 
   int end;
 
+  private AlignFrame alignFrame;
+
+  public boolean[] getGapMap()
+  {
+    return gapMap;
+  }
+
   public AbstractJabaCalcWorker(AlignViewportI alignViewport,
           AlignmentViewPanel alignPanel)
   {
@@ -168,19 +160,71 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
           WsParamSetI preset, List<ArgumentI> paramset)
   {
     this(alignFrame.getCurrentView(), alignFrame.alignPanel);
+    // TODO: both these fields needed ?
+    this.alignFrame = alignFrame;
     this.guiProgress = alignFrame;
     this.preset = preset;
     this.arguments = paramset;
     this.service = service;
+    try
+    {
+      annotService = (jalview.ws.api.SequenceAnnotationServiceI) service
+              .getEndpoint();
+    } catch (ClassCastException cce)
+    {
+      JvOptionPane.showMessageDialog(Desktop.desktop,
+              MessageManager.formatMessage(
+                      "label.service_called_is_not_an_annotation_service",
+                      new String[]
+                      { service.getName() }),
+              MessageManager.getString("label.internal_jalview_error"),
+              JvOptionPane.WARNING_MESSAGE);
+
+    }
+    // configure submission flags
+    if (service.getAlignAnalysisUI() != null)
+    {
+      // instantaneous calculation. Right now that's either AACons or RNAAliFold
+      proteinAllowed = service.getAlignAnalysisUI().isPr();
+      nucleotidesAllowed = service.getAlignAnalysisUI().isNa();
+      alignedSeqs = service.getAlignAnalysisUI().isNeedsAlignedSeqs();
+      bySequence = !service.getAlignAnalysisUI().isAA();
+      filterNonStandardResidues = service.getAlignAnalysisUI()
+              .isFilterSymbols();
+      min_valid_seqs = service.getAlignAnalysisUI().getMinimumSequences();
+      initViewportParams();
+    }
+    else
+    {
+      // assume disorder prediction : per-sequence protein only no gaps
+      // analysis.
+      // TODO - move configuration to UIInfo base class for all these flags !
+      alignedSeqs = false;
+      bySequence = true;
+      filterNonStandardResidues = true;
+      nucleotidesAllowed = false;
+      proteinAllowed = true;
+      submitGaps = false;
+      min_valid_seqs = 1;
+    }
   }
 
   /**
    * 
    * @return true if the submission thread should attempt to submit data
    */
-  abstract boolean hasService();
+  public boolean hasService()
+  {
+    return annotService != null;
+  }
+
+  protected jalview.ws.api.SequenceAnnotationServiceI annotService = null;
+
+  volatile JobId rslt = null;
 
-  volatile String rslt = "JOB NOT DEFINED";
+  AnnotationWsJob running = null;
+
+  private int min_valid_seqs;
 
   @Override
   public void run()
@@ -189,10 +233,12 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
     {
       return;
     }
+
     long progressId = -1;
 
     int serverErrorsLeft = 3;
-
+    final boolean cancellable = CancellableI.class
+            .isAssignableFrom(annotService.getClass());
     StringBuffer msg = new StringBuffer();
     try
     {
@@ -200,12 +246,14 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
       {
         return;
       }
-      List<compbio.data.sequence.FastaSequence> seqs = getInputSequences(
+      List<SequenceI> seqs = getInputSequences(
               alignViewport.getAlignment(),
               bySequence ? alignViewport.getSelectionGroup() : null);
 
-      if (seqs == null || !checkValidInputSeqs(true, seqs))
+      if (seqs == null || !checkValidInputSeqs(seqs))
       {
+        jalview.bin.Cache.log.debug(
+                "Sequences for analysis service were null or not valid");
         calcMan.workerComplete(this);
         return;
       }
@@ -214,10 +262,28 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
               .getAlignmentAnnotation();
       if (guiProgress != null)
       {
-        guiProgress.setProgressBar("JABA " + getServiceActionText(),
+        guiProgress.setProgressBar(service.getActionText(),
                 progressId = System.currentTimeMillis());
       }
-      rslt = submitToService(seqs);
+      jalview.bin.Cache.log.debug("submitted " + seqs.size()
+              + " sequences to " + service.getActionText());
+
+      rslt = annotService.submitToService(seqs, getPreset(),
+              getArguments());
+      if (rslt == null)
+      {
+        return;
+      }
+      // TODO: handle job submission error reporting here.
+      
+      // ///
+      // otherwise, construct WsJob and any UI handlers
+      running = new AnnotationWsJob();
+      running.setJobHandle(rslt);
+      running.setSeqNames(seqNames);
+      running.setStartPos(start);
+      running.setSeqs(seqs);
+
       if (guiProgress != null)
       {
         guiProgress.registerHandler(progressId,
@@ -227,110 +293,95 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
                   @Override
                   public boolean cancelActivity(long id)
                   {
-                    cancelCurrentJob();
+                    ((CancellableI) annotService).cancel(running);
                     return true;
                   }
 
                   @Override
                   public boolean canCancel()
                   {
-                    return true;
+                    return cancellable;
                   }
                 });
       }
+      
+      // ///
+      // and poll for updates until job finishes, fails or becomes stale
+      
       boolean finished = false;
       long rpos = 0;
       do
       {
-        JobStatus status = getJobStatus(rslt);
-        if (status.equals(JobStatus.FINISHED))
+        Cache.log.debug("Updating status for annotation service.");
+        annotService.updateStatus(running);
+
+        if (running.isFinished())
         {
+          Cache.log.debug("Analysis service job reported finished.");
           finished = true;
         }
-        if (calcMan.isPending(this) && isInteractiveUpdate())
+        else
         {
-          finished = true;
-          // cancel this job and yield to the new job
-          try
-          {
-            if (cancelJob(rslt))
-            {
-              System.err.println("Cancelled AACon job: " + rslt);
-            }
-            else
-            {
-              System.err.println("FAILED TO CANCEL AACon job: " + rslt);
-            }
-
-          } catch (Exception x)
-          {
-
-          }
-          rslt = "CANCELLED JOB";
-          return;
+          Cache.log.debug("Status now " + running.getState());
         }
-        long cpos;
-        ChunkHolder stats = null;
-        do
+
+        if (calcMan.isPending(this) && isInteractiveUpdate())
         {
-          cpos = rpos;
-          boolean retry = false;
-          do
-          {
+          Cache.log.debug("Analysis service job is stale. aborting.");
+          // job has become stale.
+          if (!finished) {
+            finished = true;
+            // cancel this job and yield to the new job
             try
             {
-              stats = pullExecStatistics(rslt, rpos);
-            } catch (Exception x)
-            {
-
-              if (x.getMessage().contains(
-                      "Position in a file could not be negative!"))
+              if (cancellable
+                        && ((CancellableI) annotService).cancel(running))
               {
-                // squash index out of bounds exception- seems to happen for
-                // disorder predictors which don't (apparently) produce any
-                // progress information and JABA server throws an exception
-                // because progress length is -1.
-                stats = null;
+                System.err.println("Cancelled AACon job: " + rslt);
               }
               else
               {
-                if (--serverErrorsLeft > 0)
-                {
-                  retry = true;
-                  try
-                  {
-                    Thread.sleep(200);
-                  } catch (InterruptedException q)
-                  {
-                  }
-                  ;
-                }
-                else
-                {
-                  throw x;
-                }
+                System.err.println("FAILED TO CANCEL AACon job: " + rslt);
               }
+  
+            } catch (Exception x)
+            {
+  
             }
-          } while (retry);
-          if (stats != null)
-          {
-            System.out.print(stats.getChunk());
-            msg.append(stats);
-            rpos = stats.getNextPosition();
           }
-        } while (stats != null && rpos > cpos);
+          rslt = running.getJobHandle();
+          return;
+        }
+
+        // pull any stats - some services need to flush log output before
+        // results are available
+        Cache.log.debug("Updating progress log for annotation service.");
+
+        try
+        {
+        annotService.updateJobProgress(running);
+        } catch (Throwable thr)
+        {
+          Cache.log.debug("Ignoring exception during progress update.",
+                  thr);
+        }
+        Cache.log.trace("Result of poll: " + running.getStatus());
 
-        if (!finished && status.equals(JobStatus.FAILED))
+        if (!finished && !running.isFailed())
         {
           try
           {
+            Cache.log.debug("Analysis service job thread sleeping.");
             Thread.sleep(200);
+            Cache.log.debug("Analysis service job thread woke.");
           } catch (InterruptedException x)
           {
           }
           ;
         }
       } while (!finished);
+
+      // TODO: need to poll/retry
       if (serverErrorsLeft > 0)
       {
         try
@@ -339,42 +390,71 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
         } catch (InterruptedException x)
         {
         }
-        if (collectAnnotationResultsFor(rslt))
+      }
+      // configure job with the associated view's feature renderer, if one
+      // exists.
+      // TODO: here one would also grab the 'master feature renderer' in order
+      // to enable/disable
+      // features automatically according to user preferences
+      running.setFeatureRenderer(
+              ((jalview.gui.AlignmentPanel) ap).cloneFeatureRenderer());
+      Cache.log.debug("retrieving job results.");
+      List<AlignmentAnnotation> returnedAnnot = annotService
+              .getAlignmentAnnotation(running, this);
+      Cache.log.debug("Obtained " + (returnedAnnot == null ? "no rows"
+              : ("" + returnedAnnot.size())));
+      running.setAnnotation(returnedAnnot);
+
+      if (running.hasResults())
+      {
+        jalview.bin.Cache.log.debug("Updating result annotation from Job "
+                + rslt + " at " + service.getUri());
+        updateResultAnnotation(true);
+        if (running.isTransferSequenceFeatures())
         {
-          jalview.bin.Cache.log.debug("Updating result annotation from Job "
-                  + rslt + " at " + service.getUri());
-          updateResultAnnotation(true);
-          ap.adjustAnnotationHeight();
+          jalview.bin.Cache.log.debug(
+                  "Updating feature display settings and transferring features from Job "
+                          + rslt + " at " + service.getUri());
+          ((jalview.gui.AlignmentPanel) ap)
+                  .updateFeatureRendererFrom(running.getFeatureRenderer());
+          // TODO: JAL-1150 - create sequence feature settings API for defining
+          // styles and enabling/disabling feature overlay on alignment panel
+
+          if (alignFrame.alignPanel == ap)
+          {
+            // only do this if the alignFrame is currently showing this view.
+            Desktop.getAlignFrameFor(alignViewport)
+                    .setShowSeqFeatures(true);
+          }
         }
+        ap.adjustAnnotationHeight();
       }
+      Cache.log.debug("Annotation Service Worker thread finished.");
     }
-
-    catch (JobSubmissionException x)
-    {
-
-      System.err.println(
-              "submission error with " + getServiceActionText() + " :");
-      x.printStackTrace();
-      calcMan.disableWorker(this);
-    } catch (ResultNotAvailableException x)
-    {
-      System.err.println("collection error:\nJob ID: " + rslt);
-      x.printStackTrace();
-      calcMan.disableWorker(this);
-
-    } catch (OutOfMemoryError error)
-    {
-      calcMan.disableWorker(this);
-
-      // consensus = null;
-      // hconsensus = null;
-      ap.raiseOOMWarning(getServiceActionText(), error);
-    } catch (Exception x)
+// TODO: use service specitic exception handlers
+//    catch (JobSubmissionException x)
+//    {
+//
+//      System.err.println(
+//              "submission error with " + getServiceActionText() + " :");
+//      x.printStackTrace();
+//      calcMan.disableWorker(this);
+//    } catch (ResultNotAvailableException x)
+//    {
+//      System.err.println("collection error:\nJob ID: " + rslt);
+//      x.printStackTrace();
+//      calcMan.disableWorker(this);
+//
+//    } catch (OutOfMemoryError error)
+//    {
+//      calcMan.disableWorker(this);
+//
+//      ap.raiseOOMWarning(getServiceActionText(), error);
+//    } 
+    catch (Throwable x)
     {
       calcMan.disableWorker(this);
 
-      // consensus = null;
-      // hconsensus = null;
       System.err
               .println("Blacklisting worker due to unexpected exception:");
       x.printStackTrace();
@@ -408,34 +488,36 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
   }
 
   /**
-   * validate input for dynamic/non-dynamic update context
-   * 
-   * @param dynamic
+   * validate input for dynamic/non-dynamic update context TODO: move to
+   * analysis interface ?
    * @param seqs
+   * 
    * @return true if input is valid
    */
-  abstract boolean checkValidInputSeqs(boolean dynamic,
-          List<FastaSequence> seqs);
-
-  abstract String submitToService(
-          List<compbio.data.sequence.FastaSequence> seqs)
-          throws JobSubmissionException;
-
-  abstract boolean cancelJob(String rslt) throws Exception;
-
-  abstract JobStatus getJobStatus(String rslt) throws Exception;
-
-  abstract ChunkHolder pullExecStatistics(String rslt, long rpos);
-
-  abstract boolean collectAnnotationResultsFor(String rslt)
-          throws ResultNotAvailableException;
+  boolean checkValidInputSeqs(List<SequenceI> seqs)
+  {
+    int nvalid = 0;
+    for (SequenceI sq : seqs)
+    {
+      if (sq.getStart() <= sq.getEnd()
+              && (sq.isProtein() ? proteinAllowed : nucleotidesAllowed))
+      {
+        if (submitGaps
+                || sq.getLength() == (sq.getEnd() - sq.getStart() + 1))
+        {
+          nvalid++;
+        }
+      }
+    }
+    return nvalid >= min_valid_seqs;
+  }
 
   public void cancelCurrentJob()
   {
     try
     {
-      String id = rslt;
-      if (cancelJob(rslt))
+      String id = running.getJobId();
+      if (((CancellableI) annotService).cancel(running))
       {
         System.err.println("Cancelled job " + id);
       }
@@ -457,9 +539,20 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
    * @return true if a running job should be cancelled because new input data is
    *         available for analysis
    */
-  abstract boolean isInteractiveUpdate();
+  boolean isInteractiveUpdate()
+  {
+    return service.isInteractiveUpdate();
+  }
 
-  public List<FastaSequence> getInputSequences(AlignmentI alignment,
+  /**
+   * decide what sequences will be analysed TODO: refactor to generate
+   * List<SequenceI> for submission to service interface
+   * 
+   * @param alignment
+   * @param inputSeqs
+   * @return
+   */
+  public List<SequenceI> getInputSequences(AlignmentI alignment,
           AnnotatedCollectionI inputSeqs)
   {
     if (alignment == null || alignment.getWidth() <= 0
@@ -476,7 +569,7 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
       inputSeqs = alignment;
     }
 
-    List<compbio.data.sequence.FastaSequence> seqs = new ArrayList<>();
+    List<SequenceI> seqs = new ArrayList<>();
 
     int minlen = 10;
     int ln = -1;
@@ -487,7 +580,10 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
     gapMap = new boolean[0];
     start = inputSeqs.getStartRes();
     end = inputSeqs.getEndRes();
-
+    // TODO: URGENT! unify with JPred / MSA code to handle hidden regions
+    // correctly
+    // TODO: push attributes into WsJob instance (so they can be safely
+    // persisted/restored
     for (SequenceI sq : (inputSeqs.getSequences()))
     {
       if (bySequence
@@ -501,12 +597,12 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
         {
           seqNames.put(newname, sq);
         }
-        FastaSequence seq;
+        SequenceI seq;
         if (submitGaps)
         {
-          seqs.add(seq = new compbio.data.sequence.FastaSequence(newname,
+          seqs.add(seq = new jalview.datamodel.Sequence(newname,
                   sq.getSequenceAsString()));
-          if (gapMap == null || gapMap.length < seq.getSequence().length())
+          if (gapMap == null || gapMap.length < seq.getLength())
           {
             boolean[] tg = gapMap;
             gapMap = new boolean[seq.getLength()];
@@ -530,13 +626,17 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
         }
         else
         {
-          seqs.add(seq = new compbio.data.sequence.FastaSequence(newname,
+          // TODO: add ability to exclude hidden regions
+          seqs.add(seq = new jalview.datamodel.Sequence(newname,
                   AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
                           sq.getSequenceAsString(start, end + 1))));
+          // for annotation need to also record map to sequence start/end
+          // position in range
+          // then transfer back to original sequence on return.
         }
-        if (seq.getSequence().length() > ln)
+        if (seq.getLength() > ln)
         {
-          ln = seq.getSequence().length();
+          ln = seq.getLength();
         }
       }
     }
@@ -555,11 +655,10 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
       // acids at each position, and AAcon doesn't gracefully degrade.
       for (int p = 0; p < seqs.size(); p++)
       {
-        FastaSequence sq = seqs.get(p);
-        int l = sq.getSequence().length();
+        SequenceI sq = seqs.get(p);
         // strip gapped columns
         char[] padded = new char[realw],
-                orig = sq.getSequence().toCharArray();
+                orig = sq.getSequence();
         for (int i = 0, pp = 0; i < realw; pp++)
         {
           if (gapMap[pp])
@@ -574,7 +673,7 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
             }
           }
         }
-        seqs.set(p, new compbio.data.sequence.FastaSequence(sq.getId(),
+        seqs.set(p, new jalview.datamodel.Sequence(sq.getName(),
                 new String(padded)));
       }
     }
@@ -587,9 +686,15 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
     updateResultAnnotation(false);
   }
 
-  public abstract void updateResultAnnotation(boolean immediate);
-
-  public abstract String getServiceActionText();
+  public void updateResultAnnotation(boolean immediate)
+  {
+    if ((immediate || !calcMan.isWorking(this)) && running != null
+            && running.hasResults())
+    {
+      List<AlignmentAnnotation> ourAnnot = running.getAnnotation();
+      updateOurAnnots(ourAnnot);
+    }
+  }
 
   /**
    * notify manager that we have started, and wait for a free calculation slot
@@ -647,7 +752,13 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
         }
       }
       our.clear();
-
+      // validate rows and update Alignmment state
+      for (AlignmentAnnotation an : ourAnnots)
+      {
+        alignViewport.getAlignment().validateAnnotation(an);
+      }
+      // TODO: may need a menu refresh after this
+      // af.setMenusForViewport();
       ap.adjustAnnotationHeight();
     }
   }