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