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