X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fext%2Fedu%2Fucsf%2Frbvi%2Fstrucviz2%2FChimeraManager.java;h=668039bb55da8d604f53f7225218657b0c563984;hb=57738a1f3c19b1c3a00bd3ac5108f8cd0af32f99;hp=a76c7e0fe69e2f6be247a716b6c40e26dec2ed1c;hpb=81316c6cc11e29c893d55e87ebd16d9ecd09c1f3;p=jalview.git diff --git a/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java b/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java index a76c7e0..668039b 100644 --- a/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java +++ b/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java @@ -1,13 +1,47 @@ +/* vim: set ts=2: */ +/** + * Copyright (c) 2006 The Regents of the University of California. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions, and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions, and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * 3. Redistributions must acknowledge that this software was + * originally developed by the UCSF Computer Graphics Laboratory + * under support by the NIH National Center for Research Resources, + * grant P41-RR01081. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ package ext.edu.ucsf.rbvi.strucviz2; -import jalview.ws.HttpClientUtils; - import java.awt.Color; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -21,21 +55,20 @@ 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. */ public class ChimeraManager { - private static final boolean debug = true; + private static final int REST_REPLY_TIMEOUT_MS = 15000; - /* - * true: use REST API (recommended), false: use stdout/stdin (deprecated) - */ - private static final boolean USE_REST = true; + private static final int CONNECTION_TIMEOUT_MS = 100; - // Port number for Chimera REST service - private int restPort; + private static final boolean debug = false; + + private int chimeraRestPort; private Process chimera; @@ -53,7 +86,7 @@ public class ChimeraManager this.structureManager = structureManager; chimera = null; chimeraListenerThread = null; - currentModelsMap = new HashMap(); + currentModelsMap = new HashMap<>(); } @@ -68,7 +101,7 @@ public class ChimeraManager public List getChimeraModels(String modelName, ModelType modelType) { - List models = new ArrayList(); + List models = new ArrayList<>(); for (ChimeraModel model : currentModelsMap.values()) { if (modelName.equals(model.getModelName()) @@ -82,7 +115,7 @@ public class ChimeraManager public Map> getChimeraModelsMap() { - Map> models = new HashMap>(); + Map> models = new HashMap<>(); for (ChimeraModel model : currentModelsMap.values()) { String modelName = model.getModelName(); @@ -144,10 +177,11 @@ public class ChimeraManager return hasChimeraModel(modelNubmer, 0); } - public boolean hasChimeraModel(Integer modelNubmer, Integer subModelNumber) + public boolean hasChimeraModel(Integer modelNubmer, + Integer subModelNumber) { - return currentModelsMap.containsKey(ChimUtils.makeModelKey(modelNubmer, - subModelNumber)); + return currentModelsMap.containsKey( + ChimUtils.makeModelKey(modelNubmer, subModelNumber)); } public void addChimeraModel(Integer modelNumber, Integer subModelNumber, @@ -157,7 +191,8 @@ public class ChimeraManager ChimUtils.makeModelKey(modelNumber, subModelNumber), model); } - public void removeChimeraModel(Integer modelNumber, Integer subModelNumber) + public void removeChimeraModel(Integer modelNumber, + Integer subModelNumber) { int modelKey = ChimUtils.makeModelKey(modelNumber, subModelNumber); if (currentModelsMap.containsKey(modelKey)) @@ -183,7 +218,8 @@ public class ChimeraManager ModelType type) { logger.info("chimera open " + modelPath); - stopListening(); + // stopListening(); + List modelList = getModelList(); List response = null; // TODO: [Optional] Handle modbase models if (type == ModelType.MODBASE_MODEL) @@ -203,72 +239,28 @@ public class ChimeraManager logger.warn("Could not open " + modelPath); return null; } - List models = new ArrayList(); - int[] modelNumbers = null; - if (type == ModelType.PDB_MODEL) - { - for (String line : response) - { - if (line.startsWith("#")) - { - modelNumbers = ChimUtils.parseOpenedModelNumber(line); - if (modelNumbers != null) - { - int modelNumber = ChimUtils.makeModelKey(modelNumbers[0], - modelNumbers[1]); - if (currentModelsMap.containsKey(modelNumber)) - { - continue; - } - ChimeraModel newModel = new ChimeraModel(modelName, type, - modelNumbers[0], modelNumbers[1]); - currentModelsMap.put(modelNumber, newModel); - models.add(newModel); - // patch for Jalview - set model name in Chimera - sendChimeraCommand("setattr M name " + modelName + " #" - + modelNumbers[0], false); - // end patch for Jalview - modelNumbers = null; - } - } - } - } - else + + // patch for Jalview - set model name in Chimera + // TODO: find a variant that works for sub-models + for (ChimeraModel newModel : getModelList()) { - // TODO: [Optional] Open smiles from file would fail. Do we need it? - // If parsing fails, iterate over all open models to get the right one - List openModels = getModelList(); - for (ChimeraModel openModel : openModels) + if (!modelList.contains(newModel)) { - String openModelName = openModel.getModelName(); - if (openModelName.endsWith("...")) - { - openModelName = openModelName.substring(0, - openModelName.length() - 3); - } - if (modelPath.startsWith(openModelName)) - { - openModel.setModelName(modelPath); - int modelNumber = ChimUtils - .makeModelKey(openModel.getModelNumber(), - openModel.getSubModelNumber()); - if (!currentModelsMap.containsKey(modelNumber)) - { - currentModelsMap.put(modelNumber, openModel); - models.add(openModel); - } - } + newModel.setModelName(modelName); + sendChimeraCommand("setattr M name " + modelName + " #" + + newModel.getModelNumber(), false); + modelList.add(newModel); } } // assign color and residues to open models - for (ChimeraModel newModel : models) + for (ChimeraModel chimeraModel : modelList) { // get model color - Color modelColor = getModelColor(newModel); + Color modelColor = isChimeraX() ? null : getModelColor(chimeraModel); if (modelColor != null) { - newModel.setModelColor(modelColor); + chimeraModel.setModelColor(modelColor); } // Get our properties (default color scheme, etc.) @@ -276,15 +268,15 @@ public class ChimeraManager // chimeraSend("repr stick "+newModel.toSpec()); // Create the information we need for the navigator - if (type != ModelType.SMILES) + if (type != ModelType.SMILES && !isChimeraX()) { - addResidues(newModel); + addResidues(chimeraModel); } } sendChimeraCommand("focus", false); - startListening(); - return models; + // startListening(); // see ChimeraListener + return modelList; } /** @@ -304,13 +296,12 @@ public class ChimeraManager // TODO: [Optional] Convert path to name in a better way if (modelPath.lastIndexOf(File.separator) > 0) { - modelName = modelPath.substring(modelPath - .lastIndexOf(File.separator) + 1); + modelName = modelPath + .substring(modelPath.lastIndexOf(File.separator) + 1); } else if (modelPath.lastIndexOf("/") > 0) { - modelName = modelPath - .substring(modelPath.lastIndexOf("/") + 1); + modelName = modelPath.substring(modelPath.lastIndexOf("/") + 1); } return modelName; } @@ -327,8 +318,8 @@ public class ChimeraManager { sendChimeraCommand("close " + model.toSpec(), false); // currentModelNamesMap.remove(model.getModelName()); - currentModelsMap.remove(ChimUtils.makeModelKey( - model.getModelNumber(), model.getSubModelNumber())); + currentModelsMap.remove(ChimUtils.makeModelKey(model.getModelNumber(), + model.getSubModelNumber())); // selectionList.remove(chimeraModel); } else @@ -341,12 +332,34 @@ 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); + String command = "listen stop models ; listen stop selection "; + sendChimeraCommand(command, false); + } + + /** + * Tell Chimera we are listening on the given URI + * + * @param uri + */ + public void startListening(String uri) + { + /* + * listen for model changes + */ + String command = "listen start models url " + uri; + sendChimeraCommand(command, false); + + /* + * listen for selection changes + */ + command = "listen start select prefix SelectionChanged url " + uri; + sendChimeraCommand(command, false); } /** @@ -357,8 +370,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() @@ -370,11 +383,7 @@ public class ChimeraManager { chimera = null; currentModelsMap.clear(); - if (!USE_REST) - { - chimeraListenerThread.requestStop(); - chimeraListenerThread = null; - } + this.chimeraRestPort = 0; structureManager.clearOnChimeraExit(); } @@ -385,6 +394,8 @@ public class ChimeraManager sendChimeraCommand("stop really", false); try { + // TODO is this too violent? could it force close the process + // before it has done an orderly shutdown? chimera.destroy(); } catch (Exception ex) { @@ -396,7 +407,7 @@ public class ChimeraManager public Map getSelectedModels() { - Map selectedModelsMap = new HashMap(); + Map selectedModelsMap = new HashMap<>(); List chimeraReply = sendChimeraCommand( "list selection level molecule", true); if (chimeraReply != null) @@ -413,17 +424,32 @@ 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(); - List chimeraReply = sendChimeraCommand( - "list selection level residue", true); + List selectedResidues = new ArrayList<>(); + + String command = "list selection level residue"; + List chimeraReply = sendChimeraCommand(command, true); if (chimeraReply != null) { + /* + * expect 0, 1 or more lines of the format either + * Chimera: + * residue id #0:43.A type GLY + * ChimeraX: + * residue id /A:89 name THR index 88 + * We are only interested in the atomspec (third token of the reply) + */ for (String inputLine : chimeraReply) { String[] inputLineParts = inputLine.split("\\s+"); - if (inputLineParts.length == 5) + if (inputLineParts.length >= 5) { selectedResidues.add(inputLineParts[2]); } @@ -463,15 +489,22 @@ public class ChimeraManager // TODO: [Optional] Handle smiles names in a better way in Chimera? public List getModelList() { - List modelList = new ArrayList(); - List list = sendChimeraCommand("list models type molecule", - true); + List modelList = new ArrayList<>(); + String command = "list models type " + + (isChimeraX() ? "AtomicStructure" : "molecule"); + List list = sendChimeraCommand(command, true); if (list != null) { for (String modelLine : list) { - ChimeraModel chimeraModel = new ChimeraModel(modelLine); - modelList.add(chimeraModel); + try + { + ChimeraModel chimeraModel = new ChimeraModel(modelLine); + modelList.add(chimeraModel); + } catch (NullPointerException e) + { + // hack for now + } } } return modelList; @@ -486,7 +519,7 @@ public class ChimeraManager */ public List getPresets() { - ArrayList presetList = new ArrayList(); + ArrayList presetList = new ArrayList<>(); List output = sendChimeraCommand("preset list", true); if (output != null) { @@ -520,6 +553,14 @@ public class ChimeraManager return launched; } + /** + * Launch Chimera, unless an instance linked to this object is already + * running. Returns true if chimera is successfully launched, or already + * running, else false. + * + * @param chimeraPaths + * @return + */ public boolean launchChimera(List chimeraPaths) { // Do nothing if Chimera is already launched @@ -534,51 +575,46 @@ public class ChimeraManager // iterate over possible paths for starting Chimera for (String chimeraPath : chimeraPaths) { - File path = new File(chimeraPath); - if (!path.canExecute()) - { - error += "File '" + path + "' does not exist.\n"; - continue; - } try { - List args = new ArrayList(); + // ensure symbolic links are resolved + chimeraPath = Paths.get(chimeraPath).toRealPath().toString(); + File path = new File(chimeraPath); + // uncomment the next line to simulate Chimera not installed + // path = new File(chimeraPath + "x"); + if (!path.canExecute()) + { + error += "File '" + path + "' does not exist.\n"; + continue; + } + List args = new ArrayList<>(); args.add(chimeraPath); - args.add("--start"); - args.add(USE_REST ? "RESTServer" : "ReadStdin"); + // shows Chimera output window but suppresses REST responses: + // args.add("--debug"); + addLaunchArguments(args); ProcessBuilder pb = new ProcessBuilder(args); chimera = pb.start(); error = ""; workingPath = chimeraPath; - logger.info("Starting " + chimeraPath + " with " - + (USE_REST ? "REST API" : "stdin/stdout")); break; } catch (Exception e) { - // Chimera could not be started + // Chimera could not be started using this path error += e.getMessage(); } } // If no error, then Chimera was launched successfully if (error.length() == 0) { - if (USE_REST) - { - this.restPort = getPortNumber(); - } - else - { - // Initialize the listener threads - chimeraListenerThread = new ListenerThreads(chimera, - structureManager); - chimeraListenerThread.start(); - } + this.chimeraRestPort = getPortNumber(); + System.out.println( + "Chimera REST API started on port " + chimeraRestPort); // structureManager.initChimTable(); structureManager.setChimeraPathProperty(workingPath); // TODO: [Optional] Check Chimera version and show a warning if below 1.8 // Ask Chimera to give us updates - startListening(); - return true; + // startListening(); // later - see ChimeraListener + return (chimeraRestPort > 0); } // Tell the user that Chimera could not be started because of an error @@ -587,27 +623,57 @@ public class ChimeraManager } /** + * Adds command-line arguments to start the REST server + *

+ * Method extracted for Jalview to allow override in ChimeraXManager + * + * @param args + */ + protected void addLaunchArguments(List args) + { + args.add("--start"); + args.add("RESTServer"); + } + + /** * Read and return the port number returned in the reply to --start RESTServer */ private int getPortNumber() { int port = 0; InputStream readChan = chimera.getInputStream(); - BufferedReader lineReader = new BufferedReader(new InputStreamReader( - readChan)); - String response = null; + BufferedReader lineReader = new BufferedReader( + new InputStreamReader(readChan)); + StringBuilder responses = new StringBuilder(); try { - // expect: REST server on host 127.0.0.1 port port_number - response = lineReader.readLine(); - String [] tokens = response.split(" "); - if (tokens.length == 7 && "port".equals(tokens[5])) { - port = Integer.parseInt(tokens[6]); - logger.info("Chimera REST service listening on port " + restPort); + String response = lineReader.readLine(); + while (response != null) + { + responses.append("\n" + response); + // expect: REST server on host 127.0.0.1 port port_number + // ChimeraX is the same except "REST server started on host..." + if (response.startsWith("REST server")) + { + String[] tokens = response.split(" "); + for (int i = 0; i < tokens.length - 1; i++) + { + if ("port".equals(tokens[i])) + { + port = Integer.parseInt(tokens[i + 1]); + break; + } + } + } + if (port > 0) + { + break; // hack for hanging readLine() + } + response = lineReader.readLine(); } } catch (Exception e) { - logger.error("Failed to get REST port number from " + response + ": " + logger.error("Failed to get REST port number from " + responses + ": " + e.getMessage()); } finally { @@ -618,6 +684,14 @@ public class ChimeraManager { } } + if (port == 0) + { + System.err.println( + "Failed to start Chimera with REST service, response was: " + + responses); + } + logger.info( + "Chimera REST service listening on port " + chimeraRestPort); return port; } @@ -673,8 +747,9 @@ public class ChimeraManager public List getAttrList() { - List attributes = new ArrayList(); - final List reply = sendChimeraCommand("list resattr", true); + List attributes = new ArrayList<>(); + String command = (isChimeraX() ? "info " : "list ") + "resattr"; + final List reply = sendChimeraCommand(command, true); if (reply != null) { for (String inputLine : reply) @@ -692,7 +767,7 @@ public class ChimeraManager public Map getAttrValues(String aCommand, ChimeraModel model) { - Map values = new HashMap(); + Map values = new HashMap<>(); final List reply = sendChimeraCommand("list residue spec " + model.toSpec() + " attribute " + aCommand, true); if (reply != null) @@ -702,8 +777,8 @@ public class ChimeraManager String[] lineParts = inputLine.split("\\s"); if (lineParts.length == 5) { - ChimeraResidue residue = ChimUtils - .getResidue(lineParts[2], model); + ChimeraResidue residue = ChimUtils.getResidue(lineParts[2], + model); String value = lineParts[4]; if (residue != null) { @@ -746,36 +821,41 @@ public class ChimeraManager */ public List sendChimeraCommand(String command, boolean reply) { + if (debug) + { + System.out.println("chimeradebug>> " + command); + } if (!isChimeraLaunched() || command == null || "".equals(command.trim())) { return null; } - // TODO do we need a maximum wait time before aborting? - while (busy) + /* + * set a maximum wait time before trying anyway + * to avoid hanging indefinitely + */ + int waited = 0; + int pause = 25; + while (busy && waited < 1001) { try { - Thread.sleep(25); + Thread.sleep(pause); + waited += pause; } catch (InterruptedException q) { } - ; } busy = true; long startTime = System.currentTimeMillis(); try { - if (USE_REST) - { - return sendRestCommand(command); - } - else - { - return sendStdinCommand(command, reply); - } + return sendRestCommand(command); } finally { + /* + * Make sure busy flag is reset come what may! + */ busy = false; if (debug) { @@ -783,7 +863,6 @@ public class ChimeraManager + (System.currentTimeMillis() - startTime) + "ms: " + command); } - } } @@ -795,23 +874,40 @@ public class ChimeraManager */ protected List sendRestCommand(String command) { - // TODO start a separate thread to do this so we don't block? - String restUrl = "http://127.0.0.1:" + this.restPort + "/run"; - List commands = new ArrayList(1); + String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run"; + List commands = new ArrayList<>(1); + String method = getHttpRequestMethod(); + if ("GET".equals(method)) + { + try + { + command = URLEncoder.encode(command, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) + { + command = command.replace(" ", "+").replace("#", "%23") + .replace("|", "%7C").replace(";", "%3B") + .replace(":", "%3A"); + } + } commands.add(new BasicNameValuePair("command", command)); - List reply = new ArrayList(); + List reply = new ArrayList<>(); BufferedReader response = null; - try { - response = HttpClientUtils.doHttpUrlPost(restUrl, - commands); + try + { + response = "GET".equals(method) + ? HttpClientUtils.doHttpGet(restUrl, commands, + CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS) + : HttpClientUtils.doHttpUrlPost(restUrl, commands, + CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS); String line = ""; - while ((line = response.readLine()) != null) { + while ((line = response.readLine()) != null) + { reply.add(line); } } catch (Exception e) { - logger.error("REST call " + command + " failed: " + e.getMessage()); + logger.error("REST call '" + command + "' failed: " + e.getMessage()); } finally { if (response != null) @@ -828,6 +924,17 @@ public class ChimeraManager } /** + * Returns "POST" as the HTTP request method to use for REST service calls to + * Chimera + * + * @return + */ + protected String getHttpRequestMethod() + { + return "POST"; + } + + /** * Send a command to stdin of Chimera process, and optionally read any * responses. * @@ -871,4 +978,13 @@ public class ChimeraManager return busy; } + public Process getChimeraProcess() + { + return chimera; + } + + public boolean isChimeraX() + { + return false; + } }