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