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