sexta-feira, 18 de novembro de 2011

REST - Negociação de Conteúdo HTTP / Content Negotiation

Ultimamente tenho trabalhado em um projeto com arquitetura REST e um dos requisitos, é que consigamos integrar e atender de maneira flexível o maior número possível de clients, portanto fornecemos os recursos com diferentes tipos de formatos (media types).

Basicamente o client solicita o recurso e informa em qual formato deseja receber a representação, ou seja, ele negocia o conteúdo com o servidor. Para isso o client fará uma requisição GET e colocará no request header o atributo Accept com o valor desejado: application/xml, application/json e etc...

Nosso recurso irá fornecer a representação de um funcionário e além dos formatos como xml e json, neste post também faremos uma implementação que representa o funcionário através de uma imagem.

Caso o cliente consiga trabalhar com mais de um media type, ele pode informar a ordem de prioridade, colocando o media type preferido primeiro, precedido dos menos importantes, separados por vírgula

Ex: Accept:  application/xml, application/json

Cabe esclarecer que caso o client solicite o recurso em um media type que o servidor não fornece, deverá ser retornada uma resposta com o código 406 Not Acceptable.

Para a implementação proposta, utilizaremos JAX-RS através de sua implementação padrão Jersey.

@Path("/funcionarios")
public class FuncionarioResource {
 @GET
 @Path("{id}")
 @Produces({ "application/xml", "application/json" })
 public Funcionario getFuncionario(@PathParam("id") Integer id) {
  Funcionario funcionario = repository.get(id);
  return funcionario;
 }
}
Serei breve nas explicações das anotações básicas tendo em vista que existem excelentes materiais na web e focarei no necessário para a negociação de conteúdo.

A anotação @Path("/funcionarios") informa a url da nossa RootResource.

Em seguida anotamos nosso método getFuncionario com a anotação @GET que informa que este método aceita apenas requisições feitas pelo método GET do http, isso quer dizer que nosso método é idempotente, não importa quantas vezes a requisição seja feita, ela não alterará o estado do recurso.

Também anotamos o método com @Path("{id}"), informando que devemos passar o id do funcionário na URI.  Para que consigamos acessar o recurso devemos usar o caminho funcionarios/1 por exemplo.

Como parâmetro o método espera o id do funcionário. Anotamos esse parâmetro com @PathParam("id") para que seja injetado na nossa variável id o valor passado pela URI conforme indicado na anotação @Path("{id}").

Para o assunto tratado a mais importante é a anotação @Produces({"application/xml", "application/json"}), ela informa que este método retorna o nosso recurso tanto em XML quanto JSON dependendo apenas da solicitação do cliente. A implementação JERSEY se responsabiliza em parsear o  recurso para estes formatos então precisamos apenas retornar nosso recurso:  Funcionario funcionario = repository.get(id);

Agora vamos expor nosso recurso como uma imagem. Para isso, precisamos fazer nossa própria implementação, que é transformar nosso recurso em uma imagem. Neste caso vamos usar o atributo caminhoFoto para buscar a foto e retorná-la como um array de bytes.


 @GET
 @Path("{id}")
 @Produces({ "image/jpeg", "image/jpg" })
 public byte[] getPhoto(@PathParam("id") Integer id) {
  Funcionario funcionario = repository.get(1);
  
  try {
   InputStream is = this.getClass().getResourceAsStream(funcionario.getCaminhoFoto());

   BufferedImage img = ImageIO.read(is);

   ByteArrayOutputStream bao = new ByteArrayOutputStream();

   ImageIO.write(img, "jpg", bao);

   return bao.toByteArray();

  } catch (IOException e) {
   throw new RuntimeException(e);
  }
 }

Note que são usadas as mesmas anotações no método getFuncionario e no getFoto, inclusive o @Path é o mesmo. A única diferença é que o método getFoto retorna media types do tipo "image/jpeg" ou "image/jpg".

Como o valor da anotação @Path é o mesmo, oque determina qual método atenderá a requisição é justamente o tipo de media type aceito pelo client.

Caso ele envie uma requisição com Accept: application/xml ou application/json o método getFuncionario será invocado, ou então, caso a requisição possua Accept: image/jpeg ou image/jpg, o método getFoto será invocado.

No meu github tem a implementação completa do projeto que pode ser publicado em qualquer servlet container, desenvolvido com maven.

Espero que o post possa ser útil.

Abrasssss