From c762d9525db36ffd5d3fca49fb5e7d506d13401a Mon Sep 17 00:00:00 2001 From: jprocter Date: Thu, 9 Dec 2010 16:27:59 +0000 Subject: [PATCH] initial implementation of Rest client framework (JAL-715) --- src/jalview/ws/rest/AlignmentProcessor.java | 12 + src/jalview/ws/rest/HttpResultSet.java | 146 ++++ src/jalview/ws/rest/InputType.java | 99 +++ src/jalview/ws/rest/NoValidInputDataException.java | 21 + src/jalview/ws/rest/RestClient.java | 308 +++++++ src/jalview/ws/rest/RestJob.java | 297 +++++++ src/jalview/ws/rest/RestJobThread.java | 511 +++++++++++ src/jalview/ws/rest/RestServiceDescription.java | 911 ++++++++++++++++++++ src/jalview/ws/rest/params/Alignment.java | 61 ++ src/jalview/ws/rest/params/AnnotationFile.java | 52 ++ src/jalview/ws/rest/params/JobConstant.java | 42 + .../ws/rest/params/SeqGroupIndexVector.java | 95 ++ src/jalview/ws/rest/params/SeqIdVector.java | 46 + src/jalview/ws/rest/params/SeqVector.java | 43 + src/jalview/ws/rest/params/Tree.java | 33 + 15 files changed, 2677 insertions(+) create mode 100644 src/jalview/ws/rest/AlignmentProcessor.java create mode 100644 src/jalview/ws/rest/HttpResultSet.java create mode 100644 src/jalview/ws/rest/InputType.java create mode 100644 src/jalview/ws/rest/NoValidInputDataException.java create mode 100644 src/jalview/ws/rest/RestClient.java create mode 100644 src/jalview/ws/rest/RestJob.java create mode 100644 src/jalview/ws/rest/RestJobThread.java create mode 100644 src/jalview/ws/rest/RestServiceDescription.java create mode 100644 src/jalview/ws/rest/params/Alignment.java create mode 100644 src/jalview/ws/rest/params/AnnotationFile.java create mode 100644 src/jalview/ws/rest/params/JobConstant.java create mode 100644 src/jalview/ws/rest/params/SeqGroupIndexVector.java create mode 100644 src/jalview/ws/rest/params/SeqIdVector.java create mode 100644 src/jalview/ws/rest/params/SeqVector.java create mode 100644 src/jalview/ws/rest/params/Tree.java diff --git a/src/jalview/ws/rest/AlignmentProcessor.java b/src/jalview/ws/rest/AlignmentProcessor.java new file mode 100644 index 0000000..cc6486e --- /dev/null +++ b/src/jalview/ws/rest/AlignmentProcessor.java @@ -0,0 +1,12 @@ +package jalview.ws.rest; + +import jalview.datamodel.AlignmentI; + +public interface AlignmentProcessor { + /** + * prepare the context alignment for this input + * @param al - alignment to be processed + * @return al or a new alignment with appropriate attributes/order for input + */ + public AlignmentI prepareAlignment(AlignmentI al); +} \ No newline at end of file diff --git a/src/jalview/ws/rest/HttpResultSet.java b/src/jalview/ws/rest/HttpResultSet.java new file mode 100644 index 0000000..bfef0cc --- /dev/null +++ b/src/jalview/ws/rest/HttpResultSet.java @@ -0,0 +1,146 @@ +package jalview.ws.rest; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.entity.mime.MultipartEntity; +import org.apache.james.mime4j.MimeException; +import org.apache.james.mime4j.parser.ContentHandler; +import org.apache.james.mime4j.parser.MimeStreamParser; + +import jalview.bin.Cache; +import jalview.io.FileParse; +import jalview.io.mime.JalviewMimeContentHandler; + +/** + * data source instantiated from the response of an httpclient request. + * + * @author JimP + * + */ + +public class HttpResultSet extends FileParse +{ + + private HttpRequestBase cachedRequest; + + /** + * when set, indicates that en can be recreated by repeating the HttpRequest + * in cachedRequest + */ + boolean repeatable = false; + + /** + * response that is to be parsed as jalview input data + */ + private HttpEntity en = null; + + /** + * (sub)job that produced this result set. + */ + private RestJob restJob; + + public HttpResultSet(RestJob rj, HttpResponse con, HttpRequestBase req) + throws IOException + { + super(); + setDataName(rj.getJobId() + " Part " + rj.getJobnum()); + restJob = rj; + cachedRequest = req; + initDataSource(con); + } + + private void initDataSource(HttpResponse con) throws IOException + { + en = con.getEntity(); + repeatable = en.isRepeatable(); + + if (en instanceof MultipartEntity) + { + MultipartEntity mpe = (MultipartEntity) en; + // multipart + jalview.io.packed.JalviewDataset ds = restJob.newJalviewDataset(); + ContentHandler handler = new JalviewMimeContentHandler(ds); + MimeStreamParser parser = new MimeStreamParser(); + parser.setContentHandler(handler); + try + { + parser.parse(mpe.getContent()); + } catch (MimeException me) + { + error = true; + errormessage = "Couldn't parse message from web service."; + Cache.log.warn("Failed to parse MIME multipart content", me); + en.consumeContent(); + } + } + else + { + // assume content is simple text stream that can be read from + String enc = (en.getContentEncoding()==null) ? null : en.getContentEncoding().getValue(); + if (en.getContentType()!=null) { + Cache.log.info("Result Type: " + en.getContentType().toString()); + } else { + Cache.log.info("No Result Type Specified."); + } + if (enc == null || enc.length() < 1) + { + Cache.log.debug("Assuming 'Default' Result Encoding."); + } + else + { + Cache.log.debug("Result Encoded as : "+enc); + } + // attempt to identify file and construct an appropriate DataSource + // identifier for it. + // try to parse + // Mime-Multipart or single content type will be expected. + // if (enc.equals(org.apache.http.client.utils.))) + InputStreamReader br = null; + try + { + br = (enc != null) ? new InputStreamReader(en.getContent(), enc) + : new InputStreamReader(en.getContent()); + } catch (UnsupportedEncodingException e) + { + Cache.log.error("Can't handle encoding '" + enc + + "' for response from webservice.", e); + en.consumeContent(); + error = true; + errormessage = "Can't handle encoding for response from webservice"; + return; + } + if (br != null) + { + dataIn = new BufferedReader(br); + error=false; + } + } + } + + @Override + protected void finalize() throws Throwable + { + dataIn = null; + cachedRequest = null; + try + { + if (en != null) + { + en.consumeContent(); + } + } catch (Exception e) + { + } catch (Error ex) + { + } + super.finalize(); + } + +} diff --git a/src/jalview/ws/rest/InputType.java b/src/jalview/ws/rest/InputType.java new file mode 100644 index 0000000..77583b7 --- /dev/null +++ b/src/jalview/ws/rest/InputType.java @@ -0,0 +1,99 @@ +package jalview.ws.rest; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.ArrayList; + +import org.apache.http.entity.mime.content.ContentBody; +import org.apache.http.entity.mime.content.StringBody; + +import sun.io.CharacterEncoding; +import sun.misc.CharacterEncoder; + +/** Service Input info { a sequence of [ Sequence Id vector (min,max,moltype, separator,regex,colrange(start-end)), Sequence(format-bare or alignment, moltype, min, max, separator)), Alignment(format, moltype), + */ +public abstract class InputType { + /** + * not used yet + */ + boolean replaceids; + public enum molType { NUC, PROT, MIX} + public String token; + public int min=1; + public int max=0; // unbounded + protected ArrayList inputData=new ArrayList(); + /** + * initialise the InputType with a list of jalview data classes that the RestJob needs to be able to provide to it. + * @param types + */ + protected InputType(Class[] types) + { + if(types!=null) + {for (Class t:types) + { + inputData.add(t); + } + } + } + /** + * do basic tests to ensure the job's service takes this parameter, and the job's input data can be used to generate the input data + * @param restJob + * @return + */ + public boolean validFor(RestJob restJob) + { + if (!validFor(restJob.rsd)) + return false; + for (Class cl:inputData) + { + if (!restJob.hasDataOfType(cl)) + { + return false; + } + } + return true; + } + + public boolean validFor(RestServiceDescription restServiceDescription) + { + if (!restServiceDescription.inputParams.values().contains(this)) + return false; + + return true; + } + protected ContentBody utf8StringBody(String content, String type) + { + Charset utf8 = Charset.forName("UTF-8"); + try { + if (type==null ) { + return new StringBody(utf8.encode(content).asCharBuffer().toString()); + } else { + return new StringBody(utf8.encode(content).asCharBuffer().toString(), type, utf8); + } + } catch (Exception ex) + { + System.err.println("Couldn't transform string\n"+content+"\nException was :"); + ex.printStackTrace(System.err); + } + return null; + } + /** + * + * @param rj data from which input is to be extracted and formatted + * @return StringBody or FileBody ready for posting + */ + abstract public ContentBody formatForInput(RestJob rj) throws UnsupportedEncodingException,NoValidInputDataException; + /** + * + * @return true if no input data needs to be provided for this parameter + */ + public boolean isConstant() + { + return (inputData==null || inputData.size()==0); + } +} \ No newline at end of file diff --git a/src/jalview/ws/rest/NoValidInputDataException.java b/src/jalview/ws/rest/NoValidInputDataException.java new file mode 100644 index 0000000..7accad1 --- /dev/null +++ b/src/jalview/ws/rest/NoValidInputDataException.java @@ -0,0 +1,21 @@ +package jalview.ws.rest; + +/** + * exception thrown if there is no data available to construct a valid input for a particular validInput.InputType and RestJob + * @author JimP + * + */ +public class NoValidInputDataException extends Exception +{ + + public NoValidInputDataException(String string) + { + super(string); + } + + public NoValidInputDataException(String string, Exception ex) + { + super(string,ex); + } + +} diff --git a/src/jalview/ws/rest/RestClient.java b/src/jalview/ws/rest/RestClient.java new file mode 100644 index 0000000..5a4cf61 --- /dev/null +++ b/src/jalview/ws/rest/RestClient.java @@ -0,0 +1,308 @@ +/** + * + */ +package jalview.ws.rest; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Hashtable; +import java.util.Vector; + +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.event.MenuEvent; +import javax.swing.event.MenuListener; + +import com.sun.org.apache.bcel.internal.generic.ISHL; + +import jalview.datamodel.AlignmentView; +import jalview.datamodel.SequenceGroup; +import jalview.gui.AlignFrame; +import jalview.gui.AlignViewport; +import jalview.gui.Desktop; +import jalview.gui.WebserviceInfo; +import jalview.ws.WSClient; +import jalview.ws.WSClientI; +import jalview.ws.WSMenuEntryProviderI; + +/** + * @author JimP + * + */ +public class RestClient extends WSClient implements WSClientI, + WSMenuEntryProviderI +{ + RestServiceDescription service; + + public RestClient(RestServiceDescription rsd) + { + service = rsd; + } + + /** + * parent alignframe for this job + */ + AlignFrame af; + + /** + * alignment view which provides data for job. + */ + AlignViewport av; + + /** + * get the alignFrame for the associated input data if it exists. + * + * @return + */ + protected AlignFrame recoverAlignFrameForView() + { + return jalview.gui.Desktop.getAlignFrameFor(av); + } + + public RestClient(RestServiceDescription service2, AlignFrame alignFrame) + { + service = service2; + af = alignFrame; + av = alignFrame.getViewport(); + constructJob(); + } + + public void setWebserviceInfo(boolean headless) + { + WebServiceJobTitle = service.details.Action + " using " + + service.details.Name; + WebServiceName = service.details.Name; + WebServiceReference = "No reference - go to url for more info"; + if (service.details.description != null) + { + WebServiceReference = service.details.description; + } + if (!headless) + { + wsInfo = new WebserviceInfo(WebServiceJobTitle, WebServiceName + "\n" + + WebServiceReference); + } + + } + + @Override + public boolean isCancellable() + { + // TODO define process for cancelling rsbws jobs + return false; + } + + @Override + public boolean canMergeResults() + { + // TODO process service definition to identify if the results might be + // mergeable + // TODO: change comparison for annotation merge + return false; + } + + @Override + public void cancelJob() + { + System.err.println("Cannot cancel this job type: " + service); + } + + @Override + public void attachWSMenuEntry(final JMenu wsmenu, + final AlignFrame alignFrame) + { + JMenuItem submit = new JMenuItem(service.details.Name); + submit.setToolTipText(service.details.Action+" using "+service.details.Name); + submit.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + new RestClient(service, alignFrame); + } + + }); + wsmenu.add(submit); + // TODO: menu listener should enable/disable entry depending upon selection + // state of the alignment + wsmenu.addMenuListener(new MenuListener() + { + + @Override + public void menuSelected(MenuEvent e) + { + // TODO Auto-generated method stub + + } + + @Override + public void menuDeselected(MenuEvent e) + { + // TODO Auto-generated method stub + + } + + @Override + public void menuCanceled(MenuEvent e) + { + // TODO Auto-generated method stub + + } + + }); + + } + + /** + * record of initial undoredo hash for the alignFrame providing data for this + * job. + */ + long[] undoredo = null; + + /** + * Compare the original input data to the data currently presented to the + * user. // LOGIC: compare undo/redo - if same, merge regardless (coping with + * any changes in hidden columns as normal) // if different undo/redo then + * compare region that was submitted // if same, then merge as before, if + * different then prompt user to open a new window. + * + * @return + */ + protected boolean isAlignmentModified() + { + if (undoredo == null) + { + return true; + } + if (av.isUndoRedoHashModified(undoredo)) + { + + } + return false; + + } + + /** + * TODO: combine to form a dataset+alignment+annotation context + */ + AlignmentView _input; + + /** + * input data context + */ + jalview.io.packed.JalviewDataset jds; + + protected void constructJob() + { + service.setInvolvesFlags(); + + // record all aspects of alignment view so we can merge back or recreate + // later + undoredo = av.getUndoRedoHash(); + /** + * delete ? Vector sgs = av.getAlignment().getGroups(); if (sgs!=null) { + * _sgs = new SequenceGroup[sgs.size()]; sgs.copyInto(_sgs); } else { _sgs = + * new SequenceGroup[0]; } + */ + boolean selExists = (av.getSelectionGroup() != null) + && (av.getSelectionGroup().getSize() > 1); + // TODO: revise to full focus+context+dataset input data staging model + if (selExists) + { + if (service.partitiondata) + { + if (av.getAlignment().getGroups()!=null && av.getAlignment().getGroups().size() > 0) + { + // intersect groups with selected region + _input = new AlignmentView(av.getAlignment(), + av.getColumnSelection(), + av.getSelectionGroup(), + av.hasHiddenColumns(), + true, + true); + } + else + { + // use selected region to partition alignment + _input = new AlignmentView(av.getAlignment(), + av.getColumnSelection(), + av.getSelectionGroup(), + av.hasHiddenColumns(), + false, + true); + } + // TODO: verify that some kind of partition can be constructed from input + } + else + { + // just take selected region intersection + _input = new AlignmentView(av.getAlignment(), + av.getColumnSelection(), + av.getSelectionGroup(), + av.hasHiddenColumns(), + true, + true); + } + } else { + // standard alignment view without selection present + _input = new AlignmentView(av.getAlignment(), + av.getColumnSelection(), + null, + av.hasHiddenColumns(), + false, + true); + } + + RestJobThread jobsthread = new RestJobThread(this); + + if (jobsthread.isValid()) + { + setWebserviceInfo(false); + wsInfo.setthisService(this); + jobsthread.setWebServiceInfo(wsInfo); + jobsthread.start(); + } + else + { + JOptionPane.showMessageDialog(Desktop.desktop, + "Unable to start web service analysis", + "Internal Jalview Error", JOptionPane.WARNING_MESSAGE); + } + } + + public static RestClient makeShmmrRestClient() + { + String action = "Analyse", description = "Sequence Harmony and Multi-Relief", name = "SHMR"; + Hashtable iparams = new Hashtable(); + jalview.ws.rest.params.JobConstant toolp; + //toolp = new jalview.ws.rest.JobConstant("tool","jalview"); + //iparams.put(toolp.token, toolp); + toolp = new jalview.ws.rest.params.JobConstant("mbjob[method]","shmr"); + iparams.put(toolp.token, toolp); + toolp = new jalview.ws.rest.params.JobConstant("mbjob[description]","step 1"); + iparams.put(toolp.token, toolp); + toolp = new jalview.ws.rest.params.JobConstant("start_search","1"); + iparams.put(toolp.token, toolp); + toolp = new jalview.ws.rest.params.JobConstant("blast","0"); + iparams.put(toolp.token, toolp); + + jalview.ws.rest.params.Alignment aliinput = new jalview.ws.rest.params.Alignment(); + aliinput.token = "ali_file"; + aliinput.writeAsFile=true; + iparams.put("ali_file", aliinput); + jalview.ws.rest.params.SeqGroupIndexVector sgroups = new jalview.ws.rest.params.SeqGroupIndexVector(); + iparams.put("groups", sgroups); + sgroups.token = "groups"; + sgroups.sep = " "; + RestServiceDescription shmrService = new RestServiceDescription( + action, + description, + name, + "http://www.ibi.vu.nl/programs/shmrwww/index.php?tool=jalview",// ?tool=jalview&mbjob[method]=shmr&mbjob[description]=step1", + "?tool=jalview", iparams, true, false, '-'); + return new RestClient(shmrService); + } + +} diff --git a/src/jalview/ws/rest/RestJob.java b/src/jalview/ws/rest/RestJob.java new file mode 100644 index 0000000..c7df482 --- /dev/null +++ b/src/jalview/ws/rest/RestJob.java @@ -0,0 +1,297 @@ +package jalview.ws.rest; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Hashtable; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.Vector; + +import jalview.datamodel.AlignmentI; +import jalview.datamodel.AlignmentView; +import jalview.datamodel.SequenceGroup; +import jalview.datamodel.SequenceI; +import jalview.io.packed.JalviewDataset; +import jalview.ws.AWsJob; +import jalview.ws.rest.params.Alignment; +import jalview.ws.rest.params.SeqGroupIndexVector; + +public class RestJob extends AWsJob +{ + + // TODO: input alignmentview and other data for this job + RestServiceDescription rsd; + // boolean submitted; + boolean gotresponse; + boolean error; + boolean waiting; + boolean gotresult; + Hashtable squniq; + + /** + * create a rest job using data bounded by the given start/end column. + * @param addJobPane + * @param restJobThread + * @param _input + */ + public RestJob(int jobNum, RestJobThread restJobThread, + AlignmentI _input) + { + rsd = restJobThread.restClient.service; + jobnum = jobNum; + // get sequences for the alignmentI + // get groups trimmed to alignment columns + // get any annotation trimmed to start/end columns, too. + + // prepare input + // form alignment+groups+annotation,preprocess and then record references for formatters + ArrayList alinp = new ArrayList(); + int paramsWithData=0; + // we cheat for moment - since we know a-priori what data is available and what inputs we have implemented so far + for (Map.Entryprm: rsd.inputParams.entrySet()) + { + if (!prm.getValue().isConstant()) + { + if (prm.getValue() instanceof Alignment) + { + alinp.add(prm.getValue()); + } else + { + if ((prm.getValue() instanceof SeqGroupIndexVector) + &&(_input.getGroups()!=null && _input.getGroups().size()>0)) + { + alinp.add(prm.getValue()); + } + } + } else { + paramsWithData++; + } + } + if ((paramsWithData+alinp.size())==rsd.inputParams.size()) + { + setAlignmentForInputs(alinp, _input); + validInput=true; + } else { + // not enough data, so we bail. + validInput=false; + } + } + boolean validInput=false; + @Override + public boolean hasResults() + { + return gotresult; + } + + @Override + public boolean hasValidInput() + { + return validInput; + } + + @Override + public boolean isRunning() + { + return running; // TODO: can we check the response body for status messages ? + } + + @Override + public boolean isQueued() + { + return waiting; + } + + @Override + public boolean isFinished() + { + return resSet!=null; + } + + @Override + public boolean isFailed() + { + // TODO logic for error + return error; + } + + @Override + public boolean isBroken() + { + // TODO logic for error + return error; + } + + @Override + public boolean isServerError() + { + // TODO logic for error + return error; + } + + @Override + public boolean hasStatus() + { + return statMessage != null; + } + + protected String statMessage = null; + public HttpResultSet resSet; + + @Override + public String getStatus() + { + return statMessage; + } + + @Override + public boolean hasResponse() + { + return statMessage!=null || resSet!=null; + } + + @Override + public void clearResponse() + { + // only clear the transient server response + // statMessage=null; + } + + /* (non-Javadoc) + * @see jalview.ws.AWsJob#getState() + */ + @Override + public String getState() + { + // TODO generate state string - prolly should have a default abstract method for this + return "Job is clueless"; + } + + public String getPostUrl() + { + + // TODO Auto-generated method stub + return rsd.postUrl; + } + + public Set> getInputParams() + { + return rsd.inputParams.entrySet(); + } + + // return the URL that should be polled for this job + public String getPollUrl() + { + return rsd.getDecoratedResultUrl(jobId); + } + + /** + * + * @return new context for parsing results from service + */ + public JalviewDataset newJalviewDataset() + { + /* + * TODO: initialise dataset with correct input context + */ + JalviewDataset njd = new JalviewDataset(); + return njd; + } + + /** + * Extract list of sequence IDs for input parameter 'token' with given molecule type + * @param token + * @param type + * @return + */ + public SequenceI[] getSequencesForInput(String token, InputType.molType type) throws NoValidInputDataException + { + Object sgdat = inputData.get(token); + // can we form an alignment from this data ? + if (sgdat==null) + { + throw new NoValidInputDataException("No Sequence vector data bound to input '"+token+"' for service at "+rsd.postUrl); + } + if (sgdat instanceof AlignmentI) + { + return ((AlignmentI) sgdat).getSequencesArray(); + } + if (sgdat instanceof SequenceGroup) + { + return ((SequenceGroup)sgdat).getSequencesAsArray(null); + } + if (sgdat instanceof Vector) + { + if (((Vector)sgdat).size()>0 && ((Vector)sgdat).get(0) instanceof SequenceI) + { + SequenceI[] sq = new SequenceI[((Vector)sgdat).size()]; + ((Vector)sgdat).copyInto(sq); + return sq; + } + } + throw new NoValidInputDataException("No Sequence vector data bound to input '"+token+"' for service at "+rsd.postUrl); + } + /** + * binding between input data (AlignmentI, SequenceGroup, NJTree) and input param names. + */ + private Hashtable inputData=new Hashtable(); + /** + * is the job fully submitted to server and apparently in progress ? + */ + public boolean running=false; + /** + * + * @param itypes + * @param al - reference to object to be stored as input. Note - input data may be modifed by formatter + */ + public void setAlignmentForInputs(Collection itypes, AlignmentI al) + { + for (InputType itype: itypes) { + if (!rsd.inputParams.values().contains(itype)) + { + throw new IllegalArgumentException("InputType "+itype.getClass()+ + " is not valid for service at "+rsd.postUrl); + } + if (itype instanceof AlignmentProcessor) + { + ((AlignmentProcessor)itype).prepareAlignment(al); + } + // stash a reference for recall when the alignment data is formatted + inputData.put(itype.token, al); + } + + } + /** + * + * @param token + * @param type + * @return alignment object bound to the given token + * @throws NoValidInputDataException + */ + public AlignmentI getAlignmentForInput(String token, InputType.molType type) throws NoValidInputDataException + { + Object al = inputData.get(token); + // can we form an alignment from this data ? + if (al==null || !(al instanceof AlignmentI)) + { + throw new NoValidInputDataException("No alignment data bound to input '"+token+"' for service at "+rsd.postUrl); + } + return (AlignmentI) al; + } + + /** + * test to see if the job has data of type cl that's needed for the job to run + * @param cl + * @return true or false + */ + public boolean hasDataOfType(Class cl) + { + if (AlignmentI.class.isAssignableFrom(cl)) { + return true; + } + // TODO: add more source data types + + return false; + } + +} diff --git a/src/jalview/ws/rest/RestJobThread.java b/src/jalview/ws/rest/RestJobThread.java new file mode 100644 index 0000000..ba3d6b5 --- /dev/null +++ b/src/jalview/ws/rest/RestJobThread.java @@ -0,0 +1,511 @@ +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; + } + +} diff --git a/src/jalview/ws/rest/RestServiceDescription.java b/src/jalview/ws/rest/RestServiceDescription.java new file mode 100644 index 0000000..6794a8d --- /dev/null +++ b/src/jalview/ws/rest/RestServiceDescription.java @@ -0,0 +1,911 @@ +/* + * Jalview - A Sequence Alignment Editor and Viewer (Version 2.6) + * Copyright (C) 2010 J Procter, AM Waterhouse, G Barton, M Clamp, S Searle + * + * This file is part of Jalview. + * + * Jalview is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * Jalview is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Jalview. If not, see . + */ +package jalview.ws.rest; + + +import jalview.datamodel.SequenceI; +import jalview.util.GroupUrlLink.UrlStringTooLongException; +import jalview.util.Platform; +import jalview.ws.rest.params.Alignment; +import jalview.ws.rest.params.AnnotationFile; +import jalview.ws.rest.params.SeqGroupIndexVector; + +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; + + +public class RestServiceDescription +{ + /** + * @param details + * @param postUrl + * @param urlSuffix + * @param inputParams + * @param hseparable + * @param vseparable + * @param gapCharacter + */ + public RestServiceDescription(String action,String description,String name, String postUrl, + String urlSuffix, Map inputParams, + boolean hseparable, boolean vseparable, char gapCharacter) + { + super(); + this.details = new UIinfo(); + details.Action= action; + details.description = description; + details.Name = name; + this.postUrl = postUrl; + this.urlSuffix = urlSuffix; + this.inputParams = inputParams; + this.hseparable = hseparable; + this.vseparable = vseparable; + this.gapCharacter = gapCharacter; + } + /** + * Service UI Info { Action, Specific Name of Service, Brief Description } + */ + + public class UIinfo { + String Action; + String Name; + String description; + } + UIinfo details = new UIinfo(); + + /** Service base URL + */ + String postUrl; + /** + * suffix that should be added to any url used if it does not already end in the suffix. + */ + String urlSuffix; + + /*** + * modelling the io: + * validation of input + * { formatter for type, parser for type } + * + */ + /** input info given as key/value pairs - mapped to post arguments + */ + Map inputParams=new HashMap(); + /** + * service requests alignment data + */ + boolean aligndata; + /** + * service requests alignment and/or seuqence annotationo data + */ + boolean annotdata; + /** + * service requests partitions defined over input (alignment) data + */ + boolean partitiondata; + + /** + * process ths input data and set the appropriate shorthand flags describing the input the service wants + */ + public void setInvolvesFlags() { + aligndata = inputInvolves(Alignment.class); + annotdata = inputInvolves(AnnotationFile.class); + partitiondata = inputInvolves(SeqGroupIndexVector.class); + } + + /** Service return info { alignment, annotation file (loaded back on to alignment), tree (loaded back on to alignment), sequence annotation - loaded back on to alignment), text report, pdb structures with sequence mapping ) + * + */ + + /** Start with bare minimum: input is alignment + groups on alignment + * + * @author JimP + * + */ + + /** + * Helper class based on the UrlLink class which enables URLs to be + * constructed from sequences or IDs associated with a group of sequences. URL + * definitions consist of a pipe separated string containing a