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