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