VRaptor 3 Converters - estou usando errado ou esbarrei em alguma limitação?

Olá, tenho uma dúvida avançada sobre o VRaptor. Não sei ao certo se estamos esbarrando em uma limitação dele ou se apenas o estamos usando de forma errada.
Aqui vai a controller inicial:[code]package br.com.teste;

// Vários imports

@Resource
@RequestScoped
public class UsuarioController {

private final Result result;
private final UsuarioDao dao;

public UsuarioController(Result result, UsuarioDao dao) {
    this.result = result;
    this.dao = dao;
}

@Get
@Path("/meuprojeto/{login}/perfil")
public void mostrarPerfil(String login) {
    Usuario u = dao.procurarPorLogin(login);
    // Várias coisas que faço com o usuário e/ou com o VRaptor.
}

@Post
@Path("/meuprojeto/urlqualquer/{login}")
public void outroMetodo(String login) {
    Usuario u = dao.procurarPorLogin(login);
    // Várias coisas que faço com o usuário e/ou com o VRaptor.        
    result.use(Results.logic()).redirectTo(UsuarioController.class).list(login);
}

// Outros métodos

}[/code]Isso funciona perfeitamente. No entanto não estamos satisfeitos e gostaríamos que os parâmetros fossem do tipo Usuario. Lá vai a primeira tentativa:[code]
@Get
@Path("/meuprojeto/{usuario.login}/perfil")
public void mostrarPerfil(Usuario usuario) {
// Várias coisas que faço com o usuário e/ou com o VRaptor.
}

@Post
@Path("/meuprojeto/urlqualquer/{usuario.login}")
public void outroMetodo(Usuario usuario) {
    // Várias coisas que faço com o usuário e/ou com o VRaptor.
    result.use(Results.logic()).redirectTo(UsuarioController.class).list(usuario);
}[/code]Isto falha miseravelmente. O VRaptor não pesquisa a entidade no banco de dados, o que ele faz é instanciar o Usuario e chamar os setters.

Esta outra tentativa é mais feliz:

[code]
@Get
@Path("/meuprojeto/{usuario}/perfil")
public void mostrarPerfil(Usuario usuario) {
// Várias coisas que faço com o usuário e/ou com o VRaptor.
}

@Post
@Path("/meuprojeto/urlqualquer/{usuario}")
public void outroMetodo(Usuario usuario) {
    // Várias coisas que faço com o usuário e/ou com o VRaptor.
    result.use(Results.logic()).redirectTo(UsuarioController.class).list(usuario);
}[/code]E para que o VRaptor saiba como instanciar o usuário, usamos isso:[code]package br.com.teste;

// Vários imports

@ApplicationScoped
@Convert(Usuario.class)
public class UsuarioConverter implements Converter<Usuario> {

private final UsuarioDao dao;

public UsuarioConverter(UsuarioDao dao) {
    this.dao = dao;
}

public Usuario convert(String value, Class&lt;? extends Usuario&gt; type, ResourceBundle bundle) {
    return dao.procurarPorLogin(value);
}

}[/code]E então, até aqui, a controller funciona como esperado.

No entanto, quando eu uso o metodo outroMetodo, que tem um redirectTo, o VRaptor gera umas URLs assim:
/meuprojeto/br.com.teste.Usuario@45f7e2/perfil

Apesar de a conversão de ida String->Usuario ter usado o Converter, a conversão de volta Usuario->String sempre usa o método toString().
Utilizar o método toString() sempre é muito restritivo, pois ele já é utilizado para outras finalidades.

Para contornar este problema, existem algumas soluções possíveis. Mas todas elas são meio gambiarrosas.

A primeira delas é criar um Router herdando de DefaultRouter e sobreescrever o método urlFor por um quase idêntico, mas com uma diferença:return matches.next().urlFor(type, method, creator.instanceWithParameters(resourceMethod, params));É substituído por:String toReturn = matches.next().urlFor(type, method, creator.instanceWithParameters(resourceMethod, params)); for (Object param : params) { if (param instanceof Usuario) toReturn = toReturn.replace(param.toString(), ((Usuario) param).getLogin()); } return toReturn; Este código é uma gambiarra grotesca, que consiste em consertar a URL errada que foi gerada.

Para tentar fazer um código menos gambiarroso, analisamos que a variável matches é do tipo Iterator<Route> e a implementação concreta de Route é FixedMethodStrategy.
O Iterator<Route> é criado pela classe PathAnnotationRoutesParser, que por sua vez utiliza a RouteBuilder. RouteBuilder instancia a FixedMethodStrategy, injetando nela uma instância de DefaultParametersControl.
O método fillUri(Object) de DefaultParametersControl utiliza o método toString() e é aí que é a origem do problema.
A classe PathAnnotationRoutesParser está fortemente acoplada a RouteBuilder, que por sua vez está fortemente acoplada a FixedMethodStrategy e a DefaultParametersControl, o que significa que substituir a implementação de alguma destas classes por outra coisa é algo difícil.

Uma outra possibilidade mais atraente para resolver isso sem precisar desta gambiarra seria criar uma espécie de decorator de Route e fazer o matcher conter instâncias apenas do decorator. Mas isso continua sendo algo trabalhoso.

Então vem a minha pergunta: Estamos usando o VRaptor de uma forma inadequada ou não prevista? Caso seja inadequada, eu devo me contentar em receber apenas Strings e números como parâmetros extraídos da URL e devo deixar coisas mais complexas apenas para dados de formulários? Caso contrário, gostaria de sugerir uma nova interface para um “Converter reverso”, e então o meu converter ficaria assim:[code]package br.com.teste;

// Vários imports

@ApplicationScoped
@Convert(Usuario.class)
public class UsuarioConverter implements Converter<Usuario>, ReverseConverter<Usuario> {

private final UsuarioDao dao;

public UsuarioConverter(UsuarioDao dao) {
    this.dao = dao;
}

public Usuario convert(String value, Class&lt;? extends Usuario&gt; type, ResourceBundle bundle) {
    return dao.procurarPorLogin(value);
}

public String reverseConvert(Usuario usuario, ResourceBundle bundle) {
    return usuario.getLogin();
}

}[/code]Essa interface hipotética ReverseConverter resolveria este tipo de problema.

Cara, eu não tenho muita experiência com VRaptor mas, até onde eu me lembro, ele não requer conversão de objetos para
Strings (como JSF) posto que você mesmo renderiza a página usando JSP.

<a href="<c:url value="/usuario/#{usuario.login}/perfil" />">Meu Perfil</a>

O código ai de cima não funciona?

Esquece isso. O que eu falei não tem nada a ver!!

Olá Armadilha,

Como vc explicou muito bem o seu problema eu vou ser mais curto, leia http://vraptor.caelum.com.br/documentacao/resources-rest/

OBS:

  • Não ha necessidade de colocar no @Path o contexto da aplicação.
  • O vraptor não sabe que existe banco de dados, e o comportamento por padrão dele é instaciar o objeto e popular e te entregar pronto no controller.

@victorwss
isso de usuario.login só usar os setters é o comportamento esperado do VRaptor mesmo. Ele não tem como saber qual é o seu banco de dados ou como vc quer buscar o Usuario no banco

De qqer forma achei muito interessante a sua abordagem de converters! Não tinha pensado nessa possibilidade, e acho que vale a pena fazer isso funcionar e escrever na documentação do VRaptor uma receita (http://vraptor.caelum.com.br/cookbook)

detalhes:

  • o seu converter deveria ser request scoped se o seu dao for request scoped (escopo padrão).

  • concordo que o acoplamento das classes PathAnnotationRoutesParser, RoutesBuilder e ParametersControl está bastante alta… criei uma issue para refatoração: http://github.com/caelum/vraptor/issues/issue/239

  • vcs não estão usando errado, é só uma forma não prevista mesmo…

de qqer forma, vou tentar criar essa interface ReverseConverter e fazer ela funcionar, e já te mando um snapshot pra vc testar =)

Muito obrigado pelo Hack :wink:

Mas hein?! :shock:

Eu também gostei, e nunca tinha pensado nessa prossibilidade. Porém acho que ao invés de ReverseConverter poderiamos alterar o atual converter para ser bidirecional, estilo o XmlAdapter do JAXB.

to criando um TwoWayConverter extends Converter

criado o snapshot =)

https://oss.sonatype.org/content/repositories/snapshots/br/com/caelum/vraptor/3.2.0-SNAPSHOT/vraptor-3.2.0-20100901.225614-2.jar

o nome da interface é a TwoWayConverter (que estende Converter), pode testar pra mim por favor @victorwss ?

[quote=pbnf]

  • O vraptor não sabe que existe banco de dados, e o comportamento por padrão dele é instaciar o objeto e popular e te entregar pronto no controller.
    [/quote][quote=Lucas Cavalcanti]
    isso de usuario.login só usar os setters é o comportamento esperado do VRaptor mesmo. Ele não tem como saber qual é o seu banco de dados ou como vc quer buscar o Usuario no banco
    [/quote][quote=garcia-jj]Mas hein?! :shock: [/quote]
    Eu sei que o VRaptor não conhece o banco e que aquilo não daria certo nunca. Acho que não me expressei direito, o que eu queria dizer é que a primeira ideia ingênua para resolver é aquela, mas ela falha pelo motivo óbvio que todos aqui sabem: O VRaptor não conhece o banco de dados e mesmo se conhecesse ele não teria informação suficiente para fazer aquilo, a semântica que ele usa é sempre instanciar o bean e chamar os setters e nunca fazer uma busca em algum lugar por um bean já existente. E é este um dos motivos pelo qual usamos os converters.

Obrigado. Você descobriu um bug obscuro na nossa aplicação que dificilmente iríamos perceber.

Obrigado. Vou brincar com ele um pouco e depois eu digo o que aconteceu.

Bem, uma observação que tenho que fazer, para que vocês fiquem cientes e coloquem no cookbook, é que se o bean for acessado na URL por duas ou mais formas diferentes de acordo com casos específicos (ex: em um lugar eu uso o id do usuário, em outro lugar o login, e em outro o CPF), é responsabilidade do converter lidar com isso, o que tende a ser bem complicado, e por este motivo deve ser fortemente recomendado que o acesso a um determinado bean na URL seja uniforme. Com algum tipo de converter mais complexo seria mais fácil suportar este caso, mas acho que não vale a pena. De qualquer forma gostaria que pensassem um pouco neste aspecto.

Pbnf, sei que te conheço pessoalmente, mas pelo seu nick não dá para saber quem você é. Poderia me dizer o teu nome real?

Obrigado a todos :slight_smile:

victor, só um adendo:
qdo vc coloca @Path("/users/{user.id}") o VRaptor sabe que o id é número, por exemplo, e só deixa colocar números nessa parte da url. Se vc coloca @Path("/users/{user}") o VRaptor vai aceitar qqer coisa.

Então vc teria que falar que só pode aceitar números: @Path("/users/{user:\d+}")…

Você quer escrever o cookbook, já que a idéia é sua? A gente coloca com o seu nome no site do VRaptor.

[]'s

Oi Victor! Foi uma excelente ideia. Valeu Lucas pelas implementacao para permitir isso. Parabens aos dois.

Se não me engano, por um acaso trabalho com o victorwss, e foi com o código que ele também fez que eu to aprendendo VRaptor (além da documentação do site né, mas aprender por exemplo auxilia bastante).
De início eu achava que o VRaptor chamava o construtor correto. Será que não dá pra fazer isso sem utilizar um converter?

Pensei no próprio VRaptor identificando um campo numérico, por exemplo, e chamando um construtor.

Por exemplo, eu chamo

@Path("/url/legal/com/${meu_parametro}") public void meu_metodo(MinhaClasse meu_parametro){ //meu_parametro = new MinhaClasse(meu_parametro_linha); seria executado pelo VRaptor por baixo dos panos //sendo que meu_parametro_linha é o meu_parametro atual, ou seja, String, e o VRaptor identificaria se é String mesmo ou inteiro/long e chamaria o construtor adequado //talvez possamos usar uma nova anotação para dizer qual construtor usar? }

Outra coisa que podia rolar é usar um ${parametro#tipo}, de forma que o valor dessa variável seria usado na chamada do construtor em “new MinhaClasse(tipo);”.

Não sei se fui claro, qualquer coisa eu tento explicar de outra forma.

tem como fazer isso habilitando o IOGI no VRaptor…

assim se vc tiver:

public class MinhaClasse {
    public MinhaClasse(Long id) {...}
}

e fizer

@Path("/url/legal/{minhaClasse.id}")
public void meuMetodo(MinhaClasse minhaClasse) {...}

ele vai instanciar usando o construtor, com o tipo certo (só vai aceitar numeros).

para habilitar o IOGI, coloque no web.xml:

<context-param>
        	<param-name>br.com.caelum.vraptor.packages</param-name>
	        <param-value>br.com.caelum.vraptor.http.iogi</param-value>
    </context-param>

e coloque o jar do iogi no WEB-INF/lib. Pode tirar o ognl do lib se quiser depois de fazer isso.

mais info: http://vraptor.caelum.com.br/documentacao/componentes-utilitarios-opcionais/

o problema do victor não era instanciar pelo construtor, e sim já carregar do banco.

Como minha aplicação é simples, eu implementaria essa busca do banco direto no construtor do objeto, passa o id dele, chamo o dao e copio os campos objeto que o dao retorno pra instância.

Lucas e Paulo, testei o TwoWayConverter e funcionou perfeitamente. :smiley:

Posso sim escrever o cookbook. Mas não entendi essa de colocar regex na URL. Poderia explicar melhor? Eu pessoalmente preferiria colocar este tipo de validação no Converter.

eh que é assim, suponha que user.id é um número

se o path eh @Path("/users/{user.id}"), a URI /users/banana dá 404 direto, nem passa por nada…

se vc colocar @Path("/users/{user}") tudo vai passar pelo converter

se vc colocar @Path("/users/{user:\d+}") só vai bater URIs que sejam números /users/234, /users/1, etc… se for /users/asdf é 404 direto tb…

o que vc deveria fazer tb é: se o usuario com o id passado não existir no banco, dar 404 direto. Então a gente precisa ver um jeito fácil de fazer isso. Alguma sugestão?

[quote=Lucas Cavalcanti]eh que é assim, suponha que user.id é um número

se o path eh @Path("/users/{user.id}"), a URI /users/banana dá 404 direto, nem passa por nada…

se vc colocar @Path("/users/{user}") tudo vai passar pelo converter

se vc colocar @Path("/users/{user:\d+}") só vai bater URIs que sejam números /users/234, /users/1, etc… se for /users/asdf é 404 direto tb…

o que vc deveria fazer tb é: se o usuario com o id passado não existir no banco, dar 404 direto. Então a gente precisa ver um jeito fácil de fazer isso. Alguma sugestão?[/quote]

O que estou fazendo por enquanto é lançar um ConversionError. Uma possibilidade seria lançar um outro tipo de exceção ou sobrecarregar o construtor de ConversionError com um int que represente o status HTTP.