Merge branch 'features/JAL-2320_closeChimeraAction' into releases/Release_2_10_Branch
[jalview.git] / src / ext / edu / ucsf / rbvi / strucviz2 / ChimeraManager.java
1 /* vim: set ts=2: */
2 /**
3  * Copyright (c) 2006 The Regents of the University of California.
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  *   1. Redistributions of source code must retain the above copyright
10  *      notice, this list of conditions, and the following disclaimer.
11  *   2. Redistributions in binary form must reproduce the above
12  *      copyright notice, this list of conditions, and the following
13  *      disclaimer in the documentation and/or other materials provided
14  *      with the distribution.
15  *   3. Redistributions must acknowledge that this software was
16  *      originally developed by the UCSF Computer Graphics Laboratory
17  *      under support by the NIH National Center for Research Resources,
18  *      grant P41-RR01081.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY
21  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS BE LIABLE
24  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
26  * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
27  * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
28  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
29  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
30  * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  *
32  */
33 package ext.edu.ucsf.rbvi.strucviz2;
34
35 import jalview.ws.HttpClientUtils;
36
37 import java.awt.Color;
38 import java.io.BufferedReader;
39 import java.io.File;
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.io.InputStreamReader;
43 import java.util.ArrayList;
44 import java.util.Collection;
45 import java.util.HashMap;
46 import java.util.List;
47 import java.util.Map;
48
49 import org.apache.http.NameValuePair;
50 import org.apache.http.message.BasicNameValuePair;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
55 import ext.edu.ucsf.rbvi.strucviz2.port.ListenerThreads;
56
57 /**
58  * This object maintains the Chimera communication information.
59  */
60 public class ChimeraManager
61 {
62   private static final int REST_REPLY_TIMEOUT_MS = 15000;
63
64   private static final int CONNECTION_TIMEOUT_MS = 100;
65
66   private static final boolean debug = false;
67
68   private int chimeraRestPort;
69
70   private Process chimera;
71
72   private ListenerThreads chimeraListenerThread;
73
74   private Map<Integer, ChimeraModel> currentModelsMap;
75
76   private Logger logger = LoggerFactory
77           .getLogger(ext.edu.ucsf.rbvi.strucviz2.ChimeraManager.class);
78
79   private StructureManager structureManager;
80
81   public ChimeraManager(StructureManager structureManager)
82   {
83     this.structureManager = structureManager;
84     chimera = null;
85     chimeraListenerThread = null;
86     currentModelsMap = new HashMap<Integer, ChimeraModel>();
87
88   }
89
90   public List<ChimeraModel> getChimeraModels(String modelName)
91   {
92     List<ChimeraModel> models = getChimeraModels(modelName,
93             ModelType.PDB_MODEL);
94     models.addAll(getChimeraModels(modelName, ModelType.SMILES));
95     return models;
96   }
97
98   public List<ChimeraModel> getChimeraModels(String modelName,
99           ModelType modelType)
100   {
101     List<ChimeraModel> models = new ArrayList<ChimeraModel>();
102     for (ChimeraModel model : currentModelsMap.values())
103     {
104       if (modelName.equals(model.getModelName())
105               && modelType.equals(model.getModelType()))
106       {
107         models.add(model);
108       }
109     }
110     return models;
111   }
112
113   public Map<String, List<ChimeraModel>> getChimeraModelsMap()
114   {
115     Map<String, List<ChimeraModel>> models = new HashMap<String, List<ChimeraModel>>();
116     for (ChimeraModel model : currentModelsMap.values())
117     {
118       String modelName = model.getModelName();
119       if (!models.containsKey(modelName))
120       {
121         models.put(modelName, new ArrayList<ChimeraModel>());
122       }
123       if (!models.get(modelName).contains(model))
124       {
125         models.get(modelName).add(model);
126       }
127     }
128     return models;
129   }
130
131   public ChimeraModel getChimeraModel(Integer modelNumber,
132           Integer subModelNumber)
133   {
134     Integer key = ChimUtils.makeModelKey(modelNumber, subModelNumber);
135     if (currentModelsMap.containsKey(key))
136     {
137       return currentModelsMap.get(key);
138     }
139     return null;
140   }
141
142   public ChimeraModel getChimeraModel()
143   {
144     return currentModelsMap.values().iterator().next();
145   }
146
147   public Collection<ChimeraModel> getChimeraModels()
148   {
149     // this method is invoked by the model navigator dialog
150     return currentModelsMap.values();
151   }
152
153   public int getChimeraModelsCount(boolean smiles)
154   {
155     // this method is invokes by the model navigator dialog
156     int counter = currentModelsMap.size();
157     if (smiles)
158     {
159       return counter;
160     }
161
162     for (ChimeraModel model : currentModelsMap.values())
163     {
164       if (model.getModelType() == ModelType.SMILES)
165       {
166         counter--;
167       }
168     }
169     return counter;
170   }
171
172   public boolean hasChimeraModel(Integer modelNubmer)
173   {
174     return hasChimeraModel(modelNubmer, 0);
175   }
176
177   public boolean hasChimeraModel(Integer modelNubmer, Integer subModelNumber)
178   {
179     return currentModelsMap.containsKey(ChimUtils.makeModelKey(modelNubmer,
180             subModelNumber));
181   }
182
183   public void addChimeraModel(Integer modelNumber, Integer subModelNumber,
184           ChimeraModel model)
185   {
186     currentModelsMap.put(
187             ChimUtils.makeModelKey(modelNumber, subModelNumber), model);
188   }
189
190   public void removeChimeraModel(Integer modelNumber, Integer subModelNumber)
191   {
192     int modelKey = ChimUtils.makeModelKey(modelNumber, subModelNumber);
193     if (currentModelsMap.containsKey(modelKey))
194     {
195       currentModelsMap.remove(modelKey);
196     }
197   }
198
199   public List<ChimeraModel> openModel(String modelPath, ModelType type)
200   {
201     return openModel(modelPath, getFileNameFromPath(modelPath), type);
202   }
203
204   /**
205    * Overloaded method to allow Jalview to pass in a model name.
206    * 
207    * @param modelPath
208    * @param modelName
209    * @param type
210    * @return
211    */
212   public List<ChimeraModel> openModel(String modelPath, String modelName,
213           ModelType type)
214   {
215     logger.info("chimera open " + modelPath);
216     // stopListening();
217     List<ChimeraModel> modelList = getModelList();
218     List<String> response = null;
219     // TODO: [Optional] Handle modbase models
220     if (type == ModelType.MODBASE_MODEL)
221     {
222       response = sendChimeraCommand("open modbase:" + modelPath, true);
223       // } else if (type == ModelType.SMILES) {
224       // response = sendChimeraCommand("open smiles:" + modelName, true);
225       // modelName = "smiles:" + modelName;
226     }
227     else
228     {
229       response = sendChimeraCommand("open " + modelPath, true);
230     }
231     if (response == null)
232     {
233       // something went wrong
234       logger.warn("Could not open " + modelPath);
235       return null;
236     }
237
238     // patch for Jalview - set model name in Chimera
239     // TODO: find a variant that works for sub-models
240     for (ChimeraModel newModel : getModelList())
241     {
242       if (!modelList.contains(newModel))
243       {
244         newModel.setModelName(modelName);
245         sendChimeraCommand(
246                 "setattr M name " + modelName + " #"
247                         + newModel.getModelNumber(), false);
248         modelList.add(newModel);
249       }
250     }
251
252     // assign color and residues to open models
253     for (ChimeraModel chimeraModel : modelList)
254     {
255       // get model color
256       Color modelColor = getModelColor(chimeraModel);
257       if (modelColor != null)
258       {
259         chimeraModel.setModelColor(modelColor);
260       }
261
262       // Get our properties (default color scheme, etc.)
263       // Make the molecule look decent
264       // chimeraSend("repr stick "+newModel.toSpec());
265
266       // Create the information we need for the navigator
267       if (type != ModelType.SMILES)
268       {
269         addResidues(chimeraModel);
270       }
271     }
272
273     sendChimeraCommand("focus", false);
274     // startListening(); // see ChimeraListener
275     return modelList;
276   }
277
278   /**
279    * Refactored method to extract the last (or only) element delimited by file
280    * path separator.
281    * 
282    * @param modelPath
283    * @return
284    */
285   private String getFileNameFromPath(String modelPath)
286   {
287     String modelName = modelPath;
288     if (modelPath == null)
289     {
290       return null;
291     }
292     // TODO: [Optional] Convert path to name in a better way
293     if (modelPath.lastIndexOf(File.separator) > 0)
294     {
295       modelName = modelPath
296               .substring(modelPath.lastIndexOf(File.separator) + 1);
297     }
298     else if (modelPath.lastIndexOf("/") > 0)
299     {
300       modelName = modelPath.substring(modelPath.lastIndexOf("/") + 1);
301     }
302     return modelName;
303   }
304
305   public void closeModel(ChimeraModel model)
306   {
307     // int model = structure.modelNumber();
308     // int subModel = structure.subModelNumber();
309     // Integer modelKey = makeModelKey(model, subModel);
310     stopListening();
311     logger.info("chimera close model " + model.getModelName());
312     if (currentModelsMap.containsKey(ChimUtils.makeModelKey(
313             model.getModelNumber(), model.getSubModelNumber())))
314     {
315       sendChimeraCommand("close " + model.toSpec(), false);
316       // currentModelNamesMap.remove(model.getModelName());
317       currentModelsMap.remove(ChimUtils.makeModelKey(
318               model.getModelNumber(), model.getSubModelNumber()));
319       // selectionList.remove(chimeraModel);
320     }
321     else
322     {
323       logger.warn("Could not find model " + model.getModelName()
324               + " to close.");
325     }
326     startListening();
327   }
328
329   public void startListening()
330   {
331     sendChimeraCommand("listen start models; listen start selection", false);
332   }
333
334   public void stopListening()
335   {
336     sendChimeraCommand("listen stop models ; listen stop selection ", false);
337   }
338
339   /**
340    * Tell Chimera we are listening on the given URI
341    * 
342    * @param uri
343    */
344   public void startListening(String uri)
345   {
346     sendChimeraCommand("listen start models url " + uri
347             + ";listen start select prefix SelectionChanged url " + uri,
348             false);
349   }
350
351   /**
352    * Select something in Chimera
353    * 
354    * @param command
355    *          the selection command to pass to Chimera
356    */
357   public void select(String command)
358   {
359     sendChimeraCommand("listen stop selection; " + command
360             + "; listen start selection", false);
361   }
362
363   public void focus()
364   {
365     sendChimeraCommand("focus", false);
366   }
367
368   public void clearOnChimeraExit()
369   {
370     chimera = null;
371     currentModelsMap.clear();
372     this.chimeraRestPort = 0;
373     structureManager.clearOnChimeraExit();
374   }
375
376   public void exitChimera()
377   {
378     if (isChimeraLaunched() && chimera != null)
379     {
380       sendChimeraCommand("stop really", false);
381       try
382       {
383         // TODO is this too violent? could it force close the process
384         // before it has done an orderly shutdown?
385         chimera.destroy();
386       } catch (Exception ex)
387       {
388         // ignore
389       }
390     }
391     clearOnChimeraExit();
392   }
393
394   public Map<Integer, ChimeraModel> getSelectedModels()
395   {
396     Map<Integer, ChimeraModel> selectedModelsMap = new HashMap<Integer, ChimeraModel>();
397     List<String> chimeraReply = sendChimeraCommand(
398             "list selection level molecule", true);
399     if (chimeraReply != null)
400     {
401       for (String modelLine : chimeraReply)
402       {
403         ChimeraModel chimeraModel = new ChimeraModel(modelLine);
404         Integer modelKey = ChimUtils.makeModelKey(
405                 chimeraModel.getModelNumber(),
406                 chimeraModel.getSubModelNumber());
407         selectedModelsMap.put(modelKey, chimeraModel);
408       }
409     }
410     return selectedModelsMap;
411   }
412
413   /**
414    * Sends a 'list selection level residue' command to Chimera and returns the
415    * list of selected atomspecs
416    * 
417    * @return
418    */
419   public List<String> getSelectedResidueSpecs()
420   {
421     List<String> selectedResidues = new ArrayList<String>();
422     List<String> chimeraReply = sendChimeraCommand(
423             "list selection level residue", true);
424     if (chimeraReply != null)
425     {
426       for (String inputLine : chimeraReply)
427       {
428         String[] inputLineParts = inputLine.split("\\s+");
429         if (inputLineParts.length == 5)
430         {
431           selectedResidues.add(inputLineParts[2]);
432         }
433       }
434     }
435     return selectedResidues;
436   }
437
438   public void getSelectedResidues(
439           Map<Integer, ChimeraModel> selectedModelsMap)
440   {
441     List<String> chimeraReply = sendChimeraCommand(
442             "list selection level residue", true);
443     if (chimeraReply != null)
444     {
445       for (String inputLine : chimeraReply)
446       {
447         ChimeraResidue r = new ChimeraResidue(inputLine);
448         Integer modelKey = ChimUtils.makeModelKey(r.getModelNumber(),
449                 r.getSubModelNumber());
450         if (selectedModelsMap.containsKey(modelKey))
451         {
452           ChimeraModel model = selectedModelsMap.get(modelKey);
453           model.addResidue(r);
454         }
455       }
456     }
457   }
458
459   /**
460    * Return the list of ChimeraModels currently open. Warning: if smiles model
461    * name too long, only part of it with "..." is printed.
462    * 
463    * 
464    * @return List of ChimeraModel's
465    */
466   // TODO: [Optional] Handle smiles names in a better way in Chimera?
467   public List<ChimeraModel> getModelList()
468   {
469     List<ChimeraModel> modelList = new ArrayList<ChimeraModel>();
470     List<String> list = sendChimeraCommand("list models type molecule",
471             true);
472     if (list != null)
473     {
474       for (String modelLine : list)
475       {
476         ChimeraModel chimeraModel = new ChimeraModel(modelLine);
477         modelList.add(chimeraModel);
478       }
479     }
480     return modelList;
481   }
482
483   /**
484    * Return the list of depiction presets available from within Chimera. Chimera
485    * will return the list as a series of lines with the format: Preset type
486    * number "description"
487    * 
488    * @return list of presets
489    */
490   public List<String> getPresets()
491   {
492     ArrayList<String> presetList = new ArrayList<String>();
493     List<String> output = sendChimeraCommand("preset list", true);
494     if (output != null)
495     {
496       for (String preset : output)
497       {
498         preset = preset.substring(7); // Skip over the "Preset"
499         preset = preset.replaceFirst("\"", "(");
500         preset = preset.replaceFirst("\"", ")");
501         // string now looks like: type number (description)
502         presetList.add(preset);
503       }
504     }
505     return presetList;
506   }
507
508   public boolean isChimeraLaunched()
509   {
510     boolean launched = false;
511     if (chimera != null)
512     {
513       try
514       {
515         chimera.exitValue();
516         // if we get here, process has ended
517       } catch (IllegalThreadStateException e)
518       {
519         // ok - not yet terminated
520         launched = true;
521       }
522     }
523     return launched;
524   }
525
526   /**
527    * Launch Chimera, unless an instance linked to this object is already
528    * running. Returns true if chimera is successfully launched, or already
529    * running, else false.
530    * 
531    * @param chimeraPaths
532    * @return
533    */
534   public boolean launchChimera(List<String> chimeraPaths)
535   {
536     // Do nothing if Chimera is already launched
537     if (isChimeraLaunched())
538     {
539       return true;
540     }
541
542     // Try to launch Chimera (eventually using one of the possible paths)
543     String error = "Error message: ";
544     String workingPath = "";
545     // iterate over possible paths for starting Chimera
546     for (String chimeraPath : chimeraPaths)
547     {
548       File path = new File(chimeraPath);
549       // uncomment the next line to simulate Chimera not installed
550       // path = new File(chimeraPath + "x");
551       if (!path.canExecute())
552       {
553         error += "File '" + path + "' does not exist.\n";
554         continue;
555       }
556       try
557       {
558         List<String> args = new ArrayList<String>();
559         args.add(chimeraPath);
560         // shows Chimera output window but suppresses REST responses:
561         // args.add("--debug");
562         args.add("--start");
563         args.add("RESTServer");
564         ProcessBuilder pb = new ProcessBuilder(args);
565         chimera = pb.start();
566         error = "";
567         workingPath = chimeraPath;
568         break;
569       } catch (Exception e)
570       {
571         // Chimera could not be started
572         error += e.getMessage();
573       }
574     }
575     // If no error, then Chimera was launched successfully
576     if (error.length() == 0)
577     {
578       this.chimeraRestPort = getPortNumber();
579       System.out.println("Chimera REST API started on port "
580               + chimeraRestPort);
581       // structureManager.initChimTable();
582       structureManager.setChimeraPathProperty(workingPath);
583       // TODO: [Optional] Check Chimera version and show a warning if below 1.8
584       // Ask Chimera to give us updates
585       // startListening(); // later - see ChimeraListener
586       return (chimeraRestPort > 0);
587     }
588
589     // Tell the user that Chimera could not be started because of an error
590     logger.warn(error);
591     return false;
592   }
593
594   /**
595    * Read and return the port number returned in the reply to --start RESTServer
596    */
597   private int getPortNumber()
598   {
599     int port = 0;
600     InputStream readChan = chimera.getInputStream();
601     BufferedReader lineReader = new BufferedReader(new InputStreamReader(
602             readChan));
603     StringBuilder responses = new StringBuilder();
604     try
605     {
606       String response = lineReader.readLine();
607       while (response != null)
608       {
609         responses.append("\n" + response);
610         // expect: REST server on host 127.0.0.1 port port_number
611         if (response.startsWith("REST server"))
612         {
613           String[] tokens = response.split(" ");
614           if (tokens.length == 7 && "port".equals(tokens[5]))
615           {
616             port = Integer.parseInt(tokens[6]);
617             break;
618           }
619         }
620         response = lineReader.readLine();
621       }
622     } catch (Exception e)
623     {
624       logger.error("Failed to get REST port number from " + responses
625               + ": " + e.getMessage());
626     } finally
627     {
628       try
629       {
630         lineReader.close();
631       } catch (IOException e2)
632       {
633       }
634     }
635     if (port == 0)
636     {
637       System.err
638               .println("Failed to start Chimera with REST service, response was: "
639                       + responses);
640     }
641     logger.info("Chimera REST service listening on port " + chimeraRestPort);
642     return port;
643   }
644
645   /**
646    * Determine the color that Chimera is using for this model.
647    * 
648    * @param model
649    *          the ChimeraModel we want to get the Color for
650    * @return the default model Color for this model in Chimera
651    */
652   public Color getModelColor(ChimeraModel model)
653   {
654     List<String> colorLines = sendChimeraCommand(
655             "list model spec " + model.toSpec() + " attribute color", true);
656     if (colorLines == null || colorLines.size() == 0)
657     {
658       return null;
659     }
660     return ChimUtils.parseModelColor(colorLines.get(0));
661   }
662
663   /**
664    * 
665    * Get information about the residues associated with a model. This uses the
666    * Chimera listr command. We don't return the resulting residues, but we add
667    * the residues to the model.
668    * 
669    * @param model
670    *          the ChimeraModel to get residue information for
671    * 
672    */
673   public void addResidues(ChimeraModel model)
674   {
675     int modelNumber = model.getModelNumber();
676     int subModelNumber = model.getSubModelNumber();
677     // Get the list -- it will be in the reply log
678     List<String> reply = sendChimeraCommand(
679             "list residues spec " + model.toSpec(), true);
680     if (reply == null)
681     {
682       return;
683     }
684     for (String inputLine : reply)
685     {
686       ChimeraResidue r = new ChimeraResidue(inputLine);
687       if (r.getModelNumber() == modelNumber
688               || r.getSubModelNumber() == subModelNumber)
689       {
690         model.addResidue(r);
691       }
692     }
693   }
694
695   public List<String> getAttrList()
696   {
697     List<String> attributes = new ArrayList<String>();
698     final List<String> reply = sendChimeraCommand("list resattr", true);
699     if (reply != null)
700     {
701       for (String inputLine : reply)
702       {
703         String[] lineParts = inputLine.split("\\s");
704         if (lineParts.length == 2 && lineParts[0].equals("resattr"))
705         {
706           attributes.add(lineParts[1]);
707         }
708       }
709     }
710     return attributes;
711   }
712
713   public Map<ChimeraResidue, Object> getAttrValues(String aCommand,
714           ChimeraModel model)
715   {
716     Map<ChimeraResidue, Object> values = new HashMap<ChimeraResidue, Object>();
717     final List<String> reply = sendChimeraCommand("list residue spec "
718             + model.toSpec() + " attribute " + aCommand, true);
719     if (reply != null)
720     {
721       for (String inputLine : reply)
722       {
723         String[] lineParts = inputLine.split("\\s");
724         if (lineParts.length == 5)
725         {
726           ChimeraResidue residue = ChimUtils
727                   .getResidue(lineParts[2], model);
728           String value = lineParts[4];
729           if (residue != null)
730           {
731             if (value.equals("None"))
732             {
733               continue;
734             }
735             if (value.equals("True") || value.equals("False"))
736             {
737               values.put(residue, Boolean.valueOf(value));
738               continue;
739             }
740             try
741             {
742               Double doubleValue = Double.valueOf(value);
743               values.put(residue, doubleValue);
744             } catch (NumberFormatException ex)
745             {
746               values.put(residue, value);
747             }
748           }
749         }
750       }
751     }
752     return values;
753   }
754
755   private volatile boolean busy = false;
756
757   /**
758    * Send a command to Chimera.
759    * 
760    * @param command
761    *          Command string to be send.
762    * @param reply
763    *          Flag indicating whether the method should return the reply from
764    *          Chimera or not.
765    * @return List of Strings corresponding to the lines in the Chimera reply or
766    *         <code>null</code>.
767    */
768   public List<String> sendChimeraCommand(String command, boolean reply)
769   {
770    // System.out.println("chimeradebug>> " + command);
771     if (!isChimeraLaunched() || command == null
772             || "".equals(command.trim()))
773     {
774       return null;
775     }
776     // TODO do we need a maximum wait time before aborting?
777     while (busy)
778     {
779       try
780       {
781         Thread.sleep(25);
782       } catch (InterruptedException q)
783       {
784       }
785     }
786     busy = true;
787     long startTime = System.currentTimeMillis();
788     try
789     {
790       return sendRestCommand(command);
791     } finally
792     {
793       /*
794        * Make sure busy flag is reset come what may!
795        */
796       busy = false;
797       if (debug)
798       {
799         System.out.println("Chimera command took "
800                 + (System.currentTimeMillis() - startTime) + "ms: "
801                 + command);
802       }
803
804     }
805   }
806
807   /**
808    * Sends the command to Chimera's REST API, and returns any response lines.
809    * 
810    * @param command
811    * @return
812    */
813   protected List<String> sendRestCommand(String command)
814   {
815     String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run";
816     List<NameValuePair> commands = new ArrayList<NameValuePair>(1);
817     commands.add(new BasicNameValuePair("command", command));
818
819     List<String> reply = new ArrayList<String>();
820     BufferedReader response = null;
821     try
822     {
823       response = HttpClientUtils.doHttpUrlPost(restUrl, commands, CONNECTION_TIMEOUT_MS,
824               REST_REPLY_TIMEOUT_MS);
825       String line = "";
826       while ((line = response.readLine()) != null)
827       {
828         reply.add(line);
829       }
830     } catch (Exception e)
831     {
832       logger.error("REST call '" + command + "' failed: " + e.getMessage());
833     } finally
834     {
835       if (response != null)
836       {
837         try
838         {
839           response.close();
840         } catch (IOException e)
841         {
842         }
843       }
844     }
845     return reply;
846   }
847
848   /**
849    * Send a command to stdin of Chimera process, and optionally read any
850    * responses.
851    * 
852    * @param command
853    * @param readReply
854    * @return
855    */
856   protected List<String> sendStdinCommand(String command, boolean readReply)
857   {
858     chimeraListenerThread.clearResponse(command);
859     String text = command.concat("\n");
860     try
861     {
862       // send the command
863       chimera.getOutputStream().write(text.getBytes());
864       chimera.getOutputStream().flush();
865     } catch (IOException e)
866     {
867       // logger.info("Unable to execute command: " + text);
868       // logger.info("Exiting...");
869       logger.warn("Unable to execute command: " + text);
870       logger.warn("Exiting...");
871       clearOnChimeraExit();
872       return null;
873     }
874     if (!readReply)
875     {
876       return null;
877     }
878     List<String> rsp = chimeraListenerThread.getResponse(command);
879     return rsp;
880   }
881
882   public StructureManager getStructureManager()
883   {
884     return structureManager;
885   }
886
887   public boolean isBusy()
888   {
889     return busy;
890   }
891
892   public Process getChimeraProcess()
893   {
894     return chimera;
895   }
896 }