Compartilhando objetos com a view
Para registrar objetos a serem acessados na view, usamos o método include:
@Controller
class ClientController {
@Inject
private Result result;
public void busca(int id) {
result.include("mensagem", "Alguma mensagem");
result.include("cliente", new Cliente(id));
}
}
Agora as variáveis mensagem
e cliente
estão disponíveis para uso em seu template engine. É possível registrar o objeto por meio da invocação do método include com um único argumento:
@Controller
class ClientController {
@Inject
private Result result;
public void busca(int id) {
result.include("Alguma mensagem").include(new Cliente(id));
}
}
Nesse caso, a primeira invocação registra a chave string
e a segunda, a chave cliente
. Você pode alterar o comportamento de convenção de chaves no seu próprio TypeNameExtractor
.
Custom PathResolver
Por padrão, para renderizar suas views, o VRaptor segue a convenção:
public class ClientsController {
public void list() {
//...
}
}
Este método acima renderizará a view /WEB-INF/jsp/clients/list.jsp
. No entanto, nem sempre queremos esse comportamento, e precisamos usar algum template engine, como por exemplo, Freemarker ou Velocity, e precisamos mudar essa convenção. Um jeito fácil de mudar essa convenção é estendendo a classe DefaultPathResolver
:
@Specializes
public class FreemarkerPathResolver extends DefaultPathResolver {
protected String getPrefix() {
return "/WEB-INF/freemarker/";
}
protected String getExtension() {
return "ftl";
}
}
Desse jeito, a lógica irá renderizar a view /WEB-INF/freemarker/clients/list.ftl
. Se, ainda assim, isso não for o suficiente, você pode implementar a interface PathResolver
e fazer qualquer convenção que você queira, não esquecendo de anotar a classe com @Specializes
.
View
Se você quiser mudar a view de alguma lógica específica, você pode usar o objeto Result
:
@Controller
public class ClientsController {
@Inject
private Result result;
public void list() { ... }
public void save(Client client) {
//...
result.use(Results.logic()).redirectTo(ClientsController.class).list();
}
}
Por padrão, existem estes tipos de views implementadas:
Results.logic() | que vai redirecionar para uma outra lógica qualquer do sistema |
Results.page() | que vai redirecionar diretamente para uma página, podendo ser um JSP, um HTML, ou qualquer URI relativa ao web application dir, ou ao contexto da aplicação. |
Results.http() | que manda informações do protocolo HTTP como status codes e headers. |
Results.status() | manda status codes com mais informações. |
Results.referer() | que usa o header Referer para fazer redirects ou forwards. |
Results.nothing() | apenas retorna o código de sucesso (HTTP 200 OK). |
Results.xml() | serializa objetos em XML. |
Results.json() | serializa objetos em JSON. |
Results.representation() | serializa objetos em um formato determinado pela requisição (parâmetro _format ou header Accept) |
Atalhos no Result
Alguns redirecionamentos são bastante utilizados, então foram criados atalhos para eles. Os atalhos disponíveis são:
result.forwardTo(“/some/uri”) | result.use(page()).forward(“/some/uri”); |
result.redirectTo(“/some/uri”) | result.use(page()).redirect(“/some/uri) |
result.permanentlyRedirectTo(“/some/uri”) | result.use(status()).movedPermanentlyTo(“/some/uri”); |
result.forwardTo(ClientController.class).list() | result.use(logic()).forwardTo(ClientController.class).list(); |
result.redirectTo(ClientController.class).list() | result.use(logic()).redirectTo(ClientController.class).list(); |
result.of(ClientController.class).list() | result.use(page()).of(ClientController.class).list(); |
result.permanentlyRedirectTo(Controller.class) | use(status()).movedPermanentlyTo(Controller.class); |
result.notFound() | use(status()).notFound() |
result.nothing() | use(nothing()); |
Além disso, se o redirecionamento é para um método do mesmo Controller, podemos usar:
result.forwardTo(this).list() | result.use(logic()).forwardTo(this.getClass()).list(); |
result.redirectTo(this).list() | result.use(logic()).redirectTo(this.getClass()).list(); |
result.of(this).list() | result.use(page()).of(this.getClass()).list(); |
result.permanentlyRedirectTo(this) | use(status()).movedPermanentlyTo(this.getClass()); |
Redirecionamento e forward
No VRaptor, podemos tanto realizar um redirect ou um forward do usuário para uma outra lógica ou um JSP. Apesar de serem conceitos da API de Servlets, vale a pena relembrar a diferença: o redirecionamento acontece no lado do cliente, através de códigos HTTP que farão o browser acessar uma nova URL; já o forward acontece no lado do servidor, totalmente transparente para o cliente/browser.
Um bom exemplo de uso do redirect é no chamado ‘redirect-after-post’. Por exemplo: quando você adiciona um cliente e que, após o formulário submetido, o cliente seja retornado para a página de listagem de clientes. Fazendo isso com redirect, impedimos que o usuário atualize a página (F5) e reenvie toda a requisição, acarretando em dados duplicados.
No caso do forward, um exemplo de uso é quando você possui uma validação e essa validação falhou, geralmente você quer que o usuário continue na mesma tela do formulário com os dados da requisição preenchidos, mas internamente você vai fazer o forward para outra lógica de negócios (a que prepara os dados necessários para o formulário).
Escopo Flash automático
Se você adicionar objetos no Result
e fizer um Redirect, esses objetos estarão disponíveis na próxima requisição.
public void adiciona(Cliente cliente) {
dao.adiciona(cliente);
result.include("mensagem", "Cliente adicionado com sucesso");
result.redirectTo(ClientesController.class).lista();
}
lista.jsp:
<div id="mensagem">
<h3>${mensagem}</h3>
</div>
Accepts e o parâmetro _format
Muitas vezes, precisamos renderizar formatos diferentes para uma mesma lógica. Por exemplo queremos retornar um JSON, em vez de um HTML. Para fazer isso, podemos definir o Header Accepts da requisição para que aceite o tipo desejado, ou colocar um parâmetro _format
na requisição.
Se o formato for JSON, a view renderizada por padrão será: /WEB-INF/jsp/{controller}/{logic}.json.jsp
, ou seja, em geral será renderizada a view: /WEB-INF/jsp/{controller}/{logic}.{formato}.jsp
. Se o formato for HTML você não precisa colocá-lo no nome do arquivo. O parâmetro _format
tem prioridade sobre o header Accepts.
Ajax: construindo na view
Para devolver um JSON na sua view, basta que sua lógica disponibilize o objeto para a view, e dentro da view você forme o JSON como desejar. Como no exemplo, o seu /WEB-INF/jsp/clients/load.json.jsp
:
{ nome: '${client.name}', id: '${client.id}' }
E na lógica:
@Controller
public class ClientsController {
@Inject
private Result result;
@Inject
private ClientDao dao;
public void load(Client client) {
result.include("client", dao.load(client));
}
}
Ajax: Versão programática
Se você quiser que o VRaptor serialize automaticamente seus objetos para XML ou JSON, você pode escrever em sua lógica:
import static br.com.caelum.vraptor.view.Results.*;
@Controller
public class ClientsController {
@Inject
private Result result;
@Inject
private ClientDao dao;
public void loadJson(Cliente cliente) {
result.use(json()).from(cliente).serialize();
}
public void loadXml(Cliente cliente) {
result.use(xml()).from(cliente).serialize();
}
}
Os resultados vão ser parecidos com:
{"cliente": {
"nome": "Joao"
}}
e em xml:
<cliente>
<nome>Joao</nome>
</cliente>
Por padrão, apenas campos de tipos primitivos serão serializados (String, números, enums, datas), se você quiser incluir um campo de tipo não primitivo você precisa incluí-lo explicitamente:
result.use(json()).from(cliente).include("endereco").serialize();
Vai resultar em algo parecido com:
{"cliente": {
"nome": "Joao",
"endereco" {
"rua": "Vergueiro"
}
}}
Você pode também excluir alguns da serialização:
result.use(json()).from(usuario).exclude("senha").serialize();
Vai resultar em algo parecido com:
{"usuario": {
"nome": "Joao",
"login": "joao"
}}
Você também pode excluir ou incluir vários campos.
result.use(json()).from(usuario).recursive().serialize(); // inclui todos os campos recursivamente
result.use(xml()).from(usuario).exclude("email").serialize(); // excluir o campo email
result.use(xml()).from(usuario).excludeAll().serialize(); // exclui todos os campos
Por padrão atributos com valores null
não são serializados no json, é possível modificar este comportamento chamando o método serializeNulls
: ~~~ #!java result.use(json()).serializeNulls().from(new Pessoa(“João”, null)).serialize(); ~~~
O JSON resultante será o seguinte: ~~~ #!javascript {“pessoa”:{ “nome”: “João”, “idade”: null }} ~~~
Você pode adicionar versionamento aos seus resultados baseados em JSON. Através desse recurso é possível versionar uma API. Para exemplificar a utilidade do versionamento, suponha que foi criada uma API que retorna clientes de um determinado sistema. A primeira versão, retorna clientes definidos através da seguinte estrutura:
public class Cliente {
private String nome;
private String ramo;
// getters e setters omitidos
}
Agora suponha que você criou um método no seu controlador que possui o seguinte resultado:
@Get{"/clientes"}
public void clientes() {
List<Cliente> clientes = repositorioDeClientes.getAll();
result.use(json()).from(clientes).serialize();
}
Até agora não há problema algum. Porém, caso seja necessário disponibilizar um novo atributo como resultado que só fará sentido para uma nova versão da API, como poderíamos contornar a situação? Poderiamos criar uma segunda classe chamada ClienteV2 com o novo atributo e o método do controlador que retorna os clientes poderia ser reescrito conforme abaixo:
@Get{"/{versaoParam}/clientes"}
public void clientes(String versaoParam) {
double versao = Double.parseDouble(versaoParam);
if (versao == 2.0) {
result.use(json()).from(repositorioDeClientes.getAll()).serialize();
} else {
result.use(json()).from(repositorioDeClientes.getAllFromV2()).serialize();
}
}
Certamente a solução acima não é nada boa. Como o VRaptor utiliza a biblioteca GSON (criada pelo Google), podemos utilizar a mesma classe Cliente utilizando a anotação @Since nos atributos que queremos versionar. Essa anotação indica que um determinado atributo está disponível apartir de uma versão x conforme o exemplo abaixo:
public class Cliente {
private String nome;
private String ramo;
@Since(2.0)
private Calendar dataDeRegistro;
// getters e setters omitidos
}
Dessa forma o método do controlador agora pode ser reescrito da seguinte forma:
@Get{"/{versaoParam}/clientes"}
public void clientes(String versaoParam) {
double versao = Double.parseDouble(versaoParam);
List<Cliente> clientes = repositorioDeClientes.getAll();
result.use(json()).version(versao).from(clientes).serialize();
}
Pronto, agora os clientes de nossa API, podem solicitar um resultado com uma versão específica.
A implementação para serialização de XML padrão é baseada no XStream, então é possível configurar a serialização por anotações ou configurações diretas ao XStream, bastando criar a classe:
@Specializes
public class CustomXStreamBuilder extends XStreamBuilderImpl {
@Override
protected XStream xmlInstance() {
XStream xStream = super.xmlInstance();
//suas configurações ao XStream aqui
return xStream;
}
}
Para configurar a serialização com JSON você pode criar uma classe que estenda de GsonJSONSerialization
e sobrescrever o método getSerializer()
com as suas configurações.
Na serialização JSON você pode utilizar a anotação SkipSerialization
para omitir a serialização de um campo ou de uma classe.
A classe abaixo foi anotada com SkipSerialization
, portanto os seus campos nunca serão serializados para JSON:
@SkipSerialization
public class UserPrivateInfo {
private String cpf;
private String telefone;
private String endereco;
// getters e setters
}
Nesta outra classe, o campo senha não será serializado. O campo info também não será serializado pois a classe UserPrivateInfo
foi anotado com SkipSerialization
:
public class User {
private String nome;
private String login;
@SkipSerialization
private String senha;
private UserPrivateInfo info;
// getters e setters
}
Realizando a seguinte chamada:
User user = ...;
result.use(json()).withoutRoot().from(user).recursive().serialize();
O resultado obtido é semelhante à:
{
"nome": "Joao",
"login": "joao"
}
Perceba que o campo senha e todo o UserPrivateInfo
não foram serializados.
Serializando Collections
Ao serializar coleções, o padrão é colocar a tag list
em volta dos elementos:
List<Cliente> clientes = ...;
result.use(json()).from(clientes).serialize();
//ou
result.use(xml()).from(clientes).serialize();
Vai resultar em algo como:
{"list": [
{ "nome": "Joao" },
{ "nome": "Maria" }
]}
ou
<list>
<cliente>
<nome>Joao</nome>
</cliente>
<cliente>
<nome>Maria</nome>
</cliente>
</list>
É possível personalizar o elemento de fora usando o método:
List<Cliente> clientes = ...;
result.use(json()).from(clientes, "clientes").serialize();
//ou
result.use(xml()).from(clientes, "clientes").serialize();
Vai resultar em algo como:
{"clientes": [
{"nome": "Joao"},
{"nome": "Maria"}
]}
ou
<clientes>
<cliente>
<nome>Joao</nome>
</cliente>
<cliente>
<nome>Maria</nome>
</cliente>
</clientes>
Os includes e excludes funcionam como se você os estivesse aplicando num elemento de dentro da lista. Por exemplo se você quiser incluir o endereço no cliente:
List<Cliente> clientes = ...;
result.use(json()).from(clientes).include("endereco").serialize();
Com resultado:
{"list": [
{
"nome": "Joao",
"endereco": { "rua": "Vergueiro, 3185" }
},
{
"nome": "Maria",
"endereco": { "rua": "Vergueiro, 3185" }
}
]}
Serializando JSON sem elemento raiz
Se você quiser serializar um objeto em JSON sem dar nomes a eles, pode fazer isso com o método withoutRoot:
result.use(json()).from(carro).serialize(); //=> {'carro': {'cor': 'azul'}}
result.use(json()).withoutRoot().from(carro).serialize(); //=> {'cor': 'azul'}
Indentação do resultado JSON e XML
Por padrão, o resultado da serialização é sem indentação, o que pode ser útil para melhorar o tempo de download do browser. Porém se você precisar exibir o resultado do JSON ou XML com indentação, você pode usar o método indented
.
result.use(json()).indented().from(carro).serialize();
Dessa forma o resultado seria:
{
'carro':
{
'cor': 'azul'
}
}
Ou você pode também usar o Environment para definir a indentação padrão conforme o ambiente. Neste caso você pode definir que no ambiente DEVELOPMENT
a indentação será ativada, e no ambiente PRODUCTION
será desativada.
development.properties
:
br.com.caelum.vraptor.serialization.xml.indented=true
br.com.caelum.vraptor.serialization.json.indented=true
production.properties
:
br.com.caelum.vraptor.serialization.xml.indented=false
br.com.caelum.vraptor.serialization.json.indented=false
Nota: A configuração do Environment
possui prioridade menor que no código. Ou seja, se você definir br.com.caelum.vraptor.serialization.xml.indented=false
no Environment
e chamar o método indented
no seu código, o resultado será um JSON ou XML indentado.
Criando links na view com o linkTo
LinkTo é uma funcionalidade que permite você criar links nas páginas JSP sem precisar escrever o link, mas sim indicando a classe do controller, e o método.
Para o seguinte controller:
public class ClientController {
@Path("/client/list")
public void list() {
// alguma lógica aqui
}
@Path("/client/{id}")
public void show(Long id) {
// alguma lógica aqui
}
}
Para chamar os métodos list
e show
respectivamente, teriamos que escrever no JSP:
<a href="/client/list">listar clientes</a>
<a href="/client/1">ver cliente 1</a>
Usando a funcionalidade do linkTo, podemos simplicar a chamada para o método list
:
<a href="${linkTo[ClientController].list()}">listar clientes</a>
Como o método não possui parâmetros, você também pode omitir os parênteses.
<a href="${linkTo[ClientController].list}">listar clientes</a>
Para chamar o método show
, podemos fazer a chamada desta forma:
<a href="${linkTo[ClientController].show(1)}">ver cliente 1</a>
Ao invés do parâmetro fixo, você pode passar variáveis do JSTL, como por exemplo:
<a href="${linkTo[ClientController].show(client.id)}">ver cliente ${client.name}</a>
Inclusão automática de parâmetros
Você pode anotar seu método com @IncludeParameters
para que todos os parâmetros de um método do seu controller sejam inclusos automaticamente na view
.
Então, no lugar de fazer algo como:
public void filtra(Intervalo intervalo, Representante representante,
Unidade unidade, BigDecimal valorMaximo, BigDecimal valorMin) {
// lógica de filtro
result.include("intervalo", intervalo);
result.include("representante", representante);
result.include("valorMaximo", valorMaximo);
result.include("valorMin", valorMin);
}
Agora você pode fazer:
@IncludeParameters
public void filtra(Intervalo intervalo, Representante representante,
Unidade unidade, BigDecimal valorMaximo, BigDecimal valorMin) {
// lógica de filtro
}
Todos os parâmetros estarão disponiveis pela jsp com EL: ${intervalo.inicio}