Actividad 3 - Desarrollo de un servicio web RESTful
El objetivo de esta actividad es implementar un servicio RESTful utilizando Java, Jersey y WildFly. El servicio permitirá leer y añadir usuarios dando como resultado respuestas en JSON y, por compatibilidad, también en XML.
1. Inicialización del proyecto
Section titled “1. Inicialización del proyecto”Para desarrollar la actividad se utilizará un proyecto Maven. Además, configuraremos Jersey para exponer el servicio RESTful y WildFly como servidor de aplicaciones.
-
Inicialización del proyecto Maven
Se crea un proyecto Maven con empaquetado
warusando el siguiente comando:Terminal window mvn archetype:generate -DgroupId=es.ual -DartifactId=actividad3 -DarchetypeArtifactId=maven-archetype-webapp -Dpackaging=war -DinteractiveMode=false -
Modificar web.xml
A continuación, se modifica el archivo
web.xmlpara definir el servlet de Jersey, mapear las rutas bajo/api/*e indicar el paquete donde se encuentran los recursos REST.src/main/webapp/WEB-INF/web.xml <?xml version="1.0" encoding="UTF-8"?><web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns="http://xmlns.jcp.org/xml/ns/javaee"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"version="4.0"><servlet><servlet-name>Jersey Web Application</servlet-name><servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class><init-param><param-name>jersey.config.server.provider.packages</param-name><param-value>sampleproject.service</param-value></init-param><load-on-startup>1</load-on-startup></servlet><servlet-mapping><servlet-name>Jersey Web Application</servlet-name><url-pattern>/api/*</url-pattern></servlet-mapping></web-app> -
Modificar pom.xml
Después, se actualiza el archivo
pom.xmlpara añadir las dependencias necesarias del proyecto.pom.xml <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"><modelVersion>4.0.0</modelVersion><groupId>es.ual</groupId><artifactId>actividad3</artifactId><packaging>war</packaging><version>1.0-SNAPSHOT</version><name>actividad3 Maven Webapp</name><url>http://maven.apache.org</url><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><build><finalName>${project.artifactId}</finalName><plugins><plugin><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>1.8</source><target>1.8</target></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-war-plugin</artifactId><version>3.3.1</version></plugin></plugins></build><dependencies><dependency><groupId>org.glassfish.jersey.core</groupId><artifactId>jersey-server</artifactId><version>2.26</version></dependency><dependency><groupId>org.glassfish.jersey.core</groupId><artifactId>jersey-common</artifactId><version>2.26</version></dependency><dependency><groupId>org.glassfish.jersey.test-framework.providers</groupId><artifactId>jersey-test-framework-provider-jdk-http</artifactId><version>2.26</version></dependency><dependency><groupId>org.glassfish.jersey.inject</groupId><artifactId>jersey-hk2</artifactId><version>2.26</version></dependency><dependency><groupId>org.glassfish.jersey.media</groupId><artifactId>jersey-media-json-jackson</artifactId><version>2.26</version></dependency><dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId><version>42.2.19</version></dependency><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.4.0-b180830.0359</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30</version><scope>provided</scope></dependency></dependencies></project>
2. Creación del proyecto ejemplo
Section titled “2. Creación del proyecto ejemplo”Una vez inicializado y configurado el proyecto, se crean las clases y el servicio de ejemplo que formarán la API.
-
Creamos la entidad User
Para representar la estructura de datos de cada usuario, se crea la clase
Usercon los atributos necesarios:src/main/java/sampleproject/domain/User.java package sampleproject.domain;import javax.xml.bind.annotation.XmlRootElement;import javax.xml.bind.annotation.XmlType;import lombok.Getter;import lombok.Setter;@Getter@Setter@XmlRootElement@XmlType(propOrder = { "username", "password", "dni", "name", "surnames", "age" })public class User {private String username;private String password;private String dni;private String name;private String surnames;private int age;} -
Creamos la entidad Users
También se crea una clase
Userspara agrupar varios usuarios en una única respuesta.src/main/java/sampleproject/domain/Users.java package sampleproject.domain;import java.util.ArrayList;import java.util.List;import javax.xml.bind.annotation.XmlElement;import javax.xml.bind.annotation.XmlRootElement;@SuppressWarnings("serial")@XmlRootElementpublic class Users extends ArrayList<User> {@XmlElement(name = "user")public List<User> getUsers() {return this;}} -
Creamos el servicio UserService
Después, se implementa
UserService, que contiene los endpoints y la lógica de acceso a la base de datos:src/main/java/sampleproject/service/UserService.java package sampleproject.service;import javax.ws.rs.GET;import javax.ws.rs.Path;import javax.ws.rs.PathParam;import javax.ws.rs.Produces;import javax.ws.rs.core.Response;import java.sql.*;import sampleproject.domain.User;import sampleproject.domain.Users;@Path("/users")public class UserService {@GET@Produces({ "application/json", "application/xml" })public Response getUsers() {Users users = this.getUsersFromDB();return Response.status(200).entity(users).build();}@Path("xml")@GET@Produces("application/xml")public Response getUsersXML() {Users users = this.getUsersFromDB();return Response.status(200).entity(users).build();}@Path("json")@GET@Produces("application/json")public Response getUsersJSON() {Users users = this.getUsersFromDB();return Response.status(200).entity(users).build();}@Path("{username}")@GET@Produces("application/json")public Response getExampleUser(@PathParam("username") String username) {User user = this.getUserFromDB(username);if (user != null) {return Response.status(200).entity(user).build();} else {return Response.status(400).build();}}private Connection connect2DB() {Connection conn = null;try {Class.forName("org.postgresql.Driver");String url = "jdbc:postgresql://postgres:5432/dwsc";conn = DriverManager.getConnection(url, "estudiante", "estudiante");} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException e) {e.printStackTrace();}return conn;}private Users getUsersFromDB() {Users users = new Users();Connection conn = this.connect2DB();try {Statement st = conn.createStatement();ResultSet rs = st.executeQuery("SELECT * FROM sampleusers ORDER BY age");while (rs.next()) {User user = new User();user.setUsername(rs.getString("username"));user.setPassword(rs.getString("password"));user.setDni(rs.getString("dni"));user.setName(rs.getString("name"));user.setSurnames(rs.getString("surnames"));user.setAge(rs.getInt("age"));users.add(user);}rs.close();st.close();conn.close();} catch (SQLException e) {System.err.println("[UserService - getUsersFromDB] SQLException while querying the users");System.err.println(e.getMessage());}return users;}private User getUserFromDB(String username) {User user = null;Connection conn = this.connect2DB();try {Statement st = conn.createStatement();ResultSet rs = st.executeQuery("SELECT * FROM sampleusers WHERE username = '" + username + "'");if (rs.next()) {user = new User();user.setUsername(rs.getString("username"));user.setPassword(rs.getString("password"));user.setDni(rs.getString("dni"));user.setName(rs.getString("name"));user.setSurnames(rs.getString("surnames"));user.setAge(rs.getInt("age"));}rs.close();st.close();conn.close();} catch (SQLException e) {System.err.println("[UserService - getUserFromDB] SQLException while querying a user");System.err.println(e.getMessage());}return user;}} -
Generamos el artefacto war
Por último, se genera el artefacto
.warcon el siguiente comando:Terminal window mvn clean package
3. Ejecución del proyecto
Section titled “3. Ejecución del proyecto”Para ejecutar el proyecto se utilizará WildFly como servidor de
aplicaciones y PostgreSQL como base de datos. Ambos servicios se
levantan mediante docker-compose para simplificar la puesta en marcha.
-
Inicialización de la base de datos.
Primero, se crea un script SQL para inicializar la base de datos:
init.sql CREATE TABLE sampleusers(username text NOT NULL,password text NOT NULL,dni text NOT NULL UNIQUE,name text NOT NULL,surnames text NOT NULL,age integer NOT NULL,PRIMARY KEY (username));INSERT INTO sampleusers (username, password, dni, name, surnames, age) VALUES('juan', 'juanpass', '12345678C', 'Juan', 'Lopez Garrido', 30),('lidia', 'lidiapass', '12345678B', 'Lidia', 'Saez Martinez', 32),('maría', 'juanpass', '12345678D', 'Maria', 'Lopez Rodriguez', 35),('javi', 'javipass', '12345678A', 'Javier', 'Criado Rodriguez', 37); -
Creamos el docker-compose
A continuación, se crea un archivo
docker-compose.ymlpara levantar tanto WildFly con el artefacto.warcomo PostgreSQL con el scriptinit.sql.docker-compose.yml services:postgres:image: postgres:16.13-alpinecontainer_name: postgres_dbenvironment:POSTGRES_USER: estudiantePOSTGRES_PASSWORD: estudiantePOSTGRES_DB: dwscports:- "5432:5432"volumes:- postgres_data:/var/lib/postgresql/data- ./init.sql:/docker-entrypoint-initdb.d/init.sqlnetworks:- app-networkwildfly:image: quay.io/wildfly/wildfly:26.1.3.Final-jdk11container_name: wildfly_appports:- "8080:8080"- "9990:9990"volumes:- ./target/actividad3.war:/opt/jboss/wildfly/standalone/deployments/actividad3.wardepends_on:- postgresnetworks:- app-networknetworks:app-network:volumes:postgres_data: -
Comprobación del archivo WADL
Para comprobar que el servicio se ha desplegado correctamente, se puede acceder a la URL
http://localhost:8080/actividad3/api/application.wadl.
-
Comprobación de distintos servicios.
También puede comprobarse el endpoint de usuarios en JSON desde
http://localhost:8080/actividad3/api/users/json.
Ejercicios
Section titled “Ejercicios”1. ¿En qué formato se obtienen los datos al acceder a un usuario específico y por qué?
Al acceder a un usuario concreto, la respuesta se devuelve en formato
JSON. Esto ocurre porque el método está anotado con
@Produces("application/json"), por lo que Jersey genera la respuesta en
ese formato:
@Path("{username}") @GET @Produces("application/json") public Response getExampleUser(@PathParam("username") String username) { User user = this.getUserFromDB(username); if (user != null) { return Response.status(200).entity(user).build(); } else { return Response.status(400).build(); } }2. ¿Cómo se definen qué códigos de estado deben devolverse en cada operación?
Los códigos de estado HTTP se definen explícitamente utilizando el patrón
Builder de la clase javax.ws.rs.core.Response.
En lugar de devolver directamente el objeto, se construye la respuesta
indicando el código de estado, el cuerpo (entity) y finalizando con
build():
return Response.status(CODIGO_HTTP).entity(CUERPO_RESPUESTA).build();3. ¿Es posible cambiar el orden en el que se obtienen los usuarios sin cambiar el código de la consulta a la base de datos?
Sí, es posible. El orden puede cambiarse en Java, una vez recuperados los datos de la base de datos y antes de construir la respuesta HTTP.
Para ello se puede usar Collections.sort() junto con un Comparator
que defina el nuevo criterio de ordenación.
4. Se debe implementar una operación para insertar usuarios. Se debe tener en cuenta que no deben insertarse usuarios con nombre de usuario repetidos ni con DNI repetido.
Para cumplir este requisito, se realizan dos cambios principales: reforzar
la restricción en la base de datos y añadir un endpoint POST en el
servicio.
-
Modificar el script SQL
CREATE TABLE sampleusers(username text NOT NULL,password text NOT NULL,dni text NOT NULL,dni text NOT NULL UNIQUE,name text NOT NULL,surnames text NOT NULL,age integer NOT NULL,PRIMARY KEY (username)); -
Añadir el controlador POST en UserService
package sampleproject.service;import javax.ws.rs.GET;import javax.ws.rs.POST;import javax.ws.rs.Path;import javax.ws.rs.PathParam;import javax.ws.rs.Produces;import javax.ws.rs.Consumes;import javax.ws.rs.core.MediaType;import javax.ws.rs.core.Response;import java.sql.*;import java.util.Collections;import java.util.Comparator;import sampleproject.domain.User;import sampleproject.domain.Users;@Path("/users")public class UserService {@GET@Produces({ "application/json", "application/xml" })public Response getUsers() {Users users = this.getUsersFromDB();return Response.status(200).entity(users).build();}@Path("xml")@GET@Produces("application/xml")public Response getUsersXML() {Users users = this.getUsersFromDB();return Response.status(200).entity(users).build();}@Path("json")@GET@Produces("application/json")public Response getUsersJSON() {Users users = this.getUsersFromDB();return Response.status(200).entity(users).build();}@Path("{username}")@GET@Produces("application/json")public Response getExampleUser(@PathParam("username") String username) {User user = this.getUserFromDB(username);if (user != null) {return Response.status(200).entity(user).build();} else {return Response.status(400).build();}}@POST@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })@Produces(MediaType.APPLICATION_JSON)public Response createUser(User user) {if (user.getUsername() == null || user.getDni() == null) {return Response.status(400).entity("{\"error\": \"Username and DNI are required\"}").build();}if (this.existsUsername(user.getUsername())) {return Response.status(409).entity("{\"error\": \"Username already exists\"}").build();}if (this.existsDni(user.getDni())) {return Response.status(409).entity("{\"error\": \"DNI already exists\"}").build();}boolean inserted = this.insertUser(user);if (inserted) {return Response.status(201).entity(user).build();} else {return Response.status(500).entity("{\"error\": \"Failed to insert user\"}").build();}}private boolean existsUsername(String username) {Connection conn = this.connect2DB();try {PreparedStatement ps = conn.prepareStatement("SELECT COUNT(*) FROM sampleusers WHERE username = ?");ps.setString(1, username);ResultSet rs = ps.executeQuery();if (rs.next()) {return rs.getInt(1) > 0;}rs.close();ps.close();conn.close();} catch (SQLException e) {System.err.println("[UserService - existsUsername] SQLException: " + e.getMessage());}return false;}private boolean existsDni(String dni) {Connection conn = this.connect2DB();try {PreparedStatement ps = conn.prepareStatement("SELECT COUNT(*) FROM sampleusers WHERE dni = ?");ps.setString(1, dni);ResultSet rs = ps.executeQuery();if (rs.next()) {return rs.getInt(1) > 0;}rs.close();ps.close();conn.close();} catch (SQLException e) {System.err.println("[UserService - existsDni] SQLException: " + e.getMessage());}return false;}private boolean insertUser(User user) {Connection conn = this.connect2DB();try {PreparedStatement ps = conn.prepareStatement("INSERT INTO sampleusers (username, password, dni, name, surnames, age) VALUES (?, ?, ?, ?, ?, ?)");ps.setString(1, user.getUsername());ps.setString(2, user.getPassword());ps.setString(3, user.getDni());ps.setString(4, user.getName());ps.setString(5, user.getSurnames());ps.setInt(6, user.getAge());int result = ps.executeUpdate();ps.close();conn.close();return result > 0;} catch (SQLException e) {System.err.println("[UserService - insertUser] SQLException: " + e.getMessage());}return false;}private Connection connect2DB() {Connection conn = null;try {Class.forName("org.postgresql.Driver");String url = "jdbc:postgresql://postgres:5432/dwsc";conn = DriverManager.getConnection(url, "estudiante", "estudiante");} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException e) {e.printStackTrace();}return conn;}private Users getUsersFromDB() {Users users = new Users();Connection conn = this.connect2DB();try {Statement st = conn.createStatement();ResultSet rs = st.executeQuery("SELECT * FROM sampleusers ORDER BY age");while (rs.next()) {User user = new User();user.setUsername(rs.getString("username"));user.setPassword(rs.getString("password"));user.setDni(rs.getString("dni"));user.setName(rs.getString("name"));user.setSurnames(rs.getString("surnames"));user.setAge(rs.getInt("age"));users.add(user);}rs.close();st.close();conn.close();} catch (SQLException e) {System.err.println("[UserService - getUsersFromDB] SQLException while querying the users");System.err.println(e.getMessage());}return users;}private User getUserFromDB(String username) {User user = null;Connection conn = this.connect2DB();try {Statement st = conn.createStatement();ResultSet rs = st.executeQuery("SELECT * FROM sampleusers WHERE username = '" + username + "'");if (rs.next()) {user = new User();user.setUsername(rs.getString("username"));user.setPassword(rs.getString("password"));user.setDni(rs.getString("dni"));user.setName(rs.getString("name"));user.setSurnames(rs.getString("surnames"));user.setAge(rs.getInt("age"));}rs.close();st.close();conn.close();} catch (SQLException e) {System.err.println("[UserService - getUserFromDB] SQLException while querying a user");System.err.println(e.getMessage());}return user;}}El método
createUservalida primero queusernameydniestén presentes. Después comprueba que no existan valores repetidos en la base de datos mediante consultas conPreparedStatement. Si la inserción se realiza correctamente, devuelve201 Created; en caso contrario, responde con400,409o500, según el tipo de error.
5. Se debe modificar la operación que lista los usuarios para que estén ordenados por nombre de usuario. La consulta SQL no debe ser modificada.
Esto se resuelve ordenando la colección en memoria, después de recuperar
los datos de la base de datos y sin modificar la consulta SQL original.
Para ello se utiliza Collections.sort() junto con un Comparator que
ordena por nombre de usuario:
private Users getUsersFromDB() { Users users = new Users(); Connection conn = this.connect2DB(); try { Statement st = conn.createStatement(); ResultSet rs = st.executeQuery("SELECT * FROM sampleusers ORDER BY age"); while (rs.next()) { User user = new User(); user.setUsername(rs.getString("username")); user.setPassword(rs.getString("password")); user.setDni(rs.getString("dni")); user.setName(rs.getString("name")); user.setSurnames(rs.getString("surnames")); user.setAge(rs.getInt("age")); users.add(user); }
Collections.sort(users, Comparator.comparing(User::getUsername));
rs.close(); st.close(); conn.close(); } catch (SQLException e) { System.err.println("[UserService - getUsersFromDB] SQLException while querying the users"); System.err.println(e.getMessage()); } return users; }