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