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