JAL-3672 debug=false means no debug output
[jalview.git] / src / ext / edu / ucsf / rbvi / strucviz2 / ChimeraManager.java
index 21ae914..375fa4a 100644 (file)
@@ -1,13 +1,44 @@
+/* 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.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -21,6 +52,7 @@ 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.
@@ -51,7 +83,7 @@ public class ChimeraManager
     this.structureManager = structureManager;
     chimera = null;
     chimeraListenerThread = null;
-    currentModelsMap = new HashMap<Integer, ChimeraModel>();
+    currentModelsMap = new HashMap<>();
 
   }
 
@@ -66,7 +98,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())
@@ -80,7 +112,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();
@@ -221,7 +253,7 @@ public class ChimeraManager
     for (ChimeraModel chimeraModel : modelList)
     {
       // get model color
-      Color modelColor = getModelColor(chimeraModel);
+      Color modelColor = isChimeraX() ? null : getModelColor(chimeraModel);
       if (modelColor != null)
       {
         chimeraModel.setModelColor(modelColor);
@@ -232,7 +264,7 @@ 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(chimeraModel);
       }
@@ -301,7 +333,8 @@ public class ChimeraManager
 
   public void stopListening()
   {
-    sendChimeraCommand("listen stop models ; listen stop selection ", false);
+    String command = "listen stop models ; listen stop selection ";
+    sendChimeraCommand(command, false);
   }
 
   /**
@@ -311,9 +344,17 @@ public class ChimeraManager
    */
   public void startListening(String uri)
   {
-    sendChimeraCommand("listen start models url " + uri
-            + ";listen start select prefix SelectionChanged url " + uri,
-            false);
+    /*
+     * 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);
   }
 
   /**
@@ -348,6 +389,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,7 +402,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)
@@ -384,20 +427,24 @@ public class ChimeraManager
    */
   public List<String> getSelectedResidueSpecs()
   {
-    List<String> selectedResidues = new ArrayList<String>();
-    List<String> chimeraReply = sendChimeraCommand(
-            "list selection level residue", true);
+    List<String> selectedResidues = new ArrayList<>();
+
+    String command = "list selection level residue";
+    List<String> chimeraReply = sendChimeraCommand(command, true);
     if (chimeraReply != null)
     {
       /*
-       * expect 0, 1 or more lines of the format
+       * expect 0, 1 or more lines of the format either
+       * Chimera:
        * residue id #0:43.A type GLY
-       * where we are only interested in the atomspec #0.43.A
+       * 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]);
         }
@@ -437,15 +484,22 @@ 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<String> list = sendChimeraCommand("list models type molecule",
-            true);
+    List<ChimeraModel> modelList = new ArrayList<>();
+    String command = "list models type "
+            + (isChimeraX() ? "AtomicStructure" : "molecule");
+    List<String> 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;
@@ -460,7 +514,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)
     {
@@ -516,22 +570,23 @@ public class ChimeraManager
     // iterate over possible paths for starting Chimera
     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";
-        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("RESTServer");
+        addLaunchArguments(args);
         ProcessBuilder pb = new ProcessBuilder(args);
         chimera = pb.start();
         error = "";
@@ -539,7 +594,7 @@ public class ChimeraManager
         break;
       } catch (Exception e)
       {
-        // Chimera could not be started
+        // Chimera could not be started using this path
         error += e.getMessage();
       }
     }
@@ -563,6 +618,18 @@ public class ChimeraManager
   }
 
   /**
+   * Adds command-line arguments to start the REST server
+   * <p>
+   * Method extracted for Jalview to allow override in ChimeraXManager
+   * @param args
+   */
+  protected void addLaunchArguments(List<String> args)
+  {
+    args.add("--start");
+    args.add("RESTServer");
+  }
+
+  /**
    * Read and return the port number returned in the reply to --start RESTServer
    */
   private int getPortNumber()
@@ -579,15 +646,23 @@ public class ChimeraManager
       {
         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(" ");
-          if (tokens.length == 7 && "port".equals(tokens[5]))
+          for (int i = 0; i < tokens.length - 1; i++)
           {
-            port = Integer.parseInt(tokens[6]);
-            break;
+            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)
@@ -665,8 +740,9 @@ public class ChimeraManager
 
   public List<String> getAttrList()
   {
-    List<String> attributes = new ArrayList<String>();
-    final List<String> reply = sendChimeraCommand("list resattr", true);
+    List<String> attributes = new ArrayList<>();
+    String command = (isChimeraX() ? "info " : "list ") + "resattr";
+    final List<String> reply = sendChimeraCommand(command, true);
     if (reply != null)
     {
       for (String inputLine : reply)
@@ -684,7 +760,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)
@@ -738,7 +814,9 @@ public class ChimeraManager
    */
   public List<String> sendChimeraCommand(String command, boolean reply)
   {
-   // System.out.println("chimeradebug>> " + command);
+    if (debug) {
+      System.out.println("chimeradebug>> " + command);
+    }
     if (!isChimeraLaunched() || command == null
             || "".equals(command.trim()))
     {
@@ -784,15 +862,24 @@ public class ChimeraManager
   protected List<String> sendRestCommand(String command)
   {
     String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run";
-    List<NameValuePair> commands = new ArrayList<NameValuePair>(1);
+    List<NameValuePair> commands = new ArrayList<>(1);
+    String method = getHttpRequestMethod();
+    if ("GET".equals(method))
+    {
+      command = command.replace(" ", "+").replace("#", "%23")
+              .replace("|", "%7C").replace(";", "%3B");
+    }
     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, CONNECTION_TIMEOUT_MS,
-              REST_REPLY_TIMEOUT_MS);
+      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)
       {
@@ -817,6 +904,15 @@ 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.
    * 
@@ -859,4 +955,14 @@ public class ChimeraManager
   {
     return busy;
   }
+
+  public Process getChimeraProcess()
+  {
+    return chimera;
+  }
+
+  public boolean isChimeraX()
+  {
+    return false;
+  }
 }