X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fext%2Fedu%2Fucsf%2Frbvi%2Fstrucviz2%2FChimeraManager.java;h=439d479309dfc60ac7b03e6e3ed2e7e8e2941bd6;hb=260f6abfaaafac067d5bda703e5b21be480bcc8f;hp=d0bb84e0e48828adb710f612e497b4cfea963a59;hpb=274eb184c92307edde158510463c335a5179a724;p=jalview.git diff --git a/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java b/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java index d0bb84e..439d479 100644 --- a/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java +++ b/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java @@ -1,14 +1,53 @@ +/* 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.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.http.NameValuePair; +import org.apache.http.message.BasicNameValuePair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,14 +59,21 @@ import ext.edu.ucsf.rbvi.strucviz2.port.ListenerThreads; */ public class ChimeraManager { + private static final int REST_REPLY_TIMEOUT_MS = 15000; + + private static final int CONNECTION_TIMEOUT_MS = 100; + + private static final boolean debug = false; - static private Process chimera; + private int chimeraRestPort; - static private ListenerThreads chimeraListenerThreads; + private Process chimera; - static private Map currentModelsMap; + private ListenerThreads chimeraListenerThread; - private static Logger logger = LoggerFactory + private Map currentModelsMap; + + private Logger logger = LoggerFactory .getLogger(ext.edu.ucsf.rbvi.strucviz2.ChimeraManager.class); private StructureManager structureManager; @@ -36,9 +82,9 @@ public class ChimeraManager { this.structureManager = structureManager; chimera = null; - chimeraListenerThreads = null; + chimeraListenerThread = null; currentModelsMap = new HashMap(); - + } public List getChimeraModels(String modelName) @@ -152,8 +198,23 @@ public class ChimeraManager public List openModel(String modelPath, ModelType type) { + return openModel(modelPath, getFileNameFromPath(modelPath), type); + } + + /** + * Overloaded method to allow Jalview to pass in a model name. + * + * @param modelPath + * @param modelName + * @param type + * @return + */ + public List openModel(String modelPath, String modelName, + 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) @@ -173,80 +234,29 @@ 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; - } - String modelName = modelPath; - // TODO: [Optional] Convert path to name in a better way - if (modelPath.lastIndexOf(File.separator) > 0) - { - modelName = modelPath.substring(modelPath - .lastIndexOf(File.separator) + 1); - } - else if (modelPath.lastIndexOf("/") > 0) - { - modelName = modelPath - .substring(modelPath.lastIndexOf("/") + 1); - } - ChimeraModel newModel = new ChimeraModel(modelName, type, - modelNumbers[0], modelNumbers[1]); - currentModelsMap.put(modelNumber, newModel); - models.add(newModel); - 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 = getModelColor(chimeraModel); if (modelColor != null) { - newModel.setModelColor(modelColor); + chimeraModel.setModelColor(modelColor); } // Get our properties (default color scheme, etc.) @@ -256,13 +266,40 @@ public class ChimeraManager // Create the information we need for the navigator if (type != ModelType.SMILES) { - addResidues(newModel); + addResidues(chimeraModel); } } sendChimeraCommand("focus", false); - startListening(); - return models; + // startListening(); // see ChimeraListener + return modelList; + } + + /** + * Refactored method to extract the last (or only) element delimited by file + * path separator. + * + * @param modelPath + * @return + */ + private String getFileNameFromPath(String modelPath) + { + String modelName = modelPath; + if (modelPath == null) + { + return null; + } + // TODO: [Optional] Convert path to name in a better way + if (modelPath.lastIndexOf(File.separator) > 0) + { + modelName = modelPath + .substring(modelPath.lastIndexOf(File.separator) + 1); + } + else if (modelPath.lastIndexOf("/") > 0) + { + modelName = modelPath.substring(modelPath.lastIndexOf("/") + 1); + } + return modelName; } public void closeModel(ChimeraModel model) @@ -291,12 +328,24 @@ 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 + + ";listen start select prefix SelectionChanged url " + uri, + false); } /** @@ -307,8 +356,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() @@ -320,7 +369,7 @@ public class ChimeraManager { chimera = null; currentModelsMap.clear(); - chimeraListenerThreads = null; + this.chimeraRestPort = 0; structureManager.clearOnChimeraExit(); } @@ -331,6 +380,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) { @@ -359,6 +410,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(); @@ -450,16 +507,30 @@ public class ChimeraManager public boolean isChimeraLaunched() { - // TODO: [Optional] What is the best way to test if chimera is launched? - - // sendChimeraCommand("test", true) !=null + boolean launched = false; if (chimera != null) { - return true; + try + { + chimera.exitValue(); + // if we get here, process has ended + } catch (IllegalThreadStateException e) + { + // ok - not yet terminated + launched = true; + } } - return false; + 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 @@ -475,6 +546,8 @@ public class ChimeraManager for (String chimeraPath : chimeraPaths) { 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"; @@ -484,13 +557,14 @@ public class ChimeraManager { List args = new ArrayList(); args.add(chimeraPath); + // shows Chimera output window but suppresses REST responses: + // args.add("--debug"); args.add("--start"); - args.add("ReadStdin"); + args.add("RESTServer"); ProcessBuilder pb = new ProcessBuilder(args); chimera = pb.start(); error = ""; workingPath = chimeraPath; - logger.info("Strarting " + chimeraPath); break; } catch (Exception e) { @@ -501,16 +575,15 @@ public class ChimeraManager // If no error, then Chimera was launched successfully if (error.length() == 0) { - // Initialize the listener threads - chimeraListenerThreads = new ListenerThreads(chimera, - structureManager); - chimeraListenerThreads.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 @@ -519,6 +592,57 @@ public class ChimeraManager } /** + * 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)); + StringBuilder responses = new StringBuilder(); + try + { + String response = lineReader.readLine(); + while (response != null) + { + responses.append("\n" + response); + // expect: REST server on host 127.0.0.1 port port_number + if (response.startsWith("REST server")) + { + String[] tokens = response.split(" "); + if (tokens.length == 7 && "port".equals(tokens[5])) + { + port = Integer.parseInt(tokens[6]); + break; + } + } + response = lineReader.readLine(); + } + } catch (Exception e) + { + logger.error("Failed to get REST port number from " + responses + + ": " + e.getMessage()); + } finally + { + try + { + lineReader.close(); + } catch (IOException e2) + { + } + } + 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; + } + + /** * Determine the color that Chimera is using for this model. * * @param model @@ -533,7 +657,7 @@ public class ChimeraManager { return null; } - return ChimUtils.parseModelColor((String) colorLines.get(0)); + return ChimUtils.parseModelColor(colorLines.get(0)); } /** @@ -628,6 +752,8 @@ public class ChimeraManager return values; } + private volatile boolean busy = false; + /** * Send a command to Chimera. * @@ -641,14 +767,96 @@ public class ChimeraManager */ public List sendChimeraCommand(String command, boolean reply) { - if (!isChimeraLaunched()) + // 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) + { + try + { + Thread.sleep(25); + } catch (InterruptedException q) + { + } + } + busy = true; + long startTime = System.currentTimeMillis(); + try + { + return sendRestCommand(command); + } finally + { + /* + * Make sure busy flag is reset come what may! + */ + busy = false; + if (debug) + { + System.out.println("Chimera command took " + + (System.currentTimeMillis() - startTime) + "ms: " + + command); + } + + } + } + + /** + * Sends the command to Chimera's REST API, and returns any response lines. + * + * @param command + * @return + */ + protected List sendRestCommand(String command) + { + String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run"; + List commands = new ArrayList(1); + commands.add(new BasicNameValuePair("command", command)); + + List reply = new ArrayList(); + BufferedReader response = null; + try + { + response = HttpClientUtils.doHttpUrlPost(restUrl, commands, CONNECTION_TIMEOUT_MS, + REST_REPLY_TIMEOUT_MS); + String line = ""; + while ((line = response.readLine()) != null) + { + reply.add(line); + } + } catch (Exception e) + { + logger.error("REST call '" + command + "' failed: " + e.getMessage()); + } finally + { + if (response != null) + { + try + { + response.close(); + } catch (IOException e) + { + } + } + } + return reply; + } - chimeraListenerThreads.clearResponse(command); + /** + * Send a command to stdin of Chimera process, and optionally read any + * responses. + * + * @param command + * @param readReply + * @return + */ + protected List sendStdinCommand(String command, boolean readReply) + { + chimeraListenerThread.clearResponse(command); String text = command.concat("\n"); - // System.out.println("send command to chimera: " + text); try { // send the command @@ -663,11 +871,26 @@ public class ChimeraManager clearOnChimeraExit(); return null; } - if (!reply) + if (!readReply) { return null; } - return chimeraListenerThreads.getResponse(command); + List rsp = chimeraListenerThread.getResponse(command); + return rsp; } + public StructureManager getStructureManager() + { + return structureManager; + } + + public boolean isBusy() + { + return busy; + } + + public Process getChimeraProcess() + { + return chimera; + } }