Clover icon

Coverage Report

  1. Project Clover database Thu Nov 28 2024 18:06:40 GMT
  2. Package ext.edu.ucsf.rbvi.strucviz2

File ChimeraManager.java

 

Coverage histogram

../../../../../img/srcFileCovDistChart0.png
63% of files have more coverage

Code metrics

112
296
44
1
995
706
122
0.41
6.73
44
2.77

Classes

Class Line # Actions
ChimeraManager 64 296 122
0.00%
 

Contributing tests

No tests hitting this source file were found.

Source view

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