From: gmungoc Date: Tue, 5 May 2015 15:27:16 +0000 (+0100) Subject: JAL-1725 first version of Jetty server / chimera selection listener X-Git-Tag: Jalview_2_9~35 X-Git-Url: http://source.jalview.org/gitweb/?p=jalview.git;a=commitdiff_plain;h=f2f654875a6eb5f4e90b2c5c545bfed3a70f2b5f JAL-1725 first version of Jetty server / chimera selection listener --- diff --git a/lib/jetty-http-9.2.10.v20150310.jar b/lib/jetty-http-9.2.10.v20150310.jar new file mode 100644 index 0000000..15aff51 Binary files /dev/null and b/lib/jetty-http-9.2.10.v20150310.jar differ diff --git a/lib/jetty-io-9.2.10.v20150310.jar b/lib/jetty-io-9.2.10.v20150310.jar new file mode 100644 index 0000000..56cee2c Binary files /dev/null and b/lib/jetty-io-9.2.10.v20150310.jar differ diff --git a/lib/jetty-server-9.2.10.v20150310.jar b/lib/jetty-server-9.2.10.v20150310.jar new file mode 100644 index 0000000..815cb08 Binary files /dev/null and b/lib/jetty-server-9.2.10.v20150310.jar differ diff --git a/lib/jetty-util-9.2.10.v20150310.jar b/lib/jetty-util-9.2.10.v20150310.jar new file mode 100644 index 0000000..fe27758 Binary files /dev/null and b/lib/jetty-util-9.2.10.v20150310.jar differ diff --git a/lib/servlet-api-3.1.jar b/lib/servlet-api-3.1.jar new file mode 100644 index 0000000..6b14c3d Binary files /dev/null and b/lib/servlet-api-3.1.jar differ diff --git a/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java b/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java index b74e65a..3ed27a3 100644 --- a/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java +++ b/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java @@ -1,7 +1,5 @@ package ext.edu.ucsf.rbvi.strucviz2; -import jalview.ws.HttpClientUtils; - import java.awt.Color; import java.io.BufferedReader; import java.io.File; @@ -22,6 +20,8 @@ import org.slf4j.LoggerFactory; import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType; import ext.edu.ucsf.rbvi.strucviz2.port.ListenerThreads; +import jalview.ws.HttpClientUtils; + /** * This object maintains the Chimera communication information. */ @@ -177,7 +177,7 @@ public class ChimeraManager ModelType type) { logger.info("chimera open " + modelPath); - stopListening(); + // stopListening(); List response = null; // TODO: [Optional] Handle modbase models if (type == ModelType.MODBASE_MODEL) @@ -281,7 +281,7 @@ public class ChimeraManager } sendChimeraCommand("focus", false); - startListening(); + // startListening(); // see ChimeraListener return models; } @@ -339,12 +339,35 @@ public class ChimeraManager public void startListening() { - sendChimeraCommand("listen start models; listen start select", false); + sendChimeraCommand("listen start models; listen start selection", false); } public void stopListening() { - sendChimeraCommand("listen stop models; listen stop select", false); + sendChimeraCommand("listen stop models; listen stop selection", false); + } + + /** + * Tell Chimera we are listening on the given URI + * + * @param uri + */ + public void startListening(String uri) + { + sendChimeraCommand("listen start models url " + uri, false); + sendChimeraCommand("listen start select prefix SelectionChanged url " + + uri, false); + } + + /** + * Tell Chimera we have stopped listening on the given URI + * + * @param uri + */ + public void stopListening(String uri) + { + sendChimeraCommand("listen stop models url " + uri, false); + sendChimeraCommand("listen stop selection url " + uri, false); } /** @@ -355,8 +378,8 @@ public class ChimeraManager */ public void select(String command) { - sendChimeraCommand("listen stop select; " + command - + "; listen start select", false); + sendChimeraCommand("listen stop selection; " + command + + "; listen start selection", false); } public void focus() @@ -407,6 +430,12 @@ public class ChimeraManager return selectedModelsMap; } + /** + * Sends a 'list selection level residue' command to Chimera and returns the + * list of selected atomspecs + * + * @return + */ public List getSelectedResidueSpecs() { List selectedResidues = new ArrayList(); @@ -561,7 +590,7 @@ public class ChimeraManager structureManager.setChimeraPathProperty(workingPath); // TODO: [Optional] Check Chimera version and show a warning if below 1.8 // Ask Chimera to give us updates - startListening(); + // startListening(); // later - see ChimeraListener return true; } @@ -775,6 +804,7 @@ public class ChimeraManager */ protected List sendRestCommand(String command) { + // System.out.println("Rest: " + command); // TODO start a separate thread to do this so we don't block? String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run"; List commands = new ArrayList(1); @@ -850,5 +880,4 @@ public class ChimeraManager { return busy; } - } diff --git a/src/jalview/ext/rbvi/chimera/ChimeraListener.java b/src/jalview/ext/rbvi/chimera/ChimeraListener.java new file mode 100644 index 0000000..8b48cd2 --- /dev/null +++ b/src/jalview/ext/rbvi/chimera/ChimeraListener.java @@ -0,0 +1,123 @@ +package jalview.ext.rbvi.chimera; + +import java.net.BindException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import jalview.httpserver.AbstractRequestHandler; +import jalview.httpserver.HttpServer; +import jalview.structure.SelectionSource; + +/** + * This is a simple Http handler that can listen for selections in Chimera. + *

+ * Lifecycle: + *

    + *
  • Start the Chimera process
  • + *
  • Start the REST service on Chimera, get the port number it is listening on + *
  • + *
  • Start the ChimeraListener, get the port number it is listening on
  • + *
  • Send a 'listen' command to Chimera with the URL of the listener
  • + *
+ * + * @author gmcarstairs + * + */ +public class ChimeraListener extends AbstractRequestHandler implements + SelectionSource +{ + /* + * A static counter so each listener can be associated with a distinct context + * root (/chimera0,1,2,3...). This is needed so we can fetch selections from + * multiple Chimera instances without confusion. + */ + private static int chimeraId = 0; + + /* + * Path below context root that identifies this handler + */ + private static final String LISTENER_PATH = "chimera"; + + /* + * Value of chimeraId (0, 1, 2...) for this instance + */ + private int myChimeraId = 0; + + /* + * A reference to the object by which we can talk to Chimera + */ + private JalviewChimeraBinding chimeraBinding; + + /* + * The URI of this listener + */ + private String uri; + + /** + * Constructor that also registers this as an Http request handler on path + * /chimeraN, where N is incremented for each instance. Call getUri to get the + * resulting URI for this handler. + * + * @param chimeraBinding + * @throws BindException + * if no free port can be assigned + */ + public ChimeraListener(JalviewChimeraBinding binding) + throws BindException + { + myChimeraId = chimeraId++; + this.chimeraBinding = binding; + final String path = LISTENER_PATH + myChimeraId; + this.uri = HttpServer.getInstance().registerHandler(path, this); + } + + /** + * Returns the URI on which we are listening + * + * @return + */ + public String getUri() + { + return this.uri; + } + + /** + * Process a message from Chimera + */ + @Override + protected void processRequest(HttpServletRequest request, + HttpServletResponse response) + { + // dumpRequest(request); + String message = request.getParameter("chimeraNotification"); + if ("selection changed".equals(message)) + { + this.chimeraBinding.highlightChimeraSelection(); + } + else + { + System.err.println("Unexpected chimeraNotification: " + message); + // do it anyway for now! + this.chimeraBinding.highlightChimeraSelection(); + } + } + + /** + * Deregister this listener and close it down + * + * @throws Exception + */ + public void shutdown() + { + try + { + HttpServer.getInstance().removeHandler(this); + stop(); + } catch (Exception e) + { + System.err.println("Error stopping chimera listener: " + + e.getMessage()); + } + } +} diff --git a/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java b/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java index 9d1ed43..1434c76 100644 --- a/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java +++ b/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java @@ -20,6 +20,19 @@ */ package jalview.ext.rbvi.chimera; +import java.awt.Color; +import java.net.BindException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import ext.edu.ucsf.rbvi.strucviz2.ChimeraManager; +import ext.edu.ucsf.rbvi.strucviz2.ChimeraModel; +import ext.edu.ucsf.rbvi.strucviz2.StructureManager; +import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType; + import jalview.api.AlignmentViewPanel; import jalview.api.FeatureRenderer; import jalview.api.SequenceRenderer; @@ -30,6 +43,7 @@ import jalview.datamodel.PDBEntry; import jalview.datamodel.SequenceI; import jalview.schemes.ColourSchemeI; import jalview.schemes.ResidueProperties; +import jalview.structure.AtomSpec; import jalview.structure.StructureMapping; import jalview.structure.StructureMappingcommandSet; import jalview.structure.StructureSelectionManager; @@ -37,18 +51,6 @@ import jalview.structures.models.AAStructureBindingModel; import jalview.util.Comparison; import jalview.util.MessageManager; -import java.awt.Color; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import ext.edu.ucsf.rbvi.strucviz2.ChimeraManager; -import ext.edu.ucsf.rbvi.strucviz2.ChimeraModel; -import ext.edu.ucsf.rbvi.strucviz2.StructureManager; -import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType; - public abstract class JalviewChimeraBinding extends AAStructureBindingModel { @@ -60,9 +62,17 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel private StructureManager csm; + /* + * Object through which we talk to Chimera + */ private ChimeraManager viewer; /* + * Object which listens to Chimera notifications + */ + private ChimeraListener chimeraListener; + + /* * set if chimera state is being restored from some source - instructs binding * not to apply default display style when structure set is updated for first * time. @@ -101,8 +111,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel private String lastCommand; - private String lastMessage; - private boolean loadedInline; /** @@ -114,6 +122,13 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel private List lastReply; + /* + * incremented every time a load notification is successfully handled - + * lightweight mechanism for other threads to detect when they can start + * referring to new structures. + */ + private long loadNotifiesHandled = 0; + /** * Open a PDB structure file in Chimera and set up mappings from Jalview. * @@ -146,13 +161,20 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel /* * If Chimera doesn't yet have this model, ask it to open it, and retrieve - * the model names added by Chimera. + * the model name(s) added by Chimera. */ if (!alreadyOpen) { viewer.openModel(file, pe.getId(), ModelType.PDB_MODEL); - modelsToMap = viewer.getModelList(); - modelsToMap.removeAll(oldList); + List newList = viewer.getModelList(); + // JAL-1728 newList.removeAll(oldList) does not work + for (ChimeraModel cm : newList) + { + if (cm.getModelName().equals(pe.getId())) + { + modelsToMap.add(cm); + } + } } chimeraMaps.put(file, modelsToMap); @@ -197,6 +219,23 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel } /** + * Start a dedicated HttpServer to listen for Chimera notifications, and tell + * it to start listening + */ + public void startChimeraListener() + { + try + { + chimeraListener = new ChimeraListener(this); + viewer.startListening(chimeraListener.getUri()); + } catch (BindException e) + { + System.err.println("Failed to start Chimera listener: " + + e.getMessage()); + } + } + + /** * Constructor * * @param ssm @@ -255,8 +294,8 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel } /** - * Close down the Jalview viewer, and (optionally) the associated Chimera - * window. + * Close down the Jalview viewer and listener, and (optionally) the associated + * Chimera window. */ public void closeViewer(boolean closeChimera) { @@ -265,8 +304,14 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel { viewer.exitChimera(); } + if (this.chimeraListener != null) + { + chimeraListener.shutdown(); + chimeraListener = null; + } lastCommand = null; viewer = null; + releaseUIResources(); } @@ -843,8 +888,8 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel * *
    * Done by generating a command like (to 'highlight' position 44)
-   *   ~select #0:43.C;select #0:44.C
-   * Note this removes the selection from the previous position.
+   *   ~show #0:43.C;show #0:44.C
+   * Note this removes the highlight from the previous position.
    * 
*/ public void highlightAtom(int atomIndex, int pdbResNum, String chain, @@ -872,13 +917,30 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel String cmd = command.toString(); if (cmd.length() > 0) { + viewer.stopListening(chimeraListener.getUri()); viewer.sendChimeraCommand(cmd, false); + viewer.startListening(chimeraListener.getUri()); } viewerCommandHistory(true); this.lastMousedOverAtomSpec = atomSpec; } } + /** + * Query Chimera for its current selection, and highlight it on the alignment + */ + public void highlightChimeraSelection() + { + List selection = viewer.getSelectedResidueSpecs(); + // System.out.println("Chimera selection: " + selection.toString()); + + // TODO handle more than one!! + if (selection.size() > 0) + { + highlightChimeraSelection(selection); + } + } + private void log(String message) { System.err.println("## Chimera log: " + message); @@ -890,242 +952,65 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel // + ((debug || enable) ? "on" : "off")); } - public void loadInline(String string) - { - loadedInline = true; - // TODO: re JAL-623 - // viewer.loadInline(strModel, isAppend); - // could do this: - // construct fake fullPathName and fileName so we can identify the file - // later. - // Then, construct pass a reader for the string to Jmol. - // ((org.jmol.Viewer.Viewer) viewer).loadModelFromFile(fullPathName, - // fileName, null, reader, false, null, null, 0); - // viewer.openStringInline(string); - log("cannot load inline in Chimera, yet"); - } - - public void mouseOverStructure(int atomIndex, String strInfo) + /** + * Propagate atom selections from Chimera + * + * @param atoms + * for example "#0:70.A" (model/residue/chain) + */ + public void highlightChimeraSelection(List atoms) { - // function to parse a mouseOver event from Chimera - // - int pdbResNum; - int alocsep = strInfo.indexOf("^"); - int mdlSep = strInfo.indexOf("/"); - int chainSeparator = strInfo.indexOf(":"), chainSeparator1 = -1; - - if (chainSeparator == -1) + List atomSpecs = new ArrayList(); + for (String atomSpec : atoms) { - chainSeparator = strInfo.indexOf("."); - if (mdlSep > -1 && mdlSep < chainSeparator) + int hashPos = atomSpec.indexOf("#"); + int colonPos = atomSpec.indexOf(":"); + int dotPos = atomSpec.indexOf("."); + + if (colonPos == -1) { - chainSeparator1 = chainSeparator; - chainSeparator = mdlSep; + continue; // malformed } - } - // handle insertion codes - if (alocsep != -1) - { - pdbResNum = Integer.parseInt(strInfo.substring( - strInfo.indexOf("]") + 1, alocsep)); - - } - else - { - pdbResNum = Integer.parseInt(strInfo.substring( - strInfo.indexOf("]") + 1, chainSeparator)); - } - String chainId; + int pdbResNum = Integer.parseInt(dotPos == -1 ? atomSpec + .substring(colonPos) : atomSpec.substring(colonPos + 1, + dotPos)); - if (strInfo.indexOf(":") > -1) - { - chainId = strInfo.substring(strInfo.indexOf(":") + 1, - strInfo.indexOf(".")); - } - else - { - chainId = " "; - } - - String pdbfilename = modelFileNames[frameNo]; // default is first or current - // model - if (mdlSep > -1) - { - if (chainSeparator1 == -1) - { - chainSeparator1 = strInfo.indexOf(".", mdlSep); + String chainId = dotPos == -1 ? "" : atomSpec.substring(dotPos + 1); + int modelId = 0; + try { + modelId = Integer.valueOf(atomSpec.substring(hashPos + 1, colonPos)); + } catch (NumberFormatException e) { + // ignore, default to model 0 } - String mdlId = (chainSeparator1 > -1) ? strInfo.substring(mdlSep + 1, - chainSeparator1) : strInfo.substring(mdlSep + 1); - try + + /* + * Work out the pdbfilename from the model number + */ + String pdbfilename = modelFileNames[frameNo]; + findfileloop: for (String pdbfile : this.chimeraMaps.keySet()) { - // recover PDB filename for the model hovered over. - int _mp = _modelFileNameMap.length - 1, mnumber = new Integer(mdlId) - .intValue() - 1; - while (mnumber < _modelFileNameMap[_mp]) + for (ChimeraModel cm : chimeraMaps.get(pdbfile)) { - _mp--; - } - pdbfilename = modelFileNames[_mp]; - if (pdbfilename == null) - { - // pdbfilename = new File(viewer.getModelFileName(mnumber)) - // .getAbsolutePath(); + if (cm.getModelNumber() == modelId) + { + pdbfilename = pdbfile; + break findfileloop; + } } - - } catch (Exception e) - { } - ; + atomSpecs.add(new AtomSpec(pdbfilename, chainId, pdbResNum, 0)); } - if (lastMessage == null || !lastMessage.equals(strInfo)) + if (!atomSpecs.isEmpty()) { - getSsm().mouseOverStructure(pdbResNum, chainId, pdbfilename); + getSsm().mouseOverStructure(atomSpecs); } - - lastMessage = strInfo; } - public void notifyAtomPicked(int atomIndex, String strInfo, String strData) - { - /** - * this implements the toggle label behaviour copied from the original - * structure viewer, MCView - */ - if (strData != null) - { - System.err.println("Ignoring additional pick data string " + strData); - } - // rewrite these selections for chimera (DNA, RNA and protein) - int chainSeparator = strInfo.indexOf(":"); - int p = 0; - if (chainSeparator == -1) - { - chainSeparator = strInfo.indexOf("."); - } - - String picked = strInfo.substring(strInfo.indexOf("]") + 1, - chainSeparator); - String mdlString = ""; - if ((p = strInfo.indexOf(":")) > -1) - { - picked += strInfo.substring(p + 1, strInfo.indexOf(".")); - } - - if ((p = strInfo.indexOf("/")) > -1) - { - mdlString += strInfo.substring(p, strInfo.indexOf(" #")); - } - picked = "((" + picked + ".CA" + mdlString + ")|(" + picked + ".P" - + mdlString + "))"; - viewerCommandHistory(false); - - if (!atomsPicked.contains(picked)) - { - viewer.select(picked); - atomsPicked.add(picked); - } - else - { - viewer.select("not " + picked); - atomsPicked.remove(picked); - } - viewerCommandHistory(true); - // TODO: in application this happens - // - // if (scriptWindow != null) - // { - // scriptWindow.sendConsoleMessage(strInfo); - // scriptWindow.sendConsoleMessage("\n"); - // } - - } - - // incremented every time a load notification is successfully handled - - // lightweight mechanism for other threads to detect when they can start - // referring to new structures. - private long loadNotifiesHandled = 0; - public long getLoadNotifiesHandled() { return loadNotifiesHandled; } - public void notifyFileLoaded(String fullPathName, String fileName2, - String modelName, String errorMsg, int modelParts) - { - if (errorMsg != null) - { - fileLoadingError = errorMsg; - refreshGUI(); - return; - } - // TODO: deal sensibly with models loaded inLine: - // modelName will be null, as will fullPathName. - - // the rest of this routine ignores the arguments, and simply interrogates - // the Jmol view to find out what structures it contains, and adds them to - // the structure selection manager. - fileLoadingError = null; - String[] oldmodels = modelFileNames; - modelFileNames = null; - chainNames = new ArrayList(); - chainFile = new HashMap(); - boolean notifyLoaded = false; - String[] modelfilenames = getPdbFile(); - // first check if we've lost any structures - if (oldmodels != null && oldmodels.length > 0) - { - int oldm = 0; - for (int i = 0; i < oldmodels.length; i++) - { - for (int n = 0; n < modelfilenames.length; n++) - { - if (modelfilenames[n] == oldmodels[i]) - { - oldmodels[i] = null; - break; - } - } - if (oldmodels[i] != null) - { - oldm++; - } - } - if (oldm > 0) - { - String[] oldmfn = new String[oldm]; - oldm = 0; - for (int i = 0; i < oldmodels.length; i++) - { - if (oldmodels[i] != null) - { - oldmfn[oldm++] = oldmodels[i]; - } - } - // deregister the Jmol instance for these structures - we'll add - // ourselves again at the end for the current structure set. - getSsm().removeStructureViewerListener(this, oldmfn); - } - } - - // register ourselves as a listener and notify the gui that it needs to - // update itself. - getSsm().addStructureViewerListener(this); - - if (notifyLoaded) - { - FeatureRenderer fr = getFeatureRenderer(null); - if (fr != null) - { - fr.featuresAdded(); - } - refreshGUI(); - loadNotifiesHandled++; - } - setLoadingFromArchive(false); - } - public void setJalviewColourScheme(ColourSchemeI cs) { colourBySequence = false; diff --git a/src/jalview/gui/ChimeraViewFrame.java b/src/jalview/gui/ChimeraViewFrame.java index fdc7099..fda4afb 100644 --- a/src/jalview/gui/ChimeraViewFrame.java +++ b/src/jalview/gui/ChimeraViewFrame.java @@ -476,6 +476,8 @@ public class ChimeraViewFrame extends StructureViewerBase } } jmb.setFinishedInit(true); + + jmb.startChimeraListener(); } void setChainMenuItems(List chainNames) @@ -783,8 +785,8 @@ public class ChimeraViewFrame extends StructureViewerBase if (pdbseq != null && pdbseq.getHeight() > 0) { // just use the file name from the first sequence's first PDBEntry - filePath = new File(((PDBEntry) pdbseq.getSequenceAt(0).getPDBId() - .elementAt(0)).getFile()).getAbsolutePath(); + filePath = new File(pdbseq.getSequenceAt(0).getPDBId() + .elementAt(0).getFile()).getAbsolutePath(); processingEntry.setFile(filePath); } return filePath; diff --git a/src/jalview/httpserver/AbstractRequestHandler.java b/src/jalview/httpserver/AbstractRequestHandler.java new file mode 100644 index 0000000..f48ba49 --- /dev/null +++ b/src/jalview/httpserver/AbstractRequestHandler.java @@ -0,0 +1,90 @@ +package jalview.httpserver; + +import java.io.IOException; +import java.util.Collections; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; + +/** + * + * @author gmcarstairs + * + */ +public abstract class AbstractRequestHandler extends AbstractHandler +{ + /** + * Handle an incoming Http request. + */ + @Override + public void handle(String target, Request baseRequest, + HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException + { + try + { + // dumpRequest(request); // debug + processRequest(request, response); + } catch (Throwable t) + { + /* + * Set server error status on response + */ + System.err.println("Exception handling request " + + request.getRequestURI() + " : " + t.getMessage()); + if (response.isCommitted()) + { + /* + * Can't write an HTTP header once any response content has been written + */ + System.err + .println("Unable to return HTTP 500 as response already committed"); + } + else + { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } finally + { + response.getWriter().flush(); + baseRequest.setHandled(true); + } + } + + /** + * Subclasses should override this method to perform request processing + * + * @param request + * @param response + */ + protected abstract void processRequest(HttpServletRequest request, + HttpServletResponse response); + + /** + * For debug - writes HTTP request details to stdout + * + * @param request + */ + protected void dumpRequest(HttpServletRequest request) + { + System.out.println(request.getMethod()); + for (String hdr : Collections.list(request.getHeaderNames())) + { + for (String val : Collections.list(request.getHeaders(hdr))) + { + System.out.println(hdr + ": " + val); + } + } + for (String param : Collections.list(request.getParameterNames())) + { + for (String val : request.getParameterValues(param)) + { + System.out.println(param + "=" + val); + } + } + } +} diff --git a/src/jalview/httpserver/HttpServer.java b/src/jalview/httpserver/HttpServer.java new file mode 100644 index 0000000..d39ada1 --- /dev/null +++ b/src/jalview/httpserver/HttpServer.java @@ -0,0 +1,245 @@ +package jalview.httpserver; + +import java.net.BindException; +import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.server.handler.HandlerCollection; +import org.eclipse.jetty.util.thread.QueuedThreadPool; + +/** + * An HttpServer built on Jetty + * + * @author gmcarstairs + * @see http://eclipse.org/jetty/documentation/current/embedding-jetty.html + */ +public class HttpServer +{ + private static final String JALVIEW_PATH = "/jalview"; + + /* + * Singleton instance of this server + */ + private static HttpServer instance; + + /* + * The Http server + */ + private Server server; + + /* + * Registered handlers for context paths + */ + private HandlerCollection contextHandlers; + + /* + * Lookup of ContextHandler by its wrapped handler + */ + Map myHandlers = new HashMap(); + + /* + * The context root for the server + */ + private URI contextRoot; + + /** + * Returns the singleton instance of this class. + * + * @return + * @throws BindException + */ + public static HttpServer getInstance() throws BindException + { + synchronized (HttpServer.class) + { + if (instance == null) { + instance = new HttpServer(); + } + return instance; + } + } + + /** + * Private constructor to enforce use of singleton + * + * @throws BindException + * if no free port can be assigned + */ + private HttpServer() throws BindException + { + startServer(); + } + + /** + * Start the http server with a default thread pool size of 1 + * + * @throws BindException + */ + private void startServer() throws BindException + { + try + { + // problem: how to both limit thread pool size and + // pick a random free port - two alternative constructors + QueuedThreadPool tp = new QueuedThreadPool(1, 1); + // server = new Server(tp); // ? fails with URI null + server = new Server(0); // pick a free port number + + /* + * HttpServer shuts down with Jalview process + */ + server.setStopAtShutdown(true); + + /* + * Create a mutable set of handlers (can add handlers while the server is + * running) + */ + // TODO how to combine this with context root "/jalview" + contextHandlers = new HandlerCollection(true); + server.setHandler(contextHandlers); + + server.start(); + contextRoot = server.getURI(); + System.out.println("Jalview endpoint " + contextRoot); + } catch (Exception e) + { + System.err.println("Error trying to start HttpServer: " + + e.getMessage()); + try + { + server.stop(); + } catch (Exception e1) + { + e1.printStackTrace(); + } + } + if (server == null) + { + throw new BindException("HttpServer failed to allocate a port"); + } + } + + /** + * Returns the URI on which we are listening + * + * @return + */ + public URI getUri() + { + return server == null ? null : server.getURI(); + } + + /** + * For debug - write HTTP request details to stdout + * + * @param request + * @param response + */ + protected void dumpRequest(HttpServletRequest request, + HttpServletResponse response) + { + for (String hdr : Collections.list(request.getHeaderNames())) + { + for (String val : Collections.list(request.getHeaders(hdr))) + { + System.out.println(hdr + ": " + val); + } + } + for (String param : Collections.list(request.getParameterNames())) + { + for (String val : request.getParameterValues(param)) + { + System.out.println(param + "=" + val); + } + } + } + + /** + * Stop the Http server. + */ + public void stopServer() + { + if (server != null) + { + if (server.isStarted()) + { + try + { + server.stop(); + } catch (Exception e) + { + System.err.println("Error stopping Http Server on " + + server.getURI() + ": " + e.getMessage()); + } + } + } + } + + /** + * Register a handler for the given path and returns its URI + * + * @param path + * a path below the context root (without leading or trailing + * separator) + * @param handler + * @return + */ + public String registerHandler(String path, AbstractRequestHandler handler) + { + // http://stackoverflow.com/questions/20043097/jetty-9-embedded-adding-handlers-during-runtime + ContextHandler ch = new ContextHandler(); + ch.setContextPath("/" + path); + ch.setResourceBase("."); + ch.setClassLoader(Thread.currentThread() + .getContextClassLoader()); + ch.setHandler(handler); + + /* + * Remember the association so we can remove it later + */ + this.myHandlers.put(handler, ch); + + /* + * A handler added to a running server must be started explicitly + */ + contextHandlers.addHandler(ch); + try + { + ch.start(); + } catch (Exception e) + { + System.err.println("Error starting handler for " + path + ": " + + e.getMessage()); + } + + return this.contextRoot + ch.getContextPath().substring(1); + } + + /** + * Removes the handler from the server; more precisely, remove the + * ContextHandler wrapping the specified handler + * + * @param handler + */ + public void removeHandler(Handler handler) + { + /* + * Have to use this cached lookup table since there is no method + * ContextHandler.getHandler() + */ + ContextHandler ch = myHandlers.get(handler); + if (ch != null) + { + contextHandlers.removeHandler(ch); + myHandlers.remove(handler); + } + } +} diff --git a/src/jalview/structure/AtomSpec.java b/src/jalview/structure/AtomSpec.java index d3e8d42..f22bc2b 100644 --- a/src/jalview/structure/AtomSpec.java +++ b/src/jalview/structure/AtomSpec.java @@ -1,7 +1,8 @@ package jalview.structure; /** - * Java bean representing an atom in a PDB (or similar) structure model. + * Java bean representing an atom in a PDB (or similar) structure model or + * viewer * * @author gmcarstairs * diff --git a/src/jalview/structure/StructureSelectionManager.java b/src/jalview/structure/StructureSelectionManager.java index ed5ee2d..53ed663 100644 --- a/src/jalview/structure/StructureSelectionManager.java +++ b/src/jalview/structure/StructureSelectionManager.java @@ -20,6 +20,21 @@ */ package jalview.structure; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Vector; + +import MCview.Atom; +import MCview.PDBChain; + import jalview.analysis.AlignSeq; import jalview.api.StructureSelectionManagerProvider; import jalview.commands.CommandI; @@ -36,20 +51,6 @@ import jalview.io.AppletFormatAdapter; import jalview.util.MappingUtils; import jalview.util.MessageManager; -import java.io.PrintStream; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.Vector; - -import MCview.Atom; -import MCview.PDBChain; - public class StructureSelectionManager { static IdentityHashMap instances; @@ -581,49 +582,74 @@ public class StructureSelectionManager } } + /** + * Propagate mouseover of a single position in a structure + * + * @param pdbResNum + * @param chain + * @param pdbfile + */ public void mouseOverStructure(int pdbResNum, String chain, String pdbfile) { + AtomSpec atomSpec = new AtomSpec(pdbfile, chain, pdbResNum, 0); + List atoms = Collections.singletonList(atomSpec); + mouseOverStructure(atoms); + } + + /** + * Propagate mouseover or selection of multiple positions in a structure + * + * @param atoms + */ + public void mouseOverStructure(List atoms) + { if (listeners == null) { // old or prematurely sent event return; } - SearchResults results = null; - SequenceI lastseq = null; - int lastipos = -1, indexpos; + boolean hasSequenceListener = false; for (int i = 0; i < listeners.size(); i++) { if (listeners.elementAt(i) instanceof SequenceListener) { - if (results == null) - { - results = new SearchResults(); - } - for (StructureMapping sm : mappings) + hasSequenceListener = true; + } + } + if (!hasSequenceListener) + { + return; + } + + SearchResults results = new SearchResults(); + for (AtomSpec atom : atoms) + { + SequenceI lastseq = null; + int lastipos = -1; + for (StructureMapping sm : mappings) + { + if (sm.pdbfile.equals(atom.getPdbFile()) + && sm.pdbchain.equals(atom.getChain())) { - if (sm.pdbfile.equals(pdbfile) && sm.pdbchain.equals(chain)) + int indexpos = sm.getSeqPos(atom.getPdbResNum()); + if (lastipos != indexpos && lastseq != sm.sequence) { - indexpos = sm.getSeqPos(pdbResNum); - if (lastipos != indexpos && lastseq != sm.sequence) + results.addResult(sm.sequence, indexpos, indexpos); + lastipos = indexpos; + lastseq = sm.sequence; + // construct highlighted sequence list + for (AlignedCodonFrame acf : seqmappings) { - results.addResult(sm.sequence, indexpos, indexpos); - lastipos = indexpos; - lastseq = sm.sequence; - // construct highlighted sequence list - for (AlignedCodonFrame acf : seqmappings) - { - acf.markMappedRegion(sm.sequence, indexpos, results); - } + acf.markMappedRegion(sm.sequence, indexpos, results); } } } } } - if (results != null) + if (!results.isEmpty()) { - for (int i = 0; i < listeners.size(); i++) + for (Object li : listeners) { - Object li = listeners.elementAt(i); if (li instanceof SequenceListener) { ((SequenceListener) li).highlightSequence(results); diff --git a/src/jalview/util/MappingUtils.java b/src/jalview/util/MappingUtils.java index df21355..f2213ad 100644 --- a/src/jalview/util/MappingUtils.java +++ b/src/jalview/util/MappingUtils.java @@ -494,6 +494,12 @@ public final class MappingUtils Set codonFrames = protein.getAlignment() .getCodonFrames(); ColumnSelection mappedColumns = new ColumnSelection(); + + if (colsel == null) + { + return mappedColumns; + } + char fromGapChar = mapFrom.getAlignment().getGapCharacter(); // FIXME allow for hidden columns diff --git a/test/jalview/util/MappingUtilsTest.java b/test/jalview/util/MappingUtilsTest.java index c86259b..ec9ace4 100644 --- a/test/jalview/util/MappingUtilsTest.java +++ b/test/jalview/util/MappingUtilsTest.java @@ -384,6 +384,15 @@ public class MappingUtilsTest assertEquals("[0, 1, 3]", cs.getSelected().toString()); } + @Test + public void testMapColumnSelection_null() throws IOException + { + setupMappedAlignments(); + ColumnSelection cs = MappingUtils.mapColumnSelection(null, dnaView, + proteinView); + assertTrue("mapped selection not empty", cs.getSelected().isEmpty()); + } + /** * Tests for the method that converts a series of [start, end] ranges to * single positions