REST (REpresentational State Transfer)

Padrão arquitetural para acesso a informação

Aproximação: uma aplicação vista como uma coleção de recursos

  • Um recurso é identificado por um URI/URL;
  • Um URL devolve um documento com a representação do recurso;
  • Um URL pode referir uma coleção de recursos
  • Podem-se fazer referências a outros recursos usando ligações (links)

Protocolo REST

Protocolo cliente/servidor stateless baseado em HTTP.

  • Cada pedido contém toda a informação necessária para ser processado;
  • Objetivo: tornar o sistema simples e permitir (caching) transparente.

Interface Uniforme

  • Todos os recursos são acedidos por um conjunto de operações HTTP bem definidas:
    • POST - criar recurso;
    • GET - obter recurso;
    • PUT - atualizar/substituir recurso;
    • DELETE - remover recurso;

Programação em Java

O framework Jersey (JAX-RS API) permite a criação de serviços REST de forma simples usando a linguagem Java.

A abordagem consiste em adicionar anotações ao código Java (eg., @PATH, @GET, @POST, etc.).

Através da capacidade de reflexão do Java, o runtime Jersey (JAX-RS API), com base nessas anotações, produz automaticamente código servidor para as operações em questão.

Dependencias/Bibliotecas (JARS)

O framework Jersey (JAX-RS API) requer diversas bibliotecas (jars) para o seu funcionamento.

Num projecto Java, a gestão manual dessas dependências não é trivial. Uma solução consiste em utilizar uma ferramenta como o Maven.

O Maven será a solução adoptada nesta disciplina para o efeito. Será utilizada nos materiais fornecidos para apoio às aulas práticas e desenvolvimento do projeto.

Um projecto Maven tem como base um ficheiro pom.xml que, além das dependências do projeto, também pode especificar as fases de compilação, empacotamento, instalação, etc., dos artefactos do projeto.

Dependências Maven

In [1]:
%%classpath add mvn 
com.google.code.gson gson 2.2.4
org.glassfish.jersey.core jersey-common 2.25.1
org.glassfish.jersey.core jersey-client 2.25.1
org.glassfish.jersey.core jersey-server 2.25.1
org.glassfish.jersey.media jersey-media-json-jackson 2.25.1
org.glassfish.jersey.containers jersey-container-jdk-http 2.25.1
Added jars: [jersey-guava-2.25.1.jar, jackson-module-jaxb-annotations-2.8.4.jar, validation-api-1.1.0.Final.jar, javassist-3.20.0-GA.jar, jackson-jaxrs-base-2.8.4.jar, jersey-client-2.25.1.jar, hk2-api-2.5.0-b32.jar, javax.ws.rs-api-2.0.1.jar, jackson-annotations-2.8.4.jar, jackson-core-2.8.4.jar, jersey-media-jaxb-2.25.1.jar, jersey-entity-filtering-2.25.1.jar, aopalliance-repackaged-2.5.0-b32.jar, gson-2.2.4.jar, jackson-jaxrs-json-provider-2.8.4.jar, jackson-databind-2.8.4.jar, jersey-common-2.25.1.jar, osgi-resource-locator-1.0.1.jar, jersey-server-2.25.1.jar, hk2-utils-2.5.0-b32.jar, javax.inject-2.5.0-b32.jar, jersey-container-jdk-http-2.25.1.jar, javax.annotation-api-1.2.jar, jersey-media-json-jackson-2.25.1.jar, hk2-locator-2.5.0-b32.jar]

Exemplo

Pretende-se um serviço para armazenar dados, acessível através de uma interface REST.

As operações a disponibilizar são:

  • guardar dados;
  • descarregar dados;
  • substituir/alterar os dados;
  • apagar dados.

Cada unidade dos dados será um recurso distinto, acessível a partir de um URL, eg. http://<ip>:<port>/some-path/<id>

Interface (Service API)

A interface do serviço será modelada usando as anotações Jersey (JAX-RS API), por exemplo, numa interface Java.

In [ ]:
package api;

import javax.ws.rs.*;
import javax.ws.rs.core.*;

@Path("/some-path") 
public interface DataResources {
    
    @POST
    @Path("/") 
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_OCTET_STREAM)
    String store(byte[] data);
In [ ]:
@GET
    @Path("/{id}")
    @Produces(MediaType.APPLICATION_OCTET_STREAM)
    byte[] download(@PathParam("id") String id);

    @PUT
    @Path("/{id}")
    @Consumes(MediaType.APPLICATION_OCTET_STREAM)
    void replace(@PathParam("id") String id, byte[] contents);
    
    @DELETE
    @Path("/{id}")
    void delete(@PathParam("id") String id);    
}

Observações

  • As anotações Jersey (JAX-RS API) podem ainda ser colocadas numa classe abstrata ou na classe final que tem a implementação das operações;
  • O nome dos métodos é livre. (Obviamente, o nome do método deve ser coerente com a semântica da operação);
  • Podem-se repetir operações HTTP (eg. pode haver várias operações @GET, @POST, etc.), desde que o respetivo @Path permita a sua distinção.
  • A anotação @Path na interface é o prefixo comum a todas as operações.
  • Podem-se usar vários parâmetros no caminho, por exemplo, @Path("/{var1}/{var2}"), que serão capturados nos parâmetros dos métodos através da respetiva anotação @PathParam("varX").

Implementação

Haverá uma classe com as operações sobre os recursos indicadas na interface do serviço.

Além da lógica aplicacional inerente ao serviço, os métodos deverão incluir a deteção e sinalização de erros. Os erros são assinalados através respondendo ao pedido com um status code HTTP apropriado, (eg., 404 Not Found, quando se acede a um recurso não existe).

In [ ]:
package impl;

import java.util.*;
import java.util.concurrent.*;

import javax.ws.rs.*;
import javax.ws.rs.core.Response.*;

public class DataResources implements api.DataResources {
    
    private static final int ISIZE = 128;
    private final Map<String, byte[]> storage;

    public DataResources() {
        this.storage = new ConcurrentHashMap<>(ISIZE);
    }
    
    public String store(byte[] data) {
        String id = Long.toString( System.nanoTime(), 32);
        if( storage.putIfAbsent(id, data) != null)
            throw new WebApplicationException( Status.CONFLICT );
        else
            return id;
    }
    ...
In [ ]:
public byte[] download(String id) {
        byte[]  data = storage.get(id);
        if( data == null )
            throw new WebApplicationException( Status.NOT_FOUND );
        else
            return data;
    }

    public void replace(String id, byte[] data) {
        if( storage.replace( id, data ) == null ) {
            throw new WebApplicationException(Status.NOT_FOUND );            
        }
    }
    
    public void delete(String id) {
        if( storage.remove( id ) == null ) {
            throw new WebApplicationException(Status.NOT_FOUND );            
        }      
    }
}

Observações

A classe que implementa a interface não precisa de repetir as anotações.

O construtor pode receber parâmetros caso seja necessário.

Servidor

(Instanciar o serviço)

O serviço é instanciado registando um ou mais recursos num servidor HTTP.

In [ ]:
import java.net.*;
import org.glassfish.jersey.server.*;
import org.glassfish.jersey.jdkhttp.*;

String URI_BASE = "http://0.0.0.0:9999/v1/";

ResourceConfig config = new ResourceConfig();
config.register( new impl.DataResources() );

JdkHttpServerFactory.createHttpServer( URI.create(URI_BASE), config);

System.err.println("Server ready....");

Observações

  • Podem ser registados múltiplos recursos no mesmo servidor HTTP, desde que usem @Path distintos.
  • Em vez de um objecto, pode-se registar a classe que implementa o recurso, sendo criada, neste caso, automaticamente uma nova instancia a cada novo pedido.
  • O servidor HTTP é parametrizado através de um URI base.
    • O porto pode não ser standard (80).
    • O endereço IP pode ser específico (eg. 127.0.0.1) ou 0.0.0.0, que significa aceitar pedidos vindos de qualquer das interfaces da máquina.

Invocação (Cliente)

No Browser

As operações @GET podem ser invocadas simplesmente apontando um browser para o URI do recurso.

Para as demais operações, @POST, @PUT e @DELETE pode-se continuar a usar o browser instalando uma extensão, como a </> RESTed.

Programaticamente em Java

É possível invocar um serviço REST a partir de Java, por recurso à vertente cliente das bibliotecas Jersey (JAX-RS API), como nos exemplos seguintes...

Criar Recurso (POST)

In [ ]:
import java.net.URI;
import javax.ws.rs.core.*;
import javax.ws.rs.client.*;
import org.glassfish.jersey.client.*;

ClientConfig config = new ClientConfig();
Client client = ClientBuilder.newClient(config);

URI baseURI = UriBuilder.fromUri("http://localhost:9999/v1").build();
WebTarget target = client.target( baseURI );
        
Response response = target.path("/some-path/")
    .request()
    .post( Entity.entity( new byte[1024], MediaType.APPLICATION_OCTET_STREAM));
        
if( response.hasEntity() ) {
    String id = response.readEntity(String.class);
    System.out.println( "data resource id: " + id );
} else
    System.err.println( response.getStatus() );

Descarregar Recurso (GET)

In [ ]:
import java.net.URI;
import javax.ws.rs.core.*;
import javax.ws.rs.client.*;
import org.glassfish.jersey.client.*;

ClientConfig config = new ClientConfig();
Client client = ClientBuilder.newClient(config);

URI baseURI = UriBuilder.fromUri("http://localhost:9999/v1").build();
WebTarget target = client.target( baseURI );
        
Response response = target.path("/some-path/28s0pkung0")
    .request()
    .get();
        
if( response.hasEntity() ) {
    byte[] data = response.readEntity(byte[].class);
    System.out.println( "data resource length: " + data.length );
} else
    System.err.println( response.getStatus() );

Apagar Recurso (DELETE)

In [ ]:
import java.net.URI;
import javax.ws.rs.core.*;
import javax.ws.rs.client.*;
import javax.ws.rs.core.Response.*;
import org.glassfish.jersey.client.*;

ClientConfig config = new ClientConfig();
Client client = ClientBuilder.newClient(config);

URI baseURI = UriBuilder.fromUri("http://localhost:9999/v1").build();
WebTarget target = client.target( baseURI );
        
Response response = target.path("/some-path/2486eo2pl4")
    .request()
    .delete();
        
if( response.getStatusInfo() == Status.OK ) {
    System.out.println( "deleted data resource...");
} else
    System.err.println( response.getStatus() );

Alterar Recurso (PUT)

In [ ]:
import java.net.URI;
import javax.ws.rs.core.*;
import javax.ws.rs.client.*;
import javax.ws.rs.core.Response.*;
import org.glassfish.jersey.client.*;

ClientConfig config = new ClientConfig();
Client client = ClientBuilder.newClient(config);

URI baseURI = UriBuilder.fromUri("http://localhost:9999/v1").build();
WebTarget target = client.target( baseURI );
        
Response response = target.path("/some-path/24cusirnv9")
    .request()
    .put( Entity.entity( new byte[2048], MediaType.APPLICATION_OCTET_STREAM));
        
if( response.getStatusInfo() == Status.OK ) {
    System.out.println( "updated data resource...");
} else
    System.err.println( response.getStatus() );