19 June 2011

Antes de começar a solução, vamos usar uma rota mais fácil:

@Resource
public class ProdutoController {
    @Post("/produtos")
    public void adiciona(Produto produto) {...}
}

A idéia é conseguir fazer com que:

${linkTo[ProdutoController].adiciona}

retorne a URI /produtos.

Vamos, primeiro, olhar para a primeira parte e relaxar um pouquinho a sintaxe:

${linkTo['ProdutoController']}

Qual é a única forma disso funcionar? Na EL padrão você geralmente só pode navegar pelos getters, com três exceções: Lists, Arrays e Maps. Nas Lists e Arrays você pode acessar os índices (lista[0], array[1], etc), e nos mapas você pode acessar as chaves (mapa['chave']). Então pro código acima funcionar, podemos fazer o linkTo ser um mapa e adicioná-lo no request:

Map<String, ?> linkTo = Maps.newHashMap(); // do guava, para evitar declarar os generics de novo
linkTo.put("ProdutoController", objetoMagico);

request.setAttribute("linkTo", linkTo);

Agora só precisamos criar um objetoMagico em que eu possa chamar o .adiciona. Isso significa que precisamos de um objeto que tenha o método getAdiciona(). Até é possível gerar uma classe assim em tempo de compilação (com o APT) ou em runtime (com ASM ou javassist), uma classe para cada controller do sistema com getters para o nome de cada método. Mas isso é um pouco complicado demais (bem mais difícil que implementar a Tag ou o ELResolver que eu estava evitando), então vamos relaxar um pouco mais a sintaxe. Como fazer isso funcionar?

${linkTo['ProdutoController']['adiciona']}

Outro mapa!

Map<String, Map<String,String>> linkTo = Maps.newHashMap(); // viu como o guava é útil aqui? ;)

linkTo.put("ProdutoController", ImmutableMap.of("adiciona", "/produtos"));

request.setAttribute("linkTo", linkTo);

Usei aqui a classe ImmutableMap do guava, pro código ficar mais conciso. Isso cria um mapa imutável com uma única entrada (adiciona -> /produtos).

Agora o código ${linkTo['ProdutoController']['adiciona']} retorna o que a gente queria: /produtos.
O legal é que os colchetes são apenas um dos jeitos de acessar chaves de um mapa, não o único. No caso em que a chave é uma String, podemos simplificar a sintaxe:

${linkTo['ProdutoController'].adiciona} => /produtos

Já chegamos bem próximos da sintaxe proposta no começo do post, só falta retirar as aspas. Mas será que eu posso simplificar a sintaxe para ficar assim?

${linkTo[ProdutoController].adiciona} => /produtos (?)

Infelizmente não. Quando fazemos isso, o JSP procura por uma variável chamada ProdutoController. Bom, não seja por isso, basta fazer uma pequena gambiarra adaptação:

request.setAttribute("ProdutoController", "ProdutoController");

E pronto, o código que queríamos funciona!

${linkTo[ProdutoController].adiciona} => /produtos !

Agora é só gerar esses mapas automaticamente, um para cada controller do VRaptor. Mas antes de fazer isso, existe um problema: a solução até aqui não suporta todas as rotas possíveis no VRaptor.

E se tivéssemos:

@Resource
public class ProdutoController {
    @Get("/produtos/{id}")
    public Produto visualiza(Long id) {...}

    @Delete("/produtos/{produto.id}")
    public void remove(Produto produto) {...}
}

Como fazer para gerar as URIs /produtos/4, /produtos/15, etc ainda sem Tags ou ELResolvers?
Como fazer esse tipo de código funcionar?

<c:forEach items="${produtos}" var="produto">
   <a href="${linkTo[ProdutoController].visualiza ??? produto.id ???}">Visualiza</a>
   <a href="${linkTo[ProdutoController].remove ??? produto ???}">Remove</a>
</c:forEach>

E agora, como vocês fariam isso? No próximo post eu explico o resto da solução, com um pouco mais de magia negra e detalhes internos do VRaptor que podem te ajudar a customizar várias coisas de forma bem fácil.



  • - Sérgio Lopes - Mon, 20 Jun 2011 08:35:31 -0700
    E com a EL 2.2, não resolveria o problema mais facilmente? Digo, já dá pra invocar métodos na EL, passar params, varargs etc Abraços
  • - Arthur Carvalho - Mon, 20 Jun 2011 09:17:31 -0700
    Legal o lance do Guava. Na primeira linha que eu vi, por um momento pensei que era frescura. Mas se tratando em legibilidade do código, ainda valia apena. Pra declaração do segundo Map, com certeza o efeito que causa na vizualização do código é muito melhor. Vou ver mais sobre a API. Agora sobre as URIs com id's não consegui pensar em algo tão limpo quanto está fincando. Adicionaria as URIs no mapa sem a parte de {campo.id} e utilizaria o varStatus do c:forEach mesmo. Aguardo o próximo post! Valeu.
  • - Kenneth Reis - Mon, 20 Jun 2011 11:23:12 -0700
    Oi Lucas, eu solucionaria isso usando reflexão e uma function declarada numa TLD: taglib.tld 1.1 cf http://abc.com.br/abc link org.sistema.Utils java.lang.String link(java.lang.String) org.sistema.Utils package org.sistema; public class Utils{ public static String link(String actionPath) { Class klazz = null; String classNamePath = actionPath.substring(0, actionPath.lastIndexOf(".")); String methodName = actionPath.substring(actionPath.lastIndexOf(".") + 1); try { klazz = Class.forName(classNamePath); } catch (ClassNotFoundException ex) { Logger.getLogger(Util.class.getName()).log(Level.SEVERE, null, ex); } Method met = null; for (int i = 0; i < klazz.getMethods().length; ++i) if (klazz.getMethods()[i].getName().equals(methodName)) met = klazz.getMethods()[i]; return met.getAnnotation(Post.class).value()[0]; } } e para usar numa jsp: ${cf:link('com.meusistema.controllers.ProdutoController.adiciona')} Poderia ser usado em qualquer projeto, melhor ainda se encapsulado num .jar da vida! xD
  • - Kenneth Reis - Mon, 20 Jun 2011 11:34:22 -0700
    Eita, a formatação saiu errada, colei no pastebin taglib.tld http://pastebin.com/dEgsu0Kk org.sistema.Utils.java http://pastebin.com/9mGyyxeR Queria achar um jeito de usar só o nome da Classe na function: ${cf:link('ProdutoController.adiciona')}
  • - Lucas Cavalcanti - Mon, 20 Jun 2011 11:42:07 -0700
    a sua solução ainda não suporta os parâmetros ;) mas também é interessante. Você pode convencionar que todos os controllers estão no pacote com.meusistema.controllers, e usar a partir de lá. No próximo post eu mostro como saber quais são todos os controllers do sistema, então tudo fica mais fácil, a gente não precisa chutar.
  • - Lucas Cavalcanti - Mon, 20 Jun 2011 14:06:33 -0700
    Dá pra executar métodos, mas não dá pra forçar que eles retornem string. No caso em que os métodos do controller retornam void não tem muito o que fazer. []'s