====== 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:
* Maestro detalle. Por ejemplo, un pedido con varios enlaces al //endpoint// de productos, en lugar de incluir todos los detalles de los productos en el //payload// del pedido.
* Paginación. Por ejemplo, devolver los 10 primeros elementos de un listado y un enlace al //endpoint// que devuelve la siguiente página, en lugar de obtener el listado completo de elementos.
* Informar sobre las acciones disponibles. Por ejemplo, en una petición //GET// a un recurso, incluir enlaces a los //endpoints// que permiten modificar o borrar dicho recurso.
\\
¿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 [[https://tools.ietf.org/html/rfc5988|RFC 5988]]) y **UriBuilder**.
==== Link ====
La clase [[https://tools.ietf.org/html/rfc5988#section-3|Link]] es el pilar central de //HATEOAS//. Representa el enlace entre dos recursos y consta de las siguientes propiedades:
* **rel** - (String) Tipo de relación entre el recurso ubicado en __href__ y el actual. Ver el subapartado [[fdw2.0:fundeweb2.0:gt:rest:hateoas#relaciones|Relaciones]].
* **href** - (URI) URI que especifica el //endpoint// del recurso relacionado.
* **type** - (String) //MediaType// del recurso relacionado ("//application/xml//","//application/json//", etc.).
* **title** - (String) Título o etiqueta usado para identificar la relación.
\\
__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//__:
\\
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:
* **fromUri(String uri)** - Crea un objeto Link.Builder a partir de un String que contiene una uri.
* **fromUri(URI uri)** - Crea un objeto Link.Builder a partir de un objeto URI.
* **fromLink(Link link)** - Crea un objeto Link.Builder a partir de los campos contenidos en otro Link.
* **rel(String rel)** - Completa el campo rel de un builder. Establece el tipo de relación entre recursos.
* **title(String title)** - Completa el campo title de un builder. Establece la etiqueta de la relación entre recursos.
* **type(String type)** - Completa el campo type de un builder. Establece el MediaType del endpoint al que apunta el enlace.
* **build()** - Construye un objeto Link a partir de los campos establecidos en su builder.
\\
=== Relaciones ===
Aunque es posible etiquetar el tipo de relación (campo **rel**) de un Link como se desee, existe un [[http://www.iana.org/assignments/link-relations/link-relations.xhtml|estándar]] que pretende definir y registrar todos los posibles tipos de relaciones.
Algunas de las relaciones existentes más interesantes son:
* **self**: representa un enlace al recurso actual.
* **edit**: representa un enlace a los servicios que permiten modificar/eliminar el recurso actual.
* **prev/next** : representa un enlace a los recursos anteriores y posteriores, respectivamente, cuando se está navegando entre elementos en una lista.
* **first/last** : representa un enlace al primer y último recurso, respectivamente, cuando se está navegando entre elementos en una lista.
* **item**: representa un enlace a un elemento de una colección.
* **related**: representa un enlace con algún tipo de relación. Podría usarse como relación por defecto.
* **search**: representa un enlace al servicio de búsqueda del recurso actual.
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:
* **fromUri(String string)** - Crea una instancia de //URIBuilder// a partir una //URI// pasada como String.
* **fromUri(URI uri)** - Crea una instancia de //URIBuilder// a partir un objeto //URI// (ver UriInfo).
* **path(Class c)** - Añade al //URIBuilder// el path definido en el servicio ubicado en la clase **c**.
* **path(Class c, String method)** - Añade al //URIBuilder// el path definido en el recurso ubicado en **c.method**.
* **queryParam(String name, Object... values)** - Añade a la //URI// un parámetro definido con @//QueryParam// en el recurso destino.
* **build(Object... values)** - Construye el objeto //URI//. Los valores **values** se corresponden con los parámetros en el //path// del recurso destino.
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:
* fundeweb-jaxrs-2.0.102
* fundeweb-java-services-config-2.0.102
* fundeweb-jersey-2.0.103
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__.
\\
=== 1. Añadir Link a los DTOs ===
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 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 getAcciones() {
if (acciones == null) {
acciones = new ArrayList<>();
}
return acciones;
}
@JsonDeserialize(contentUsing = LinkDeserializer.class)
public void setAcciones( List acciones ) {
this.acciones = acciones;
}
//..
}
Como se puede observar en el código, __no es necesario declarar un List__ 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 [[fdw2.0:fundeweb2.0:gt:rest:guia_de_rest_con_json#activar_la_configuracion_de_rest_con_json|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 [[fdw2.0:fundeweb2.0:gt:rest:buenas_pracitcas|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 );
\\
* Creación de un objeto **Link** a partir de una **Uri** construida con **UriInfo.getAbsolutePath()**. Se establece el resto de valores del enlace con los métodos vistos en [[fdw2.0:fundeweb2.0:gt:rest:hateoas#link|Link]]. \\ Ejemplo típico de enlace "//self//".
//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 );
\\
* Creación del **Link** al //endpoint// de los cargos de la persona. Ejemplo de uso de **UriInfo.getBaseUri()**, obtiene el //path// de la aplicación, del método **path(Class c)**, obtiene la notación //path// de la clase, y de **path(Class c, String method)** obtiene la notación //path// del método.
//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 );
\\
* Creación de los **Link** a las distintas operaciones //CRUD// sobre el recurso. Ejemplo de uso de **Link.fromLink**, crea un **Link.Builder** a partir de los parámetros de un //Link//. Para reutilizar el //UriBuilder// **ubPersonas** es necesario usar el método **clone()**, ya que cada llamada a **path()** en un //UriBuilder// concatena el valor existente con el parámetro pasado, quedando el objeto modificado. Si no se clonara se irían anexando paths uno tras otro con cada llamada.
//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() );
\\
* Captura de excepciones y creación del **Problem** que describe el error. Ver WiKi [[fdw2.0:fundeweb2.0:gt:rest:manejo_errores_rest|Manejo de errores en servicios REST]].
} 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 =====
* [[https://dennis-xlc.gitbooks.io/restful-java-with-jax-rs-2-0-en/en/part1/chapter10/hateoas.html|RESTful Java with JAX-RS 2.0 - Chapter 10. HATEOAS]]
* [[https://www.logicbig.com/tutorials/java-ee-tutorial/jax-rs/hateoas.html|
JAX-RS - Applying HATEOAS]]
* [[https://tools.ietf.org/html/rfc5988|RFC 5988 - Web Linking]]
* [[http://www.iana.org/assignments/link-relations/link-relations.xhtml|Link Relations]]
* [[https://restfulapi.net/richardson-maturity-model/|Richardson Maturity Model for Rest Services]]
* [[https://blog.arkey.fr/2017/10/02/pagination_hateoas_link.en/|JAX-RS 2.0 pagination with Link header]]
* [[https://www.oreilly.com/content/how-a-restful-api-represents-resources/|How a RESTful API represents resources]]
* [[https://www.baeldung.com/rest-api-pagination-in-spring|REST Pagination in Spring]]
===== 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@ticarum.es|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@ticarum.es|RAMON GINEL GEA]] 17/02/2020 12:57//