JAL-2629 fix file format issues preventing hmmsearch, jackhmmer running
[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, boolean removeSS)
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 (removeSS && seq.getAnnotation() != null)
284       {
285         for (AlignmentAnnotation ann : seq.getAnnotation())
286         {
287           // TODO investigate how to make hmmsearch and jackhmmer work with annotations
288           /*
289           if (ann.label.equals("Secondary Structure"))
290           {
291             seq.removeAlignmentAnnotation(ann);
292           }
293           */
294           seq.removeAlignmentAnnotation(ann);
295         }
296       }
297     }
298
299     StockholmFile file = new StockholmFile(newAl);
300     String output = file.print(seqs, false);
301     PrintWriter writer = new PrintWriter(toFile);
302     writer.println(output);
303     writer.close();
304   }
305
306   /**
307    * Answers the full path to the given hmmer executable, or null if file cannot
308    * be found or is not executable
309    * 
310    * @param cmd
311    *          command short name e.g. hmmalign
312    * @return
313    * @throws IOException
314    */
315   protected String getCommandPath(String cmd)
316           throws IOException
317   {
318     String binariesFolder = Cache.getProperty(Preferences.HMMER_PATH);
319     // ensure any symlink to the directory is resolved:
320     binariesFolder = Paths.get(binariesFolder).toRealPath().toString();
321     File file = FileUtils.getExecutable(cmd, binariesFolder);
322     if (file == null && af != null)
323     {
324       JvOptionPane.showInternalMessageDialog(af, MessageManager
325               .formatMessage("label.executable_not_found", cmd));
326     }
327
328     return file == null ? null : getFilePath(file, true);
329   }
330
331   /**
332    * Exports an HMM to the specified file
333    * 
334    * @param hmm
335    * @param hmmFile
336    * @throws IOException
337    */
338   public void exportHmm(HiddenMarkovModel hmm, File hmmFile)
339           throws IOException
340   {
341     if (hmm != null)
342     {
343       HMMFile file = new HMMFile(hmm);
344       PrintWriter writer = new PrintWriter(hmmFile);
345       writer.print(file.print());
346       writer.close();
347     }
348   }
349
350   // TODO is needed?
351   /**
352    * Exports a sequence to the specified file
353    * 
354    * @param hmm
355    * @param hmmFile
356    * @throws IOException
357    */
358   public void exportSequence(SequenceI seq, File seqFile) throws IOException
359   {
360     if (seq != null)
361     {
362       FastaFile file = new FastaFile();
363       PrintWriter writer = new PrintWriter(seqFile);
364       writer.print(file.print(new SequenceI[] { seq }, false));
365       writer.close();
366     }
367   }
368
369   /**
370    * Answers the HMM profile for the profile sequence the user selected (default
371    * is just the first HMM sequence in the alignment)
372    * 
373    * @return
374    */
375   protected HiddenMarkovModel getHmmProfile()
376   {
377     String alignToParamName = MessageManager.getString("label.use_hmm");
378     for (ArgumentI arg : params)
379     {
380       String name = arg.getName();
381       if (name.equals(alignToParamName))
382       {
383         String seqName = arg.getValue();
384         SequenceI hmmSeq = alignment.findName(seqName);
385         if (hmmSeq.hasHMMProfile())
386         {
387           return hmmSeq.getHMM();
388         }
389       }
390     }
391     return null;
392   }
393
394   /**
395    * Answers the HMM profile for the profile sequence the user selected (default
396    * is just the first HMM sequence in the alignment)
397    * 
398    * @return
399    */
400   protected SequenceI getSequence()
401   {
402     String alignToParamName = MessageManager
403             .getString("label.use_sequence");
404     for (ArgumentI arg : params)
405     {
406       String name = arg.getName();
407       if (name.equals(alignToParamName))
408       {
409         String seqName = arg.getValue();
410         SequenceI seq = alignment.findName(seqName);
411         return seq;
412       }
413     }
414     return null;
415   }
416
417   /**
418    * Answers an absolute path to the given file, in a format suitable for
419    * processing by a hmmer command. On a Windows platform, the native Windows file
420    * path is converted to Cygwin format, by replacing '\'with '/' and drive letter
421    * X with /cygdrive/x.
422    * 
423    * @param resultFile
424    * @param isInCygwin
425    *                     True if file is to be read/written from within the Cygwin
426    *                     shell. Should be false for any imports.
427    * @return
428    */
429   protected String getFilePath(File resultFile, boolean isInCygwin)
430   {
431     String path = resultFile.getAbsolutePath();
432     if (Platform.isWindows() && isInCygwin)
433     {
434       // the first backslash escapes '\' for the regular expression argument
435       path = path.replaceAll("\\" + File.separator, "/");
436       int colon = path.indexOf(':');
437       if (colon > 0)
438       {
439         String drive = path.substring(0, colon);
440         path = path.replaceAll(drive + ":", "/cygdrive/" + drive);
441       }
442     }
443
444     return path;
445   }
446
447   /**
448    * A helper method that deletes any HMM consensus sequence from the given
449    * collection, and from the parent alignment if <code>ac</code> is a subgroup
450    * 
451    * @param ac
452    */
453   void deleteHmmSequences(AnnotatedCollectionI ac)
454   {
455     List<SequenceI> hmmSeqs = ac.getHmmSequences();
456     for (SequenceI hmmSeq : hmmSeqs)
457     {
458       if (ac instanceof SequenceGroup)
459       {
460         ((SequenceGroup) ac).deleteSequence(hmmSeq, false);
461         AnnotatedCollectionI context = ac.getContext();
462         if (context != null && context instanceof AlignmentI)
463         {
464           ((AlignmentI) context).deleteSequence(hmmSeq);
465         }
466       }
467       else
468       {
469         ((AlignmentI) ac).deleteSequence(hmmSeq);
470       }
471     }
472   }
473
474   void renameDuplicates(SequenceI[] seqs)
475   {
476     // rename duplicate sequences, hmmsearch fails db contains duplicates
477     for (int i = 0; i < seqs.length; i++)
478     {
479       boolean renamed = false;
480       for (int j = 0; j < seqs.length; j++)
481       {
482         renamed = true;
483         if (seqs[i].getName().equals(seqs[j].getName()) && i != j)
484         {
485           String range = "/" + seqs[j].getStart() + "-" + seqs[j].getEnd();
486           // setting sequence name to include range - to differentiate between
487           // sequences of the same name. Currently have to include the range twice
488           // because the range is removed (once) when setting the name
489           // TODO come up with a better way of doing this
490           seqs[j].setName(seqs[j].getName() + range + range);
491         }
492
493       }
494       if (renamed)
495       {
496         String range = "/" + seqs[i].getStart() + "-" + seqs[i].getEnd();
497         seqs[i].setName(seqs[i].getName() + range + range);
498       }
499     }
500   }
501
502 }