JAL-3013 resolve symlink to hmmer binaries folder
[jalview.git] / src / jalview / hmmer / HmmerCommand.java
index 0f45184..b5c1b25 100644 (file)
@@ -1,5 +1,6 @@
 package jalview.hmmer;
 
+import jalview.analysis.SeqsetUtils;
 import jalview.bin.Cache;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentAnnotation;
@@ -13,13 +14,18 @@ import jalview.gui.JvOptionPane;
 import jalview.gui.Preferences;
 import jalview.io.HMMFile;
 import jalview.io.StockholmFile;
+import jalview.util.FileUtils;
 import jalview.util.MessageManager;
+import jalview.util.Platform;
+import jalview.ws.params.ArgumentI;
 
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.PrintWriter;
+import java.nio.file.Paths;
+import java.util.ArrayList;
 import java.util.Hashtable;
 import java.util.List;
 
@@ -29,74 +35,84 @@ import java.util.List;
  * @author TZVanaalten
  *
  */
-public class HmmerCommand
+public abstract class HmmerCommand implements Runnable
 {
   public static final String HMMBUILD = "hmmbuild";
 
-  public String JALVIEWDIRECTORY = System.getProperty("user.dir")
-          + File.separator;
+  protected final AlignFrame af;
 
-  public String OUTPUTALIGNMENT;
+  protected final AlignmentI alignment;
 
-  public final String SPACE = " ";
+  protected final List<ArgumentI> params;
 
-  public final String ALLCOL = "--allcol ";
-
-  public final String TRIM = "--trim ";
-
-  public final String FORCEAMINO = "--amino ";
-
-  public final String FORCEDNA = "--dna ";
-
-  public final String FORCERNA = "--rna ";
-
-  Hashtable hash = new Hashtable();
-
-  List<SequenceI> hmmSeqs;
-
-  protected AlignFrame af;
+  /**
+   * Constructor
+   * 
+   * @param alignFrame
+   * @param args
+   */
+  public HmmerCommand(AlignFrame alignFrame, List<ArgumentI> args)
+  {
+    af = alignFrame;
+    alignment = af.getViewport().getAlignment();
+    params = args;
+  }
 
+  /**
+   * Answers true if preference HMMER_PATH is set, and its value is the path to
+   * a directory that contains an executable <code>hmmbuild</code> or
+   * <code>hmmbuild.exe</code>, else false
+   * 
+   * @return
+   */
   public static boolean isHmmerAvailable()
   {
-    File exec = getExecutable(HMMBUILD, Cache.getProperty(Preferences.HMMER_PATH));
+    File exec = FileUtils.getExecutable(HMMBUILD,
+            Cache.getProperty(Preferences.HMMER_PATH));
     return exec != null;
   }
 
   /**
    * Uniquifies the sequences when exporting and stores their details in a
-   * hashtable.
+   * hashtable
    * 
    * @param seqs
    */
-  public void uniquifySequences(SequenceI[] seqs)
+  protected Hashtable stashSequences(SequenceI[] seqs)
   {
-    hash = jalview.analysis.SeqsetUtils.uniquify(seqs, true);
+    return SeqsetUtils.uniquify(seqs, true);
   }
 
   /**
-   * Recover the sequence data lost by uniquifying.
+   * Restores the sequence data lost by uniquifying
    * 
+   * @param hashtable
    * @param seqs
    */
-  public void recoverSequenceNames(SequenceI[] seqs)
+  protected void recoverSequences(Hashtable hashtable, SequenceI[] seqs)
   {
-    jalview.analysis.SeqsetUtils.deuniquify(hash, seqs);
+    SeqsetUtils.deuniquify(hashtable, seqs);
   }
 
   /**
-   * Runs a command in the command line.
+   * Runs a command as a separate process and waits for it to complete. Answers
+   * true if the process return status is zero, else false.
    * 
-   * @param command
+   * @param commands
+   *          the executable command and any arguments to it
    * @throws IOException
-   * @throws InterruptedException
    */
-  public boolean runCommand(String command)
-          throws IOException, InterruptedException
+  public boolean runCommand(List<String> commands)
+          throws IOException
   {
+    List<String> args = Platform.isWindows() ? wrapWithCygwin(commands)
+            : commands;
+
     try
     {
-      final Process p = Runtime.getRuntime().exec(command);
-
+      ProcessBuilder pb = new ProcessBuilder(args);
+      pb.redirectErrorStream(true); // merge syserr to sysout
+      final Process p = pb.start();
       new Thread(new Runnable()
       {
         @Override
@@ -104,13 +120,13 @@ public class HmmerCommand
         {
           BufferedReader input = new BufferedReader(
                   new InputStreamReader(p.getInputStream()));
-          String line = null;
-
           try
           {
-            while ((line = input.readLine()) != null)
+            String line = input.readLine();
+            while (line != null)
             {
               System.out.println(line);
+              line = input.readLine();
             }
           } catch (IOException e)
           {
@@ -120,30 +136,82 @@ public class HmmerCommand
       }).start();
 
       p.waitFor();
+      int exitValue = p.exitValue();
+      if (exitValue != 0)
+      {
+        Cache.log.error("Command failed, return code = " + exitValue);
+        Cache.log.error("Command/args were: " + args.toString());
+      }
+      return exitValue == 0; // 0 is success, by convention
     } catch (Exception e)
     {
       e.printStackTrace();
       return false;
     }
-    return true;
   }
 
   /**
-   * Exports an alignment and/or HMM to the specified file.
+   * Converts the given command to a Cygwin "bash" command wrapper. The hmmer
+   * command and any arguments to it are converted into a single parameter to the
+   * bash command.
+   * 
+   * @param commands
+   */
+  protected List<String> wrapWithCygwin(List<String> commands)
+  {
+    File bash = FileUtils.getExecutable("bash",
+            Cache.getProperty(Preferences.CYGWIN_PATH));
+    if (bash == null)
+    {
+      Cache.log.error("Cygwin shell not found");
+      return commands;
+    }
+
+    List<String> wrapped = new ArrayList<>();
+    wrapped.add(bash.getAbsolutePath());
+    wrapped.add("-c");
+
+    /*
+     * combine hmmbuild/search/align and arguments to a single string
+     */
+    StringBuilder sb = new StringBuilder();
+    for (String cmd : commands)
+    {
+      sb.append(" ").append(cmd);
+    }
+    wrapped.add(sb.toString());
+
+    return wrapped;
+  }
+
+  /**
+   * Exports an alignment, and reference (RF) annotation if present, to the
+   * specified file, in Stockholm format
    * 
-   * @param alignment
+   * @param seqs
+   * @param toFile
+   * @param annotated
    * @throws IOException
    */
-  public void exportData(SequenceI[] seqs, File stoLocation,
-          HiddenMarkovModel hmm, File hmmLocation, AnnotatedCollectionI al)
-          throws IOException
+  public void exportStockholm(SequenceI[] seqs, File toFile,
+          AnnotatedCollectionI annotated) throws IOException
   {
-    if (seqs != null)
+    if (seqs == null)
+    {
+      return;
+    }
+    AlignmentI newAl = new Alignment(seqs);
+    if (!newAl.isAligned())
     {
-      AlignmentI newAl = new Alignment(seqs);
-      if (stoLocation != null && al != null)
+      newAl.padGaps();
+    }
+
+    if (toFile != null && annotated != null)
+    {
+      AlignmentAnnotation[] annots = annotated.getAlignmentAnnotation();
+      if (annots != null)
       {
-        for (AlignmentAnnotation annot : al.getAlignmentAnnotation())
+        for (AlignmentAnnotation annot : annots)
         {
           if (annot.label.contains("Reference") || "RF".equals(annot.label))
           {
@@ -164,112 +232,107 @@ public class HmmerCommand
           }
         }
       }
-
-      StockholmFile file = new StockholmFile(newAl);
-      String output = file.print(seqs, false);
-      PrintWriter writer = new PrintWriter(stoLocation);
-      writer.println(output);
-      writer.close();
     }
 
-    if (hmm != null)
-    {
-      HMMFile file = new HMMFile(hmm);
-      PrintWriter writer = new PrintWriter(hmmLocation);
-      writer.print(file.print());
-      writer.close();
-    }
+    StockholmFile file = new StockholmFile(newAl);
+    String output = file.print(seqs, false);
+    PrintWriter writer = new PrintWriter(toFile);
+    writer.println(output);
+    writer.close();
   }
 
   /**
-   * Adds any HMM sequences removed before submitting the alignment as a job
-   * back into the alignment.
+   * Answers the full path to the given hmmer executable, or null if file cannot
+   * be found or is not executable
    * 
-   * @param af
+   * @param cmd
+   *          command short name e.g. hmmalign
+   * @return
+   * @throws IOException
    */
-  public void addHMMConsensusSequences(AlignFrame af)
+  protected String getCommandPath(String cmd) throws IOException
   {
-    AlignmentI al = af.getViewport().getAlignment();
-    if (hmmSeqs == null || hmmSeqs.size() < 1)
-    {
-      return;
-    }
-    for (SequenceI seq : hmmSeqs)
+    String binariesFolder = Cache.getProperty(Preferences.HMMER_PATH);
+    // ensure any symlink to the directory is resolved:
+    binariesFolder = Paths.get(binariesFolder).toRealPath().toString();
+    File file = FileUtils.getExecutable(cmd, binariesFolder);
+    if (file == null && af != null)
     {
-      Integer position = seq.getPreviousPosition();
-      al.getSequences().add(position, seq);
+      JvOptionPane.showInternalMessageDialog(af, MessageManager
+              .formatMessage("label.executable_not_found", cmd));
     }
-    af.getViewport().setAlignment(al);
-    af.alignPanel.adjustAnnotationHeight();
-    af.getViewport().updateSequenceIdColours();
-    af.buildSortByAnnotationScoresMenu();
-  }
 
-  /**
-   * Returns the list of HMM sequences removed
-   * 
-   * @return
-   */
-  public List<SequenceI> getHmmSeqs()
-  {
-    return hmmSeqs;
+    return file == null ? null : getFilePath(file);
   }
 
   /**
-   * Sets the list of removed HMM sequences
+   * Exports an HMM to the specified file
    * 
-   * @param hmmSeqs
+   * @param hmm
+   * @param hmmFile
+   * @throws IOException
    */
-  public void setHmmSeqs(List<SequenceI> hmmSeqs)
+  public void exportHmm(HiddenMarkovModel hmm, File hmmFile)
+          throws IOException
   {
-    this.hmmSeqs = hmmSeqs;
+    if (hmm != null)
+    {
+      HMMFile file = new HMMFile(hmm);
+      PrintWriter writer = new PrintWriter(hmmFile);
+      writer.print(file.print());
+      writer.close();
+    }
   }
 
   /**
-   * Answers the full path to the given hmmer executable, or null if file cannot
-   * be found or is not executable
+   * Answers the HMM profile for the profile sequence the user selected (default
+   * is just the first HMM sequence in the alignment)
    * 
-   * @param cmd
-   *          command short name e.g. hmmalign
    * @return
    */
-  protected String getCommandRoot(String cmd)
+  protected HiddenMarkovModel getHmmProfile()
   {
-    String binariesFolder = Cache.getProperty(Preferences.HMMER_PATH);
-    File file = getExecutable(cmd, binariesFolder);
-    if (file == null && af != null)
+    String alignToParamName = MessageManager.getString("label.use_hmm");
+    for (ArgumentI arg : params)
     {
-        JvOptionPane.showInternalMessageDialog(af,
-                MessageManager.getString("warn.hmm_command_failed"));
+      String name = arg.getName();
+      if (name.equals(alignToParamName))
+      {
+        String seqName = arg.getValue();
+        SequenceI hmmSeq = alignment.findName(seqName);
+        if (hmmSeq.hasHMMProfile())
+        {
+          return hmmSeq.getHMM();
+        }
+      }
     }
-
-    return file == null ? null : file.getAbsolutePath();
+    return null;
   }
 
   /**
-   * Answers the executable file for the given hmmer command, or null if not
-   * found or not executable. The path to the executable is the command name
-   * prefixed by the hmmer binaries folder path, optionally with .exe appended.
+   * Answers an absolute path to the given file, in a format suitable for
+   * processing by a hmmer command. On a Windows platform, the native Windows file
+   * path is converted to Cygwin format, by replacing '\'with '/' and drive letter
+   * X with /cygdrive/x.
    * 
-   * @param cmd
-   *          hmmer command short name, for example hmmbuild
-   * @param binaryPath
-   *          parent folder containing hmmer executables
+   * @param resultFile
    * @return
    */
-  public static File getExecutable(String cmd, String binaryPath)
+  protected String getFilePath(File resultFile)
   {
-    File file = new File(binaryPath, cmd);
-    if (!file.canExecute())
+    String path = resultFile.getAbsolutePath();
+    if (Platform.isWindows())
     {
-      file = new File(binaryPath, cmd + ".exe");
+      // the first backslash escapes '\' for the regular expression argument
+      path = path.replaceAll("\\" + File.separator, "/");
+      int colon = path.indexOf(':');
+      if (colon > 0)
       {
-        if (!file.canExecute())
-        {
-          file = null;
-        }
+        String drive = path.substring(0, colon);
+        path = path.replaceAll(drive + ":", "/cygdrive/" + drive);
       }
     }
-    return file;
+
+    return path;
   }
 }