tags:

views:

21

answers:

0

I am building a wrapper for FTP (File Transfer Protocol) in order to support multiple FTP III party API for FTP. The intention of writing this wrapper is to facilitate the user to select and configure any III party FTP API through some configuration file and use it through the wrapper. I have written code for the same and wondering if there are scope of improvements, which I feel are definitely there.

Please see the code snippet below:

FTPServiceFactory.java

package com.cts.automation.base.service;

import com.cts.automation.base.exception.FTPException;

/**
 * Factory to create references for the FTP service implementation provided by the user. This is a
 * singleton class.
 * 
 * @author Gaurav Saini
 */
public class FTPServiceFactory {

    /**
     * Private constructor
     */
    private FTPServiceFactory() {

    }

    /**
     * Static method to create an instance of the service.
     * 
     * @param type Fully classified class name
     * @return Reference of type FTPBaseService
     * @throws FTPException Throws exception if something goes wrong during instance creation
     */
    public static FTPBaseService createInstance(String type) throws FTPException {
     FTPBaseService service = null;
     try {
      Class classType = Class.forName(type);
      service = (FTPBaseService) classType.newInstance();
     } catch (ClassNotFoundException cnfe) {
      throw new FTPException("Class not found : " + type);
     } catch (Exception e) {
      throw new FTPException("Problem intializing class : " + type);
     }
     return service;
    }
}

FTPBaseService.java

package com.cts.automation.base.service;

import java.io.IOException;
import java.util.Map;
import java.util.StringTokenizer;

import com.cts.automation.base.exception.ConfigParseException;
import com.cts.automation.base.exception.FTPException;
import com.cts.automation.ftp.field.PasswordField;
import com.cts.automation.ftp.field.TextField;
import com.cts.automation.ftp.util.CommonConstants;
import com.cts.automation.ftp.util.FtpUtilities;

/**
 * An abstract base service used to create the FTP services for various IIIrd party FTP
 * implementations.
 * 
 * @author Gaurav Saini
 */
public abstract class FTPBaseService {

    /**
     * A map of values read from the property file
     */
    private static Map<String, String> configMap;

    /**
     * Starts the service. This method does parsing of the config file and invokes the download
     * process for the service. You can't overload this method.
     * 
     * @return true if process ran successfully; false otherwise.
     * @throws FTPException Throws exception if something goes wrong during downloading
     */
    public final boolean startService() throws FTPException {
     boolean status = false;
     parseConfig();
     if (configMap != null && configMap.size() > 0) {
      System.out.println("Config parsed successfully: " + configMap.toString());
      setPasswordInMap();
      status = downloadFiles();
     } else {
      System.out.println("Config parsing failed.");
     }
     return status;
    }

    /**
     * This method does the actual downloading of files for each of the servers.
     * 
     * @return true if process ran successfully; false otherwise.
     */
    private boolean downloadFiles() throws FTPException {
     boolean status = false;
     int serverCount = Integer.parseInt(configMap.get(CommonConstants.SERVER_COUNT));
     for (int i = 0; i < serverCount; i++) {
      String serverIP = configMap.get(serverCount + "." + CommonConstants.SERVER_URL);
      String userName = configMap.get(serverCount + "." + CommonConstants.USERNAME);
      String password = configMap.get(serverCount + "." + CommonConstants.PASSWORD);
      String[] arrDirList = convertToArray(configMap.get(serverCount + "."
        + CommonConstants.REMOTE_DIR_LIST));
      String[] arrFileType = convertToArray(configMap.get(serverCount + "."
        + CommonConstants.FILE_TYPE_LIST));
      ;
      String localDir = getLocalDirPath(i);
      downloadFiles(i, serverIP, userName, password, arrDirList, arrFileType, localDir);
     }
     return status;
    }

    /**
     * Converts comma delimited string to an array of String
     * 
     * @param str Comma delimited string
     * @return Array of String
     */
    private String[] convertToArray(String str) {
     StringTokenizer st = new StringTokenizer(str, ";");
     String[] arrList = new String[st.countTokens()];
     for (int i = 0; i < arrList.length; i++) {
      arrList[i] = st.nextToken();
     }
     return arrList;
    }

    /**
     * Retrieve the local directory path from the configuration map. If LOCAL_DIR exists in the
     * global paths then for every server this path is used; otherwise server specific setting in
     * LOCAL_DIR_PATH is used.
     * 
     * @param index Server list index
     * @return Local directory path
     */
    private String getLocalDirPath(int index) {
     String localDir = configMap.get(CommonConstants.LOCAL_DIR);
     String localDirPath = null;
     if (localDir == null || localDir.equals("")) {
      localDirPath = configMap.get(index + "." + CommonConstants.LOCAL_DIR_PATH);
     } else {
      localDirPath = localDir;
     }
     return localDirPath;
    }

    /**
     * Sets the password in the configuration map. If IS_PROMPT_PASSWORD == 'Y' then you have to
     * enter password for each serevr entry; otherwise server specific entry is used.
     */
    private void setPasswordInMap() {
     if (configMap.get(CommonConstants.IS_PROMPT_PASSWORD).equalsIgnoreCase("Y")) {
      int serverCount = Integer.parseInt(configMap.get(CommonConstants.SERVER_COUNT));
      for (int i = 0; i < serverCount; i++) {
       System.out.println("Server - " + (i + 1));
       String passwordField = serverCount + "." + CommonConstants.PASSWORD;
       String password = getPassword();
       configMap.put(passwordField, password);
      }
     }
    }

    /**
     * Parse the config file and loads it in the map.
     * 
     * @throws FTPException Throws exception if something goes wrong during parsing
     */
    private void parseConfig() throws FTPException {
     try {
      String configPath = System.getProperty("config");
      configMap = FtpUtilities.parseConfig(configPath);
     } catch (ConfigParseException e) {
      System.out.println(e.toString());
     } catch (Exception e) {
      System.out.println(e.toString());
      throw new FTPException("System error. Please contact the vendor");
     }
    }

    /**
     * Abstract method that does the downloading of files.
     * 
     * @param index Server index from where the files are to be downloaded
     * @param serverIP Server's IP address from where the files are to be downloaded
     * @param userName Username used for FTP on that server
     * @param password Password used for FTP on that server
     * @param arrRemDirList String array of remote directories from where the files would be
     *            downloaded
     * @param arrFileType String array of file names which would be downloaded from the remote
     *            server
     * @param localDir Local directory path on your machine where the files would be stored.
     * @return true if process ran successfully; false otherwise.
     * @throws FTPException Throws exception if something goes wrong during downloading
     */
    protected abstract boolean downloadFiles(int index, String serverIP, String userName,
      String password, String[] arrRemDirList, String[] arrFileType, String localDir)
      throws FTPException;

    /**
     * Receive username as input from user
     * 
     * @return Username
     */
    protected String getUserName() {
     char username[] = null;
     try {
      username = new TextField().getValue(System.in, "\rEnter your FTP username: ");
     } catch (IOException ioe) {
      ioe.printStackTrace();
     }
     if (username == null) {
      System.out.println("\rNo username entered");
     }

     return String.valueOf(username);
    }

    /**
     * Receive password as input from user
     * 
     * @return Password
     */
    protected String getPassword() {
     char password[] = null;
     try {
      password = new PasswordField().getValue(System.in, "\010Enter your FTP password: ");
     } catch (IOException ioe) {
      ioe.printStackTrace();
     }
     if (password == null) {
      System.out.println("\010No password entered");
     }

     return String.valueOf(password);
    }
}

FtpUtilities.java

package com.cts.automation.ftp.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;

import com.cts.automation.base.exception.ConfigParseException;

/**
 * Utility class consisting of useful utility methods. This class cannot be extended.
 * 
 * @author Gaurav Saini
 */
public final class FtpUtilities {

    /**
     * Parse the configuration properties file and store it in a map.
     * 
     * @param configPath Absolute path to the property file, excluding the name of property file.
     * @return Map of properties
     * @throws ConfigParseException Exception thrown if something goes wrong during parsing.
     */
    public static Map<String, String> parseConfig(String configPath) throws ConfigParseException {
     File file = new File(configPath + File.separator + CommonConstants.CONFIG_FILE);
     Map<String, String> configMap = new HashMap<String, String>();
     try {
      InputStream is = new FileInputStream(file);
      ResourceBundle rb = new PropertyResourceBundle(is);

      // check for [CONFIG] and [SERVERS] block
      configMap = parseBlocks(configMap, rb);
     } catch (MissingResourceException mre) {
      throw new ConfigParseException("Key missing from the config file : <" + mre.getKey()
        + ">");
     } catch (FileNotFoundException fnfe) {
      System.out.println(fnfe.toString());
     } catch (IOException ioe) {
      System.out.println(ioe.toString());
     }
     return configMap;
    }

    /**
     * Parse the global and server blocks.
     * 
     * @param configMap Map of properties.
     * @param rb Reference to ResourceBundle.
     * @return Map of properties.
     * @throws ConfigParseException Exception thrown if something goes wrong during parsing.
     */
    private static Map<String, String> parseBlocks(Map<String, String> configMap, ResourceBundle rb)
      throws ConfigParseException {
     if (rb.getString(CommonConstants.CONFIG).equals("")
       && rb.getString(CommonConstants.SERVERS).equals("")) {
      // check for prompt-password field and its valid values Y or N
      String isPromptPassword = rb.getString(CommonConstants.IS_PROMPT_PASSWORD);
      if (!isPromptPassword.equals("")
        && (isPromptPassword.equalsIgnoreCase("Y") || isPromptPassword
          .equalsIgnoreCase("N"))) {
       configMap.put(CommonConstants.IS_PROMPT_PASSWORD, isPromptPassword);
       // check if GLOBAL local directory exists
       String localDir = rb.getString(CommonConstants.LOCAL_DIR);
       if (!localDir.equals("")) {
        configMap.put(CommonConstants.LOCAL_DIR, localDir);
       } else {
        System.out.println("Ignoring GLOBAL local path settings...");
       }

       // iterate every server block
       int serverCount = 0;
       while (true) {
        serverCount++;
        try {
         rb.getString(CommonConstants.BEGIN_TAG_PREF + serverCount
           + CommonConstants.BEGIN_TAG_SUFF);
        } catch (MissingResourceException mre) {
         // check for server block sequence. Must begin
         // with 1.
         if (serverCount == 1) {
          System.out
            .println("Either NO server blocks are present or they are NOT in sequence.");
          configMap = null;
         }
         // do nothing; break while loop on first
         // encounter of empty server "<>"
         break;
        }
        // check for [server-<##>] block
        parseServerBlock(configMap, rb, localDir, serverCount);
       }
       serverCount--;
       configMap.put(CommonConstants.SERVER_COUNT, new Integer(serverCount).toString());
      } else {
       throw new ConfigParseException("Invalid value in <"
         + CommonConstants.IS_PROMPT_PASSWORD + "> field.");
      }
     } else {
      throw new ConfigParseException("Problem in Config and Server section's declaration.");
     }
     return configMap;
    }

    /**
     * Parse the server block.
     * 
     * @param configMap Map of properties.
     * @param rb Reference to ResourceBundle.
     * @param localDir Local directory path.
     * @param serverCount Index of server.
     * @throws ConfigParseException Exception thrown if something goes wrong during parsing.
     */
    private static void parseServerBlock(Map<String, String> configMap, ResourceBundle rb,
      String localDir, int serverCount) throws ConfigParseException {
     if (rb.getString(
       CommonConstants.BEGIN_TAG_PREF + serverCount + CommonConstants.BEGIN_TAG_SUFF)
       .equals("")) {
      // check if the <##>.server-ip is not empty
      parseServerIP(configMap, rb, serverCount);

      // check if the <##>.username is not empty
      parseUsername(configMap, rb, serverCount);

      // check for prompt-password field is "N"
      parsePassword(configMap, rb, serverCount);

      // check if the <##>.remote-dir-list is not empty
      parseRemDirList(configMap, rb, serverCount);

      // check if the <##>.file-types is not empty
      parseFileTypeList(configMap, rb, serverCount);

      // check if the <##>.local-dir-path is not empty
      parseLocalDirPath(configMap, rb, localDir, serverCount);
     } else {
      throw new ConfigParseException("Invalid server block entry for <"
        + CommonConstants.BEGIN_TAG_PREF + serverCount + CommonConstants.BEGIN_TAG_SUFF
        + ">");
     }
    }

    /**
     * Parse the server IP.
     * 
     * @param configMap Map of properties.
     * @param rb Reference to ResourceBundle.
     * @param serverCount Index of server.
     * @throws ConfigParseException Exception thrown if something goes wrong during parsing.
     */
    private static void parseServerIP(Map<String, String> configMap, ResourceBundle rb,
      int serverCount) throws ConfigParseException {
     String serverIp = rb.getString(serverCount + "." + CommonConstants.SERVER_URL);
     if (serverIp.equals("")) {
      throw new ConfigParseException("Server IP missing for : <" + serverCount + "."
        + CommonConstants.SERVER_URL + ">");
     } else {
      configMap.put(serverCount + "." + CommonConstants.SERVER_URL, serverIp);
     }
    }

    /**
     * Parse the local directory path where the files would be stored.
     * 
     * @param configMap Map of properties.
     * @param rb Reference to ResourceBundle.
     * @param localDir Local directory path.
     * @param serverCount Index of server.
     * @throws ConfigParseException Exception thrown if something goes wrong during parsing.
     */
    private static void parseLocalDirPath(Map<String, String> configMap, ResourceBundle rb,
      String localDir, int serverCount) throws ConfigParseException {
     if (localDir.equals("")) {
      String localDirPath = rb.getString(serverCount + "." + CommonConstants.LOCAL_DIR_PATH);
      if (localDirPath.equals("")) {
       throw new ConfigParseException("Local directory path missing for : <" + serverCount
         + "." + CommonConstants.LOCAL_DIR_PATH + ">");
      } else {
       configMap.put(serverCount + "." + CommonConstants.LOCAL_DIR_PATH, localDirPath);
      }
     }
    }

    /**
     * Parse the file list to be downloaded.
     * 
     * @param configMap Map of properties.
     * @param rb Reference to ResourceBundle.
     * @param serverCount Index of server.
     * @throws ConfigParseException Exception thrown if something goes wrong during parsing.
     */
    private static void parseFileTypeList(Map<String, String> configMap, ResourceBundle rb,
      int serverCount) throws ConfigParseException {
     String strFileTypeList = rb.getString(serverCount + "." + CommonConstants.FILE_TYPE_LIST);
     if (strFileTypeList.equals("")) {
      throw new ConfigParseException("File type list missing for : <" + serverCount + "."
        + CommonConstants.FILE_TYPE_LIST + ">");
     } else {
      configMap.put(serverCount + "." + CommonConstants.FILE_TYPE_LIST, strFileTypeList);
     }
    }

    /**
     * Parse the remote directory list.
     * 
     * @param configMap Map of properties.
     * @param rb Reference to ResourceBundle.
     * @param serverCount Index of server.
     * @throws ConfigParseException Exception thrown if something goes wrong during parsing.
     */
    private static void parseRemDirList(Map<String, String> configMap, ResourceBundle rb,
      int serverCount) throws ConfigParseException {
     String strDirList = rb.getString(serverCount + "." + CommonConstants.REMOTE_DIR_LIST);
     if (strDirList.equals("")) {
      throw new ConfigParseException("Directory list missing for : <" + serverCount + "."
        + CommonConstants.REMOTE_DIR_LIST + ">");
     } else {
      configMap.put(serverCount + "." + CommonConstants.REMOTE_DIR_LIST, strDirList);
     }
    }

    /**
     * Parse the password.
     * 
     * @param configMap Map of properties.
     * @param rb Reference to ResourceBundle.
     * @param serverCount Index of server.
     * @throws ConfigParseException Exception thrown if something goes wrong during parsing.
     */
    private static void parsePassword(Map<String, String> configMap, ResourceBundle rb,
      int serverCount) throws ConfigParseException {
     if (rb.getString(CommonConstants.IS_PROMPT_PASSWORD).equalsIgnoreCase("N")) {
      // check if the <##>.password is empty when
      // flag = N
      String password = rb.getString(serverCount + "." + CommonConstants.PASSWORD);
      if (password.equals("")) {
       throw new ConfigParseException("Password missing for : <" + serverCount + "."
         + CommonConstants.PASSWORD + ">");
      } else {
       configMap.put(+serverCount + "." + CommonConstants.PASSWORD, password);
      }
     }
    }

    /**
     * Parse the username.
     * 
     * @param configMap Map of properties.
     * @param rb Reference to ResourceBundle.
     * @param serverCount Index of server.
     * @throws ConfigParseException Exception thrown if something goes wrong during parsing.
     */
    private static void parseUsername(Map<String, String> configMap, ResourceBundle rb,
      int serverCount) throws ConfigParseException {
     String username = rb.getString(serverCount + "." + CommonConstants.USERNAME);
     if (username.equals("")) {
      throw new ConfigParseException("Username missing for : <" + serverCount + "."
        + CommonConstants.USERNAME + ">");
     } else {
      configMap.put(serverCount + "." + CommonConstants.USERNAME, username);
     }
    }

    /**
     * Reads the value for the input type at runtime.
     * 
     * @param input Stream to be used (e.g. System.in).
     * @param prompt The prompt to display to the user.
     * @return The value entered by the user.
     * @throws IOException Exception is thrown if something goes wrong while accepting input from
     *             user.
     */
    public static char[] getInput(InputStream in, String prompt) throws IOException {
     char[] lineBuffer;
     char[] buf;

     System.out.print(prompt);

     buf = lineBuffer = new char[128];

     int room = buf.length;
     int offset = 0;
     int c;
     boolean defaultFlag = true;
     while (true) {
      switch (c = in.read()) {
      case -1:
      case '\n':
       defaultFlag = false;
       break;

      case '\r':
       int c2 = in.read();
       if ((c2 != '\n') && (c2 != -1)) {
        if (!(in instanceof PushbackInputStream)) {
         in = new PushbackInputStream(in);
        }
        ((PushbackInputStream) in).unread(c2);
       } else {
        defaultFlag = false;
        break;
       }
      default:
       if (--room < 0) {
        buf = new char[offset + 128];
        room = buf.length - offset - 1;
        System.arraycopy(lineBuffer, 0, buf, 0, offset);
        Arrays.fill(lineBuffer, ' ');
        lineBuffer = buf;
       }
       buf[offset++] = (char) c;
       defaultFlag = true;
       break;
      }
      if (!defaultFlag) {
       break;
      }
     }
     if (offset == 0) {
      return null;
     }

     char[] ret = new char[offset];
     System.arraycopy(buf, 0, ret, 0, offset);
     Arrays.fill(buf, ' ');

     return ret;
    }
}

*ftp_config.properties*

[GLOBAL_CONFIG]
# Ask user for password for every server entry
prompt-password=Y
# Global directory path for all server blocks. If present, then all local DIR paths are ignored.
local-dir=
# Configuration for servers from where files are downloaded 
[SERVERS]
# Define server attributes
[server-1]
# Server URL or IP
1.server-ip=10.200.12.13
# FTP user name
1.username=xcvxv
# FTP password (This is optional. Required only if prompt-password=N; Ignored when prompt-password=Y)
1.password=asdfsd
# Semicolon (';') delimited list of directories to be looked-up for files. Sub directories to be listed specifically.
1.remote-dir-list=abc1;abc2
# Semicolon (';') delimited list of file extensions to be looked-up.
1.file-types=test.txt
# Specify the local dir path where the files would be stored. (This is optional. Required only if local-dir=<EMPTY>)
1.local-dir-path=c:

###########################

# Define server attributes
[server-2]
# Server URL or IP
2.server-ip=10.250.15.16
# FTP user name
2.username=sdfsd
# FTP password (This is optional. Required only if prompt-password=N; Ignored when prompt-password=Y)
2.password=sdfsd
# Semicolon (';') delimited list of directories to be looked-up for files. Sub directories to be listed specifically.
2.remote-dir-list=abc3;abc4
# Semicolon (';') delimited list of file extensions to be looked-up.
2.file-types=test.txt
# Specify the local dir path where the files would be stored. (This is optional. Required only if local-dir=<EMPTY>)
2.local-dir-path=d:

FTPClient.java

package com.cts.automation.ftp.client;

import com.cts.automation.base.exception.FTPException;
import com.cts.automation.base.service.FTPBaseService;
import com.cts.automation.base.service.FTPServiceFactory;
import com.cts.automation.ftp.util.CommonConstants;

/**
 * Main class for executing the application. This class cannot be extended.
 * 
 * @author Gaurav Saini
 */
public final class FTPClient {

    /**
     * Starting point of the application
     * 
     * @param args Command line arguments if any. Not required.
     * @throws FTPException Throws exception if something goes wrong during execution of
     *             application.
     */
    public static void main(String[] args) throws FTPException {
     String serviceClass = System.getProperty(CommonConstants.SERVICE_CLASS);
     if (serviceClass == null || serviceClass.equals("")) {
      throw new FTPException("System property '" + CommonConstants.SERVICE_CLASS
        + "' not found.");
     }
     FTPBaseService service = FTPServiceFactory.createInstance(serviceClass);
     service.startService();
    }
}

The code above demonstrates how the client would be invoked after the user configures the FTP service to be used. Also I have written a Handler to use the Apache's FTP API. Know the user would have to pass the fully qualified class name of the class as VM argument to execute the client.