Merge branch 'develop' into features/JAL-2295setChimeraAttributes
[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         chimera.destroy();
384       } catch (Exception ex)
385       {
386         // ignore
387       }
388     }
389     clearOnChimeraExit();
390   }
391
392   public Map<Integer, ChimeraModel> getSelectedModels()
393   {
394     Map<Integer, ChimeraModel> selectedModelsMap = new HashMap<Integer, ChimeraModel>();
395     List<String> chimeraReply = sendChimeraCommand(
396             "list selection level molecule", true);
397     if (chimeraReply != null)
398     {
399       for (String modelLine : chimeraReply)
400       {
401         ChimeraModel chimeraModel = new ChimeraModel(modelLine);
402         Integer modelKey = ChimUtils.makeModelKey(
403                 chimeraModel.getModelNumber(),
404                 chimeraModel.getSubModelNumber());
405         selectedModelsMap.put(modelKey, chimeraModel);
406       }
407     }
408     return selectedModelsMap;
409   }
410
411   /**
412    * Sends a 'list selection level residue' command to Chimera and returns the
413    * list of selected atomspecs
414    * 
415    * @return
416    */
417   public List<String> getSelectedResidueSpecs()
418   {
419     List<String> selectedResidues = new ArrayList<String>();
420     List<String> chimeraReply = sendChimeraCommand(
421             "list selection level residue", true);
422     if (chimeraReply != null)
423     {
424       /*
425        * expect 0, 1 or more lines of the format
426        * residue id #0:43.A type GLY
427        * where we are only interested in the atomspec #0.43.A
428        */
429       for (String inputLine : chimeraReply)
430       {
431         String[] inputLineParts = inputLine.split("\\s+");
432         if (inputLineParts.length == 5)
433         {
434           selectedResidues.add(inputLineParts[2]);
435         }
436       }
437     }
438     return selectedResidues;
439   }
440
441   public void getSelectedResidues(
442           Map<Integer, ChimeraModel> selectedModelsMap)
443   {
444     List<String> chimeraReply = sendChimeraCommand(
445             "list selection level residue", true);
446     if (chimeraReply != null)
447     {
448       for (String inputLine : chimeraReply)
449       {
450         ChimeraResidue r = new ChimeraResidue(inputLine);
451         Integer modelKey = ChimUtils.makeModelKey(r.getModelNumber(),
452                 r.getSubModelNumber());
453         if (selectedModelsMap.containsKey(modelKey))
454         {
455           ChimeraModel model = selectedModelsMap.get(modelKey);
456           model.addResidue(r);
457         }
458       }
459     }
460   }
461
462   /**
463    * Return the list of ChimeraModels currently open. Warning: if smiles model
464    * name too long, only part of it with "..." is printed.
465    * 
466    * 
467    * @return List of ChimeraModel's
468    */
469   // TODO: [Optional] Handle smiles names in a better way in Chimera?
470   public List<ChimeraModel> getModelList()
471   {
472     List<ChimeraModel> modelList = new ArrayList<ChimeraModel>();
473     List<String> list = sendChimeraCommand("list models type molecule",
474             true);
475     if (list != null)
476     {
477       for (String modelLine : list)
478       {
479         ChimeraModel chimeraModel = new ChimeraModel(modelLine);
480         modelList.add(chimeraModel);
481       }
482     }
483     return modelList;
484   }
485
486   /**
487    * Return the list of depiction presets available from within Chimera. Chimera
488    * will return the list as a series of lines with the format: Preset type
489    * number "description"
490    * 
491    * @return list of presets
492    */
493   public List<String> getPresets()
494   {
495     ArrayList<String> presetList = new ArrayList<String>();
496     List<String> output = sendChimeraCommand("preset list", true);
497     if (output != null)
498     {
499       for (String preset : output)
500       {
501         preset = preset.substring(7); // Skip over the "Preset"
502         preset = preset.replaceFirst("\"", "(");
503         preset = preset.replaceFirst("\"", ")");
504         // string now looks like: type number (description)
505         presetList.add(preset);
506       }
507     }
508     return presetList;
509   }
510
511   public boolean isChimeraLaunched()
512   {
513     boolean launched = false;
514     if (chimera != null)
515     {
516       try
517       {
518         chimera.exitValue();
519         // if we get here, process has ended
520       } catch (IllegalThreadStateException e)
521       {
522         // ok - not yet terminated
523         launched = true;
524       }
525     }
526     return launched;
527   }
528
529   /**
530    * Launch Chimera, unless an instance linked to this object is already
531    * running. Returns true if chimera is successfully launched, or already
532    * running, else false.
533    * 
534    * @param chimeraPaths
535    * @return
536    */
537   public boolean launchChimera(List<String> chimeraPaths)
538   {
539     // Do nothing if Chimera is already launched
540     if (isChimeraLaunched())
541     {
542       return true;
543     }
544
545     // Try to launch Chimera (eventually using one of the possible paths)
546     String error = "Error message: ";
547     String workingPath = "";
548     // iterate over possible paths for starting Chimera
549     for (String chimeraPath : chimeraPaths)
550     {
551       File path = new File(chimeraPath);
552       // uncomment the next line to simulate Chimera not installed
553       // path = new File(chimeraPath + "x");
554       if (!path.canExecute())
555       {
556         error += "File '" + path + "' does not exist.\n";
557         continue;
558       }
559       try
560       {
561         List<String> args = new ArrayList<String>();
562         args.add(chimeraPath);
563         // shows Chimera output window but suppresses REST responses:
564         // args.add("--debug");
565         args.add("--start");
566         args.add("RESTServer");
567         ProcessBuilder pb = new ProcessBuilder(args);
568         chimera = pb.start();
569         error = "";
570         workingPath = chimeraPath;
571         break;
572       } catch (Exception e)
573       {
574         // Chimera could not be started
575         error += e.getMessage();
576       }
577     }
578     // If no error, then Chimera was launched successfully
579     if (error.length() == 0)
580     {
581       this.chimeraRestPort = getPortNumber();
582       System.out.println("Chimera REST API started on port "
583               + chimeraRestPort);
584       // structureManager.initChimTable();
585       structureManager.setChimeraPathProperty(workingPath);
586       // TODO: [Optional] Check Chimera version and show a warning if below 1.8
587       // Ask Chimera to give us updates
588       // startListening(); // later - see ChimeraListener
589       return (chimeraRestPort > 0);
590     }
591
592     // Tell the user that Chimera could not be started because of an error
593     logger.warn(error);
594     return false;
595   }
596
597   /**
598    * Read and return the port number returned in the reply to --start RESTServer
599    */
600   private int getPortNumber()
601   {
602     int port = 0;
603     InputStream readChan = chimera.getInputStream();
604     BufferedReader lineReader = new BufferedReader(new InputStreamReader(
605             readChan));
606     StringBuilder responses = new StringBuilder();
607     try
608     {
609       String response = lineReader.readLine();
610       while (response != null)
611       {
612         responses.append("\n" + response);
613         // expect: REST server on host 127.0.0.1 port port_number
614         if (response.startsWith("REST server"))
615         {
616           String[] tokens = response.split(" ");
617           if (tokens.length == 7 && "port".equals(tokens[5]))
618           {
619             port = Integer.parseInt(tokens[6]);
620             break;
621           }
622         }
623         response = lineReader.readLine();
624       }
625     } catch (Exception e)
626     {
627       logger.error("Failed to get REST port number from " + responses
628               + ": " + e.getMessage());
629     } finally
630     {
631       try
632       {
633         lineReader.close();
634       } catch (IOException e2)
635       {
636       }
637     }
638     if (port == 0)
639     {
640       System.err
641               .println("Failed to start Chimera with REST service, response was: "
642                       + responses);
643     }
644     logger.info("Chimera REST service listening on port " + chimeraRestPort);
645     return port;
646   }
647
648   /**
649    * Determine the color that Chimera is using for this model.
650    * 
651    * @param model
652    *          the ChimeraModel we want to get the Color for
653    * @return the default model Color for this model in Chimera
654    */
655   public Color getModelColor(ChimeraModel model)
656   {
657     List<String> colorLines = sendChimeraCommand(
658             "list model spec " + model.toSpec() + " attribute color", true);
659     if (colorLines == null || colorLines.size() == 0)
660     {
661       return null;
662     }
663     return ChimUtils.parseModelColor(colorLines.get(0));
664   }
665
666   /**
667    * 
668    * Get information about the residues associated with a model. This uses the
669    * Chimera listr command. We don't return the resulting residues, but we add
670    * the residues to the model.
671    * 
672    * @param model
673    *          the ChimeraModel to get residue information for
674    * 
675    */
676   public void addResidues(ChimeraModel model)
677   {
678     int modelNumber = model.getModelNumber();
679     int subModelNumber = model.getSubModelNumber();
680     // Get the list -- it will be in the reply log
681     List<String> reply = sendChimeraCommand(
682             "list residues spec " + model.toSpec(), true);
683     if (reply == null)
684     {
685       return;
686     }
687     for (String inputLine : reply)
688     {
689       ChimeraResidue r = new ChimeraResidue(inputLine);
690       if (r.getModelNumber() == modelNumber
691               || r.getSubModelNumber() == subModelNumber)
692       {
693         model.addResidue(r);
694       }
695     }
696   }
697
698   public List<String> getAttrList()
699   {
700     List<String> attributes = new ArrayList<String>();
701     final List<String> reply = sendChimeraCommand("list resattr", true);
702     if (reply != null)
703     {
704       for (String inputLine : reply)
705       {
706         String[] lineParts = inputLine.split("\\s");
707         if (lineParts.length == 2 && lineParts[0].equals("resattr"))
708         {
709           attributes.add(lineParts[1]);
710         }
711       }
712     }
713     return attributes;
714   }
715
716   public Map<ChimeraResidue, Object> getAttrValues(String aCommand,
717           ChimeraModel model)
718   {
719     Map<ChimeraResidue, Object> values = new HashMap<ChimeraResidue, Object>();
720     final List<String> reply = sendChimeraCommand("list residue spec "
721             + model.toSpec() + " attribute " + aCommand, true);
722     if (reply != null)
723     {
724       for (String inputLine : reply)
725       {
726         String[] lineParts = inputLine.split("\\s");
727         if (lineParts.length == 5)
728         {
729           ChimeraResidue residue = ChimUtils
730                   .getResidue(lineParts[2], model);
731           String value = lineParts[4];
732           if (residue != null)
733           {
734             if (value.equals("None"))
735             {
736               continue;
737             }
738             if (value.equals("True") || value.equals("False"))
739             {
740               values.put(residue, Boolean.valueOf(value));
741               continue;
742             }
743             try
744             {
745               Double doubleValue = Double.valueOf(value);
746               values.put(residue, doubleValue);
747             } catch (NumberFormatException ex)
748             {
749               values.put(residue, value);
750             }
751           }
752         }
753       }
754     }
755     return values;
756   }
757
758   private volatile boolean busy = false;
759
760   /**
761    * Send a command to Chimera.
762    * 
763    * @param command
764    *          Command string to be send.
765    * @param reply
766    *          Flag indicating whether the method should return the reply from
767    *          Chimera or not.
768    * @return List of Strings corresponding to the lines in the Chimera reply or
769    *         <code>null</code>.
770    */
771   public List<String> sendChimeraCommand(String command, boolean reply)
772   {
773    // System.out.println("chimeradebug>> " + command);
774     if (!isChimeraLaunched() || command == null
775             || "".equals(command.trim()))
776     {
777       return null;
778     }
779     // TODO do we need a maximum wait time before aborting?
780     while (busy)
781     {
782       try
783       {
784         Thread.sleep(25);
785       } catch (InterruptedException q)
786       {
787       }
788     }
789     busy = true;
790     long startTime = System.currentTimeMillis();
791     try
792     {
793       return sendRestCommand(command);
794     } finally
795     {
796       /*
797        * Make sure busy flag is reset come what may!
798        */
799       busy = false;
800       if (debug)
801       {
802         System.out.println("Chimera command took "
803                 + (System.currentTimeMillis() - startTime) + "ms: "
804                 + command);
805       }
806
807     }
808   }
809
810   /**
811    * Sends the command to Chimera's REST API, and returns any response lines.
812    * 
813    * @param command
814    * @return
815    */
816   protected List<String> sendRestCommand(String command)
817   {
818     String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run";
819     List<NameValuePair> commands = new ArrayList<NameValuePair>(1);
820     commands.add(new BasicNameValuePair("command", command));
821
822     List<String> reply = new ArrayList<String>();
823     BufferedReader response = null;
824     try
825     {
826       response = HttpClientUtils.doHttpUrlPost(restUrl, commands, CONNECTION_TIMEOUT_MS,
827               REST_REPLY_TIMEOUT_MS);
828       String line = "";
829       while ((line = response.readLine()) != null)
830       {
831         reply.add(line);
832       }
833     } catch (Exception e)
834     {
835       logger.error("REST call '" + command + "' failed: " + e.getMessage());
836     } finally
837     {
838       if (response != null)
839       {
840         try
841         {
842           response.close();
843         } catch (IOException e)
844         {
845         }
846       }
847     }
848     return reply;
849   }
850
851   /**
852    * Send a command to stdin of Chimera process, and optionally read any
853    * responses.
854    * 
855    * @param command
856    * @param readReply
857    * @return
858    */
859   protected List<String> sendStdinCommand(String command, boolean readReply)
860   {
861     chimeraListenerThread.clearResponse(command);
862     String text = command.concat("\n");
863     try
864     {
865       // send the command
866       chimera.getOutputStream().write(text.getBytes());
867       chimera.getOutputStream().flush();
868     } catch (IOException e)
869     {
870       // logger.info("Unable to execute command: " + text);
871       // logger.info("Exiting...");
872       logger.warn("Unable to execute command: " + text);
873       logger.warn("Exiting...");
874       clearOnChimeraExit();
875       return null;
876     }
877     if (!readReply)
878     {
879       return null;
880     }
881     List<String> rsp = chimeraListenerThread.getResponse(command);
882     return rsp;
883   }
884
885   public StructureManager getStructureManager()
886   {
887     return structureManager;
888   }
889
890   public boolean isBusy()
891   {
892     return busy;
893   }
894 }