SvnTask.java

package uk.co.researchkitchen.javasvn.ant;

import java.io.File;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import org.tmatesoft.svn.core.ISVNWorkspace;
import org.tmatesoft.svn.core.SVNStatus;
import org.tmatesoft.svn.core.SVNWorkspaceAdapter;
import org.tmatesoft.svn.core.SVNWorkspaceManager;
import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory;
import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl;
import org.tmatesoft.svn.core.internal.ws.fs.FSEntryFactory;
import org.tmatesoft.svn.core.io.SVNException;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.io.SVNRepositoryLocation;
import org.tmatesoft.svn.core.io.SVNSimpleCredentialsProvider;
import org.tmatesoft.svn.util.SVNUtil;

/**
 * 
 * @author rwinston
 *
 * Ant task that encapsulates some simple Subversion functionality (e.g. checkout) in pure Java, using the
 * JavaSVN libraries from http://tmate.org/svn/
 * 
 * <svn commmand="checkout" username="foo" password="bar" svnRoot="http://localhost/svn/sample/trunk" 
 * dest="/usr/sandbox/sample" revision="1" />
 * 
 */
public class SvnTask extends Task {

  // The URL of the project we are checking out (e.g. http://localhost/svn/sample/trunk)
  private String svnRoot = null;

  // The destination directory for a checkout
  private File dest = null;

  // The revision to check out
  private long revision = ISVNWorkspace.HEAD;

  // The default command to execute
  private static final String DEFAULT_COMMAND = "checkout";

  // The SVN command to execute
  private String command = null;

  // Credentials
  private String username = null;

  private String password = null;

  boolean usingCredentials = false;

  // The SVN repository location
  private SVNRepositoryLocation location = null;

  // The repository representation
  private SVNRepository repository = null;

  // Show SVN command output?
  private boolean verbose = false;

  // Are SVN operations recursive?
  private boolean recursive = true;

  // SVN Commit/Add/Delete/Move message
  private String message = null;

  /**
   * 
   * @param msg
   */
  public void setMessage(String msg) {
    message = msg;
  }

  /**
   * 
   * @return
   */
  public String getMessage() {
    return message;
  }

  /**
   * @return Returns the recursive flag.
   */
  public boolean getRecursive() {
    return recursive;
  }

  /**
   * @param recursive 
   */
  public void setRecursive(boolean recursive) {
    this.recursive = recursive;
  }

  /**
   * @return Returns the verbose.
   */
  public boolean getVerbose() {
    return verbose;
  }

  /**
   * @param verbose The verbose to set.
   */
  public void setVerbose(boolean verbose) {
    this.verbose = verbose;
  }

  /**
   * 
   */
  public SvnTask() {
    setTaskName("svn");
  }

  /**
   * @return Returns the password.
   */
  public String getPassword() {
    return password;
  }

  /**
   * @param password The password to set.
   */
  public void setPassword(String password) {
    this.password = password;
  }

  /**
   * @return Returns the username.
   */
  public String getUsername() {
    return username;
  }

  /**
   * @param username The username to set.
   */
  public void setUsername(String username) {
    this.username = username;
  }

  /**
   * @return Returns the command.
   */
  public String getCommand() {
    return command;
  }

  /**
   * @param command The command to set.
   */
  public void setCommand(String command) {
    this.command = command;
  }

  public long getRevision() {
    return revision;
  }

  public void setRevision(long revision) {
    this.revision = revision;
  }

  /**
   * @return Returns the dest.
   */
  public File getDest() {
    return dest;
  }

  /**
   * @param dest The dest to set.
   */
  public void setDest(File dest) {
    this.dest = dest;
  }

  /**
   * @return Returns the svnRoot.
   */
  public String getSvnRoot() {
    return svnRoot;
  }

  /**
   * @param svnRoot The svnRoot to set.
   */
  public void setSvnRoot(String svnRoot) {
    this.svnRoot = svnRoot;
  }

  /**
   * Set up and execute the relevant SVN command 
   *
   */
  public void execute() throws BuildException {

    if (command == null || command.equals("")) {
      command = DEFAULT_COMMAND;
    }

    if (username != null && password == null) {
      log("Password cannot be null if username is defined!");
      return;
    } else {
      usingCredentials = true;
    }

    try {
      // Initialize the DAV and local FS factories
      DAVRepositoryFactory.setup();
      FSEntryFactory.setup();
      SVNRepositoryFactoryImpl.setup();

      // SVN Root is not needed for every operation (e.g. update)
      if(svnRoot != null) {
        location = SVNRepositoryLocation.parseURL(svnRoot);
        repository = SVNRepositoryFactory.create(location);

        //  TODO how do we handle creds for updates? 
        if (usingCredentials){
          log("Setting credentials [" + username + "," + password + "]");
          repository.setCredentialsProvider(new SVNSimpleCredentialsProvider(username, password));
        }
      }

      if (dest == null) {
        dest = getProject().getBaseDir();
      }

      if (!dest.exists()) {
        dest.mkdirs();
      }

      // Execute the relevant SVN command  
      dispatchCommand(command);

    } catch (SVNException e) {
      throw new BuildException(e, getLocation());
    }
  }

  /**
   * Dispatch to a named method based on a command name (or abbreviation)
   * @param command
   * @throws BuildException
   */
  private void dispatchCommand(String command) throws BuildException {

    final HashMap commands = new HashMap();

    // Map of commands and short names ( See org.tmatesoft.svn.cli.SVNCommand for details)
    commands.put(new String[] { "status", "st", "stat" }, "doStatus");
    commands.put(new String[] { "import" }, "doImport");
    commands.put(new String[] { "checkout", "co" }, "doCheckout");
    commands.put(new String[] { "add" }, "doAdd");
    commands.put(new String[] { "commit", "ci" }, "doCommit");
    commands.put(new String[] { "update", "up" }, "doUpdate");
    commands.put(new String[] { "delete", "rm", "remove", "del" }, "doDelete");
    commands.put(new String[] { "move", "mv", "rename", "ren" }, "doMove");
    commands.put(new String[] { "copy", "cp" }, "doCopy");
    commands.put(new String[] { "revert" }, "doRevert");
    commands.put(new String[] { "mkdir" }, "doMkdir");
    commands.put(new String[] { "propset", "pset", "ps" }, "doPropSet");
    commands.put(new String[] { "propget", "pget", "pg" }, "doPropGet");
    commands.put(new String[] { "proplist", "plist", "pl" }, "doPropList");
    commands.put(new String[] { "info" }, "doInfo");
    commands.put(new String[] { "resolved" }, "doResolved");
    commands.put(new String[] { "cat" }, "doCat");
    commands.put(new String[] { "ls" }, "doLs");
    commands.put(new String[] { "log" }, "doLog");
    commands.put(new String[] { "switch", "sw" }, "doSwitch");

    String methName = null;

    // Search the command map
    for (Iterator keys = commands.keySet().iterator(); keys.hasNext();) {
      String[] names = (String[]) keys.next();
      for (int i = 0; i < names.length; i++) {
        if (command.equals(names[i])) {
          methName = (String) commands.get(names);
          break;
        }
      }
      if (methName != null) {
        break;
      }
    }
    if (methName == null) {
      log("Command name " + command + " not recognized");
      throw new BuildException();
    }

    // Now locate and execute the appropriate method
    Method[] methods = this.getClass().getMethods();
    Method theMethod = null;

    for (int mIndex = 0; mIndex < methods.length; ++mIndex) {
      if (methods[mIndex].getName().equals(methName)) {
        theMethod = methods[mIndex];
        break;
      }
    }

    if (theMethod == null) {
      log("Cannot find a method called " + methName + " for SVN command "
          + command);
      throw new BuildException();
    }

    try {
      theMethod.invoke(this, new Object[]{});
    } catch (Exception e) {
      throw new BuildException(e, getLocation());
    }

  }

  /**
   * Execute a svn checkout
   * @throws SVNException
   */
  public void doCheckout() throws SVNException {
    ISVNWorkspace workspace = SVNWorkspaceManager.createWorkspace("file",
        dest.getAbsolutePath());

    if (verbose) {
      workspace.addWorkspaceListener(new AntSVNWorkSpaceAdapter());
    }

    log("Checking out " + location.getPath() + " to "
        + dest.getAbsolutePath());
    revision = workspace.checkout(location, revision, false, recursive);
    log("Checked out " + location.getPath() + " [revision " + revision
        + "]");

  }

  /**
   * Execute a svn update
   * @throws SVNException
   */
  public void doUpdate() throws SVNException {
    ISVNWorkspace workspace = SVNUtil.createWorkspace(dest.getAbsolutePath());
    final String path = SVNUtil.getWorkspacePath(workspace, dest.getAbsolutePath());

    if (verbose) {
      workspace.addWorkspaceListener(new AntSVNWorkSpaceAdapter());
    }

    log("Updating: [" + dest.getAbsolutePath() + "]");
    revision = workspace.update(dest.getAbsolutePath(), revision, recursive);
    log("Updated to revision " + revision);
  }

  /**
   * Executes a svn commit
   * @throws SVNException
   */
  public void doCommit() throws SVNException {
    final ISVNWorkspace workspace = SVNUtil.createWorkspace(dest.getAbsolutePath());

    // We won't necessarily have a repository instance for commits, so we 
    // explicitly set the credentials on the ISVNWorkspace
    if(usingCredentials)
      workspace.setCredentials(username, password);

    if(verbose) {
      workspace.addWorkspaceListener(new AntSVNWorkSpaceAdapter());
    }

    log("Committing changes in: " + dest.getAbsolutePath());
    log("Using credentials: " + usingCredentials);
    log("Message = " + message);

    log("Committing in " + dest.getAbsolutePath());

    // TODO The first parameter is being misused here
    long revision = workspace.commit(new String[]{""}, message, true, false);

    if(revision <= 0)
      log("Nothing to commit");
    else
      log("Committed at revision " + revision);

  }

  /**
   * Nested class that implements logging of some SVN command output
   * 
   */
  class AntSVNWorkSpaceAdapter extends SVNWorkspaceAdapter {

    private AntSVNWorkSpaceAdapter() {
    }

    /**
     * Update handler
     */
    public void updated(String updatedPath, int contentsStatus,
        int propertiesStatus, long rev) {
      char contents = 'U';
      char properties = ' ';

      if (contentsStatus == SVNStatus.ADDED) {
        contents = 'A';
      } else if (contentsStatus == SVNStatus.DELETED) {
        contents = 'D';
      } else if (contentsStatus == SVNStatus.MERGED) {
        contents = 'G';
      } else if (contentsStatus == SVNStatus.CONFLICTED) {
        contents = 'C';
      } else if (contentsStatus == SVNStatus.NOT_MODIFIED) {
        contents = ' ';
      } else if (contentsStatus == SVNStatus.CORRUPTED) {
        contents = 'U';
      }

      if (propertiesStatus == SVNStatus.UPDATED) {
        properties = 'U';
      } else if (propertiesStatus == SVNStatus.CONFLICTED) {
        properties = 'C';
      }

      log(contents + "" + properties + ' ' + updatedPath);

      if (contents == ' ' && properties == ' ') {
        return;
      }

      log(contents + "" + properties + ' ' + updatedPath);
      if (contentsStatus == SVNStatus.CORRUPTED) {
        log("svn: Checksum error: base version of file '" + updatedPath
            + "' is corrupted and was not updated.");
      }
    }

    /**
     * Commit handler
     */
    public void committed(String committedPath, int kind) {
            log("Committing: " + committedPath);

            String verb = "Sending ";
            if (kind == SVNStatus.ADDED) {
                verb = "Adding ";
                // TODO we need a reference to the workspace if we want to deduce MIME-type
                /*
                try {
                   String mimeType = workspace.getPropertyValue(committedPath, SVNProperty.MIME_TYPE);

                    if (mimeType != null && !mimeType.startsWith("text")) {
                        verb += " (bin) ";
                    }
                    log("mimetype: " + mimeType);
                } catch (SVNException e1) {
                    DebugLog.error(e1);
                }
                */
            } else if (kind == SVNStatus.DELETED) {
                verb = "Deleting ";
            } else if (kind == SVNStatus.REPLACED) {
                verb = "Replacing ";
            }

            log(verb + committedPath);
        }
  }
}