[linu.com.br] - [parte 1] - [parte 2] - [parte 3] - [parte 4] - lgapontes@gmail.com |
Continuaremos aqui, a saga do SuperFórum.
Boa leitura.
Desenvolvendo as classes da camada de business
De forma semelhante à camada DAO, é interessante separar regras de
negócio na Business Core. Aqui vamos implementar as classes de negócio
da aplicação.
Para criar a Business Core, faremos uso de um novo recurso do Java EE
6: a CDI (injeção de dependência). Essa nova versão do Java EE trouxe
muitas inovações em relação à injeção de dependência, dentre elas, a
anotação @Inject. Agora, (1) qualquer classe concreta (ou anotada com @Decorator - que veremos em breve); (2) com um constructor sem parâmetros (ou anotado com @Inject); (3) que não implemente a interface javax.enterprise.inject.spi.Extension e (4) não seja uma Entity, EJB ou classe interna do Java, será considerada um Bean administrado pelo servidor de aplicação (esses Beans são conhecidos como Managed Beans -
que, apesar do nome, não estão relacionados unicamente com o JSF). Para
habilitar isso nas aplicações EE, basta acrescentar o arquivo beans.xml (mesmo o arquivo sendo vazio) em META-INF. Dessa forma, poderemos fazer referência a esses Managed Beans através da anotação @Inject. Observe que nenhuma anotação é necessária para que uma classe se torne um Managed Bean. Precisamos apenas atender às quatro regras destacadas acima.
Ou seja, quase todo mundo agora virou Bean!
Não vou mostrar a implementação simples desse recurso. Vou aproveitar e
já mostrá-la juntamente com outra novidade: a anotação @Qualifier. Bom, como quase todas as classes de um projeto EE serão consideradas Managed Beans, no momento da @Inject
atuar e descobrir qual classe está sendo injetada, podem ocorrer erros
de ambiguidade. Para resolver esses problemas, o Java EE 6 também
oferece recursos para criarmos qualificadores que serão anotados nos Managed Beans.
Portanto, o que faremos? Primeiro nós vamos definir uma interface com comportamentos padrões para a camada de negócios. Em seguida, vamos definir nossos @Qualifiers para cada tipo de objeto de negócios. Por fim, vamos criar os próprios objetos de negócio (que chamaremos de BO - Business Object), utilizando um pouco de herança para reaproveitamento de código. Vamos à luta!
Como de costume, criaremos um novo pacote para guardar as classes
associadas a essa camada. Clique com o botão direito sobre o projeto SuperForumEJB, opção new, Package. Defina o nome do novo pacote como br.com.linu.forum.business. Antes de criarmos qualquer coisa, precisamos definir uma Exception para disparar erros de negócio. Crie uma nova classe chamada BusinessException dentro do pacote criado.
package br.com.linu.forum.business;
public class BusinessException extends Exception {
private static final long serialVersionUID = 1L;
public BusinessException(String message) {
super( message );
}
}
Nada de novo foi criado nesse código - ele é praticamente igual ao DaoException. Vamos agora criar a interface Business. Vamos criá-la dentro do pacote br.com.linu.forum.business. Crie uma nova interface chamada Business dentro desse pacote e digite (leia-se copy/paste) o código abaixo.
package br.com.linu.forum.business;
import java.io.Serializable;
import java.util.List;
import javax.ejb.Local;
@Local
public interface Business extends Serializable {
public void validate(Object entity) throws BusinessException;
public void create(Object entity) throws BusinessException;
public void update(Object entity) throws BusinessException;
public Object read(Class<?> entityClass, long id) throws BusinessException;
public List<?> read(String namedQuery) throws BusinessException;
public List<?> read(String namedQuery, Object parameter) throws BusinessException;
}
Conforme você pode notar, essa também é uma interface @Local que implementa os mesmos métodos que a interface Dao, com acréscimo apenas do método validate()
- acho que não é necessário entrarmos em detalhes sobre sua utilidade.
Bom, já temos nossa interface. Porém a grande parte de seus
comportamentos pode ser facilmente unificada em uma classe-mãe - pois
são repetidos, independentemente da entidade tratada. Será essa nossa
próxima classe. Crie uma classe abstrata BasicBo no pacote de negócios.
package br.com.linu.forum.business;
import java.io.Serializable;
import java.util.List;
import javax.ejb.EJB;
import br.com.linu.forum.dao.Dao;
import br.com.linu.forum.dao.DaoException;
public abstract class BasicBo implements Business, Serializable {
private static final long serialVersionUID = 1L;
@EJB(name="ejb/dao")
private Dao basicDao;
@Override
public void create(Object entity) throws BusinessException {
try {
validate(entity);
basicDao.create(entity);
} catch (DaoException e) {
throw new BusinessException( e.getMessage() );
}
}
@Override
public void update(Object entity) throws BusinessException {
try {
validate(entity);
basicDao.update(entity);
} catch (DaoException e) {
throw new BusinessException( e.getMessage() );
}
}
@Override
public Object read(Class<?> entityClass, long id) throws BusinessException {
try {
return basicDao.read(entityClass, id);
} catch (DaoException e) {
throw new BusinessException( e.getMessage() );
}
}
@Override
public List<?> read(String namedQuery) throws BusinessException {
try {
return basicDao.read(namedQuery);
} catch (DaoException e) {
throw new BusinessException( e.getMessage() );
}
}
@Override
public List<?> read(String namedQuery, Object parameter)
throws BusinessException {
try {
return basicDao.read(namedQuery, parameter);
} catch (DaoException e) {
throw new BusinessException( e.getMessage() );
}
}
}
Nada de novo foi criado nessa classe. Devemos apenas nos atentar na utilização do @EJB(name="ejb/dao"), responsável por trazer a única instância do @Singleton BasicDao.
O restante dos métodos apenas transforma erros de acesso aos dados, em
erros de negócio. Veja que, para se criar ou atualizar um objeto, a
passagem pelo método validate() é necessária - que inclusive, só será implementada na sub-classe (orientação a objetos é incrível né?!?!).
Conceitos de Orientação a Objetos
Para que fique mais clara nossa intenção no
método validate(), resolvi explicá-lo à contento. Trataremos aqui de
conceitos básicos de orientação a objetos, portanto, caso você já tenha
compreendido, perdoe-me pela mastigação demasiada do conteúdo :)
Conceito 1: uma interface em Java (e em OO) é um contrato. Toda classe que implementa essa interface assina esse contrato dizendo o seguinte: eu prometo sobrescrever todos os métodos definidos nessa interface.
Conceito 2: toda classe abstrata não pode ser instanciada. Isso significa que, mesmo que a classe implemente uma interface, ela não é obrigada a implementar todos os seus métodos. Isso é exatamente o que acontece em BasicBo. Como ela nunca poderá ser instanciada, não existe a necessidade de implementar o método validate(). Mas como suas classes filhas serão obrigadas a implementá-lo, ela pode fazer uso desse método internamente. É o que ocorre nos métodos create() e update().
Conceito 3: quando uma classe filha estender a BasicBo, precisará implementar o método validate() (desde que essa classe também não seja abstrata) e assim fará as devidas validações apropriadas ao caso. Ou seja, se for uma BO de Topic, nós faremos a validação de um tópico. O mesmo é válido para Metadata e Comment.
Conceito 4: Quando um objeto TopicBo (filho de BasicBo) estiver na JVM, e o método create() for chamado, ele fará uso do código definido em BasicBo, recorrendo a implementação de validate() de TopicBo.
Isso é o que vai acontecer na prática em nossa camada de Business. Como eu disse, fiz uma explicação muito detalhada.
Conceito 1: uma interface em Java (e em OO) é um contrato. Toda classe que implementa essa interface assina esse contrato dizendo o seguinte: eu prometo sobrescrever todos os métodos definidos nessa interface.
Conceito 2: toda classe abstrata não pode ser instanciada. Isso significa que, mesmo que a classe implemente uma interface, ela não é obrigada a implementar todos os seus métodos. Isso é exatamente o que acontece em BasicBo. Como ela nunca poderá ser instanciada, não existe a necessidade de implementar o método validate(). Mas como suas classes filhas serão obrigadas a implementá-lo, ela pode fazer uso desse método internamente. É o que ocorre nos métodos create() e update().
Conceito 3: quando uma classe filha estender a BasicBo, precisará implementar o método validate() (desde que essa classe também não seja abstrata) e assim fará as devidas validações apropriadas ao caso. Ou seja, se for uma BO de Topic, nós faremos a validação de um tópico. O mesmo é válido para Metadata e Comment.
Conceito 4: Quando um objeto TopicBo (filho de BasicBo) estiver na JVM, e o método create() for chamado, ele fará uso do código definido em BasicBo, recorrendo a implementação de validate() de TopicBo.
Isso é o que vai acontecer na prática em nossa camada de Business. Como eu disse, fiz uma explicação muito detalhada.
Vamos agora criar os @Qualifiers. Devem existir três tipos de
qualificadores: um para cada classe do modelo. Para criá-los, precisamos
criar novas anotações. Primeiramente, vamos criar o @Qualifier da entidade Topic: clique com o botão direito sobre o pacote br.com.linu.forum.business, selecione a opção New, Annotation. Ela deve-se chamar QTopic.
package br.com.linu.forum.business;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.inject.Qualifier;
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface QTopic {}
Como você pode notar, o código é simples. Crie agora a anotação QComment para a entidade Comment (também no pacote br.com.linu.forum.business).
package br.com.linu.forum.business;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.inject.Qualifier;
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface QComment {}
Por fim, vamos criar o código da anotação QMetadata.
package br.com.linu.forum.business;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.inject.Qualifier;
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface QMetadata {}
Só veremos a real necessidade dessas firulas na camada service. Agora que já temos nossas anotações, vamos efetivamente criar as classes de negócio. Primeiro, vamos criar a classe TopicBo no pacote br.com.linu.forum.business. Como já existe a superclasse BasicBo, só precisaremos implementar o código de validate() - que é o código mais importante dessa camada. Veja abaixo o código de TopicBo:
package br.com.linu.forum.business;
import java.io.Serializable;
import java.util.List;
import br.com.linu.forum.model.Topic;
@QTopic
public class TopicBo extends BasicBo implements Serializable {
private static final long serialVersionUID = 1L;
@SuppressWarnings("unchecked")
@Override
public void validate(Object entity) throws BusinessException {
try {
Topic topic = (Topic) entity;
/*
* É gente, esse foi nosso primeiro IF na aplicação
*/
if ( (topic.getUserName() == null) || (topic.getUserName().equals("")) )
topic.setUserName("Visitante");
List<Topic> list = (List<Topic>) super.read("topicByName", topic.getTitle());
if ( (list != null) && (list.size() > 0) )
throw new BusinessException("Já existe um tópico com esse título.");
} catch (BusinessException e) {
throw new BusinessException( e.getMessage() );
} catch (Exception e) {
throw new BusinessException("Tópico inválido.");
}
}
}
Bom, a ideia desse método é justamente atender as necessidades indicadas nos requisitos que ainda não foram definidas. A primeira condição define o valor "Visitante" caso não exista o nome do usuário criador (lembre-se que não anotamos userName com a @NotNull). Em seguida, uma pesquisa é realizada com o título do tópico. Caso exista algum tópico cadastrado com esse título, o cadastro será cancelado pelo disparo da BusinessException - lembre-se que esse método é chamado antes do comando create() e update(). Só para constar, os requisitos 1, 4 e 6 foram atendidos por esse método. Note ainda que o requisito 1 não foi, plenamente, atendido. Isso se deve ao fato que o Topic ainda não contém a data de criação. Trataremos isso no pacote Services.
package br.com.linu.forum.business;
import java.io.Serializable;
import java.util.List;
import br.com.linu.forum.model.Topic;
@QTopic
public class TopicBo extends BasicBo implements Serializable {
private static final long serialVersionUID = 1L;
@SuppressWarnings("unchecked")
@Override
public void validate(Object entity) throws BusinessException {
try {
Topic topic = (Topic) entity;
/*
* É gente, esse foi nosso primeiro IF na aplicação
*/
if ( (topic.getUserName() == null) || (topic.getUserName().equals("")) )
topic.setUserName("Visitante");
List<Topic> list = (List<Topic>) super.read("topicByName", topic.getTitle());
if ( (list != null) && (list.size() > 0) )
throw new BusinessException("Já existe um tópico com esse título.");
} catch (BusinessException e) {
throw new BusinessException( e.getMessage() );
} catch (Exception e) {
throw new BusinessException("Tópico inválido.");
}
}
}
Bom, a ideia desse método é justamente atender as necessidades indicadas nos requisitos que ainda não foram definidas. A primeira condição define o valor "Visitante" caso não exista o nome do usuário criador (lembre-se que não anotamos userName com a @NotNull). Em seguida, uma pesquisa é realizada com o título do tópico. Caso exista algum tópico cadastrado com esse título, o cadastro será cancelado pelo disparo da BusinessException - lembre-se que esse método é chamado antes do comando create() e update(). Só para constar, os requisitos 1, 4 e 6 foram atendidos por esse método. Note ainda que o requisito 1 não foi, plenamente, atendido. Isso se deve ao fato que o Topic ainda não contém a data de criação. Trataremos isso no pacote Services.
Conceitos de Orientação a Objetos
Bom, não pude deixar de comentar a utilização
de IFs nesse código. Observe que essas foram as primeiras estruturas de
condição que utilizamos em nosso projeto. Naturalmente, em métodos de
negócio, a utilização de IFs é comum - portanto, don´t worry!! Vamos
tentar construir nossa aplicação com a menor quantidade de condições
possíveis. A orientação a objetos agradece!
Outro detalhe é que nós qualificamos essa classe com a anotação @QTopic - um @Qualifier que usaremos para evitar problemas de injeção de dependência nas classes futuras.
Vamos agora implementar o objeto de negócio da entidade Comment. Cria uma classe CommentBo no pacote business.
package br.com.linu.forum.business;
import java.io.Serializable;
import br.com.linu.forum.model.Comment;
@QComment
public class CommentBo extends BasicBo implements Serializable {
private static final long serialVersionUID = 1L;
@Override
public void validate(Object entity) throws BusinessException {
try {
Comment comment = (Comment) entity;
if ( comment.getTopic().getId() == 0 )
throw new BusinessException("Tópico inválido.");
if ( (comment.getUserName() == null) || (comment.getUserName().equals("")) )
comment.setUserName("Visitante");
else {
String topicUser = comment.getTopic().getUserName();
if (comment.getUserName().equals( topicUser ) )
throw new BusinessException("O usuário criador do tópico não pode comentá-lo.");
}
} catch (BusinessException e) {
throw new BusinessException( e.getMessage() );
} catch (Exception e) {
throw new BusinessException( e.getMessage() );
}
}
}
De forma semelhante, utilizamos nesse BO o respectivo qualificador @QComment. Logo após o casting para o objeto Comment,
a sequência verifica a validade do tópico associado ao comentário. Em
seguida, os comandos são responsáveis pelos requisitos 4 e 6. Veja que,
caso o usuário seja um Visitante, não há necessidade de validar a regra 6 - pois outros visitantes poderão comentar o tópico criado por um Visitante. Caso algum erro exista na Comment, o procedimento de criação e atualização na base de dados será interrompido pelo disparo de uma BusinessException.
Por fim, vamos criar o BO para a classe Metadata. No pacote br.com.linu.forum.business, crie uma classe chamada MetadataBo.
package br.com.linu.forum.business;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import br.com.linu.forum.model.Metadata;
@QMetadata
public class MetadataBo extends BasicBo implements Serializable {
private static final long serialVersionUID = 1L;
private static char dots[] = { '.' , ',' , ';' , ':' , '?' , '\"' , '\'' , '!' , '\n' };
private boolean correctWord(String word) {
if ( (word == null) || (word.equals("")) ) return false;
return true;
}
public List<String> correctWords(String words) {
String temp = words;
for (char dot : dots) {
temp = temp.replace(dot, ' ');
}
List<String> list = new ArrayList<String>();
Scanner scanner = new Scanner( temp ).useDelimiter( " " );
while ( scanner.hasNext() ) {
String word = scanner.next();
if ( correctWord(word) )
list.add( (word.trim()).toUpperCase() );
}
return list;
}
@Override
public void validate(Object entity) throws BusinessException {
try {
Metadata metadata = (Metadata) entity;
int size = metadata.getWord().length();
if ( (size < 3) || (size > 255) )
throw new BusinessException("A palavra de pesquisa precisa conter entre 3 e 255 caracteres.");
metadata.setWord( metadata.getWord() );
} catch (BusinessException e) {
throw new BusinessException( e.getMessage() );
} catch (Exception e) {
throw new BusinessException( e.getMessage() );
}
}
}
Veja que, dependendo do tamanho da palavra que será inserida (ou atualizada), será disparada uma BusinessException.
Isso se faz necessário para poder controlar quais palavras serão
utilizadas na pesquisa (requisito 8). Não faz sentido buscar palavras
como "de" ou "só", por exemplo. Da mesma forma, palavras
maiores que 255 podem ser consideradas inválidas. Esses parâmetros são
validados no método validate(). Observe que foram também acrescidos dois métodos públicos para correção dos valores. O método correctWord() retorna true caso a String seja válida (diferente de null e diferente de vazio). Já o método correctWords() retorna uma lista com todas as palavras de um texto. Adicionamos também (dentro da lógica de correctWords()) uma correção que retira espaços em branco (pelo trim()),
quebra de linhas e pontos das palavras. Isso é necessário para
facilitar a consulta de termos encontrados no final de frases (tenha em
mente que "casa" é diferente de "casa." ou "casa\n"). Ainda neste método faremos com que todas as palavras sejam definidas em caixa-alta - através do método toUpperCase() - pois existe diferença nas palavras "CaSa" e "casa". Esses métodos, contudo, não serão utilizados internamente. Eles foram construídos para fornecer recursos de validação do Metadata para a camada Service - como veremos adiante. Por fim, observe que a anotação @QMetadata foi definida. Com essa classe terminamos a implementação das classes de negócio.
Conceitos de Orientação a Objetos
Essa foi a melhor forma de se implementar uma
camada BO? Certamente, não! Optei em utilizar todos esses recursos para
tratar de elementos novos oferecidos pelo Java EE 6. Veremos como o @Qualifier faz seu controle nas classes da camada Services.
Está tudo certo? Não!
Lembre-se que, a única restrição (além daqueles 4 pontos identificados no início) para que esses Beans sejam considerados Managed Beans é que exista o arquivo beans.xml no projeto. E é exatamente isso que vamos fazer agora. Como o plugin do Java EE 6 do Eclipse ainda não oferece todas as ferramentas da especificação EE 6, vamos adicionar o XSD (modelo necessário para autenticar o XML) correspondente ao beans.xml antes de criar o arquivo em si. Para isso, vamos seguir os seguintes passos:
Clique com o botão direito sobre o projeto SuperForumEJB, selecione a opção New, Other...

Selecione a opção XML Schema dentro da pasta XML. Clique em Next

Selecione a pasta META-INF localizada dentro de ejbModule, no projeto SuperForumEJB e defina o valor de File name como beans_1_0.xsd
Em seguida, clique em Finish
O arquivo XSD foi criado, porém ainda não contém as regras de validação do XML beans.xml. Vamos codificá-lo (na verdade, copiá-lo).
Abra o arquivo beans_1_0.xsd, e clique na aba Source (aba localizada na parte inferior do visualizador do arquivo, á direita do Eclipse). Nesta aba, você poderá modificar manualmente o código do arquivo.
Acesse o site: http://java.sun.com/xml/ns/javaee/beans_1_0.xsd
Copei todo o conteúdo apresentado no site e cole no arquivo beans_1_0.xsd.
Salve as alterações no arquivo e feche-o. Dessa forma, manteremos os
detalhes de configuração do XML em nosso próprio projeto. Podemos agora,
enfim, criar o arquivo beans.xmlEm seguida, clique em Finish
O arquivo XSD foi criado, porém ainda não contém as regras de validação do XML beans.xml. Vamos codificá-lo (na verdade, copiá-lo).
Abra o arquivo beans_1_0.xsd, e clique na aba Source (aba localizada na parte inferior do visualizador do arquivo, á direita do Eclipse). Nesta aba, você poderá modificar manualmente o código do arquivo.
Acesse o site: http://java.sun.com/xml/ns/javaee/beans_1_0.xsd
Clique com o botão direito sobre o projeto SuperForumEJB, menu New, opção Other...

Selecione a opção XML dentro da pasta XML e clique em Next

Selecione o projeto SuperForumEJB, o diretório ejbModule, o sub-diretório META-INF como localização do novo arquivo. Defina seu nome como beans.xml e clique em Next

Nessa próxima janela, você deverá escolher a opção Create XML file from an XML schema file. Clique em Next

Selecione a opção Select file from Workspace e indique o caminho do arquivo beans_1_0.xsd criado anteriormente (SuperForumEJB/ejbModule/META-INF). Clique em Next


No campo Prefix, havia o valor javaee. Apague esse valor e deixe o campo Prefix em branco (como na figura acima). Clique em OK

Dessa forma, estamos indicando que não haverá prefixos no elemento principal do XML chamado beans - formatação geralmente utilizada nesse arquivo beans.xml. Clique em Finish
O Eclipse criará um arquivo beans.xml que conterá o seguinte conteúdo:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee beans_1_0.xsd ">
Esse conteúdo, conforme havíamos indicado, é suficiente para que o CDI esteja disponível nesse projeto. A utilização da @Inject agora já é possível. Em futuros plugins do Eclipse, todos esses passos não serão mais necessários.
As classes da camada de negócios terminaram. Vamos agora implementar as últimas classes do projeto SuperForumEJB: as classes da camada Service.
Desenvolvendo as classes Services
Vamos agora disponibilizar os serviços de nosso sistema. Esses serviços atuam diretamente relacionados com a camada de negócios, trabalhando para cumprir os requisitos estabelecidos.
Conceitos de Orientação a Objetos
As classes da camada de negócios terminaram. Vamos agora implementar as últimas classes do projeto SuperForumEJB: as classes da camada Service.
Desenvolvendo as classes Services
Vamos agora disponibilizar os serviços de nosso sistema. Esses serviços atuam diretamente relacionados com a camada de negócios, trabalhando para cumprir os requisitos estabelecidos.
Conceitos de Orientação a Objetos
É importante ressaltar que, em projetos reais, o pacote service e a camada Business Core
poderiam ficar disponíveis em classes comuns. Isso porque o
funcionamento dos métodos (serviços) seria controlado em métodos de
negócio - talvez em uma única classe para cada entidade. Aqui optamos em
separar regras BO e serviços para intensificar a ideia de separação de
responsabilidades e para utilizar uma gama maior de recursos novos do
Java EE 6.
Iniciaremos com a criação de um novo pacote. Clique com o botão direito no projeto SuperForumEJB, opção New, Package. Defina o nome do novo pacote como br.com.linu.forum.services
O que faremos agora?
A criação dos serviços está diretamente relacionada aos recursos que precisamos disponibilizar - ou seja, muito ligados aos requisitos funcionais. Os requisitos 1 e 3 indicam dados que compõem objetos do modelo. Logo, esses objetos precisam ser cadastrados no banco de dados. Vamos criar métodos de serviços para cada ocasião. Um tópico pode ser criado sem a necessidade de estar associado a nenhuma outra entidade. Portanto, um serviço createTopic() já pode ser identificado.
Como vamos fazer?
Fiz essas observações para que você visualize a existência de uma forte lógica de orientação a objetos nessa definição. Sempre que os conceitos de OO precisarem ser explorados com maior ênfase do quê a própria lógica interna dos métodos, é interessante que exista uma modelagem, pelo menos, por um diagrama de classes. Portanto, para facilitar a identificação dos outros métodos (serviços), vamos modelá-los através de um novo diagrama.

Como você pode ver, a definição dos comportamentos é mais fácil de
ser identificada através da modelagem em UML (é lógico né!!! É para isso
que serve). De forma explícita, o 1º requisito funcional será atendido
pelo método createTopic() da classe TopicService. A adição de comentários aos tópicos (requisito 2) é controlada pela classe CommentService, através do método addComment(). O 3º requisito já está implícito nesse mesmo método. O requisito 4 já havia sido atendido na camada de negócios pelo método validate(). O 5º requisito funcional será atendido pelo método getTopics(). Os requisitos 6 e 7 já foram atendidos pelo validate() da camada de negócios. Por fim, o 8º requisito será atendido pelos métodos searchTopic() de TopicService e createMetadata()/updateMetadata() da classe MetadataService. As classes HitList e Word e o método putWords() (da classe MetadataService)
serão utilizados para construir o mecanismo de metadados associados com
a pesquisa do requisito 8. Mais adiante entraremos em maiores detalhes
sobre a implementação. Existe também o método getTopic() de TopicService para buscar um tópico pelo Id. Esse método faz parte, implicitamente, do mecanismo de exibição de comentários.
Após a programação desse pacote, todos os requisitos funcionais serão atendidos. Restará apenas a criação das telas (camada View). Vamos ao trabalho!Crie a classe TopicService dentro do pacote br.com.linu.forum.services. O código dessa classe pode ser visualizado abaixo.
package br.com.linu.forum.services;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import br.com.linu.forum.business.Business;
import br.com.linu.forum.business.BusinessException;
import br.com.linu.forum.business.QMetadata;
import br.com.linu.forum.business.QTopic;
import br.com.linu.forum.model.Metadata;
import br.com.linu.forum.model.Topic;
public class TopicService implements Serializable {
private static final long serialVersionUID = 1L;
private @Inject @QTopic Business business;
private @Inject @QMetadata Business metadataBo;
public void createTopic(Topic topic) throws BusinessException {
topic.setCreationDate( new Date() );
business.create( topic );
}
@SuppressWarnings("unchecked")
public List<Topic> getTopics() throws BusinessException {
return (List<Topic>) business.read("topics");
}
@SuppressWarnings("unchecked")
public List<Topic> searchTopics(String word) throws BusinessException {
if ( word.trim().indexOf(" ") > -1 )
throw new BusinessException("Somente uma palavra pode ser consultada por vez.");
List<Metadata> list = (List<Metadata>) metadataBo.read( "metadatas" , word.toUpperCase() );
List<Topic> topics = new ArrayList<Topic>();
for ( Metadata metadata : list ) {
topics.add( metadata.getTopic() );
}
return topics;
}
public Topic getTopic(long id) throws BusinessException {
return (Topic) this.business.read(Topic.class, id);
}
public Business getBusiness() {
return this.business;
}
}
Aqui nós temos novos recursos. A novidade gira em torno do CDI. Veja que a sintaxe @Inject foi definida. Nela nós estamos adicionando um dos Managed Beans do projeto na interface Business. Como nós criamos o qualificador @QTopic, o servidor de aplicação será capaz de entender que o Managed Bean injetado será o TopicBo (pois este foi anotado com @QTopic). Pois é, o famoso recurso de injeção de dependência proposto por Martin Fowler (http://martinfowler.com/articles/injection.html) agora existe nativamente na especificação enterprise do Java. Como você pode notar, o mesmo mecanismo de CDI (uso do @Inject e @QMetadata) foi utilizado para injetar um MetadataBo no atributo de mesmo nome.
Conforme a modelagem, temos aqui a implementação dos métodos de serviço. Veja que a data atual foi definida dentro do método createTopic(). Dessa forma o 1º requisito foi atendido plenamente. O método getBusiness() não estava presente na modelagem por se tratar apenas de um getter para o atributo business. Note ainda que, no método searchTopics(), a condição que procura por " " (espaços) em word garante que só exista uma palavra na String - isso é exatamente o que sugere o requisito 8. Veja que esse método utiliza o Business Object de Metadata (ao invés do TopicBo chamado business) e depois obtém, de cada Metadata, os tópicos associados.
Pronto, nossos serviços associados ao tópico estarão disponíveis,
naturalmente, pelo CDI. Faremos referência a ele numa classe Facade mais adiante.
Crie agora uma classe chamada CommentService no pacote br.com.linu.forum.services.
Crie agora uma classe chamada CommentService no pacote br.com.linu.forum.services.
package br.com.linu.forum.services;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import br.com.linu.forum.business.Business;
import br.com.linu.forum.business.BusinessException;
import br.com.linu.forum.business.QComment;
import br.com.linu.forum.model.Comment;
import br.com.linu.forum.model.Topic;
public class CommentService implements Serializable {
private static final long serialVersionUID = 1L;
private @Inject @QComment Business business;
@SuppressWarnings("unchecked")
public List<Comment> getComments(Topic topic) throws BusinessException {
Long id = topic.getId();
return (List<Comment>) business.read("comments", id);
}
public void addComment(Topic topic, Comment comment) throws BusinessException {
comment.setCreationDate( new Date() );
comment.setTopic(topic);
business.create( comment );
}
public Business getBusiness() {
return this.business;
}
}
Usamos aqui os recursos de CDI e @Qualifier para injetar o CommentBo em Business. Os métodos oferecidos são simples. Repare que em addComment() a data de criação e o tópico ao qual o comentário está associado são definidos (em conformidade com o requisito 3). A busca getComments() foi construída para buscar os comentários associados a um tópico específico.
Por fim, faremos agora a implementação do MetadataService, HitList e Word. Esse conjunto de classes apresenta a maior complexidade do sistema. Antes da codificação, faremos um apanhado de como funcionará a lógica de busca de tópicos.
Como comentado, o MetadataService utilizará as classes HitList e Word (veja que isso foi representado na UML por uma dependência) para auxiliar o mecanismo de pesquisa do requisito 8. A classe HitList contém uma associação com Word. Ela será responsável por gerenciar ocorrências das palavras encontradas em um tópico ou comentário. Através dos métodos polifórmicos put() da classe HitList, acrescentaremos palavras obtidas dos textos (dos tópicos ou comentários) a uma listagem. Quando a palavra já existe na listagem, um contador é acrescido (indicando que existe mais uma ocorrência da palavra). Quando a palavra não existe, um novo elemento é acrescido na listagem. A classe controladora das palavras é a Word. A classe HitList mantém justamente uma listagem de Word. Esse mecanismo será utilizado nos serviços disponíveis em MetadataService.
Por fim, faremos agora a implementação do MetadataService, HitList e Word. Esse conjunto de classes apresenta a maior complexidade do sistema. Antes da codificação, faremos um apanhado de como funcionará a lógica de busca de tópicos.
Como comentado, o MetadataService utilizará as classes HitList e Word (veja que isso foi representado na UML por uma dependência) para auxiliar o mecanismo de pesquisa do requisito 8. A classe HitList contém uma associação com Word. Ela será responsável por gerenciar ocorrências das palavras encontradas em um tópico ou comentário. Através dos métodos polifórmicos put() da classe HitList, acrescentaremos palavras obtidas dos textos (dos tópicos ou comentários) a uma listagem. Quando a palavra já existe na listagem, um contador é acrescido (indicando que existe mais uma ocorrência da palavra). Quando a palavra não existe, um novo elemento é acrescido na listagem. A classe controladora das palavras é a Word. A classe HitList mantém justamente uma listagem de Word. Esse mecanismo será utilizado nos serviços disponíveis em MetadataService.
Veremos agora o código dessas classes. Crie uma classe Word no pacote br.com.linu.forum.services.
package br.com.linu.forum.services;
import java.io.Serializable;
public class Word implements Serializable {
private static final long serialVersionUID = 1L;
private String value;
private long count;
public Word(String value) {
this( value , 1 );
}
public Word(String value, long count) {
this.value = value;
this.count = count;
}
public void add() { this.count++; }
public String getValue() { return this.value; }
public long getCount() { return this.count; }
@Override
public boolean equals(Object word) {
if ( word instanceof Word ) {
Word temp = (Word) word;
return this.getValue().equals( temp.getValue() );
} return false;
}
}
Veja que nada de novo existe aqui. Observe apenas que o constructor Word() com a assinatura String value define count como 1. Veja também que o método equals() foi sobrescrito. Neste caso, para que um objeto Word seja igual a outro, basta que seus values sejam iguais.
Dentro do pacote br.com.linu.forum.services, crie uma classe chamada HitList.
package br.com.linu.forum.services;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class HitList implements Serializable {
private static final long serialVersionUID = 1L;
private List<Word> words;
public HitList() {
this.words = new ArrayList<Word>();
}
public void put(List<Word> list) {
for ( Word newWord : list ) {
for (int i = 0; i < newWord.getCount(); i++) {
this.put( newWord.getValue() );
}
}
}
public void put(String value) {
for (Word word : this.words) {
if ( word.getValue().equals( value ) ) {
word.add();
return;
}
}
this.words.add( new Word( value ) );
}
public List<Word> get() {
return this.words;
}
}
Não entraremos em detalhes sobre a lógica interna dos métodos, apenas vamos identificar suas utilidades. Os métodos put() servem para incluir uma nova palavra na listagem (assinado com String value) e um conjunto de Word à listagem (quando assinado com List<Word> list). A principal ideia de put(String value) é
inserir ou atualizar uma palavra. Veja que esse método percorre toda a
lista de palavras. Se ele encontrar uma palavra igual ao value, o
contador da palavra na listagem é acrescido e o método termina. Caso
não exista ocorrência da palavra na listagem, o método adiciona um novo Word (com construtor Word(String value), que automaticamente define count como 1) na lista. Dessa forma, a lista terá várias palavras com no mínimo uma ocorrência cada. Já o método put(List<Word> list) adiciona cada um dos elementos de list (parâmetro) na listagem de palavras através do método put(String value) - que já conhecemos. Por fim, o método get() apenas retorna a lista de Word. Veja que a lista nunca será nula, pois o construtor de HitList sempre irá iniciá-la.
Vamos agora a codificação da classe MetadataService. Cria uma classe com esse nome no pacote de serviços.
package br.com.linu.forum.services;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import br.com.linu.forum.business.Business;
import br.com.linu.forum.business.BusinessException;
import br.com.linu.forum.business.MetadataBo;
import br.com.linu.forum.business.QMetadata;
import br.com.linu.forum.model.Comment;
import br.com.linu.forum.model.Metadata;
import br.com.linu.forum.model.Topic;
public class MetadataService implements Serializable {
private static final long serialVersionUID = 1L;
private @Inject @QMetadata Business business;
public void createMetadata(Topic topic) {
HitList hitList = new HitList();
this.putWords(hitList, topic.getTitle());
this.putWords(hitList, topic.getText());
for (Word word : hitList.get()) {
Metadata metadata = new Metadata();
metadata.setWord( word.getValue() );
metadata.setOccurrences( word.getCount() );
metadata.setTopic( topic );
try {
business.create(metadata);
} catch (BusinessException e) {
/* String inválidas não serão gravadas */
}
}
}
@SuppressWarnings("unchecked")
public void updateMetadata(Comment comment) throws BusinessException {
HitList hitList = new HitList();
this.putWords(hitList, comment.getText() );
Long id = comment.getTopic().getId();
Map<String, Metadata> map = new HashMap<String, Metadata>();
List<Metadata> list = (List<Metadata>) business.read( "metadatasByTopic" , id );
List<Word> words = new ArrayList<Word>();
for ( Metadata md : list ) {
map.put(md.getWord(), md);
Word word = new Word( md.getWord() , md.getOccurrences() );
words.add( word );
}
hitList.put( words );
for (Word word : hitList.get()) {
Metadata metadata = map.get( word.getValue() );
if ( metadata == null ) {
metadata = new Metadata();
metadata.setWord( word.getValue() );
metadata.setTopic( comment.getTopic() );
}
metadata.setOccurrences( word.getCount() );
try {
business.update(metadata);
} catch (BusinessException e) {
/* String inválidas não serão gravadas */
}
}
}
private void putWords(HitList hitList, String words) {
List<String> correctWords = ( (MetadataBo) business ).correctWords( words );
for (String word : correctWords) {
hitList.put( word );
}
}
}
Assim como as outras classes de serviços, utilizamos dos recursos de @Qualifier e @Inject oferecidos pelo CDI para injetar a classe de Business do Metadata no atributo business. A grande complexidade dessa classe gira em torno de seus métodos. Veremos agora o funcionamento específico de cada um.
O método privado putWords() foi definido para obter todas as palavras do texto (parâmetro words) e colocá-las, uma a uma, na HitList. Lembre-se que nós criamos um método para corrigir (tirar pontos, espaços e transformar as palavras em caixa-alta) em MetadataBo. O atributo business precisou sofrer um casting para MetadataBo antes que o método correctWords() fosse chamado. Dessa forma, HitList só conterá palavras corretas. Esse método será utilizado internamente nos outros métodos da classe.
Antes de explicarmos o funcionamento do createMetadata(), tenha em mente que um tópico jamais poderá ser alterado. Isso significa que, sempre que incluirmos um novo tópico no sistema, nenhum Metadata existirá para esse tópico e, depois de adicionado, nenhum Metadata poderá sofrer alterações a partir de modificações no tópico – apenas com adições de comentários (como veremos adiante). Por esse motivo o método createMetadata() é assinado com um Topic – sempre que criarmos tópicos, estaremos criando novos Metadatas.
Em primeiro momento, o método createMetadata() cria uma nova instância de HitList. Em seguida, todas as ocorrências de palavras presentes no título e texto deste tópico serão adicionadas na lista. O próximo comando (looping for-each – veja em http://linubr.blogspot.com.br/2012/08/utilizando-variavel-final-no-looping.html) cria entidades Metadata para cada ocorrência de Word de HitList. Esses Metadatas são então, um a um, incluídos no banco de dados através do comando business.create(metadata);. Veja que, caso ocorra algum erro na inserção dessas entidades, nada será realizado. Isso ocorre pelo fato que palavras inapropriadas (com length inferior a 3 e superior a 255) não serão armazenadas, porém, o usuário não precisa ter conhecimento disso. Esse é o funcionamento de createMetadata().
Vamos agora entender o funcionamento de updateMetadata(). Lembre-se que a única forma de atualizar Metadatas associadas a determinado tópico seria através da adição de comentários a esse tópico. Logo, a assinatura de updateMetada() só nos trás o parâmetro Comment. É importante também conhecer como o mecanismo merge() do JPA (interface EntityManager) trabalha. Esse mecanismo é utilizado para atualizar entidades no banco de dados. Se a entidade existir, ele atualiza, caso contrário ele cria uma nova entidade. Isso é exatamente o que precisamos em nosso método.
Por quê? Quando um comentário é adicionado a um tópico, podem existir palavras nesse comentário que não existiam no tópico (e com grande frequência isso vai ocorrer). Portanto, o comando business.update(metadata); tentará atualizar a entidade Metadata e, caso esta não exista, criará uma nova entidade. Perfeito, vamos agora a lógica do método.
Primeiramente ele adiciona as palavras do texto do comentário numa nova instância de HitList. Em seguida ele obtém todas as Metadatas associadas ao id do tópico (tópico associado, logicamente, ao comentário) e adiciona essas palavras na HitList e numa Map (onde a key da Map é a própria palavra). Por fim o método atualiza (ou inseri) todas as ocorrências de palavras existentes na HitList no banco de dados.
Veja que se a Metadata existir na Map (que é buscada pela palavra), o loop não criará uma nova Metadata. Porque isso é necessário? A Map guarda os objetos obtidos do banco de dados (inclusive com seus IDs). Se criássemos novas Metadatas e chamássemos o método merge() do EntityManager (que está encapsulado no update() de business), novos objetos Metadata seriam automaticamente inseridos nas tabelas (pois esses não possuem seus IDs originais). Já com os objetos resgatados do banco, precisamos apenas alterar seus dados (no caso, occurrences) e pedir ao business para atualizá-lo (através de update()). Para os objetos que ainda não existem (logo, não estão presentes no Map), novos Metadatas serão instanciados, preenchidos e registrados.
Bom, essa é a lógica de nosso objeto MetadataService.
Agora já temos todos os nossos serviços programados. As classes TopicService e CommentService estarão disponibilizadas para o projeto SuperForumWeb através de uma classe de fachada que criaremos. Já a classe MetadataService não será chamada diretamente pelos recursos da camada View.
Mas então quem vai chamar os métodos createMetadata() e updateMetadata()?
Elementar meu caro Watson! Vamos aproveitar a situação para mostrar um novo recurso disponível no Java EE 6: a anotação @Decorator. Uauuuu! Essa anotação implementa o pattern Decorator? Exato.
Utilizando o pattern Decorator através da anotação @Decorator
Como sabemos, os conceitos de padrões GoF nos oferece um recurso Decorator. Esse padrão de projeto é utilizado para adicionar funcionalidades extras a objetos em tempo de execução. Muito do Java SE já utiliza desses recursos em sua API (por exemplo, um BufferedReader decora um FileReader). Para maiores detalhes, veja http://en.wikipedia.org/wiki/Decorator_pattern
O Java EE 6 permite que o servidor de aplicação gerencie decoradores através da anotação @Decorator. O que isso tem haver com nosso sistema? Pois é, lembre-se que sempre que um tópico for criado, deverão ser criadas Metadatas e sempre que um comentário for adicionado, deverão ser atualizadas Metadatas. Não percebe que se criarmos classes decoradoras para os serviços createTopic() de TopicService e addComment() de CommentService, poderíamos chamar os métodos já implementados da classe MetadataService. E é exatamente isso que faremos agora.
Antes de explicarmos as novas anotações, vamos enfatizar que serão construídos dois Decorators: um para decorar a TopicService e o outro para decorar a CommentService. Crie uma nova classe chamada TopicServiceDecorator no pacote br.com.linu.forum.services e codifique-a com o conteúdo apresentado abaixo.
package br.com.linu.forum.services;
import java.io.Serializable;
import javax.decorator.Decorator;
import javax.decorator.Delegate;
import javax.inject.Inject;
import br.com.linu.forum.business.BusinessException;
import br.com.linu.forum.model.Topic;
@Decorator
public class TopicServiceDecorator implements Serializable {
private static final long serialVersionUID = 1L;
private @Inject MetadataService metadataService;
private @Inject @Delegate TopicService topicService;
public void createTopic(Topic topic) throws BusinessException {
topicService.createTopic(topic);
metadataService.createMetadata(topic);
}
}
Vamos aos detalhes. Note que a classe é anotada com @Decorator. Existe também a necessidade de declarar quem será o Delegate Injection Point da classe. Esse cara indica qual classe será decorada por esse @Decorator. Isso é obrigatório! Como se pode notar, a utilização da anotação @Delegate serve
justamente para isso. Aqui utilizamos injeção de dependência de forma
direta, sem a necessidade de qualificadores - tanto para MetadataService quanto para TopicService
(neste caso, os atributos, por si só, são capazes de dizer ao servidor
de aplicação qual instância deve ser obtida - pois estamos guardando uma
instância de MetadataService na própria classe MetadataService). Além dessa codificação, precisamos declarar todos os nossos decoradores no arquivo beans.xml. Deixaremos isso para o final, pois criaremos ainda outro decorador.
Conceitos de Orientação a Objetos
É importante ressaltar que o método createTopic() da classe decoradora deve ser exatamente igual ao método da classe decorada (neste caso, TopicService). Seria interessante inclusive criar uma interface comum entre @Decorator e @Delegate (o que inclusive ocorre no padrão GoF original). Porém, como nosso serviço TopicService foi publicado sem interface (@Local ou @Remote)
para enfatizar que isso é possível no Java EE 6, vamos mantê-lo sem
essa característica. Em casos reais, recomendo o uso dessa interface
comum, ok?
Vamos agora ao próximo decorador. Crie uma classe chamada CommentServiceDecorator no pacote de serviços.
package br.com.linu.forum.services;
import java.io.Serializable;
import javax.decorator.Decorator;
import javax.decorator.Delegate;
import javax.inject.Inject;
import br.com.linu.forum.business.BusinessException;
import br.com.linu.forum.model.Comment;
import br.com.linu.forum.model.Topic;
@Decorator
public class CommentServiceDecorator implements Serializable {
private static final long serialVersionUID = 1L;
private @Inject MetadataService metadataService;
private @Inject @Delegate CommentService commentService;
public void addComment(Topic topic, Comment comment) throws BusinessException {
commentService.addComment(topic, comment);
metadataService.updateMetadata(comment);
}
}
De forma semelhante, utilizamos a anotação @Decorator para indicar que está classe será uma decoradora, um @Inject para injetar a classe de serviços de Metadata e @Delegate para identificar a classe decorada. O método addComment(), como vimos, precisa ser idêntica ao método addComment() do objeto @Delegate - aqui também optamos em não usar uma interface.
Agora, como a especificação manda, faremos a identificação das classes
decoradoras para o servidor de aplicação. Abra o arquivo beans.xml localizado em SuperForumEJB/ejbModule/META-INF e deixe-o igual ao código abaixo.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee beans_1_0.xsd ">
<decorators>
<class>br.com.linu.forum.services.TopicServiceDecorator</class>
<class>br.com.linu.forum.services.CommentServiceDecorator</class>
</decorators>
</beans>
Através das tags <decorators> e <class> podemos indicar os decoradores criados.
Dessa forma, o fluxo natural da aplicação foi redefinido. Agora,
sempre que um objeto gerenciado pelo servidor de aplicação EE 6 fizer
uma chamada aos métodos createTopic() de TopicService e addComment() de CommentService, automaticamente, seus decoradores TopicServiceDecorator e CommentServiceDecorator serão chamados, respectivamente.
Criaremos agora uma classe de fachada para facilitar o acesso da outra aplicação (SuperForumWeb). Crie uma classe chamada ServiceFacade no pacote br.com.linu.forum.services. Veja o código abaixo.
package br.com.linu.forum.services;
import java.io.Serializable;
import javax.ejb.Stateless;
import javax.inject.Inject;
@Stateless(name="ejb/facade")
public class ServiceFacade implements Serializable {
private static final long serialVersionUID = 1L;
private @Inject TopicService topicService;
private @Inject CommentService commentService;
public TopicService getTopicService() {
return topicService;
}
public CommentService getCommentService() {
return commentService;
}
}
Primeiramente observe que estamos disponibilizando um EJB com o comando @Stateless(name="ejb/facade") sem a prévia criação de uma interface (@Local ou @Remote). Como já havia comentado, no Java EE 6 isso não é mais necessário. Fizemos aqui a injeção dos serviços TopicService e CommentService. O acesso ao MetadataService será
realizado automaticamente através da configuração dos decoradores -
como comentado. Esse Session Bean será o único meio de acesso para os
recursos da camada View.
Conceitos de Orientação a Objetos
Conceitos de Orientação a Objetos
Mais uma vez estou enfatizando que a criação de todos esses artefatos (@Qualifier, @Decorator, objetos
em CDI, classes BO e Service) para a construção da camada de negócios
não é necessária (e nem recomendada num sistema simples como esse).
Estamos criando neste artigo uma aplicação-exemplo pequena que tentará
explorar vários recursos do Java EE 6. Certamente em uma grande
aplicação, tudo isso será necessário.
Bom, é isso! Nossa extensa camada de negócios foi concluída. Vamos, enfim, para o projeto SuperForumWeb.
Continuação
Conforme vimos, esse documento foi dividido. Caso queira ver a primeira parte, clique aqui.
A 3ª parte, cujo link está abaixo, tratará exclusivamente da camada View.
"A farda modela o corpo e atrofia a mente." - Che Guevara
Att, Guilherme Pontes
[linu.com.br] - [parte 1] - [parte 2] - [parte 3] - [parte 4] - lgapontes@gmail.com |
Nenhum comentário:
Postar um comentário