First patch 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     j.setjobStatus(server.getJobStatus(job.getJobId()));
497     StringBuffer response = j.jobProgress;
498     ChunkHolder chunk = server.pullExecStatistics(job.getJobId(), j.getLastChunk());
499     response.append(chunk.getChunk());
500     j.setLastChunk(chunk.getNextPosition());
501      
502   }
503
504   public void StartJob(AWsJob job)
505   {
506     // boiler plate template
507     if (!(job instanceof MsaWSJob))
508     {
509       throw new Error("StartJob(MsaWSJob) called on a WSJobInstance "
510               + job.getClass());
511     }
512     MsaWSJob j = (MsaWSJob) job;
513     if (j.isSubmitted())
514     {
515       if (Cache.log.isDebugEnabled())
516       {
517         Cache.log.debug("Tried to submit an already submitted job "
518                 + j.getJobId());
519       }
520       return;
521     }
522     // end boilerplate
523     
524     if (j.seqs == null || j.seqs.size()==0)
525     {
526       // special case - selection consisted entirely of empty sequences...
527       j.setjobStatus(JobStatus.FINISHED);
528       j.setStatus("Empty Alignment Job");
529     }
530     try
531     {
532       // TODO: get the parameters (if any) for this job and submit the job
533       j.setJobId(server.align(j.seqs));
534
535       if (j.getJobId()!= null)
536       {
537         j.setSubmitted(true);
538         j.setSubjobComplete(false);
539         // System.out.println(WsURL + " Job Id '" + jobId + "'");
540       }
541       else
542       {
543         throw new Exception(
544                   "Server at "
545                           + WsUrl
546                           + " returned null string for job id, it probably cannot be contacted. Try again later ?");
547         }
548     } catch (Exception e)
549     {
550       // Boilerplate code here
551       // TODO: JBPNote catch timeout or other fault types explicitly
552       // For unexpected errors
553       System.err
554               .println(WebServiceName
555                       + "Client: Failed to submit the sequences for alignment (probably a server side problem)\n"
556                       + "When contacting Server:" + WsUrl + "\n"
557                       + e.toString() + "\n");
558       j.setAllowedServerExceptions(0);
559       wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR);
560       wsInfo.setStatus(j.getJobnum(), WebserviceInfo.STATE_STOPPED_SERVERERROR);
561       wsInfo
562               .appendProgressText(
563                       j.getJobnum(),
564                       "Failed to submit sequences for alignment.\n"
565                               + "It is most likely that there is a problem with the server.\n"
566                               + "Just close the window\n");
567
568       // e.printStackTrace(); // TODO: JBPNote DEBUG
569     }
570   }
571
572
573   public void parseResult()
574   {
575     int results = 0; // number of result sets received
576     JobStateSummary finalState = new JobStateSummary();
577     try
578     {
579       for (int j = 0; j < jobs.length; j++)
580       {
581         MsaWSJob msjob = ((MsaWSJob) jobs[j]);
582         if (jobs[j].isFinished() && msjob.alignment==null)
583         {
584           try {
585             msjob.alignment = server.getResult(msjob.getJobId());
586           }
587           catch (Exception e)
588           {
589             Cache.log.error("Couldn't get Alignment for job.",e);
590           }
591         }
592         finalState.updateJobPanelState(wsInfo, OutputHeader, jobs[j]);
593         if (jobs[j].isSubmitted() && jobs[j].isSubjobComplete()
594                 && jobs[j].hasResults())
595         {
596           results++;
597           compbio.data.sequence.Alignment alignment = ((MsaWSJob) jobs[j]).alignment;
598           if (alignment != null)
599           {
600             wsInfo.appendProgressText(jobs[j].getJobnum(),
601                     "\nAlignment Object Method Notes\n");
602             wsInfo.appendProgressText(jobs[j].getJobnum(), "Calculated with "+alignment.getMetadata().getProgram().toString());
603             // JBPNote The returned files from a webservice could be
604             // hidden behind icons in the monitor window that,
605             // when clicked, pop up their corresponding data
606           }
607         }
608       }
609     } catch (Exception ex)
610     {
611
612       Cache.log.error("Unexpected exception when processing results for "
613               + alTitle, ex);
614       wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
615     }
616     if (results > 0)
617     {
618       wsInfo.showResultsNewFrame
619               .addActionListener(new java.awt.event.ActionListener()
620               {
621                 public void actionPerformed(java.awt.event.ActionEvent evt)
622                 {
623                   displayResults(true);
624                 }
625               });
626       wsInfo.mergeResults
627               .addActionListener(new java.awt.event.ActionListener()
628               {
629                 public void actionPerformed(java.awt.event.ActionEvent evt)
630                 {
631                   displayResults(false);
632                 }
633               });
634       wsInfo.setResultsReady();
635     }
636     else
637     {
638       wsInfo.setFinishedNoResults();
639     }
640   }
641
642   void displayResults(boolean newFrame)
643   {
644     // view input or result data for each block
645     Vector alorders = new Vector();
646     SequenceI[][] results = new SequenceI[jobs.length][];
647     AlignmentOrder[] orders = new AlignmentOrder[jobs.length];
648     String lastProgram = null;
649     MsaWSJob msjob;
650     for (int j = 0; j < jobs.length; j++)
651     {
652       if (jobs[j].hasResults())
653       {
654         msjob = (MsaWSJob)jobs[j];
655         Object[] res = msjob.getAlignment();
656         lastProgram = msjob.alignment.getMetadata().getProgram().name();
657         alorders.add(res[1]);
658         results[j] = (SequenceI[]) res[0];
659         orders[j] = (AlignmentOrder) res[1];
660
661         // SequenceI[] alignment = input.getUpdated
662       }
663       else
664       {
665         results[j] = null;
666       }
667     }
668     Object[] newview = input.getUpdatedView(results, orders, getGapChar());
669     // trash references to original result data
670     for (int j = 0; j < jobs.length; j++)
671     {
672       results[j] = null;
673       orders[j] = null;
674     }
675     SequenceI[] alignment = (SequenceI[]) newview[0];
676     ColumnSelection columnselection = (ColumnSelection) newview[1];
677     Alignment al = new Alignment(alignment);
678     // TODO: add 'provenance' property to alignment from the method notes
679     if (lastProgram!=null) {
680       al.setProperty("Alignment Program", lastProgram);
681     }
682     // accompanying each subjob
683     if (dataset != null)
684     {
685       al.setDataset(dataset);
686     }
687
688     propagateDatasetMappings(al);
689     // JBNote- TODO: warn user if a block is input rather than aligned data ?
690
691     if (newFrame)
692     {
693       AlignFrame af = new AlignFrame(al, columnselection,
694               AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
695
696       // initialise with same renderer settings as in parent alignframe.
697       af.getFeatureRenderer().transferSettings(this.featureSettings);
698       // update orders
699       if (alorders.size() > 0)
700       {
701         if (alorders.size() == 1)
702         {
703           af.addSortByOrderMenuItem(WebServiceName + " Ordering",
704                   (AlignmentOrder) alorders.get(0));
705         }
706         else
707         {
708           // construct a non-redundant ordering set
709           Vector names = new Vector();
710           for (int i = 0, l = alorders.size(); i < l; i++)
711           {
712             String orderName = new String(" Region " + i);
713             int j = i + 1;
714
715             while (j < l)
716             {
717               if (((AlignmentOrder) alorders.get(i))
718                       .equals(((AlignmentOrder) alorders.get(j))))
719               {
720                 alorders.remove(j);
721                 l--;
722                 orderName += "," + j;
723               }
724               else
725               {
726                 j++;
727               }
728             }
729
730             if (i == 0 && j == 1)
731             {
732               names.add(new String(""));
733             }
734             else
735             {
736               names.add(orderName);
737             }
738           }
739           for (int i = 0, l = alorders.size(); i < l; i++)
740           {
741             af.addSortByOrderMenuItem(WebServiceName
742                     + ((String) names.get(i)) + " Ordering",
743                     (AlignmentOrder) alorders.get(i));
744           }
745         }
746       }
747
748       Desktop.addInternalFrame(af, alTitle, AlignFrame.DEFAULT_WIDTH,
749               AlignFrame.DEFAULT_HEIGHT);
750
751     }
752     else
753     {
754       System.out.println("MERGE WITH OLD FRAME");
755       // TODO: modify alignment in original frame, replacing old for new
756       // alignment using the commands.EditCommand model to ensure the update can
757       // be undone
758     }
759   }
760
761   public boolean canMergeResults()
762   {
763     return false;
764   }
765 }