JAL-3848 print exception for failed align jobs
[jalview.git] / src / jalview / ws / gui / 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.gui;
22
23 import jalview.bin.Cache;
24 import jalview.datamodel.Alignment;
25 import jalview.datamodel.AlignmentI;
26 import jalview.datamodel.AlignmentOrder;
27 import jalview.datamodel.AlignmentView;
28 import jalview.datamodel.HiddenColumns;
29 import jalview.datamodel.SequenceI;
30 import jalview.gui.AlignFrame;
31 import jalview.gui.Desktop;
32 import jalview.gui.SplitFrame;
33 import jalview.gui.WebserviceInfo;
34 import jalview.util.MessageManager;
35 import jalview.ws.AWSThread;
36 import jalview.ws.AWsJob;
37 import jalview.ws.JobStateSummary;
38 import jalview.ws.WSClientI;
39 import jalview.ws.api.CancellableI;
40 import jalview.ws.api.JobId;
41 import jalview.ws.api.MultipleSequenceAlignmentI;
42 import jalview.ws.gui.WsJob.JobState;
43 import jalview.ws.params.ArgumentI;
44 import jalview.ws.params.WsParamSetI;
45
46 import java.util.ArrayList;
47 import java.util.List;
48
49 import javax.swing.JInternalFrame;
50
51 public class MsaWSThread extends AWSThread implements WSClientI
52 {
53   boolean submitGaps = false; // pass sequences including gaps to alignment
54
55   // service
56
57   boolean preserveOrder = true; // and always store and recover sequence
58
59   // order
60
61   String alTitle; // name which will be used to form new alignment window.
62
63   AlignmentI dataset; // dataset to which the new alignment will be
64
65   // associated.
66
67   MultipleSequenceAlignmentI server = null;
68
69   /**
70    * set basic options for this (group) of Msa jobs
71    * 
72    * @param subgaps
73    *          boolean
74    * @param presorder
75    *          boolean
76    */
77   private MsaWSThread(MultipleSequenceAlignmentI server, String wsUrl,
78           WebserviceInfo wsinfo,
79           jalview.gui.AlignFrame alFrame, AlignmentView alview,
80           String wsname, boolean subgaps, boolean presorder)
81   {
82     super(alFrame, wsinfo, alview, wsname, wsUrl);
83     this.server = server;
84     this.submitGaps = subgaps;
85     this.preserveOrder = presorder;
86   }
87
88   /**
89    * create one or more Msa jobs to align visible sequences in _msa
90    * 
91    * @param title
92    *          String
93    * @param _msa
94    *          AlignmentView
95    * @param subgaps
96    *          boolean
97    * @param presorder
98    *          boolean
99    * @param seqset
100    *          Alignment
101    */
102   public MsaWSThread(MultipleSequenceAlignmentI server2, WsParamSetI preset,
103           List<ArgumentI> paramset,
104           String wsUrl, WebserviceInfo wsinfo,
105           jalview.gui.AlignFrame alFrame, String wsname, String title,
106           AlignmentView _msa, boolean subgaps, boolean presorder,
107           AlignmentI seqset)
108   {
109     this(server2, wsUrl, wsinfo, alFrame, _msa, wsname, subgaps, presorder);
110     OutputHeader = wsInfo.getProgressText();
111     alTitle = title;
112     dataset = seqset;
113
114     SequenceI[][] conmsa = _msa.getVisibleContigs('-');
115     if (conmsa != null)
116     {
117       int nvalid = 0, njobs = conmsa.length;
118       jobs = new AWsJob[njobs];
119       for (int j = 0; j < njobs; j++)
120       {
121         if (j != 0)
122         {
123           jobs[j] = new MsaWSJob(this, wsinfo.addJobPane(), conmsa[j]);
124         }
125         else
126         {
127           jobs[j] = new MsaWSJob(this, 0, conmsa[j]);
128         }
129         if (jobs[j].hasValidInput())
130         {
131           nvalid++;
132         }
133         jobs[j].setPreset(preset);
134         jobs[j].setArguments(paramset);
135         ((MsaWSJob) jobs[j]).alignmentProgram = wsname;
136         if (njobs > 0)
137         {
138           wsinfo.setProgressName("region " + jobs[j].getJobnum(),
139                   jobs[j].getJobnum());
140         }
141         wsinfo.setProgressText(jobs[j].getJobnum(), OutputHeader);
142       }
143       validInput = nvalid > 0;
144     }
145   }
146
147   boolean validInput = false;
148
149   /**
150    * 
151    * @return true if the thread will perform a calculation
152    */
153   public boolean hasValidInput()
154   {
155     return validInput;
156   }
157
158   @Override
159   public boolean isCancellable()
160   {
161     return server instanceof CancellableI;
162   }
163
164   @Override
165   public void cancelJob()
166   {
167     // TODO decide if when some jobs are not cancellable to shut down the thread
168     // anyhow ?
169     if (!jobComplete && jobs != null)
170     {
171       boolean cancelled = true;
172       for (int job = 0; job < jobs.length; job++)
173       {
174         if (jobs[job].isSubmitted() && !jobs[job].isSubjobComplete())
175         {
176           String cancelledMessage = "";
177           try
178           {
179             CancellableI service = (CancellableI) server;
180             boolean cancelledJob = service.cancel((WsJob) jobs[job]);
181             if (cancelledJob)
182             {
183               // CANCELLED_JOB
184               cancelledMessage = "Job cancelled.";
185               ((MsaWSJob) jobs[job]).cancel(); // TODO: refactor to avoid this
186                                                // ugliness -
187               wsInfo.setStatus(jobs[job].getJobnum(),
188                       WebserviceInfo.STATE_CANCELLED_OK);
189             }
190             else
191             {
192               // VALID UNSTOPPABLE JOB
193               cancelledMessage += "Server cannot cancel this job. just close the window.\n";
194               cancelled = false;
195               // wsInfo.setStatus(jobs[job].jobnum,
196               // WebserviceInfo.STATE_RUNNING);
197             }
198           } catch (Exception exc)
199           {
200             cancelledMessage += ("\nProblems cancelling the job : Exception received...\n"
201                     + exc + "\n");
202             Cache.log.warn(
203                     "Exception whilst cancelling " + jobs[job].getJobId(),
204                     exc);
205           }
206           wsInfo.setProgressText(jobs[job].getJobnum(),
207                   OutputHeader + cancelledMessage + "\n");
208         }
209         else
210         {
211           // if we hadn't submitted then just mark the job as cancelled.
212           jobs[job].setSubjobComplete(true);
213           wsInfo.setStatus(jobs[job].getJobnum(),
214                   WebserviceInfo.STATE_CANCELLED_OK);
215
216         }
217       }
218       if (cancelled)
219       {
220         wsInfo.setStatus(WebserviceInfo.STATE_CANCELLED_OK);
221         jobComplete = true;
222       }
223       this.interrupt(); // kick thread to update job states.
224     }
225     else
226     {
227       if (!jobComplete)
228       {
229         wsInfo.setProgressText(OutputHeader
230                 + "Server cannot cancel this job because it has not been submitted properly. just close the window.\n");
231       }
232     }
233   }
234
235   @Override
236   public void pollJob(AWsJob job) throws Exception
237   {
238     // TODO: investigate if we still need to cast here in J1.6
239     MsaWSJob j = ((MsaWSJob) job);
240     // this is standard code, but since the interface doesn't comprise of a
241     // basic one that implements (getJobStatus, pullExecStatistics) we have to
242     // repeat the code for all jw2s services.
243     server.updateStatus(j);
244     server.updateJobProgress(j);
245   }
246
247   @Override
248   public void StartJob(AWsJob job)
249   {
250     Exception lex = null;
251     // boiler plate template
252     if (!(job instanceof MsaWSJob))
253     {
254       throw new Error(MessageManager.formatMessage(
255               "error.implementation_error_msawbjob_called", new String[]
256               { job.getClass().toString() }));
257     }
258     MsaWSJob j = (MsaWSJob) job;
259     if (j.isSubmitted())
260     {
261       if (Cache.log.isDebugEnabled())
262       {
263         Cache.log.debug(
264                 "Tried to submit an already submitted job " + j.getJobId());
265       }
266       return;
267     }
268     // end boilerplate
269
270     if (j.seqs == null || j.seqs.size() == 0)
271     {
272       // special case - selection consisted entirely of empty sequences...
273       j.setState(JobState.FINISHED);
274       j.setStatus(MessageManager.getString("label.empty_alignment_job"));
275     }
276     try
277     {
278       j.addInitialStatus(); // list the presets/parameters used for the job in
279                             // status
280       try
281       {
282         JobId jobHandle = server.align(j.seqs, j.getPreset(),
283                 j.getArguments());
284         if (jobHandle != null)
285         {
286           j.setJobHandle(jobHandle);
287         }
288
289       } catch (Throwable throwable)
290       {
291         Cache.log.error("failed to send the job to the alignment server", throwable);
292         if (!server.handleSubmitError(throwable, j, wsInfo))
293         {
294           if (throwable instanceof Exception)
295           {
296             throw ((Exception) throwable);
297           }
298           if (throwable instanceof Error)
299           {
300             throw ((Error) throwable);
301           }
302         }
303       }
304       ///// generic
305       if (j.getJobId() != null)
306       {
307         j.setSubmitted(true);
308         j.setSubjobComplete(false);
309         // System.out.println(WsURL + " Job Id '" + jobId + "'");
310         return;
311       }
312       else
313       {
314         throw new Exception(MessageManager.formatMessage(
315                 "exception.web_service_returned_null_try_later",
316                 new String[]
317                 { WsUrl }));
318       }
319     }
320     //// jabaws specific
321
322     //// generic
323     catch (Error e)
324     {
325       // For unexpected errors
326       System.err.println(WebServiceName
327               + "Client: Failed to submit the sequences for alignment (probably a server side problem)\n"
328               + "When contacting Server:" + WsUrl + "\n");
329       e.printStackTrace(System.err);
330       wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR);
331       wsInfo.setStatus(j.getJobnum(),
332               WebserviceInfo.STATE_STOPPED_SERVERERROR);
333     } catch (Exception e)
334     {
335       // For unexpected errors
336       System.err.println(WebServiceName
337               + "Client: Failed to submit the sequences for alignment (probably a server side problem)\n"
338               + "When contacting Server:" + WsUrl + "\n");
339       e.printStackTrace(System.err);
340       wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR);
341       wsInfo.setStatus(j.getJobnum(),
342               WebserviceInfo.STATE_STOPPED_SERVERERROR);
343     } finally
344     {
345       if (!j.isSubmitted())
346       {
347         // Boilerplate code here
348         // TODO: JBPNote catch timeout or other fault types explicitly
349
350         j.setAllowedServerExceptions(0);
351         wsInfo.appendProgressText(j.getJobnum(), MessageManager.getString(
352                 "info.failed_to_submit_sequences_for_alignment"));
353       }
354     }
355   }
356
357   @Override
358   public void parseResult()
359   {
360     long progbar = System.currentTimeMillis();
361     wsInfo.setProgressBar(
362             MessageManager.getString("status.collecting_job_results"),
363             progbar);
364     int results = 0; // number of result sets received
365     JobStateSummary finalState = new JobStateSummary();
366     try
367     {
368       for (int j = 0; j < jobs.length; j++)
369       {
370         MsaWSJob msjob = ((MsaWSJob) jobs[j]);
371         if (jobs[j].isFinished() && msjob.alignment == null)
372         {
373           int nunchanged = 3, nexcept = 3;
374           boolean jpchanged = false, jpex = false;
375           do
376           {
377             try
378             {
379               jpchanged = server.updateJobProgress(msjob);
380               jpex = false;
381               if (jpchanged)
382               {
383                 nexcept = 3;
384               }
385             } catch (Exception e)
386             {
387
388               Cache.log.warn(
389                       "Exception when retrieving remaining Job progress data for job "
390                               + msjob.getJobId() + " on server " + WsUrl);
391               e.printStackTrace();
392               nexcept--;
393               nunchanged = 3;
394               // set flag remember that we've had an exception.
395               jpex = true;
396               jpchanged = false;
397             }
398             if (!jpchanged)
399             {
400               try
401               {
402                 Thread.sleep(jpex ? 2400 : 1200); // wait a bit longer if we
403                                                   // experienced an exception.
404               } catch (Exception ex)
405               {
406               }
407               ;
408               nunchanged--;
409             }
410           } while (nunchanged > 0 && nexcept > 0);
411
412           if (Cache.log.isDebugEnabled())
413           {
414             System.out.println("Job Execution file for job: "
415                     + msjob.getJobId() + " on server " + WsUrl);
416             System.out.println(msjob.getStatus());
417             System.out.println("*** End of status");
418
419           }
420           ///// jabaws specific(ish) Get Result from Server when available
421           try
422           {
423             msjob.alignment = server.getAlignmentFor(msjob.getJobHandle());
424           } catch (Exception e)
425           {
426             if (!server.handleCollectionException(e, msjob, wsInfo))
427             {
428               Cache.log.error("Couldn't get Alignment for job.", e);
429               // TODO: Increment count and retry ?
430               msjob.setState(JobState.SERVERERROR);
431             }
432           }
433         }
434         finalState.updateJobPanelState(wsInfo, OutputHeader, jobs[j]);
435         if (jobs[j].isSubmitted() && jobs[j].isSubjobComplete()
436                 && jobs[j].hasResults())
437         {
438           results++;
439         }
440       }
441     } catch (Exception ex)
442     {
443
444       Cache.log.error(
445               "Unexpected exception when processing results for " + alTitle,
446               ex);
447       wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
448     }
449     if (results > 0)
450     {
451       wsInfo.showResultsNewFrame
452               .addActionListener(new java.awt.event.ActionListener()
453               {
454                 @Override
455                 public void actionPerformed(java.awt.event.ActionEvent evt)
456                 {
457                   displayResults(true);
458                 }
459               });
460       wsInfo.mergeResults
461               .addActionListener(new java.awt.event.ActionListener()
462               {
463                 @Override
464                 public void actionPerformed(java.awt.event.ActionEvent evt)
465                 {
466                   displayResults(false);
467                 }
468               });
469       wsInfo.setResultsReady();
470     }
471     else
472     {
473       wsInfo.setFinishedNoResults();
474     }
475     updateGlobalStatus(finalState);
476     wsInfo.setProgressBar(null, progbar);
477   }
478
479   /**
480    * Display alignment results in a new frame (or - not currently supported -
481    * added to an existing alignment).
482    * 
483    * @param newFrame
484    */
485   void displayResults(boolean newFrame)
486   {
487     // view input or result data for each block
488     List<AlignmentOrder> alorders = new ArrayList<>();
489     SequenceI[][] results = new SequenceI[jobs.length][];
490     AlignmentOrder[] orders = new AlignmentOrder[jobs.length];
491     String lastProgram = null;
492     MsaWSJob msjob;
493     for (int j = 0; j < jobs.length; j++)
494     {
495       if (jobs[j].hasResults())
496       {
497         msjob = (MsaWSJob) jobs[j];
498         Object[] res = msjob.getAlignment();
499         lastProgram = msjob.getAlignmentProgram();
500         alorders.add((AlignmentOrder) res[1]);
501         results[j] = (SequenceI[]) res[0];
502         orders[j] = (AlignmentOrder) res[1];
503
504         // SequenceI[] alignment = input.getUpdated
505       }
506       else
507       {
508         results[j] = null;
509       }
510     }
511     Object[] newview = input.getUpdatedView(results, orders, getGapChar());
512     // trash references to original result data
513     for (int j = 0; j < jobs.length; j++)
514     {
515       results[j] = null;
516       orders[j] = null;
517     }
518     SequenceI[] alignment = (SequenceI[]) newview[0];
519     HiddenColumns hidden = (HiddenColumns) newview[1];
520     Alignment al = new Alignment(alignment);
521     // TODO: add 'provenance' property to alignment from the method notes
522     if (lastProgram != null)
523     {
524       al.setProperty("Alignment Program", lastProgram);
525     }
526     // accompanying each subjob
527     if (dataset != null)
528     {
529       al.setDataset(dataset);
530     }
531
532     propagateDatasetMappings(al);
533     // JBNote- TODO: warn user if a block is input rather than aligned data ?
534
535     if (newFrame)
536     {
537       displayInNewFrame(al, alorders, hidden);
538     }
539     else
540     {
541       // TODO 2.9.x feature
542       System.out.println("MERGE WITH OLD FRAME");
543       // TODO: modify alignment in original frame, replacing old for new
544       // alignment using the commands.EditCommand model to ensure the update can
545       // be undone
546     }
547   }
548
549   /**
550    * Display the alignment result in a new frame.
551    * 
552    * @param al
553    * @param alorders
554    * @param columnselection
555    */
556   protected void displayInNewFrame(AlignmentI al,
557           List<AlignmentOrder> alorders, HiddenColumns hidden)
558   {
559     AlignFrame af = new AlignFrame(al, hidden, AlignFrame.DEFAULT_WIDTH,
560             AlignFrame.DEFAULT_HEIGHT);
561
562     // initialise with same renderer settings as in parent alignframe.
563     af.getFeatureRenderer().transferSettings(this.featureSettings);
564
565     if (alorders.size() > 0)
566     {
567       addSortByMenuItems(af, alorders);
568     }
569
570     // TODO: refactor retrieve and show as new splitFrame as Desktop method
571
572     /*
573      * If alignment was requested from one half of a SplitFrame, show in a
574      * SplitFrame with the other pane similarly aligned.
575      */
576     AlignFrame requestedBy = getRequestingAlignFrame();
577     if (requestedBy != null && requestedBy.getSplitViewContainer() != null
578             && requestedBy.getSplitViewContainer()
579                     .getComplement(requestedBy) != null)
580     {
581       AlignmentI complement = requestedBy.getSplitViewContainer()
582               .getComplement(requestedBy);
583       String complementTitle = requestedBy.getSplitViewContainer()
584               .getComplementTitle(requestedBy);
585       // becomes null if the alignment window was closed before the alignment
586       // job finished.
587       AlignmentI copyComplement = new Alignment(complement);
588       // todo should this be done by copy constructor?
589       copyComplement.setGapCharacter(complement.getGapCharacter());
590       // share the same dataset (and the mappings it holds)
591       copyComplement.setDataset(complement.getDataset());
592       copyComplement.alignAs(al);
593       if (copyComplement.getHeight() > 0)
594       {
595         af.setTitle(alTitle);
596         AlignFrame af2 = new AlignFrame(copyComplement,
597                 AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
598         af2.setTitle(complementTitle);
599         String linkedTitle = MessageManager
600                 .getString("label.linked_view_title");
601         JInternalFrame splitFrame = new SplitFrame(
602                 al.isNucleotide() ? af : af2, al.isNucleotide() ? af2 : af);
603         Desktop.addInternalFrame(splitFrame, linkedTitle, -1, -1);
604         return;
605       }
606     }
607
608     /*
609      * Not from SplitFrame, or failed to created a complementary alignment
610      */
611     Desktop.addInternalFrame(af, alTitle, AlignFrame.DEFAULT_WIDTH,
612             AlignFrame.DEFAULT_HEIGHT);
613   }
614
615   /**
616    * Add sort order options to the AlignFrame menus.
617    * 
618    * @param af
619    * @param alorders
620    */
621   protected void addSortByMenuItems(AlignFrame af,
622           List<AlignmentOrder> alorders)
623   {
624     // update orders
625     if (alorders.size() == 1)
626     {
627       af.addSortByOrderMenuItem(WebServiceName + " Ordering",
628               alorders.get(0));
629     }
630     else
631     {
632       // construct a non-redundant ordering set
633       List<String> names = new ArrayList<>();
634       for (int i = 0, l = alorders.size(); i < l; i++)
635       {
636         String orderName = " Region " + i;
637         int j = i + 1;
638
639         while (j < l)
640         {
641           if (alorders.get(i).equals(alorders.get(j)))
642           {
643             alorders.remove(j);
644             l--;
645             orderName += "," + j;
646           }
647           else
648           {
649             j++;
650           }
651         }
652
653         if (i == 0 && j == 1)
654         {
655           names.add("");
656         }
657         else
658         {
659           names.add(orderName);
660         }
661       }
662       for (int i = 0, l = alorders.size(); i < l; i++)
663       {
664         af.addSortByOrderMenuItem(
665                 WebServiceName + (names.get(i)) + " Ordering",
666                 alorders.get(i));
667       }
668     }
669   }
670
671   @Override
672   public boolean canMergeResults()
673   {
674     return false;
675   }
676 }