Archive for the ‘Seam’ Category.

Identity Management de verdade com novo Seam 2.1 Beta

Poxa vida, tava preparando um post sobre RecordProcessor, e sai a versão nova do Jboss Seam ontem, e com ela um novo Identity Manager, e desta vez com suporte a LDAP!!!!
Bem, desta vez vou dividir em mais partes o post, nesta primeira parte vou focar apenas na configuração do Apache Directory Server (LDAP) e na autenticação com seam.

Pré requisitos:

Configurando o LDAP

Bem, o primeiro passo é criar uma aplicação com Seam, usando o bom e velho seam-gen. Veja este post para maiores informações.
Instale o Apache Directory Studio usando o update-site para eclipse e marque as opções Studio e Directory Server (isso vai lhe permitir iniciar um servidor LDAP de dentro do eclipse)

Bem, com o plugin configurado, abra a perspectiva de Servers do Directory Studio e crie um novo servidor, assim como na figura abaixo:

Servidor de LDAP do Directory Studio (clique para fullsize)

Servidor de LDAP do Directory Studio (clique para fullsize)

Inicie o servidor, clique com o botão direito sobre o mesmo, e então LDAP Browser -> New Connection. Isto vai conecta-lo ao servidor de LDAP, na aba esquerda “LDAP Browser” você pode agora criar os usuários e roles.

Bem, vamos criar uma árvore onde abaixo de nosso domain (dc), existe uma Organizational Unit (ou) para users e outra para roles.

Clique com o botão da direita em dc=example,dc=com e depois new entry. Selecione uma nova entrada e na tela de object classes “Organization Unit” e Add…

Adicionando uma OU (clique para fullsize)

Adicionando uma OU (clique para fullsize)

Na próxima tela no RDN use ou e roles como na figura abaixo:

Definindo o OU (clique para fullsize)

Definindo o OU (clique para fullsize)

Bem, agora repita este passo e crie uma ou com nome de users.

Bem, agora é preciso criar um usuário, basta seguir as telas abaixo:

Criando um usuário (clique para fullsize)

Criando um usuário (clique para fullsize)

Você deve adicionar os objetos Person e extensibleObject em sua entrada:

Adicionando extensible Object (clique para fullsize)

Adicionando extensible Object (clique para fullsize)

Na próxima tela é importante que seu DN fique da seguinte maneira : uid=$USER,ou=users,dc=example,dc=com pois esta é a string de pesquisa usada pelo componente do Seam no LDAP, então adicione apenas uid no seu RDN como na figura abaixo:

Cuidado com RDN... (clique para fullsize)

Cuidado com RDN... (clique para fullsize)

Na próxima tela você terá que adicionar dois campos a mais que são obrigatórios (CN=Common Name e SN=Surname) adicione um nome e sobrenome. Além disto, você deve adicionar um atributo member que vai apontar para o role que seu usuário participa.

Para adicionar um atributo clique no ícone mostrado na figura abaixo:

Clique no ícone em destaque (segundo da esquerda pra direita)

Clique no ícone em destaque (segundo da esquerda pra direita)

Agora escolha member:

Adicionando um member em nossa entrada (clique para fullsize)

Adicionando um member em nossa entrada (clique para fullsize)

Além disto você vai precisar de um userpassword:

Adicionando atributo userPassword

Adicionando atributo userPassword

No final, sua entrada deve se paracer com a figura abaixo:

Resultado final da entrada de um usuário (clique para fullsize)

Resultado final da entrada de um usuário (clique para fullsize)

Ok, o usuário esta configurado (a propósito, criei o usuário bob com senha bob :P )

Para cada novo role de um usuário, basta adicionar um novo atributo member para o usuário em questão

Agora podemos partir para o seam :)

Configurando o Identity Management

Configura o identityManagement é muito simples. IdentityManagement é uma classe que delega a um IdentityStore (no nosso caso LDAP), então você precisa configura o ldapIdentityStore e um security-identity-manager

Agora basta adicionar o ldapIdentityStore e security-identity-manager:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<security:ldap-identity-store name="ldapIdentityStore"
   			server-address="localhost"
   			server-port="10389"
   			user-DN-prefix="uid="
   			user-DN-suffix=",ou=users,dc=example,dc=com"
   			user-context-DN="ou=users,dc=example,dc=com"
   			user-password-attribute="userPassword"
   			role-context-DN="ou=roles,dc=example,dc=com"
   			role-DN-prefix="cn="
   			role-DN-suffix=",ou=roles,dc=example,dc=com"
   			bind-DN="uid=admin,ou=system"
   			bind-credentials="secret"
   			user-role-attribute="member"
   			user-object-classes="person,uidObject">
   </security:ldap-identity-store>
 
   <security:identity-manager identity-store="#{ldapIdentityStore}"></security:identity-manager>

Pronto, você pode inclusive remover seu authenticator agora :) O Seam cuida desta nova forma de autenticação para você.

Dos descritores acima quero falar apenas de dois pontos (ta tarde pra caralho e quero dormir :|). Bem, o server-port não existe no XSD do seam, parece ser um bug pois o componente tem o atributo e você pode usar, eu já fui fominha e abri um bug mas podem usar sem problemas. Outro ponto é o atributo enabled que quando usado (nao foi nosso caso), deve ter o valor TRUE e não true, sabe por que? Abaixo um trecho do LDAPIdentityStore

431
if (LDAP_BOOLEAN_TRUE.equals(value)) return true;

O valor LDAP_BOOLEAN_TRUE é “TRUE” ou seja, se você usar “true” minúsculo vai levar graxa na cara.

Bom, agora, quando voce acessar sua aplicação e for na tela de login, ja vai estar usando o LDAP. Não precisou mexer em xhtml, nem em classe alguma. Não é uma beleza?

Pessoal, esse post eu fiz questão de varar a noite, primeiro porque só esta feature já é um motivo para vocês usarem Seam, segundo porque sou fominha e queria postar antes de qualquer um :D

O próximo post vou continuar daqui, vou mostrar como usar o IdentityStore para adicionar users via web no LDAP (sem você escrever uma linha de código), como funciona o novo mecanismo de permissions e como mesclar LDAP e banco de dados para armazenar permissions no banco de dados.

Inté

Monitorando seus serviços

Bem, foi uma semana complicada, por isso o hiato tão grande desde meu último post, uma nova proposta consumiu todo meu tempo na empresa que trabalho, mas pelo menos tive a oportunidade de testar um conceito que vou apresentar em um próximo post :)

Bem, todo mundo fala de SOA e junto com essa sigla dos infernos, vem mais um monte de outras siglas e aparentemente a preferida de todos é BAM (Business Activity Monitoring). Muitos dos clientes que visito, reclamam de a suite JBoss não tem um BAM integrado, como outras ferramentas. O grande problema de se usar BAM, é que o cliente deve primeiro conhecer o seu negócio antes mesmo de querer monitorar algo, e isso sabemos que não reflete a realidade na maioria das vezes.

Bem, não satisifeitos em não ter BAM, começam a reclamar do SAM (Service Activity Monitoring). Bem, monitoramento de serviços o JBoss WS tem, e em dois nívels: Endpoint management (coberto neste post) e Records management (próximo post).

Se você deseja monitorar algo no seu JBoss eu recomendo que adquira a versão com subscrição e com isso o JON, que apesar de ainda não possuir um monitoramento para webservices (funcionalidade prevista para setembro deste ano), é muito simples criar um plugin para monitorar os webservices do jboss através de JMX.

Bem, apresentei então o console de métricas do JBoss WS:

Métricas do JBoss WS (clique para fullsize)

Métricas do JBoss WS (clique para fullsize)

A reação do cliente foi de repúdio claro, afinal de contas o Oracle tem um console “bonitinho”, e então me apresentaram a seguinte tela:

Oracle SAM (clique para fullsize)

Oracle SAM (clique para fullsize)

PQP, É foda viu, escolher OC4J como broker de webservices por causa dessa telinha? So por que tem esse gráficozinho de velocímetro? Bem, cheguei em casa puto, e gastei cerca de duas horas para produzir a aplicação que vou apresentar abaixo. Peço singelas desculpas pela falta de conhecimento em JFreeChart, e principalmente pelos componentes de imagem do richfaces, mas acredito que o que vale é provar um conceito: É possível termos um console bacana de SAM no jboss, e bem mais barato que pagar uma licensa absurda por causa deste consolezinho de OC4J.

Bem, mãos na massa. A primeira classe é minha MeterImage, que gera o gráfico usando bibliotecas do JFreeChart:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package com.furiousbob.ws;
 
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.awt.image.WritableRaster;
import java.io.Serializable;
 
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.DialShape;
import org.jfree.chart.plot.MeterInterval;
import org.jfree.chart.plot.MeterPlot;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.plot.dial.ArcDialFrame;
import org.jfree.chart.plot.dial.DialBackground;
import org.jfree.chart.plot.dial.DialPlot;
import org.jfree.chart.plot.dial.DialPointer;
import org.jfree.chart.plot.dial.StandardDialScale;
import org.jfree.data.Range;
import org.jfree.data.general.ValueDataset;
 
public class MeterImage implements Serializable{
 
	private ValueDataset dataset;
	private JFreeChart chart;
	private String title;
 
 
 
	public void  createChart(){
		Plot plot = createPlot();
		chart = new JFreeChart(title,plot);
		chart.setBackgroundPaint(Color.WHITE);
	}
 
 
 
	private MeterPlot createPlot(){
		MeterPlot plot = new MeterPlot(dataset);
		plot.setBackgroundAlpha(0.0f);
        plot.setMeterAngle(180); 
        plot.setDialShape(DialShape.CHORD); 
        plot.setDialBackgroundPaint(Color.white);
        plot.setTickLabelsVisible(false);
        plot.setDialOutlinePaint(Color.black);
        plot.setOutlineStroke(new BasicStroke(6.0f));
        plot.setNeedlePaint(Color.black);
        plot.setDrawBorder(false);
 
        plot.addInterval(new MeterInterval("Good",new Range(0,55.0),new Color(0,0,0),null,Color.GREEN));
        plot.addInterval(new MeterInterval("Warn",new Range(55,80.0),new Color(0,0,0),null,Color.YELLOW));
        plot.addInterval(new MeterInterval("Bad",new Range(80,100.0),new Color(0,0,0),null,Color.RED));
        plot.setValuePaint(Color.black);
        plot.setUnits("%");
        return plot;
	}
 
	public BufferedImage getImage(){
		createChart();
		return chart.createBufferedImage(300, 300);
	}
 
	public ValueDataset getDataset() {
		return dataset;
	}
 
	public void setDataset(ValueDataset dataset) {
		this.dataset = dataset;
	}
 
	public String getTitle() {
		return title;
	}
 
	public void setTitle(String title) {
		this.title = title;
	}
}

Bem, preciso de um webservice para ser monitorado, então um SampleService quase HelloWorld:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.furiousbob.services;
 
import java.util.Random;
 
import javax.ejb.Stateless;
import javax.jws.WebMethod;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;
 
import org.jboss.wsf.spi.annotation.WebContext;
 
@Stateless
@WebService
@WebContext(contextRoot="/furiousbob/services",urlPattern="/SampleService")
@SOAPBinding(use = SOAPBinding.Use.LITERAL, style = SOAPBinding.Style.DOCUMENT)
public class SampleServiceBean implements SampleService {
 
	@WebMethod
	public String sayHello(String name) {
		int delay = 0;
		try {
			Random r = new Random();
			delay = r.nextInt(3000);
			Thread.sleep(delay);
		} catch (Exception e) {
			// TODO: handle exception
		}
		return "Hello " + name +"! I've waited " + delay + " ms before saying hello back!";
	}
 
}

Notem que o método realiza um sleep de até 3s de forma randomica, fiz isto, para gerar valores alternados para poder coletar dados na minha aplicação: bobsam

A primeira tela, bem simples, lista todos serviços disponibilizados no servidor:

Listando serviços disponíveis no servidor (clique para fullsize)

Listando serviços disponíveis no servidor (clique para fullsize)

<ui:define name="body">
 
    <h:messages globalOnly="true" styleClass="message"/>
 
    <rich:panel>
    <f:facet name="header">Welcome!</f:facet>
		<rich:dataTable value="#{sam.registredEndpoints}" var="_endpoint">
			<rich:column>
				<f:facet name="header">Service name</f:facet>
				<s:link value="#{_endpoint}" view="/sam.xhtml">
					<f:param name="endpoint" value="#{_endpoint}"/>
				</s:link>
 
			</rich:column>
 
		</rich:dataTable>   
    </rich:panel>
 
</ui:define>

Agora, como vamos buscar nossos endpoints? JMX, o JBoss WS expõe seus endpoints através de um objeto EndpointRegistry onde é possível ver todos serviços listados, a classe WebServiceManagement possui o método responsável por isso, note o trecho de código nas linhas 83-97.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package com.furiousbob.ws;
 
 
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
import javax.imageio.ImageIO;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.Query;
 
import org.jboss.mx.util.MBeanProxyExt;
import org.jboss.mx.util.MBeanServerLocator;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Out;
import org.jboss.seam.annotations.Scope;
import org.jboss.wsf.framework.management.ManagedEndpointMBean;
import org.jboss.wsf.framework.management.ManagedEndpointRegistry;
import org.jboss.wsf.framework.management.ManagedEndpointRegistryMBean;
import org.jboss.wsf.spi.deployment.Endpoint;
import org.jfree.data.general.DefaultValueDataset;
import org.jfree.data.general.ValueDataset;
 
@Name("sam")
@Scope(ScopeType.CONVERSATION)
public class WebServiceManagementAction implements Serializable {
 
	private MeterImage meterImage = new MeterImage();
 
	private ValueDataset dataset;
 
 
	private String endpoint;
 
	@Out(required=false,scope=ScopeType.CONVERSATION)
	@In(required=false)
	private ManagedEndpointMBean mep;
 
	public void paint(OutputStream out, Object data){
		updateDataset();
		meterImage.setDataset(dataset);
		Pattern p = Pattern.compile("jboss.ws:context=(.*),endpoint=(.*)");
		Matcher matcher = p.matcher(endpoint);
		matcher.find();
		meterImage.setTitle("Performance indicator for: " + matcher.group(2));
		try {
			ImageIO.write(meterImage.getImage(), "png", out);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
 
 
	private void updateDataset(){
		long average = mep.getAverageProcessingTime();
		average = (average*100)/3000;
		this.dataset = new DefaultValueDataset(average);
	}
 
 
 
 
	public String getEndpoint() {
		return endpoint;
	}
 
 
 
 
 
	public void setEndpoint(String endpoint) {
		this.endpoint = endpoint;
	}
 
 
	public List<String> getRegistredEndpoints(){
		List<String> endpoints = new LinkedList<String>();
		try {
			MBeanServer server = MBeanServerLocator.locate();
			ManagedEndpointRegistryMBean registry = (ManagedEndpointRegistryMBean)MBeanProxyExt.create(ManagedEndpointRegistryMBean.class, "jboss.ws:service=EndpointRegistry");
			Set<ObjectName> names = registry.getEndpoints();
			for(ObjectName o : names){
				endpoints.add(o.getCanonicalName());
			}
 
		} catch (Exception e) {
			e.printStackTrace();
		}
		return endpoints;
	}
 
 
	public void create(){
		try {
			MBeanServer server = MBeanServerLocator.locate();
			this.mep = (ManagedEndpointMBean)MBeanProxyExt.create(ManagedEndpointMBean.class, endpoint);
		} catch (Exception e) {
 
		}
	}
 
}

O método public List getRegistredEndpoints() retorna uma lista com os nomes de todos webservices registrados em nosso servidor. Agora, basta selecionarmos nosso serviço para apresentarmos o seu gráfico. A listagem abaixo mostra nossa página sam.xhtml

1
2
3
4
5
6
7
8
9
10
11
12
13
<ui:define name="body">
 
    <h:messages globalOnly="true" styleClass="message"/>
 
    <rich:panel>
    <f:facet name="header">Service Activity Monitoring for #{sam.endpoint}</f:facet>
	<a4j:mediaOutput element="img" cacheable="false" createContent="#{sam.paint}" value="#{meterImage}" mimeType="image/png" >
		<a4j:actionparam name="cid" value="#{conversation.id}"></a4j:actionparam>
	</a4j:mediaOutput>
 
    </rich:panel>
 
</ui:define>

Bem, pelo pouco que aprendi de suporte a imagens com richfaces é que preciso implementar o método paint. Este método é o responsável por invocar o ManagedEndpoint, buscar as métricas que desejo plotar (no caso estou verificando o tempo médio de processamento do serviço) e então gerar a imagem. O resultado final é a imagem abaixo:

Gráfico de monitoramento (clique para fullsize)

Gráfico de monitoramento (clique para fullsize)

Claro que se não gerarmos alguns acessos ao nosso serviço não será possível ver algum resultado. Para isso eu uso o soap-ui que dentre outras inúmeras funcionalidades , possui um assistente para gerar testes de carga, a figura abaixo mostra um teste de carga feito com o soap-ui:

Testes de carga com SOAP-ui (clique para fullsize)

Testes de carga com SOAP-ui (clique para fullsize)

Após 1 min, com 100 threads no modelo de variância (que randomiza o número de acessos concorrentes) o soap-ui gerou 4174 requisições no meu webservice (Alguém ainda duvida que JBoss WS é escalável????).

Ao acessar a página de monitoramento do meu webservice, temos agora uma figura que representa o estado de nosso serviço:

Gráfico de monitoramento (clique para fullsize)

Gráfico de monitoramento (clique para fullsize)

Bem, agora não tem mais motivo para ficar achando que não dá pra ter SAM com JBoss não é? Eu (que sou um cara TOSCO assim como meu amigo e mentor Edgar Silva) consegui criar em 2 horas, isso porque apanhei do JFreeChart :P, uma aplicação simples para SAM.

Bem, é isso ai pessoal, Inté a próxima.

Eu sei o que vocês fizeram no verão passado

Não se assustem! Não se trata de mais um filme de quinta categoria com adolescentes gostosas e sem nenhum sentido do cinema americano.
Vim aqui hoje falar do Envers. O Envers é um dos mais novos frameworks da JBoss a alcançar a maturidade GA (General Availability), a grande sacada do envers é gerar um histórico de todas modificações que ocorrem para uma determinada entidade. Quantas vezes você já não esbarrou em um requisito de projeto onde o cliente solicitava a capacidade de restaurar uma versão do objeto modificado há 3 meses atrás com os valores que este tinha na época? Ou então por motivo de alguma auditoria interna, você precisou guardar esta modificações em uma tabela “histórico”. Bem, eu já passei pelas duas situações, e tenho certeza que muitos de vocês já tiveram o mesmo problema.
Para resolver este problema chegou o Envers. O Envers funciona como um interceptor do Hibernate que atua em toda modificação que é realizada para uma classe anotada para ser versionada. Quando sua Session modifica a classe o envers gera uma entrada em uma tabela de histórico. A configuração é extremamente simples. E o seu uso mais ainda, mas achei que valia a pena fazer um pequeno post sobre ele.
Como todos exemplos do meu blog, vamos iniciar criando um novo projeto seam (consulte posts anteriores e a documentação do Seam para isso)
Uma vez que seu projeto esteja criado. Defina uma nova entidade:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.furiousbob.com;
 
import java.io.Serializable;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
 
import org.jboss.envers.Versioned;
 
@Entity
@Table(name="NEWS")
@Versioned
public class News implements Serializable {
	@Id
	@Column(name="ID")
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	private long id;
 
 
	@Column(name="TITLE")
	private String title;
 
	@Column(name="CONTENTS")
	private String contents;
 
	public long getId() {
		return id;
	}
 
	public String getTitle() {
		return title;
	}
 
	public String getContents() {
		return contents;
	}
 
	public void setId(long id) {
		this.id = id;
	}
 
	public void setTitle(String title) {
		this.title = title;
	}
 
	public void setContents(String contents) {
		this.contents = contents;
	}
}

Note na linha 16 a presença de uma anotação que define que nossa classe é agora versionada. O Envers suporta versionamento de classes completas ou de atributos.

Vá no console do seam, e execute ./seam generate-ui. Isso vai gerar todo o código e interfaces html para nosso CRUD.

Vamos então criar uma nova entidade. Ao clicar em create news, o sistema vai solicitar que você se autentique (admin:admin). Vamos então entrar com algum texto para nossa entidade, conforme a figura abaixo:

Criando uma entidade (clique para fullsize)

Criando uma entidade (clique para fullsize)

Clique em “Done” e você será redirecionado para a tela que apresenta a entidade. Uma das colunas possui uma ação para selecionar a entidade para edição “Select”, clique nesta ação e mude por exemplo o valor da propriedade contents conforme a figura abaixo:

Conteudo alterado de forma indevida (clique para fullsize)

Conteudo alterado de forma indevida (clique para fullsize)

Oops, alguem modificou o conteúdo! Como podemos restaurar a ordem no sistema? Se você estiver usando o Envers a tarefa é simples :). Baixe o envers, adicione o jar ao seu lib. Edite o arquivo deployed-jars-ear.list e adicione a lib do envers. Edite seu persistence.xml de forma que fique parecido com o abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="UTF-8"?>
<!-- Persistence deployment descriptor for dev profile -->
<persistence xmlns="http://java.sun.com/xml/ns/persistence" 
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" 
             version="1.0">
 
   <persistence-unit name="envers">
      <provider>org.hibernate.ejb.HibernatePersistence</provider>
      <jta-data-source>java:/enversDatasource</jta-data-source>
      <properties>
         <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
         <property name="hibernate.hbm2ddl.auto" value="update"/>
         <property name="hibernate.show_sql" value="true"/>
         <property name="hibernate.format_sql" value="true"/>
         <property name="jboss.entity.manager.factory.jndi.name" value="java:/enversEntityManagerFactory"/>
           <property name="hibernate.ejb.event.post-insert"   
             value="org.jboss.envers.event.VersionsEventListener" />  
          <property name="hibernate.ejb.event.post-update"   
             value="org.jboss.envers.event.VersionsEventListener" />  
          <property name="hibernate.ejb.event.post-delete"   
             value="org.jboss.envers.event.VersionsEventListener" />  
 
      </properties>
   </persistence-unit>
 
</persistence>

As linhas 17-22 são as responsáveis por ativar o envers. Se você checar seu SGDB agora, pode notar 2 tabelas novas: NEWS_versions e _revisions_info. Estas são as tabelas criadas automaticamente pelo Envers para você. A tabela _versions, guarda a informação da entidade versionada, é gerada uma para cada entidade anotada, a outra tabela é de controle interno do Envers.

Bem, vamos modificar nosso arquivo NewsList.xhtml (gerado pelo Seam) de forma que agora possamos verificar as versões existentes para cada uma de nossas entidades. A lista abaixo apresenta a nova coluna que inserimos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
                             "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
 
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:s="http://jboss.com/products/seam/taglib"
                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:rich="http://richfaces.org/rich"
                template="layout/template.xhtml">
 
<ui:define name="body">
 
    <h:messages globalOnly="true" styleClass="message" id="globalMessages"/>
 
    <h:form id="newsSearch" styleClass="edit">
 
        <rich:simpleTogglePanel label="News search parameters" switchType="ajax">
 
            <s:decorate template="layout/display.xhtml">
                <ui:define name="label">contents</ui:define>
                <h:inputText id="contents" value="#{newsList.news.contents}"/>
            </s:decorate>
 
            <s:decorate template="layout/display.xhtml">
                <ui:define name="label">title</ui:define>
                <h:inputText id="title" value="#{newsList.news.title}"/>
            </s:decorate>
 
 
        </rich:simpleTogglePanel>
 
        <div class="actionButtons">
            <h:commandButton id="search" value="Search" action="/NewsList.xhtml"/>
        </div>
 
    </h:form>
 
    <rich:panel>
        <f:facet name="header">News search results</f:facet>
    <div class="results" id="newsList">
 
    <h:outputText value="The news search returned no results." 
               rendered="#{empty newsList.resultList}"/>
 
    <rich:dataTable id="newsList" 
                var="news"
              value="#{newsList.resultList}" 
           rendered="#{not empty newsList.resultList}">
        <h:column>
            <f:facet name="header">
                <s:link styleClass="columnHeader"
                             value="id #{newsList.order=='id asc' ? messages.down : ( newsList.order=='id desc' ? messages.up : '' )}">
                    <f:param name="order" value="#{newsList.order=='id asc' ? 'id desc' : 'id asc'}"/>
                </s:link>
            </f:facet>
            #{news.id}
        </h:column>
        <h:column>
            <f:facet name="header">
                <s:link styleClass="columnHeader"
                             value="contents #{newsList.order=='contents asc' ? messages.down : ( newsList.order=='contents desc' ? messages.up : '' )}">
                    <f:param name="order" value="#{newsList.order=='contents asc' ? 'contents desc' : 'contents asc'}"/>
                </s:link>
            </f:facet>
            #{news.contents}
        </h:column>
        <h:column>
            <f:facet name="header">
                <s:link styleClass="columnHeader"
                             value="title #{newsList.order=='title asc' ? messages.down : ( newsList.order=='title desc' ? messages.up : '' )}">
                    <f:param name="order" value="#{newsList.order=='title asc' ? 'title desc' : 'title asc'}"/>
                </s:link>
            </f:facet>
            #{news.title}
        </h:column>
        <h:column>
            <f:facet name="header">action</f:facet>
            <s:link view="/#{empty from ? 'News' : from}.xhtml" 
                   value="Select" 
                      id="news">
                <f:param name="newsId" 
                        value="#{news.id}"/>
            </s:link>
        </h:column>
        <h:column>
        	<f:facet name="header">Revisions</f:facet>
        	<s:link view="/newsRevision.xhtml" value="check revisions">
        		<f:param name="id" value="#{news.id}"/>
        	</s:link>
        </h:column>
    </rich:dataTable>
 
    </div>
    </rich:panel>
 
    <div class="tableControl">
 
        <s:link view="/NewsList.xhtml" 
            rendered="#{newsList.previousExists}" 
               value="#{messages.left}#{messages.left} First Page"
                  id="firstPage">
          <f:param name="firstResult" value="0"/>
        </s:link>
 
        <s:link view="/NewsList.xhtml" 
            rendered="#{newsList.previousExists}" 
               value="#{messages.left} Previous Page"
                  id="previousPage">
            <f:param name="firstResult" 
                    value="#{newsList.previousFirstResult}"/>
        </s:link>
 
        <s:link view="/NewsList.xhtml" 
            rendered="#{newsList.nextExists}" 
               value="Next Page #{messages.right}"
                  id="nextPage">
            <f:param name="firstResult" 
                    value="#{newsList.nextFirstResult}"/>
        </s:link>
 
        <s:link view="/NewsList.xhtml" 
            rendered="#{newsList.nextExists}" 
               value="Last Page #{messages.right}#{messages.right}"
                  id="lastPage">
            <f:param name="firstResult" 
                    value="#{newsList.lastFirstResult}"/>
        </s:link>
 
    </div>
 
    <s:div styleClass="actionButtons" rendered="#{empty from}">
        <s:button view="/NewsEdit.xhtml"
                    id="create" 
                 value="Create news">
            <f:param name="newsId"/>
        </s:button>
    </s:div>
 
</ui:define>
 
</ui:composition>

O que nos interessa aqui são as linhas 87-92, que adicionam uma nova coluna a tabela de listagem de entidades. Após ter feito este passo, sua tela deverá parecer com a tela abaixo:

Nova listagem de entidades (clique para fullsize)

Nova listagem de entidades (clique para fullsize)

Bem, precisamos agora criar a página que irá apresentar as versões de nosso objeto, a página newsRevision.xhtml é apresentada abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
                             "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:s="http://jboss.com/products/seam/taglib"
                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:rich="http://richfaces.org/rich"
                template="layout/template.xhtml">
 
<ui:define name="body">
 
    <h:messages globalOnly="true" styleClass="message" id="globalMessages"/>
 
    <rich:panel>
        <f:facet name="header">Revision history for news id #{newsRevision.newsId}</f:facet>
			<rich:dataTable value="#{revisions}" var="_revision">
				<rich:column>
					<f:facet name="header">Title</f:facet>
					#{_revision[0].title}
				</rich:column>	
				<rich:column>
					<f:facet name="header">Contents</f:facet>
					#{_revision[0].contents}
				</rich:column>	
				<rich:column>
					<f:facet name="header">Version</f:facet>
					#{_revision[1]}
				</rich:column>
				<rich:column>
					<f:facet name="header">Modified at</f:facet>
					#{newsRevision.fetchDate(_revision[1])}
				</rich:column>
				<rich:column>
					<f:facet name="header">Action</f:facet>
					<s:link action="#{newsRevision.replaceRevision(_revision[0])}" value="Replace current instances"></s:link>
				</rich:column>	
			</rich:dataTable>
        <div style="clear:both"/>
 
    </rich:panel>
 
</ui:define>
 
</ui:composition>

Esta página precisa de receber um parâmetro chamado newsId, por isso, adicione as seguintes linhas no seu pages.xml

19
20
21
<page view-id="/newsRevision.xhtml" action="#{newsRevision.fetchRevisions}">
		<param name="id" value="#{newsRevision.newsId}"/>
	</page>

A tela produzida pelo código acima é mostrada na figura abaixo:

Listagem com as versões (clique para fullsize)

Listagem com as versões (clique para fullsize)

Finalmente a nosso backing bean associado à página (NewsRevisionService.java):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.furiousbob.beans;
 
import java.io.Serializable;
import java.util.Date;
import java.util.List;
 
import javax.persistence.EntityManager;
 
import org.jboss.envers.VersionsReader;
import org.jboss.envers.VersionsReaderFactory;
import org.jboss.envers.query.VersionsRestrictions;
import org.jboss.seam.annotations.Factory;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.datamodel.DataModel;
 
import com.furiousbob.com.News;
 
@Name("newsRevision")
public class NewsRevisionService implements Serializable {
 
	@In
	private EntityManager entityManager;
 
	private Long newsId;
 
	@DataModel(value="revisions")
	private List revisions;
 
 
	@Factory(value="revisions")
	public void fetchRevisions(){
		VersionsReader reader = VersionsReaderFactory.get(entityManager);
		this.revisions = reader.createQuery().forRevisionsOfEntity(News.class, false).add(VersionsRestrictions.idEq(newsId)).getResultList();
	}
 
	public Date fetchDate(long n){
		VersionsReader reader = VersionsReaderFactory.get(entityManager);
		Date d = reader.getRevisionDate((int)n);
		return d;
	}
 
	public void replaceRevision(News n){
		entityManager.merge(n);
	}
 
	public Long getNewsId() {
		return newsId;
	}
 
 
	public void setNewsId(Long newsId) {
		this.newsId = newsId;
	}
 
 
 
}

Para buscar as revisões de uma entidade, usamos o método fetchRevisions, para acessar o SessionFactory do envers, basta criarmos seu objeto e passarmos como referência nosso entityManager. O método createQuery().forRevisionsOfEntity(News.class, false) retorna uma lista de arrays de tamanho 2, onde a primeira posição é a entidade procurada e a segunda posição o número da revisão. Além disto precisamos buscar a data em que a revisão foi feita, note que em nosso template usamos a EL extendida do Seam para invocar um método no nosso backing bean passando um parâmetro para o mesmo #{newsRevision.fetchDate(_revision[1])}.
Nosso backing bean possui ainda um método para voltar com a versão da entidade, de acordo com a versão selecionada.
Este exemplo demonstra como é fácil gerar versionamento de entidades em seu sistema com o envers. Usando o envers e a API hibernate é possível realizar operações de restauração de objetos ou então apenas auditoria no seu sistema.
Isso é apenas um exemplo simples, uma funcionalidade desejável seria a adição do usuário corrente dentro de sua entidade, para isso basta usar as interfaces de callback do Hibernate (@PrePersist,@PreUpdate e @PreRemove) de forma que adicionem o usuário corrente em uma propriedade base de sua entidade.
Uma coisa que na minha opinião, faltou ao envers é expor de forma simples, além da versão e timestamp, o tipo de operação (INSERT,UPDATE,REMOVE) que foi realizada na entidade. Mas para isso, já abri um JIRA junto ao projeto, se você acha que esta funcionalidade vai ajudá-lo por favor vote nela para aumentar a chance de a mesma ser implementada ;)

Bem, acho que ficamos por aqui, próximos posts vou sair um pouco de Seam e vou falar de WebServices, fiquem atentos.

Inté