JAL-3070 configure annotation service manager from service’s input validation flags
[jalview.git] / src / jalview / ws / jws2 / SeqAnnotationServiceCalcWorker.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 SeqAnnotationServiceCalcWorker 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 SeqAnnotationServiceCalcWorker(AlignViewportI alignViewport,
155           AlignmentViewPanel alignPanel)
156   {
157     super(alignViewport, alignPanel);
158   }
159
160   public SeqAnnotationServiceCalcWorker(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     proteinAllowed = service.isProteinService();
187     nucleotidesAllowed = service.isNucleotideService();
188     alignedSeqs = service.isNeedsAlignedSequences();
189     bySequence = !service.isAlignmentAnalysis();
190     filterNonStandardResidues = service.isFilterSymbols();
191     min_valid_seqs = service.getMinimumInputSequences();
192
193     if (service.isInteractiveUpdate())
194     {
195       initViewportParams();
196     }
197   }
198
199   /**
200    * 
201    * @return true if the submission thread should attempt to submit data
202    */
203   public boolean hasService()
204   {
205     return annotService != null;
206   }
207
208   protected jalview.ws.api.SequenceAnnotationServiceI annotService = null;
209
210   volatile JobId rslt = null;
211
212   AnnotationWsJob running = null;
213
214   private int min_valid_seqs;
215
216   @Override
217   public void run()
218   {
219     if (!hasService())
220     {
221       return;
222     }
223
224     long progressId = -1;
225
226     int serverErrorsLeft = 3;
227     final boolean cancellable = CancellableI.class
228             .isAssignableFrom(annotService.getClass());
229     StringBuffer msg = new StringBuffer();
230     try
231     {
232       if (checkDone())
233       {
234         return;
235       }
236       List<SequenceI> seqs = getInputSequences(
237               alignViewport.getAlignment(),
238               bySequence ? alignViewport.getSelectionGroup() : null);
239
240       if (seqs == null || !checkValidInputSeqs(seqs))
241       {
242         jalview.bin.Cache.log.debug(
243                 "Sequences for analysis service were null or not valid");
244         calcMan.workerComplete(this);
245         return;
246       }
247
248       AlignmentAnnotation[] aa = alignViewport.getAlignment()
249               .getAlignmentAnnotation();
250       if (guiProgress != null)
251       {
252         guiProgress.setProgressBar(service.getActionText(),
253                 progressId = System.currentTimeMillis());
254       }
255       jalview.bin.Cache.log.debug("submitted " + seqs.size()
256               + " sequences to " + service.getActionText());
257
258       rslt = annotService.submitToService(seqs, getPreset(),
259               getArguments());
260       if (rslt == null)
261       {
262         return;
263       }
264       // TODO: handle job submission error reporting here.
265       
266       // ///
267       // otherwise, construct WsJob and any UI handlers
268       running = new AnnotationWsJob();
269       running.setJobHandle(rslt);
270       running.setSeqNames(seqNames);
271       running.setStartPos(start);
272       running.setSeqs(seqs);
273
274       if (guiProgress != null)
275       {
276         guiProgress.registerHandler(progressId,
277                 new IProgressIndicatorHandler()
278                 {
279
280                   @Override
281                   public boolean cancelActivity(long id)
282                   {
283                     ((CancellableI) annotService).cancel(running);
284                     return true;
285                   }
286
287                   @Override
288                   public boolean canCancel()
289                   {
290                     return cancellable;
291                   }
292                 });
293       }
294       
295       // ///
296       // and poll for updates until job finishes, fails or becomes stale
297       
298       boolean finished = false;
299       long rpos = 0;
300       do
301       {
302         Cache.log.debug("Updating status for annotation service.");
303         annotService.updateStatus(running);
304
305         if (running.isFinished())
306         {
307           Cache.log.debug("Analysis service job reported finished.");
308           finished = true;
309         }
310         else
311         {
312           Cache.log.debug("Status now " + running.getState());
313         }
314
315         if (calcMan.isPending(this) && isInteractiveUpdate())
316         {
317           Cache.log.debug("Analysis service job is stale. aborting.");
318           // job has become stale.
319           if (!finished) {
320             finished = true;
321             // cancel this job and yield to the new job
322             try
323             {
324               if (cancellable
325                         && ((CancellableI) annotService).cancel(running))
326               {
327                 System.err.println("Cancelled AACon job: " + rslt);
328               }
329               else
330               {
331                 System.err.println("FAILED TO CANCEL AACon job: " + rslt);
332               }
333   
334             } catch (Exception x)
335             {
336   
337             }
338           }
339           rslt = running.getJobHandle();
340           return;
341         }
342
343         // pull any stats - some services need to flush log output before
344         // results are available
345         Cache.log.debug("Updating progress log for annotation service.");
346
347         try
348         {
349         annotService.updateJobProgress(running);
350         } catch (Throwable thr)
351         {
352           Cache.log.debug("Ignoring exception during progress update.",
353                   thr);
354         }
355         Cache.log.trace("Result of poll: " + running.getStatus());
356
357         if (!finished && !running.isFailed())
358         {
359           try
360           {
361             Cache.log.debug("Analysis service job thread sleeping.");
362             Thread.sleep(200);
363             Cache.log.debug("Analysis service job thread woke.");
364           } catch (InterruptedException x)
365           {
366           }
367           ;
368         }
369       } while (!finished);
370
371       // TODO: need to poll/retry
372       if (serverErrorsLeft > 0)
373       {
374         try
375         {
376           Thread.sleep(200);
377         } catch (InterruptedException x)
378         {
379         }
380       }
381       // configure job with the associated view's feature renderer, if one
382       // exists.
383       // TODO: here one would also grab the 'master feature renderer' in order
384       // to enable/disable
385       // features automatically according to user preferences
386       running.setFeatureRenderer(
387               ((jalview.gui.AlignmentPanel) ap).cloneFeatureRenderer());
388       Cache.log.debug("retrieving job results.");
389       List<AlignmentAnnotation> returnedAnnot = annotService
390               .getAlignmentAnnotation(running, this);
391       Cache.log.debug("Obtained " + (returnedAnnot == null ? "no rows"
392               : ("" + returnedAnnot.size())));
393       running.setAnnotation(returnedAnnot);
394
395       if (running.hasResults())
396       {
397         jalview.bin.Cache.log.debug("Updating result annotation from Job "
398                 + rslt + " at " + service.getUri());
399         updateResultAnnotation(true);
400         if (running.isTransferSequenceFeatures())
401         {
402           jalview.bin.Cache.log.debug(
403                   "Updating feature display settings and transferring features from Job "
404                           + rslt + " at " + service.getUri());
405           ((jalview.gui.AlignmentPanel) ap)
406                   .updateFeatureRendererFrom(running.getFeatureRenderer());
407           // TODO: JAL-1150 - create sequence feature settings API for defining
408           // styles and enabling/disabling feature overlay on alignment panel
409
410           if (alignFrame.alignPanel == ap)
411           {
412             // only do this if the alignFrame is currently showing this view.
413             Desktop.getAlignFrameFor(alignViewport)
414                     .setShowSeqFeatures(true);
415           }
416         }
417         ap.adjustAnnotationHeight();
418       }
419       Cache.log.debug("Annotation Service Worker thread finished.");
420     }
421 // TODO: use service specitic exception handlers
422 //    catch (JobSubmissionException x)
423 //    {
424 //
425 //      System.err.println(
426 //              "submission error with " + getServiceActionText() + " :");
427 //      x.printStackTrace();
428 //      calcMan.disableWorker(this);
429 //    } catch (ResultNotAvailableException x)
430 //    {
431 //      System.err.println("collection error:\nJob ID: " + rslt);
432 //      x.printStackTrace();
433 //      calcMan.disableWorker(this);
434 //
435 //    } catch (OutOfMemoryError error)
436 //    {
437 //      calcMan.disableWorker(this);
438 //
439 //      ap.raiseOOMWarning(getServiceActionText(), error);
440 //    } 
441     catch (Throwable x)
442     {
443       calcMan.disableWorker(this);
444
445       System.err
446               .println("Blacklisting worker due to unexpected exception:");
447       x.printStackTrace();
448     } finally
449     {
450
451       calcMan.workerComplete(this);
452       if (ap != null)
453       {
454         calcMan.workerComplete(this);
455         if (guiProgress != null && progressId != -1)
456         {
457           guiProgress.setProgressBar("", progressId);
458         }
459         // TODO: may not need to paintAlignment again !
460         ap.paintAlignment(false, false);
461       }
462       if (msg.length() > 0)
463       {
464         // TODO: stash message somewhere in annotation or alignment view.
465         // code below shows result in a text box popup
466         /*
467          * jalview.gui.CutAndPasteTransfer cap = new
468          * jalview.gui.CutAndPasteTransfer(); cap.setText(msg.toString());
469          * jalview.gui.Desktop.addInternalFrame(cap,
470          * "Job Status for "+getServiceActionText(), 600, 400);
471          */
472       }
473     }
474
475   }
476
477   /**
478    * validate input for dynamic/non-dynamic update context TODO: move to
479    * analysis interface ?
480    * @param seqs
481    * 
482    * @return true if input is valid
483    */
484   boolean checkValidInputSeqs(List<SequenceI> seqs)
485   {
486     int nvalid = 0;
487     for (SequenceI sq : seqs)
488     {
489       if (sq.getStart() <= sq.getEnd()
490               && (sq.isProtein() ? proteinAllowed : nucleotidesAllowed))
491       {
492         if (submitGaps
493                 || sq.getLength() == (sq.getEnd() - sq.getStart() + 1))
494         {
495           nvalid++;
496         }
497       }
498     }
499     return nvalid >= min_valid_seqs;
500   }
501
502   public void cancelCurrentJob()
503   {
504     try
505     {
506       String id = running.getJobId();
507       if (((CancellableI) annotService).cancel(running))
508       {
509         System.err.println("Cancelled job " + id);
510       }
511       else
512       {
513         System.err.println("Job " + id + " couldn't be cancelled.");
514       }
515     } catch (Exception q)
516     {
517       q.printStackTrace();
518     }
519   }
520
521   /**
522    * Interactive updating. Analysis calculations that work on the currently
523    * displayed alignment data should cancel existing jobs when the input data
524    * has changed.
525    * 
526    * @return true if a running job should be cancelled because new input data is
527    *         available for analysis
528    */
529   boolean isInteractiveUpdate()
530   {
531     return service.isInteractiveUpdate();
532   }
533
534   /**
535    * decide what sequences will be analysed TODO: refactor to generate
536    * List<SequenceI> for submission to service interface
537    * 
538    * @param alignment
539    * @param inputSeqs
540    * @return
541    */
542   public List<SequenceI> getInputSequences(AlignmentI alignment,
543           AnnotatedCollectionI inputSeqs)
544   {
545     if (alignment == null || alignment.getWidth() <= 0
546             || alignment.getSequences() == null || alignment.isNucleotide()
547                     ? !nucleotidesAllowed
548                     : !proteinAllowed)
549     {
550       return null;
551     }
552     if (inputSeqs == null || inputSeqs.getWidth() <= 0
553             || inputSeqs.getSequences() == null
554             || inputSeqs.getSequences().size() < 1)
555     {
556       inputSeqs = alignment;
557     }
558
559     List<SequenceI> seqs = new ArrayList<>();
560
561     int minlen = 10;
562     int ln = -1;
563     if (bySequence)
564     {
565       seqNames = new HashMap<>();
566     }
567     gapMap = new boolean[0];
568     start = inputSeqs.getStartRes();
569     end = inputSeqs.getEndRes();
570     // TODO: URGENT! unify with JPred / MSA code to handle hidden regions
571     // correctly
572     // TODO: push attributes into WsJob instance (so they can be safely
573     // persisted/restored
574     for (SequenceI sq : (inputSeqs.getSequences()))
575     {
576       if (bySequence
577               ? sq.findPosition(end + 1)
578                       - sq.findPosition(start + 1) > minlen - 1
579               : sq.getEnd() - sq.getStart() > minlen - 1)
580       {
581         String newname = SeqsetUtils.unique_name(seqs.size() + 1);
582         // make new input sequence with or without gaps
583         if (seqNames != null)
584         {
585           seqNames.put(newname, sq);
586         }
587         SequenceI seq;
588         if (submitGaps)
589         {
590           seqs.add(seq = new jalview.datamodel.Sequence(newname,
591                   sq.getSequenceAsString()));
592           if (gapMap == null || gapMap.length < seq.getLength())
593           {
594             boolean[] tg = gapMap;
595             gapMap = new boolean[seq.getLength()];
596             System.arraycopy(tg, 0, gapMap, 0, tg.length);
597             for (int p = tg.length; p < gapMap.length; p++)
598             {
599               gapMap[p] = false; // init as a gap
600             }
601           }
602           for (int apos : sq.gapMap())
603           {
604             char sqc = sq.getCharAt(apos);
605             if (!filterNonStandardResidues
606                     || (sq.isProtein() ? ResidueProperties.aaIndex[sqc] < 20
607                             : ResidueProperties.nucleotideIndex[sqc] < 5))
608             {
609               gapMap[apos] = true; // aligned and real amino acid residue
610             }
611             ;
612           }
613         }
614         else
615         {
616           // TODO: add ability to exclude hidden regions
617           seqs.add(seq = new jalview.datamodel.Sequence(newname,
618                   AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
619                           sq.getSequenceAsString(start, end + 1))));
620           // for annotation need to also record map to sequence start/end
621           // position in range
622           // then transfer back to original sequence on return.
623         }
624         if (seq.getLength() > ln)
625         {
626           ln = seq.getLength();
627         }
628       }
629     }
630     if (alignedSeqs && submitGaps)
631     {
632       realw = 0;
633       for (int i = 0; i < gapMap.length; i++)
634       {
635         if (gapMap[i])
636         {
637           realw++;
638         }
639       }
640       // try real hard to return something submittable
641       // TODO: some of AAcon measures need a minimum of two or three amino
642       // acids at each position, and AAcon doesn't gracefully degrade.
643       for (int p = 0; p < seqs.size(); p++)
644       {
645         SequenceI sq = seqs.get(p);
646         // strip gapped columns
647         char[] padded = new char[realw],
648                 orig = sq.getSequence();
649         for (int i = 0, pp = 0; i < realw; pp++)
650         {
651           if (gapMap[pp])
652           {
653             if (orig.length > pp)
654             {
655               padded[i++] = orig[pp];
656             }
657             else
658             {
659               padded[i++] = '-';
660             }
661           }
662         }
663         seqs.set(p, new jalview.datamodel.Sequence(sq.getName(),
664                 new String(padded)));
665       }
666     }
667     return seqs;
668   }
669
670   @Override
671   public void updateAnnotation()
672   {
673     updateResultAnnotation(false);
674   }
675
676   public void updateResultAnnotation(boolean immediate)
677   {
678     if ((immediate || !calcMan.isWorking(this)) && running != null
679             && running.hasResults())
680     {
681       List<AlignmentAnnotation> ourAnnot = running.getAnnotation();
682       updateOurAnnots(ourAnnot);
683     }
684   }
685
686   /**
687    * notify manager that we have started, and wait for a free calculation slot
688    * 
689    * @return true if slot is obtained and work still valid, false if another
690    *         thread has done our work for us.
691    */
692   protected boolean checkDone()
693   {
694     calcMan.notifyStart(this);
695     ap.paintAlignment(false, false);
696     while (!calcMan.notifyWorking(this))
697     {
698       if (calcMan.isWorking(this))
699       {
700         return true;
701       }
702       try
703       {
704         if (ap != null)
705         {
706           ap.paintAlignment(false, false);
707         }
708
709         Thread.sleep(200);
710       } catch (Exception ex)
711       {
712         ex.printStackTrace();
713       }
714     }
715     if (alignViewport.isClosed())
716     {
717       abortAndDestroy();
718       return true;
719     }
720     return false;
721   }
722
723   protected void updateOurAnnots(List<AlignmentAnnotation> ourAnnot)
724   {
725     List<AlignmentAnnotation> our = ourAnnots;
726     ourAnnots = ourAnnot;
727     AlignmentI alignment = alignViewport.getAlignment();
728     if (our != null)
729     {
730       if (our.size() > 0)
731       {
732         for (AlignmentAnnotation an : our)
733         {
734           if (!ourAnnots.contains(an))
735           {
736             // remove the old annotation
737             alignment.deleteAnnotation(an);
738           }
739         }
740       }
741       our.clear();
742       // validate rows and update Alignmment state
743       for (AlignmentAnnotation an : ourAnnots)
744       {
745         alignViewport.getAlignment().validateAnnotation(an);
746       }
747       // TODO: may need a menu refresh after this
748       // af.setMenusForViewport();
749       ap.adjustAnnotationHeight();
750     }
751   }
752
753   public SequenceAnnotationServiceI getService()
754   {
755     return annotService;
756   }
757
758 }