Testando um FTP Wrapper com JavaServer Faces

Veja neste artigo como criar um Wrapper para o cliente FTP e acessá-lo via JSF.

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.

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:

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 # 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"); }

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 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> # </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:

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.

Artigos relacionados