JAL-1479 minor refactor and housekeeping
[jalview.git] / src / jalview / ws / jws1 / MsaWSThread.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.jws1;
22
23 import jalview.analysis.AlignSeq;
24 import jalview.bin.Cache;
25 import jalview.datamodel.Alignment;
26 import jalview.datamodel.AlignmentOrder;
27 import jalview.datamodel.AlignmentView;
28 import jalview.datamodel.ColumnSelection;
29 import jalview.datamodel.SequenceI;
30 import jalview.gui.AlignFrame;
31 import jalview.gui.Desktop;
32 import jalview.gui.WebserviceInfo;
33 import jalview.util.MessageManager;
34 import jalview.ws.AWsJob;
35 import jalview.ws.JobStateSummary;
36 import jalview.ws.WSClientI;
37
38 import java.util.Hashtable;
39 import java.util.Vector;
40
41 import vamsas.objects.simple.MsaResult;
42
43 class MsaWSThread extends JWS1Thread implements WSClientI
44 {
45   boolean submitGaps = false; // pass sequences including gaps to alignment
46
47   // service
48
49   boolean preserveOrder = true; // and always store and recover sequence
50
51   // order
52
53   class MsaWSJob extends WSJob
54   {
55     // hold special input for this
56     vamsas.objects.simple.SequenceSet seqs = new vamsas.objects.simple.SequenceSet();
57
58     /**
59      * MsaWSJob
60      * 
61      * @param jobNum
62      *          int
63      * @param jobId
64      *          String
65      */
66     public MsaWSJob(int jobNum, SequenceI[] inSeqs)
67     {
68       this.jobnum = jobNum;
69       if (!prepareInput(inSeqs, 2))
70       {
71         submitted = true;
72         subjobComplete = true;
73         result = new MsaResult();
74         result.setFinished(true);
75         result.setStatus(MessageManager.getString("label.job_never_ran"));
76       }
77
78     }
79
80     Hashtable SeqNames = new Hashtable();
81
82     Vector emptySeqs = new Vector();
83
84     /**
85      * prepare input sequences for MsaWS service
86      * 
87      * @param seqs
88      *          jalview sequences to be prepared
89      * @param minlen
90      *          minimum number of residues required for this MsaWS service
91      * @return true if seqs contains sequences to be submitted to service.
92      */
93     private boolean prepareInput(SequenceI[] seqs, int minlen)
94     {
95       int nseqs = 0;
96       if (minlen < 0)
97       {
98         throw new Error(
99                 MessageManager
100                         .getString("error.implementation_error_minlen_must_be_greater_zero"));
101       }
102       for (int i = 0; i < seqs.length; i++)
103       {
104         if (seqs[i].getEnd() - seqs[i].getStart() > minlen - 1)
105         {
106           nseqs++;
107         }
108       }
109       boolean valid = nseqs > 1; // need at least two seqs
110       vamsas.objects.simple.Sequence[] seqarray = (valid) ? new vamsas.objects.simple.Sequence[nseqs]
111               : null;
112       for (int i = 0, n = 0; i < seqs.length; i++)
113       {
114
115         String newname = jalview.analysis.SeqsetUtils.unique_name(i); // same
116         // for
117         // any
118         // subjob
119         SeqNames.put(newname,
120                 jalview.analysis.SeqsetUtils.SeqCharacterHash(seqs[i]));
121         if (valid && seqs[i].getEnd() - seqs[i].getStart() > minlen - 1)
122         {
123           seqarray[n] = new vamsas.objects.simple.Sequence();
124           seqarray[n].setId(newname);
125           seqarray[n++].setSeq((submitGaps) ? seqs[i].getSequenceAsString()
126                   : AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
127                           seqs[i].getSequenceAsString()));
128         }
129         else
130         {
131           String empty = null;
132           if (seqs[i].getEnd() >= seqs[i].getStart())
133           {
134             empty = (submitGaps) ? seqs[i].getSequenceAsString() : AlignSeq
135                     .extractGaps(jalview.util.Comparison.GapChars,
136                             seqs[i].getSequenceAsString());
137           }
138           emptySeqs.add(new String[] { newname, empty });
139         }
140       }
141       this.seqs = new vamsas.objects.simple.SequenceSet();
142       this.seqs.setSeqs(seqarray);
143       return valid;
144     }
145
146     /**
147      * 
148      * @return true if getAlignment will return a valid alignment result.
149      */
150     public boolean hasResults()
151     {
152       if (subjobComplete && result != null && result.isFinished()
153               && ((MsaResult) result).getMsa() != null
154               && ((MsaResult) result).getMsa().getSeqs() != null)
155       {
156         return true;
157       }
158       return false;
159     }
160
161     public Object[] getAlignment()
162     {
163
164       if (result != null && result.isFinished())
165       {
166         SequenceI[] alseqs = null;
167         char alseq_gapchar = '-';
168         int alseq_l = 0;
169         if (((MsaResult) result).getMsa() != null)
170         {
171           alseqs = getVamsasAlignment(((MsaResult) result).getMsa());
172           alseq_gapchar = ((MsaResult) result).getMsa().getGapchar()
173                   .charAt(0);
174           alseq_l = alseqs.length;
175         }
176         if (emptySeqs.size() > 0)
177         {
178           SequenceI[] t_alseqs = new SequenceI[alseq_l + emptySeqs.size()];
179           // get width
180           int i, w = 0;
181           if (alseq_l > 0)
182           {
183             for (i = 0, w = alseqs[0].getLength(); i < alseq_l; i++)
184             {
185               if (w < alseqs[i].getLength())
186               {
187                 w = alseqs[i].getLength();
188               }
189               t_alseqs[i] = alseqs[i];
190               alseqs[i] = null;
191             }
192           }
193           // check that aligned width is at least as wide as emptySeqs width.
194           int ow = w, nw = w;
195           for (i = 0, w = emptySeqs.size(); i < w; i++)
196           {
197             String[] es = (String[]) emptySeqs.get(i);
198             if (es != null && es[1] != null)
199             {
200               int sw = es[1].length();
201               if (nw < sw)
202               {
203                 nw = sw;
204               }
205             }
206           }
207           // make a gapped string.
208           StringBuffer insbuff = new StringBuffer(w);
209           for (i = 0; i < nw; i++)
210           {
211             insbuff.append(alseq_gapchar);
212           }
213           if (ow < nw)
214           {
215             for (i = 0; i < alseq_l; i++)
216             {
217               int sw = t_alseqs[i].getLength();
218               if (nw > sw)
219               {
220                 // pad at end
221                 alseqs[i].setSequence(t_alseqs[i].getSequenceAsString()
222                         + insbuff.substring(0, sw - nw));
223               }
224             }
225           }
226           for (i = 0, w = emptySeqs.size(); i < w; i++)
227           {
228             String[] es = (String[]) emptySeqs.get(i);
229             if (es[1] == null)
230             {
231               t_alseqs[i + alseq_l] = new jalview.datamodel.Sequence(es[0],
232                       insbuff.toString(), 1, 0);
233             }
234             else
235             {
236               if (es[1].length() < nw)
237               {
238                 t_alseqs[i + alseq_l] = new jalview.datamodel.Sequence(
239                         es[0],
240                         es[1] + insbuff.substring(0, nw - es[1].length()),
241                         1, 1 + es[1].length());
242               }
243               else
244               {
245                 t_alseqs[i + alseq_l] = new jalview.datamodel.Sequence(
246                         es[0], es[1]);
247               }
248             }
249           }
250           alseqs = t_alseqs;
251         }
252         AlignmentOrder msaorder = new AlignmentOrder(alseqs);
253         // always recover the order - makes parseResult()'s life easier.
254         jalview.analysis.AlignmentSorter.recoverOrder(alseqs);
255         // account for any missing sequences
256         jalview.analysis.SeqsetUtils.deuniquify(SeqNames, alseqs);
257         return new Object[] { alseqs, msaorder };
258       }
259       return null;
260     }
261
262     /**
263      * mark subjob as cancelled and set result object appropriatly
264      */
265     void cancel()
266     {
267       cancelled = true;
268       subjobComplete = true;
269       result = null;
270     }
271
272     /**
273      * 
274      * @return boolean true if job can be submitted.
275      */
276     public boolean hasValidInput()
277     {
278       if (seqs.getSeqs() != null)
279       {
280         return true;
281       }
282       return false;
283     }
284   }
285
286   String alTitle; // name which will be used to form new alignment window.
287
288   Alignment dataset; // dataset to which the new alignment will be
289
290   // associated.
291
292   ext.vamsas.MuscleWS server = null;
293
294   /**
295    * set basic options for this (group) of Msa jobs
296    * 
297    * @param subgaps
298    *          boolean
299    * @param presorder
300    *          boolean
301    */
302   MsaWSThread(ext.vamsas.MuscleWS server, String wsUrl,
303           WebserviceInfo wsinfo, jalview.gui.AlignFrame alFrame,
304           AlignmentView alview, String wsname, boolean subgaps,
305           boolean presorder)
306   {
307     super(alFrame, wsinfo, alview, wsname, wsUrl);
308     this.server = server;
309     this.submitGaps = subgaps;
310     this.preserveOrder = presorder;
311   }
312
313   /**
314    * create one or more Msa jobs to align visible seuqences in _msa
315    * 
316    * @param title
317    *          String
318    * @param _msa
319    *          AlignmentView
320    * @param subgaps
321    *          boolean
322    * @param presorder
323    *          boolean
324    * @param seqset
325    *          Alignment
326    */
327   MsaWSThread(ext.vamsas.MuscleWS server, String wsUrl,
328           WebserviceInfo wsinfo, jalview.gui.AlignFrame alFrame,
329           String wsname, String title, AlignmentView _msa, boolean subgaps,
330           boolean presorder, Alignment seqset)
331   {
332     this(server, wsUrl, wsinfo, alFrame, _msa, wsname, subgaps, presorder);
333     OutputHeader = wsInfo.getProgressText();
334     alTitle = title;
335     dataset = seqset;
336
337     SequenceI[][] conmsa = _msa.getVisibleContigs('-');
338     if (conmsa != null)
339     {
340       int njobs = conmsa.length;
341       jobs = new MsaWSJob[njobs];
342       for (int j = 0; j < njobs; j++)
343       {
344         if (j != 0)
345         {
346           jobs[j] = new MsaWSJob(wsinfo.addJobPane(), conmsa[j]);
347         }
348         else
349         {
350           jobs[j] = new MsaWSJob(0, conmsa[j]);
351         }
352         if (njobs > 0)
353         {
354           wsinfo.setProgressName("region " + jobs[j].getJobnum(),
355                   jobs[j].getJobnum());
356         }
357         wsinfo.setProgressText(jobs[j].getJobnum(), OutputHeader);
358       }
359     }
360   }
361
362   public boolean isCancellable()
363   {
364     return true;
365   }
366
367   public void cancelJob()
368   {
369     if (!jobComplete && jobs != null)
370     {
371       boolean cancelled = true;
372       for (int job = 0; job < jobs.length; job++)
373       {
374         if (jobs[job].isSubmitted() && !jobs[job].isSubjobComplete())
375         {
376           String cancelledMessage = "";
377           try
378           {
379             vamsas.objects.simple.WsJobId cancelledJob = server
380                     .cancel(jobs[job].getJobId());
381             if (cancelledJob.getStatus() == 2)
382             {
383               // CANCELLED_JOB
384               cancelledMessage = "Job cancelled.";
385               ((MsaWSJob) jobs[job]).cancel();
386               wsInfo.setStatus(jobs[job].getJobnum(),
387                       WebserviceInfo.STATE_CANCELLED_OK);
388             }
389             else if (cancelledJob.getStatus() == 3)
390             {
391               // VALID UNSTOPPABLE JOB
392               cancelledMessage += "Server cannot cancel this job. just close the window.\n";
393               cancelled = false;
394               // wsInfo.setStatus(jobs[job].jobnum,
395               // WebserviceInfo.STATE_RUNNING);
396             }
397
398             if (cancelledJob.getJobId() != null)
399             {
400               cancelledMessage += ("[" + cancelledJob.getJobId() + "]");
401             }
402
403             cancelledMessage += "\n";
404           } catch (Exception exc)
405           {
406             cancelledMessage += ("\nProblems cancelling the job : Exception received...\n"
407                     + exc + "\n");
408             Cache.log.warn(
409                     "Exception whilst cancelling " + jobs[job].getJobId(),
410                     exc);
411           }
412           wsInfo.setProgressText(jobs[job].getJobnum(), OutputHeader
413                   + cancelledMessage + "\n");
414         }
415       }
416       if (cancelled)
417       {
418         wsInfo.setStatus(WebserviceInfo.STATE_CANCELLED_OK);
419         jobComplete = true;
420       }
421       this.interrupt(); // kick thread to update job states.
422     }
423     else
424     {
425       if (!jobComplete)
426       {
427         wsInfo.setProgressText(OutputHeader
428                 + "Server cannot cancel this job because it has not been submitted properly. just close the window.\n");
429       }
430     }
431   }
432
433   public void pollJob(AWsJob job) throws Exception
434   {
435     ((MsaWSJob) job).result = server.getResult(((MsaWSJob) job).getJobId());
436   }
437
438   public void StartJob(AWsJob job)
439   {
440     if (!(job instanceof MsaWSJob))
441     {
442       throw new Error(MessageManager.formatMessage(
443               "error.implementation_error_msawbjob_called",
444               new String[] { job.getClass().toString() }));
445     }
446     MsaWSJob j = (MsaWSJob) job;
447     if (j.isSubmitted())
448     {
449       if (Cache.log.isDebugEnabled())
450       {
451         Cache.log.debug("Tried to submit an already submitted job "
452                 + j.getJobId());
453       }
454       return;
455     }
456     if (j.seqs.getSeqs() == null)
457     {
458       // special case - selection consisted entirely of empty sequences...
459       j.setSubmitted(true);
460       j.result = new MsaResult();
461       j.result.setFinished(true);
462       j.result.setStatus(MessageManager
463               .getString("label.empty_alignment_job"));
464       ((MsaResult) j.result).setMsa(null);
465     }
466     try
467     {
468       vamsas.objects.simple.WsJobId jobsubmit = server.align(j.seqs);
469
470       if ((jobsubmit != null) && (jobsubmit.getStatus() == 1))
471       {
472         j.setJobId(jobsubmit.getJobId());
473         j.setSubmitted(true);
474         j.setSubjobComplete(false);
475         // System.out.println(WsURL + " Job Id '" + jobId + "'");
476       }
477       else
478       {
479         if (jobsubmit == null)
480         {
481           throw new Exception(MessageManager.formatMessage(
482                   "exception.web_service_returned_null_try_later",
483                   new String[] { WsUrl }));
484         }
485
486         throw new Exception(jobsubmit.getJobId());
487       }
488     } catch (Exception e)
489     {
490       // TODO: JBPNote catch timeout or other fault types explicitly
491       // For unexpected errors
492       System.err
493               .println(WebServiceName
494                       + "Client: Failed to submit the sequences for alignment (probably a server side problem)\n"
495                       + "When contacting Server:" + WsUrl + "\n"
496                       + e.toString() + "\n");
497       j.setAllowedServerExceptions(0);
498       wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR);
499       wsInfo.setStatus(j.getJobnum(),
500               WebserviceInfo.STATE_STOPPED_SERVERERROR);
501       wsInfo.appendProgressText(j.getJobnum(), MessageManager
502               .getString("info.failed_to_submit_sequences_for_alignment"));
503
504       // e.printStackTrace(); // TODO: JBPNote DEBUG
505     }
506   }
507
508   private jalview.datamodel.Sequence[] getVamsasAlignment(
509           vamsas.objects.simple.Alignment valign)
510   {
511     // TODO: refactor to helper class for vamsas.objects.simple objects
512     vamsas.objects.simple.Sequence[] seqs = valign.getSeqs().getSeqs();
513     jalview.datamodel.Sequence[] msa = new jalview.datamodel.Sequence[seqs.length];
514
515     for (int i = 0, j = seqs.length; i < j; i++)
516     {
517       msa[i] = new jalview.datamodel.Sequence(seqs[i].getId(),
518               seqs[i].getSeq());
519     }
520
521     return msa;
522   }
523
524   public void parseResult()
525   {
526     int results = 0; // number of result sets received
527     JobStateSummary finalState = new JobStateSummary();
528     try
529     {
530       for (int j = 0; j < jobs.length; j++)
531       {
532         finalState.updateJobPanelState(wsInfo, OutputHeader, jobs[j]);
533         if (jobs[j].isSubmitted() && jobs[j].isSubjobComplete()
534                 && jobs[j].hasResults())
535         {
536           results++;
537           // if (Cache.log.isDebugEnabled())
538           // {
539           // System.out.println("Job lob for job "+jobs[j].getJobId()+":"+jobs[j].getJobnum());
540           // System.out.println(jobs[j].getStatus());
541           // }
542
543           vamsas.objects.simple.Alignment valign = ((MsaResult) ((MsaWSJob) jobs[j]).result)
544                   .getMsa();
545           if (valign != null)
546           {
547             wsInfo.appendProgressText(jobs[j].getJobnum(), MessageManager
548                     .getString("info.alignment_object_method_notes"));
549             String[] lines = valign.getMethod();
550             for (int line = 0; line < lines.length; line++)
551             {
552               wsInfo.appendProgressText(jobs[j].getJobnum(), lines[line]
553                       + "\n");
554             }
555             // JBPNote The returned files from a webservice could be
556             // hidden behind icons in the monitor window that,
557             // when clicked, pop up their corresponding data
558
559           }
560         }
561       }
562     } catch (Exception ex)
563     {
564
565       Cache.log.error("Unexpected exception when processing results for "
566               + alTitle, ex);
567       wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
568     }
569     if (results > 0)
570     {
571       wsInfo.showResultsNewFrame
572               .addActionListener(new java.awt.event.ActionListener()
573               {
574                 public void actionPerformed(java.awt.event.ActionEvent evt)
575                 {
576                   displayResults(true);
577                 }
578               });
579       wsInfo.mergeResults
580               .addActionListener(new java.awt.event.ActionListener()
581               {
582                 public void actionPerformed(java.awt.event.ActionEvent evt)
583                 {
584                   displayResults(false);
585                 }
586               });
587       wsInfo.setResultsReady();
588     }
589     else
590     {
591       wsInfo.setFinishedNoResults();
592     }
593   }
594
595   void displayResults(boolean newFrame)
596   {
597     // view input or result data for each block
598     Vector alorders = new Vector();
599     SequenceI[][] results = new SequenceI[jobs.length][];
600     AlignmentOrder[] orders = new AlignmentOrder[jobs.length];
601     for (int j = 0; j < jobs.length; j++)
602     {
603       if (jobs[j].hasResults())
604       {
605         Object[] res = ((MsaWSJob) jobs[j]).getAlignment();
606         alorders.add(res[1]);
607         results[j] = (SequenceI[]) res[0];
608         orders[j] = (AlignmentOrder) res[1];
609
610         // SequenceI[] alignment = input.getUpdated
611       }
612       else
613       {
614         results[j] = null;
615       }
616     }
617     Object[] newview = input.getUpdatedView(results, orders, getGapChar());
618     // trash references to original result data
619     for (int j = 0; j < jobs.length; j++)
620     {
621       results[j] = null;
622       orders[j] = null;
623     }
624     SequenceI[] alignment = (SequenceI[]) newview[0];
625     ColumnSelection columnselection = (ColumnSelection) newview[1];
626     Alignment al = new Alignment(alignment);
627     // TODO: add 'provenance' property to alignment from the method notes
628     // accompanying each subjob
629     if (dataset != null)
630     {
631       al.setDataset(dataset);
632     }
633
634     propagateDatasetMappings(al);
635     // JBNote- TODO: warn user if a block is input rather than aligned data ?
636
637     if (newFrame)
638     {
639       AlignFrame af = new AlignFrame(al, columnselection,
640               AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
641
642       // initialise with same renderer settings as in parent alignframe.
643       af.getFeatureRenderer().transferSettings(this.featureSettings);
644       // update orders
645       if (alorders.size() > 0)
646       {
647         if (alorders.size() == 1)
648         {
649           af.addSortByOrderMenuItem(WebServiceName + " Ordering",
650                   (AlignmentOrder) alorders.get(0));
651         }
652         else
653         {
654           // construct a non-redundant ordering set
655           Vector names = new Vector();
656           for (int i = 0, l = alorders.size(); i < l; i++)
657           {
658             String orderName = new String(" Region " + i);
659             int j = i + 1;
660
661             while (j < l)
662             {
663               if (((AlignmentOrder) alorders.get(i))
664                       .equals(((AlignmentOrder) alorders.get(j))))
665               {
666                 alorders.remove(j);
667                 l--;
668                 orderName += "," + j;
669               }
670               else
671               {
672                 j++;
673               }
674             }
675
676             if (i == 0 && j == 1)
677             {
678               names.add(new String(""));
679             }
680             else
681             {
682               names.add(orderName);
683             }
684           }
685           for (int i = 0, l = alorders.size(); i < l; i++)
686           {
687             af.addSortByOrderMenuItem(
688                     WebServiceName + ((String) names.get(i)) + " Ordering",
689                     (AlignmentOrder) alorders.get(i));
690           }
691         }
692       }
693
694       Desktop.addInternalFrame(af, alTitle, AlignFrame.DEFAULT_WIDTH,
695               AlignFrame.DEFAULT_HEIGHT);
696
697     }
698     else
699     {
700       System.out.println("MERGE WITH OLD FRAME");
701       // TODO: modify alignment in original frame, replacing old for new
702       // alignment using the commands.EditCommand model to ensure the update can
703       // be undone
704     }
705   }
706
707   public boolean canMergeResults()
708   {
709     return false;
710   }
711 }