[Vraptor] Conversor

Olá,

Eu estou tentando implementar um conversor do Vraptor para minhas classes de modelo. Mas meu conversor não está sendo chamado:

[code]@Convert(Model.class)
public class ModelConverter implements Converter {

private final Logger logger = LoggerFactory.getLogger(this.getClass());
private Session session;

public ModelConverter(Session session) {
	this.session = session;
}

public Model convert(String value, Class<? extends Model> type, ResourceBundle bundle) {
	if (value == null || value.equals("")) {
		return null;
	}

	try {
		long modelId = Long.parseLong(value);
		ModelDao<Model> modelDao = new ModelDao<Model>(session, type);
		return modelDao.visualizar(modelId);
	} catch (Exception e) {
		logger.error("Erro ao converter Convênio. Valor: " + value, e);
		throw new ConversionError(bundle.getString("is_not_a_valid_value"));
	}
}

}[/code]
Está lançando a seguinte exceção:

[quote]br.com.caelum.vraptor.http.InvalidParameterException: Exception when trying to instantiate Target(name=paciente, type=class br.com.metamorfosevirtual.models.Paciente)
at br.com.caelum.vraptor.http.iogi.VRaptorInstantiator.handleException(VRaptorInstantiator.java:96)
at br.com.caelum.vraptor.http.iogi.VRaptorInstantiator.instantiate(VRaptorInstantiator.java:88)
at br.com.caelum.vraptor.http.iogi.VRaptorInstantiator.instantiate(VRaptorInstantiator.java:81)
at br.com.caelum.vraptor.http.iogi.IogiParametersProvider.instantiateOrAddError(IogiParametersProvider.java:80)
at br.com.caelum.vraptor.http.iogi.IogiParametersProvider.instantiateParameters(IogiParametersProvider.java:73)
at br.com.caelum.vraptor.http.iogi.IogiParametersProvider.getParametersFor(IogiParametersProvider.java:63)
at br.com.caelum.vraptor.interceptor.ParametersInstantiatorInterceptor.getParametersFor(ParametersInstantiatorInterceptor.java:132)
at br.com.caelum.vraptor.interceptor.ParametersInstantiatorInterceptor.intercept(ParametersInstantiatorInterceptor.java:86)
at br.com.caelum.vraptor.core.LazyInterceptorHandler.execute(LazyInterceptorHandler.java:59)
at br.com.caelum.vraptor.core.DefaultInterceptorStack.next(DefaultInterceptorStack.java:54)
at br.com.caelum.vraptor.core.ToInstantiateInterceptorHandler.execute(ToInstantiateInterceptorHandler.java:56)
at br.com.caelum.vraptor.core.DefaultInterceptorStack.next(DefaultInterceptorStack.java:54)
at br.com.caelum.vraptor.core.ToInstantiateInterceptorHandler.execute(ToInstantiateInterceptorHandler.java:56)
at br.com.caelum.vraptor.core.DefaultInterceptorStack.next(DefaultInterceptorStack.java:54)
at br.com.caelum.vraptor.interceptor.InstantiateInterceptor.intercept(InstantiateInterceptor.java:48)
at br.com.caelum.vraptor.core.ToInstantiateInterceptorHandler.execute(ToInstantiateInterceptorHandler.java:54)
at br.com.caelum.vraptor.core.DefaultInterceptorStack.next(DefaultInterceptorStack.java:54)
at br.com.caelum.vraptor.interceptor.FlashInterceptor.intercept(FlashInterceptor.java:83)
at br.com.caelum.vraptor.core.ToInstantiateInterceptorHandler.execute(ToInstantiateInterceptorHandler.java:54)
at br.com.caelum.vraptor.core.DefaultInterceptorStack.next(DefaultInterceptorStack.java:54)
at br.com.caelum.vraptor.interceptor.ExceptionHandlerInterceptor.intercept(ExceptionHandlerInterceptor.java:67)
at br.com.caelum.vraptor.core.ToInstantiateInterceptorHandler.execute(ToInstantiateInterceptorHandler.java:54)
at br.com.caelum.vraptor.core.DefaultInterceptorStack.next(DefaultInterceptorStack.java:54)
at br.com.metamorfosevirtual.interceptors.HibernateInterceptor.intercept(HibernateInterceptor.java:27)
at br.com.caelum.vraptor.core.ToInstantiateInterceptorHandler.execute(ToInstantiateInterceptorHandler.java:54)
at br.com.caelum.vraptor.core.DefaultInterceptorStack.next(DefaultInterceptorStack.java:54)
at br.com.caelum.vraptor.interceptor.ResourceLookupInterceptor.intercept(ResourceLookupInterceptor.java:69)
at br.com.caelum.vraptor.core.ToInstantiateInterceptorHandler.execute(ToInstantiateInterceptorHandler.java:54)
at br.com.caelum.vraptor.core.DefaultInterceptorStack.next(DefaultInterceptorStack.java:54)
at br.com.caelum.vraptor.core.EnhancedRequestExecution.execute(EnhancedRequestExecution.java:44)
at br.com.caelum.vraptor.VRaptor$1.insideRequest(VRaptor.java:93)
at br.com.caelum.vraptor.ioc.guice.GuiceProvider.provideForRequest(GuiceProvider.java:82)
at br.com.caelum.vraptor.VRaptor.doFilter(VRaptor.java:99)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:472)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:168)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:99)
at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:929)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:407)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1002)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:585)
at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:312)
at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.lang.Thread.run(Unknown Source)
Caused by: java.lang.NullPointerException
at br.com.caelum.vraptor.http.iogi.VRaptorInstantiator$VRaptorTypeConverter.setPropertiesAfterConversions(VRaptorInstantiator.java:144)
at br.com.caelum.vraptor.http.iogi.VRaptorInstantiator$VRaptorTypeConverter.instantiate(VRaptorInstantiator.java:135)
at br.com.caelum.iogi.MultiInstantiator.instantiate(MultiInstantiator.java:20)
at br.com.caelum.vraptor.http.iogi.VRaptorInstantiator.instantiate(VRaptorInstantiator.java:86)
… 47 more[/quote]
O que tem de errado com ele?

Acredito que você esteja se confundindo…

O seu converter é da classe Model
O erro está dando na hora de instanciar a class br.com.metamorfosevirtual.models.Paciente

A única explicação para isso funcionar é se Model for superClass de Paciente, mas para isso, preciso ver o que você está passando como parâmetro na requisição, a sua action e a classe Paciente…

Ah sim, esqueci de falar: Model é superclass de Paciente sim.
Não entendi o que eu teria que passar como parâmetro na requisição.

Se você quer converter uma String em um Model (Paciente) você precisa ter uma estrutura parecida com isso:
Html:

&lt;input type="text" name="paciente" value="123"&gt;

Action

@Post("/faz/algo")
public void fazAlgo(Paciente paciente) {
//...
}

Sim, eu to fazendo isso. Mas em algumas actions eu preciso “preencher” o model. E aí não funciona. Por exemplo, eu tenho o seguinte código:

[code] var url = $(this).attr(“data-url”);
var data = {
“paciente.cpf” : $("#lightbox-login input[name=‘cpf’]").val(),
“paciente.senha” : $("#lightbox-login input[name=‘senha’]").val()
};

		$.post(url, data, function(data) {
			enviando = false;
			
			if(data === "true") {
				window.location.pathname = consultasURL;
			} else if (data === "false") {
				$("#lightbox-login .validacao").text("Dados incorretos");
				$("#lightbox-login .validacao").removeClass("esconder");
			} else {
				$("#lightbox-login .validacao").text("Ocorreu um erro. Tente novamente");
				$("#lightbox-login .validacao").removeClass("esconder");
			}
		}, "text");[/code]

Com a seguinte action:

@Post("/login") public void login(Paciente paciente) { if (pacienteDao.login(paciente)) { result.use(Results.http()).body("true"); } else { result.use(Results.http()).body("false"); } }
E não está funcionando. Mas quando eu comento meu conversor de model, funciona. Tem 2 coisas que eu não entendo:
1- Porque o conversor de model impede o funcionamento do código acima, já que no código acima eu só preciso converter o cpf e a senha, que são strings?
2- Porque quando eu descomento o conversor de Model e passo um model da maneira que você falou não funciona?
Acredito que as duas estejam relacionadas…

O que o VRaptor faria nesse caso é o seguinte:

  • Chama o Converter
  • Pega o objeto gerado e seta as outras propriedades.

Talvez, dependendo da ordem em que vc está mandando os parâmetros, ele tá tentando setar as outras propriedades antes de chamar o converter.

Ou ainda, o Converter está retornando null.

Pode verificar por favor se o converter está retornando null?

como vc tá setando o valor que vai passar pelo converter?

Oi Lucas, valeu pela ajuda.

Então, eu coloco um breakpoint na primeira linha do meu controller e na primeira linha do meu converter. E quando rodo dá a NullPointerException antes de cair neles. Ou seja, meu converter nem é chamado.

Só um detalhe: nessa action que tá dando erro, eu não espero que passe pelo converter. Eu preciso do converter em outras actions do sistema, que estão funcionando certinho. Nessa action eu faço o seguinte:

[code]var url = $(this).attr(“data-url”);
var data = {
“paciente.cpf” : $("#lightbox-login input[name=‘cpf’]").val(),
“paciente.senha” : $("#lightbox-login input[name=‘senha’]").val()
};

$.post(url, data, function(data) {
enviando = false;

if(data === "true") {  
    window.location.pathname = consultasURL;  
} else if (data === "false") {  
    $("#lightbox-login .validacao").text("Dados incorretos");  
    $("#lightbox-login .validacao").removeClass("esconder");  
} else {  
    $("#lightbox-login .validacao").text("Ocorreu um erro. Tente novamente");  
    $("#lightbox-login .validacao").removeClass("esconder");  
}  

}, “text”);[/code]

Ou seja, eu não espero que o meu converter crie o Paciente. Eu espero que o Vraptor crie e popule com os campos que eu mandei.

tente fazer isso:

var data = {    
    "paciente" : "",
    "paciente.cpf" : $("#lightbox-login input[name='cpf']").val(),    
    "paciente.senha" : $("#lightbox-login input[name='senha']").val()    
}; 

E veja se passa pelo seu converter. E se passar, tente trocar o primeiro if dele por:

if (Strings.isNullOrEmpty(value)) return new Paciente();

O problema é que se vc criou um converter pro tipo, o VRaptor vai tentar usar esse converter sempre.

Agora funcionou. Mas ainda não entendi algumas coisas:

1- Quando eu fiz o que você falou, o converter só foi chamado 1 vez, pra resolver o “paciente”. Ele não foi chamado pra resolver o “paciente.cpf” nem “paciente.senha”. Então, porque quando eu tiro a linha do paciente não funciona, já que eu só to setando strings e nenhum modelo?

2- Pra cada lugar do meu código que eu populo meus modelos eu teria que passar uma string vazia pro modelo em si? Não teria como fazer de outra forma?

3- Como fica a questão de segurança nesse caso: esse javascript fica no front-end. A pessoa poderia alterar o código pra mandar o vraptor popular o “paciente” que eu recebo no controller com o id de um outro paciente. Nesse caso ela poderia se logar como outra pessoa, sem ter a senha dela. Bastaria saber o id.

1- Isso pq o converter só é chamado para os parâmetros que caem em um Paciente. “paciente.cpf” e “paciente.senha” são strings.
Não funciona, pq qdo vc tem um converter pra um tipo, o VRaptor vai sempre usar esse converter. A mudança de retornar new Paciente() deveria funcionar, em todo caso.

2 - Só se vc tiver converters pra esses modelos

3 - Isso vale pra qqer formulário que vc submeter também. Você tem que controlar a permissão do usuário, para que ele não altere um paciente que ele não controla.

Você criou converters pra todos os tipos?

A ideia era criar 1 converter de Model para que todos os meus modelos (que herdam Model) possam ter um converter. Mas nesse caso eu teria que mudar praticamente todas as views e javascript do sistema. Não vejo muito sentido de não funcionar apenas o seguinte:

var data = { "paciente.cpf" : $("#lightbox-login input[name='cpf']").val(), "paciente.senha" : $("#lightbox-login input[name='senha']").val() };

Eu não entendi uma coisa ainda: porque no exemplo acima o Vraptor não chama nem meu conversor e nem dá um new Paciente() por si só? Porque eu penso o seguinte: pra setar o cpf de um paciente, vc precisa antes “ter” esse paciente. Então eu diria que o Vraptor teria que fazer um dos dois: ou chamar meu conversor de Paciente pra ele “ter” o paciente ou criar um Paciente por si só. Mas ele não faz nenhum dos dois. O que ele faz exatamente nesse caso?

Tem certeza que não está chamando o converter?

vc testou isso na primeira linha do convert?

chegou a fazer a modificação do if que eu te falei?

o código que faz isso são esses 3 métodos:

de acordo com isso, ele deveria tentar converter antes de setar os outros parâmetros.

Sim, na primeira linha. E não caiu lá não. Mesmo com seu if não funcionou.

[quote=Lucas Cavalcanti]o código que faz isso são esses 3 métodos:

de acordo com isso, ele deveria tentar converter antes de setar os outros parâmetros.[/quote]

Em que trecho ele criaria o Paciente antes de setar o cpf?

no setPropertiesAfterConversion…

vc consegue colocar um breakpoint nesse trecho do código?

vc precisa colocar o source do VRaptor no eclipse antes de fazer isso.

Aconteceu o seguinte em cada um dos metodos:

public boolean isAbleToInstantiate(Target<?> target) { return !String.class.equals(target.getClassType()) && converters.existsFor(target.getClassType()); }
Aqui, o target é “paciente” e o retorno do método é “true”.

public Object instantiate(Target<?> target, Parameters parameters) { try { Parameter parameter = parameters.namedAfter(target); return converterForTarget(target).convert(parameter.getValue(), target.getClassType(), localization.getBundle()); } catch (ConversionError ex) { errors.add(new ValidationMessage(ex.getMessage(), target.getName())); } catch (IllegalStateException e) { return setPropertiesAfterConversions(target, parameters); } return null; }
Nesse outro, o target ainda é “paciente” e parameter é “Parameters(Parameter(paciente.senha -> 1234), Parameter(paciente.cpf -> 01234567890))”. O método “namedAfter” lança uma IllegalStateException e a execução cai no segundo catch (IllegalStateException) e vai para o outro método.

[code] private Object setPropertiesAfterConversions(Target<?> target, Parameters parameters) {
List params = parameters.forTarget(target);
Parameter parameter = findParamFor(params, target);

		Object converted = converterForTarget(target).convert(parameter.getValue(), target.getClassType(), localization.getBundle());

		return new NewObject(this, parameters.focusedOn(target), converted).valueWithPropertiesSet();
	}[/code]

É aqui que dá o problema. Na segunda linha, o método findParamFor é chamado. O problema é que ele tenta buscar um parâmetro pra “paciente” e não existe nenhum. Então ele retorna null e guarda na variável “parameter”. Aí na linha de baixo ele faz “parameter.getValue()” e dá NullPointerException.

Isso tudo acontece antes do meu controller ou meu conversor ser chamado.

Acho que a raiz do problema está na classe Parameters, no método “assertFoundAtMostOneTarget”. Nesse método, ele diz que espera apenas 1 parâmetro para a classe Paciente, mas ele considera que eu passei 2: “paciente.cpf” e “paciente.senha”. E aí dá IllegalStateException. Seria um bug?

Então, se parameter.getValue() é null, ele deveria simplesmente passar null pro seu converter.

então se no seu converter ele fizesse:

if (value == null) return new Paciente();

deveria funcionar sem problemas.

O converterForTarget tá retornando o seu converter mesmo?

Entendi o problema… o Parameter está null, não o getValue dele.

Faça o seguinte então:
copie essa classe pro seu projeto:

mude a linha 144 pra:

Object converted = converterForTarget(target).convert(parameter == null ? null : parameter.getValue(), target.getClassType(), localization.getBundle());

Isso deve funcionar.

Se puder, abra uma issue:

Ou melhor ainda, faça essa correção no próprio VRaptor e mande um pull request, de preferência com um teste associado.

Obrigado!

Isso, é o parameter que chega null. Eu só discordo da sua solução. Acho que a causa do problema não está exatamente aí não. Acho que o erro tá no método anterior (instantiate). O que acontece é o seguinte: a execução cai no método instantiate com os parâmentros:

(br.com.caelum.iogi.reflection.Target) Target(name=paciente, type=class br.com.caelum.vraptor.blank.model.Paciente)
(br.com.caelum.iogi.parameters.Parameters) Parameters(Parameter(paciente.senha -> Teste), Parameter(paciente.nome -> Teste))

Aí ele chama o parameters.namedAfter(target). O que você esperaria que essa linha retornasse? Eu esperaria um null, já que não tem nenhum parametro para a target “paciente”. Eu NÃO considero que a target de “paciente.senha” e “paciente.nome” seja paciente. Porém esse método considera que existem 2 parâmetros para essa target. E logo depois disso ele faz um “assertFoundAtMostOneTarget(target, named);”. E aí dá IllegalStateException. Essa IllegalStateException é capturada lá fora.

Ou seja, o Vraptor considera que a target de “paciente.senha” seja a classe paciente. Ele também considera que a target de “paciente.nome” seja a classe paciente. Depois ele tenta garantir que não existam 2 parâmetros pra mesma target (e nesse caso há). E aí dá IllegalStateException que vai ser capturado.

Tem algum motivo especial pra isso ser tratado dessa forma ou é um bug mesmo?

Agora, agora eu estou meio sem tempo, mas assim que der um tempinho posso criar o teste e a correção sim. Enquanto isso vale a pena cadastrar a issue ou não precisaria?