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.