Restlet

De Energía y Red

Contenido

Introducción

RESTLET es un API Java que sirve para crear y consumir servicios REST.

RESTLET permite crear arquitecturas usando un API propia y JAX-RS. El API propia sigue un diseño orientado a clases clásico en la que los principales conceptos REST y HTTP tienen una clase Java.

JAX-RS sigue un diseño más orientado a anotaciones, se promueve partir de POJOs y usar anotaciones para exponer un API REST, en teoría también se pueden anotar esos POJOs con JAXB para describir como deben serializar a XML o JSON.

Con REST existe la opción de anotar las clases recurso siguiendo los principios de JAX-RS o extender de la clase Resource (en determindas situaciones puede llegar a ser más potente).

Restlet porporciona:

  • Conectores para los protocolos (HTTP, SMTP, POP3, FILE, JDBC, etc.)
  • Mapeados de URI dinámicos y flexibles
  • Soporte para la creación de conectores
  • Autenticación y hosts virtuales.
  • Clases para servir directorios de archivos estáticos con negociación de contenidos automática y soporte para actualizaciones remotas.
  • Todo lo necesario para configurar un proxy a APIs REST externas.

Las instancias de la clase Resource son las manejadoras finales de las llamadas recibidas por los conectores del servidor. El recurso es el encargado de declarar los tipos de representaciones que soporta (instancias de la clase Variant) y de implementar los métodos REST sobre los que se quiere ofrecer servicio.

Cada llamada entrante es tratada por una instacia de recurso dedicada, el framework es multiproceso.

Restlet proporciona soporte WADL respondiendo una representación WADL cuando se solicita un recurso a través del método OPTIONS.

Modelo de Restlet

  • Reference - Mutable java.net.URI
  • Request - Petición de cliente
  • Response - Respuesta de un servidor
  • MediaType - tipo mime (p.e text/html, application/xml)
  • Resource - Entidad que puede ser referenciada, p.e. vídeo, documento ...
  • Variant - Metainformación sobre una representación
  • Representation - Valor/estado actual de un recurso.

Resource API

Método HTTP Método invocado en Resource
GET Representation represent(Variant)
POST void acceptRepresentation(Representation)
PUT void storeRepresentation(Representation)
DELETE void removeRepresentations()
HEAD Calls GET without stream
OPTIONS Updates Response’s getAllowedMethods

Aplicaciones y Componentes

// Create Application for http://localhost/helloWorld
Application application = new Application() {
    public synchronized Restlet createRoot() {
        Router router = new Router(getContext());
        // Attach a resource
        router.attach("helloWorld", HelloWorldResource.class);
        return router;
    }
};
// Create the component and attach the application
Component component = new Component();
component.getServers().add(Protocol.HTTP);
component.getDefaultHost().attach(application);
// Start the web server
component.start();

Datos y Recursos

public HelloWorldResource(Context context, Request request,
    Response response) {
    super(context, request, response);
    // Declare all kind of representations supported by the resource
    getVariants().add(new Variant(MediaType.TEXT_PLAIN));
}
// Respond to GET
public Representation represent(Variant variant) throws ResourceException {
    Representation representation = null;
    // Generate a representation according to the variant media type.
    if (MediaType.TEXT_PLAIN.equals(variant.getMediaType())) {
        representation = new StringRepresentation("hello, world",
            MediaType.TEXT_PLAIN);
    }
    return representation;
}

API Cliente

// GET a resource from uri as a XML DOM.
Response response = new Client(Protocol.HTTP).get(uri);
DomRepresentation document = response.getEntityAsDom();
// Specify content types.
ClientInfo clientInfo = request.getClientInfo();
List<Preference<MediaType>> preferenceList = new
    ArrayList<Preference<MediaType>>();
preferenceList.add(new Preference<MediaType>(TEXT_XML));
preferenceList.add(new Preference<MediaType>(APPLICATION_XML));
preferenceList.add(new Preference<MediaType>(APPLICATION_XHTML_XML));
preferenceList.add(new Preference<MediaType>(TEXT_HTML, 0.9f));
clientInfo.setAcceptedMediaTypes(preferenceList);
// Specify encodings
clientInfo.setAcceptedEncodings(Arrays.asList(new
    Preference<Encoding>(Encoding.GZIP));
// Finally, GET.
client.get(uri);

ServerResources

La extensión MediaType de los argumentos de las anotaciones @Get, @Post, @Put y @Delete se refiere a las representaciones devueltas no ha las recibidas de los clientes, de eso se tiene que encargar la aplicación.

ServerResource ofrece mucha flexibilidad en lo que se refiere al método de retorno del método Java, se puede devolver cualquier cosa que pueda ser convertida en Representation, una cadena (que se convierte de forma interna en un StringRepresentation), si no se quiere devolver nada (por ejemplo en un @Delete) se puede poner un void.

En modo anotación no se deben lanzar excepciones en los métodos manejadores si no la clase ServerResource se colapsa, la forma correcta de tratar un error consiste en llamar ServerResource.setStatus() y devolver null

La forma correcta de hacer una inserción consiste en hacer un Post y después un Get:

Extracto de la Status Code Definitions

10.3.4 303 See Other

The response to the request can be found under a different URI and SHOULD be retrieved using a GET method on that resource. 

This method exists primarily to allow the output of a POST-activated script to redirect the user agent to a selected resource. 

The new URI is not a substitute reference for the originally requested resource. 

The 303 response MUST NOT be cached, but the response to the second (redirected) request might be cacheable.

The different URI SHOULD be given by the Location field in the response. 

Unless the request method was HEAD, the entity of the response SHOULD contain a short hypertext note with a hyperlink to the new URI(s).

      Note: Many pre-HTTP/1.1 user agents do not understand the 303
      status. When interoperability with such clients is a concern, the
      302 status code may be used instead, since most user agents react
      to a 302 response as described here for 303.

Forma correcta de mapear peticiones PUT/POST:

  • PUT /cursos/{id}
  • POSTS /cursos

Filters

Los filtros sirven para transformar peticiones o respuestas, extienden de la clase abstracta org.restlet.Filter (que a su vez extiende de Restlet).

Para implementar filtrado se suelen sobreescribir:

  • afterHandle(Request request, Response response) - Permite filtrado después del próximo Restlet.
  • beforeHandle(Request request, Response response) - Permite filtrado antes del próximo Restlet.
  • doHandle(Request request, Response response) - Maneja la llamada pasándola al próximo Restlet.

Los Filtros son de hilos seguros por lo que se pueden añadir y quitar objetivos al procesar las llamadas.

Para implementar un filtro que monitorice la frecuencia con la que una IP remota está haciendo peticiones se usará la clase AccessMonitorFilter. Para ello se tiene que añadir el Filter y el Router:

public class Naviquan extends Application {
  ...
  public static void main(String ...args) throws Exception {
    ...
    component.getDefaultHost().attach("", new Naviquan(component.getContext()));
    ...
    component.start();
  }

  public Naviquan(Context context) {
    super(context);
    ...
  }

  /** createRoot */
  public Restlet createRoot() {
    ...
    Router router = new Router(getContext());
    ...
    //creates the filter and add it in front of the router
    AccessMonitorFilter ipFilter = new AccessMonitorFilter();
    ipFilter.setNext(router);

    return ipFilter;
  }
}

public class AccessMonitorFilter extends Filter {
  private static String[] unprotectedResources = {"scripts", "styles", 
"images", "html"};

  protected void beforeHandle(Request request, Response response) {
    String path = request.getResourceRef().getPath();
    if (isProtectedResource(path)) {
      String ip = request.getClientInfo().getAddress();
      try {
        if (AccessInstanceMonitor.isAccessBanned(ip)) {
          response.setStatus(Status.CONNECTOR_ERROR_CONNECTION); }
      }
      catch (Exception ex) {}
    }

  }

  private boolean isProtectedResource(String path) {
    for (int i = 0; i < unprotectedResources.length; i++) {
      if (path.indexOf(unprotectedResources[i]) == 1) {
        return false; }
    }
    return true;
  }

}


public abstract class AccessInstanceMonitor {

  /** Map of current AccessInstance's keyed by IP address  */
  private static Map accessMap = new ConcurrentHashMap ();

  /** Map of banned AccessInstance's keyed by IP address  */
  private static Map bannedMap = new ConcurrentHashMap();

  /** load banned IPs from persistent storage at start-up */
  public static void loadBannedAccessInstances() {
    ...
  }

  public static boolean isAccessBanned(String ip) throws PoolException {
    if (bannedMap.containsKey(ip)) {
      return true; }
    else {
      addAccessInstance(ip);
      return false;
    }
  }

  public static void addAccessInstance(String ip) throws PoolException {
    AccessInstance monitoredAccessInstance = (AccessInstance) accessMap.get(ip);
    if (monitoredAccessInstance == null) {
      monitoredAccessInstance = new AccessInstance(ip);
      accessMap.put(ip, monitoredAccessInstance);
    }
    else {
      if (monitoredAccessInstance.addHit()) {
        bannedMap.put(ip, monitoredAccessInstance);
        /** Log blocking event  */
        /** Add IP to persistent storage */
      }
    }
  }
}
public class AccessInstance {
  public String ip;
  public long firstTime;
  public long lastTime;
  public int count;

  public AccessInstance(String ip) {
    this.ip = ip;
    firstTime = System.currentTimeMillis();
    lastTime = firstTime;
    count = 1;
  }

  public boolean addHit() {
    count = count + 1;
    lastTime = System.currentTimeMillis();
    return isToBeBanned();
  }

  public float getFrequency() {
    float result = 0f;
    if (lastTime - firstTime >= 0) {
      result = (float)count / (lastTime - firstTime); }

    return result;
  }

  public boolean isToBeBanned() {
    return lastTime - firstTime > 
Constants.ALLOWED_ACCESS_TIME_DECISION_INTERVAL_MILLISECONDS && getFrequency() 
> Constants.ALLOWED_ACCESS_COUNT_PER_TIME_DECISION_INTERVAL;
  }
}

Restlet Cookbook - Using Filters in Restlet Applications (Recipe 6)

eTag

Para verificar el estado de una etiqueta y el contenido con el objetivo de mandar una nueva respuesta XML o una respuesta de contenido no modificado sólo es necesario usar subclases de Resource, los gets condicionales se manejan de forma automática poniendo la etiqueta Representation#tag en las entidades resultado.

Si se quiere comprobar manualmente la etiqueta de la petición para devolver un 304 y evitar la genetración del XML se debe usar ServerResource (en vez de Resource), el método getInfo() y la clase RepresentationInfo contiene la propiedad 'tag' sin el resto de la representación.

Los navegadores suelen enviar peticiones con el encabezado "If-Modified-Since" indicando que se les envie la representación sólo si ha cambiado desde la última petición. ServerResource soporta las consultas condicionadas por defecto, para poder implementar la funcionalidad se debe obtener la representación actual (invocando el método GET) y comporbar la fecha de modificación, o la etiqueta, etc (dependiendo de las condiciones impuestas por el cliente). Se puede hacer un ajuste fino usando el método ServerResource#setConditional

JPA

Manejo de sesiones JPA/Hibernate

Hay cambios en el trunk del SVN para que se realicen llamadas a métodos callback de ConnectorService después de enviar representaciones/respuestas. La idea es que se proporcionen implementaciones propias de ConnectorService que intercepten esas llamadas y actualicen el EntityManager de acuerdo a ellas.

Restlet y GWT

Trabajando con GWT en modo hosted pueden presentarse problemas con el cacheado de las peticiones, para solucionarlo hay que usar un mecanismo no estandar de actualización de encabezados para manipular el control de la cache:

Form headers = (Form) response.getAttributes().get("org.restlet.http.headers");
if (headers == null) {
  headers = new Form();
  response.getAttributes().put("org.restlet.http.headers", headers);
}
headers.add("Cache-Control", "no-cache");

No existe todavía ningún mecanismo que permita hacer algo como message.setCacheControl(CacheControl.NO_CACHE).

Código que intercambia XML entre cliente GWT y servidor GAE:

Client:

import org.restlet.gwt.Callback;
import org.restlet.gwt.Client;
import org.restlet.gwt.data.MediaType;
import org.restlet.gwt.data.Protocol;
import org.restlet.gwt.data.Request;
import org.restlet.gwt.data.Response;
import org.restlet.gwt.resource.XmlRepresentation;

import com.google.gwt.xml.client.Element;
import com.google.gwt.xml.client.NodeList;
import com.google.gwt.xml.client.Node;
import com.google.gwt.xml.client.Text;
import com.google.gwt.xml.client.XMLParser;


...


       Button acceptButton = new Button(constants.AulaCreateCourseDialogBoxAccept());
       acceptButton.addClickListener(new ClickListener() {
           @Override
           public void onClick(Widget sender) {
               final Client client = new Client(Protocol.HTTP);
               client.post("http://localhost:8080/rest/test", getXMLCourseRepresentation(),new Callback() {
                   @Override
                   public void onEvent(Request request, Response response) {
                       // Get the representation as an XmlRepresentation
                       XmlRepresentation rep = response.getEntityAsXml();
                       System.out.println("Client side response received: " + rep.getText());
                       // Loop on the nodes to retrieve the node names and text content.
                       NodeList nodes = rep.getDocument().getDocumentElement().getChildNodes();
                       for (int i = 0; i < nodes.getLength(); i++) {
                               Node node = nodes.item(i);
                               System.out.println( "Node: " + node.getNodeName() + "- Node value: " + node.getFirstChild().getNodeValue());
                       }
                   }
               });                          }
       });


...


   protected XmlRepresentation getXMLCourseRepresentation() {
       XmlRepresentation result = new XmlRepresentation(MediaType.TEXT_XML);
       Document xml= XMLParser.createDocument();
       Element course = xml.createElement("course");
       xml.appendChild(course);
             Element name = xml.createElement("name");
       Text nameText = xml.createTextNode(nameTextBox.getValue());
       name.appendChild(nameText);
       course.appendChild(name);
             Element description = xml.createElement("description");
       Text descriptionText = xml.createTextNode(decriptionTextBox.getValue());
       description.appendChild(descriptionText);
       course.appendChild(description);
             //return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
         //    xml.toString();
       System.out.println("Enviando: " + xml.toString());
       result.setDocument(xml);
       return result;
   }


--------------------------------------------------------------------------------------------------------------

Server:

import org.restlet.Context;
import org.restlet.data.MediaType;
import org.restlet.data.Request;
import org.restlet.data.Response;
import org.restlet.ext.json.JsonRepresentation;
import org.restlet.representation.DomRepresentation;
import org.restlet.representation.Representation;
import org.restlet.representation.Variant;
import org.restlet.resource.Resource;
import org.restlet.resource.ResourceException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;


...


   /**
    * Handle a POST Http request. Create a new user
    *
    * @param entity
    * @throws ResourceException
    */
     public void acceptRepresentation(Representation entity)
     throws ResourceException {           try {
           Document xmlDoc = string2DOM(entity.getText(), false);
           Element root = xmlDoc.getDocumentElement();
           Element courseName = (Element) root.getElementsByTagName("name").item(0);
           System.out.println(" server - courseName: " + courseName.getTextContent());
           Element courseDescription = (Element) root.getElementsByTagName("name").item(0);              } catch (Exception ex) {
           ex.printStackTrace();
       }
       getResponse().redirectSeeOther(getRequest().getResourceRef());
       getResponse().setLocationRef("/rest/test");
   }


....


  public Document string2DOM(String s) {
      Document tmpX=null;
      DocumentBuilder builder = null;
      try{
          builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
      }catch(javax.xml.parsers.ParserConfigurationException error){
          error.printStackTrace();
      }
      try{
          tmpX=builder.parse(new ByteArrayInputStream(s.getBytes()));
      }catch(org.xml.sax.SAXException error){
          error.printStackTrace();
      }catch(IOException error){
          error.printStackTrace();
      }
      return tmpX;
  }

WADL

En caso de no tener nada en la raíz de un servicio puede ser aconsejable configurar el soporte WADL:

Con la extensión WADL se puede devolver un descriptor WADL en formato XML o HTML sin necesidad de tocar nada (con conneg). La aplicación utiliza de forma interna las hojas XSLT proporcionadas por Yahoo. Para ello es necesario disponer de una versión reciente de Xalan.

JAXB

Mapping cyclic references to XML

Generate an XML Document from an Object Model with JAXB 2

Spring

Con RestletFrameworkServlet y SpringBeanRouter se puede configurar una aplicación. De esta forma se puede usar IoC para configurar las instancias de Resource sin necesidad de acceder directamente ApplicationContext, si por lo que sea se necesitara acceder desde el Resource al ApplicationContext bastaría con que el Resource implementara ApplicationContextAware.

Autenticación

En algunas pruebas el cliente manda la autentificación HTTP básica como una cadena codificada en Base64 que contiene usuario + ":" + pass:

public static boolean authenticate(ChallengeResponse challenge) {
        if (challenge != null) {
            String username = "";
            String password = "";
            BASE64Decoder decoder = new BASE64Decoder();
            try {
                logger.debug("Credentials: "+challenge.getCredentials());
                String usernpass = new String(decoder.decodeBuffer(challenge.getCredentials().toString()));
                username = usernpass.substring(0, usernpass.indexOf(":"));
                password = usernpass.substring(usernpass.indexOf(":") + 1);
                boolean auth = User.authenticate(username, password);
                logger.debug(auth);
                return auth;
            } catch (IOException ex) {
                return false;
            }
        } else {
            return false;
        }

    }

En algunos casos se solucionad usando ChallengeResponse#getIdentifier y ChallengeResponse#getSecret() si da problemas probar con new String(getRequest().getChallengeResponse().getSecret().

Para securizar toda la aplicación en vez de una ruta concreta:

 public Restlet createRoot() {
	Router securedRoute = new Router(getContext());
	securedRouter.add("/users", UsersResource.class);
	securedRouter.add("/user/{id}", UserResource.class);
	Guard guard = new Guard(getContext(), ChallengeScheme.HTTP_BASIC, "whatever");
        guard.setNext(securedRoute);
	return guard;
  }

Si por ejemplo se quiere tener un recurso que envie información y permita o no permita la creación/modificación de recursos en función del estado de autentificación de la petición, p.e:

GET user/{username}

Se puede implementar comprobando en el recurso la presencia de ChallengeResponse en la petición y comporbando si se ha producido autentificación (ChallengeResponse#isAuthenticated).


Si por ejemplo se desea tener un Guard que actue en función del id del recurso de la petición habría que crear un Router que extendiese del Router tradicional y sobreescribir el método getNext(Request request, Response response)

@Override
public Restlet getNext(Request request, Response response) {
    Restlet restlet = super.getNext(request, response);

    if (restlet != null) {
        if (restlet instanceof Route) {
            // Should be the case for classic routers.
            Route route = (Route) restlet;
            guard.setNext(route.getNext());
            route.setNext(guard);
            return route;
        }
        guard.setNext(restlet);
        return guard;
    } else {
        return null;
    }
}

En la aplicación se tendría:

Guard guard = new ExtendedGuard(...);
Router router = new Router(getContext());
router.attach("{itemId}", ItemResource.class);
guard.setNext(router);
return guard;

De esta forma se podrá acceder desde el ExtendedGuard a información del estilo request.getAttributes().get("itemId") y tomar decisiones en función del mismo.

JAX-RS

Para lanzar el servidor http embebido:

// create Component (as ever for Restlet)
Component comp = new Component();
Server server = comp.getServers().add(Protocol.HTTP, 8182);

// create JAX-RS runtime environment
JaxRsApplication application = new JaxRsApplication(comp.getContext());

// attach ApplicationConfig
application.add(new RegisteredResources());

// Attach the application to the component and start it
comp.getDefaultHost().attach(application);
comp.start();
public class CourseResource extends ServerResource {
	private static final Logger log = Logger.getLogger(CourseResource.class.getName());
	@Get
       @Produces("application/json")
	public String getTrace() {
		log.info("get");
		return "get";
	}

    @Post
    public String postTrace(Representation entity) {
        Form form = new Form(entity);
        return form.getFirstValue("firstname");
    }
}

Preguntas frecuentes

¿Cómo extraer parámetros de consulta desde la URL?

  • Modo 1

Añadiendo en la parte del código en la que se asocian las rutas a los recursos:

router.attach("/cursos", CursosResource.class)
        .extractQuery("firsResult", "firsResult", true)
        .extractQuery("maxResults", "maxResults", true);

Luego se puede acceder a los parámetros desde el recurso:

getRequest().getAttributes().get('firsResult');
  • Modo 2

Diréctamente desde el recurso:

String fooParam = getQuery().getFirstValue('foo'); 

¿Cómo acceder a encabezados de peticiones?

  • Lectura de encabezados HTTP estandars

Restlet facilita el acceso a los encabezados estandars a través de la clase Request, en la siguiente tabla se muestran los más usados (no pretende ser una referencia exhaustiva):

Encabezado HTTP Método de Request
Referer getReferrerRef()
Cookie getCookies()
If-Modified-Since, If-Unmodified-Since, If-Match, If-None-Match getConditions()
Authorization getChallengeResponse()
  • Lectura de encabezados HTTP propios

Restlet pone todos los encabezados de las peticiones recibidas en un objeto Form

Form requestHeaders = (Form) getRequest().getAttributes().get('org.restlet.http.headers');
String gruposp2pHeader = requestHeaders.getFirstValue('X-gruposp2pHeader');

Como escribir encabezados de respuesta

  • Escritura de encabezados HTTP estandars

Para escribir encabezados HTTP estandars se utilizan las clases Response y Representation, las siguientes tablas no son exhaustivas:

Encabezado HTTP Método de Response
Location setLocationRef()
Server setServerInfo()
WWW-Authenticate setChallengeRequest()


Encabezado HTTP Método de Representation
Last-Modified setModificationDate()()
Etag setTag()
Content-Size setSize()
Expires setExpirationDate()
  • Escritura de encabezados HTTP propios
Form responseHeaders = (Form) getResponse().getAttributes().get('org.restlet.http.headers');

if (responseHeaders == null)
{
    responseHeaders = new Form();
    getResponse().getAttributes().put('org.restlet.http.headers', responseHeaders);    
}

responseHeaders.add('X-gruposp2p', 'true');

¿Cómo devolver XML en una respuesta a una invocación 'acceptRepresent' (POST)?

Si se está usando Restlet 1.1 invocar desde el método:

Representation rep = <your XML representation>;

getResponse().setEntity(rep);

¿Cómo leer una entidad sin destruirla? getEntityAsForm() cachea el resultado obtenido. Si hiciera falta (por temas de filtros) seguir pasando la entidad se puede solucionar:

request.setEntity(myForm.getWebRepresentation()); 

Para saber más

Sitio oficial de Restlet

Creating a Web of Data with Restlet (Andrew Newman)

Restlet Newbie FAQ

Build a RESTful Web service

Herramientas personales