pullJobExecutionStats for JAL-493
[jalview.git] / src / jalview / ws / jws2 / MsaWSThread.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.5)
3  * Copyright (C) 2010 J Procter, AM Waterhouse, G Barton, M Clamp, S Searle
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 of the License, or (at your option) any later version.
10  * 
11  * Jalview is distributed in the hope that it will be useful, but 
12  * WITHOUT ANY WARRANTY; without even the implied warranty 
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
14  * PURPOSE.  See the GNU General Public License for more details.
15  * 
16  * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 package jalview.ws.jws2;
19
20 import java.util.*;
21
22 import compbio.data.msa.MsaWS;
23 import compbio.data.sequence.AlignmentMetadata;
24 import compbio.data.sequence.Program;
25 import compbio.metadata.ChunkHolder;
26 import compbio.metadata.JobStatus;
27
28 import jalview.analysis.*;
29 import jalview.bin.*;
30 import jalview.datamodel.*;
31 import jalview.gui.*;
32 import jalview.ws.AWsJob;
33 import jalview.ws.WSClientI;
34 import jalview.ws.JobStateSummary;
35
36 /**
37  * <p>
38  * Title:
39  * </p>
40  * 
41  * <p>
42  * Description:
43  * </p>
44  * 
45  * <p>
46  * Copyright: Copyright (c) 2004
47  * </p>
48  * 
49  * <p>
50  * Company: Dundee University
51  * </p>
52  * 
53  * @author not attributable
54  * @version 1.0
55  */
56 class MsaWSThread extends AWS2Thread implements WSClientI
57 {
58   boolean submitGaps = false; // pass sequences including gaps to alignment
59
60   // service
61
62   boolean preserveOrder = true; // and always store and recover sequence
63
64   // order
65
66   class MsaWSJob extends JWs2Job
67   {
68     long lastChunk=0;
69     
70     /**
71      * input 
72      */
73     ArrayList<compbio.data.sequence.FastaSequence> seqs = new ArrayList<compbio.data.sequence.FastaSequence>();
74     /**
75      * output
76      */
77     compbio.data.sequence.Alignment alignment;
78     // set if the job didn't get run - then the input is simply returned to the user
79     private boolean returnInput=false;
80     
81     /**
82      * MsaWSJob
83      * 
84      * @param jobNum
85      *          int
86      * @param jobId
87      *          String
88      */
89     public MsaWSJob(int jobNum, SequenceI[] inSeqs)
90     {
91       this.jobnum = jobNum;
92       if (!prepareInput(inSeqs, 2))
93       {
94         submitted = true;
95         subjobComplete = true;
96         returnInput = true;
97       }
98
99     }
100
101     Hashtable<String,Map> SeqNames = new Hashtable();
102
103     Vector<String[]> emptySeqs = new Vector();
104
105     /**
106      * prepare input sequences for MsaWS service
107      * 
108      * @param seqs
109      *          jalview sequences to be prepared
110      * @param minlen
111      *          minimum number of residues required for this MsaWS service
112      * @return true if seqs contains sequences to be submitted to service.
113      */
114     // TODO: return compbio.seqs list or nothing to indicate validity.
115     private boolean prepareInput(SequenceI[] seqs, int minlen)
116     {
117       int nseqs = 0;
118       if (minlen < 0)
119       {
120         throw new Error(
121                 "Implementation error: minlen must be zero or more.");
122       }
123       for (int i = 0; i < seqs.length; i++)
124       {
125         if (seqs[i].getEnd() - seqs[i].getStart() > minlen - 1)
126         {
127           nseqs++;
128         }
129       }
130       boolean valid = nseqs > 1; // need at least two seqs
131       compbio.data.sequence.FastaSequence seq;
132       for (int i = 0, n = 0; i < seqs.length; i++)
133       {
134
135         String newname = jalview.analysis.SeqsetUtils.unique_name(i); // same
136         // for
137         // any
138         // subjob
139         SeqNames.put(newname, jalview.analysis.SeqsetUtils
140                 .SeqCharacterHash(seqs[i]));
141         if (valid && seqs[i].getEnd() - seqs[i].getStart() > minlen - 1)
142         {
143           // make new input sequence with or without gaps
144           seq = new compbio.data.sequence.FastaSequence(newname,
145                   (submitGaps) ? seqs[i].getSequenceAsString()
146                   : AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
147                           seqs[i].getSequenceAsString()));
148           this.seqs.add(seq);
149         }
150         else
151         {
152           String empty = null;
153           if (seqs[i].getEnd() >= seqs[i].getStart())
154           {
155             empty = (submitGaps) ? seqs[i].getSequenceAsString() : AlignSeq
156                     .extractGaps(jalview.util.Comparison.GapChars, seqs[i]
157                             .getSequenceAsString());
158           }
159           emptySeqs.add(new String[]
160           { newname, empty });
161         }
162       }
163       return valid;
164     }
165
166     /**
167      * 
168      * @return true if getAlignment will return a valid alignment result.
169      */
170     public boolean hasResults()
171     {
172       if (subjobComplete && isFinished() && (alignment!=null || (emptySeqs!=null && emptySeqs.size()>0)))
173       {
174         return true;
175       }
176       return false;
177     }
178
179     /**
180      * 
181      * get the alignment including any empty sequences in the original order with original ids. Caller must access the alignment.getMetadata() object to annotate the final result passsed to the user.
182      * @return { SequenceI[], AlignmentOrder }
183      */
184     public Object[] getAlignment()
185     {
186       // is this a generic subjob or a Jws2 specific Object[] return signature
187       if (hasResults())
188       {
189         SequenceI[] alseqs = null;
190         char alseq_gapchar = '-';
191         int alseq_l = 0;
192         if (alignment.getSequences().size()>0)
193         {
194           alseqs = new SequenceI[alignment.getSequences().size()];
195           for (compbio.data.sequence.FastaSequence seq: alignment.getSequences())
196           {
197             alseqs[alseq_l++] = new Sequence(seq.getId(),seq.getSequence());
198           }
199           alseq_gapchar = alignment.getMetadata().getGapchar();
200           
201         }
202         // add in the empty seqs.
203         if (emptySeqs.size() > 0)
204         {
205           SequenceI[] t_alseqs = new SequenceI[alseq_l + emptySeqs.size()];
206           // get width
207           int i, w = 0;
208           if (alseq_l > 0)
209           {
210             for (i = 0, w = alseqs[0].getLength(); i < alseq_l; i++)
211             {
212               if (w < alseqs[i].getLength())
213               {
214                 w = alseqs[i].getLength();
215               }
216               t_alseqs[i] = alseqs[i];
217               alseqs[i] = null;
218             }
219           }
220           // check that aligned width is at least as wide as emptySeqs width.
221           int ow = w, nw = w;
222           for (i = 0, w = emptySeqs.size(); i < w; i++)
223           {
224             String[] es = (String[]) emptySeqs.get(i);
225             if (es != null && es[1] != null)
226             {
227               int sw = es[1].length();
228               if (nw < sw)
229               {
230                 nw = sw;
231               }
232             }
233           }
234           // make a gapped string.
235           StringBuffer insbuff = new StringBuffer(w);
236           for (i = 0; i < nw; i++)
237           {
238             insbuff.append(alseq_gapchar);
239           }
240           if (ow < nw)
241           {
242             for (i = 0; i < alseq_l; i++)
243             {
244               int sw = t_alseqs[i].getLength();
245               if (nw > sw)
246               {
247                 // pad at end
248                 alseqs[i].setSequence(t_alseqs[i].getSequenceAsString()
249                         + insbuff.substring(0, sw - nw));
250               }
251             }
252           }
253           for (i = 0, w = emptySeqs.size(); i < w; i++)
254           {
255             String[] es = (String[]) emptySeqs.get(i);
256             if (es[1] == null)
257             {
258               t_alseqs[i + alseq_l] = new jalview.datamodel.Sequence(es[0],
259                       insbuff.toString(), 1, 0);
260             }
261             else
262             {
263               if (es[1].length() < nw)
264               {
265                 t_alseqs[i + alseq_l] = new jalview.datamodel.Sequence(
266                         es[0],
267                         es[1] + insbuff.substring(0, nw - es[1].length()),
268                         1, 1 + es[1].length());
269               }
270               else
271               {
272                 t_alseqs[i + alseq_l] = new jalview.datamodel.Sequence(
273                         es[0], es[1]);
274               }
275             }
276           }
277           alseqs = t_alseqs;
278         }
279         AlignmentOrder msaorder = new AlignmentOrder(alseqs);
280         // always recover the order - makes parseResult()'s life easier.
281         jalview.analysis.AlignmentSorter.recoverOrder(alseqs);
282         // account for any missing sequences
283         jalview.analysis.SeqsetUtils.deuniquify(SeqNames, alseqs);
284         return new Object[]
285         { alseqs, msaorder };
286       }
287       return null;
288     }
289
290     /**
291      * mark subjob as cancelled and set result object appropriatly
292      */
293     void cancel()
294     {
295       cancelled = true;
296       subjobComplete = true;
297       alignment = null;
298     }
299
300     /**
301      * 
302      * @return boolean true if job can be submitted.
303      */
304     public boolean hasValidInput()
305     {
306       // TODO: get attributes for this MsaWS instance to check if it can do two sequence alignment.
307       if (seqs != null && seqs.size()>=2) // two or more sequences is valid ? 
308       {
309         return true;
310       }
311       return false;
312     }
313     StringBuffer jobProgress=new StringBuffer();
314     public void setStatus(String string)
315     {
316       jobProgress.setLength(0);
317       jobProgress.append(string);
318     }
319
320     @Override
321     public String getStatus()
322     {
323       return jobProgress.toString();
324     }
325
326     @Override
327     public boolean hasStatus()
328     {
329       return jobProgress!=null;
330     }
331
332     /**
333      * @return the lastChunk
334      */
335     public long getLastChunk()
336     {
337       return lastChunk;
338     }
339
340     /**
341      * @param lastChunk the lastChunk to set
342      */
343     public void setLastChunk(long lastChunk)
344     {
345       this.lastChunk = lastChunk;
346     }
347     
348   }
349
350   String alTitle; // name which will be used to form new alignment window.
351
352   Alignment dataset; // dataset to which the new alignment will be
353
354   // associated.
355
356   @SuppressWarnings("unchecked")
357   MsaWS server = null;
358
359   /**
360    * set basic options for this (group) of Msa jobs
361    * 
362    * @param subgaps
363    *          boolean
364    * @param presorder
365    *          boolean
366    */
367   MsaWSThread(MsaWS server, String wsUrl,
368           WebserviceInfo wsinfo, jalview.gui.AlignFrame alFrame,
369           AlignmentView alview, String wsname, boolean subgaps,
370           boolean presorder)
371   {
372     super(alFrame, wsinfo, alview, wsname, wsUrl);
373     this.server = server;
374     this.submitGaps = subgaps;
375     this.preserveOrder = presorder;
376   }
377
378   /**
379    * create one or more Msa jobs to align visible seuqences in _msa
380    * 
381    * @param title
382    *          String
383    * @param _msa
384    *          AlignmentView
385    * @param subgaps
386    *          boolean
387    * @param presorder
388    *          boolean
389    * @param seqset
390    *          Alignment
391    */
392   MsaWSThread(MsaWS server2, String wsUrl,
393           WebserviceInfo wsinfo, jalview.gui.AlignFrame alFrame,
394           String wsname, String title, AlignmentView _msa, boolean subgaps,
395           boolean presorder, Alignment seqset)
396   {
397     this(server2,wsUrl, wsinfo, alFrame, _msa, wsname, subgaps, presorder);
398     OutputHeader = wsInfo.getProgressText();
399     alTitle = title;
400     dataset = seqset;
401
402     SequenceI[][] conmsa = _msa.getVisibleContigs('-');
403     if (conmsa != null)
404     {
405       int njobs = conmsa.length;
406       jobs = new MsaWSJob[njobs];
407       for (int j = 0; j < njobs; j++)
408       {
409         if (j != 0)
410         {
411           jobs[j] = new MsaWSJob(wsinfo.addJobPane(), conmsa[j]);
412         }
413         else
414         {
415           jobs[j] = new MsaWSJob(0, conmsa[j]);
416         }
417         if (njobs > 0)
418         {
419           wsinfo
420                   .setProgressName("region " + jobs[j].getJobnum(),
421                           jobs[j].getJobnum());
422         }
423         wsinfo.setProgressText(jobs[j].getJobnum(), OutputHeader);
424       }
425     }
426   }
427
428   public boolean isCancellable()
429   {
430     return true;
431   }
432
433   public void cancelJob()
434   {
435     if (!jobComplete && jobs != null)
436     {
437       boolean cancelled = true;
438       for (int job = 0; job < jobs.length; job++)
439       {
440         if (jobs[job].isSubmitted() && !jobs[job].isSubjobComplete())
441         {
442           String cancelledMessage = "";
443           try
444           {
445             boolean cancelledJob = server
446                     .cancelJob(jobs[job].getJobId());
447             if (cancelledJob)
448             {
449               // CANCELLED_JOB
450               cancelledMessage = "Job cancelled.";
451               ((MsaWSJob) jobs[job]).cancel(); // TODO: refactor to avoid this ugliness - 
452               wsInfo.setStatus(jobs[job].getJobnum(),
453                       WebserviceInfo.STATE_CANCELLED_OK);
454             } 
455             else 
456             {
457               // VALID UNSTOPPABLE JOB
458               cancelledMessage += "Server cannot cancel this job. just close the window.\n";
459               cancelled = false;
460               // wsInfo.setStatus(jobs[job].jobnum,
461               // WebserviceInfo.STATE_RUNNING);
462             }
463           } catch (Exception exc)
464           {
465             cancelledMessage += ("\nProblems cancelling the job : Exception received...\n"
466                     + exc + "\n");
467             Cache.log.warn(
468                     "Exception whilst cancelling " + jobs[job].getJobId(), exc);
469           }
470           wsInfo.setProgressText(jobs[job].getJobnum(), OutputHeader
471                   + cancelledMessage + "\n");
472         }
473       }
474       if (cancelled)
475       {
476         wsInfo.setStatus(WebserviceInfo.STATE_CANCELLED_OK);
477         jobComplete = true;
478       }
479       this.interrupt(); // kick thread to update job states.
480     }
481     else
482     {
483       if (!jobComplete)
484       {
485         wsInfo
486                 .setProgressText(OutputHeader
487                         + "Server cannot cancel this job because it has not been submitted properly. just close the window.\n");
488       }
489     }
490   }
491
492   public void pollJob(AWsJob job) throws Exception
493   {
494     // TODO: investigate if we still need to cast here in J1.6
495     MsaWSJob j=((MsaWSJob) job);
496     // this is standard code, but since the interface doesn't comprise of a basic one that implements (getJobStatus, pullExecStatistics) we have to repeat the code for all jw2s services.
497     j.setjobStatus(server.getJobStatus(job.getJobId()));
498     updateJobProgress(j);
499   }
500   protected void updateJobProgress(MsaWSJob j) throws Exception {
501     StringBuffer response = j.jobProgress;
502     long lastchunk = j.getLastChunk();
503     do {
504       j.setLastChunk(lastchunk);
505       ChunkHolder chunk = server.pullExecStatistics(j.getJobId(), lastchunk);
506       if (chunk!=null) {
507         response.append(chunk.getChunk());
508         lastchunk = chunk.getNextPosition();
509       };
510     } while (lastchunk>=0 && j.getLastChunk()!=lastchunk);
511   }
512
513   public void StartJob(AWsJob job)
514   {
515     // boiler plate template
516     if (!(job instanceof MsaWSJob))
517     {
518       throw new Error("StartJob(MsaWSJob) called on a WSJobInstance "
519               + job.getClass());
520     }
521     MsaWSJob j = (MsaWSJob) job;
522     if (j.isSubmitted())
523     {
524       if (Cache.log.isDebugEnabled())
525       {
526         Cache.log.debug("Tried to submit an already submitted job "
527                 + j.getJobId());
528       }
529       return;
530     }
531     // end boilerplate
532     
533     if (j.seqs == null || j.seqs.size()==0)
534     {
535       // special case - selection consisted entirely of empty sequences...
536       j.setjobStatus(JobStatus.FINISHED);
537       j.setStatus("Empty Alignment Job");
538     }
539     try
540     {
541       // TODO: get the parameters (if any) for this job and submit the job
542       j.setJobId(server.align(j.seqs));
543
544       if (j.getJobId()!= null)
545       {
546         j.setSubmitted(true);
547         j.setSubjobComplete(false);
548         // System.out.println(WsURL + " Job Id '" + jobId + "'");
549       }
550       else
551       {
552         throw new Exception(
553                   "Server at "
554                           + WsUrl
555                           + " returned null string for job id, it probably cannot be contacted. Try again later ?");
556         }
557     } catch (Exception e)
558     {
559       // Boilerplate code here
560       // TODO: JBPNote catch timeout or other fault types explicitly
561       // For unexpected errors
562       System.err
563               .println(WebServiceName
564                       + "Client: Failed to submit the sequences for alignment (probably a server side problem)\n"
565                       + "When contacting Server:" + WsUrl + "\n"
566                       + e.toString() + "\n");
567       j.setAllowedServerExceptions(0);
568       wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR);
569       wsInfo.setStatus(j.getJobnum(), WebserviceInfo.STATE_STOPPED_SERVERERROR);
570       wsInfo
571               .appendProgressText(
572                       j.getJobnum(),
573                       "Failed to submit sequences for alignment.\n"
574                               + "It is most likely that there is a problem with the server.\n"
575                               + "Just close the window\n");
576
577       // e.printStackTrace(); // TODO: JBPNote DEBUG
578     }
579   }
580
581
582   public void parseResult()
583   {
584     int results = 0; // number of result sets received
585     JobStateSummary finalState = new JobStateSummary();
586     try
587     {
588       for (int j = 0; j < jobs.length; j++)
589       {
590         MsaWSJob msjob = ((MsaWSJob) jobs[j]);
591         if (jobs[j].isFinished() && msjob.alignment==null)
592         {
593           try {
594             updateJobProgress(msjob);
595           } catch (Exception e)
596           {
597             Cache.log.warn("Exception when retrieving remaining Job progress data for job "+msjob.getJobId()+" on server "+WsUrl);
598             e.printStackTrace();
599           }
600           if (Cache.log.isDebugEnabled())
601           {
602             System.out.println("Job Execution file for job: "+msjob.getJobId()+" on server "+WsUrl);
603             System.out.println(msjob.getStatus());
604             System.out.println("*** End of status");
605             
606           }
607           try {
608             msjob.alignment = server.getResult(msjob.getJobId());
609           }
610           catch (Exception e)
611           {
612             Cache.log.error("Couldn't get Alignment for job.",e);
613           }
614         }
615         finalState.updateJobPanelState(wsInfo, OutputHeader, jobs[j]);
616         if (jobs[j].isSubmitted() && jobs[j].isSubjobComplete()
617                 && jobs[j].hasResults())
618         {
619           results++;
620           compbio.data.sequence.Alignment alignment = ((MsaWSJob) jobs[j]).alignment;
621           if (alignment != null)
622           {
623             wsInfo.appendProgressText(jobs[j].getJobnum(),
624                     "\nAlignment Object Method Notes\n");
625             wsInfo.appendProgressText(jobs[j].getJobnum(), "Calculated with "+alignment.getMetadata().getProgram().toString());
626             // JBPNote The returned files from a webservice could be
627             // hidden behind icons in the monitor window that,
628             // when clicked, pop up their corresponding data
629           }
630         }
631       }
632     } catch (Exception ex)
633     {
634
635       Cache.log.error("Unexpected exception when processing results for "
636               + alTitle, ex);
637       wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
638     }
639     if (results > 0)
640     {
641       wsInfo.showResultsNewFrame
642               .addActionListener(new java.awt.event.ActionListener()
643               {
644                 public void actionPerformed(java.awt.event.ActionEvent evt)
645                 {
646                   displayResults(true);
647                 }
648               });
649       wsInfo.mergeResults
650               .addActionListener(new java.awt.event.ActionListener()
651               {
652                 public void actionPerformed(java.awt.event.ActionEvent evt)
653                 {
654                   displayResults(false);
655                 }
656               });
657       wsInfo.setResultsReady();
658     }
659     else
660     {
661       wsInfo.setFinishedNoResults();
662     }
663   }
664
665   void displayResults(boolean newFrame)
666   {
667     // view input or result data for each block
668     Vector alorders = new Vector();
669     SequenceI[][] results = new SequenceI[jobs.length][];
670     AlignmentOrder[] orders = new AlignmentOrder[jobs.length];
671     String lastProgram = null;
672     MsaWSJob msjob;
673     for (int j = 0; j < jobs.length; j++)
674     {
675       if (jobs[j].hasResults())
676       {
677         msjob = (MsaWSJob)jobs[j];
678         Object[] res = msjob.getAlignment();
679         lastProgram = msjob.alignment.getMetadata().getProgram().name();
680         alorders.add(res[1]);
681         results[j] = (SequenceI[]) res[0];
682         orders[j] = (AlignmentOrder) res[1];
683
684         // SequenceI[] alignment = input.getUpdated
685       }
686       else
687       {
688         results[j] = null;
689       }
690     }
691     Object[] newview = input.getUpdatedView(results, orders, getGapChar());
692     // trash references to original result data
693     for (int j = 0; j < jobs.length; j++)
694     {
695       results[j] = null;
696       orders[j] = null;
697     }
698     SequenceI[] alignment = (SequenceI[]) newview[0];
699     ColumnSelection columnselection = (ColumnSelection) newview[1];
700     Alignment al = new Alignment(alignment);
701     // TODO: add 'provenance' property to alignment from the method notes
702     if (lastProgram!=null) {
703       al.setProperty("Alignment Program", lastProgram);
704     }
705     // accompanying each subjob
706     if (dataset != null)
707     {
708       al.setDataset(dataset);
709     }
710
711     propagateDatasetMappings(al);
712     // JBNote- TODO: warn user if a block is input rather than aligned data ?
713
714     if (newFrame)
715     {
716       AlignFrame af = new AlignFrame(al, columnselection,
717               AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
718
719       // initialise with same renderer settings as in parent alignframe.
720       af.getFeatureRenderer().transferSettings(this.featureSettings);
721       // update orders
722       if (alorders.size() > 0)
723       {
724         if (alorders.size() == 1)
725         {
726           af.addSortByOrderMenuItem(WebServiceName + " Ordering",
727                   (AlignmentOrder) alorders.get(0));
728         }
729         else
730         {
731           // construct a non-redundant ordering set
732           Vector names = new Vector();
733           for (int i = 0, l = alorders.size(); i < l; i++)
734           {
735             String orderName = new String(" Region " + i);
736             int j = i + 1;
737
738             while (j < l)
739             {
740               if (((AlignmentOrder) alorders.get(i))
741                       .equals(((AlignmentOrder) alorders.get(j))))
742               {
743                 alorders.remove(j);
744                 l--;
745                 orderName += "," + j;
746               }
747               else
748               {
749                 j++;
750               }
751             }
752
753             if (i == 0 && j == 1)
754             {
755               names.add(new String(""));
756             }
757             else
758             {
759               names.add(orderName);
760             }
761           }
762           for (int i = 0, l = alorders.size(); i < l; i++)
763           {
764             af.addSortByOrderMenuItem(WebServiceName
765                     + ((String) names.get(i)) + " Ordering",
766                     (AlignmentOrder) alorders.get(i));
767           }
768         }
769       }
770
771       Desktop.addInternalFrame(af, alTitle, AlignFrame.DEFAULT_WIDTH,
772               AlignFrame.DEFAULT_HEIGHT);
773
774     }
775     else
776     {
777       System.out.println("MERGE WITH OLD FRAME");
778       // TODO: modify alignment in original frame, replacing old for new
779       // alignment using the commands.EditCommand model to ensure the update can
780       // be undone
781     }
782   }
783
784   public boolean canMergeResults()
785   {
786     return false;
787   }
788 }