JAL-3070 match AbstractJabaCalcWorker’s annotation service interface with class from...
[jalview.git] / src / jalview / ws / jws2 / AbstractJabaCalcWorker.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.ws.jws2;
22
23 import jalview.analysis.AlignSeq;
24 import jalview.analysis.SeqsetUtils;
25 import jalview.api.AlignViewportI;
26 import jalview.api.AlignmentViewPanel;
27 import jalview.bin.Cache;
28 import jalview.datamodel.AlignmentAnnotation;
29 import jalview.datamodel.AlignmentI;
30 import jalview.datamodel.AnnotatedCollectionI;
31 import jalview.datamodel.SequenceI;
32 import jalview.gui.AlignFrame;
33 import jalview.gui.Desktop;
34 import jalview.gui.IProgressIndicator;
35 import jalview.gui.IProgressIndicatorHandler;
36 import jalview.gui.JvOptionPane;
37 import jalview.schemes.ResidueProperties;
38 import jalview.util.MessageManager;
39 import jalview.workers.AlignCalcWorker;
40 import jalview.ws.api.CancellableI;
41 import jalview.ws.api.JobId;
42 import jalview.ws.api.SequenceAnnotationServiceI;
43 import jalview.ws.api.WSAnnotationCalcManagerI;
44 import jalview.ws.gui.AnnotationWsJob;
45 import jalview.ws.jws2.dm.AAConSettings;
46 import jalview.ws.jws2.jabaws2.Jws2Instance;
47 import jalview.ws.params.ArgumentI;
48 import jalview.ws.params.WsParamSetI;
49
50 import java.util.ArrayList;
51 import java.util.HashMap;
52 import java.util.List;
53 import java.util.Map;
54
55 public class AbstractJabaCalcWorker extends AlignCalcWorker
56         implements WSAnnotationCalcManagerI
57 {
58
59   protected Jws2Instance service;
60
61   protected WsParamSetI preset;
62
63   protected List<ArgumentI> arguments;
64
65   protected IProgressIndicator guiProgress;
66
67   protected boolean submitGaps = true;
68
69   /**
70    * by default, we filter out non-standard residues before submission
71    */
72   protected boolean filterNonStandardResidues = true;
73
74   /**
75    * Recover any existing parameters for this service
76    */
77   protected void initViewportParams()
78   {
79     if (getCalcId() != null)
80     {
81       ((jalview.gui.AlignViewport) alignViewport).setCalcIdSettingsFor(
82               getCalcId(),
83               new AAConSettings(true, service, this.preset, arguments),
84               true);
85     }
86   }
87
88   /**
89    * 
90    * @return null or a string used to recover all annotation generated by this
91    *         worker
92    */
93   public String getCalcId()
94   {
95     return service.getAlignAnalysisUI() == null ? null
96             : service.getAlignAnalysisUI().getCalcId();
97   }
98
99   public WsParamSetI getPreset()
100   {
101     return preset;
102   }
103
104   public List<ArgumentI> getArguments()
105   {
106     return arguments;
107   }
108
109   /**
110    * reconfigure and restart the AAConClient. This method will spawn a new
111    * thread that will wait until any current jobs are finished, modify the
112    * parameters and restart the conservation calculation with the new values.
113    * 
114    * @param newpreset
115    * @param newarguments
116    */
117   public void updateParameters(final WsParamSetI newpreset,
118           final List<ArgumentI> newarguments)
119   {
120     preset = newpreset;
121     arguments = newarguments;
122     calcMan.startWorker(this);
123     initViewportParams();
124   }
125   protected boolean alignedSeqs = true;
126
127   protected boolean nucleotidesAllowed = false;
128
129   protected boolean proteinAllowed = false;
130
131   /**
132    * record sequences for mapping result back to afterwards
133    */
134   protected boolean bySequence = false;
135
136   protected Map<String, SequenceI> seqNames;
137
138   // TODO: convert to bitset
139   protected boolean[] gapMap;
140
141   int realw;
142
143   protected int start;
144
145   int end;
146
147   private AlignFrame alignFrame;
148
149   public boolean[] getGapMap()
150   {
151     return gapMap;
152   }
153
154   public AbstractJabaCalcWorker(AlignViewportI alignViewport,
155           AlignmentViewPanel alignPanel)
156   {
157     super(alignViewport, alignPanel);
158   }
159
160   public AbstractJabaCalcWorker(Jws2Instance service, AlignFrame alignFrame,
161           WsParamSetI preset, List<ArgumentI> paramset)
162   {
163     this(alignFrame.getCurrentView(), alignFrame.alignPanel);
164     // TODO: both these fields needed ?
165     this.alignFrame = alignFrame;
166     this.guiProgress = alignFrame;
167     this.preset = preset;
168     this.arguments = paramset;
169     this.service = service;
170     try
171     {
172       annotService = (jalview.ws.api.SequenceAnnotationServiceI) service
173               .getEndpoint();
174     } catch (ClassCastException cce)
175     {
176       JvOptionPane.showMessageDialog(Desktop.desktop,
177               MessageManager.formatMessage(
178                       "label.service_called_is_not_an_annotation_service",
179                       new String[]
180                       { service.getName() }),
181               MessageManager.getString("label.internal_jalview_error"),
182               JvOptionPane.WARNING_MESSAGE);
183
184     }
185     // configure submission flags
186     if (service.getAlignAnalysisUI() != null)
187     {
188       // instantaneous calculation. Right now that's either AACons or RNAAliFold
189       proteinAllowed = service.getAlignAnalysisUI().isPr();
190       nucleotidesAllowed = service.getAlignAnalysisUI().isNa();
191       alignedSeqs = service.getAlignAnalysisUI().isNeedsAlignedSeqs();
192       bySequence = !service.getAlignAnalysisUI().isAA();
193       filterNonStandardResidues = service.getAlignAnalysisUI()
194               .isFilterSymbols();
195       min_valid_seqs = service.getAlignAnalysisUI().getMinimumSequences();
196       initViewportParams();
197     }
198     else
199     {
200       // assume disorder prediction : per-sequence protein only no gaps
201       // analysis.
202       // TODO - move configuration to UIInfo base class for all these flags !
203       alignedSeqs = false;
204       bySequence = true;
205       filterNonStandardResidues = true;
206       nucleotidesAllowed = false;
207       proteinAllowed = true;
208       submitGaps = false;
209       min_valid_seqs = 1;
210     }
211   }
212
213   /**
214    * 
215    * @return true if the submission thread should attempt to submit data
216    */
217   public boolean hasService()
218   {
219     return annotService != null;
220   }
221
222   protected jalview.ws.api.SequenceAnnotationServiceI annotService = null;
223
224   volatile JobId rslt = null;
225
226   AnnotationWsJob running = null;
227
228   private int min_valid_seqs;
229
230   @Override
231   public void run()
232   {
233     if (!hasService())
234     {
235       return;
236     }
237
238     long progressId = -1;
239
240     int serverErrorsLeft = 3;
241     final boolean cancellable = CancellableI.class
242             .isAssignableFrom(annotService.getClass());
243     StringBuffer msg = new StringBuffer();
244     try
245     {
246       if (checkDone())
247       {
248         return;
249       }
250       List<SequenceI> seqs = getInputSequences(
251               alignViewport.getAlignment(),
252               bySequence ? alignViewport.getSelectionGroup() : null);
253
254       if (seqs == null || !checkValidInputSeqs(seqs))
255       {
256         jalview.bin.Cache.log.debug(
257                 "Sequences for analysis service were null or not valid");
258         calcMan.workerComplete(this);
259         return;
260       }
261
262       AlignmentAnnotation[] aa = alignViewport.getAlignment()
263               .getAlignmentAnnotation();
264       if (guiProgress != null)
265       {
266         guiProgress.setProgressBar(service.getActionText(),
267                 progressId = System.currentTimeMillis());
268       }
269       jalview.bin.Cache.log.debug("submitted " + seqs.size()
270               + " sequences to " + service.getActionText());
271
272       rslt = annotService.submitToService(seqs, getPreset(),
273               getArguments());
274       if (rslt == null)
275       {
276         return;
277       }
278       // TODO: handle job submission error reporting here.
279       
280       // ///
281       // otherwise, construct WsJob and any UI handlers
282       running = new AnnotationWsJob();
283       running.setJobHandle(rslt);
284       running.setSeqNames(seqNames);
285       running.setStartPos(start);
286       running.setSeqs(seqs);
287
288       if (guiProgress != null)
289       {
290         guiProgress.registerHandler(progressId,
291                 new IProgressIndicatorHandler()
292                 {
293
294                   @Override
295                   public boolean cancelActivity(long id)
296                   {
297                     ((CancellableI) annotService).cancel(running);
298                     return true;
299                   }
300
301                   @Override
302                   public boolean canCancel()
303                   {
304                     return cancellable;
305                   }
306                 });
307       }
308       
309       // ///
310       // and poll for updates until job finishes, fails or becomes stale
311       
312       boolean finished = false;
313       long rpos = 0;
314       do
315       {
316         Cache.log.debug("Updating status for annotation service.");
317         annotService.updateStatus(running);
318
319         if (running.isFinished())
320         {
321           Cache.log.debug("Analysis service job reported finished.");
322           finished = true;
323         }
324         else
325         {
326           Cache.log.debug("Status now " + running.getState());
327         }
328
329         if (calcMan.isPending(this) && isInteractiveUpdate())
330         {
331           Cache.log.debug("Analysis service job is stale. aborting.");
332           // job has become stale.
333           if (!finished) {
334             finished = true;
335             // cancel this job and yield to the new job
336             try
337             {
338               if (cancellable
339                         && ((CancellableI) annotService).cancel(running))
340               {
341                 System.err.println("Cancelled AACon job: " + rslt);
342               }
343               else
344               {
345                 System.err.println("FAILED TO CANCEL AACon job: " + rslt);
346               }
347   
348             } catch (Exception x)
349             {
350   
351             }
352           }
353           rslt = running.getJobHandle();
354           return;
355         }
356
357         // pull any stats - some services need to flush log output before
358         // results are available
359         Cache.log.debug("Updating progress log for annotation service.");
360
361         try
362         {
363         annotService.updateJobProgress(running);
364         } catch (Throwable thr)
365         {
366           Cache.log.debug("Ignoring exception during progress update.",
367                   thr);
368         }
369         Cache.log.trace("Result of poll: " + running.getStatus());
370
371         if (!finished && !running.isFailed())
372         {
373           try
374           {
375             Cache.log.debug("Analysis service job thread sleeping.");
376             Thread.sleep(200);
377             Cache.log.debug("Analysis service job thread woke.");
378           } catch (InterruptedException x)
379           {
380           }
381           ;
382         }
383       } while (!finished);
384
385       // TODO: need to poll/retry
386       if (serverErrorsLeft > 0)
387       {
388         try
389         {
390           Thread.sleep(200);
391         } catch (InterruptedException x)
392         {
393         }
394       }
395       // configure job with the associated view's feature renderer, if one
396       // exists.
397       // TODO: here one would also grab the 'master feature renderer' in order
398       // to enable/disable
399       // features automatically according to user preferences
400       running.setFeatureRenderer(
401               ((jalview.gui.AlignmentPanel) ap).cloneFeatureRenderer());
402       Cache.log.debug("retrieving job results.");
403       List<AlignmentAnnotation> returnedAnnot = annotService
404               .getAlignmentAnnotation(running, this);
405       Cache.log.debug("Obtained " + (returnedAnnot == null ? "no rows"
406               : ("" + returnedAnnot.size())));
407       running.setAnnotation(returnedAnnot);
408
409       if (running.hasResults())
410       {
411         jalview.bin.Cache.log.debug("Updating result annotation from Job "
412                 + rslt + " at " + service.getUri());
413         updateResultAnnotation(true);
414         if (running.isTransferSequenceFeatures())
415         {
416           jalview.bin.Cache.log.debug(
417                   "Updating feature display settings and transferring features from Job "
418                           + rslt + " at " + service.getUri());
419           ((jalview.gui.AlignmentPanel) ap)
420                   .updateFeatureRendererFrom(running.getFeatureRenderer());
421           // TODO: JAL-1150 - create sequence feature settings API for defining
422           // styles and enabling/disabling feature overlay on alignment panel
423
424           if (alignFrame.alignPanel == ap)
425           {
426             // only do this if the alignFrame is currently showing this view.
427             Desktop.getAlignFrameFor(alignViewport)
428                     .setShowSeqFeatures(true);
429           }
430         }
431         ap.adjustAnnotationHeight();
432       }
433       Cache.log.debug("Annotation Service Worker thread finished.");
434     }
435 // TODO: use service specitic exception handlers
436 //    catch (JobSubmissionException x)
437 //    {
438 //
439 //      System.err.println(
440 //              "submission error with " + getServiceActionText() + " :");
441 //      x.printStackTrace();
442 //      calcMan.disableWorker(this);
443 //    } catch (ResultNotAvailableException x)
444 //    {
445 //      System.err.println("collection error:\nJob ID: " + rslt);
446 //      x.printStackTrace();
447 //      calcMan.disableWorker(this);
448 //
449 //    } catch (OutOfMemoryError error)
450 //    {
451 //      calcMan.disableWorker(this);
452 //
453 //      ap.raiseOOMWarning(getServiceActionText(), error);
454 //    } 
455     catch (Throwable x)
456     {
457       calcMan.disableWorker(this);
458
459       System.err
460               .println("Blacklisting worker due to unexpected exception:");
461       x.printStackTrace();
462     } finally
463     {
464
465       calcMan.workerComplete(this);
466       if (ap != null)
467       {
468         calcMan.workerComplete(this);
469         if (guiProgress != null && progressId != -1)
470         {
471           guiProgress.setProgressBar("", progressId);
472         }
473         // TODO: may not need to paintAlignment again !
474         ap.paintAlignment(false, false);
475       }
476       if (msg.length() > 0)
477       {
478         // TODO: stash message somewhere in annotation or alignment view.
479         // code below shows result in a text box popup
480         /*
481          * jalview.gui.CutAndPasteTransfer cap = new
482          * jalview.gui.CutAndPasteTransfer(); cap.setText(msg.toString());
483          * jalview.gui.Desktop.addInternalFrame(cap,
484          * "Job Status for "+getServiceActionText(), 600, 400);
485          */
486       }
487     }
488
489   }
490
491   /**
492    * validate input for dynamic/non-dynamic update context TODO: move to
493    * analysis interface ?
494    * @param seqs
495    * 
496    * @return true if input is valid
497    */
498   boolean checkValidInputSeqs(List<SequenceI> seqs)
499   {
500     int nvalid = 0;
501     for (SequenceI sq : seqs)
502     {
503       if (sq.getStart() <= sq.getEnd()
504               && (sq.isProtein() ? proteinAllowed : nucleotidesAllowed))
505       {
506         if (submitGaps
507                 || sq.getLength() == (sq.getEnd() - sq.getStart() + 1))
508         {
509           nvalid++;
510         }
511       }
512     }
513     return nvalid >= min_valid_seqs;
514   }
515
516   public void cancelCurrentJob()
517   {
518     try
519     {
520       String id = running.getJobId();
521       if (((CancellableI) annotService).cancel(running))
522       {
523         System.err.println("Cancelled job " + id);
524       }
525       else
526       {
527         System.err.println("Job " + id + " couldn't be cancelled.");
528       }
529     } catch (Exception q)
530     {
531       q.printStackTrace();
532     }
533   }
534
535   /**
536    * Interactive updating. Analysis calculations that work on the currently
537    * displayed alignment data should cancel existing jobs when the input data
538    * has changed.
539    * 
540    * @return true if a running job should be cancelled because new input data is
541    *         available for analysis
542    */
543   boolean isInteractiveUpdate()
544   {
545     return service.isInteractiveUpdate();
546   }
547
548   /**
549    * decide what sequences will be analysed TODO: refactor to generate
550    * List<SequenceI> for submission to service interface
551    * 
552    * @param alignment
553    * @param inputSeqs
554    * @return
555    */
556   public List<SequenceI> getInputSequences(AlignmentI alignment,
557           AnnotatedCollectionI inputSeqs)
558   {
559     if (alignment == null || alignment.getWidth() <= 0
560             || alignment.getSequences() == null || alignment.isNucleotide()
561                     ? !nucleotidesAllowed
562                     : !proteinAllowed)
563     {
564       return null;
565     }
566     if (inputSeqs == null || inputSeqs.getWidth() <= 0
567             || inputSeqs.getSequences() == null
568             || inputSeqs.getSequences().size() < 1)
569     {
570       inputSeqs = alignment;
571     }
572
573     List<SequenceI> seqs = new ArrayList<>();
574
575     int minlen = 10;
576     int ln = -1;
577     if (bySequence)
578     {
579       seqNames = new HashMap<>();
580     }
581     gapMap = new boolean[0];
582     start = inputSeqs.getStartRes();
583     end = inputSeqs.getEndRes();
584     // TODO: URGENT! unify with JPred / MSA code to handle hidden regions
585     // correctly
586     // TODO: push attributes into WsJob instance (so they can be safely
587     // persisted/restored
588     for (SequenceI sq : (inputSeqs.getSequences()))
589     {
590       if (bySequence
591               ? sq.findPosition(end + 1)
592                       - sq.findPosition(start + 1) > minlen - 1
593               : sq.getEnd() - sq.getStart() > minlen - 1)
594       {
595         String newname = SeqsetUtils.unique_name(seqs.size() + 1);
596         // make new input sequence with or without gaps
597         if (seqNames != null)
598         {
599           seqNames.put(newname, sq);
600         }
601         SequenceI seq;
602         if (submitGaps)
603         {
604           seqs.add(seq = new jalview.datamodel.Sequence(newname,
605                   sq.getSequenceAsString()));
606           if (gapMap == null || gapMap.length < seq.getLength())
607           {
608             boolean[] tg = gapMap;
609             gapMap = new boolean[seq.getLength()];
610             System.arraycopy(tg, 0, gapMap, 0, tg.length);
611             for (int p = tg.length; p < gapMap.length; p++)
612             {
613               gapMap[p] = false; // init as a gap
614             }
615           }
616           for (int apos : sq.gapMap())
617           {
618             char sqc = sq.getCharAt(apos);
619             if (!filterNonStandardResidues
620                     || (sq.isProtein() ? ResidueProperties.aaIndex[sqc] < 20
621                             : ResidueProperties.nucleotideIndex[sqc] < 5))
622             {
623               gapMap[apos] = true; // aligned and real amino acid residue
624             }
625             ;
626           }
627         }
628         else
629         {
630           // TODO: add ability to exclude hidden regions
631           seqs.add(seq = new jalview.datamodel.Sequence(newname,
632                   AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
633                           sq.getSequenceAsString(start, end + 1))));
634           // for annotation need to also record map to sequence start/end
635           // position in range
636           // then transfer back to original sequence on return.
637         }
638         if (seq.getLength() > ln)
639         {
640           ln = seq.getLength();
641         }
642       }
643     }
644     if (alignedSeqs && submitGaps)
645     {
646       realw = 0;
647       for (int i = 0; i < gapMap.length; i++)
648       {
649         if (gapMap[i])
650         {
651           realw++;
652         }
653       }
654       // try real hard to return something submittable
655       // TODO: some of AAcon measures need a minimum of two or three amino
656       // acids at each position, and AAcon doesn't gracefully degrade.
657       for (int p = 0; p < seqs.size(); p++)
658       {
659         SequenceI sq = seqs.get(p);
660         // strip gapped columns
661         char[] padded = new char[realw],
662                 orig = sq.getSequence();
663         for (int i = 0, pp = 0; i < realw; pp++)
664         {
665           if (gapMap[pp])
666           {
667             if (orig.length > pp)
668             {
669               padded[i++] = orig[pp];
670             }
671             else
672             {
673               padded[i++] = '-';
674             }
675           }
676         }
677         seqs.set(p, new jalview.datamodel.Sequence(sq.getName(),
678                 new String(padded)));
679       }
680     }
681     return seqs;
682   }
683
684   @Override
685   public void updateAnnotation()
686   {
687     updateResultAnnotation(false);
688   }
689
690   public void updateResultAnnotation(boolean immediate)
691   {
692     if ((immediate || !calcMan.isWorking(this)) && running != null
693             && running.hasResults())
694     {
695       List<AlignmentAnnotation> ourAnnot = running.getAnnotation();
696       updateOurAnnots(ourAnnot);
697     }
698   }
699
700   /**
701    * notify manager that we have started, and wait for a free calculation slot
702    * 
703    * @return true if slot is obtained and work still valid, false if another
704    *         thread has done our work for us.
705    */
706   protected boolean checkDone()
707   {
708     calcMan.notifyStart(this);
709     ap.paintAlignment(false, false);
710     while (!calcMan.notifyWorking(this))
711     {
712       if (calcMan.isWorking(this))
713       {
714         return true;
715       }
716       try
717       {
718         if (ap != null)
719         {
720           ap.paintAlignment(false, false);
721         }
722
723         Thread.sleep(200);
724       } catch (Exception ex)
725       {
726         ex.printStackTrace();
727       }
728     }
729     if (alignViewport.isClosed())
730     {
731       abortAndDestroy();
732       return true;
733     }
734     return false;
735   }
736
737   protected void updateOurAnnots(List<AlignmentAnnotation> ourAnnot)
738   {
739     List<AlignmentAnnotation> our = ourAnnots;
740     ourAnnots = ourAnnot;
741     AlignmentI alignment = alignViewport.getAlignment();
742     if (our != null)
743     {
744       if (our.size() > 0)
745       {
746         for (AlignmentAnnotation an : our)
747         {
748           if (!ourAnnots.contains(an))
749           {
750             // remove the old annotation
751             alignment.deleteAnnotation(an);
752           }
753         }
754       }
755       our.clear();
756       // validate rows and update Alignmment state
757       for (AlignmentAnnotation an : ourAnnots)
758       {
759         alignViewport.getAlignment().validateAnnotation(an);
760       }
761       // TODO: may need a menu refresh after this
762       // af.setMenusForViewport();
763       ap.adjustAnnotationHeight();
764     }
765   }
766
767   public SequenceAnnotationServiceI getService()
768   {
769     return annotService;
770   }
771
772 }