Neste artigo construiremos um wrapper para um cliente FTP básico, com funções apenas de conectar, mudar o diretório e listar os arquivos presentes. Nosso wrapper poderá ser utilizado através de interface JSF que construiremos com ajuda do Primefaces, para que não tenhamos muito trabalho com layout e componentes customizados.

Um Wrapper pode servir para adicionar mais funcionalidades a determinado objeto ou mesmo encapsular as funcionalidades já existentes fornecendo um menor nível de acoplamento, o que é muito bom para sistemas extensíveis.

Temos no Java diversos exemplos de Wrappers nativos, por exemplo, a classe Double é um Wrapper para o tipo primitivo double, pois ela adiciona diversas funcionalidades a este tipo primitivo. Projeto

Em nosso projeto usaremos JSF para que o usuário posso executar os comandos básicos no cliente FTP, para isso precisamos primeiro configurar nosso projeto, e na Figura 1 temos a estrutura de diretório usado.

Estrutura do Projeto

Figura 1. Estrutura do Projeto

Perceba que estamos usando Maven como gerenciador de dependências e nosso pom.xml contém as bibliotecas necessárias para funcionamento do nosso projeto, tais como JSF e Primefaces, como mostra a Listagem 1. Usamos o Primefaces apenas para dar um visual melhor a nossa interface, mas você poderá usar o JSF puro apenas para efeito didático.

Listagem 1. pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
       <modelVersion>4.0.0</modelVersion>
       <groupId>br.com</groupId>
       <artifactId>lab</artifactId>
       <packaging>war</packaging>
       <version>0.0.1-SNAPSHOT</version>
       <name>lab Maven Webapp</name>
       <url>http://maven.apache.org</url>
       <dependencies>

       <dependency>
       <groupId>commons-net</groupId>
       <artifactId>commons-net</artifactId>
       <version>2.0</version>
</dependency>


             <dependency>
                    <groupId>org.omnifaces</groupId>
                    <artifactId>omnifaces</artifactId>
                    <version>1.11-M1</version>
             </dependency>

             <dependency>
                    <groupId>com.sun.faces</groupId>
                    <artifactId>jsf-api</artifactId>
                    <version>2.1.6</version>
             </dependency>

             <dependency>
                    <groupId>com.sun.faces</groupId>
                    <artifactId>jsf-impl</artifactId>
                    <version>2.1.6</version>
             </dependency>

             <dependency>
                    <groupId>org.primefaces.extensions</groupId>
                    <artifactId>primefaces-extensions</artifactId>
                    <version>0.7.1</version>
             </dependency>

             <dependency>
                    <groupId>org.primefaces</groupId>
                    <artifactId>primefaces</artifactId>
                    <version>3.5</version>
             </dependency>

             <dependency>
                    <groupId>org.primefaces.themes</groupId>
                    <artifactId>bootstrap</artifactId>
                    <version>1.0.9</version>
             </dependency>
       </dependencies>
       <build>
             <finalName>lab</finalName>
       </build>
</project>

Vamos explicar o uso das bibliotecas importadas:

  • omnifaces: A importação do omnifaces, em nosso caso específico, serve apenas para uso da classe UnmappedResourceHandler.

Quando vamos mapear um recurso em nossa aplicação (por recurso entenda imagens, CSS, JavaScript e quaisquer outros arquivos externos), nós precisamos definir o caminho onde o mesmo se encontra, mas em se tratando de um projeto web, os recursos geralmente ficam alocados na pasta “WeContent/resources”. Por isso, vamos apontar este caminho em um arquivo CSS por exemplo, usando a seguinte estrutura:

body {
     background: url("#{resource['css/images/background.png']}");
}

Agora imagine se tivéssemos um layout Bootstrap, com mais de 500 imagens dispersas por vários arquivos CSS: pense no trabalho que você teria em mudar caminho por caminho. Pensando nisso, a classe UnmappedResourceHandler faz automaticamente o mapeamento de todos esses arquivos, adicionando o caminho “javax.faces.resource” ao seu caminho, que é exatamente o que o EL Expression #{resource} produz. Isso só é possível porque no arquivo web.xml adicionamos o url-pattern “/javax.faces.resource/*” e no faces-config.xml temos o resource-handler mapeado para a classe UnmappedResourceHandler.

Então, ao invés de usarmos o EL Expression no background:url, podemos deixar no padrão comumente utilizado:

body {
 background: url("images/background.png");
}

  • Com.sun.faces: Para uso do JSF;
  • Primefaces: Para usar componentes customizados e ter um melhor design;
  • Commons-net: O principal import para nosso artigo é este, pois é através deste jar que faremos uso da classe FTPClient fornecida pelo Apache.

Já que citamos o faces-config.xml, segue na Listagem 2 como realizamos sua declaração.

Listagem 2. faces-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<faces-config version="2.1" xmlns="http://java.sun.com/xml/ns/javaee"
       xmlns:xi="http://www.w3.org/2001/XInclude" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_1.xsd">
       <application>
             <resource-handler>org.omnifaces.resourcehandler.UnmappedResourceHandler</resource-handler>
       </application>
</faces-config>

Para configuração do projeto falta apenas o web.xml então podemos começar a desenvolver nosso wrapper, como mostra a Listagem 3.

Listagem 3. web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
 http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
       id="WebApp_ID">
       <display-name>LAB</display-name>

       <welcome-file-list>
             <welcome-file>index.xhtml</welcome-file>
       </welcome-file-list>

       <context-param>
    <param-name>primefaces.THEME</param-name>
    <param-value>bootstrap</param-value>
       </context-param>

       <!-- Configuracao DO JSF -->
       <servlet>
             <servlet-name>Faces Servlet</servlet-name>
             <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
             <load-on-startup>1</load-on-startup>
       </servlet>
       <servlet-mapping>
             <servlet-name>Faces Servlet</servlet-name>
             <url-pattern>*.xhtml</url-pattern>
             <url-pattern>/javax.faces.resource/*</url-pattern>
       </servlet-mapping>
</web-app>

São três pontos específicos configurados em nosso web.xml, primeiramente o arquivo inicial que será carregado ao acessar o link do projeto, em nosso caso o index.xhtml, em segundo lugar o tema usado pelo Primefaces, que é o BOOTSTRAP, e por último e mais importante são os arquivos que o JSF irá renderizar, neste caso, todos os com final *.xhml ou que tenham javax.faces.resource em seu path.

Vamos começar a construir a classe FtpWrapper, presente na Listagem 4.

Listagem 4. FtpWrapper.java

package br.com.lab;

import java.io.IOException;
import java.net.SocketException;

import org.apache.commons.net.ftp.FTPClient;

public class FtpWrapper {

         private static FtpWrapper instance;
         private FTPClient ftp;

         private FtpWrapper(){
                   this.ftp = new FTPClient();
         }

         public static FtpWrapper getInstance(){
                   if (instance == null){
                            instance = new FtpWrapper();
                   }
                   return instance;
         }

         public String currentDirectory(){
                   try {
                            checkIfConected();
                            return ftp.printWorkingDirectory();
                   } catch (IOException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                            return null;
                   }
         }

         private void checkIfConected(){
                   if (!ftp.isConnected()){
                            throw new RuntimeException("Não está conectado ao FTP");
                   }
         }

         public void connect(String hostname){
                   try {
                            if (ftp.isConnected()){
                                      throw new RuntimeException("Já conectado");
                            }
                            ftp.connect(hostname);
                   } catch (SocketException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                   } catch (IOException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                   }
         }

         public void login(String username, String password){
                   try {
                            ftp.login(username, password);
                   } catch (IOException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                   }
         }

         public void changeWorkingDirectory(String targetDir){
                   try {
                            checkIfConected();
                            ftp.changeWorkingDirectory(targetDir);
                   } catch (IOException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                   }
         }

         public String[] listNames(){
                   try {
                            checkIfConected();
                            return ftp.listNames();
                   } catch (IOException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                            return null;
                   }
         }
}

Nessa classe a ideia é que todos os métodos de um FTP sejam encapsulados pelo nosso wrapper, deixando transparente para quem o usa qual provider FTP está sendo usado, seja do pacote Apache ou do pacote “sun”. Assim, se decidirmos mais à frente mudar por algum motivo o provider do FTP então o único trabalho será refazer os pontos da classe FtpWrapper, mas para todo o resto da aplicação isso será totalmente transparente.

Para aqueles que usavam a classe FtpClient do pacote sun.net.ftp desde o Java 6, devem ter percebido que ouve uma mudança drástica desta classe a partir do Java 7 em diante, tornando-se quase impossível reaproveitar desenvolvimentos já realizados. Para quem teve a genialidade de usar um wrapper desde o começo então o impacto será menor.

Perceba que estamos usando um padrão de projeto Singleton para garantir apenas uma instância desta classe durante toda a aplicação. O atributo “ftp” é quem de fato faz a conexão com o provider que escolhemos, em nosso caso, o FTPClient do pacote “org.apache.commons.net.ftp”.

Todas as chamadas dos nossos métodos farão referência para o atributo “ftp”. Por exemplo, o método checkIfConected método checa se estamos conectados ao FTP, caso contrário, é lançada uma exceção para evitar qualquer tarefa com o FTP desconectado.

O método String currentDirectory primeiro faz chamada ao checkIfConected, que irá interromper a execução imediatamente se o FTP não estiver conectado, depois chamamos o printWorkingDirectory(), que irá retornar o diretório atual que estamos conectados no FTP, lembrando que esse comando pode alterar de acordo com o provider (classe FTP) utilizado.

De acordo com o “hostname” passado (por exemplo: ftp.seudominio.com.br) realizamos a conexão a este host, mas antes checamos se já não temos uma conexão estabelecida.

Depois de conectado realizamos o login através do usuário e senha passados.

Podemos também mudar o diretório que estamos visualizando, passando como parâmetro o “targetDir” que corresponde ao diretório destino.

Por último retornamos uma lista com o nome dos arquivos do diretório atual.

A alteração do provider implicará na mudança dos métodos obviamente, mas será muito menos impactante do que mudar toda a aplicação.

Vamos agora iniciar a criação do ManagedBean que irá fazer a interface entre a página HTML (em JSF) e o FtpWrapper, como mostra a Listagem 5.

Listagem 5. FtpMB.java

package br.com.lab;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.faces.application.FacesMessage;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;
import javax.faces.context.FacesContext;

@ManagedBean(name = "ftpMB")
@ViewScoped
public class FtpMB {

         private String hostName, username, password, newDir;
         private List<String> arquivos;

         private void showMessage(String message) {
                   FacesContext fc = FacesContext.getCurrentInstance();
                   fc.addMessage(null, new FacesMessage(message));
         }

         public void conectar() {
                   try {
                            FtpWrapper.getInstance().connect(hostName);
                            FtpWrapper.getInstance().login(username, password);
                            newDir = FtpWrapper.getInstance().currentDirectory();
                            showMessage("Conectado com sucesso em " + hostName);
                   } catch (Exception e) {
                            showMessage(e.getMessage());
                  }
         }

         public void mudarDiretorio() {
                   try {
                            FtpWrapper.getInstance().changeWorkingDirectory(newDir);
                            showMessage("Diretório alterado para "+newDir);
                   } catch (Exception e) {
                            showMessage(e.getMessage());
                   }
         }

         public void listarArquivos() {
                   try {
                            arquivos = new ArrayList<String>(Arrays.asList(FtpWrapper
                                               .getInstance().listNames()));
                            showMessage("Arquivos carregados com sucesso");
                   } catch (Exception e) {
                            showMessage(e.getMessage());
                   }
         }

         public String getHostName() {
                   return hostName;
         }

         public void setHostName(String hostName) {
                   this.hostName = hostName;
         }

         public String getUsername() {
                   return username;
         }

         public void setUsername(String username) {
                   this.username = username;
         }

         public String getPassword() {
                   return password;
         }

         public void setPassword(String password) {
                   this.password = password;
         }

         public String getNewDir() {
                   return newDir;
         }

         public void setNewDir(String newDir) {
                   this.newDir = newDir;
         }

         public List<String> getArquivos() {
                   return arquivos;
         }

         public void setArquivos(List<String> arquivos) {
                   this.arquivos = arquivos;
         }

}

O nosso ManagedBean usa o nome “ftpMB” para ser referenciado na página JSF, além disso, usamos o @ViewScoped para garantir que o estado do objeto seja mantido enquanto o usuário não mudar de página.

São cinco os atributos que usaremos durante toda a aplicação:

  • o hostName, username e password servem para conexão ao FTP;
  • o atributo newDir irá possuir o diretório atual que estamos trabalhando e também possibilitará a mudança para um novo diretório caso o usuário deseje;
  • o atributo “arquivos” do tipo “List” possuirá a lista com o nome de todos os arquivos do diretório atual, quando requisitado pelo usuário.

O método showMEssage() irá mostrar determinada mensagem na localização da tag <h:messages> que veremos na página XHTML.

O método conectar() faz a conexão e login no FTP usando o FtpWrapper sempre com o método getInstance(), que retorna à instância atual do nosso wrapper, ou cria uma nova caso ainda não exista. Perceba que além da conexão ao FTP ainda retornamos o diretório atual através do método currentdirectory(), finalizando mostramos a mensagem através do showMessage().

O método mudarDiretorio permite a mudança do diretório atual para um novo diretório escolhido pelo usuário.

No método listarArquivos chamamos o “listNames()” do nosso Wrapper para capturar a lista com o nome dos arquivos no diretório atual, como precisamos de um List, então fazemos a conversão para tal usando o Arrays.asList(), pois o listNames() retorna um vetor do tipo String[]. Os últimos métodos são os getters e setters necessários para que o JSF consiga fazer o binding entre o valor do XHTML e o ManagedBean.

Enfim podemos criar a página XHTML que irá realizar as operações de conexão ao FTP, mudança de diretório e listagem de arquivos, como mostra a Listagem 6. Lembrando que nosso objetivo não é criar um cliente FTP completo, mas mostrar como o encapsulamento pode ser realizado e acessado através de uma interface JSF.

Listagem 6. index.xhtml

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
       xmlns:h="http://java.sun.com/jsf/html"
       xmlns:f="http://java.sun.com/jsf/core"
       xmlns:ui="http://java.sun.com/jsf/facelets"
       xmlns:p="http://primefaces.org/ui">

<h:head>
       <title>LAB</title>
</h:head>
<h:body>
       <h:form prependId="false">


             <h:messages />

             <p:panelGrid columns="2">
                    <label>Host</label>
                    <p:inputText value="#{ftpMB.hostName}" />

                    <label>Usuário</label>
                    <p:inputText value="#{ftpMB.username}" />

                    <label>Senha</label>
                    <p:password value="#{ftpMB.password}" />

                    <label>Diretório</label>
                    <p:inputText value="#{ftpMB.newDir}" />

             </p:panelGrid>

             <p:commandButton value="Conectar" actionListener="#{ftpMB.conectar}"
                    update="@form" />
             <p:commandButton value="Mudar diretório"
                    actionListener="#{ftpMB.mudarDiretorio}" update="@form" />
             <p:commandButton value="Listar Arquivos"
                    actionListener="#{ftpMB.listarArquivos}" update="@form" />

             <h:dataTable value="#{ftpMB.arquivos}" var="arq" id="dataTable">
                    <h:column>
                           #{arq}
                </h:column>
             </h:dataTable>

       </h:form>
</h:body>
</html>

Dentro da tag <h:form> temos a tag <h:messages> que será responsável por mostrar as mensagens que estão vindo no ManagedBean, através do método showMessage() que mostramos anteriormente. O panelGrid contém quatro campos (host, usuário, senha e diretório) onde todos os valores digitados só são submetidos ao managedbean quando um dos botões a seguir são acionados:

  • Botão Conectar: Usa um componente commandButton do Primefaces que chama um actionListener do ManagedBean ftpMB, que é o método conectar(). Após a requisição é realizado um update em todo formulário, pois trata-se de uma requisição ajax. Optamos por usar o @form no update para que todo conteúdo dentro da tag “<form>” seja atualizado, mas você poderia especificar campo a campo que deseja atualizar.
  • Botão Mudar Diretório: Usa a mesma estrutura do botão Conectar porém este captura o valor digitado no campo “Diretório” e realiza a mudança do diretório atual para este novo diretório.
  • Botão Listar Arquivos: Quando acionado o atributo “arquivos” é populado e o dataTable do JSF mostra os valores contidos nesta List. Assim que a propriedade update=@form é chamado o dataTable é imediatamente atualizado com os valores que foram populados no ManagedBean.

O objetivo principal deste artigo foi demonstrar como é útil utilizar um wrapper para manter um menor nível de acoplamento no seu projeto. Em nosso caso usamos o JSF para fazer uso desse Wrapper, mas você poderia usar uma interface Android, Swing ou qualquer outro que sua regra de negócio exigir.