31f5dc86ecf61ab733f99171f302a40ead76d0ca
[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 java.awt.Color;
36 import java.io.BufferedReader;
37 import java.io.File;
38 import java.io.IOException;
39 import java.io.InputStream;
40 import java.io.InputStreamReader;
41 import java.nio.file.Paths;
42 import java.util.ArrayList;
43 import java.util.Collection;
44 import java.util.HashMap;
45 import java.util.List;
46 import java.util.Map;
47
48 import org.apache.http.NameValuePair;
49 import org.apache.http.message.BasicNameValuePair;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
54 import ext.edu.ucsf.rbvi.strucviz2.port.ListenerThreads;
55 import jalview.ws.HttpClientUtils;
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<>();
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<>();
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<>();
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 = isChimeraX() ? null : 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 && !isChimeraX())
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     // TODO send this command when viewer connection is closed in Jalview
337     String command = isChimeraX
338             ? "info notify stop models jalview; info notify stop selection jalview"
339             : "listen stop models ; listen stop selection ";
340     sendChimeraCommand(command, false);
341   }
342
343   /**
344    * Tell Chimera we are listening on the given URI
345    * 
346    * @param uri
347    */
348   public void startListening(String uri)
349   {
350     /*
351      * listen for model changes
352      */
353     String command = isChimeraX
354             ? ("info notify start models prefix ModelChanged jalview url "
355                     + uri)
356             : ("listen start models url " + uri);
357     sendChimeraCommand(command, false);
358
359     /*
360      * listen for selection changes
361      */
362     command = isChimeraX
363             ? ("info notify start selection jalview prefix SelectionChanged url "
364                     + uri)
365             : ("listen start select prefix SelectionChanged url " + uri);
366     sendChimeraCommand(command, false);
367   }
368
369   /**
370    * Select something in Chimera
371    * 
372    * @param command
373    *          the selection command to pass to Chimera
374    */
375   public void select(String command)
376   {
377     sendChimeraCommand("listen stop selection; " + command
378             + "; listen start selection", false);
379   }
380
381   public void focus()
382   {
383     sendChimeraCommand("focus", false);
384   }
385
386   public void clearOnChimeraExit()
387   {
388     chimera = null;
389     currentModelsMap.clear();
390     this.chimeraRestPort = 0;
391     structureManager.clearOnChimeraExit();
392   }
393
394   public void exitChimera()
395   {
396     if (isChimeraLaunched() && chimera != null)
397     {
398       sendChimeraCommand("stop really", false);
399       try
400       {
401         // TODO is this too violent? could it force close the process
402         // before it has done an orderly shutdown?
403         chimera.destroy();
404       } catch (Exception ex)
405       {
406         // ignore
407       }
408     }
409     clearOnChimeraExit();
410   }
411
412   public Map<Integer, ChimeraModel> getSelectedModels()
413   {
414     Map<Integer, ChimeraModel> selectedModelsMap = new HashMap<>();
415     List<String> chimeraReply = sendChimeraCommand(
416             "list selection level molecule", true);
417     if (chimeraReply != null)
418     {
419       for (String modelLine : chimeraReply)
420       {
421         ChimeraModel chimeraModel = new ChimeraModel(modelLine);
422         Integer modelKey = ChimUtils.makeModelKey(
423                 chimeraModel.getModelNumber(),
424                 chimeraModel.getSubModelNumber());
425         selectedModelsMap.put(modelKey, chimeraModel);
426       }
427     }
428     return selectedModelsMap;
429   }
430
431   /**
432    * Sends a 'list selection level residue' command to Chimera and returns the
433    * list of selected atomspecs
434    * 
435    * @return
436    */
437   public List<String> getSelectedResidueSpecs()
438   {
439     List<String> selectedResidues = new ArrayList<>();
440
441     // in fact 'listinfo' (undocumented) works in ChimeraX
442     String command = (isChimeraX
443             ? "info"
444             : "list") + " selection level residue";
445     List<String> chimeraReply = sendChimeraCommand(command, true);
446     if (chimeraReply != null)
447     {
448       /*
449        * expect 0, 1 or more lines of the format either
450        * Chimera:
451        * residue id #0:43.A type GLY
452        * ChimeraX:
453        * residue id /A:89 name THR index 88
454        * We are only interested in the atomspec (third token of the reply)
455        */
456       for (String inputLine : chimeraReply)
457       {
458         String[] inputLineParts = inputLine.split("\\s+");
459         if (inputLineParts.length >= 5)
460         {
461           selectedResidues.add(inputLineParts[2]);
462         }
463       }
464     }
465     return selectedResidues;
466   }
467
468   public void getSelectedResidues(
469           Map<Integer, ChimeraModel> selectedModelsMap)
470   {
471     List<String> chimeraReply = sendChimeraCommand(
472             "list selection level residue", true);
473     if (chimeraReply != null)
474     {
475       for (String inputLine : chimeraReply)
476       {
477         ChimeraResidue r = new ChimeraResidue(inputLine);
478         Integer modelKey = ChimUtils.makeModelKey(r.getModelNumber(),
479                 r.getSubModelNumber());
480         if (selectedModelsMap.containsKey(modelKey))
481         {
482           ChimeraModel model = selectedModelsMap.get(modelKey);
483           model.addResidue(r);
484         }
485       }
486     }
487   }
488
489   /**
490    * Return the list of ChimeraModels currently open. Warning: if smiles model
491    * name too long, only part of it with "..." is printed.
492    * 
493    * 
494    * @return List of ChimeraModel's
495    */
496   // TODO: [Optional] Handle smiles names in a better way in Chimera?
497   public List<ChimeraModel> getModelList()
498   {
499     List<ChimeraModel> modelList = new ArrayList<>();
500     String command = "list models type "
501             + (isChimeraX ? "AtomicStructure" : "molecule");
502     List<String> list = sendChimeraCommand(command, true);
503     if (list != null)
504     {
505       for (String modelLine : list)
506       {
507         try
508         {
509           ChimeraModel chimeraModel = new ChimeraModel(modelLine);
510           modelList.add(chimeraModel);
511         } catch (NullPointerException e)
512         {
513           // hack for now
514         }
515       }
516     }
517     return modelList;
518   }
519
520   /**
521    * Return the list of depiction presets available from within Chimera. Chimera
522    * will return the list as a series of lines with the format: Preset type
523    * number "description"
524    * 
525    * @return list of presets
526    */
527   public List<String> getPresets()
528   {
529     ArrayList<String> presetList = new ArrayList<>();
530     List<String> output = sendChimeraCommand("preset list", true);
531     if (output != null)
532     {
533       for (String preset : output)
534       {
535         preset = preset.substring(7); // Skip over the "Preset"
536         preset = preset.replaceFirst("\"", "(");
537         preset = preset.replaceFirst("\"", ")");
538         // string now looks like: type number (description)
539         presetList.add(preset);
540       }
541     }
542     return presetList;
543   }
544
545   public boolean isChimeraLaunched()
546   {
547     boolean launched = false;
548     if (chimera != null)
549     {
550       try
551       {
552         chimera.exitValue();
553         // if we get here, process has ended
554       } catch (IllegalThreadStateException e)
555       {
556         // ok - not yet terminated
557         launched = true;
558       }
559     }
560     return launched;
561   }
562
563   /**
564    * Launch Chimera, unless an instance linked to this object is already
565    * running. Returns true if chimera is successfully launched, or already
566    * running, else false.
567    * 
568    * @param chimeraPaths
569    * @return
570    */
571   public boolean launchChimera(List<String> chimeraPaths)
572   {
573     // Do nothing if Chimera is already launched
574     if (isChimeraLaunched())
575     {
576       return true;
577     }
578
579     // Try to launch Chimera (eventually using one of the possible paths)
580     String error = "Error message: ";
581     String workingPath = "";
582     // iterate over possible paths for starting Chimera
583     for (String chimeraPath : chimeraPaths)
584     {
585       try
586       {
587         // ensure symbolic links are resolved
588         chimeraPath = Paths.get(chimeraPath).toRealPath().toString();
589         isChimeraX = chimeraPath.toLowerCase().contains("chimerax");
590         File path = new File(chimeraPath);
591         // uncomment the next line to simulate Chimera not installed
592         // path = new File(chimeraPath + "x");
593         if (!path.canExecute())
594         {
595           error += "File '" + path + "' does not exist.\n";
596           continue;
597         }
598         List<String> args = new ArrayList<>();
599         args.add(chimeraPath);
600         // shows Chimera output window but suppresses REST responses:
601         // args.add("--debug");
602         if (isChimeraX())
603         {
604           args.add("--cmd");
605           args.add("remote rest start");
606         }
607         else
608         {
609           args.add("--start");
610           args.add("RESTServer");
611         }
612         ProcessBuilder pb = new ProcessBuilder(args);
613         chimera = pb.start();
614         error = "";
615         workingPath = chimeraPath;
616         break;
617       } catch (Exception e)
618       {
619         // Chimera could not be started using this path
620         error += e.getMessage();
621       }
622     }
623     // If no error, then Chimera was launched successfully
624     if (error.length() == 0)
625     {
626       this.chimeraRestPort = getPortNumber();
627       System.out.println("Chimera REST API started on port "
628               + chimeraRestPort);
629       // structureManager.initChimTable();
630       structureManager.setChimeraPathProperty(workingPath);
631       // TODO: [Optional] Check Chimera version and show a warning if below 1.8
632       // Ask Chimera to give us updates
633       // startListening(); // later - see ChimeraListener
634       return (chimeraRestPort > 0);
635     }
636
637     // Tell the user that Chimera could not be started because of an error
638     logger.warn(error);
639     return false;
640   }
641
642   /**
643    * Read and return the port number returned in the reply to --start RESTServer
644    */
645   private int getPortNumber()
646   {
647     int port = 0;
648     InputStream readChan = chimera.getInputStream();
649     BufferedReader lineReader = new BufferedReader(new InputStreamReader(
650             readChan));
651     StringBuilder responses = new StringBuilder();
652     try
653     {
654       String response = lineReader.readLine();
655       while (response != null)
656       {
657         responses.append("\n" + response);
658         // expect: REST server on host 127.0.0.1 port port_number
659         // ChimeraX is the same except "REST server started on host..."
660         if (response.startsWith("REST server"))
661         {
662           String[] tokens = response.split(" ");
663           for (int i = 0; i < tokens.length - 1; i++)
664           {
665             if ("port".equals(tokens[i]))
666             {
667               port = Integer.parseInt(tokens[i + 1]);
668               break;
669             }
670           }
671         }
672         if (port > 0)
673         {
674           break; // hack for hanging readLine()
675         }
676         response = lineReader.readLine();
677       }
678     } catch (Exception e)
679     {
680       logger.error("Failed to get REST port number from " + responses
681               + ": " + e.getMessage());
682     } finally
683     {
684       try
685       {
686         lineReader.close();
687       } catch (IOException e2)
688       {
689       }
690     }
691     if (port == 0)
692     {
693       System.err
694               .println("Failed to start Chimera with REST service, response was: "
695                       + responses);
696     }
697     logger.info("Chimera REST service listening on port " + chimeraRestPort);
698     return port;
699   }
700
701   /**
702    * Determine the color that Chimera is using for this model.
703    * 
704    * @param model
705    *          the ChimeraModel we want to get the Color for
706    * @return the default model Color for this model in Chimera
707    */
708   public Color getModelColor(ChimeraModel model)
709   {
710     List<String> colorLines = sendChimeraCommand(
711             "list model spec " + model.toSpec() + " attribute color", true);
712     if (colorLines == null || colorLines.size() == 0)
713     {
714       return null;
715     }
716     return ChimUtils.parseModelColor(colorLines.get(0));
717   }
718
719   /**
720    * 
721    * Get information about the residues associated with a model. This uses the
722    * Chimera listr command. We don't return the resulting residues, but we add
723    * the residues to the model.
724    * 
725    * @param model
726    *          the ChimeraModel to get residue information for
727    * 
728    */
729   public void addResidues(ChimeraModel model)
730   {
731     int modelNumber = model.getModelNumber();
732     int subModelNumber = model.getSubModelNumber();
733     // Get the list -- it will be in the reply log
734     List<String> reply = sendChimeraCommand(
735             "list residues spec " + model.toSpec(), true);
736     if (reply == null)
737     {
738       return;
739     }
740     for (String inputLine : reply)
741     {
742       ChimeraResidue r = new ChimeraResidue(inputLine);
743       if (r.getModelNumber() == modelNumber
744               || r.getSubModelNumber() == subModelNumber)
745       {
746         model.addResidue(r);
747       }
748     }
749   }
750
751   public List<String> getAttrList()
752   {
753     List<String> attributes = new ArrayList<>();
754     String command = (isChimeraX ? "info " : "list ") + "resattr";
755     final List<String> reply = sendChimeraCommand(command, true);
756     if (reply != null)
757     {
758       for (String inputLine : reply)
759       {
760         String[] lineParts = inputLine.split("\\s");
761         if (lineParts.length == 2 && lineParts[0].equals("resattr"))
762         {
763           attributes.add(lineParts[1]);
764         }
765       }
766     }
767     return attributes;
768   }
769
770   public Map<ChimeraResidue, Object> getAttrValues(String aCommand,
771           ChimeraModel model)
772   {
773     Map<ChimeraResidue, Object> values = new HashMap<>();
774     final List<String> reply = sendChimeraCommand("list residue spec "
775             + model.toSpec() + " attribute " + aCommand, true);
776     if (reply != null)
777     {
778       for (String inputLine : reply)
779       {
780         String[] lineParts = inputLine.split("\\s");
781         if (lineParts.length == 5)
782         {
783           ChimeraResidue residue = ChimUtils
784                   .getResidue(lineParts[2], model);
785           String value = lineParts[4];
786           if (residue != null)
787           {
788             if (value.equals("None"))
789             {
790               continue;
791             }
792             if (value.equals("True") || value.equals("False"))
793             {
794               values.put(residue, Boolean.valueOf(value));
795               continue;
796             }
797             try
798             {
799               Double doubleValue = Double.valueOf(value);
800               values.put(residue, doubleValue);
801             } catch (NumberFormatException ex)
802             {
803               values.put(residue, value);
804             }
805           }
806         }
807       }
808     }
809     return values;
810   }
811
812   private volatile boolean busy = false;
813
814   private boolean isChimeraX;
815
816   /**
817    * Send a command to Chimera.
818    * 
819    * @param command
820    *          Command string to be send.
821    * @param reply
822    *          Flag indicating whether the method should return the reply from
823    *          Chimera or not.
824    * @return List of Strings corresponding to the lines in the Chimera reply or
825    *         <code>null</code>.
826    */
827   public List<String> sendChimeraCommand(String command, boolean reply)
828   {
829     System.out.println("chimeradebug>> " + command);
830     if (!isChimeraLaunched() || command == null
831             || "".equals(command.trim()))
832     {
833       return null;
834     }
835     // TODO do we need a maximum wait time before aborting?
836     while (busy)
837     {
838       try
839       {
840         Thread.sleep(25);
841       } catch (InterruptedException q)
842       {
843       }
844     }
845     busy = true;
846     long startTime = System.currentTimeMillis();
847     try
848     {
849       return sendRestCommand(command);
850     } finally
851     {
852       /*
853        * Make sure busy flag is reset come what may!
854        */
855       busy = false;
856       if (debug)
857       {
858         System.out.println("Chimera command took "
859                 + (System.currentTimeMillis() - startTime) + "ms: "
860                 + command);
861       }
862
863     }
864   }
865
866   /**
867    * Sends the command to Chimera's REST API, and returns any response lines.
868    * 
869    * @param command
870    * @return
871    */
872   protected List<String> sendRestCommand(String command)
873   {
874     String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run";
875     List<NameValuePair> commands = new ArrayList<>(1);
876     String method = isChimeraX() ? "GET" : "POST";
877     if ("GET".equals(method))
878     {
879       command = command.replace(" ", "+").replace("#", "%23")
880               .replace("|", "%7C").replace(";", "%3B");
881     }
882     commands.add(new BasicNameValuePair("command", command));
883
884     List<String> reply = new ArrayList<>();
885     BufferedReader response = null;
886     try
887     {
888       response = "GET".equals(method)
889               ? HttpClientUtils.doHttpGet(restUrl, commands,
890                       CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS)
891               : HttpClientUtils.doHttpUrlPost(restUrl, commands,
892                       CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS);
893       String line = "";
894       while ((line = response.readLine()) != null)
895       {
896         reply.add(line);
897       }
898     } catch (Exception e)
899     {
900       logger.error("REST call '" + command + "' failed: " + e.getMessage());
901     } finally
902     {
903       if (response != null)
904       {
905         try
906         {
907           response.close();
908         } catch (IOException e)
909         {
910         }
911       }
912     }
913     return reply;
914   }
915
916   /**
917    * Send a command to stdin of Chimera process, and optionally read any
918    * responses.
919    * 
920    * @param command
921    * @param readReply
922    * @return
923    */
924   protected List<String> sendStdinCommand(String command, boolean readReply)
925   {
926     chimeraListenerThread.clearResponse(command);
927     String text = command.concat("\n");
928     try
929     {
930       // send the command
931       chimera.getOutputStream().write(text.getBytes());
932       chimera.getOutputStream().flush();
933     } catch (IOException e)
934     {
935       // logger.info("Unable to execute command: " + text);
936       // logger.info("Exiting...");
937       logger.warn("Unable to execute command: " + text);
938       logger.warn("Exiting...");
939       clearOnChimeraExit();
940       return null;
941     }
942     if (!readReply)
943     {
944       return null;
945     }
946     List<String> rsp = chimeraListenerThread.getResponse(command);
947     return rsp;
948   }
949
950   public StructureManager getStructureManager()
951   {
952     return structureManager;
953   }
954
955   public boolean isBusy()
956   {
957     return busy;
958   }
959
960   public Process getChimeraProcess()
961   {
962     return chimera;
963   }
964
965   public boolean isChimeraX()
966   {
967     return isChimeraX;
968   }
969
970   public void setChimeraX(boolean b)
971   {
972     isChimeraX = b;
973   }
974 }