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