6eb294f553e6833f2a80dafae6b12588f8a5852c
[jalview.git] / src / jalview / hmmer / HmmerCommand.java
1 package jalview.hmmer;
2
3 import jalview.analysis.SeqsetUtils;
4 import jalview.bin.Cache;
5 import jalview.datamodel.Alignment;
6 import jalview.datamodel.AlignmentAnnotation;
7 import jalview.datamodel.AlignmentI;
8 import jalview.datamodel.AnnotatedCollectionI;
9 import jalview.datamodel.Annotation;
10 import jalview.datamodel.HiddenMarkovModel;
11 import jalview.datamodel.SequenceGroup;
12 import jalview.datamodel.SequenceI;
13 import jalview.gui.AlignFrame;
14 import jalview.gui.JvOptionPane;
15 import jalview.gui.Preferences;
16 import jalview.io.FastaFile;
17 import jalview.io.HMMFile;
18 import jalview.io.StockholmFile;
19 import jalview.util.FileUtils;
20 import jalview.util.MessageManager;
21 import jalview.util.Platform;
22 import jalview.ws.params.ArgumentI;
23
24 import java.io.BufferedReader;
25 import java.io.File;
26 import java.io.IOException;
27 import java.io.InputStreamReader;
28 import java.io.PrintWriter;
29 import java.nio.file.Paths;
30 import java.util.ArrayList;
31 import java.util.Hashtable;
32 import java.util.List;
33
34 /**
35  * Base class for hmmbuild, hmmalign and hmmsearch
36  * 
37  * @author TZVanaalten
38  *
39  */
40 public abstract class HmmerCommand implements Runnable
41 {
42   public static final String HMMBUILD = "hmmbuild";
43
44   protected final AlignFrame af;
45
46   protected final AlignmentI alignment;
47
48   protected final List<ArgumentI> params;
49
50   /*
51    * constants for i18n lookup of passed parameter names
52    */
53   static final String DATABASE_KEY = "label.database";
54
55   static final String THIS_ALIGNMENT_KEY = "label.this_alignment";
56
57   static final String USE_ACCESSIONS_KEY = "label.use_accessions";
58
59   static final String AUTO_ALIGN_SEQS_KEY = "label.auto_align_seqs";
60
61   static final String NUMBER_OF_RESULTS_KEY = "label.number_of_results";
62
63   static final String TRIM_TERMINI_KEY = "label.trim_termini";
64
65   static final String REPORTING_CUTOFF_KEY = "label.reporting_cutoff";
66
67   static final String CUTOFF_NONE = "None";
68
69   static final String CUTOFF_SCORE = "Score";
70
71   static final String CUTOFF_EVALUE = "E-Value";
72
73   static final String SEQ_EVALUE_KEY = "label.seq_evalue";
74
75   static final String DOM_EVALUE_KEY = "label.dom_evalue";
76
77   static final String SEQ_SCORE_KEY = "label.seq_score";
78
79   static final String DOM_SCORE_KEY = "label.dom_score";
80
81   static final String ARG_TRIM = "--trim";
82
83   /**
84    * Constructor
85    * 
86    * @param alignFrame
87    * @param args
88    */
89   public HmmerCommand(AlignFrame alignFrame, List<ArgumentI> args)
90   {
91     af = alignFrame;
92     alignment = af.getViewport().getAlignment();
93     params = args;
94   }
95
96   /**
97    * Answers true if preference HMMER_PATH is set, and its value is the path to
98    * a directory that contains an executable <code>hmmbuild</code> or
99    * <code>hmmbuild.exe</code>, else false
100    * 
101    * @return
102    */
103   public static boolean isHmmerAvailable()
104   {
105     File exec = FileUtils.getExecutable(HMMBUILD,
106             Cache.getProperty(Preferences.HMMER_PATH));
107     return exec != null;
108   }
109
110   /**
111    * Uniquifies the sequences when exporting and stores their details in a
112    * hashtable
113    * 
114    * @param seqs
115    */
116   protected Hashtable stashSequences(SequenceI[] seqs)
117   {
118     return SeqsetUtils.uniquify(seqs, true);
119   }
120
121   /**
122    * Restores the sequence data lost by uniquifying
123    * 
124    * @param hashtable
125    * @param seqs
126    */
127   protected void recoverSequences(Hashtable hashtable, SequenceI[] seqs)
128   {
129     SeqsetUtils.deuniquify(hashtable, seqs);
130   }
131
132   /**
133    * Runs a command as a separate process and waits for it to complete. Answers
134    * true if the process return status is zero, else false.
135    * 
136    * @param commands
137    *          the executable command and any arguments to it
138    * @throws IOException
139    */
140   public boolean runCommand(List<String> commands)
141           throws IOException
142   {
143     List<String> args = Platform.isWindows() ? wrapWithCygwin(commands)
144             : commands;
145
146     try
147     {
148       ProcessBuilder pb = new ProcessBuilder(args);
149       pb.redirectErrorStream(true); // merge syserr to sysout
150       if (Platform.isWindows())
151       {
152         String path = pb.environment().get("Path");
153         path = jalview.bin.Cache.getProperty("CYGWIN_PATH") + ";" + path;
154         pb.environment().put("Path", path);
155       }
156       final Process p = pb.start();
157       new Thread(new Runnable()
158       {
159         @Override
160         public void run()
161         {
162           BufferedReader input = new BufferedReader(
163                   new InputStreamReader(p.getInputStream()));
164           try
165           {
166             String line = input.readLine();
167             while (line != null)
168             {
169               System.out.println(line);
170               line = input.readLine();
171             }
172           } catch (IOException e)
173           {
174             e.printStackTrace();
175           }
176         }
177       }).start();
178
179       p.waitFor();
180       int exitValue = p.exitValue();
181       if (exitValue != 0)
182       {
183         Cache.log.error("Command failed, return code = " + exitValue);
184         Cache.log.error("Command/args were: " + args.toString());
185       }
186       return exitValue == 0; // 0 is success, by convention
187     } catch (Exception e)
188     {
189       e.printStackTrace();
190       return false;
191     }
192   }
193
194   /**
195    * Converts the given command to a Cygwin "bash" command wrapper. The hmmer
196    * command and any arguments to it are converted into a single parameter to the
197    * bash command.
198    * 
199    * @param commands
200    */
201   protected List<String> wrapWithCygwin(List<String> commands)
202   {
203     File bash = FileUtils.getExecutable("bash",
204             Cache.getProperty(Preferences.CYGWIN_PATH));
205     if (bash == null)
206     {
207       Cache.log.error("Cygwin shell not found");
208       return commands;
209     }
210
211     List<String> wrapped = new ArrayList<>();
212     // wrapped.add("C:\Users\tva\run");
213     wrapped.add(bash.getAbsolutePath());
214     wrapped.add("-c");
215
216     /*
217      * combine hmmbuild/search/align and arguments to a single string
218      */
219     StringBuilder sb = new StringBuilder();
220     for (String cmd : commands)
221     {
222       sb.append(" ").append(cmd);
223     }
224     wrapped.add(sb.toString());
225
226     return wrapped;
227   }
228
229   /**
230    * Exports an alignment, and reference (RF) annotation if present, to the
231    * specified file, in Stockholm format, removing all HMM sequences
232    * 
233    * @param seqs
234    * @param toFile
235    * @param annotated
236    * @throws IOException
237    */
238   public void exportStockholm(SequenceI[] seqs, File toFile,
239           AnnotatedCollectionI annotated)
240           throws IOException
241   {
242     if (seqs == null)
243     {
244       return;
245     }
246     AlignmentI newAl = new Alignment(seqs);
247
248     if (!newAl.isAligned())
249     {
250       newAl.padGaps();
251     }
252
253     if (toFile != null && annotated != null)
254     {
255       AlignmentAnnotation[] annots = annotated.getAlignmentAnnotation();
256       if (annots != null)
257       {
258         for (AlignmentAnnotation annot : annots)
259         {
260           if (annot.label.contains("Reference") || "RF".equals(annot.label))
261           {
262             AlignmentAnnotation newRF;
263             if (annot.annotations.length > newAl.getWidth())
264             {
265               Annotation[] rfAnnots = new Annotation[newAl.getWidth()];
266               System.arraycopy(annot.annotations, 0, rfAnnots, 0,
267                       rfAnnots.length);
268               newRF = new AlignmentAnnotation("RF", "Reference Positions",
269                       rfAnnots);
270             }
271             else
272             {
273               newRF = new AlignmentAnnotation(annot);
274             }
275             newAl.addAnnotation(newRF);
276           }
277         }
278       }
279     }
280
281     for (SequenceI seq : newAl.getSequencesArray())
282     {
283       if (seq.getAnnotation() != null)
284       {
285         for (AlignmentAnnotation ann : seq.getAnnotation())
286         {
287           seq.removeAlignmentAnnotation(ann);
288         }
289       }
290     }
291
292     StockholmFile file = new StockholmFile(newAl);
293     String output = file.print(seqs, false);
294     PrintWriter writer = new PrintWriter(toFile);
295     writer.println(output);
296     writer.close();
297   }
298
299   /**
300    * Answers the full path to the given hmmer executable, or null if file cannot
301    * be found or is not executable
302    * 
303    * @param cmd
304    *          command short name e.g. hmmalign
305    * @return
306    * @throws IOException
307    */
308   protected String getCommandPath(String cmd)
309           throws IOException
310   {
311     String binariesFolder = Cache.getProperty(Preferences.HMMER_PATH);
312     // ensure any symlink to the directory is resolved:
313     binariesFolder = Paths.get(binariesFolder).toRealPath().toString();
314     File file = FileUtils.getExecutable(cmd, binariesFolder);
315     if (file == null && af != null)
316     {
317       JvOptionPane.showInternalMessageDialog(af, MessageManager
318               .formatMessage("label.executable_not_found", cmd));
319     }
320
321     return file == null ? null : getFilePath(file, true);
322   }
323
324   /**
325    * Exports an HMM to the specified file
326    * 
327    * @param hmm
328    * @param hmmFile
329    * @throws IOException
330    */
331   public void exportHmm(HiddenMarkovModel hmm, File hmmFile)
332           throws IOException
333   {
334     if (hmm != null)
335     {
336       HMMFile file = new HMMFile(hmm);
337       PrintWriter writer = new PrintWriter(hmmFile);
338       writer.print(file.print());
339       writer.close();
340     }
341   }
342
343   // TODO is needed?
344   /**
345    * Exports a sequence to the specified file
346    * 
347    * @param hmm
348    * @param hmmFile
349    * @throws IOException
350    */
351   public void exportSequence(SequenceI seq, File seqFile) throws IOException
352   {
353     if (seq != null)
354     {
355       FastaFile file = new FastaFile();
356       PrintWriter writer = new PrintWriter(seqFile);
357       writer.print(file.print(new SequenceI[] { seq }, false));
358       writer.close();
359     }
360   }
361
362   /**
363    * Answers the HMM profile for the profile sequence the user selected (default
364    * is just the first HMM sequence in the alignment)
365    * 
366    * @return
367    */
368   protected HiddenMarkovModel getHmmProfile()
369   {
370     String alignToParamName = MessageManager.getString("label.use_hmm");
371     for (ArgumentI arg : params)
372     {
373       String name = arg.getName();
374       if (name.equals(alignToParamName))
375       {
376         String seqName = arg.getValue();
377         SequenceI hmmSeq = alignment.findName(seqName);
378         if (hmmSeq.hasHMMProfile())
379         {
380           return hmmSeq.getHMM();
381         }
382       }
383     }
384     return null;
385   }
386
387   /**
388    * Answers the HMM profile for the profile sequence the user selected (default
389    * is just the first HMM sequence in the alignment)
390    * 
391    * @return
392    */
393   protected SequenceI getSequence()
394   {
395     String alignToParamName = MessageManager
396             .getString("label.use_sequence");
397     for (ArgumentI arg : params)
398     {
399       String name = arg.getName();
400       if (name.equals(alignToParamName))
401       {
402         String seqName = arg.getValue();
403         SequenceI seq = alignment.findName(seqName);
404         return seq;
405       }
406     }
407     return null;
408   }
409
410   /**
411    * Answers an absolute path to the given file, in a format suitable for
412    * processing by a hmmer command. On a Windows platform, the native Windows file
413    * path is converted to Cygwin format, by replacing '\'with '/' and drive letter
414    * X with /cygdrive/x.
415    * 
416    * @param resultFile
417    * @param isInCygwin
418    *                     True if file is to be read/written from within the Cygwin
419    *                     shell. Should be false for any imports.
420    * @return
421    */
422   protected String getFilePath(File resultFile, boolean isInCygwin)
423   {
424     String path = resultFile.getAbsolutePath();
425     if (Platform.isWindows() && isInCygwin)
426     {
427       // the first backslash escapes '\' for the regular expression argument
428       path = path.replaceAll("\\" + File.separator, "/");
429       int colon = path.indexOf(':');
430       if (colon > 0)
431       {
432         String drive = path.substring(0, colon);
433         path = path.replaceAll(drive + ":", "/cygdrive/" + drive);
434       }
435     }
436
437     return path;
438   }
439
440   /**
441    * A helper method that deletes any HMM consensus sequence from the given
442    * collection, and from the parent alignment if <code>ac</code> is a subgroup
443    * 
444    * @param ac
445    */
446   void deleteHmmSequences(AnnotatedCollectionI ac)
447   {
448     List<SequenceI> hmmSeqs = ac.getHmmSequences();
449     for (SequenceI hmmSeq : hmmSeqs)
450     {
451       if (ac instanceof SequenceGroup)
452       {
453         ((SequenceGroup) ac).deleteSequence(hmmSeq, false);
454         AnnotatedCollectionI context = ac.getContext();
455         if (context != null && context instanceof AlignmentI)
456         {
457           ((AlignmentI) context).deleteSequence(hmmSeq);
458         }
459       }
460       else
461       {
462         ((AlignmentI) ac).deleteSequence(hmmSeq);
463       }
464     }
465   }
466
467   /**
468    * Sets the names of any duplicates within the given sequences to include their
469    * respective lengths. Deletes any duplicates that have the same name after this
470    * step
471    * 
472    * @param seqs
473    */
474   void renameDuplicates(AlignmentI al)
475   {
476
477     SequenceI[] seqs = al.getSequencesArray();
478     List<Boolean> wasRenamed = new ArrayList<>();
479
480     for (SequenceI seq : seqs)
481     {
482       wasRenamed.add(false);
483     }
484
485     for (int i = 0; i < seqs.length; i++)
486     {
487       for (int j = 0; j < seqs.length; j++)
488       {
489         if (seqs[i].getName().equals(seqs[j].getName()) && i != j
490                 && !wasRenamed.get(j))
491         {
492
493           wasRenamed.set(i, true);
494           String range = "/" + seqs[j].getStart() + "-" + seqs[j].getEnd();
495           // setting sequence name to include range - to differentiate between
496           // sequences of the same name. Currently have to include the range twice
497           // because the range is removed (once) when setting the name
498           // TODO come up with a better way of doing this
499           seqs[j].setName(seqs[j].getName() + range + range);
500         }
501
502       }
503       if (wasRenamed.get(i))
504       {
505         String range = "/" + seqs[i].getStart() + "-" + seqs[i].getEnd();
506         seqs[i].setName(seqs[i].getName() + range + range);
507       }
508     }
509
510     for (int i = 0; i < seqs.length; i++)
511     {
512       for (int j = 0; j < seqs.length; j++)
513       {
514         if (seqs[i].getName().equals(seqs[j].getName()) && i != j)
515         {
516           al.deleteSequence(j);
517         }
518       }
519     }
520   }
521
522 }