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