2012/03/05

Making a custom ant task (and debugging it!)

As I stated in my previous post I've been working in the improvement on the release process of one of our products. One of the steps in the release process requires uploading several files to a FPT server of our own. Now that I have the wrapper ftp library implemented, next step is to create a custom ant task.

Ask the ant!

If you are an Ant user, you probaly know the bunch of already existing tasks available (some here and here) that will surely cover all needs you can think of. Unfortunately for me, the existing FTP ant task was not working for me, so I decided to create my own.


I strongly recommend reading the official apache guides regarding custom tasks:

Will save you some headaches afterwards :D. Making story short, it all comes down to extend the org.apache.tools.ant.Task class in a class of your own and then implement the execute method, the one that actually "executes" the task whenever you use it in your ant files.

That said, let's focus in the problem at hand. I wanted to have a custom task that allowed me to use the ftp client library implemented in my previous post in order to be able to do basic ftp operations with our FTP server. Those operations include:

  • Connecting
  • Login
  • Disconnecting
  • Upload file
  • Upload directory
  • Download file
  • Donwload directory
  • Delete file
  • Delete directory
    Say hello to my little...custom ftp ant task


    I packaged the custom ant task in a single class. I won't go into boring details such as fields and methods unless necessary. Anyway, you can check out the code here whenever you please :).



    "I hate ants!!!!"
    Let us see the main method, the one that actually executes our ant task:

    /**
      * This is the method that actually executes the Ant task
      * 
      */
        public void execute() {
            
         //check that given parameters are correct
         if (checkParams()){
          
          //get connection type
          FtpClient.secure type;
          switch (connType){
           case 0:
            type = FtpClient.secure.FTP;
            break;
           case 1:
            type = FtpClient.secure.FTPS;
            break;
           case 2:
            type = FtpClient.secure.FTPES;
            break;
           default:
            type = FtpClient.secure.FTP;
          }
          
          //create a FTPClient object with given parameters
          ftpClient = new FtpClient(this.host, this.port, this.user, this.password, type, this.bypass);
          
          //setup first
          if (ftpClient.setupClient()){
              //try to connect
              if (ftpClient.connect()){
               //try to login
               if (ftpClient.login()){
                //once connected, depending on the action we do one thing or another
                switch (action){
                
                 //upload directory
                 case PUTDIR:
                  if (ftpClient.uploadDirFiles(this.sourceDir, this.destDir)){
                   log("Uploading directory was sucsseful");
                   //disconnect from ftp server
                   ftpClient.disconnect();
                   break;
                  }
                  else{
                   throw new BuildException("Uploading directory went wrong. Aborting build.");
                  }
                    //upload file
                 case PUTFILE:
                  if (ftpClient.uploadFile(this.file, destDir)){
                   log("Uploading file was sucsseful");
                   //disconnect from ftp server
                   ftpClient.disconnect();
                   break;
                  }
                  else{
                   throw new BuildException("Uploading file went wrong. Aborting build.");
                  }
                 //download directory
                 case GETDIR:
                  if (ftpClient.downloadDirFiles(this.sourceDir, this.destDir)){
                   log("Downloading directory was sucsseful");
                   //disconnect from ftp server
                   ftpClient.disconnect();
                   break;
                  }
                  else{
                   throw new BuildException("Uploading directory went wrong. Aborting build.");
                  }
                    //download file
                 case GETFILE:
                  if (ftpClient.downloadFile(this.file, this.sourceDir, this.destDir)){
                   log("Downloading file was sucsseful");
                   //disconnect from ftp server
                   ftpClient.disconnect();
                   break;
                  }
                  else{
                   throw new BuildException("Downloading file went wrong. Aborting build.");
                  }
                 //delete file
                 case DELFILE:
                  if (ftpClient.deleteFile(this.file, this.destDir)){
                   log("Deleting file " + this.file + "from directory " + this.destDir + " was sucsseful");
                   //disconnect from ftp server
                   ftpClient.disconnect();
                   break;
                  }
                  else{
                   throw new BuildException("Deleting file went wrong. Aborting build.");
                  }
                    //delete directory
                 case DELDIR:
                  if (ftpClient.deleteDir(this.destDir)){
                   log("Deleting directory " + this.destDir + " was sucsseful");
                   //disconnect from ftp server
                   ftpClient.disconnect();
                   break;
                  }
                  else{
                   throw new BuildException("Downloading file went wrong. Aborting build.");
                  }
                }
               }
              }       
          }
         }
        }
    

    As you can see it's quite a lot of work, since we have to process everything in a single method. First thing we do is calling checkParams, that (tada!) checks that passed parameters when calling the task are valid. These params are:

    • host: ftp server name or ip address
    • port: listening server port
    • user: username, if anonymous login is desired, "anonymous" must be provided
    • password: user password, if anonymous login, type anything, although some servers require a valid email address.
    • action: Action describes the desired operation to be performed. Possible values are mapped to the following ftp operations this way (action - operation): {putdir, upload directory}, {putfile, upload file}, {getdir,download directory}, {getfile, download file}, {deldir, delete directory}, {delfile, delete file}.
    • connType: This specifies the possible connection types (FTP, FTPS or FTPES). Mapping is {0, FTP}, {1, FTPS}, {2, FTPES}.  
    • destDir: destination directory used in all operations related with files and directories.
    • sourceDir: source directory used in all operations related with files and directories.
    • file: file name, depending in the operation to be performed it can be full path or file name only.
    /**
         * Helping method that validates parameters based on the required action to perform
         * 
         * @return true if all validation was successful
         */
     private boolean checkParams() {
      
      //since all operations required connection and login check related params first
      if (host.isEmpty() || (port<=0 || port > 65535) || user.isEmpty() || password.isEmpty()){
       throw new BuildException("Some of the mandatory parameters: host, port, user, password are empty or are invalid");
      }
      else{
       //based on the action, validation differs
       switch (action){
       
        case PUTDIR:
         //to upload a directory, both sourceDir and destDir are mandatory
         if (destDir.isEmpty() || sourceDir.isEmpty()){
          throw new BuildException("Cannot upload a directory if source folder or destination folder are not specified. Use sourceDir and destDir to specify both.");
         }
         else{
          return true;
         }
        case PUTFILE:
         //to upload a file both destDir and file are mandatory
         if (destDir.isEmpty() || file.isEmpty()){
          throw new BuildException("Cannot upload file with an unspecified destination folder or unspecified file name. Please use destDir and/or file parameter to specify");
         }
         else{
          return true;
         }
        case GETDIR:
         //to download a folder both sourceDir and destDir are mandatory
         if (destDir.isEmpty() || sourceDir.isEmpty()){
          throw new BuildException("Cannot download a directory if source folder or destination folder are not specified. Use sourceDir and destDir to specify both.");
         }
         else{
          return true;
         }
        case GETFILE:
         //to download a file destDir, sourceDir and file are mandatory
         if (destDir.isEmpty() || file.isEmpty() || sourceDir.isEmpty()){
          throw new BuildException("Cannot download file with an unspecified destination and/or source folder and/or unspecified file name. Please use destDir, sourceDir and/or file parameter to specify");
         }
         else{
          return true;
         }
        case DELFILE:
         //to delete a file destDir and file are mandatory
         if (destDir.isEmpty() || file.isEmpty()){
          throw new BuildException("Cannot delete file with an unspecified destination and/or unspecified file name. Please use destDir and/or file parameter to specify");
         }
         else{
          return true;
         }
        case DELDIR:
         //to delete a directory destDir is mandatory
         if (destDir.isEmpty()){
          throw new BuildException("Cannot delete directory with an unspecified destination folder. Please use destDir to specify the parameter.");
         }
         else{
          return true;
         }
        default:
         return false;
       }
      }
     }
    

    Right after checking parameters are valid, an instance of org.ftp.simpleclient.FtpClient, a class implemented in my previous post that wraps functionalities provided by ftp4j library. Once created and depending in the action to be performed, we use this object's methods to execute the desired ftp operation.

    I won't go into the details of each operation, since its out of the scope of this post and it won't provide you anything extra, as before, if you are feeling curious you can check out the code at github whenever you please.

    Debugging ant tasks 

    Now you have your custom ant task ready, you create an ant file to test it, give it a try and...fail! If this happens, you should try to debug the custom ant taks to see what happened.

    "Best bug tracking system ever"

    Debugging ant tasks is not as simple as plain old java debugging. While you can debug an Ant file adding breakpoints, digging inside the code of specific custom task will require you to add a remote debugger in order to be able to "catch" the running process.

    I will explain how to do this in Eclipse, altough I recon it can be achieved with all major java IDEs. First thing is to create a new run configuration for the ant file where you plan to use your customized new task. To do so, go to:

    Run -> External Tools -> External Tools configuration...

    Right click in Ant Build -> New and in the Main tab select your ant script in Buildfile field. Then go to JRE tab and insert the following JVM arguments:

    -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n


    New Ant Run configuration

    If you wonder what these arguments mean check this, although a bit updated (Java 1.5) still works.

    Once this is done, you must create a new Debug configuration for a remote Java application. To do so, navigate to:

    Run-> Debug configurations

    Drop down the list in the left column and right click in Remote Java Application -> New. Select the project name in the Project field. Default values for host and port are okay as long as you used the same ones for the Ant configuration (JVM arguments).

    Remote Java Application debug config
    Everything is ready for the test run! Add breakpoints wherever you consider necessary. In my case, I added one both in the ant script that uses the custom ant task as well as in the custom ant task, in the execute method.

    Right click in your ant script or task -> Debug As...-> Ant >Build first:

    Start the ant script first!
    Now BEFORE calling your custom ant task code, go to Run-> Debug Configurations and debug your previously created Java Remote Application config. This will start a separate thread that will debug your custom ant task code, provided that you included some breakpoints :) You can see in the following image how in my case, thread stopped in the execute method of my custom ant task.

    Custom task debugging thread
    After this point, it is up to you to decide what to do next...

    Check this fantastic article from Vitor Rodrigues if you want to know more. 

    Laters!

    References

    • http://ant.apache.org/manual/tasksoverview.html
    • http://ant.apache.org/external.html
    • https://github.com/internetmosquito/custom_ftp_ant_task
    • http://docs.oracle.com/javase/1.5.0/docs/guide/jpda/conninv.html
    • http://www.vitorrodrigues.com/blog/2009/07/10/debugging-ant-tasks-in-eclipse/





    No comments:

    Post a Comment