incremental refinement of results parsing
[jalview.git] / src / jalview / ws / rest / RestJobThread.java
1 package jalview.ws.rest;
2
3 import jalview.bin.Cache;
4 import jalview.datamodel.AlignmentI;
5 import jalview.datamodel.ColumnSelection;
6 import jalview.gui.AlignFrame;
7 import jalview.gui.WebserviceInfo;
8 import jalview.io.packed.DataProvider;
9 import jalview.io.packed.JalviewDataset;
10 import jalview.io.packed.JalviewDataset.AlignmentSet;
11 import jalview.io.packed.ParsePackedSet;
12 import jalview.io.packed.SimpleDataProvider;
13 import jalview.io.packed.DataProvider.JvDataType;
14 import jalview.ws.AWSThread;
15 import jalview.ws.AWsJob;
16
17 import java.awt.Desktop;
18 import java.awt.event.ActionEvent;
19 import java.awt.event.ActionListener;
20 import java.io.IOException;
21 import java.util.ArrayList;
22 import java.util.List;
23 import java.util.Map.Entry;
24
25 import org.apache.axis.transport.http.HTTPConstants;
26 import org.apache.http.Header;
27 import org.apache.http.HttpEntity;
28 import org.apache.http.HttpResponse;
29 import org.apache.http.client.ClientProtocolException;
30 import org.apache.http.client.methods.HttpGet;
31 import org.apache.http.client.methods.HttpPost;
32 import org.apache.http.client.methods.HttpRequestBase;
33 import org.apache.http.entity.mime.HttpMultipartMode;
34 import org.apache.http.entity.mime.MultipartEntity;
35 import org.apache.http.impl.client.DefaultHttpClient;
36 import org.apache.http.protocol.BasicHttpContext;
37 import org.apache.http.protocol.HttpContext;
38 import org.apache.http.util.EntityUtils;
39
40 public class RestJobThread extends AWSThread
41 {
42   enum Stage
43   {
44     SUBMIT, POLL
45   }
46
47   protected RestClient restClient;;
48
49   public RestJobThread(RestClient restClient)
50   {
51     super(restClient.af, null, restClient._input,
52             restClient.service.postUrl);
53     this.restClient = restClient; // may not be needed
54     // Test Code
55     // minimal job - submit given input and parse result onto alignment as
56     // annotation/whatever
57
58     // look for tree data, etc.
59
60     // for moment do following job type only
61     // input=visiblealignment,groupsindex
62     // ie one subjob using groups defined on alignment.
63     if (!restClient.service.isHseparable())
64     {
65       jobs = new RestJob[1];
66       jobs[0] = new RestJob(0, this,
67               restClient._input.getVisibleAlignment(restClient.service
68                       .getGapCharacter()),
69               restClient._input.getVisibleContigs());
70       // need a function to get a range on a view/alignment and return both
71       // annotation, groups and selection subsetted to just that region.
72
73     }
74     else
75     {
76       int[] viscontig = restClient._input.getVisibleContigs();
77       AlignmentI[] viscontigals = restClient._input
78               .getVisibleContigAlignments(restClient.service
79                       .getGapCharacter());
80       if (viscontigals != null && viscontigals.length > 0)
81       {
82         jobs = new RestJob[viscontigals.length];
83         for (int j = 0; j < jobs.length; j++)
84         {
85           int[] visc = new int[]
86           { viscontig[j * 2], viscontig[j * 2 + 1] };
87           if (j != 0)
88           {
89             jobs[j] = new RestJob(j, this, viscontigals[j], visc);
90           }
91           else
92           {
93             jobs[j] = new RestJob(0, this, viscontigals[j], visc);
94           }
95         }
96       }
97     }
98     // end Test Code
99     /**
100      * subjob types row based: per sequence in alignment/selected region { input
101      * is one sequence or sequence ID } per alignment/selected region { input is
102      * set of sequences, alignment, one or more sets of sequence IDs,
103      */
104
105     if (!restClient.service.isHseparable())
106     {
107
108       // create a bunch of subjobs per visible contig to ensure result honours
109       // hidden boundaries
110       // TODO: determine if a 'soft' hSeperable property is needed - e.g. if
111       // user does SS-pred on sequence with big hidden regions, its going to be
112       // less reliable.
113     }
114     else
115     {
116       // create a single subjob for the visible/selected input
117
118     }
119     // TODO: decide if vSeperable exists: eg. column wide analysis where hidden
120     // rows do not affect output - generally no analysis that generates
121     // alignment annotation is vSeparable -
122   }
123
124   /**
125    * create gui components for monitoring jobs
126    * 
127    * @param webserviceInfo
128    */
129   public void setWebServiceInfo(WebserviceInfo webserviceInfo)
130   {
131     wsInfo = webserviceInfo;
132     for (int j = 0; j < jobs.length; j++)
133     {
134       wsInfo.addJobPane();
135       // Copy over any per job params
136       if (jobs.length > 1)
137       {
138         wsInfo.setProgressName("region " + jobs[j].getJobnum(),
139                 jobs[j].getJobnum());
140       }
141       else
142       {
143         wsInfo.setProgressText(jobs[j].getJobnum(), OutputHeader);
144       }
145     }
146   }
147
148   private String getStage(Stage stg)
149   {
150     if (stg == Stage.SUBMIT)
151       return "submitting ";
152     if (stg == Stage.POLL)
153       return "checking status of ";
154
155     return (" being confused about ");
156   }
157
158   private void doPoll(RestJob rj) throws Exception
159   {
160     String postUrl = rj.getPollUrl();
161     doHttpReq(Stage.POLL, rj, postUrl);
162   }
163
164   /**
165    * construct the post and handle the response.
166    * 
167    * @throws Exception
168    */
169   public void doPost(RestJob rj) throws Exception
170   {
171     String postUrl = rj.getPostUrl();
172     doHttpReq(Stage.SUBMIT, rj, postUrl);
173   }
174
175   /**
176    * do the HTTP request - and respond/set statuses appropriate to the current
177    * stage.
178    * 
179    * @param stg
180    * @param rj
181    *          - provides any data needed for posting and used to record state
182    * @param postUrl
183    *          - actual URL to post/get from
184    * @throws Exception
185    */
186   protected void doHttpReq(Stage stg, RestJob rj, String postUrl)
187           throws Exception
188   {
189     StringBuffer respText = new StringBuffer();
190     // con.setContentHandlerFactory(new
191     // jalview.ws.io.mime.HttpContentHandler());
192     HttpRequestBase request = null;
193     String messages = "";
194     if (stg == Stage.SUBMIT)
195     {
196       // Got this from
197       // http://evgenyg.wordpress.com/2010/05/01/uploading-files-multipart-post-apache/
198
199       HttpPost htpost = new HttpPost(postUrl);
200       MultipartEntity postentity = new MultipartEntity(
201               HttpMultipartMode.STRICT);
202       for (Entry<String, InputType> input : rj.getInputParams())
203       {
204         if (input.getValue().validFor(rj))
205         {
206           postentity.addPart(input.getKey(), input.getValue()
207                   .formatForInput(rj));
208         }
209         else
210         {
211           messages += "Skipped an input (" + input.getKey()
212                   + ") - Couldn't generate it from available data.";
213         }
214       }
215       htpost.setEntity(postentity);
216       request = htpost;
217     }
218     else
219     {
220       request = new HttpGet(postUrl);
221     }
222     if (request != null)
223     {
224       DefaultHttpClient httpclient = new DefaultHttpClient();
225
226       HttpContext localContext = new BasicHttpContext();
227       HttpResponse response = null;
228       try
229       {
230         response = httpclient.execute(request);
231       } catch (ClientProtocolException he)
232       {
233         rj.statMessage = "Web Protocol Exception when attempting to  "
234                 + getStage(stg) + "Job. See Console output for details.";
235         rj.setAllowedServerExceptions(0);// unrecoverable;
236         rj.error = true;
237         Cache.log.fatal("Unexpected REST Job " + getStage(stg)
238                 + "exception for URL " + rj.rsd.postUrl);
239         throw (he);
240       } catch (IOException e)
241       {
242         rj.statMessage = "IO Exception when attempting to  "
243                 + getStage(stg) + "Job. See Console output for details.";
244         Cache.log.warn("IO Exception for REST Job " + getStage(stg)
245                 + "exception for URL " + rj.rsd.postUrl);
246
247         throw (e);
248       }
249       switch (response.getStatusLine().getStatusCode())
250       {
251       case 200:
252         rj.running = false;
253         Cache.log.debug("Processing result set.");
254         processResultSet(rj, response, request);
255         break;
256       case 202:
257         rj.statMessage = "<br>Job submitted successfully. Results available at this URL:\n"
258                 + "<a href="
259                 + rj.getJobId()
260                 + "\">"
261                 + rj.getJobId()
262                 + "</a><br>";
263         rj.running = true;
264         break;
265       case 302:
266         Header[] loc;
267         if (!rj.isSubmitted()
268                 && (loc = response
269                         .getHeaders(HTTPConstants.HEADER_LOCATION)) != null
270                 && loc.length > 0)
271         {
272           if (loc.length > 1)
273           {
274             Cache.log
275                     .warn("Ignoring additional "
276                             + (loc.length - 1)
277                             + " location(s) provided in response header ( next one is '"
278                             + loc[1].getValue() + "' )");
279           }
280           rj.setJobId(loc[0].getValue());
281           rj.setSubmitted(true);
282         }
283         completeStatus(rj, response);
284         break;
285       case 500:
286         // Failed.
287         rj.setSubmitted(true);
288         rj.setAllowedServerExceptions(0);
289         rj.setSubjobComplete(true);
290         rj.error = true;
291         rj.running = false;
292         completeStatus(rj, response, "" + getStage(stg)
293                 + "failed. Reason below:\n");
294         break;
295       default:
296         // Some other response. Probably need to pop up the content in a window.
297         // TODO: deal with all other HTTP response codes from server.
298         Cache.log.warn("Unhandled response status when " + getStage(stg)
299                 + "for " + postUrl + ": " + response.getStatusLine());
300         try
301         {
302           response.getEntity().consumeContent();
303         } catch (IOException e)
304         {
305           Cache.log.debug("IOException when consuming unhandled response",
306                   e);
307         }
308         ;
309       }
310     }
311   }
312
313   /**
314    * job has completed. Something valid should be available from con
315    * 
316    * @param rj
317    * @param con
318    * @param req
319    *          is a stateless request - expected to return the same data
320    *          regardless of how many times its called.
321    */
322   private void processResultSet(RestJob rj, HttpResponse con,
323           HttpRequestBase req)
324   {
325     if (rj.statMessage == null)
326     {
327       rj.statMessage = "";
328     }
329     rj.statMessage += "Job Complete.\n";
330     try
331     {
332       rj.resSet = new HttpResultSet(rj, con, req);
333       rj.gotresult = true;
334     } catch (IOException e)
335     {
336       rj.statMessage += "Couldn't parse results. Failed.";
337       rj.error = true;
338       rj.gotresult = false;
339     }
340   }
341
342   private void completeStatus(RestJob rj, HttpResponse con)
343           throws IOException
344   {
345     completeStatus(rj, con, null);
346
347   }
348
349   private void completeStatus(RestJob rj, HttpResponse con, String prefix)
350           throws IOException
351   {
352     StringBuffer sb = new StringBuffer();
353     if (prefix != null)
354     {
355       sb.append(prefix);
356     }
357     ;
358     if (rj.statMessage != null && rj.statMessage.length() > 0)
359     {
360       sb.append(rj.statMessage);
361     }
362     HttpEntity en = con.getEntity();
363     /*
364      * Just append the content as a string.
365      */
366     String f;
367     StringBuffer content = new StringBuffer(f = EntityUtils.toString(en));
368     f = f.toLowerCase();
369     int body = f.indexOf("<body");
370     if (body > -1)
371     {
372       content.delete(0, f.indexOf(">", body));
373     }
374     if (body > -1 && sb.length() > 0)
375     {
376       sb.append("\n");
377       content.insert(0, sb);
378       sb = null;
379     }
380     f = null;
381     rj.statMessage = content.toString();
382   }
383
384   @Override
385   public void pollJob(AWsJob job) throws Exception
386   {
387     assert (job instanceof RestJob);
388     System.err.println("Debug RestJob: Polling Job");
389     doPoll((RestJob) job);
390   }
391
392   @Override
393   public void StartJob(AWsJob job)
394   {
395     assert (job instanceof RestJob);
396     try
397     {
398       System.err.println("Debug RestJob: Posting Job");
399       doPost((RestJob) job);
400     } catch (Exception ex)
401     {
402       job.setSubjobComplete(true);
403       job.setAllowedServerExceptions(-1);
404       Cache.log.error("Exception when trying to start Rest Job.", ex);
405     }
406   }
407
408   @Override
409   public void parseResult()
410   {
411     // crazy users will see this message
412     // TODO: finish this! and remove the message below!
413     Cache.log.warn("Rest job result parser is currently INCOMPLETE!");
414     int validres = 0;
415     for (RestJob rj : (RestJob[]) jobs)
416     {
417       if (rj.hasResponse() && rj.resSet != null && rj.resSet.isValid())
418       {
419         String ln = null;
420         try
421         {
422           Cache.log.debug("Parsing data for job " + rj.getJobId());
423           rj.parseResultSet();
424           if (rj.hasResults())
425           {
426             validres++;
427           }
428           Cache.log.debug("Finished parsing data for job " + rj.getJobId());
429
430         } catch (Error ex)
431         {
432           Cache.log.warn("Failed to finish parsing data for job "
433                   + rj.getJobId());
434           ex.printStackTrace();
435         } catch (Exception ex)
436         {
437           Cache.log.warn("Failed to finish parsing data for job "
438                   + rj.getJobId());
439           ex.printStackTrace();
440         }
441       }
442     }
443     if (validres > 0)
444     {
445       // add listeners and activate result display gui elements
446       /**
447        * decisions based on job result content + state of alignFrame that
448        * originated the job:
449        */
450       /*
451        * 1. Can/Should this job automatically open a new window for results
452        */
453       if (true)
454       {
455         wsInfo.setViewResultsImmediatly(false);
456       }
457       else
458       {
459         // realiseResults(true, true);
460       }
461       // otherwise, should automatically view results
462
463       // TODO: check if at least one or more contexts are valid - if so, enable
464       // gui
465       wsInfo.showResultsNewFrame.addActionListener(new ActionListener()
466       {
467
468         @Override
469         public void actionPerformed(ActionEvent e)
470         {
471           realiseResults(false);
472         }
473
474       });
475       wsInfo.mergeResults.addActionListener(new ActionListener()
476       {
477
478         @Override
479         public void actionPerformed(ActionEvent e)
480         {
481           realiseResults(true);
482         }
483
484       });
485
486       wsInfo.setResultsReady();
487     }
488     else
489     {
490       // tell the user nothing was returned.
491     }
492   }
493
494   public void realiseResults(boolean merge)
495   {
496     /*
497      * 2. Should the job modify the parent alignment frame/view(s) (if they
498      * still exist and the alignment hasn't been edited) in order to display new
499      * annotation/features.
500      */
501     /**
502      * alignment panels derived from each alignment set returned by service. 
503      */
504     ArrayList<jalview.gui.AlignmentPanel> destPanels = new ArrayList<jalview.gui.AlignmentPanel>();
505     /**
506      * current pane being worked with
507      */
508     jalview.gui.AlignmentPanel destPanel;
509     /**
510      * when false, zeroth pane is panel derived from input deta.
511      */
512     boolean newAlignment = false;
513     if (merge)
514     {
515       if (!restClient.isAlignmentModified())
516       {
517         destPanel = restClient.recoverAlignPanelForView();
518         if (restClient.isShowResultsInNewView())
519         {
520           destPanel = destPanel.alignFrame.newView(false);
521         }
522         // add the destination panel to frame zero of result panel set
523         destPanels.add(destPanel);
524       }
525     }
526     if (destPanels.size()==0)
527     {
528       Object[] idat = input.getAlignmentAndColumnSelection(restClient.av
529               .getGapCharacter());
530       AlignFrame af = new AlignFrame((AlignmentI) idat[0],
531               (ColumnSelection) idat[1], AlignFrame.DEFAULT_WIDTH,
532               AlignFrame.DEFAULT_HEIGHT);
533       jalview.gui.Desktop.addInternalFrame(af,
534               "Results for " + restClient.service.details.Name + " "
535                       + restClient.service.details.Action + " on "
536                       + restClient.af.getTitle(), AlignFrame.DEFAULT_WIDTH,
537               AlignFrame.DEFAULT_HEIGHT);
538       destPanel = af.alignPanel;
539       // create totally new alignment from stashed data/results
540       newAlignment = true;
541     }
542     // Now process results, adding/creating new views as necessary.
543     {
544       boolean hsepjobs = restClient.service.isHseparable();
545       boolean vsepjobs = restClient.service.isVseparable();
546       // total number of distinct alignment sets generated by job set.
547       int totalSets = 0, numAlSets = 0;
548       for (int j = 0; j < jobs.length; j++)
549       {
550         RestJob rj = (RestJob) jobs[j];
551         if (rj.hasResults())
552         {
553           JalviewDataset rset = rj.context;
554           numAlSets = rset.hasAlignments() ? 0 : rset.getAl().size();
555           if (numAlSets > 0)
556           {
557             for (int als = 0; als < numAlSets; als++)
558             {
559               // gather data from context
560               if (vsepjobs)
561               {
562                 // todo: merge data from each group/sequence onto whole
563                 // alignment
564               }
565               else
566               {
567                 if (hsepjobs)
568                 {
569                   // map single result back on to all visible region of original alignment
570                   if (als==0 && rj.isInputContextModified())
571                   {
572                     // transfer features, annotation, groups, etc, from input context to align panel derived from input data
573                     new jalview.datamodel.Alignment(new jalview.datamodel.SequenceI[] {null}).getAlignmentAnnotation();
574                   }
575                   
576                 }
577                 else
578                 {
579                   // map result onto visible contigs.
580                   AlignmentSet alset = rset.getAl().get(als);
581                   if (als>0 )
582                   if (als == 0)
583                   {
584                     
585                     // alignment is added straight to 
586                   }
587                 }
588               }
589             }
590           }
591         }
592         // transfer results onto panel
593
594       }
595       /**
596        * alignments. New alignments are added to dataset, and subsequently
597        * annotated/visualised accordingly. 1. New alignment frame created for
598        * new alignment. Decide if any vis settings should be inherited from old
599        * alignment frame (e.g. sequence ordering ?). 2. Subsequent data added to
600        * alignment as below:
601        */
602       /**
603        * annotation update to original/newly created context alignment: 1.
604        * identify alignment where annotation is to be loaded onto. 2. Add
605        * annotation, excluding any duplicates. 3. Ensure annotation is visible
606        * on alignment - honouring ordering given by file.
607        */
608       /**
609        * features updated to original or newly created context alignment: 1.
610        * Features are(or were already) added to dataset. 2. Feature settings
611        * modified to ensure all features are displayed - honouring any ordering
612        * given by result file. Consider merging action with the code used by the
613        * DAS fetcher to update alignment views with new info.
614        */
615       /**
616        * Seq associated data files (PDB files). 1. locate seq association in
617        * current dataset/alignment context and add file as normal - keep handle
618        * of any created ref objects. 2. decide if new data should be displayed :
619        * PDB display: if alignment has PDB display already, should new pdb files
620        * be aligned to it ?
621        * 
622        */
623
624     }
625     // destPanel.adjustAnnotationHeight();
626
627   }
628
629   /**
630    * 
631    * @return true if the run method is safe to call
632    */
633   public boolean isValid()
634   {
635     if (jobs != null)
636     {
637       for (RestJob rj : (RestJob[]) jobs)
638       {
639         if (!rj.hasValidInput())
640         {
641           // invalid input for this job
642           System.err.println("Job " + rj.getJobnum()
643                   + " has invalid input.");
644           return false;
645         }
646       }
647       return true;
648     }
649     // TODO Auto-generated method stub
650     return false;
651   }
652
653 }