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:
- 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.