package jalview.ws.rest; import jalview.bin.Cache; import jalview.datamodel.AlignmentI; import jalview.gui.WebserviceInfo; import jalview.io.packed.DataProvider; import jalview.io.packed.JalviewDataset; import jalview.io.packed.ParsePackedSet; import jalview.io.packed.SimpleDataProvider; import jalview.io.packed.DataProvider.JvDataType; import jalview.ws.AWSThread; import jalview.ws.AWsJob; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map.Entry; import org.apache.axis.transport.http.HTTPConstants; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.mime.HttpMultipartMode; import org.apache.http.entity.mime.MultipartEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.protocol.BasicHttpContext; import org.apache.http.protocol.HttpContext; import org.apache.http.util.EntityUtils; public class RestJobThread extends AWSThread { enum Stage { SUBMIT, POLL } protected RestClient restClient;; public RestJobThread(RestClient restClient) { super(); this.restClient = restClient; // may not be needed // Test Code // minimal job - submit given input and parse result onto alignment as // annotation/whatever // look for tree data, etc. // for moment do following job type only // input=visiblealignment,groupsindex // ie one subjob using groups defined on alignment. if (!restClient.service.isHseparable()) { jobs = new RestJob[1]; jobs[0] = new RestJob(0, this, restClient._input.getVisibleAlignment(restClient.service .getGapCharacter())); // need a function to get a range on a view/alignment and return both // annotation, groups and selection subsetted to just that region. } else { AlignmentI[] viscontigals = restClient._input .getVisibleContigAlignments(restClient.service .getGapCharacter()); if (viscontigals != null && viscontigals.length > 0) { jobs = new RestJob[viscontigals.length]; for (int j = 0; j < jobs.length; j++) { if (j != 0) { jobs[j] = new RestJob(j, this, viscontigals[j]); } else { jobs[j] = new RestJob(0, this, viscontigals[j]); } } } } // end Test Code /** * subjob types row based: per sequence in alignment/selected region { input * is one sequence or sequence ID } per alignment/selected region { input is * set of sequences, alignment, one or more sets of sequence IDs, */ if (!restClient.service.isHseparable()) { // create a bunch of subjobs per visible contig to ensure result honours // hidden boundaries // TODO: determine if a 'soft' hSeperable property is needed - e.g. if // user does SS-pred on sequence with big hidden regions, its going to be // less reliable. } else { // create a single subjob for the visible/selected input } // TODO: decide if vSeperable exists: eg. column wide analysis where hidden // rows do not affect output - generally no analysis that generates // alignment annotation is vSeparable - } /** * create gui components for monitoring jobs * * @param webserviceInfo */ public void setWebServiceInfo(WebserviceInfo webserviceInfo) { wsInfo = webserviceInfo; for (int j = 0; j < jobs.length; j++) { wsInfo.addJobPane(); // Copy over any per job params if (jobs.length > 1) { wsInfo.setProgressName("region " + jobs[j].getJobnum(), jobs[j].getJobnum()); } else { wsInfo.setProgressText(jobs[j].getJobnum(), OutputHeader); } } } private String getStage(Stage stg) { if (stg == Stage.SUBMIT) return "submitting "; if (stg == Stage.POLL) return "checking status of "; return (" being confused about "); } private void doPoll(RestJob rj) throws Exception { String postUrl = rj.getPollUrl(); doHttpReq(Stage.POLL, rj, postUrl); } /** * construct the post and handle the response. * * @throws Exception */ public void doPost(RestJob rj) throws Exception { String postUrl = rj.getPostUrl(); doHttpReq(Stage.SUBMIT, rj, postUrl); } /** * do the HTTP request - and respond/set statuses appropriate to the current * stage. * * @param stg * @param rj * - provides any data needed for posting and used to record state * @param postUrl * - actual URL to post/get from * @throws Exception */ protected void doHttpReq(Stage stg, RestJob rj, String postUrl) throws Exception { StringBuffer respText = new StringBuffer(); // con.setContentHandlerFactory(new jalview.io.mime.HttpContentHandler()); HttpRequestBase request = null; String messages = ""; if (stg == Stage.SUBMIT) { // Got this from // http://evgenyg.wordpress.com/2010/05/01/uploading-files-multipart-post-apache/ HttpPost htpost = new HttpPost(postUrl); MultipartEntity postentity = new MultipartEntity( HttpMultipartMode.STRICT); for (Entry input : rj.getInputParams()) { if (input.getValue().validFor(rj)) { postentity.addPart(input.getKey(), input.getValue() .formatForInput(rj)); } else { messages += "Skipped an input (" + input.getKey() + ") - Couldn't generate it from available data."; } } htpost.setEntity(postentity); request = htpost; } else { request = new HttpGet(postUrl); } if (request != null) { DefaultHttpClient httpclient = new DefaultHttpClient(); HttpContext localContext = new BasicHttpContext(); HttpResponse response = null; try { response = httpclient.execute(request); } catch (ClientProtocolException he) { rj.statMessage = "Web Protocol Exception when attempting to " + getStage(stg) + "Job. See Console output for details."; rj.setAllowedServerExceptions(0);// unrecoverable; rj.error = true; Cache.log.fatal("Unexpected REST Job " + getStage(stg) + "exception for URL " + rj.rsd.postUrl); throw (he); } catch (IOException e) { rj.statMessage = "IO Exception when attempting to " + getStage(stg) + "Job. See Console output for details."; Cache.log.warn("IO Exception for REST Job " + getStage(stg) + "exception for URL " + rj.rsd.postUrl); throw (e); } switch (response.getStatusLine().getStatusCode()) { case 200: rj.running = false; Cache.log.debug("Processing result set."); processResultSet(rj, response, request); break; case 202: rj.statMessage = "Job submitted successfully. Results available at this URL:\n" + rj.getJobId() + "\n"; rj.running = true; break; case 302: Header[] loc; if (!rj.isSubmitted() && (loc = response .getHeaders(HTTPConstants.HEADER_LOCATION)) != null && loc.length > 0) { if (loc.length > 1) { Cache.log .warn("Ignoring additional " + (loc.length - 1) + " location(s) provided in response header ( next one is '" + loc[1].getValue() + "' )"); } rj.setJobId(loc[0].getValue()); rj.setSubmitted(true); } completeStatus(rj, response); break; case 500: // Failed. rj.setSubmitted(true); rj.setAllowedServerExceptions(0); rj.setSubjobComplete(true); rj.error = true; completeStatus(rj, response, "" + getStage(stg) + "failed. Reason below:\n"); break; default: // Some other response. Probably need to pop up the content in a window. // TODO: deal with all other HTTP response codes from server. Cache.log.warn("Unhandled response status when " + getStage(stg) + "for " + postUrl + ": " + response.getStatusLine()); try { response.getEntity().consumeContent(); } catch (IOException e) { Cache.log.debug("IOException when consuming unhandled response", e); } ; } } } /** * job has completed. Something valid should be available from con * * @param rj * @param con * @param req * is a stateless request - expected to return the same data * regardless of how many times its called. */ private void processResultSet(RestJob rj, HttpResponse con, HttpRequestBase req) { if (rj.statMessage == null) { rj.statMessage = ""; } rj.statMessage += "Job Complete.\n"; try { rj.resSet = new HttpResultSet(rj, con, req); rj.gotresult = true; } catch (IOException e) { rj.statMessage += "Couldn't parse results. Failed."; rj.error = true; rj.gotresult = false; } } private void completeStatus(RestJob rj, HttpResponse con) throws IOException { completeStatus(rj, con, null); } private void completeStatus(RestJob rj, HttpResponse con, String prefix) throws IOException { StringBuffer sb = new StringBuffer(); if (prefix != null) { sb.append(prefix); } ; if (rj.statMessage != null && rj.statMessage.length() > 0) { sb.append(rj.statMessage); } HttpEntity en = con.getEntity(); /* * Just show the content as a string. */ rj.statMessage = EntityUtils.toString(en); } @Override public void pollJob(AWsJob job) throws Exception { assert (job instanceof RestJob); System.err.println("Debug RestJob: Polling Job"); doPoll((RestJob) job); } @Override public void StartJob(AWsJob job) { assert (job instanceof RestJob); try { System.err.println("Debug RestJob: Posting Job"); doPost((RestJob) job); } catch (Exception ex) { job.setSubjobComplete(true); job.setAllowedServerExceptions(-1); Cache.log.error("Exception when trying to start Rest Job.", ex); } } @Override public void parseResult() { // crazy users will see this message System.err.println("WARNING: Rest job result parser is INCOMPLETE!"); for (RestJob rj : (RestJob[]) jobs) { // TODO: call each jobs processResults() method and collect valid // contexts. if (rj.hasResponse() && rj.resSet != null && rj.resSet.isValid()) { String ln = null; System.out.println("Parsing data for job " + rj.getJobId()); if (!restClient.isAlignmentModified()) { try { /* * while ((ln=rj.resSet.nextLine())!=null) { System.out.println(ln); * } } */ List dp = new ArrayList(); restClient.af.newView_actionPerformed(null); dp.add(new SimpleDataProvider(JvDataType.ANNOTATION, rj.resSet, null)); JalviewDataset context = new JalviewDataset(restClient.av.getAlignment().getDataset(), null, null,restClient.av.getAlignment()); ParsePackedSet pps = new ParsePackedSet(); pps.getAlignment(context, dp); // do an ap.refresh restClient.av.alignmentChanged(Desktop.getAlignmentPanels(restClient.av.getViewId())[0]); System.out.println("Finished parsing data for job " + rj.getJobId()); } catch (Exception ex) { System.out.println("Failed to finish parsing data for job " + rj.getJobId()); ex.printStackTrace(); } } } } /** * decisions based on job result content + state of alignFrame that * originated the job: */ /* * 1. Can/Should this job automatically open a new window for results */ wsInfo.setViewResultsImmediatly(false); /* * 2. Should the job modify the parent alignment frame/view(s) (if they * still exist and the alignment hasn't been edited) in order to display new * annotation/features. */ /** * alignments. New alignments are added to dataset, and subsequently * annotated/visualised accordingly. 1. New alignment frame created for new * alignment. Decide if any vis settings should be inherited from old * alignment frame (e.g. sequence ordering ?). 2. Subsequent data added to * alignment as below: */ /** * annotation update to original/newly created context alignment: 1. * identify alignment where annotation is to be loaded onto. 2. Add * annotation, excluding any duplicates. 3. Ensure annotation is visible on * alignment - honouring ordering given by file. */ /** * features updated to original or newly created context alignment: 1. * Features are(or were already) added to dataset. 2. Feature settings * modified to ensure all features are displayed - honouring any ordering * given by result file. Consider merging action with the code used by the * DAS fetcher to update alignment views with new info. */ /** * Seq associated data files (PDB files). 1. locate seq association in * current dataset/alignment context and add file as normal - keep handle of * any created ref objects. 2. decide if new data should be displayed : PDB * display: if alignment has PDB display already, should new pdb files be * aligned to it ? * */ // TODO: check if at least one or more contexts are valid - if so, enable // gui wsInfo.showResultsNewFrame.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // TODO: call method to show results in new window } }); wsInfo.mergeResults.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // TODO: call method to merge results into existing window } }); wsInfo.setResultsReady(); } /** * * @return true if the run method is safe to call */ public boolean isValid() { if (jobs != null) { for (RestJob rj : (RestJob[]) jobs) { if (!rj.hasValidInput()) { // invalid input for this job System.err.println("Job " + rj.getJobnum() + " has invalid input."); return false; } } return true; } // TODO Auto-generated method stub return false; } }