[RESOLVIDO] Problema no ActionContext.getContext().getSession() do Struts 2

Pessoal,

Estou implementando auditoria em um sistema que utiliza Struts 2. Resolvi utilizar o Hibernate Envers para a auditoria.
Já segui vários exemplos, mas não consigo fazer funcionar. O erro acontece na classe que implementa RevisionListener.
Nela eu tento pegar os dados do usuário da sessão da seguinte forma:

package tarefas.auditoria;

import java.util.Map;

import org.hibernate.envers.RevisionListener;

import tarefas.modelo.Usuario;

import com.opensymphony.xwork2.ActionContext;

public class AuditoriaListener implements RevisionListener {

	@Override
	public void newRevision(Object arg0) {
		AuditoriaRevEntity auditoriaRevEntity = (AuditoriaRevEntity) arg0;

		Map<String, Object> sessao = ActionContext.getContext().getSession();
		Usuario usuarioLogado = (Usuario) sessao.get("usuarioLogado");
		auditoriaRevEntity.setLogin(usuarioLogado.getLogin());
	}
}

É lançada uma exceção na linha 17.

Map<String, Object> sessao = ActionContext.getContext().getSession();

Essa é a saída do console quando passa por esta linha:

11:52:06 ERROR [AssertionFailure] an assertion failure occured (this may indicate a bug in Hibernate, but is more likely due to unsafe use of the session)
java.lang.NullPointerException
	at tarefas.auditoria.AuditoriaListener.newRevision(AuditoriaListener.java:17)
	at org.hibernate.envers.revisioninfo.DefaultRevisionInfoGenerator.generate(DefaultRevisionInfoGenerator.java:95)
	at org.hibernate.envers.synchronization.AuditProcess.getCurrentRevisionData(AuditProcess.java:124)
	at org.hibernate.envers.synchronization.AuditProcess.executeInSession(AuditProcess.java:106)
	at org.hibernate.envers.synchronization.AuditProcess.doBeforeTransactionCompletion(AuditProcess.java:155)
	at org.hibernate.engine.ActionQueue$BeforeTransactionCompletionProcessQueue.beforeTransactionCompletion(ActionQueue.java:554)
	at org.hibernate.engine.ActionQueue.beforeTransactionCompletion(ActionQueue.java:216)
	at org.hibernate.impl.SessionImpl.beforeTransactionCompletion(SessionImpl.java:571)
	at org.hibernate.jdbc.JDBCContext.beforeTransactionCompletion(JDBCContext.java:250)
	at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:138)
	at tarefas.persistence.hibernate.HibernateSessionRequestFilter.doFilter(HibernateSessionRequestFilter.java:40)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:233)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:191)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:127)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:298)
	at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:852)
	at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:588)
	at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:489)
	at java.lang.Thread.run(Thread.java:662)

Estou utilizando o Struts 2.3.1.2, Hibernate 3.6.10.Final e Tomcat 6.0.24.

Outro detalhe é que o sistema usa o pattern Open Session In View: https://community.jboss.org/wiki/OpenSessionInView

Alguém pode me ajudar? Desde já agradeço!

Fiz o seguinte teste: comentei a parte do código que pega o usário da sessão e setei um valor qualquer no login e funcionou. Como no código abaixo:

public class AuditoriaListener implements RevisionListener {

	@Override
	public void newRevision(Object arg0) {
		AuditoriaRevEntity auditoriaRevEntity = (AuditoriaRevEntity) arg0;

//		Map<String, Object> sessao = ActionContext.getContext().getSession();
//		Usuario usuarioLogado = (Usuario) sessao.get("usuarioLogado");
//		auditoriaRevEntity.setLogin(usuarioLogado.getLogin());
		
		auditoriaRevEntity.setLogin("usuário logado");
	}
}

Então acho que o problema é na forma que estou pegando o usuário da sessão.
Alguém sabe se é possível pegar o usuário da sessão no Struts 2 fora das classes Action?
Estou fazendo da forma errada?

Encontrei um dos problemas: a versão do Struts que estou usando (2.3.1.2) depende do xwork-core-2.3.1.2 e essa versão do Xwork está com problemas.
Pois, de acordo com a documentação, o metódo ActionContext.getContext() nunca retorna null --> http://struts.apache.org/2.3.1.2/xwork-core/apidocs/com/opensymphony/xwork2/ActionContext.html#getContext%28%29
Testei o Struts 2.0.11.2 que usa o xwork-2.0.5 e agora sim o método está retornando o ActionContext.
Acontece que o método getSession() retorna null.

ActionContext context = ActionContext.getContext();		
Map<String, Object> session = context.getSession();

Alguém sabe o porquê?

Trabalhei em um projeto com struts 2 e envers, no meu caso fiz da seguinte forma, e funcionou perfeitamente. Uso o struts2-core-2.1.8.1.

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.hibernate.envers.RevisionListener;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

public class AuditoriaListener implements RevisionListener{
	
	public void newRevision(Object revisionEntity) {
		
		String ip = getRequest().getRemoteAddr().toString();
		Usuario usuario = (Usuario)getSession().getAttribute("usuarioLogado");
		
		Revisao revisao = (Revisao) revisionEntity;
		revisao.setIp(ip);
		revisao.setUsuario(usuario.getPessoa().getMatricula());
	}
	
    private HttpServletRequest getRequest() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return requestAttributes.getRequest();
    }

    private HttpSession getSession() {
        return getRequest().getSession();
    }
	
}

mmmbrito,

Pelo que vi o seu projeto usa o Spring para guardar/recuperar os dados do usuário na sessão.
Infelizmente o projeto que estou trabalhando não usa. E é um sistema muito grande.
Não sei se compensa usar o Spring agora.

Obrigado pela atenção.

Na verdade usei apenas ai visto que com o ActionContext não conseguia buscar a informação, mas de qualquer forma boa sorte!

Não mexo mais no struts a muito tempo, então nao sei dizer nele.

Mas vou passar a ideia geral.

  1. Voçê deve criar um interceptor (AuthenticationContextHolder) global que irá pegar o usuário logado ou a session, e armazenar localmente (ThreadLocal) …

  2. No seu AuditoriaListener do você chama um método estático qualquer (ex: getCurrentUser) para retornar o seu usuário…

Essa abordagem é parecido com o do Spring:
http://kickjava.com/src/org/springframework/web/context/request/RequestContextHolder.java.htm

Fiz uma implementação só que em mentawai para se ter uma idéia.

Filtro , que se assemelha ao Interceptor do Struts.

public class AuthenticationContextFilter implements AfterConsequenceFilter {
	private ThreadLocal&lt;Object&gt; userThreadLocal = new ThreadLocal&lt;Object&gt;();
 
	@Override
	public String filter(InvocationChain chain) throws Exception {
		Context context =  chain.getAction().getSession();
		userThreadLocal.set(BaseLoginAction.getUserSession(context));
		return chain.invoke();
	}
	
	@Override
	public void afterConsequence(Action action, Consequence c,boolean conseqExecuted, boolean actionExecuted, String result) {
		userThreadLocal.set(null);
	}

	@Override
	public void destroy() {
	}
	
	public Object getUserSession() {
		return userThreadLocal.get();
	}

}

Listener

/**
 * Listener do Hibernate Envers, que registra o usuário logado.
 * A Classe de auditoria da aplicação (anotada com @RevisionEntity) deve extender {@link AuditRevisionEntity} para que possa 
 * ser setado o usuário logado, no momento da alteração de alguma entidade.
 * 
 * @author Ricardo JL Rufino
 * @serial 1.2.2
 * @date 09/04/2011
 */
public class AuditRevisionListener implements RevisionListener {
	
	private static final Logger logger = LoggerFactory.getLogger(AuditRevisionListener.class);

	@Override
	public void newRevision(Object revisionEntity) {
		
		if( ! (revisionEntity instanceof AuditRevisionEntity)) throw new AuditException("Class annoted with @RevisionEntity shold extend AuditRevisionEntity !");
		
		if(ApplicationManager.getInstance() == null){
			logger.warn("ApplicationManager not initialized !");
			return;
		}
		
		List&lt;Filter&gt; filters = ApplicationManager.getInstance().getGlobalFilters();
		for (Filter filter : filters) {
			if(filter instanceof AuthenticationContextFilter ){
				AuthenticationContextFilter contextFilter = (AuthenticationContextFilter) filter;
				
				// Entidade usada pra identificar quem fez a alteração no Objeto.
				AuditRevisionEntity entity = (AuditRevisionEntity) revisionEntity;
				
				Object object = contextFilter.getUserSession();
				
				try {
					if(object != null){
						long id = Long.parseLong(InjectionUtils.getProperty(object, Identity.USER_ID_PROPERTY));
						entity.setUserID(id);
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
	}

}

Emfim, pode ser que o Struts já tenha uma implementação pra isso, caso não, você pode seguir essa dica…

RicardoCobain,

Cara, você acertou em cheio. O sistema já usava a abordagem que você sugeriu. Mas eu não tive a ideia de armazenar localmente o usuário logado.
O código ficou assim:

package tarefas.interceptor;

import tarefas.modelo.Usuario;

import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.Interceptor;

public class AutorizadorInterceptor implements Interceptor {

	/**
	 * 
	 */
	private static final long serialVersionUID = 5019156825527658895L;
	private static final ThreadLocal<Usuario> userThreadLocal = new ThreadLocal<Usuario>();

	@Override
	public void destroy() {
		userThreadLocal.set(null);
	}

	@Override
	public void init() {
		// TODO Auto-generated method stub

	}

	@Override
	public String intercept(ActionInvocation invocation) throws Exception {

		Usuario usuarioLogado = (Usuario) invocation.getInvocationContext().getSession().get("usuarioLogado");
		if (usuarioLogado == null) {
			return "naoLogado";
		}

		userThreadLocal.set(usuarioLogado);
		return invocation.invoke();
	}

	public static Usuario getUserSession() {
		return userThreadLocal.get();
	}
}

E o AuditoriaListener:

package tarefas.auditoria;

import org.hibernate.envers.RevisionListener;

import tarefas.interceptor.AutorizadorInterceptor;
import tarefas.modelo.Usuario;

public class AuditoriaListener implements RevisionListener {

	@Override
	public void newRevision(Object arg0) {
		AuditoriaRevEntity auditoriaRevEntity = (AuditoriaRevEntity) arg0;

		Usuario usuarioLogado = AutorizadorInterceptor.getUserSession();

		auditoriaRevEntity.setLogin(usuarioLogado.getLogin());
	}
}

Muito obrigado pela dica!

Só fica uma dúvida: será se não terei problemas de concorrência?

A maioria dos servidores de aplicação utilizam uma thread por requisição, o que “garante” o isolamento e esse controle de concorrência, agora não sei se todos os servidores estão usando isso, ou irão usar isso pra sempre…
Estava vendo umas implementações do node JS, que com apenas 1 thread ele trata várias requisições, usando uma estratégia de non-blocking I/O… esse é a tendência dos servidores de CLOUD, para otimizar recursos…

Mas atualmente essa abordagem que te passei é muito usada… mas é bem pesquisar outra possibilidade, se achar me fala … =]

Mais sobre

[EDIT] Aqui algumas provas
http://www.ibm.com/developerworks/library/j-nioserver/