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