05d5bcc0befbf51376b9d509fdaf74a350068811
[jalview.git] / src / jalview / ext / pymol / PymolManager.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.ext.pymol;
22
23 import java.io.BufferedReader;
24 import java.io.File;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.InputStreamReader;
28 import java.io.PrintWriter;
29 import java.net.HttpURLConnection;
30 import java.net.SocketException;
31 import java.net.URL;
32 import java.nio.file.Paths;
33 import java.util.ArrayList;
34 import java.util.List;
35
36 import jalview.bin.Cache;
37 import jalview.bin.Console;
38 import jalview.gui.Preferences;
39 import jalview.structure.StructureCommand;
40 import jalview.structure.StructureCommandI;
41
42 public class PymolManager
43 {
44   private static final int RPC_REPLY_TIMEOUT_MS = 15000;
45
46   private static final int CONNECTION_TIMEOUT_MS = 100;
47
48   private static final String POST1 = "<methodCall><methodName>";
49
50   private static final String POST2 = "</methodName><params>";
51
52   private static final String POST3 = "</params></methodCall>";
53
54   private Process pymolProcess;
55
56   private int pymolXmlRpcPort;
57
58   /**
59    * Returns a list of paths to try for the PyMOL executable. Any user
60    * preference is placed first, otherwise 'standard' paths depending on the
61    * operating system.
62    * 
63    * @return
64    */
65   public static List<String> getPymolPaths()
66   {
67     return getPymolPaths(System.getProperty("os.name"));
68   }
69
70   /**
71    * Returns a list of paths to try for the PyMOL executable. Any user
72    * preference is placed first, otherwise 'standard' paths depending on the
73    * operating system.
74    * 
75    * @param os
76    *          operating system as reported by environment variable
77    *          {@code os.name}
78    * @return
79    */
80   protected static List<String> getPymolPaths(String os)
81   {
82     List<String> pathList = new ArrayList<>();
83
84     String userPath = Cache.getDefault(Preferences.PYMOL_PATH, null);
85     if (userPath != null)
86     {
87       pathList.add(userPath);
88     }
89
90     /*
91      * add default installation paths
92      */
93     String pymol = "PyMOL";
94     if (os.startsWith("Linux"))
95     {
96       pathList.add("/usr/local/pymol/bin/" + pymol);
97       pathList.add("/usr/local/bin/" + pymol);
98       pathList.add("/usr/bin/" + pymol);
99       pathList.add(System.getProperty("user.home") + "/opt/bin/" + pymol);
100     }
101     else if (os.startsWith("Windows"))
102     {
103       // todo Windows installation path(s)
104     }
105     else if (os.startsWith("Mac"))
106     {
107       pathList.add("/Applications/PyMOL.app/Contents/MacOS/" + pymol);
108     }
109     return pathList;
110   }
111
112   public boolean isPymolLaunched()
113   {
114     // TODO pull up generic methods for external viewer processes
115     boolean launched = false;
116     if (pymolProcess != null)
117     {
118       try
119       {
120         pymolProcess.exitValue();
121         // if we get here, process has ended
122       } catch (IllegalThreadStateException e)
123       {
124         // ok - not yet terminated
125         launched = true;
126       }
127     }
128     return launched;
129   }
130
131   /**
132    * Sends the command to Pymol; if requested, tries to get and return any
133    * replies, else returns null
134    * 
135    * @param command
136    * @param getReply
137    * @return
138    */
139   public List<String> sendCommand(StructureCommandI command,
140           boolean getReply)
141   {
142     String postBody = getPostRequest(command);
143     // System.out.println(postBody);// debug
144     String rpcUrl = "http://127.0.0.1:" + this.pymolXmlRpcPort;
145     PrintWriter out = null;
146     BufferedReader in = null;
147     List<String> result = getReply ? new ArrayList<>() : null;
148
149     try
150     {
151       URL realUrl = new URL(rpcUrl);
152       HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection();
153       conn.setRequestProperty("accept", "*/*");
154       conn.setRequestProperty("content-type", "text/xml");
155       conn.setDoOutput(true);
156       conn.setDoInput(true);
157       out = new PrintWriter(conn.getOutputStream());
158       out.print(postBody);
159       out.flush();
160       int rc = conn.getResponseCode();
161       if (rc != HttpURLConnection.HTTP_OK)
162       {
163         Console.error(
164                 String.format("Error status from %s: %d", rpcUrl, rc));
165         return result;
166       }
167
168       InputStream inputStream = conn.getInputStream();
169       if (getReply)
170       {
171         in = new BufferedReader(new InputStreamReader(inputStream));
172         String line;
173         while ((line = in.readLine()) != null)
174         {
175           result.add(line);
176         }
177       }
178     } catch (SocketException e)
179     {
180       // thrown when 'quit' command is sent to PyMol
181       Console.warn(String.format("Request to %s returned %s", rpcUrl,
182               e.toString()));
183     } catch (Exception e)
184     {
185       e.printStackTrace();
186     } finally
187     {
188       if (out != null)
189       {
190         out.close();
191       }
192       if (Console.isTraceEnabled())
193       {
194         Console.trace("Sent: " + command.toString());
195         if (result != null)
196         {
197           Console.trace("Received: " + result);
198         }
199       }
200     }
201     return result;
202   }
203
204   /**
205    * Builds the body of the XML-RPC format POST request to execute the command
206    * 
207    * @param command
208    * @return
209    */
210   static String getPostRequest(StructureCommandI command)
211   {
212     StringBuilder sb = new StringBuilder(64);
213     sb.append(POST1).append(command.getCommand()).append(POST2);
214     if (command.hasParameters())
215     {
216       for (String p : command.getParameters())
217       {
218         /*
219          * for now assuming all are string - <string> element is optional
220          * refactor in future if other data types needed
221          * https://www.tutorialspoint.com/xml-rpc/xml_rpc_data_model.htm
222          */
223         sb.append("<parameter><value>").append(p)
224                 .append("</value></parameter>");
225       }
226     }
227     sb.append(POST3);
228     return sb.toString();
229   }
230
231   public Process launchPymol()
232   {
233     // todo pull up much of this
234     // Do nothing if already launched
235     if (isPymolLaunched())
236     {
237       return pymolProcess;
238     }
239
240     String error = "Error message: ";
241     for (String pymolPath : getPymolPaths())
242     {
243       try
244       {
245         // ensure symbolic links are resolved
246         pymolPath = Paths.get(pymolPath).toRealPath().toString();
247         File path = new File(pymolPath);
248         // uncomment the next line to simulate Pymol not installed
249         // path = new File(pymolPath + "x");
250         if (!path.canExecute())
251         {
252           error += "File '" + path + "' does not exist.\n";
253           continue;
254         }
255         List<String> args = new ArrayList<>();
256         args.add(pymolPath);
257         args.add("-R"); // https://pymolwiki.org/index.php/RPC
258         ProcessBuilder pb = new ProcessBuilder(args);
259         pymolProcess = pb.start();
260         error = "";
261         break;
262       } catch (Exception e)
263       {
264         // Pymol could not be started using this path
265         error += e.getMessage();
266       }
267     }
268
269     if (pymolProcess != null)
270     {
271       this.pymolXmlRpcPort = getPortNumber();
272       if (pymolXmlRpcPort > 0)
273       {
274         Console.info("PyMOL XMLRPC started on port " + pymolXmlRpcPort);
275       }
276       else
277       {
278         error += "Failed to read PyMOL XMLRPC port number";
279         Console.error(error);
280         pymolProcess.destroy();
281         pymolProcess = null;
282       }
283     }
284
285     return pymolProcess;
286   }
287
288   private int getPortNumber()
289   {
290     // TODO pull up most of this!
291     int port = 0;
292     InputStream readChan = pymolProcess.getInputStream();
293     BufferedReader lineReader = new BufferedReader(
294             new InputStreamReader(readChan));
295     StringBuilder responses = new StringBuilder();
296     try
297     {
298       String response = lineReader.readLine();
299       while (response != null)
300       {
301         responses.append("\n" + response);
302         // expect: xml-rpc server running on host localhost, port 9123
303         if (response.contains("xml-rpc"))
304         {
305           String[] tokens = response.split(" ");
306           for (int i = 0; i < tokens.length - 1; i++)
307           {
308             if ("port".equals(tokens[i]))
309             {
310               port = Integer.parseInt(tokens[i + 1]);
311               break;
312             }
313           }
314         }
315         if (port > 0)
316         {
317           break; // hack for hanging readLine()
318         }
319         response = lineReader.readLine();
320       }
321     } catch (Exception e)
322     {
323       Console.error("Failed to get REST port number from " + responses
324               + ": " + e.getMessage());
325       // logger.error("Failed to get REST port number from " + responses + ": "
326       // + e.getMessage());
327     } finally
328     {
329       try
330       {
331         lineReader.close();
332       } catch (IOException e2)
333       {
334       }
335     }
336     if (port == 0)
337     {
338       Console.error("Failed to start PyMOL with XMLRPC, response was: "
339               + responses);
340     }
341     Console.info("PyMOL started with XMLRPC on port " + port);
342     return port;
343   }
344
345 }