Skip to content

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.

Para desarrollar la actividad se utilizará un proyecto Maven. Además, configuraremos Jersey para exponer el servicio RESTful y WildFly como servidor de aplicaciones.

  1. Inicialización del proyecto Maven

    Se crea un proyecto Maven con empaquetado war usando el siguiente comando:

    Terminal window
    mvn archetype:generate -DgroupId=es.ual -DartifactId=actividad3 -DarchetypeArtifactId=maven-archetype-webapp -Dpackaging=war -DinteractiveMode=false
  2. Modificar web.xml

    A continuación, se modifica el archivo web.xml para 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>
  3. Modificar pom.xml

    Después, se actualiza el archivo pom.xml para 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>

Una vez inicializado y configurado el proyecto, se crean las clases y el servicio de ejemplo que formarán la API.

  1. Creamos la entidad User

    Para representar la estructura de datos de cada usuario, se crea la clase User con 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;
    }
  2. Creamos la entidad Users

    También se crea una clase Users para 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")
    @XmlRootElement
    public class Users extends ArrayList<User> {
    @XmlElement(name = "user")
    public List<User> getUsers() {
    return this;
    }
    }
  3. 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;
    }
    }
  4. Generamos el artefacto war

    Por último, se genera el artefacto .war con el siguiente comando:

    Terminal window
    mvn clean package

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.

  1. 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);
  2. Creamos el docker-compose

    A continuación, se crea un archivo docker-compose.yml para levantar tanto WildFly con el artefacto .war como PostgreSQL con el script init.sql.

    docker-compose.yml
    services:
    postgres:
    image: postgres:16.13-alpine
    container_name: postgres_db
    environment:
    POSTGRES_USER: estudiante
    POSTGRES_PASSWORD: estudiante
    POSTGRES_DB: dwsc
    ports:
    - "5432:5432"
    volumes:
    - postgres_data:/var/lib/postgresql/data
    - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
    - app-network
    wildfly:
    image: quay.io/wildfly/wildfly:26.1.3.Final-jdk11
    container_name: wildfly_app
    ports:
    - "8080:8080"
    - "9990:9990"
    volumes:
    - ./target/actividad3.war:/opt/jboss/wildfly/standalone/deployments/actividad3.war
    depends_on:
    - postgres
    networks:
    - app-network
    networks:
    app-network:
    volumes:
    postgres_data:
  3. 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.

    WADL Application Documentation
  4. Comprobación de distintos servicios.

    También puede comprobarse el endpoint de usuarios en JSON desde http://localhost:8080/actividad3/api/users/json.

    Users Documentation

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:

src/main/java/sampleproject/service/UserService.java
@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.

  1. 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)
    );
  2. 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 createUser valida primero que username y dni estén presentes. Después comprueba que no existan valores repetidos en la base de datos mediante consultas con PreparedStatement. Si la inserción se realiza correctamente, devuelve 201 Created; en caso contrario, responde con 400, 409 o 500, 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;
}