Skip to content

Manual do Desenvolvedor – Arquitetura e Organização do Projeto

Vítor E. Silva Souza edited this page Jul 30, 2020 · 4 revisions

Para começar a contribuir com código para o Marvin, é preciso em primeiro lugar entender sua arquitetura a organização do projeto Marvin no Eclipse.

Arquitetura do Marvin

A arquitetura do Marvin é muito ligada à arquitetura do JButler, vista no exercício JButler incluído neste manual.

Em primeiro lugar todo o código do Marvin encontra-se abaixo do pacote br.ufes.informatica.marvin. A partir deste pacote, são definidos pacotes que representam os módulos do Marvin. A figura abaixo mostra os módulos que já haviam sido criados no momento que esta página do manual foi escrita (você pode ver essa pasta clicando no ícone 🔗, que será usado ao longo do texto para ligar com o respectivo código no repositório do Marvin):

São eles:

  • admin: inclui funcionalidades restritas ao administrador do sistema (que é tipo um usuário root), geralmente os cadastros (CRUDs) irrestritos devem ser apenas para o administrador;
  • core: inclui as funcionalidades do núcleo do sistema, como login/logout, criar/editar sua conta, mudar senha, etc.;
  • people: inclui classes de domínio genéricas para representação de pessoas (apenas domínio e persistência);
  • research: inclui funcionalidades gerais relacionadas a atividades de pesquisa, como importação de dados de currículo Lattes e geração de arquivos de bibliografia com base nesta importação. Fora os módulos de núcleo, este pode ser considerado o primeiro módulo do Marvin, caso você queira se espelhar para criar um novo módulo.

Cada módulo é então subdividido em cinco pacotes, organizados em três camadas: apresentação (visão e controle), lógica de negócio (aplicação e domínio) e acesso a dados (persistência). A figura abaixo (adaptada do JButler) ilustra esta arquitetura e indica os componentes que estão presentes em cada pacote, bem como a atuação das tecnologias Jakarta EE envolvidas:

Para explicar em mais detalhes como os diversos componentes se conectam, formando a arquitetura acima, vamos usar como exemplo o módulo core e a funcionalidade de troca de senha (Change Password). A figura abaixo mostra os módulos do Marvin no projeto Eclipse e as subdivisões do módulo core em pacotes (note que há um pacote não mencionado acima, exceptions, vamos explicá-lo mais adiante):

Classes de domínio

Geralmente começamos a implementação pelas classes que representam o domínio do problema, ou seja, o pacote domain. Na funcionalidade de troca de senha, a classe de domínio envolvida é a classe Academic 🔗, que representa um usuário do sistema (Nota: nos exemplos de código abaixo não incluiremos os imports e removeremos também partes não relevantes do código, indicado por /* ... */ ao final da classe):

package br.ufes.informatica.marvin.core.domain;

@Entity
public class Academic extends Person {
	/** Electronic address, which also serves as username for identification. */
	@Basic
	@NotNull
	@Size(max = 100)
	private String email;

	/** The password, which identifies the user. */
	@Basic
	@Size(max = 32)
	private String password;

	/** The code that has to be provided in order to reset an academic's password. */
	@Basic
	@Size(max = 40)
	private String passwordCode;

    /* ... */
}

Conforme a arquitetura mostrada anteriormente, as classes do pacote domain são Entities (vide a anotação @Entity na classe Academic), ou seja, classes que possuem mapeamento objeto/relacional para que o JPA possa efetuar sua persistência. Usuários (acadêmicos) no Marvin se identificam por e-mail (email) e senha (password) e utilizam o atributo passwordCode na funcionalidade de troca de senha. É importante ressaltar que Academic herda de Person, que herda de PersistentObjectSupport do JButler, sobre o qual você já deve ter aprendido quando fez o exercício JButler incluído neste manual.

Classes de persistência

Para cada classe de domínio que precisarmos efetuar operações de persistência em nossa lógica de negócios, criaremos um DAO, ou Data Access Object. No Marvin, DAOs são Stateless Enterprise Java Beans (EJB) compostos por uma interface local (acessível apenas pelo próprio código do Marvin) e uma implementação. Seguindo, então, para o pacote persistence do módulo core, temos a interface AcademicDAO 🔗:

package br.ufes.informatica.marvin.core.persistence;

@Local
public interface AcademicDAO extends BaseDAO<Academic> {
	Academic retrieveByEmail(String email) throws PersistentObjectNotFoundException, MultiplePersistentObjectsFoundException;

	Academic retrieveByPasswordCode(String passwordCode) throws PersistentObjectNotFoundException, MultiplePersistentObjectsFoundException;

    /* ... */
}

E temos também a classe AcademicJPADAO 🔗:

package br.ufes.informatica.marvin.core.persistence;

@Stateless
public class AcademicJPADAO extends BaseJPADAO<Academic> implements AcademicDAO {
	/** The application's persistent context provided by the application server. */
	@PersistenceContext
	private EntityManager entityManager;

	@Override
	protected EntityManager getEntityManager() {
		return entityManager;
	}

	@Override
	protected List<Order> getOrderList(CriteriaBuilder cb, Root<Academic> root) {
		// Orders by name.
		List<Order> orderList = new ArrayList<Order>();
		orderList.add(cb.asc(root.get(Person_.name)));
		return orderList;
	}

	@Override
	public Academic retrieveByEmail(String email) throws PersistentObjectNotFoundException, MultiplePersistentObjectsFoundException {
		logger.log(Level.FINE, "Retrieving the academic whose e-mail is \"{0}\"...", email);

		// Constructs the query over the Academic class.
		CriteriaBuilder cb = entityManager.getCriteriaBuilder();
		CriteriaQuery<Academic> cq = cb.createQuery(Academic.class);
		Root<Academic> root = cq.from(Academic.class);

		// Filters the query with the email.
		cq.where(cb.equal(root.get(Academic_.email), email));
		Academic result = executeSingleResultQuery(cq, email);
		logger.log(Level.INFO, "Retrieve academic by the email \"{0}\" returned \"{1}\"", new Object[] { email, result });
		return result;
    }
    
	@Override
	public Academic retrieveByPasswordCode(String passwordCode) throws PersistentObjectNotFoundException, MultiplePersistentObjectsFoundException {
		logger.log(Level.FINE, "Retrieving the academic whose password code is \"{0}\"...", passwordCode);

		// Constructs the query over the Academic class.
		CriteriaBuilder cb = entityManager.getCriteriaBuilder();
		CriteriaQuery<Academic> cq = cb.createQuery(Academic.class);
		Root<Academic> root = cq.from(Academic.class);

		// Filters the query with the email.
		cq.where(cb.equal(root.get(Academic_.passwordCode), passwordCode));
		Academic result = executeSingleResultQuery(cq, passwordCode);
		logger.log(Level.INFO, "Retrieve academic by the password code \"{0}\" returned \"{1}\"", new Object[] { passwordCode, result });
		return result;
	}
}

Como parte do exercício JButler, você também já deve ter aprendido sobre a superinterface BaseDAO e a superclasse BaseJPADAO, que trazem uma série de funcionalidades básicas aos DAOs. Os métodos getEntityManager() e getOrderList() tem a ver com essa herança do JButler. Já o método retrieveByEmail() existe para que seja implementado o login: dado um e-mail, retorna o acadêmico correspondente (ou lança PersistentObjectNotFoundException se não há cadastro com o e-mail especificado). Finalmente, retrieveByPasswordCode() será usado pela funcionalidade de troca de senha, como será explicado a seguir.

Páginas Web

Implementada a classe de domínio e o seu DAO, vamos para o outro extremo da arquitetura e apresentar as páginas Web que permitem que o usuário acesse a funcionalidade de troca de senha (pacote View). Um link para esta funcionalidade encontra-se na página de login. Em Deployed Resources, abra webapp/login.xhtml 🔗: e veja em uma de suas últimas linhas o link:

<p:link outcome="/core/changePassword/index.xhtml" value="#{msgsCore['login.text.forgot']}" /><br/>

Se você ainda se lembre da parte 3 do tutorial do JButler, sabe que #{msgsCore['login.text.forgot']} é uma referência a um resource bundle cadastrado no faces-config.xml 🔗 para internacionalização da aplicação. Se abrir no pacote br.ufes.informatica.marvin.core.view o arquivo messages.properties 🔗 verá que login.text.forgot = I forgot my password. Ao clicar neste link, o resultado (outcome) é a página /core/changePassword/index.xhtml 🔗:

<?xml version="1.0" encoding="UTF-8"?>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
	xmlns:ui="http://java.sun.com/jsf/facelets"
	xmlns:f="http://java.sun.com/jsf/core"
	xmlns:h="http://java.sun.com/jsf/html"
	xmlns:p="http://primefaces.org/ui"
	xmlns:pt="http://xmlns.jcp.org/jsf/passthrough"
	xmlns:adm="http://github.com/adminfaces"
	template="/WEB-INF/templates/template.xhtml">

	<f:metadata>
	    <f:viewParam name="code" value="#{changePasswordController.passwordCode}" />
	    <f:viewAction action="#{changePasswordController.checkCode}" />
	</f:metadata>	

	<ui:define name="title">
		<h:outputText value="#{msgsCore['changePassword.title']}" />
	</ui:define>
    
    <ui:define name="description">
    	<h:outputText value="#{msgsCore['changePassword.title.description']}" />
    </ui:define>

  	<ui:define name="body">
		<!-- Shown if the password code is invalid. -->
		<h:panelGroup id="invalidCodePanel" rendered="#{not changePasswordController.validCode}">
			<p><h:outputText value="#{msgsCore['changePassword.text.instructions.reset']}" /></p>
		
			<!-- The form to be filled with the e-mail in order to reset the password. -->
			<ui:decorate template="/WEB-INF/templates/form.xhtml">
				<ui:param name="formName" value="resetForm" /> 
				<ui:param name="formLabel" value="#{msgsCore['changePassword.form.reset']}" />
				<p:focus />

				<ui:decorate template="/WEB-INF/templates/formfield.xhtml">
					<ui:param name="fieldName" value="email" />
					<ui:param name="label" value="#{msgsCore['changePassword.field.email']}" />
					<p:inputText id="email" value="#{changePasswordController.email}" required="true">
						<f:ajax event="blur" render="emailField" />
					</p:inputText>
				</ui:decorate>

				<ui:decorate template="/WEB-INF/templates/formbuttonpanel.xhtml">
					<p:button id="cancelButton" value="#{msgsCore['changePassword.button.cancel']}" outcome="/login.xhtml" icon="fa fa-fw fa-times-circle" />
					<p:commandButton id="resetButton" value="#{msgsCore['changePassword.button.reset']}" action="#{changePasswordController.resetPassword}" update=":messages :info-messages resetForm" icon="fa fa-fw fa-repeat" />
				</ui:decorate>
			</ui:decorate>			
		</h:panelGroup>

		<!-- Shown if the password code is valid. -->
		<h:panelGroup id="validCodePanel" rendered="#{changePasswordController.validCode}">
			<p><h:outputFormat value="#{msgsCore['changePassword.text.instructions.new']}"><f:param value="#{changePasswordController.academic.shortName}" /></h:outputFormat></p>

			<!-- The form to be filled with the new password to set. -->
			<ui:decorate template="/WEB-INF/templates/form.xhtml">
				<ui:param name="formName" value="form" /> 
				<ui:param name="formLabel" value="#{msgsCore['changePassword.form.new']} #{changePasswordController.academic.name}" />
				<p:focus />

				<ui:decorate template="/WEB-INF/templates/formfield.xhtml">
					<ui:param name="fieldName" value="password" />
					<ui:param name="label" value="#{msgsCore['changePassword.field.password']}" />
					<p:password id="password" minLength="6" value="#{changePasswordController.password}" required="true" feedback="true" promptLabel="#{msgs['primefaces.password.prompt']}" weakLabel="#{msgs['primefaces.password.weak']}" goodLabel="#{msgs['primefaces.password.good']}" strongLabel="#{msgs['primefaces.password.strong']}" />
				</ui:decorate>

				<ui:decorate template="/WEB-INF/templates/formfield.xhtml">
					<ui:param name="fieldName" value="repeatPassword" />
					<ui:param name="label" value="#{msgsCore['changePassword.field.repeatPassword']}" />
					<p:password id="repeatPassword" value="#{changePasswordController.repeatPassword}" required="true" feedback="false">
						<f:ajax event="blur" render=":messages" listener="#{changePasswordController.ajaxCheckPasswords}" execute="password repeatPassword" />
					</p:password>
				</ui:decorate>

				<ui:decorate template="/WEB-INF/templates/formbuttonpanel.xhtml">
					<p:button id="cancelButton" value="#{msgsCore['changePassword.button.cancel']}" icon="fa fa-fw fa-times-circle" outcome="/login" />
					<p:commandButton id="newPasswordButton" action="#{changePasswordController.setNewPassword}" value="#{msgsCore['changePassword.button.new']}" update=":messages :info-messages :validCodePanel :invalidCodePanel" icon="fa fa-fw fa-save" />
				</ui:decorate>
			</ui:decorate>
		</h:panelGroup>
	</ui:define>
</ui:composition>

A página é uma composição Facelets, nos mesmos moldes do que é feito na parte 2 do tutorial JButler, mas não muito bem explicado: ela parte do template /WEB-INF/templates/template.xhtml 🔗, que por sua vez é baseado no AdminFaces. Ela completa, por meio da tag <ui:define />, as três lacunas deixadas pelo template: title (título da página), description (frase que descreve aquela página/funcionalidade e aparece logo abaixo do título) e body (corpo da página). Além disso, esta página possui a tag <f:metadata /> que serve para receber um parâmetro, como veremos a seguir.

No corpo da página, podemos ver que a mesma página serve em dois momentos diferentes analisando as tags <h:panelGroup />, mais especificamente quais são as condições para que esses paineis sejam renderizados (parâmetro rendered). O primeiro painel é renderizado quando não há um código válido (not changePasswordController.validCode) enquanto o segundo é o contrário (changePasswordController.validCode). Tais expressões se referem ao controlador associado a esta funcionalidade, que apresentaremos mais abaixo. Por enquanto basta saber que quando chegamos à página a partir do link na página de login, ainda não há código e, portanto, validCode é falso e o primeiro painel é mostrado.

Neste painel, há um formulário que é construído com base em outros templates do Facelets: /WEB-INF/templates/form.xhtml 🔗, /WEB-INF/templates/formfield.xhtml 🔗 e /WEB-INF/templates/formbuttonpanel.xhtml 🔗. Isso serve para padronizar o visual dos formulários em todo o sistema, assim a página se concentra em quais campos existem e não em como eles devem ser apresentados. O formulário contém um único campo para que o visitante digite seu e-mail, além de um botão para cancelar a troca de senha (volta para o login) e um para continuar a troca de senha.

Classes de controle

Neste momento, precisamos apresentar o controlador associado à funcionalidade de troca de senha, a classe ChangePasswordController 🔗, no pacote br.ufes.informatica.marvin.core.controller, para depois falar da relação entre View e Controller:

package br.ufes.informatica.marvin.core.controller;

@Named
@ConversationScoped
public class ChangePasswordController extends JSFController {
	/** The "Change Password" service. */
	@EJB
	private ChangePasswordService changePasswordService;

	/** Input: the e-mail supplied by the user to initiate the password changing process. */
	private String email;

	/** Getter for email. */
	public String getEmail() {
		return email;
	}

	/** Setter for email. */
	public void setEmail(String email) {
		this.email = email;
	}

	/**
	 * Controller method that starts the password reset process by asking the service class to send the e-mail with the
	 * code to the given address.
	 */
	public void resetPassword() {
		// Request the password reset and display a message.
		changePasswordService.resetPassword(email);
		addGlobalI18nMessage("msgsCore", FacesMessage.SEVERITY_INFO, "changePassword.message.resetRequested.summary", new Object[] {}, "changePassword.message.resetRequested.detail", new Object[] { email });
	}
    
    /* ... */
}

No formulário, o campo do e-mail possui como value a expressão #{changePasswordController.email}, o que faz com que o JSF chame o método setEmail() no controlador e armazene o valor digitado no formulário no atributo email do objeto ChangePasswordController. Analogamente, o botão resetButton possui como action a expressão #{changePasswordController.resetPassword}, o que significa que o método resetPassword() do objeto controlador será chamado quando o visitante clicar no botão. Assim, portanto, se dá a relação entre View (a página XHTML) e Controller (a instância da classe controladora).

O controlador então chama serviços das classes de aplicação, estabelecendo a relação unidirecional entre Controller e Application. No exemplo em questão, uma instância da classe de aplicação é injetada como atributo do controlador (changePasswordService), que chama o método resetPassword() passando o e-mail que chegou do formulário. O resultado é exibido de volta na página XHTML por meio de uma mensagem de informação (addGlobalI18nMessage() é um método oferecido pelo JButler), mostrando que a relação entre View e Controller é bidirecional.

Classes de aplicação

Finalizando a arquitetura, chegamos ao seu ponto central, o pacote Application e sua relação com Domain e Persistence. Em br.ufes.informatica.marvin.core.application temos a classe de aplicação responsável pelo serviço de troca de senha implementada da mesma forma que os DAOs no pacote Persistence, ou seja, um Stateless EJB composto por uma interface local e uma implementação. Apresentamos primeiro a interface ChangePasswordService 🔗:

package br.ufes.informatica.marvin.core.application;

@Local
public interface ChangePasswordService extends Serializable {
	Academic retrieveAcademicByPasswordCode(String passwordCode) throws InvalidPasswordCodeException;

	void setNewPassword(String passwordCode, String password) throws InvalidPasswordCodeException, OperationFailedException;

	void resetPassword(String email);
}

E temos também a classe ChangePasswordServiceBean 🔗, da qual mostraremos apenas a implementação do método resetPassword():

package br.ufes.informatica.marvin.core.application;

@Stateless
public class ChangePasswordServiceBean implements ChangePasswordService {
	@EJB
	private AcademicDAO academicDAO;

	@EJB
	private CoreInformation coreInformation;

	@Inject
	private Event<MailEvent> mailEvent;

	@Override
	public void resetPassword(String email) {
		try {
			// Retrieves the academic that owns the email and sets a new password code.
			Academic academic = academicDAO.retrieveByEmail(email);
			academic.setPasswordCode(UUID.randomUUID().toString());

			// Creates the data model with the information needed to send an e-mail with the reset code.
			Map<String, Object> dataModel = new HashMap<>();
			dataModel.put("config", coreInformation.getCurrentConfig());
			dataModel.put("academic", academic);

			// Then, fire an e-mail event so the e-mail gets sent.
			mailEvent.fire(new MailEvent(email, MailerTemplate.RESET_PASSWORD, dataModel));
		}
		catch (PersistentObjectNotFoundException | MultiplePersistentObjectsFoundException e) {
			logger.log(Level.WARNING, "No academic with the given e-mail: {0}. Staying silent, since this is a reset password feature.", new Object[] { email });
		}
	}
    
    /* ... */
}

A implementação de resetPassword() ilustra as relações unidirecionais que o pacote Application tem com Domain (um objeto da classe de domínio Academic é manipulado) e com Persistence (o DAO relativo a esta classe de domínio é usado para recuperar o objeto que é manipulado). Além de atribuir um código aleatório (UUID.randomUUID()) ao acadêmico cujo e-mail foi informado pelo usuário, a classe de aplicação dispara um evento que faz uma outra classe 🔗 do mesmo pacote enviar um e-mail com base em um template para o e-mail informado, incluindo o código gerado, o que permite ao usuário do sistema redefinir sua senha. Qualquer exceção, neste caso, é ignorada pois não queremos informar ao visitante se o e-mail inserido está ou não nas bases de dados do Marvin, visto que ele pode estar sondando se uma terceira pessoa está cadastrada ou não.

Falando em exceções, na arquitetura do Marvin existe um pacote específico para exceções que representam cursos alternativos nos cenários/casos de uso do sistema. O método resetPassword() não utiliza nenhuma exceção deste tipo (PersistentObjectNotFoundException e MultiplePersistentObjectsFoundException são exceções ligadas ao pacote Persistence e vem do JButler), mas os outros dois métodos desse EJB de aplicação fazem referência a InvalidPasswordCodeException 🔗, que representa o cenário em que alguém tenta prosseguir com o processo de troca de seha com um código inválido.

Resumo

Em resumo, temos as seguintes relações entre as camadas da arquitetura do Marvin:

  • View ↔️ Controller: páginas Web XHTML enviam dados de formulários que são capturados por classes controladoras por meio de métodos setNomeDoCampo() e geralmente armazenados em atributos, bem como acionam métodos das classes controladoras. Estas enviam dados de volta por meio de seus atributos e métodos getNomeDoCampo() referenciados nas páginas XHTML que são exibidas como resultado (usando AJAX ou não) ou por meio de exibição de mensagens de erro/informação (como no exemplo acima). As referências das páginas Web a elementos da classe controladora são feitas por meio de Expression Language do JSF;

  • Controller ➡️ (Application & Exception): a classe controladora, de posse dos dados e comandos que vieram da página Web, chamam serviços de EJBs do tipo stateless, possivelmente recebendo de volta algum valor de retorno ou uma exceção que represente um curso alternativo do cenário/caso de uso relativo à funcionalidade que está sendo executada. Com base nisso o controlador deve dar o retorno para a página Web. A classe de aplicação é injetada na classe controladora pelo CDI;

  • Application ➡️ (Domain & Persistence): para realizar um cenário/caso de uso do sistema, a classe de aplicação instancia e manipula objetos de domínio, muitas vezes recuperando-os do banco de dados via DAOs (que também são stateless EJBs e são injetados pelo CDI). Os DAOs se comunicam com o banco de dados via JPA e as classes de domínio usam anotações JPA para definir o mapeamento objeto/relacional.

Exercício

Os passos seguintes do procedimento de troca de senha são descritos brevemente abaixo, com links para as classes e outros recursos referenciados. Sugerimos como exercício ao leitor, tentar localizar os elementos mencionados na descrição abaixo no código do Marvin em sua máquina local:

  1. Partindo do que já foi descrito acima, o visitante deverá receber um e-mail baseado em um template 🔗, que contém uma URL que ele deverá clicar para prosseguir: {{ config.baseURL }}/servlet/changePassword/{{ academic.passwordCode }}. O Marvin substitui {{ config.baseURL }} pela URL do sistema (e.g., http://localhost:8080/marvin/) e {{ academic.passwordCode }} pelo UUID gerado pela classe de aplicação ChangePasswordServiceBean 🔗 e armazenado no objeto Academic 🔗;

  2. A classe ChangePasswordServlet 🔗 registra o mapeamento do padrão /servlet/changePassword/* para si própria, tratando a requisição que ocorre quando o visitante clica no link do e-mail recebido. Quando recebe a requisição, o UUID passado pela URL é extraído e o Servlet transfere o controle para o JSF redirecionando o visitante para /core/changePassword/index.xhtml?code=..., substituindo ... pelo UUID recebido;

  3. A página /core/changePassword/index.xhtml 🔗, já apresentada anteriormente, irá usar agora as tags que se encontram dentro de <f:metadata>: a tag <f:viewParam /> vai atribuir ao atributo passwordCode do controlador ChangePasswordController 🔗 (via setPasswordCode()) o valor do parâmetro ?code=... da URL para a qual o visitante foi direcionado, em seguida a tag <f:viewAction /> vai chamar o método checkCode() no controlador pra ver se o código é válido;

  4. O método checkCode() chama o método retrieveAcademicByPasswordCode() na classe de aplicação ChangePasswordServiceBean 🔗. Por sua vez, este chama o método retrieveByPasswordCode() no DAO AcademicJPADAO 🔗. Caso tudo dê certo, o objeto Academic 🔗 é retornado ao controlador. Em caso de problemas, o controlador receberá uma exceção do tipo InvalidPasswordCodeException 🔗. Note que tanto a chamada da classe de aplicação quanto a do DAO se dão por meio de referências às suas interfaces, mas que apontam para instâncias das implementações;

  5. De volta a checkCode() no controlador ChangePasswordController 🔗, no caso de tudo dar certo, ele armazena o código (agora validado) no atributo validatedPasswordCode, inicia uma conversação CDI e validCode fica com o valor true. Caso ele receba a exceção, ele apenas atribui false ao atributo validCode;

  6. Voltando, então, à página /core/changePassword/index.xhtml 🔗, assumindo que o código era válido, agora ela vai exibir o segundo <h:panelGroup />, pois changePasswordController.validCode é true. Este formulário oferece um campo para que se digite a senha (password), outro para que se repita a senha (repeatPassword), um botão para cancelar e outro para prosseguir. Este último possui como ação a expressão #{changePasswordController.setNewPassword}, que chama o método setNewPassword() no controlador ;

  7. Em ChangePasswordController 🔗, o método setNewPassword() verifica se ambos os campo senha são iguais (via método checkPasswords(), que adiciona uma mensagem de aviso caso não sejam), retornando nulo em caso de problemas, o que faz o JSF reexibir a mesma página de antes (só que agora com o aviso). Se estiver tudo certo, o controlador chama novamente a classe de aplicação ChangePasswordServiceBean 🔗, desta vez o método setNewPassword(), passando os atributos validatedPasswordCode (vide passo 5) e password (senha recebida do formulário da página Web);

  8. Em ChangePasswordServiceBean 🔗, o método setNewPassword() vai novamente usar o DAO AcademicJPADAO 🔗, recuperando o objeto Academic 🔗 com retrieveByPasswordCode() novamente, atribuindo uma nova senha a este objeto, anulando o código de troca de senha neste mesmo objeto e salvando-o com o método save() do DAO;

  9. De volta ao método setNewPassword() do controlador ChangePasswordController 🔗, se tudo deu certo ele retorna a string "done.xhtml?faces-redirect=true", o que faz com que o JSF faça um redirecionamento (nova requisição) a /core/changePassword/done.xhtml. Esta página simplesmente diz que a troca de senha foi efetuada com sucesso (vide changePassword.text.done no messages.properties 🔗 do módulo core) e um botão leva o visitante de volta à página de login. Note também que uma tag <f:viewAction /> nesta página chama o método end() no controlador para terminar a conversação CDI.

Organização do projeto

O Marvin segue a organização padrão de um projeto gerenciado pelo Maven e que é apresentada de uma forma mais amigável pela view Project Explorer do Eclipse. Descrevemos abaixo os arquivos e pastas que compõem o projeto (sem a reorganização do Eclipse):

  • LICENSE 🔗: arquivo que descreve a licença adotada pelo projeto (GNU GPL 3.0);

  • pom.xml 🔗: arquivo de configuração do Maven;

  • README.md 🔗: conteúdo que aparece na página inicial do repositório do Marvin no GitHub;

  • src/main/java 🔗: pasta que contém as classes principais do projeto, por exemplo todas as que foram descritas acima como parte da função de troca de senha;

  • src/main/jpa 🔗: pasta que contém os metamodelos das classes de domínio para que possam ser feitas consultas typesafe em JPA com a API de Critérios (este artigo fala sobre este assunto, caso você não saiba do que se trata, vale a pena aprender antes de trabalhar no Marvin). Tais classes são geradas automaticamente pelo Eclipse, por isso inclusive às vezes só de trazer o Marvin pro seu computador pode gerar alterações no repositório que o git vai querer incluir no commit (mais sobre isso depois);

  • src/main/resources 🔗: pasta que contém arquivos que fazem parte da base de código (classpath) do Marvin, porém não são classes Java e, portanto, não ficam em src/main/java. Por exemplo, encontram-se ali a configuração do AdminFaces (admin-config.properties), dados a serem inseridos no sistema durante sua instalação (arquivos na pasta marvin/installSystem), templates das mensagens enviadas por e-mail pelo Marvin (na pasta marvin/mailer, separados por língua), frases ditas pelo Marvin na página inicial (arquivo marvin/quotes.txt) e a configuração do JPA (arquivo META-INF/persistence.xml);

  • src/main/webapp 🔗: pasta que contém as páginas XHTML e demais recursos Web (imagens, scripts, folhas de estilo, etc.). Na raiz, encontram-se as páginas inicial (index.xhtml) e de login (login.xhtml) e uma série de pastas. A pasta WEB-INF contém os arquivos de configuração e templates Facelets. A pasta resources serve para agrupar os recursos Web citados acima e pode ser organizada em bibliotecas de recursos (conforme explicado neste tutorial, também interessante conhecer). As demais pastas se referem aos diferentes módulos do Marvin (admin, core, research, ...) e contém as páginas de cada módulo organizadas por funcionalidade (caso de uso). Por exemplo, as páginas relativas à funcionalidade de troca de senha ficam em core/changePassword;

  • src/test/java 🔗: pasta que contém os testes automatizados, explicados em detalhes em outra seção deste manual;

  • target: pasta gerada pelo Maven, contém classes compiladas e outros arquivos referentes ao processo de construção da aplicação, feito automaticamente por ele. Portanto, este diretório não é enviado ao repositório.

Além destes arquivos e pastas, há alguns arquivos ocultos: o .gitignore do Git e os demais arquivos que começam com o caractere ., contendo configurações do projeto Eclipse.

Falando no Eclipse, concluimos esta parte do manual com a visão Project Explorer, que organiza a estrutura descrita acima de uma forma mais amigável, conforme mostra a figura abaixo:

As configurações do src/main/webapp/WEB-INF/web.xml podem ser vistas no item Deployment Descriptor: marvin; a configuração do JPA src/main/resources/META-INF/persistence.xml encontra-se abaixo de JPA Content; com exceção da pasta com as páginas/recursos Web, todas as pastas de código-fonte encontram-se abaixo de Java Resources; já a pasta src/main/webapp encontra-se abaixo de Deployed Resources.

➡️ Implementação: convenções de código

⬆️ Voltar ao índice