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