JAL-1432 updated copyright notices
[jalview.git] / src / jalview / ws / jws1 / SeqSearchWSThread.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8.0b1)
3  * Copyright (C) 2014 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 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  * The Jalview Authors are detailed in the 'AUTHORS' file.
18  */
19 package jalview.ws.jws1;
20
21 import java.util.*;
22
23 import jalview.analysis.*;
24 import jalview.bin.*;
25 import jalview.datamodel.*;
26 import jalview.gui.*;
27 import jalview.io.NewickFile;
28 import jalview.ws.AWsJob;
29 import jalview.ws.JobStateSummary;
30 import jalview.ws.WSClientI;
31 import vamsas.objects.simple.MsaResult;
32 import vamsas.objects.simple.SeqSearchResult;
33
34 class SeqSearchWSThread extends JWS1Thread implements WSClientI
35 {
36   String dbs = null;
37
38   boolean profile = false;
39
40   class SeqSearchWSJob extends WSJob
41   {
42     // hold special input for this
43     vamsas.objects.simple.SequenceSet seqs = new vamsas.objects.simple.SequenceSet();
44
45     /**
46      * MsaWSJob
47      * 
48      * @param jobNum
49      *          int
50      * @param jobId
51      *          String
52      */
53     public SeqSearchWSJob(int jobNum, SequenceI[] inSeqs)
54     {
55       this.jobnum = jobNum;
56       if (!prepareInput(inSeqs, 2))
57       {
58         submitted = true;
59         subjobComplete = true;
60         result = new MsaResult();
61         result.setFinished(true);
62         result.setStatus("Job never ran - input returned to user.");
63       }
64
65     }
66
67     Hashtable SeqNames = new Hashtable();
68
69     Vector emptySeqs = new Vector();
70
71     /**
72      * prepare input sequences for service
73      * 
74      * @param seqs
75      *          jalview sequences to be prepared
76      * @param minlen
77      *          minimum number of residues required for this MsaWS service
78      * @return true if seqs contains sequences to be submitted to service.
79      */
80     private boolean prepareInput(SequenceI[] seqs, int minlen)
81     {
82       int nseqs = 0;
83       if (minlen < 0)
84       {
85         throw new Error(
86                 "Implementation error: minlen must be zero or more.");
87       }
88       for (int i = 0; i < seqs.length; i++)
89       {
90         if (seqs[i].getEnd() - seqs[i].getStart() > minlen - 1)
91         {
92           nseqs++;
93         }
94       }
95       boolean valid = nseqs >= 1; // need at least one sequence for valid input
96       // TODO: generalise
97       vamsas.objects.simple.Sequence[] seqarray = (valid) ? new vamsas.objects.simple.Sequence[nseqs]
98               : null;
99       boolean submitGaps = (nseqs == 1) ? false : true; // profile is submitted
100       // with gaps
101       for (int i = 0, n = 0; i < seqs.length; i++)
102       {
103
104         String newname = jalview.analysis.SeqsetUtils.unique_name(i); // same
105         // for
106         // any
107         // subjob
108         SeqNames.put(newname,
109                 jalview.analysis.SeqsetUtils.SeqCharacterHash(seqs[i]));
110         if (valid && seqs[i].getEnd() - seqs[i].getStart() > minlen - 1)
111         {
112           seqarray[n] = new vamsas.objects.simple.Sequence();
113           seqarray[n].setId(newname);
114           seqarray[n++].setSeq((submitGaps) ? seqs[i].getSequenceAsString()
115                   : AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
116                           seqs[i].getSequenceAsString()));
117         }
118         else
119         {
120           String empty = null;
121           if (seqs[i].getEnd() >= seqs[i].getStart())
122           {
123             empty = (submitGaps) ? seqs[i].getSequenceAsString() : AlignSeq
124                     .extractGaps(jalview.util.Comparison.GapChars,
125                             seqs[i].getSequenceAsString());
126           }
127           emptySeqs.add(new String[]
128           { newname, empty });
129         }
130       }
131       if (submitGaps)
132       {
133         // almost certainly have to remove gapped columns here
134       }
135       this.seqs = new vamsas.objects.simple.SequenceSet();
136       this.seqs.setSeqs(seqarray);
137       return valid;
138     }
139
140     /**
141      * 
142      * @return true if getAlignment will return a valid alignment result.
143      */
144     public boolean hasResults()
145     {
146       if (subjobComplete
147               && result != null
148               && result.isFinished()
149               && ((SeqSearchResult) result).getAlignment() != null
150               && ((SeqSearchResult) result).getAlignment().getSeqs() != null)
151       {
152         return true;
153       }
154       return false;
155     }
156
157     /**
158      * return sequence search results for display
159      * 
160      * @return null or { Alignment(+features and annotation), NewickFile)}
161      */
162     public Object[] getAlignment(Alignment dataset, Hashtable featureColours)
163     {
164
165       if (result != null && result.isFinished())
166       {
167         SequenceI[] alseqs = null;
168         // char alseq_gapchar = '-';
169         // int alseq_l = 0;
170         if (((SeqSearchResult) result).getAlignment() != null)
171         {
172           alseqs = getVamsasAlignment(((SeqSearchResult) result)
173                   .getAlignment());
174           // alseq_gapchar = ( (SeqSearchResult)
175           // result).getAlignment().getGapchar().charAt(0);
176           // alseq_l = alseqs.length;
177         }
178         /**
179          * what has to be done. 1 - annotate returned alignment with annotation
180          * file and sequence features file, and associate any tree-nodes. 2.
181          * connect alignment back to any associated dataset: 2.a. deuniquify
182          * recovers sequence information - but additionally, relocations must be
183          * made from the returned aligned sequence back to the dataset.
184          */
185         // construct annotated alignment as it would be done by the jalview
186         // applet
187         jalview.datamodel.Alignment al = new Alignment(alseqs);
188         // al.setDataset(dataset);
189         // make dataset
190         String inFile = null;
191         try
192         {
193           inFile = ((SeqSearchResult) result).getAnnotation();
194           if (inFile != null && inFile.length() > 0)
195           {
196             new jalview.io.AnnotationFile().readAnnotationFile(al, inFile,
197                     jalview.io.AppletFormatAdapter.PASTE);
198           }
199         } catch (Exception e)
200         {
201           System.err
202                   .println("Failed to parse the annotation file associated with the alignment.");
203           System.err.println(">>>EOF" + inFile + "\n<<<EOF\n");
204           e.printStackTrace(System.err);
205         }
206
207         try
208         {
209           inFile = ((SeqSearchResult) result).getFeatures();
210           if (inFile != null && inFile.length() > 0)
211           {
212             jalview.io.FeaturesFile ff = new jalview.io.FeaturesFile(
213                     inFile, jalview.io.AppletFormatAdapter.PASTE);
214             ff.parse(al, featureColours, false);
215           }
216         } catch (Exception e)
217         {
218           System.err
219                   .println("Failed to parse the Features file associated with the alignment.");
220           System.err.println(">>>EOF" + inFile + "\n<<<EOF\n");
221           e.printStackTrace(System.err);
222         }
223         jalview.io.NewickFile nf = null;
224         try
225         {
226           inFile = ((SeqSearchResult) result).getNewickTree();
227           if (inFile != null && inFile.length() > 0)
228           {
229             nf = new jalview.io.NewickFile(inFile,
230                     jalview.io.AppletFormatAdapter.PASTE);
231             if (!nf.isValid())
232             {
233               nf.close();
234               nf = null;
235             }
236           }
237         } catch (Exception e)
238         {
239           System.err
240                   .println("Failed to parse the treeFile associated with the alignment.");
241           System.err.println(">>>EOF" + inFile + "\n<<<EOF\n");
242           e.printStackTrace(System.err);
243         }
244
245         /*
246          * TODO: housekeeping w.r.t. recovery of dataset and annotation
247          * references for input sequences, and then dataset sequence creation
248          * for new sequences retrieved from service // finally, attempt to
249          * de-uniquify to recover input sequence identity, and try to map back
250          * onto dataset Note: this
251          * jalview.analysis.SeqsetUtils.deuniquify(SeqNames, alseqs, true); will
252          * NOT WORK - the returned alignment may contain multiple versions of
253          * the input sequence, each being a subsequence of the original.
254          * deuniquify also removes existing annotation and features added in the
255          * previous step... al.setDataset(dataset); // add in new sequences
256          * retrieved from sequence search which are not already in dataset. //
257          * trigger a 'fetchDBids' to annotate sequences with database ids...
258          */
259
260         return new Object[]
261         { al, nf };
262       }
263       return null;
264     }
265
266     /**
267      * mark subjob as cancelled and set result object appropriatly
268      */
269     void cancel()
270     {
271       cancelled = true;
272       subjobComplete = true;
273       result = null;
274     }
275
276     /**
277      * 
278      * @return boolean true if job can be submitted.
279      */
280     public boolean hasValidInput()
281     {
282       if (seqs.getSeqs() != null)
283       {
284         return true;
285       }
286       return false;
287     }
288   }
289
290   String alTitle; // name which will be used to form new alignment window.
291
292   Alignment dataset; // dataset to which the new alignment will be
293
294   // associated.
295
296   ext.vamsas.SeqSearchI server = null;
297
298   private String dbArg;
299
300   /**
301    * set basic options for this (group) of Msa jobs
302    * 
303    * @param subgaps
304    *          boolean
305    * @param presorder
306    *          boolean
307    */
308   SeqSearchWSThread(ext.vamsas.SeqSearchI server, String wsUrl,
309           WebserviceInfo wsinfo, jalview.gui.AlignFrame alFrame,
310           AlignmentView alview, String wsname, String db)
311   {
312     super(alFrame, wsinfo, alview, wsname, wsUrl);
313     this.server = server;
314     this.dbArg = db;
315   }
316
317   /**
318    * create one or more Msa jobs to align visible seuqences in _msa
319    * 
320    * @param title
321    *          String
322    * @param _msa
323    *          AlignmentView
324    * @param subgaps
325    *          boolean
326    * @param presorder
327    *          boolean
328    * @param seqset
329    *          Alignment
330    */
331   SeqSearchWSThread(ext.vamsas.SeqSearchI server, String wsUrl,
332           WebserviceInfo wsinfo, jalview.gui.AlignFrame alFrame,
333           String wsname, String title, AlignmentView _msa, String db,
334           Alignment seqset)
335   {
336     this(server, wsUrl, wsinfo, alFrame, _msa, wsname, db);
337     OutputHeader = wsInfo.getProgressText();
338     alTitle = title;
339     dataset = seqset;
340
341     SequenceI[][] conmsa = _msa.getVisibleContigs('-');
342     if (conmsa != null)
343     {
344       int njobs = conmsa.length;
345       jobs = new SeqSearchWSJob[njobs];
346       for (int j = 0; j < njobs; j++)
347       {
348         if (j != 0)
349         {
350           jobs[j] = new SeqSearchWSJob(wsinfo.addJobPane(), conmsa[j]);
351         }
352         else
353         {
354           jobs[j] = new SeqSearchWSJob(0, conmsa[j]);
355         }
356         if (njobs > 0)
357         {
358           wsinfo.setProgressName("region " + jobs[j].getJobnum(),
359                   jobs[j].getJobnum());
360         }
361         wsinfo.setProgressText(jobs[j].getJobnum(), OutputHeader);
362       }
363     }
364   }
365
366   public boolean isCancellable()
367   {
368     return true;
369   }
370
371   public void cancelJob()
372   {
373     if (!jobComplete && jobs != null)
374     {
375       boolean cancelled = true;
376       for (int job = 0; job < jobs.length; job++)
377       {
378         if (jobs[job].isSubmitted() && !jobs[job].isSubjobComplete())
379         {
380           String cancelledMessage = "";
381           try
382           {
383             vamsas.objects.simple.WsJobId cancelledJob = server
384                     .cancel(jobs[job].getJobId());
385             if (cancelledJob.getStatus() == 2)
386             {
387               // CANCELLED_JOB
388               cancelledMessage = "Job cancelled.";
389               ((SeqSearchWSJob) jobs[job]).cancel();
390               wsInfo.setStatus(jobs[job].getJobnum(),
391                       WebserviceInfo.STATE_CANCELLED_OK);
392             }
393             else if (cancelledJob.getStatus() == 3)
394             {
395               // VALID UNSTOPPABLE JOB
396               cancelledMessage += "Server cannot cancel this job. just close the window.\n";
397               cancelled = false;
398               // wsInfo.setStatus(jobs[job].jobnum,
399               // WebserviceInfo.STATE_RUNNING);
400             }
401
402             if (cancelledJob.getJobId() != null)
403             {
404               cancelledMessage += ("[" + cancelledJob.getJobId() + "]");
405             }
406
407             cancelledMessage += "\n";
408           } catch (Exception exc)
409           {
410             cancelledMessage += ("\nProblems cancelling the job : Exception received...\n"
411                     + exc + "\n");
412             Cache.log.warn(
413                     "Exception whilst cancelling " + jobs[job].getJobId(),
414                     exc);
415           }
416           wsInfo.setProgressText(jobs[job].getJobnum(), OutputHeader
417                   + cancelledMessage + "\n");
418         }
419       }
420       if (cancelled)
421       {
422         wsInfo.setStatus(WebserviceInfo.STATE_CANCELLED_OK);
423         jobComplete = true;
424       }
425       this.interrupt(); // kick thread to update job states.
426     }
427     else
428     {
429       if (!jobComplete)
430       {
431         wsInfo.setProgressText(OutputHeader
432                 + "Server cannot cancel this job because it has not been submitted properly. just close the window.\n");
433       }
434     }
435   }
436
437   public void pollJob(AWsJob job) throws Exception
438   {
439     ((SeqSearchWSJob) job).result = server.getResult(((SeqSearchWSJob) job)
440             .getJobId());
441   }
442
443   public void StartJob(AWsJob job)
444   {
445     if (!(job instanceof SeqSearchWSJob))
446     {
447       throw new Error("StartJob(MsaWSJob) called on a WSJobInstance "
448               + job.getClass());
449     }
450     SeqSearchWSJob j = (SeqSearchWSJob) job;
451     if (j.isSubmitted())
452     {
453       if (Cache.log.isDebugEnabled())
454       {
455         Cache.log.debug("Tried to submit an already submitted job "
456                 + j.getJobId());
457       }
458       return;
459     }
460     if (j.seqs.getSeqs() == null)
461     {
462       // special case - selection consisted entirely of empty sequences...
463       j.setSubmitted(true);
464       j.result = new MsaResult();
465       j.result.setFinished(true);
466       j.result.setStatus("Empty Alignment Job");
467       ((MsaResult) j.result).setMsa(null);
468     }
469     try
470     {
471       vamsas.objects.simple.WsJobId jobsubmit = server.search(
472               j.seqs.getSeqs()[0], dbArg);
473
474       if ((jobsubmit != null) && (jobsubmit.getStatus() == 1))
475       {
476         j.setJobId(jobsubmit.getJobId());
477         j.setSubmitted(true);
478         j.setSubjobComplete(false);
479         // System.out.println(WsURL + " Job Id '" + jobId + "'");
480       }
481       else
482       {
483         if (jobsubmit == null)
484         {
485           throw new Exception(
486                   "Server at "
487                           + WsUrl
488                           + " returned null object, it probably cannot be contacted. Try again later ?");
489         }
490
491         throw new Exception(jobsubmit.getJobId());
492       }
493     } catch (Exception e)
494     {
495       // TODO: JBPNote catch timeout or other fault types explicitly
496       // For unexpected errors
497       System.err
498               .println(WebServiceName
499                       + "Client: Failed to submit the sequences for alignment (probably a server side problem)\n"
500                       + "When contacting Server:" + WsUrl + "\n"
501                       + e.toString() + "\n");
502       j.setAllowedServerExceptions(0);
503       wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR);
504       wsInfo.setStatus(j.getJobnum(),
505               WebserviceInfo.STATE_STOPPED_SERVERERROR);
506       wsInfo.appendProgressText(
507               j.getJobnum(),
508               "Failed to submit sequences for alignment.\n"
509                       + "It is most likely that there is a problem with the server.\n"
510                       + "Just close the window\n");
511
512       // e.printStackTrace(); // TODO: JBPNote DEBUG
513     }
514   }
515
516   private jalview.datamodel.Sequence[] getVamsasAlignment(
517           vamsas.objects.simple.Alignment valign)
518   {
519     vamsas.objects.simple.Sequence[] seqs = valign.getSeqs().getSeqs();
520     jalview.datamodel.Sequence[] msa = new jalview.datamodel.Sequence[seqs.length];
521
522     for (int i = 0, j = seqs.length; i < j; i++)
523     {
524       msa[i] = new jalview.datamodel.Sequence(seqs[i].getId(),
525               seqs[i].getSeq());
526     }
527
528     return msa;
529   }
530
531   public void parseResult()
532   {
533     int results = 0; // number of result sets received
534     JobStateSummary finalState = new JobStateSummary();
535     try
536     {
537       for (int j = 0; j < jobs.length; j++)
538       {
539         finalState.updateJobPanelState(wsInfo, OutputHeader, jobs[j]);
540         if (jobs[j].isSubmitted() && jobs[j].isSubjobComplete()
541                 && jobs[j].hasResults())
542         {
543           results++;
544           vamsas.objects.simple.Alignment valign = ((SeqSearchResult) ((SeqSearchWSJob) jobs[j]).result)
545                   .getAlignment();
546           if (valign != null)
547           {
548             wsInfo.appendProgressText(jobs[j].getJobnum(),
549                     "\nAlignment Object Method Notes\n");
550             String[] lines = valign.getMethod();
551             for (int line = 0; line < lines.length; line++)
552             {
553               wsInfo.appendProgressText(jobs[j].getJobnum(), lines[line]
554                       + "\n");
555             }
556             // JBPNote The returned files from a webservice could be
557             // hidden behind icons in the monitor window that,
558             // when clicked, pop up their corresponding data
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     if (!newFrame)
598     {
599       System.err.println("MERGE WITH OLD FRAME NOT IMPLEMENTED");
600       return;
601     }
602     // each subjob is an independent alignment for the moment
603     // Alignment al[] = new Alignment[jobs.length];
604     // NewickFile nf[] = new NewickFile[jobs.length];
605     for (int j = 0; j < jobs.length; j++)
606     {
607       Hashtable featureColours = new Hashtable();
608       Alignment al = null;
609       NewickFile nf = null;
610       if (jobs[j].hasResults())
611       {
612         Object[] res = ((SeqSearchWSJob) jobs[j]).getAlignment(dataset,
613                 featureColours);
614         if (res == null)
615         {
616           continue;
617         }
618         ;
619         al = (Alignment) res[0];
620         nf = (NewickFile) res[1];
621       }
622       else
623       {
624         al = null;
625         nf = null;
626         continue;
627       }
628       /*
629        * We can't map new alignment back with insertions from input's hidden
630        * regions until dataset mapping is sorted out... but basically it goes
631        * like this: 1. Merge each domain hit back onto the visible segments in
632        * the same way as a Jnet prediction is mapped back
633        * 
634        * Object[] newview = input.getUpdatedView(results, orders, getGapChar());
635        * // trash references to original result data for (int j = 0; j <
636        * jobs.length; j++) { results[j] = null; orders[j] = null; } SequenceI[]
637        * alignment = (SequenceI[]) newview[0]; ColumnSelection columnselection =
638        * (ColumnSelection) newview[1]; Alignment al = new Alignment(alignment);
639        * 
640        * if (dataset != null) { al.setDataset(dataset); }
641        * 
642        * propagateDatasetMappings(al); }
643        */
644
645       AlignFrame af = new AlignFrame(al,// columnselection,
646               AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
647       if (nf != null)
648       {
649         af.ShowNewickTree(nf, "Tree from " + this.alTitle);
650       }
651       // initialise with same renderer settings as in parent alignframe.
652       af.getFeatureRenderer().transferSettings(this.featureSettings);
653       Desktop.addInternalFrame(af, alTitle, AlignFrame.DEFAULT_WIDTH,
654               AlignFrame.DEFAULT_HEIGHT);
655     }
656   }
657
658   public boolean canMergeResults()
659   {
660     return false;
661   }
662 }