Local FTP server

This page was created because of  OLMIS-1381 - Getting issue details... STATUS  and provide basic information about how to add a simple FTP server as a docker container to the reference distribution (openlmis-ref-distro).

Docker container

I found three FTP servers that could be used as a docker container:

I chose ProFTP due to the ease in use. We do not have to do anything after we add the following lines into docker-compose.yml file.

docker-compose.yml
ftp:
  image: hauptmedia/proftpd
  ports:
    - "21:21"
    - "20:20"
  environment:
    - FTP_USERNAME=username
    - FTP_PASSWORD=password

In the real scenario the environment variables should be moved into a configuration file. Then instead of environment property should be env_file: <<path_to_config_file>>.

Requirements

Before we start creating a java code needed to establish a connection to the FTP server we need to add a java library to the project. I decided to use Apache Commons NET library because it provides support for FTP protocol and it is easy to use. We add library by adding a single line into our build.gradle file:

build.gradle
dependencies {
    // other dependencies
    compile 'commons-net:commons-net:3.5'
    // other dependencies
}

Example code

The following code firstly create a new instance of FTPClient. We add a protocol command listener to this instance which just prints out to standard output stream all command/reply traffic. Then we try to connect to the FTP server. We check reply code from the FTP server if it is not positive it means that we could not connected to the server. Next we try to log into the server with the given username and password. The method returns true if we logon to the system and false in opposite situation. Depends on result of login method we print success or failure message. After all we close the connection to the FTP server. The is also one additional method that helps to print server replies to the standard output.

FTP Hello World
public static void main(String[] args) {
  String server = "ftp";
  int port = 21;
  String user = "username";
  String pass = "password";

  FTPClient ftp = new FTPClient();
  ftp.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true));

  try {
    ftp.connect(server, port);

    showServerReply(ftp);

    int replyCode = ftp.getReplyCode();
    if (!FTPReply.isPositiveCompletion(replyCode)) {
      System.out.println("Operation failed. Server reply code: " + replyCode);
      return;
    }

    boolean success = ftp.login(user, pass);
    showServerReply(ftp);

    if (!success) {
      System.out.println("Could not login to the server");
    } else {
      System.out.println("LOGGED IN SERVER");
    }

    ftp.disconnect();
    System.out.println("DISCONNECTED");
  } catch (IOException ex) {
    System.err.println("Oops! Something wrong happened");
    ex.printStackTrace();
  }
}

private static void showServerReply(FTPClient ftpClient) {
  String[] replies = ftpClient.getReplyStrings();
  if (replies != null && replies.length > 0) {
    for (String aReply : replies) {
      System.out.println("SERVER: " + aReply);
    }
  }
}

Executing the above code we should get the following output:

Output
220 ProFTPD 1.3.5rc3 Server (proftpd) [::ffff:172.24.0.3]
SERVER: 220 ProFTPD 1.3.5rc3 Server (proftpd) [::ffff:172.24.0.3]
USER *******
331 Password required for username
PASS *******
230 User username logged in
SERVER: 230 User username logged in
LOGGED IN SERVER
DISCONNECTED

We can see that we successfully connected with the local FTP server (on 21 port) and we could logon as a username: username with password: password.

Spring Integration FTP

There is other way to establish a connection with the FTP server. Instead of using Apache Commons and write a code we could use the Spring Integration which has a support for the FTP protocol. The first thing is to change dependencies in the build.gradle file:

build.gradle
dependencies {
    // other dependencies
    compile "org.springframework.boot:spring-boot-starter-integration:1.4.1.RELEASE"
    compile "org.springframework.integration:spring-integration-ftp:4.3.5.RELEASE"
    // other dependencies
    // we don't need the commons-net:commons-net:3.5 dependency
}

Also, we have to add configuration for the FTP protocol. The following configuration create a standard channel with name toFtpChannel, standard poller with fixed rate equal to 500, session factory which will create a connection with the FTP server and integration flow for outbound adapter:


FtpConfiguration
import org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration;
import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.integration.annotation.IntegrationComponentScan;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.core.Pollers;
import org.springframework.integration.dsl.ftp.Ftp;
import org.springframework.integration.file.FileHeaders;
import org.springframework.integration.file.support.FileExistsMode;
import org.springframework.integration.ftp.session.DefaultFtpSessionFactory;
import org.springframework.integration.scheduling.PollerMetadata;
import org.springframework.messaging.PollableChannel;

@Configuration
@Import({JmxAutoConfiguration.class, IntegrationAutoConfiguration.class})
@IntegrationComponentScan
public class FtpConfiguration {
  private static final String FTP_HOST_NAME = "ftp";
  private static final int FTP_PORT = 21;
  private static final String FTP_USER = "admin";
  private static final String FTP_PASS = "p@ssw0rd";
  private static final String FTP_PATH = "/orders/files/csv";

  /**
   * Creates new DefaultFtpSessionFactory.
   *
   * @return Created DefaultFtpSessionFactory.
   */
  @Bean
  public DefaultFtpSessionFactory ftpSessionFactory() {
    DefaultFtpSessionFactory factory = new DefaultFtpSessionFactory();
    factory.setHost(FTP_HOST_NAME);
    factory.setPort(FTP_PORT);
    factory.setUsername(FTP_USER);
    factory.setPassword(FTP_PASS);

    return factory;
  }

  @Bean
  public PollableChannel toFtpChannel() {
    return new QueueChannel();
  }

  @Bean(name = PollerMetadata.DEFAULT_POLLER)
  public PollerMetadata poller() {
    return Pollers.fixedRate(500).get();
  }

  /**
   * Creates new IntegrationFlow.
   *
   * @return Created IntegrationFlow.
   */
  @Bean
  public IntegrationFlow ftpOutboundFlow() {
    return IntegrationFlows.from("toFtpChannel")
        .handle(Ftp.outboundAdapter(ftpSessionFactory(), FileExistsMode.FAIL)
            .useTemporaryFileName(false)
            .fileNameExpression("headers['" + FileHeaders.FILENAME + "']")
            .remoteDirectory(FTP_PATH)
            .autoCreateDirectory(true)
        ).get();
  }

}


Now to transfer a file we had to inject our channel. In the following example we inject the channel in the OrderService class:

OrderService
@Autowired
@Qualifier("toFtpChannel")
private MessageChannel toFtpChannel;

In the end to send a single file to the FTP server we have to create a new message with payload (in this case it will be path to file located in the local directory) and send it. In the following example in the OrderService class we create a CSV file in the local directory and then we send it into the FTP server:

OrderService
order = // retrieve an order
OrderFileTemplate template = // retrieve an order file template
String fileName = template.getFilePrefix() + order.getOrderCode() + ".csv";

Path path = Paths.get("/var/files/fulfillment", fileName);

try (Writer writer = Files.newBufferedWriter(path)) {
  // we write data into the local file (which will be send to the FTP server in the next step)
  csvHelper.writeCsvFile(order, template, writer);
  // when the file is ready we create a message and send it into the FTP server
  toFtpChannel.send(MessageBuilder.withPayload(path.toFile()).build());
}

OpenLMIS: the global initiative for powerful LMIS software