Merge branch 'bug/JAL-2846' into develop
[jalview.git] / src / ext / edu / ucsf / rbvi / strucviz2 / ChimeraManager.java
index b45404e..a910a5a 100644 (file)
@@ -1,3 +1,35 @@
+/* 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;
@@ -8,6 +40,7 @@ import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -27,13 +60,11 @@ import ext.edu.ucsf.rbvi.strucviz2.port.ListenerThreads;
  */
 public class ChimeraManager
 {
-  private static final boolean debug = false;
+  private static final int REST_REPLY_TIMEOUT_MS = 15000;
 
-  /*
-   * true: use REST API (recommended), false: use stdout/stdin (deprecated)
-   */
-  // TODO remove once definitely happy with using REST
-  private static final boolean USE_REST = true;
+  private static final int CONNECTION_TIMEOUT_MS = 100;
+
+  private static final boolean debug = false;
 
   private int chimeraRestPort;
 
@@ -53,7 +84,7 @@ public class ChimeraManager
     this.structureManager = structureManager;
     chimera = null;
     chimeraListenerThread = null;
-    currentModelsMap = new HashMap<Integer, ChimeraModel>();
+    currentModelsMap = new HashMap<>();
 
   }
 
@@ -68,7 +99,7 @@ public class ChimeraManager
   public List<ChimeraModel> getChimeraModels(String modelName,
           ModelType modelType)
   {
-    List<ChimeraModel> models = new ArrayList<ChimeraModel>();
+    List<ChimeraModel> models = new ArrayList<>();
     for (ChimeraModel model : currentModelsMap.values())
     {
       if (modelName.equals(model.getModelName())
@@ -82,7 +113,7 @@ public class ChimeraManager
 
   public Map<String, List<ChimeraModel>> getChimeraModelsMap()
   {
-    Map<String, List<ChimeraModel>> models = new HashMap<String, List<ChimeraModel>>();
+    Map<String, List<ChimeraModel>> models = new HashMap<>();
     for (ChimeraModel model : currentModelsMap.values())
     {
       String modelName = model.getModelName();
@@ -183,7 +214,8 @@ public class ChimeraManager
           ModelType type)
   {
     logger.info("chimera open " + modelPath);
-    stopListening();
+    // stopListening();
+    List<ChimeraModel> modelList = getModelList();
     List<String> response = null;
     // TODO: [Optional] Handle modbase models
     if (type == ModelType.MODBASE_MODEL)
@@ -203,72 +235,29 @@ public class ChimeraManager
       logger.warn("Could not open " + modelPath);
       return null;
     }
-    List<ChimeraModel> models = new ArrayList<ChimeraModel>();
-    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<ChimeraModel> 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.)
@@ -278,13 +267,13 @@ 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;
   }
 
   /**
@@ -304,13 +293,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;
   }
@@ -341,12 +329,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);
   }
 
   /**
@@ -357,8 +357,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,15 +370,7 @@ public class ChimeraManager
   {
     chimera = null;
     currentModelsMap.clear();
-    if (USE_REST)
-    {
-      this.chimeraRestPort = 0;
-    }
-    else
-    {
-      chimeraListenerThread.requestStop();
-      chimeraListenerThread = null;
-    }
+    this.chimeraRestPort = 0;
     structureManager.clearOnChimeraExit();
   }
 
@@ -389,6 +381,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)
       {
@@ -400,7 +394,7 @@ public class ChimeraManager
 
   public Map<Integer, ChimeraModel> getSelectedModels()
   {
-    Map<Integer, ChimeraModel> selectedModelsMap = new HashMap<Integer, ChimeraModel>();
+    Map<Integer, ChimeraModel> selectedModelsMap = new HashMap<>();
     List<String> chimeraReply = sendChimeraCommand(
             "list selection level molecule", true);
     if (chimeraReply != null)
@@ -417,13 +411,24 @@ public class ChimeraManager
     return selectedModelsMap;
   }
 
+  /**
+   * Sends a 'list selection level residue' command to Chimera and returns the
+   * list of selected atomspecs
+   * 
+   * @return
+   */
   public List<String> getSelectedResidueSpecs()
   {
-    List<String> selectedResidues = new ArrayList<String>();
+    List<String> selectedResidues = new ArrayList<>();
     List<String> chimeraReply = sendChimeraCommand(
             "list selection level residue", true);
     if (chimeraReply != null)
     {
+      /*
+       * expect 0, 1 or more lines of the format
+       * residue id #0:43.A type GLY
+       * where we are only interested in the atomspec #0.43.A
+       */
       for (String inputLine : chimeraReply)
       {
         String[] inputLineParts = inputLine.split("\\s+");
@@ -467,7 +472,7 @@ public class ChimeraManager
   // TODO: [Optional] Handle smiles names in a better way in Chimera?
   public List<ChimeraModel> getModelList()
   {
-    List<ChimeraModel> modelList = new ArrayList<ChimeraModel>();
+    List<ChimeraModel> modelList = new ArrayList<>();
     List<String> list = sendChimeraCommand("list models type molecule",
             true);
     if (list != null)
@@ -490,7 +495,7 @@ public class ChimeraManager
    */
   public List<String> getPresets()
   {
-    ArrayList<String> presetList = new ArrayList<String>();
+    ArrayList<String> presetList = new ArrayList<>();
     List<String> output = sendChimeraCommand("preset list", true);
     if (output != null)
     {
@@ -524,6 +529,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<String> chimeraPaths)
   {
     // Do nothing if Chimera is already launched
@@ -538,52 +551,47 @@ 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<String> args = new ArrayList<String>();
+        // 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<String> args = new ArrayList<>();
         args.add(chimeraPath);
+        // shows Chimera output window but suppresses REST responses:
+        // args.add("--debug");
         args.add("--start");
-        args.add(USE_REST ? "RESTServer" : "ReadStdin");
+        args.add("RESTServer");
         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.chimeraRestPort = getPortNumber();
-        System.out.println("Chimera REST API on port " + chimeraRestPort);
-      }
-      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
@@ -600,21 +608,29 @@ public class ChimeraManager
     InputStream readChan = chimera.getInputStream();
     BufferedReader lineReader = new BufferedReader(new InputStreamReader(
             readChan));
-    String response = null;
+    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 "
-                + chimeraRestPort);
+      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 " + response + ": "
-              + e.getMessage());
+      logger.error("Failed to get REST port number from " + responses
+              + ": " + e.getMessage());
     } finally
     {
       try
@@ -624,6 +640,13 @@ 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;
   }
 
@@ -679,7 +702,7 @@ public class ChimeraManager
 
   public List<String> getAttrList()
   {
-    List<String> attributes = new ArrayList<String>();
+    List<String> attributes = new ArrayList<>();
     final List<String> reply = sendChimeraCommand("list resattr", true);
     if (reply != null)
     {
@@ -698,7 +721,7 @@ public class ChimeraManager
   public Map<ChimeraResidue, Object> getAttrValues(String aCommand,
           ChimeraModel model)
   {
-    Map<ChimeraResidue, Object> values = new HashMap<ChimeraResidue, Object>();
+    Map<ChimeraResidue, Object> values = new HashMap<>();
     final List<String> reply = sendChimeraCommand("list residue spec "
             + model.toSpec() + " attribute " + aCommand, true);
     if (reply != null)
@@ -752,6 +775,7 @@ public class ChimeraManager
    */
   public List<String> sendChimeraCommand(String command, boolean reply)
   {
+   // System.out.println("chimeradebug>> " + command);
     if (!isChimeraLaunched() || command == null
             || "".equals(command.trim()))
     {
@@ -766,22 +790,17 @@ public class ChimeraManager
       } 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)
       {
@@ -801,23 +820,24 @@ public class ChimeraManager
    */
   protected List<String> sendRestCommand(String 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<NameValuePair> commands = new ArrayList<NameValuePair>(1);
+    List<NameValuePair> commands = new ArrayList<>(1);
     commands.add(new BasicNameValuePair("command", command));
 
-    List<String> reply = new ArrayList<String>();
+    List<String> reply = new ArrayList<>();
     BufferedReader response = null;
-    try {
-      response = HttpClientUtils.doHttpUrlPost(restUrl,
-              commands);
+    try
+    {
+      response = 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)
@@ -877,4 +897,8 @@ public class ChimeraManager
     return busy;
   }
 
+  public Process getChimeraProcess()
+  {
+    return chimera;
+  }
 }