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