1a361c48ec1107258d64738200ade5eaf3848b1e
[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.datamodel.AlignmentAnnotation;
28 import jalview.datamodel.AlignmentI;
29 import jalview.datamodel.AnnotatedCollectionI;
30 import jalview.datamodel.SequenceI;
31 import jalview.gui.AlignFrame;
32 import jalview.gui.IProgressIndicator;
33 import jalview.gui.IProgressIndicatorHandler;
34 import jalview.schemes.ResidueProperties;
35 import jalview.workers.AlignCalcWorker;
36 import jalview.ws.jws2.dm.AAConSettings;
37 import jalview.ws.jws2.dm.JabaWsParamSet;
38 import jalview.ws.jws2.jabaws2.Jws2Instance;
39 import jalview.ws.params.ArgumentI;
40 import jalview.ws.params.WsParamSetI;
41
42 import java.util.ArrayList;
43 import java.util.HashMap;
44 import java.util.List;
45 import java.util.Map;
46
47 import compbio.data.sequence.FastaSequence;
48 import compbio.metadata.ChunkHolder;
49 import compbio.metadata.JobStatus;
50 import compbio.metadata.JobSubmissionException;
51 import compbio.metadata.Option;
52 import compbio.metadata.ResultNotAvailableException;
53
54 public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
55 {
56
57   protected Jws2Instance service;
58
59   protected WsParamSetI preset;
60
61   protected List<ArgumentI> arguments;
62
63   protected IProgressIndicator guiProgress;
64
65   protected boolean submitGaps = true;
66
67   /**
68    * by default, we filter out non-standard residues before submission
69    */
70   protected boolean filterNonStandardResidues = true;
71
72   /**
73    * Recover any existing parameters for this service
74    */
75   protected void initViewportParams()
76   {
77     if (getCalcId() != null)
78     {
79       ((jalview.gui.AlignViewport) alignViewport).setCalcIdSettingsFor(
80               getCalcId(),
81               new AAConSettings(true, service, this.preset, arguments),
82               true);
83     }
84   }
85
86   /**
87    * 
88    * @return null or a string used to recover all annotation generated by this
89    *         worker
90    */
91   public abstract String getCalcId();
92
93   public WsParamSetI getPreset()
94   {
95     return preset;
96   }
97
98   public List<ArgumentI> getArguments()
99   {
100     return arguments;
101   }
102
103   /**
104    * reconfigure and restart the AAConClient. This method will spawn a new
105    * thread that will wait until any current jobs are finished, modify the
106    * parameters and restart the conservation calculation with the new values.
107    * 
108    * @param newpreset
109    * @param newarguments
110    */
111   public void updateParameters(final WsParamSetI newpreset,
112           final List<ArgumentI> newarguments)
113   {
114     preset = newpreset;
115     arguments = newarguments;
116     calcMan.startWorker(this);
117     initViewportParams();
118   }
119
120   public List<Option> getJabaArguments()
121   {
122     List<Option> newargs = new ArrayList<>();
123     if (preset != null && preset instanceof JabaWsParamSet)
124     {
125       newargs.addAll(((JabaWsParamSet) preset).getjabaArguments());
126     }
127     if (arguments != null && arguments.size() > 0)
128     {
129       for (Object rg : JabaParamStore.getJabafromJwsArgs(arguments))
130       {
131         if (Option.class.isAssignableFrom(rg.getClass()))
132         {
133           newargs.add((Option) rg);
134         }
135       }
136     }
137     return newargs;
138   }
139
140   protected boolean alignedSeqs = true;
141
142   protected boolean nucleotidesAllowed = false;
143
144   protected boolean proteinAllowed = false;
145
146   /**
147    * record sequences for mapping result back to afterwards
148    */
149   protected boolean bySequence = false;
150
151   protected Map<String, SequenceI> seqNames;
152
153   protected boolean[] gapMap;
154
155   int realw;
156
157   protected int start;
158
159   int end;
160
161   public AbstractJabaCalcWorker(AlignViewportI alignViewport,
162           AlignmentViewPanel alignPanel)
163   {
164     super(alignViewport, alignPanel);
165   }
166
167   public AbstractJabaCalcWorker(Jws2Instance service, AlignFrame alignFrame,
168           WsParamSetI preset, List<ArgumentI> paramset)
169   {
170     this(alignFrame.getCurrentView(), alignFrame.alignPanel);
171     this.guiProgress = alignFrame;
172     this.preset = preset;
173     this.arguments = paramset;
174     this.service = service;
175   }
176
177   /**
178    * 
179    * @return true if the submission thread should attempt to submit data
180    */
181   abstract boolean hasService();
182
183   volatile String rslt = "JOB NOT DEFINED";
184
185   @Override
186   public void run()
187   {
188     if (!hasService())
189     {
190       return;
191     }
192     long progressId = -1;
193
194     int serverErrorsLeft = 3;
195
196     StringBuffer msg = new StringBuffer();
197     try
198     {
199       if (checkDone())
200       {
201         return;
202       }
203       List<compbio.data.sequence.FastaSequence> seqs = getInputSequences(
204               alignViewport.getAlignment(),
205               bySequence ? alignViewport.getSelectionGroup() : null);
206
207       if (seqs == null || !checkValidInputSeqs(true, seqs))
208       {
209         calcMan.workerComplete(this);
210         return;
211       }
212
213       AlignmentAnnotation[] aa = alignViewport.getAlignment()
214               .getAlignmentAnnotation();
215       if (guiProgress != null)
216       {
217         guiProgress.setProgressBar("JABA " + getServiceActionText(),
218                 progressId = System.currentTimeMillis());
219       }
220       rslt = submitToService(seqs);
221       if (guiProgress != null)
222       {
223         guiProgress.registerHandler(progressId,
224                 new IProgressIndicatorHandler()
225                 {
226
227                   @Override
228                   public boolean cancelActivity(long id)
229                   {
230                     cancelCurrentJob();
231                     return true;
232                   }
233
234                   @Override
235                   public boolean canCancel()
236                   {
237                     return true;
238                   }
239                 });
240       }
241       boolean finished = false;
242       long rpos = 0;
243       do
244       {
245         JobStatus status = getJobStatus(rslt);
246         if (status.equals(JobStatus.FINISHED))
247         {
248           finished = true;
249         }
250         if (calcMan.isPending(this) && isInteractiveUpdate())
251         {
252           finished = true;
253           // cancel this job and yield to the new job
254           try
255           {
256             if (cancelJob(rslt))
257             {
258               System.err.println("Cancelled AACon job: " + rslt);
259             }
260             else
261             {
262               System.err.println("FAILED TO CANCEL AACon job: " + rslt);
263             }
264
265           } catch (Exception x)
266           {
267
268           }
269           rslt = "CANCELLED JOB";
270           return;
271         }
272         long cpos;
273         ChunkHolder stats = null;
274         do
275         {
276           cpos = rpos;
277           boolean retry = false;
278           do
279           {
280             try
281             {
282               stats = pullExecStatistics(rslt, rpos);
283             } catch (Exception x)
284             {
285
286               if (x.getMessage().contains(
287                       "Position in a file could not be negative!"))
288               {
289                 // squash index out of bounds exception- seems to happen for
290                 // disorder predictors which don't (apparently) produce any
291                 // progress information and JABA server throws an exception
292                 // because progress length is -1.
293                 stats = null;
294               }
295               else
296               {
297                 if (--serverErrorsLeft > 0)
298                 {
299                   retry = true;
300                   try
301                   {
302                     Thread.sleep(200);
303                   } catch (InterruptedException q)
304                   {
305                   }
306                   ;
307                 }
308                 else
309                 {
310                   throw x;
311                 }
312               }
313             }
314           } while (retry);
315           if (stats != null)
316           {
317             System.out.print(stats.getChunk());
318             msg.append(stats);
319             rpos = stats.getNextPosition();
320           }
321         } while (stats != null && rpos > cpos);
322
323         if (!finished && status.equals(JobStatus.FAILED))
324         {
325           try
326           {
327             Thread.sleep(200);
328           } catch (InterruptedException x)
329           {
330           }
331           ;
332         }
333       } while (!finished);
334       if (serverErrorsLeft > 0)
335       {
336         try
337         {
338           Thread.sleep(200);
339         } catch (InterruptedException x)
340         {
341         }
342         if (collectAnnotationResultsFor(rslt))
343         {
344           jalview.bin.Cache.log.debug("Updating result annotation from Job "
345                   + rslt + " at " + service.getUri());
346           updateResultAnnotation(true);
347           ap.adjustAnnotationHeight();
348         }
349       }
350     }
351
352     catch (JobSubmissionException x)
353     {
354
355       System.err.println(
356               "submission error with " + getServiceActionText() + " :");
357       x.printStackTrace();
358       calcMan.disableWorker(this);
359     } catch (ResultNotAvailableException x)
360     {
361       System.err.println("collection error:\nJob ID: " + rslt);
362       x.printStackTrace();
363       calcMan.disableWorker(this);
364
365     } catch (OutOfMemoryError error)
366     {
367       calcMan.disableWorker(this);
368
369       // consensus = null;
370       // hconsensus = null;
371       ap.raiseOOMWarning(getServiceActionText(), error);
372     } catch (Exception x)
373     {
374       calcMan.disableWorker(this);
375
376       // consensus = null;
377       // hconsensus = null;
378       System.err
379               .println("Blacklisting worker due to unexpected exception:");
380       x.printStackTrace();
381     } finally
382     {
383
384       calcMan.workerComplete(this);
385       if (ap != null)
386       {
387         calcMan.workerComplete(this);
388         if (guiProgress != null && progressId != -1)
389         {
390           guiProgress.setProgressBar("", progressId);
391         }
392         // TODO: may not need to paintAlignment again !
393         ap.paintAlignment(false, false);
394       }
395       if (msg.length() > 0)
396       {
397         // TODO: stash message somewhere in annotation or alignment view.
398         // code below shows result in a text box popup
399         /*
400          * jalview.gui.CutAndPasteTransfer cap = new
401          * jalview.gui.CutAndPasteTransfer(); cap.setText(msg.toString());
402          * jalview.gui.Desktop.addInternalFrame(cap,
403          * "Job Status for "+getServiceActionText(), 600, 400);
404          */
405       }
406     }
407
408   }
409
410   /**
411    * validate input for dynamic/non-dynamic update context
412    * 
413    * @param dynamic
414    * @param seqs
415    * @return true if input is valid
416    */
417   abstract boolean checkValidInputSeqs(boolean dynamic,
418           List<FastaSequence> seqs);
419
420   abstract String submitToService(
421           List<compbio.data.sequence.FastaSequence> seqs)
422           throws JobSubmissionException;
423
424   abstract boolean cancelJob(String rslt) throws Exception;
425
426   abstract JobStatus getJobStatus(String rslt) throws Exception;
427
428   abstract ChunkHolder pullExecStatistics(String rslt, long rpos);
429
430   abstract boolean collectAnnotationResultsFor(String rslt)
431           throws ResultNotAvailableException;
432
433   public void cancelCurrentJob()
434   {
435     try
436     {
437       String id = rslt;
438       if (cancelJob(rslt))
439       {
440         System.err.println("Cancelled job " + id);
441       }
442       else
443       {
444         System.err.println("Job " + id + " couldn't be cancelled.");
445       }
446     } catch (Exception q)
447     {
448       q.printStackTrace();
449     }
450   }
451
452   /**
453    * Interactive updating. Analysis calculations that work on the currently
454    * displayed alignment data should cancel existing jobs when the input data
455    * has changed.
456    * 
457    * @return true if a running job should be cancelled because new input data is
458    *         available for analysis
459    */
460   abstract boolean isInteractiveUpdate();
461
462   public List<FastaSequence> getInputSequences(AlignmentI alignment,
463           AnnotatedCollectionI inputSeqs)
464   {
465     if (alignment == null || alignment.getWidth() <= 0
466             || alignment.getSequences() == null || alignment.isNucleotide()
467                     ? !nucleotidesAllowed
468                     : !proteinAllowed)
469     {
470       return null;
471     }
472     if (inputSeqs == null || inputSeqs.getWidth() <= 0
473             || inputSeqs.getSequences() == null
474             || inputSeqs.getSequences().size() < 1)
475     {
476       inputSeqs = alignment;
477     }
478
479     List<compbio.data.sequence.FastaSequence> seqs = new ArrayList<>();
480
481     int minlen = 10;
482     int ln = -1;
483     if (bySequence)
484     {
485       seqNames = new HashMap<>();
486     }
487     gapMap = new boolean[0];
488     start = inputSeqs.getStartRes();
489     end = inputSeqs.getEndRes();
490
491     for (SequenceI sq : (inputSeqs.getSequences()))
492     {
493       if (bySequence
494               ? sq.findPosition(end + 1)
495                       - sq.findPosition(start + 1) > minlen - 1
496               : sq.getEnd() - sq.getStart() > minlen - 1)
497       {
498         String newname = SeqsetUtils.unique_name(seqs.size() + 1);
499         // make new input sequence with or without gaps
500         if (seqNames != null)
501         {
502           seqNames.put(newname, sq);
503         }
504         FastaSequence seq;
505         if (submitGaps)
506         {
507           seqs.add(seq = new compbio.data.sequence.FastaSequence(newname,
508                   sq.getSequenceAsString()));
509           if (gapMap == null || gapMap.length < seq.getSequence().length())
510           {
511             boolean[] tg = gapMap;
512             gapMap = new boolean[seq.getLength()];
513             System.arraycopy(tg, 0, gapMap, 0, tg.length);
514             for (int p = tg.length; p < gapMap.length; p++)
515             {
516               gapMap[p] = false; // init as a gap
517             }
518           }
519           for (int apos : sq.gapMap())
520           {
521             char sqc = sq.getCharAt(apos);
522             if (!filterNonStandardResidues
523                     || (sq.isProtein() ? ResidueProperties.aaIndex[sqc] < 20
524                             : ResidueProperties.nucleotideIndex[sqc] < 5))
525             {
526               gapMap[apos] = true; // aligned and real amino acid residue
527             }
528             ;
529           }
530         }
531         else
532         {
533           seqs.add(seq = new compbio.data.sequence.FastaSequence(newname,
534                   AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
535                           sq.getSequenceAsString(start, end + 1))));
536         }
537         if (seq.getSequence().length() > ln)
538         {
539           ln = seq.getSequence().length();
540         }
541       }
542     }
543     if (alignedSeqs && submitGaps)
544     {
545       realw = 0;
546       for (int i = 0; i < gapMap.length; i++)
547       {
548         if (gapMap[i])
549         {
550           realw++;
551         }
552       }
553       // try real hard to return something submittable
554       // TODO: some of AAcon measures need a minimum of two or three amino
555       // acids at each position, and AAcon doesn't gracefully degrade.
556       for (int p = 0; p < seqs.size(); p++)
557       {
558         FastaSequence sq = seqs.get(p);
559         int l = sq.getSequence().length();
560         // strip gapped columns
561         char[] padded = new char[realw],
562                 orig = sq.getSequence().toCharArray();
563         for (int i = 0, pp = 0; i < realw; pp++)
564         {
565           if (gapMap[pp])
566           {
567             if (orig.length > pp)
568             {
569               padded[i++] = orig[pp];
570             }
571             else
572             {
573               padded[i++] = '-';
574             }
575           }
576         }
577         seqs.set(p, new compbio.data.sequence.FastaSequence(sq.getId(),
578                 new String(padded)));
579       }
580     }
581     return seqs;
582   }
583
584   @Override
585   public void updateAnnotation()
586   {
587     updateResultAnnotation(false);
588   }
589
590   public abstract void updateResultAnnotation(boolean immediate);
591
592   public abstract String getServiceActionText();
593
594   /**
595    * notify manager that we have started, and wait for a free calculation slot
596    * 
597    * @return true if slot is obtained and work still valid, false if another
598    *         thread has done our work for us.
599    */
600   protected boolean checkDone()
601   {
602     calcMan.notifyStart(this);
603     ap.paintAlignment(false, false);
604     while (!calcMan.notifyWorking(this))
605     {
606       if (calcMan.isWorking(this))
607       {
608         return true;
609       }
610       try
611       {
612         if (ap != null)
613         {
614           ap.paintAlignment(false, false);
615         }
616
617         Thread.sleep(200);
618       } catch (Exception ex)
619       {
620         ex.printStackTrace();
621       }
622     }
623     if (alignViewport.isClosed())
624     {
625       abortAndDestroy();
626       return true;
627     }
628     return false;
629   }
630
631   protected void updateOurAnnots(List<AlignmentAnnotation> ourAnnot)
632   {
633     List<AlignmentAnnotation> our = ourAnnots;
634     ourAnnots = ourAnnot;
635     AlignmentI alignment = alignViewport.getAlignment();
636     if (our != null)
637     {
638       if (our.size() > 0)
639       {
640         for (AlignmentAnnotation an : our)
641         {
642           if (!ourAnnots.contains(an))
643           {
644             // remove the old annotation
645             alignment.deleteAnnotation(an);
646           }
647         }
648       }
649       our.clear();
650
651       ap.adjustAnnotationHeight();
652     }
653   }
654
655 }