JAL-2422 proof of concept adaptations for ChimeraX commands
[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.nio.file.Paths;
44 import java.util.ArrayList;
45 import java.util.Collection;
46 import java.util.HashMap;
47 import java.util.List;
48 import java.util.Map;
49
50 import org.apache.http.NameValuePair;
51 import org.apache.http.message.BasicNameValuePair;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
56 import ext.edu.ucsf.rbvi.strucviz2.port.ListenerThreads;
57
58 /**
59  * This object maintains the Chimera communication information.
60  */
61 public class ChimeraManager
62 {
63   private static final int REST_REPLY_TIMEOUT_MS = 15000;
64
65   private static final int CONNECTION_TIMEOUT_MS = 100;
66
67   private static final boolean debug = false;
68
69   private int chimeraRestPort;
70
71   private Process chimera;
72
73   private ListenerThreads chimeraListenerThread;
74
75   private Map<Integer, ChimeraModel> currentModelsMap;
76
77   private Logger logger = LoggerFactory
78           .getLogger(ext.edu.ucsf.rbvi.strucviz2.ChimeraManager.class);
79
80   private StructureManager structureManager;
81
82   public ChimeraManager(StructureManager structureManager)
83   {
84     this.structureManager = structureManager;
85     chimera = null;
86     chimeraListenerThread = null;
87     currentModelsMap = new HashMap<>();
88
89   }
90
91   public List<ChimeraModel> getChimeraModels(String modelName)
92   {
93     List<ChimeraModel> models = getChimeraModels(modelName,
94             ModelType.PDB_MODEL);
95     models.addAll(getChimeraModels(modelName, ModelType.SMILES));
96     return models;
97   }
98
99   public List<ChimeraModel> getChimeraModels(String modelName,
100           ModelType modelType)
101   {
102     List<ChimeraModel> models = new ArrayList<>();
103     for (ChimeraModel model : currentModelsMap.values())
104     {
105       if (modelName.equals(model.getModelName())
106               && modelType.equals(model.getModelType()))
107       {
108         models.add(model);
109       }
110     }
111     return models;
112   }
113
114   public Map<String, List<ChimeraModel>> getChimeraModelsMap()
115   {
116     Map<String, List<ChimeraModel>> models = new HashMap<>();
117     for (ChimeraModel model : currentModelsMap.values())
118     {
119       String modelName = model.getModelName();
120       if (!models.containsKey(modelName))
121       {
122         models.put(modelName, new ArrayList<ChimeraModel>());
123       }
124       if (!models.get(modelName).contains(model))
125       {
126         models.get(modelName).add(model);
127       }
128     }
129     return models;
130   }
131
132   public ChimeraModel getChimeraModel(Integer modelNumber,
133           Integer subModelNumber)
134   {
135     Integer key = ChimUtils.makeModelKey(modelNumber, subModelNumber);
136     if (currentModelsMap.containsKey(key))
137     {
138       return currentModelsMap.get(key);
139     }
140     return null;
141   }
142
143   public ChimeraModel getChimeraModel()
144   {
145     return currentModelsMap.values().iterator().next();
146   }
147
148   public Collection<ChimeraModel> getChimeraModels()
149   {
150     // this method is invoked by the model navigator dialog
151     return currentModelsMap.values();
152   }
153
154   public int getChimeraModelsCount(boolean smiles)
155   {
156     // this method is invokes by the model navigator dialog
157     int counter = currentModelsMap.size();
158     if (smiles)
159     {
160       return counter;
161     }
162
163     for (ChimeraModel model : currentModelsMap.values())
164     {
165       if (model.getModelType() == ModelType.SMILES)
166       {
167         counter--;
168       }
169     }
170     return counter;
171   }
172
173   public boolean hasChimeraModel(Integer modelNubmer)
174   {
175     return hasChimeraModel(modelNubmer, 0);
176   }
177
178   public boolean hasChimeraModel(Integer modelNubmer, Integer subModelNumber)
179   {
180     return currentModelsMap.containsKey(ChimUtils.makeModelKey(modelNubmer,
181             subModelNumber));
182   }
183
184   public void addChimeraModel(Integer modelNumber, Integer subModelNumber,
185           ChimeraModel model)
186   {
187     currentModelsMap.put(
188             ChimUtils.makeModelKey(modelNumber, subModelNumber), model);
189   }
190
191   public void removeChimeraModel(Integer modelNumber, Integer subModelNumber)
192   {
193     int modelKey = ChimUtils.makeModelKey(modelNumber, subModelNumber);
194     if (currentModelsMap.containsKey(modelKey))
195     {
196       currentModelsMap.remove(modelKey);
197     }
198   }
199
200   public List<ChimeraModel> openModel(String modelPath, ModelType type)
201   {
202     return openModel(modelPath, getFileNameFromPath(modelPath), type);
203   }
204
205   /**
206    * Overloaded method to allow Jalview to pass in a model name.
207    * 
208    * @param modelPath
209    * @param modelName
210    * @param type
211    * @return
212    */
213   public List<ChimeraModel> openModel(String modelPath, String modelName,
214           ModelType type)
215   {
216     logger.info("chimera open " + modelPath);
217     // stopListening();
218     List<ChimeraModel> modelList = getModelList();
219     List<String> response = null;
220     // TODO: [Optional] Handle modbase models
221     if (type == ModelType.MODBASE_MODEL)
222     {
223       response = sendChimeraCommand("open modbase:" + modelPath, true);
224       // } else if (type == ModelType.SMILES) {
225       // response = sendChimeraCommand("open smiles:" + modelName, true);
226       // modelName = "smiles:" + modelName;
227     }
228     else
229     {
230       response = sendChimeraCommand("open " + modelPath, true);
231     }
232     if (response == null)
233     {
234       // something went wrong
235       logger.warn("Could not open " + modelPath);
236       return null;
237     }
238
239     // patch for Jalview - set model name in Chimera
240     // TODO: find a variant that works for sub-models
241     for (ChimeraModel newModel : getModelList())
242     {
243       if (!modelList.contains(newModel))
244       {
245         newModel.setModelName(modelName);
246         sendChimeraCommand(
247                 "setattr M name " + modelName + " #"
248                         + newModel.getModelNumber(), false);
249         modelList.add(newModel);
250       }
251     }
252
253     // assign color and residues to open models
254     for (ChimeraModel chimeraModel : modelList)
255     {
256       // get model color
257       Color modelColor = isChimeraX() ? null : getModelColor(chimeraModel);
258       if (modelColor != null)
259       {
260         chimeraModel.setModelColor(modelColor);
261       }
262
263       // Get our properties (default color scheme, etc.)
264       // Make the molecule look decent
265       // chimeraSend("repr stick "+newModel.toSpec());
266
267       // Create the information we need for the navigator
268       if (type != ModelType.SMILES && !isChimeraX())
269       {
270         addResidues(chimeraModel);
271       }
272     }
273
274     sendChimeraCommand("focus", false);
275     // startListening(); // see ChimeraListener
276     return modelList;
277   }
278
279   /**
280    * Refactored method to extract the last (or only) element delimited by file
281    * path separator.
282    * 
283    * @param modelPath
284    * @return
285    */
286   private String getFileNameFromPath(String modelPath)
287   {
288     String modelName = modelPath;
289     if (modelPath == null)
290     {
291       return null;
292     }
293     // TODO: [Optional] Convert path to name in a better way
294     if (modelPath.lastIndexOf(File.separator) > 0)
295     {
296       modelName = modelPath
297               .substring(modelPath.lastIndexOf(File.separator) + 1);
298     }
299     else if (modelPath.lastIndexOf("/") > 0)
300     {
301       modelName = modelPath.substring(modelPath.lastIndexOf("/") + 1);
302     }
303     return modelName;
304   }
305
306   public void closeModel(ChimeraModel model)
307   {
308     // int model = structure.modelNumber();
309     // int subModel = structure.subModelNumber();
310     // Integer modelKey = makeModelKey(model, subModel);
311     stopListening();
312     logger.info("chimera close model " + model.getModelName());
313     if (currentModelsMap.containsKey(ChimUtils.makeModelKey(
314             model.getModelNumber(), model.getSubModelNumber())))
315     {
316       sendChimeraCommand("close " + model.toSpec(), false);
317       // currentModelNamesMap.remove(model.getModelName());
318       currentModelsMap.remove(ChimUtils.makeModelKey(
319               model.getModelNumber(), model.getSubModelNumber()));
320       // selectionList.remove(chimeraModel);
321     }
322     else
323     {
324       logger.warn("Could not find model " + model.getModelName()
325               + " to close.");
326     }
327     startListening();
328   }
329
330   public void startListening()
331   {
332     sendChimeraCommand("listen start models; listen start selection", false);
333   }
334
335   public void stopListening()
336   {
337     // TODO send this command when viewer connection is closed in Jalview
338     String command = isChimeraX
339             ? "info notify stop models jalview; info notify stop selection jalview"
340             : "listen stop models ; listen stop selection ";
341     sendChimeraCommand(command, false);
342   }
343
344   /**
345    * Tell Chimera we are listening on the given URI
346    * 
347    * @param uri
348    */
349   public void startListening(String uri)
350   {
351     String command = isChimeraX
352             ? ("info notify start models prefix ModelChanged jalview url "
353                     + uri
354                     + "; info notify start selection jalview prefix SelectionChanged url "
355                     + uri)
356             : ("listen start models url " + uri
357                     + ";listen start select prefix SelectionChanged url "
358                     + uri);
359     sendChimeraCommand(command, false);
360   }
361
362   /**
363    * Select something in Chimera
364    * 
365    * @param command
366    *          the selection command to pass to Chimera
367    */
368   public void select(String command)
369   {
370     sendChimeraCommand("listen stop selection; " + command
371             + "; listen start selection", false);
372   }
373
374   public void focus()
375   {
376     sendChimeraCommand("focus", false);
377   }
378
379   public void clearOnChimeraExit()
380   {
381     chimera = null;
382     currentModelsMap.clear();
383     this.chimeraRestPort = 0;
384     structureManager.clearOnChimeraExit();
385   }
386
387   public void exitChimera()
388   {
389     if (isChimeraLaunched() && chimera != null)
390     {
391       sendChimeraCommand("stop really", false);
392       try
393       {
394         // TODO is this too violent? could it force close the process
395         // before it has done an orderly shutdown?
396         chimera.destroy();
397       } catch (Exception ex)
398       {
399         // ignore
400       }
401     }
402     clearOnChimeraExit();
403   }
404
405   public Map<Integer, ChimeraModel> getSelectedModels()
406   {
407     Map<Integer, ChimeraModel> selectedModelsMap = new HashMap<>();
408     List<String> chimeraReply = sendChimeraCommand(
409             "list selection level molecule", true);
410     if (chimeraReply != null)
411     {
412       for (String modelLine : chimeraReply)
413       {
414         ChimeraModel chimeraModel = new ChimeraModel(modelLine);
415         Integer modelKey = ChimUtils.makeModelKey(
416                 chimeraModel.getModelNumber(),
417                 chimeraModel.getSubModelNumber());
418         selectedModelsMap.put(modelKey, chimeraModel);
419       }
420     }
421     return selectedModelsMap;
422   }
423
424   /**
425    * Sends a 'list selection level residue' command to Chimera and returns the
426    * list of selected atomspecs
427    * 
428    * @return
429    */
430   public List<String> getSelectedResidueSpecs()
431   {
432     List<String> selectedResidues = new ArrayList<>();
433
434     /*
435      * skip for now if ChimeraX - times out
436      */
437     if (isChimeraX)
438     {
439       return selectedResidues;
440     }
441
442     // in fact 'listinfo' (undocumented) works in ChimeraX
443     String command = (isChimeraX ? "info" : "list")
444             + " 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     modelList.add(new ChimeraModel("4zhp", ModelType.PDB_MODEL, 1, 0));
501     return modelList; // ChimeraX doesn't have 'list models' command
502     // List<String> list = sendChimeraCommand("list models type molecule",
503     // true);
504     // if (list != null)
505     // {
506     // for (String modelLine : list)
507     // {
508     // ChimeraModel chimeraModel = new ChimeraModel(modelLine);
509     // modelList.add(chimeraModel);
510     // }
511     // }
512     // return modelList;
513   }
514
515   /**
516    * Return the list of depiction presets available from within Chimera. Chimera
517    * will return the list as a series of lines with the format: Preset type
518    * number "description"
519    * 
520    * @return list of presets
521    */
522   public List<String> getPresets()
523   {
524     ArrayList<String> presetList = new ArrayList<>();
525     List<String> output = sendChimeraCommand("preset list", true);
526     if (output != null)
527     {
528       for (String preset : output)
529       {
530         preset = preset.substring(7); // Skip over the "Preset"
531         preset = preset.replaceFirst("\"", "(");
532         preset = preset.replaceFirst("\"", ")");
533         // string now looks like: type number (description)
534         presetList.add(preset);
535       }
536     }
537     return presetList;
538   }
539
540   public boolean isChimeraLaunched()
541   {
542     boolean launched = false;
543     if (chimera != null)
544     {
545       try
546       {
547         chimera.exitValue();
548         // if we get here, process has ended
549       } catch (IllegalThreadStateException e)
550       {
551         // ok - not yet terminated
552         launched = true;
553       }
554     }
555     return launched;
556   }
557
558   /**
559    * Launch Chimera, unless an instance linked to this object is already
560    * running. Returns true if chimera is successfully launched, or already
561    * running, else false.
562    * 
563    * @param chimeraPaths
564    * @return
565    */
566   public boolean launchChimera(List<String> chimeraPaths)
567   {
568     // Do nothing if Chimera is already launched
569     if (isChimeraLaunched())
570     {
571       return true;
572     }
573
574     // Try to launch Chimera (eventually using one of the possible paths)
575     String error = "Error message: ";
576     String workingPath = "";
577     // iterate over possible paths for starting Chimera
578     for (String chimeraPath : chimeraPaths)
579     {
580       try
581       {
582         // ensure symbolic links are resolved
583         chimeraPath = Paths.get(chimeraPath).toRealPath().toString();
584         isChimeraX = chimeraPath.toLowerCase().contains("chimerax");
585         File path = new File(chimeraPath);
586         // uncomment the next line to simulate Chimera not installed
587         // path = new File(chimeraPath + "x");
588         if (!path.canExecute())
589         {
590           error += "File '" + path + "' does not exist.\n";
591           continue;
592         }
593         List<String> args = new ArrayList<>();
594         args.add(chimeraPath);
595         // shows Chimera output window but suppresses REST responses:
596         // args.add("--debug");
597         if (isChimeraX())
598         {
599           args.add("--cmd");
600           args.add("remote rest start");
601         }
602         else
603         {
604           args.add("--start");
605           args.add("RESTServer");
606         }
607         ProcessBuilder pb = new ProcessBuilder(args);
608         chimera = pb.start();
609         error = "";
610         workingPath = chimeraPath;
611         break;
612       } catch (Exception e)
613       {
614         // Chimera could not be started using this path
615         error += e.getMessage();
616       }
617     }
618     // If no error, then Chimera was launched successfully
619     if (error.length() == 0)
620     {
621       this.chimeraRestPort = getPortNumber();
622       System.out.println("Chimera REST API started on port "
623               + chimeraRestPort);
624       // structureManager.initChimTable();
625       structureManager.setChimeraPathProperty(workingPath);
626       // TODO: [Optional] Check Chimera version and show a warning if below 1.8
627       // Ask Chimera to give us updates
628       // startListening(); // later - see ChimeraListener
629       return (chimeraRestPort > 0);
630     }
631
632     // Tell the user that Chimera could not be started because of an error
633     logger.warn(error);
634     return false;
635   }
636
637   /**
638    * Read and return the port number returned in the reply to --start RESTServer
639    */
640   private int getPortNumber()
641   {
642     int port = 0;
643     InputStream readChan = chimera.getInputStream();
644     BufferedReader lineReader = new BufferedReader(new InputStreamReader(
645             readChan));
646     StringBuilder responses = new StringBuilder();
647     try
648     {
649       String response = lineReader.readLine();
650       while (response != null)
651       {
652         responses.append("\n" + response);
653         // expect: REST server on host 127.0.0.1 port port_number
654         // ChimeraX is the same except "REST server started on host..."
655         if (response.startsWith("REST server"))
656         {
657           String[] tokens = response.split(" ");
658           for (int i = 0; i < tokens.length - 1; i++)
659           {
660             if ("port".equals(tokens[i]))
661             {
662               port = Integer.parseInt(tokens[i + 1]);
663               break;
664             }
665           }
666         }
667         if (port > 0)
668         {
669           break; // hack for hanging readLine()
670         }
671         response = lineReader.readLine();
672       }
673     } catch (Exception e)
674     {
675       logger.error("Failed to get REST port number from " + responses
676               + ": " + e.getMessage());
677     } finally
678     {
679       try
680       {
681         lineReader.close();
682       } catch (IOException e2)
683       {
684       }
685     }
686     if (port == 0)
687     {
688       System.err
689               .println("Failed to start Chimera with REST service, response was: "
690                       + responses);
691     }
692     logger.info("Chimera REST service listening on port " + chimeraRestPort);
693     return port;
694   }
695
696   /**
697    * Determine the color that Chimera is using for this model.
698    * 
699    * @param model
700    *          the ChimeraModel we want to get the Color for
701    * @return the default model Color for this model in Chimera
702    */
703   public Color getModelColor(ChimeraModel model)
704   {
705     List<String> colorLines = sendChimeraCommand(
706             "list model spec " + model.toSpec() + " attribute color", true);
707     if (colorLines == null || colorLines.size() == 0)
708     {
709       return null;
710     }
711     return ChimUtils.parseModelColor(colorLines.get(0));
712   }
713
714   /**
715    * 
716    * Get information about the residues associated with a model. This uses the
717    * Chimera listr command. We don't return the resulting residues, but we add
718    * the residues to the model.
719    * 
720    * @param model
721    *          the ChimeraModel to get residue information for
722    * 
723    */
724   public void addResidues(ChimeraModel model)
725   {
726     int modelNumber = model.getModelNumber();
727     int subModelNumber = model.getSubModelNumber();
728     // Get the list -- it will be in the reply log
729     List<String> reply = sendChimeraCommand(
730             "list residues spec " + model.toSpec(), true);
731     if (reply == null)
732     {
733       return;
734     }
735     for (String inputLine : reply)
736     {
737       ChimeraResidue r = new ChimeraResidue(inputLine);
738       if (r.getModelNumber() == modelNumber
739               || r.getSubModelNumber() == subModelNumber)
740       {
741         model.addResidue(r);
742       }
743     }
744   }
745
746   public List<String> getAttrList()
747   {
748     List<String> attributes = new ArrayList<>();
749     String command = (isChimeraX ? "info " : "list") + "resattr";
750     final List<String> reply = sendChimeraCommand(command, true);
751     if (reply != null)
752     {
753       for (String inputLine : reply)
754       {
755         String[] lineParts = inputLine.split("\\s");
756         if (lineParts.length == 2 && lineParts[0].equals("resattr"))
757         {
758           attributes.add(lineParts[1]);
759         }
760       }
761     }
762     return attributes;
763   }
764
765   public Map<ChimeraResidue, Object> getAttrValues(String aCommand,
766           ChimeraModel model)
767   {
768     Map<ChimeraResidue, Object> values = new HashMap<>();
769     final List<String> reply = sendChimeraCommand("list residue spec "
770             + model.toSpec() + " attribute " + aCommand, true);
771     if (reply != null)
772     {
773       for (String inputLine : reply)
774       {
775         String[] lineParts = inputLine.split("\\s");
776         if (lineParts.length == 5)
777         {
778           ChimeraResidue residue = ChimUtils
779                   .getResidue(lineParts[2], model);
780           String value = lineParts[4];
781           if (residue != null)
782           {
783             if (value.equals("None"))
784             {
785               continue;
786             }
787             if (value.equals("True") || value.equals("False"))
788             {
789               values.put(residue, Boolean.valueOf(value));
790               continue;
791             }
792             try
793             {
794               Double doubleValue = Double.valueOf(value);
795               values.put(residue, doubleValue);
796             } catch (NumberFormatException ex)
797             {
798               values.put(residue, value);
799             }
800           }
801         }
802       }
803     }
804     return values;
805   }
806
807   private volatile boolean busy = false;
808
809   private boolean isChimeraX;
810
811   /**
812    * Send a command to Chimera.
813    * 
814    * @param command
815    *          Command string to be send.
816    * @param reply
817    *          Flag indicating whether the method should return the reply from
818    *          Chimera or not.
819    * @return List of Strings corresponding to the lines in the Chimera reply or
820    *         <code>null</code>.
821    */
822   public List<String> sendChimeraCommand(String command, boolean reply)
823   {
824     System.out.println("chimeradebug>> " + command);
825     if (!isChimeraLaunched() || command == null
826             || "".equals(command.trim()))
827     {
828       return null;
829     }
830     // TODO do we need a maximum wait time before aborting?
831     while (busy)
832     {
833       try
834       {
835         Thread.sleep(25);
836       } catch (InterruptedException q)
837       {
838       }
839     }
840     busy = true;
841     long startTime = System.currentTimeMillis();
842     try
843     {
844       return sendRestCommand(command);
845     } finally
846     {
847       /*
848        * Make sure busy flag is reset come what may!
849        */
850       busy = false;
851       if (debug)
852       {
853         System.out.println("Chimera command took "
854                 + (System.currentTimeMillis() - startTime) + "ms: "
855                 + command);
856       }
857
858     }
859   }
860
861   /**
862    * Sends the command to Chimera's REST API, and returns any response lines.
863    * 
864    * @param command
865    * @return
866    */
867   protected List<String> sendRestCommand(String command)
868   {
869     String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run";
870     List<NameValuePair> commands = new ArrayList<>(1);
871     String encoded = command.replace(" ", "+").replace("#", "%23")
872             .replace("|", "%7C").replace(";", "%3B");
873     commands.add(new BasicNameValuePair("command", encoded));
874
875     List<String> reply = new ArrayList<>();
876     BufferedReader response = null;
877     try
878     {
879       if (isChimeraX())
880       {
881         response = HttpClientUtils.doHttpGet(restUrl, commands,
882                 CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS);
883       }
884       else
885       {
886         response = HttpClientUtils.doHttpUrlPost(restUrl, commands,
887                 CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS);
888       }
889       String line = "";
890       while ((line = response.readLine()) != null)
891       {
892         reply.add(line);
893       }
894     } catch (Exception e)
895     {
896       logger.error("REST call '" + command + "' failed: " + e.getMessage());
897     } finally
898     {
899       if (response != null)
900       {
901         try
902         {
903           response.close();
904         } catch (IOException e)
905         {
906         }
907       }
908     }
909     return reply;
910   }
911
912   /**
913    * Send a command to stdin of Chimera process, and optionally read any
914    * responses.
915    * 
916    * @param command
917    * @param readReply
918    * @return
919    */
920   protected List<String> sendStdinCommand(String command, boolean readReply)
921   {
922     chimeraListenerThread.clearResponse(command);
923     String text = command.concat("\n");
924     try
925     {
926       // send the command
927       chimera.getOutputStream().write(text.getBytes());
928       chimera.getOutputStream().flush();
929     } catch (IOException e)
930     {
931       // logger.info("Unable to execute command: " + text);
932       // logger.info("Exiting...");
933       logger.warn("Unable to execute command: " + text);
934       logger.warn("Exiting...");
935       clearOnChimeraExit();
936       return null;
937     }
938     if (!readReply)
939     {
940       return null;
941     }
942     List<String> rsp = chimeraListenerThread.getResponse(command);
943     return rsp;
944   }
945
946   public StructureManager getStructureManager()
947   {
948     return structureManager;
949   }
950
951   public boolean isBusy()
952   {
953     return busy;
954   }
955
956   public Process getChimeraProcess()
957   {
958     return chimera;
959   }
960
961   public boolean isChimeraX()
962   {
963     return isChimeraX;
964   }
965 }