JAL-1725 first version of Jetty server / chimera selection listener
[jalview.git] / src / ext / edu / ucsf / rbvi / strucviz2 / ChimeraManager.java
index d0bb84e..3ed27a3 100644 (file)
@@ -1,33 +1,43 @@
 package ext.edu.ucsf.rbvi.strucviz2;
 
 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;
 
 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 = false;
+
+  private int chimeraRestPort;
 
-  static private Process chimera;
+  private Process chimera;
 
-  static private ListenerThreads chimeraListenerThreads;
+  private ListenerThreads chimeraListenerThread;
 
-  static private Map<Integer, ChimeraModel> currentModelsMap;
+  private Map<Integer, ChimeraModel> currentModelsMap;
 
-  private static Logger logger = LoggerFactory
+  private Logger logger = LoggerFactory
           .getLogger(ext.edu.ucsf.rbvi.strucviz2.ChimeraManager.class);
 
   private StructureManager structureManager;
@@ -36,9 +46,9 @@ public class ChimeraManager
   {
     this.structureManager = structureManager;
     chimera = null;
-    chimeraListenerThreads = null;
+    chimeraListenerThread = null;
     currentModelsMap = new HashMap<Integer, ChimeraModel>();
-    
+
   }
 
   public List<ChimeraModel> getChimeraModels(String modelName)
@@ -152,8 +162,22 @@ public class ChimeraManager
 
   public List<ChimeraModel> 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<ChimeraModel> openModel(String modelPath, String modelName,
+          ModelType type)
+  {
     logger.info("chimera open " + modelPath);
-    stopListening();
+    // stopListening();
     List<String> response = null;
     // TODO: [Optional] Handle modbase models
     if (type == ModelType.MODBASE_MODEL)
@@ -190,22 +214,18 @@ public class ChimeraManager
             {
               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);
+
+            //
+            // patch for Jalview - set model name in Chimera
+            //
+            sendChimeraCommand("setattr M name " + modelName + " #"
+                    + modelNumbers[0], false);
+            // end patch for Jalview
+
             modelNumbers = null;
           }
         }
@@ -261,10 +281,38 @@ public class ChimeraManager
     }
 
     sendChimeraCommand("focus", false);
-    startListening();
+    // startListening(); // see ChimeraListener
     return models;
   }
 
+  /**
+   * 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)
   {
     // int model = structure.modelNumber();
@@ -291,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);
   }
 
   /**
@@ -307,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()
@@ -320,7 +391,7 @@ public class ChimeraManager
   {
     chimera = null;
     currentModelsMap.clear();
-    chimeraListenerThreads = null;
+      this.chimeraRestPort = 0;
     structureManager.clearOnChimeraExit();
   }
 
@@ -359,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<String> getSelectedResidueSpecs()
   {
     List<String> selectedResidues = new ArrayList<String>();
@@ -450,14 +527,20 @@ 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;
   }
 
   public boolean launchChimera(List<String> chimeraPaths)
@@ -485,12 +568,11 @@ public class ChimeraManager
         List<String> args = new ArrayList<String>();
         args.add(chimeraPath);
         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,15 +583,14 @@ 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();
+      // startListening(); // later - see ChimeraListener
       return true;
     }
 
@@ -519,6 +600,42 @@ 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));
+    String response = null;
+    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);
+      }
+    } catch (Exception e)
+    {
+      logger.error("Failed to get REST port number from " + response + ": "
+              + e.getMessage());
+    } finally
+    {
+      try
+      {
+        lineReader.close();
+      } catch (IOException e2)
+      {
+      }
+    }
+    return port;
+  }
+
+  /**
    * Determine the color that Chimera is using for this model.
    * 
    * @param model
@@ -533,7 +650,7 @@ public class ChimeraManager
     {
       return null;
     }
-    return ChimUtils.parseModelColor((String) colorLines.get(0));
+    return ChimUtils.parseModelColor(colorLines.get(0));
   }
 
   /**
@@ -628,6 +745,8 @@ public class ChimeraManager
     return values;
   }
 
+  private volatile boolean busy = false;
+
   /**
    * Send a command to Chimera.
    * 
@@ -641,14 +760,95 @@ public class ChimeraManager
    */
   public List<String> sendChimeraCommand(String command, boolean reply)
   {
-    if (!isChimeraLaunched())
+    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);
+      }
 
-    chimeraListenerThreads.clearResponse(command);
+    }
+  }
+
+  /**
+   * Sends the command to Chimera's REST API, and returns any response lines.
+   * 
+   * @param command
+   * @return
+   */
+  protected List<String> 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<NameValuePair> commands = new ArrayList<NameValuePair>(1);
+    commands.add(new BasicNameValuePair("command", command));
+
+    List<String> reply = new ArrayList<String>();
+    BufferedReader response = null;
+    try {
+      response = HttpClientUtils
+              .doHttpUrlPost(restUrl, commands, 100, 2000);
+      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;
+  }
+
+  /**
+   * Send a command to stdin of Chimera process, and optionally read any
+   * responses.
+   * 
+   * @param command
+   * @param readReply
+   * @return
+   */
+  protected List<String> 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 +863,21 @@ public class ChimeraManager
       clearOnChimeraExit();
       return null;
     }
-    if (!reply)
+    if (!readReply)
     {
       return null;
     }
-    return chimeraListenerThreads.getResponse(command);
+    List<String> rsp = chimeraListenerThread.getResponse(command);
+    return rsp;
+  }
+
+  public StructureManager getStructureManager()
+  {
+    return structureManager;
   }
 
+  public boolean isBusy()
+  {
+    return busy;
+  }
 }