JAL-3551 working proof of concept of Jalview driving PyMOL
[jalview.git] / src / jalview / ext / pymol / PymolManager.java
1 package jalview.ext.pymol;
2
3 import jalview.bin.Cache;
4 import jalview.gui.Preferences;
5 import jalview.structure.StructureCommand;
6 import jalview.structure.StructureCommandI;
7
8 import java.io.BufferedReader;
9 import java.io.File;
10 import java.io.IOException;
11 import java.io.InputStream;
12 import java.io.InputStreamReader;
13 import java.io.PrintWriter;
14 import java.net.URL;
15 import java.net.URLConnection;
16 import java.nio.file.Paths;
17 import java.util.ArrayList;
18 import java.util.List;
19
20 public class PymolManager
21 {
22   private static final int RPC_REPLY_TIMEOUT_MS = 15000;
23
24   private static final int CONNECTION_TIMEOUT_MS = 100;
25
26   private static final String POST1 = "<methodCall><methodName>";
27
28   private static final String POST2 = "</methodName><params>";
29
30   private static final String POST3 = "</params></methodCall>";
31
32   private Process pymolProcess;
33
34   private int pymolXmlRpcPort;
35
36   /**
37    * Returns a list of paths to try for the PyMOL executable. Any user
38    * preference is placed first, otherwise 'standard' paths depending on the
39    * operating system.
40    * 
41    * @return
42    */
43   public static List<String> getPymolPaths()
44   {
45     List<String> pathList = new ArrayList<>();
46   
47     String userPath = Cache
48             .getDefault(Preferences.PYMOL_PATH, null);
49     if (userPath != null)
50     {
51       pathList.add(0, userPath);
52     }
53   
54     /*
55      * add default installation paths
56      */
57     String pymol = "PyMOL";
58     String os = System.getProperty("os.name");
59     if (os.startsWith("Linux"))
60     {
61       pathList.add("/usr/local/pymol/bin/" + pymol);
62       pathList.add("/usr/local/bin/" + pymol);
63       pathList.add("/usr/bin/" + pymol);
64       pathList.add(System.getProperty("user.home") + "/opt/bin/" + pymol);
65     }
66     else if (os.startsWith("Windows"))
67     {
68       // todo Windows installation path(s)
69     }
70     else if (os.startsWith("Mac"))
71     {
72       pathList.add("/Applications/PyMOL.app/Contents/MacOS/" + pymol);
73     }
74     return pathList;
75   }
76
77   public boolean isPymolLaunched()
78   {
79     // TODO pull up generic methods for external viewer processes
80     boolean launched = false;
81     if (pymolProcess != null)
82     {
83       try
84       {
85         pymolProcess.exitValue();
86         // if we get here, process has ended
87       } catch (IllegalThreadStateException e)
88       {
89         // ok - not yet terminated
90         launched = true;
91       }
92     }
93     return launched;
94   }
95
96   public void exitPymol()
97   {
98     if (isPymolLaunched() && pymolProcess != null)
99     {
100       sendCommand(new StructureCommand("quit"), false);
101     }
102     pymolProcess = null;
103     // currentModelsMap.clear();
104     this.pymolXmlRpcPort = 0;
105   }
106
107   /**
108    * Sends the command to Pymol; if requested, tries to get and return any
109    * replies, else returns null
110    * 
111    * @param command
112    * @param getReply
113    * @return
114    */
115   public List<String> sendCommand(StructureCommandI command,
116           boolean getReply)
117   {
118     String postBody = getPostRequest(command);
119     // System.out.println(postBody);// debug
120     String rpcUrl = "http://127.0.0.1:" + this.pymolXmlRpcPort;
121     PrintWriter out = null;
122     BufferedReader in = null;
123     List<String> result = new ArrayList<>();
124     try
125     {
126       URL realUrl = new URL(rpcUrl);
127       URLConnection conn = realUrl.openConnection();
128       conn.setRequestProperty("accept", "*/*");
129       conn.setRequestProperty("content-type", "text/xml");
130       conn.setDoOutput(true);
131       conn.setDoInput(true);
132       out = new PrintWriter(conn.getOutputStream());
133       out.print(postBody);
134       out.flush();
135       in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
136       String line;
137       while ((line = in.readLine()) != null)
138       {
139         result.add(line);
140       }
141     } catch (Exception e)
142     {
143       e.printStackTrace();
144     } finally
145     {
146       try
147       {
148         if (out != null)
149         {
150           out.close();
151         }
152         if (in != null)
153         {
154           in.close();
155         }
156       } catch (IOException ex)
157       {
158         ex.printStackTrace();
159         }
160     }
161     return result;
162   }
163
164   /**
165    * Builds the body of the XML-RPC format POST request to execute the command
166    * 
167    * @param command
168    * @return
169    */
170   static String getPostRequest(StructureCommandI command)
171   {
172     StringBuilder sb = new StringBuilder(64);
173     sb.append(POST1).append(command.getCommand()).append(POST2);
174     if (command.hasParameters())
175     {
176       for (String p : command.getParameters())
177       {
178         /*
179          * for now assuming all are string - <string> element is optional
180          * refactor in future if other data types needed
181          * https://www.tutorialspoint.com/xml-rpc/xml_rpc_data_model.htm
182          */
183         sb.append("<parameter><value>").append(p)
184                 .append("</value></parameter>");
185       }
186     }
187     sb.append(POST3);
188     return sb.toString();
189   }
190
191   public boolean launchPymol()
192   {
193     // todo pull up much of this
194     // Do nothing if already launched
195     if (isPymolLaunched())
196     {
197       return true;
198     }
199
200     String error = "Error message: ";
201     for (String pymolPath : getPymolPaths())
202     {
203       try
204       {
205         // ensure symbolic links are resolved
206         pymolPath = Paths.get(pymolPath).toRealPath().toString();
207         File path = new File(pymolPath);
208         // uncomment the next line to simulate Pymol not installed
209         // path = new File(pymolPath + "x");
210         if (!path.canExecute())
211         {
212           error += "File '" + path + "' does not exist.\n";
213           continue;
214         }
215         List<String> args = new ArrayList<>();
216         args.add(pymolPath);
217         args.add("-R"); // https://pymolwiki.org/index.php/RPC
218         ProcessBuilder pb = new ProcessBuilder(args);
219         pymolProcess = pb.start();
220         error = "";
221         break;
222       } catch (Exception e)
223       {
224         // pPymol could not be started using this path
225         error += e.getMessage();
226       }
227     }
228     if (error.length() == 0)
229     {
230       this.pymolXmlRpcPort = getPortNumber();
231       System.out.println(
232               "PyMOL XMLRPC started on port " + pymolXmlRpcPort);
233       return (pymolXmlRpcPort > 0);
234     }
235
236     // logger.warn(error);
237     return false;
238   }
239
240   private int getPortNumber()
241   {
242     // TODO pull up most of this!
243     int port = 0;
244     InputStream readChan = pymolProcess.getInputStream();
245     BufferedReader lineReader = new BufferedReader(
246             new InputStreamReader(readChan));
247     StringBuilder responses = new StringBuilder();
248     try
249     {
250       String response = lineReader.readLine();
251       while (response != null)
252       {
253         responses.append("\n" + response);
254         // expect: xml-rpc server running on host localhost, port 9123
255         if (response.contains("xml-rpc"))
256         {
257           String[] tokens = response.split(" ");
258           for (int i = 0; i < tokens.length - 1; i++)
259           {
260             if ("port".equals(tokens[i]))
261             {
262               port = Integer.parseInt(tokens[i + 1]);
263               break;
264             }
265           }
266         }
267         if (port > 0)
268         {
269           break; // hack for hanging readLine()
270         }
271         response = lineReader.readLine();
272       }
273     } catch (Exception e)
274     {
275       System.err.println(
276               "Failed to get REST port number from " + responses + ": "
277               + e.getMessage());
278       // logger.error("Failed to get REST port number from " + responses + ": "
279       // + e.getMessage());
280     } finally
281     {
282       try
283       {
284         lineReader.close();
285       } catch (IOException e2)
286       {
287       }
288     }
289     if (port == 0)
290     {
291       System.err.println("Failed to start PyMOL with XMLRPC, response was: "
292               + responses);
293     }
294     System.err.println("PyMOL started with XMLRPC on port " + port);
295     return port;
296   }
297
298 }