Tabla de Contenidos

HATEOAS

Introducción

HATEOAS (Hypermedia as the Engine of Application State) es un principio de diseño de arquitecturas API REST donde cada recurso incluye en su respuesta enlaces a otros recursos relacionados. Este concepto está basado en el antiguo paradigma de Internet como red de información conectada entre sí mediante enlaces en páginas HTML.

Aplicado a servicios REST este principio permite descubrir, a partir de un único punto de entrada a un servicio, toda la información disponible relacionada con el recurso así como todas las acciones permitidas (consultar, crear, modificar, eliminar).
Con esta arquitectura se obtienen servicios autodescriptivos, se reduce la carga en servicios que devuelven gran cantidad de datos o se abstraen a los clientes de las rutas de los endpoints.

Algunos ejemplos de casos de uso son:


¿Qué es un Recurso?. Un recurso hace referencia a un concepto importante de nuestro negocio (Facturas, Cursos, Compras, etc).

HATEOAS con JAX-RS

Para construir y añadir enlaces en los recursos REST, JAX-RS proporciona las clases Link (basada en la especificación RFC 5988) y UriBuilder.

La clase Link es el pilar central de HATEOAS. Representa el enlace entre dos recursos y consta de las siguientes propiedades:


Ejemplo:

 "link": {
          "type": "application/json",
          "title": "Hola Mundo",
          "rel": "self",
          "href": "http://localhost:8001/proyectoPrueba/rest/public/v1/saluda"
  }

En formato XML esta estructura se conoce también como Atom Link:

<link href="http://localhost:8001/proyectoPrueba/rest/public/v1/saluda" rel="self" type="application/xml" title="Hola Mundo"/> 


La clase Link dispone de un Builder que permite instanciar objetos de forma clara y legible a partir de métodos estáticos.

Se ampliará información sobre el patrón Builder en el apartado correspondiente a la clase UriBuilder.

Los métodos más interesantes para crear Links y definir sus valores son:


Relaciones

Aunque es posible etiquetar el tipo de relación (campo rel) de un Link como se desee, existe un estándar que pretende definir y registrar todos los posibles tipos de relaciones.

Algunas de las relaciones existentes más interesantes son:

Como se ha mencionado anteriormente, estas son algunas de las relaciones predefinidas y es recomendable su uso, pero es posible crear tipos ad hoc estableciendo el campo rel con la etiqueta que se desee.

URIBuilder

URIBuilder es una clase abstracta que implementa el patrón de diseño Builder y que, por lo tanto, proporciona una serie de métodos estáticos que permiten construir objetos de la clase URI de forma fácil y legible. Estos objetos URI constituyen la otra base principal de HATEOAS y permiten completar el campo href de los objetos Link vistos en el apartado anterior.

Algunos de los métodos más interesantes son:

Para usar el método path(Class c, String method), es necesario haber usado previamente path(Class c).
Uno añade el path definido en el método, y el otro añade el path definido en la clase.

Se verán algunos ejemplos de uso de todos estos métodos en el siguiente apartado.

UriInfo

UriInfo es una clase proporcionada por JAX-RS que aporta información sobre las URIs de una aplicación o petición. Puede usarse junto a URIBuilder para crear URIs de forma rápida y sencilla.

Ejemplo:

   //...
   @Context
   UriInfo uriInfo;
   //...
   // Crea una URI con la dirección base donde se publican los servicios en nuestra aplicación: http://localhost:8001/[APLICACION]/rest/
   URI uri = UriBuilder.fromUri( uriInfo.getBaseUri() ).build( );
 
   // Crea una URI con la dirección completa absoluta de la petición.
   URI uri = UriBuilder.fromUri( uriInfo.getAbsolutePath() ).build( );
 
   // Devuelve un String con el path de la clase+método de la petición.
   String path = uriInfo.getPath();

UriInfo se declara con la anotación @Context y puede definirse a nivel de clase, como una propiedad más, o como parámetro en el método que se desee usar.

Uso de HATEOAS en FundeWeb

FundeWeb incorpora el soporte para las clases Link y UriBuilder desde las siguientes versiones de librerias:

A fecha de redacción de esta WiKi solo es posible utilizar HATEOAS en aplicaciones desplegadas en WebLogic 12.2.
Si desea utilizar HATEOAS en aplicaciones desplegadas en WebLogic 12.1.3, póngase en contacto con MNCS.

Modo de Empleo

Una vez comentada la teoría de HATEOAS y conocidas las clases implicadas en su uso, a continuación se detalla como configurar nuestro servicio REST en FundeWeb para hacer uso de esta arquitectura.

:!: Importante: Como se ha mencionado en la introducción, HATEOAS es un principio arquitectónico. Esto implica un cambio en la filosofía habitual de desarrollo, tanto en los propios servicios como en el modelo de objetos DTO usados. En el ejemplo siguiente se comenta esta situación.

Queda a criterio de los desarrolladores estimar o desestimar el uso de HATEOAS en función de las necesidades de su negocio. Consultar con MNCS en caso de duda.

Vamos a mostrar la configuración y uso partiendo de un ejemplo de servicio REST con dos recursos: Personas y Cargos.


El primer paso es añadir tantas propiedades de la clase Link como enlaces queramos representar en nuestra respuesta:

PersonaDTO:

@JsonSerialize( include = Inclusion.NON_EMPTY )
@JsonPropertyOrder({"identificador", "correo", "nombre", "apellido", "link", "cargos","acciones" })
@JsonRootName( value = "personaPrueba" )
public class PersonaDTO {
 
//...
 
   private Link link;
   private Link cargos;
   private List<Link> acciones;  
 
//...   
 
   @XmlJavaTypeAdapter( Link.JaxbAdapter.class )
   @JsonProperty
   @JsonSerialize(using = LinkSerializer.class)
   public Link getLink() {
	return link;
   }
 
   @JsonDeserialize(using = LinkDeserializer.class)
   public void setLink( Link link ) {
	this.link = link;
   }
 
   @XmlElementWrapper( name = "cargos" )
   @XmlElement( name = "link" )
   @XmlJavaTypeAdapter( Link.JaxbAdapter.class )
   @JsonProperty
   @JsonSerialize(using = LinkSerializer.class)
   public Link getCargos() {
	return cargos;
   }
 
   @JsonDeserialize(contentUsing = LinkDeserializer.class)
   public void setCargos( Link cargos ) {
	this.cargos = cargos;
   }
 
   @XmlJavaTypeAdapter( Link.JaxbAdapter.class )
   @XmlElementWrapper( name = "acciones" )
   @XmlElement( name = "link" )
   @JsonProperty
   @JsonSerialize(contentUsing = LinkSerializer.class)
   public List<Link> getAcciones() {
      if (acciones == null) {
	acciones = new ArrayList<>();
      }
      return acciones;
   }
 
   @JsonDeserialize(contentUsing = LinkDeserializer.class)
   public void setAcciones( List<Link> acciones ) {
      this.acciones = acciones;
   }
   //..
 }

Como se puede observar en el código, no es necesario declarar un List<CargoDTO> para representar los cargos de una persona.
Al utilizar el Link cargos, crearemos un enlace al endpoint que obtiene esta información.

:!: Esto es un buen ejemplo del principio HATEOAS: en lugar de devolver una persona con toda sus campos y, en la misma respuesta, todos la información desglosada de los cargos que ocupa esa persona, se devuelve una persona con un enlace al recurso que obtiene sus cargos.

:?: Otra aproximación diferente habría podido ser crear una lista de Link para representar la lista de cargos de una persona, y que cada Link apunte al endpoint que obtiene los detalles de un cargo específico (GET /cargos/{id}).

No olvidar activar el mapeo de objetos a JSON como se indica en la WiKi REST con JSON.

Sino utilizamos las anotaciones JAXB, y si las anotaciones de Jackson, para serializar podemos usar.

   @JsonProperty
   @JsonSerialize(contentUsing = LinkSerializer.class)
   public Link getLink() {
	return link;
   }


2. Definir el servicio

En este segundo paso se define el servicio para el recurso de Personas con todos los métodos que vamos a enlazar posteriormente.

Servicio:

   //...
   @Path( "/public/v1/personas" )
   public class PersonasREST {
 
      //...
      /**
      * Proporciona información sobre las URIs del servicio y de la request solicitada.
      */
      @Context
      UriInfo uriInfo;
      //...
 
      /**
      * Método GET que obtiene la persona con correo "correo"
      */
      @GET
      @Path( "/{correo}" )
      @Produces( {MediaType.APPLICATION_JSON, ProblemMediaType.APPLICATION_PROBLEM_JSON	} )
      public Response getPersona( @Email @PathParam( "correo" ) String correo ) {
	//...
      }
 
      /**
      * Método POST que crea una Persona "persona".
      */
      @POST
      @Consumes(MediaType.APPLICATION_JSON)
      @Produces( {MediaType.APPLICATION_JSON, ProblemMediaType.APPLICATION_PROBLEM_JSON	} )
      public Response createPersona( PersonaDTO persona ) {
         //...
      }
 
      /**
      * Método PUT que actualiza una Persona con correo "correo" con los datos de "persona".
      */ 
      @PUT 
      @Path( "/{correo}" )
      @Consumes(MediaType.APPLICATION_JSON)
      @Produces( {MediaType.APPLICATION_JSON, ProblemMediaType.APPLICATION_PROBLEM_JSON	} )
      public Response updatePersona(  @Email @PathParam( "correo" ) String correo, PersonaDTO persona ) {
        //...
      }
 
      /**
      * Método DELETE que borra la persona con correo "correo".
      */  
      @DELETE
      @Path( "/{correo}" )
      @Produces( {MediaType.APPLICATION_JSON, ProblemMediaType.APPLICATION_PROBLEM_JSON	} )
      public Response deletePersona(  @Email @PathParam( "correo" ) String correo ) {
         //...
      }
 
      /**
      * Método GET que devuelve todos los cargos de la persona con correo "correo".
      */ 
      @GET
      @Path("/{correo}/cargos")
      @Produces( {MediaType.APPLICATION_JSON, ProblemMediaType.APPLICATION_PROBLEM_JSON	} )
      public Response getCargosPersona(@Email @PathParam( "correo" ) String correo) {
         //...
      }      
   }

Como se puede observar, existe un método para realizar todas las operaciones CRUD y otro método para obtener todos los cargos de una persona determinada.
:!: El método createPersona no necesita Path, se hace una petición POST directamente al Path del servicio. Ver WiKi Buenas prácticas con servicios REST.


3. Definir los enlaces

En este último paso vamos a ver de forma detallada el método getPersona y algunas formas diferentes de construir enlaces usando URIBuilder.

Método GET:

@GET
@Path( "/{correo}" )
@Produces( {MediaType.APPLICATION_JSON, ProblemMediaType.APPLICATION_PROBLEM_JSON} )
public Response getPersona( @Email @PathParam( "correo" ) String correo ) {
 
   PersonaDTO persona = null;
   Persona entity = null;
 
  try {
   // se obtiene la persona desde el servicio de Gente
   entity = servicioGenteUmu.getPersonaByUsuario( EnvironmentManagerBean.instance().getApplicationName(),correo );
   // convierte el objeto Persona de Gente a PersonaDTO 
   persona = PersonaDTO.fromPersona( entity );


   //Enlace "self" usando Link.fromUri y uriInfo.getAbsolutePath() que obtiene la ruta completa del servicio solicitado.
   Link linkPersona = Link.fromUri( uriInfo.getAbsolutePath() ).rel( "self" )
           		  .type( MediaType.APPLICATION_JSON ).title( "Persona" ).build();
   persona.setLink( linkPersona );


   //UriBuilder a partir de baseUri (path rest de la aplicación) y path del servicio.
   UriBuilder ubPersonas = UriBuilder.fromUri( uriInfo.getBaseUri() ).path( PersonasREST.class );
 
   //Enlace a cargos a partir del path del método getCargosPersona.
   Link linkCargos = Link.fromUri( ubPersonas.clone().path( PersonasREST.class, "getCargosPersona" ).build(correo)).rel( "cargos" )
			 .type( MediaType.APPLICATION_JSON ).title( "Cargos" ).build();
   persona.setCargos( linkCargos );


   //Enlaces a las distintas operaciones disponibles sobre el recurso Personas. 
   // GET mediante Link.fromLink para reutilizar link existente y cambiarle algún valor.
   persona.addAccion( Link.fromLink( linkPersona ).title("GET").build( ) );
 
   // POST sin path ya que el endpoint coincide con el path del servicio
   persona.addAccion( Link.fromUri( ubPersonas.clone().build()).rel("edit").type( MediaType.APPLICATION_JSON ).title( "POST" ).build() );
   persona.addAccion( Link.fromUri( ubPersonas.clone().path( getClass(), "updatePersona" ).build(correo )).rel("edit")
                           .type( MediaType.APPLICATION_JSON ).title( "PUT" ).build() );
   persona.addAccion( Link.fromUri( ubPersonas.clone().path( getClass(), "deletePersona" ).build(correo )).rel("edit")
                           .type( MediaType.APPLICATION_JSON ).title( "DELETE" ).build() );


  } catch ( PersonaException | PersonaNotFoundException e ) {
    log.error( "Se ha producido un error al buscar el usuario [#0].", e, correo );
    GenericProblem gp = ( GenericProblem ) GenericProblem.fromStatusDetail( Response.Status.NOT_FOUND,"No existe el usuario: " + correo );
    gp.getParameters().put( "documento", correo );
    throw new NotFoundException( gp );
  }

  return Response.ok( persona ).build();
}


4. Ejemplo de petición

A continuación se muestra un ejemplo de petición GET al recurso Persona explicado anteriormente y el resultado que produce la llamada.

Petición:

 http://ramongg.atica.um.es:8001/proyectoPrueba/rest/public/v1/personas/ramongg@um.es 


Respuesta:

{
    "correo": "ramongg@um.es",
    "nombre": "RAMON",
    "apellido": "GINEL GEA",
    "link": {
        "type": "application/json",
        "rel": "self",
        "title": "Persona",
        "href": "http://ramongg.atica.um.es:8001/proyectoPrueba/rest/public/v1/personas/ramongg@um.es"
    },
    "cargos": {
        "type": "application/json",
        "rel": "cargos",
        "title": "Cargos",
        "href": "http://ramongg.atica.um.es:8001/proyectoPrueba/rest/public/v1/personas/ramongg@um.es/cargos"
    },
    "acciones": [
        {
            "type": "application/json",
            "rel": "self",
            "title": "GET",
            "href": "http://ramongg.atica.um.es:8001/proyectoPrueba/rest/public/v1/personas/ramongg@um.es"
        },
        {
            "type": "application/json",
            "rel": "edit",
            "title": "POST",
            "href": "http://ramongg.atica.um.es:8001/proyectoPrueba/rest/public/v1/personas"
        },
        {
            "type": "application/json",
            "rel": "edit",
            "title": "PUT",
            "href": "http://ramongg.atica.um.es:8001/proyectoPrueba/rest/public/v1/personas/ramongg@um.es"
        },
        {
            "type": "application/json",
            "rel": "edit",
            "title": "DELETE",
            "href": "http://ramongg.atica.um.es:8001/proyectoPrueba/rest/public/v1/personas/ramongg@um.es"
        }
    ]
}

Referencias y Bibliografía

FAQs

URIBuilder.path con CORS


Si se está utilizando CORS y se quiere construir un enlace a un método que tiene su equivalente @OPTIONS, el método path(Class c, String method) fallará al encontrar dos métodos nombrados de la misma manera.

(FundeWeb 2)

Solución
Renombrar el método anotado con @OPTIONS. ¡OJO! solo hay que renombrar el método Java, no hay que modificar el @Path del recurso ni ningún otro parámetro.

RAMON GINEL GEA 20/02/2020 14:37


Solicitud/Registro de FAQ

Pon aquí tus propuestas de FAQs, indicando qué problema tienes, y buscaremos la solución lo antes posible. Si además lo has resuelto, puedes indicar cómo lo has hecho.

Formato de petición de FAQ:

**Título**
//Descripción del problema//
//Tecnología afectada (Fundeweb 1/2)//
//Cómo reproducir el error//
**Solución**
//Descripción de la solución//

Tus datos de contacto  --- //[[correo@umOticarum.es|Tu Nombre]] dd/mm/yyyy //

RAMON GINEL GEA 17/02/2020 12:57