[Resolvido]Problemas com joda datetime + VRaptor

Boa Noite,

Eu estou usando VRaptor 3.5.3 + jodatime 2.3+ joda-time-hibernate-1.3+ joda-time-jsptags-1.1.1, porém estou tendo problemas para retornar para o controller um datetime. Quando eu estou inserindo a data, ele funciona muito bem, inclusive chama meu @Convert sem problemas, fazendo a correta formatação da data e gravando no banco corretamente também. Porém quando eu estou editando o registro em que a data não foi alterada, o converter não é chamado novamente e o campo na minha entidade fica como null.

Entidade:

...
import org.joda.time.DateTime;

@Entity  
@Table(name="crmconversarec")  
public class CrmConversaRec implements Serializable {
	
	private static final long serialVersionUID = 12166L;

	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
	@Column(name="codigo_ccr")
	private Integer codigo;
	
    @ManyToOne
	@JoinColumn(name="codrec_ccr")
	private CrmRecepcaoCliente atendimento;
    
    @Column(name="descri_ccr")
    private String descricao;
  
    @Type(type = "org.joda.time.contrib.hibernate.PersistentDateTime")
    @Column(name="dahcon_ccr")
    private DateTime dataConversa;
       
    @Type(type = "org.joda.time.contrib.hibernate.PersistentDateTime")   
    @Column(name="dahret_ccr")
    private DateTime dataRetorno;
    
    @Column(name="retage_ccr")
    private Boolean retornoAgendado;
    
    @Column(name="retrea_ccr")
    private Boolean retornoRealizado;
    
    @ManyToOne
    @JoinColumn(name="codusu_ccr")
    private Usuario responsavelRetorno;
    
    @ManyToOne
    @JoinColumn(name="usuapr_ccr")
    private Usuario usuarioAprovacao;
    
    @Type(type = "org.joda.time.contrib.hibernate.PersistentDateTime")
    @Column(name="dahapr_ccr")
    private DateTime dataHoraAprovacao;
    
    @ManyToOne
    @JoinColumn(name="codcsa_ccr")
    private CrmSituacaoAprovacaoConversa situacaoAprovacao;
	
    @ManyToOne
    @JoinColumn(name="codcfa_ccr")
    private CrmFaseAtendimento fase;
    
    @ManyToOne
    @JoinColumn(name="usuins_ccr")
    private Usuario usuarioInsercao;
    
    @Type(type = "org.joda.time.contrib.hibernate.PersistentDateTime")
    @Column(name="dahins_ccr")
    private DateTime dataInsercao;
       
    @ManyToOne
    @JoinColumn(name="usualt_ccr")
    private Usuario usuarioAlteracao;
    
    @Type(type = "org.joda.time.contrib.hibernate.PersistentDateTime")
    @Column(name="dahalt_ccr")
    private DateTime dataAlteracao;

... Getters and setters

O campo que estou analisando é o dataRetorno.

Classe Controller

@Resource
public class AtendimentoController {

	private final Result result;
	private final Validator validator;
	private UsuarioWeb logado;
	private CrmFaseAtendimentoDao daoFase;
	private CrmCategoriaAtendimentoDao daoCategoria;
	private CrmTipoAtendimentoClienteDao daoTipo;
	private CrmFormaContatoDao daoForma;
	private TipoMidiaDao daoMidia;
	private CrmRecepcaoClienteDao dao;
	private CrmConversaRecDao daoConversa;
	private EmpresaDao daoEmp;
	private CrmSituacaoAtendimentoDao daoSit;


	public AtendimentoController(Result result, Validator validator,UsuarioWeb logado,CrmFaseAtendimentoDao daoFase,CrmCategoriaAtendimentoDao daoCategoria,CrmTipoAtendimentoClienteDao daoTipo,CrmFormaContatoDao daoForma,TipoMidiaDao daoMidia,CrmRecepcaoClienteDao dao,EmpresaDao daoEmp,CrmSituacaoAtendimentoDao daoSit,CrmConversaRecDao daoConversa) {
		this.result = result;
		this.validator = validator;
		this.logado = logado;
		this.daoFase = daoFase;
		this.daoCategoria = daoCategoria;
		this.daoTipo = daoTipo;
		this.daoForma = daoForma;
		this.daoMidia = daoMidia;
		this.dao = dao;
		this.daoEmp = daoEmp;
		this.daoSit = daoSit;
		this.daoConversa = daoConversa;

	}

	@Restrito
	@Post("/atendimento")
	public void adiciona(final CrmRecepcaoCliente crmRecepcaoCliente){
		crmRecepcaoCliente.setAtendente(logado.getUsuario());
		crmRecepcaoCliente.setDataAtendimento(new DateTime());		
		crmRecepcaoCliente.setEmpresa(daoEmp.carrega(1));//Fixo
		crmRecepcaoCliente.setSituacao(daoSit.carrega(1));//Fixo
		
		validator.validate(crmRecepcaoCliente);
		result.include("faseList",daoFase.listaTudo());
		result.include("categoriaList",daoCategoria.listaTudo());
		result.include("tipoList",daoTipo.listaTudo());
		result.include("formaContatoList",daoForma.listaTudo());
		result.include("midiaList",daoMidia.listaTudo());
		validator.onErrorUsePageOf(this).formulario();
		try {
			dao.salva(crmRecepcaoCliente);
			for (CrmConversaRec conversa : crmRecepcaoCliente.getConversas()) {
				conversa.setDataConversa(new DateTime());
				conversa.setAtendimento(crmRecepcaoCliente);
				conversa.setDataInsercao(new DateTime());
				
				// Se tem data de retorno é porque foi agendado retorno
				if(conversa.getDataRetorno()!= null){
					conversa.setRetornoAgendado(true);
				}

			}
			daoConversa.salva(crmRecepcaoCliente.getConversas());

			result.include("msg","Registro de atendimento cadastrado com sucesso!");
		} catch (Exception e) {
			e.printStackTrace();
			//				result.include("errors",e.getMessage());
		}
		result.redirectTo(this).formulario();
	}

	@Restrito
	@Get("/atendimento/novo")
	public void formulario() {		
		result.include("faseList",daoFase.listaTudo());
		result.include("categoriaList",daoCategoria.listaTudo());
		result.include("tipoList",daoTipo.listaTudo());
		result.include("formaContatoList",daoForma.listaTudo());
		result.include("midiaList",daoMidia.listaTudo());
		result.include("qtdConversa",null);
		 
	}

	@Restrito
	@Get("/atendimento/{codigoRec}")
	public CrmRecepcaoCliente edita(Integer codigoRec){
		result.include("faseList",daoFase.listaTudo());
		result.include("categoriaList",daoCategoria.listaTudo());
		result.include("tipoList",daoTipo.listaTudo());
		result.include("formaContatoList",daoForma.listaTudo());
		result.include("midiaList",daoMidia.listaTudo());
		CrmRecepcaoCliente crmRecepcaoCliente = dao.carrega(codigoRec);
		result.include("qtdConversa",crmRecepcaoCliente.getConversas().size() == 0?null:crmRecepcaoCliente.getConversas().size()-1);
		return crmRecepcaoCliente ;
	}

	@Restrito
	@Put("/atendimento/{atendimento.codigoRec}")
	public void altera(CrmRecepcaoCliente crmRecepcaoCliente) {

		validator.validate(crmRecepcaoCliente);
		result.include("faseList",daoFase.listaTudo());
		result.include("categoriaList",daoCategoria.listaTudo());
		result.include("tipoList",daoTipo.listaTudo());
		result.include("formaContatoList",daoForma.listaTudo());
		result.include("midiaList",daoMidia.listaTudo());
		validator.onErrorUsePageOf(this).edita(crmRecepcaoCliente.getCodigo());
		try {
			CrmRecepcaoCliente entidadeBanco = dao.carrega(crmRecepcaoCliente.getCodigo());
			
			crmRecepcaoCliente.setAtendente(entidadeBanco.getAtendente());
			crmRecepcaoCliente.setDataAtendimento(entidadeBanco.getDataAtendimento());
			crmRecepcaoCliente.setEmpresa(entidadeBanco.getEmpresa());
			crmRecepcaoCliente.setSituacao(entidadeBanco.getSituacao());
			
			dao.altera(crmRecepcaoCliente);
			
			//Percorre a lista de conversas
			for (CrmConversaRec conversa : crmRecepcaoCliente.getConversas()) {
				conversa.setAtendimento(crmRecepcaoCliente);
				conversa.setDataAlteracao(new DateTime());
												
				if(conversa.getCodigo()!= null && conversa.getCodigo()>0){
					daoConversa.altera(conversa);					
				}else{
					daoConversa.salva(conversa);
				}
			}
			
			List<CrmConversaRec> conversasBanco = daoConversa.busca(crmRecepcaoCliente.getCodigo());			
			for (CrmConversaRec crmConversaRec : conversasBanco) {
				
				// Se está gravado no banco e não contém mais o registro, é porque foi removido e deve apagar do banco.
				if(!crmRecepcaoCliente.getConversas().contains(crmConversaRec)){
					daoConversa.remove(crmConversaRec);
				}
			}
			
			result.include("msg","Registro de atendimento alterado com sucesso!");
		} catch (Exception e) {				
			e.printStackTrace();
		}
		result.redirectTo(ListagemController.class).busca();
	}

Classe Converter

[code]
import java.util.ResourceBundle;

import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;

import br.com.caelum.vraptor.Convert;
import br.com.caelum.vraptor.Converter;
import br.com.caelum.vraptor.ioc.ApplicationScoped;

@ApplicationScoped
@Convert(DateTime.class)
public class DateTimeConverter implements Converter {

@Override
public DateTime convert(String strDateTime, Class<? extends DateTime> arg1,
		ResourceBundle arg2) {
	System.out.println("arg0: "+ strDateTime);
	System.out.println("### Passou por aqui!!!! ###");

	// Se a formatação de data for com traços em vez de barra, formata diferente
	DateTimeFormatter dateFormat = DateTimeFormat.forPattern("dd/MM/yyyy HH:mm:ss");
	try {
		return dateFormat.parseDateTime(strDateTime);						
	} catch (Exception e) {
		e.printStackTrace();
		dateFormat = ISODateTimeFormat.dateTime();
        return dateFormat.parseDateTime(strDateTime);
	}
	
}    

}[/code]

Campo data do meu jsp

<div class='input-group date'
	id="dataHoraRetorno_${status.index+1}"											
	onclick="chamaCalendario(this)">
	<input type='text' class="form-control" id="txtDataHoraRetorno_${status.index+1}" data-date-format="DD/MM/YYYY hh:mm:ss"
	name="crmRecepcaoCliente.conversas[${status.index}].dataRetorno"																										
	value="<joda:format value="${crmRecepcaoCliente.conversas[status.index].dataRetorno}" style="F" pattern="dd/MM/yyyy HH:mm:ss"  />" />
	<span class="input-group-addon"><span
	class="glyphicon glyphicon-calendar"></span> </span>
</div>	

Alguém tem alguma idéia de o que pode ser?

Eu consegui resolver o meu problema. O problema acredito ser no componente que uso para data/hora. Se no momento que vou fazer submit no form ele estiver readonly, a data é retornada como null no vraptor.
Fiz uma solução de contorno enquanto não acho uma solução definitiva, que é adicionar um método que habilita todos os campos de data no método onsubmit, ou seja, antes de ele efetivamente chegar no controller.

<form id="formUsuario" role="form" method="post"
	action="<c:url value="/atendimento/${crmRecepcaoCliente.codigo}"/>" onsubmit="habilitaTodosCalendario()">
function habilitaTodosCalendario(){
		var totalLinhas = qtdRegistros()		
		for ( var i = 1; i < totalLinhas; i++) {
			
			jQuery('#dataHoraRetorno_' + i).data("DateTimePicker").enable();			
		}		
	}

Beleza, está certo que a data deveria vir preenchida.

Apenas 2 pontos importantes:
1 - Quando o elemento está readonly=“readonly” ele é enviado ao controller sim. Ele não é enviado quando está disabled=“disabled”. Ou seja, você pode deixá-lo apenas readonly que o usuário não vai alterá-lo da mesma maneira.

2 - Você disse: “Porém quando eu estou editando o registro em que a data não foi alterada, o converter não é chamado novamente e o campo na minha entidade fica como null.” Isso mostra, na verdade que você tem um problema de mass assignment. Esse problema acontece quando você recebe uma classe que tem tudo, ou seja, a própria entidade, causando algumas práticas nada boas e falhas de segurança.
Por exemplo, provavelmente você já fez uma tela de cadastro e alteração de usuários. Imagine que na action de alteração do usuário você receba uma entidade de um usuário. Se você persistir isso provavelmente a senha do usuário vai ficar nula. Para resolver, você coloca a senha em um campo hidden na tela. Isso é um exemplo de mass assignment, o usuário pode alterar o valor desse campo e alterar no banco sem que você saiba disso.

Ou seja, você recebe a entidade já populada no controller e já toma uma ação com ela. É importante você ter uma classe chamada DTO (Data Transfer Object) que irá receber os dados para determinada action. No exemplo acima, seria um DTO que contém apenas os campos que poderão ser alterados (e mais o ID, que também pode causar o mass assignment, é interessante usar algum outro identificador).