JAL-3013 resolve symlink to hmmer binaries folder
[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.SequenceI;
12 import jalview.gui.AlignFrame;
13 import jalview.gui.JvOptionPane;
14 import jalview.gui.Preferences;
15 import jalview.io.HMMFile;
16 import jalview.io.StockholmFile;
17 import jalview.util.FileUtils;
18 import jalview.util.MessageManager;
19 import jalview.util.Platform;
20 import jalview.ws.params.ArgumentI;
21
22 import java.io.BufferedReader;
23 import java.io.File;
24 import java.io.IOException;
25 import java.io.InputStreamReader;
26 import java.io.PrintWriter;
27 import java.nio.file.Paths;
28 import java.util.ArrayList;
29 import java.util.Hashtable;
30 import java.util.List;
31
32 /**
33  * Base class for hmmbuild, hmmalign and hmmsearch
34  * 
35  * @author TZVanaalten
36  *
37  */
38 public abstract class HmmerCommand implements Runnable
39 {
40   public static final String HMMBUILD = "hmmbuild";
41
42   protected final AlignFrame af;
43
44   protected final AlignmentI alignment;
45
46   protected final List<ArgumentI> params;
47
48   /**
49    * Constructor
50    * 
51    * @param alignFrame
52    * @param args
53    */
54   public HmmerCommand(AlignFrame alignFrame, List<ArgumentI> args)
55   {
56     af = alignFrame;
57     alignment = af.getViewport().getAlignment();
58     params = args;
59   }
60
61   /**
62    * Answers true if preference HMMER_PATH is set, and its value is the path to
63    * a directory that contains an executable <code>hmmbuild</code> or
64    * <code>hmmbuild.exe</code>, else false
65    * 
66    * @return
67    */
68   public static boolean isHmmerAvailable()
69   {
70     File exec = FileUtils.getExecutable(HMMBUILD,
71             Cache.getProperty(Preferences.HMMER_PATH));
72     return exec != null;
73   }
74
75   /**
76    * Uniquifies the sequences when exporting and stores their details in a
77    * hashtable
78    * 
79    * @param seqs
80    */
81   protected Hashtable stashSequences(SequenceI[] seqs)
82   {
83     return SeqsetUtils.uniquify(seqs, true);
84   }
85
86   /**
87    * Restores the sequence data lost by uniquifying
88    * 
89    * @param hashtable
90    * @param seqs
91    */
92   protected void recoverSequences(Hashtable hashtable, SequenceI[] seqs)
93   {
94     SeqsetUtils.deuniquify(hashtable, seqs);
95   }
96
97   /**
98    * Runs a command as a separate process and waits for it to complete. Answers
99    * true if the process return status is zero, else false.
100    * 
101    * @param commands
102    *          the executable command and any arguments to it
103    * @throws IOException
104    */
105   public boolean runCommand(List<String> commands)
106           throws IOException
107   {
108     List<String> args = Platform.isWindows() ? wrapWithCygwin(commands)
109             : commands;
110
111     try
112     {
113       ProcessBuilder pb = new ProcessBuilder(args);
114       pb.redirectErrorStream(true); // merge syserr to sysout
115       final Process p = pb.start();
116       new Thread(new Runnable()
117       {
118         @Override
119         public void run()
120         {
121           BufferedReader input = new BufferedReader(
122                   new InputStreamReader(p.getInputStream()));
123           try
124           {
125             String line = input.readLine();
126             while (line != null)
127             {
128               System.out.println(line);
129               line = input.readLine();
130             }
131           } catch (IOException e)
132           {
133             e.printStackTrace();
134           }
135         }
136       }).start();
137
138       p.waitFor();
139       int exitValue = p.exitValue();
140       if (exitValue != 0)
141       {
142         Cache.log.error("Command failed, return code = " + exitValue);
143         Cache.log.error("Command/args were: " + args.toString());
144       }
145       return exitValue == 0; // 0 is success, by convention
146     } catch (Exception e)
147     {
148       e.printStackTrace();
149       return false;
150     }
151   }
152
153   /**
154    * Converts the given command to a Cygwin "bash" command wrapper. The hmmer
155    * command and any arguments to it are converted into a single parameter to the
156    * bash command.
157    * 
158    * @param commands
159    */
160   protected List<String> wrapWithCygwin(List<String> commands)
161   {
162     File bash = FileUtils.getExecutable("bash",
163             Cache.getProperty(Preferences.CYGWIN_PATH));
164     if (bash == null)
165     {
166       Cache.log.error("Cygwin shell not found");
167       return commands;
168     }
169
170     List<String> wrapped = new ArrayList<>();
171     wrapped.add(bash.getAbsolutePath());
172     wrapped.add("-c");
173
174     /*
175      * combine hmmbuild/search/align and arguments to a single string
176      */
177     StringBuilder sb = new StringBuilder();
178     for (String cmd : commands)
179     {
180       sb.append(" ").append(cmd);
181     }
182     wrapped.add(sb.toString());
183
184     return wrapped;
185   }
186
187   /**
188    * Exports an alignment, and reference (RF) annotation if present, to the
189    * specified file, in Stockholm format
190    * 
191    * @param seqs
192    * @param toFile
193    * @param annotated
194    * @throws IOException
195    */
196   public void exportStockholm(SequenceI[] seqs, File toFile,
197           AnnotatedCollectionI annotated) throws IOException
198   {
199     if (seqs == null)
200     {
201       return;
202     }
203     AlignmentI newAl = new Alignment(seqs);
204     if (!newAl.isAligned())
205     {
206       newAl.padGaps();
207     }
208
209     if (toFile != null && annotated != null)
210     {
211       AlignmentAnnotation[] annots = annotated.getAlignmentAnnotation();
212       if (annots != null)
213       {
214         for (AlignmentAnnotation annot : annots)
215         {
216           if (annot.label.contains("Reference") || "RF".equals(annot.label))
217           {
218             AlignmentAnnotation newRF;
219             if (annot.annotations.length > newAl.getWidth())
220             {
221               Annotation[] rfAnnots = new Annotation[newAl.getWidth()];
222               System.arraycopy(annot.annotations, 0, rfAnnots, 0,
223                       rfAnnots.length);
224               newRF = new AlignmentAnnotation("RF", "Reference Positions",
225                       rfAnnots);
226             }
227             else
228             {
229               newRF = new AlignmentAnnotation(annot);
230             }
231             newAl.addAnnotation(newRF);
232           }
233         }
234       }
235     }
236
237     StockholmFile file = new StockholmFile(newAl);
238     String output = file.print(seqs, false);
239     PrintWriter writer = new PrintWriter(toFile);
240     writer.println(output);
241     writer.close();
242   }
243
244   /**
245    * Answers the full path to the given hmmer executable, or null if file cannot
246    * be found or is not executable
247    * 
248    * @param cmd
249    *          command short name e.g. hmmalign
250    * @return
251    * @throws IOException
252    */
253   protected String getCommandPath(String cmd) throws IOException
254   {
255     String binariesFolder = Cache.getProperty(Preferences.HMMER_PATH);
256     // ensure any symlink to the directory is resolved:
257     binariesFolder = Paths.get(binariesFolder).toRealPath().toString();
258     File file = FileUtils.getExecutable(cmd, binariesFolder);
259     if (file == null && af != null)
260     {
261       JvOptionPane.showInternalMessageDialog(af, MessageManager
262               .formatMessage("label.executable_not_found", cmd));
263     }
264
265     return file == null ? null : getFilePath(file);
266   }
267
268   /**
269    * Exports an HMM to the specified file
270    * 
271    * @param hmm
272    * @param hmmFile
273    * @throws IOException
274    */
275   public void exportHmm(HiddenMarkovModel hmm, File hmmFile)
276           throws IOException
277   {
278     if (hmm != null)
279     {
280       HMMFile file = new HMMFile(hmm);
281       PrintWriter writer = new PrintWriter(hmmFile);
282       writer.print(file.print());
283       writer.close();
284     }
285   }
286
287   /**
288    * Answers the HMM profile for the profile sequence the user selected (default
289    * is just the first HMM sequence in the alignment)
290    * 
291    * @return
292    */
293   protected HiddenMarkovModel getHmmProfile()
294   {
295     String alignToParamName = MessageManager.getString("label.use_hmm");
296     for (ArgumentI arg : params)
297     {
298       String name = arg.getName();
299       if (name.equals(alignToParamName))
300       {
301         String seqName = arg.getValue();
302         SequenceI hmmSeq = alignment.findName(seqName);
303         if (hmmSeq.hasHMMProfile())
304         {
305           return hmmSeq.getHMM();
306         }
307       }
308     }
309     return null;
310   }
311
312   /**
313    * Answers an absolute path to the given file, in a format suitable for
314    * processing by a hmmer command. On a Windows platform, the native Windows file
315    * path is converted to Cygwin format, by replacing '\'with '/' and drive letter
316    * X with /cygdrive/x.
317    * 
318    * @param resultFile
319    * @return
320    */
321   protected String getFilePath(File resultFile)
322   {
323     String path = resultFile.getAbsolutePath();
324     if (Platform.isWindows())
325     {
326       // the first backslash escapes '\' for the regular expression argument
327       path = path.replaceAll("\\" + File.separator, "/");
328       int colon = path.indexOf(':');
329       if (colon > 0)
330       {
331         String drive = path.substring(0, colon);
332         path = path.replaceAll(drive + ":", "/cygdrive/" + drive);
333       }
334     }
335
336     return path;
337   }
338 }