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