Table of Contents

Creación de una aplicación web

Creación del proyecto

Estructura del proyecto

Figure 1: Estructura de un proyecto JSP

Dependencias

pom.xml
. . .
<dependencies>
  <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
    <scope>provided</scope>
  </dependency>
 
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.30</version>
  </dependency>
 
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.26</version>
  </dependency>
 
  <dependency>
    <groupId>org.jdbi</groupId>
    <artifactId>jdbi3-core</artifactId>
    <version>3.37.1</version>
  </dependency>
 
  <dependency>
    <groupId>org.jdbi</groupId>
    <artifactId>jdbi3-sqlobject</artifactId>
    <version>3.37.1</version>
  </dependency>
</dependencies>
. . .
<build>
  <finalName>ParqueNaturalApp</finalName>
  <pluginManagement>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>3.2.2</version>
      </plugin>
 
      <plugin>
        <!--
        mvn tomcat7:deploy para desplegar
        mvn tomcat7:undeploy para replegar
        mvn tomcat7:redeploy para redesplegar
        -->
        <groupId>org.apache.tomcat.maven</groupId>
        <artifactId>tomcat7-maven-plugin</artifactId>
        <version>2.2</version>
 
        <configuration>
          <url>http://localhost:8082/manager/text</url>
          <server>tomcat9</server>
          <path>/parque</path>
          <username>tomcat</username>
          <password>tomcat</password>
        </configuration>
      </plugin>
    </plugins>
  </pluginManagement>
</build>
. . .

Ficheros de configuración del proyecto

context.xml
<?xml version="1.0" encoding="UTF-8" ?>
<Context path="/amazon"/>
web.xml
<?xml version="1.0" encoding="UTF-8"?>
 
<web-app>
  <display-name>Amazon web application</display-name>
</web-app>

Base de datos

CREATE TABLE activities (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(50) UNIQUE NOT NULL,
  description VARCHAR(250),
  datetime DATETIME,
  price FLOAT,
  picture VARCHAR(50)
);
 
CREATE TABLE users (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(50) NOT NULL DEFAULT 'no-name',
  username VARCHAR(50) UNIQUE NOT NULL,
  password VARCHAR(40) NOT NULL,
  ROLE VARCHAR(50) DEFAULT 'user'
);

Creación de una página JSP

Creación de un Servlet

Crear un formulario para registrar información

Formulario JSP

<section class="py-5 container">
  <h1>Registrar nueva Actividad</h1>
  <h1>Modificar Actividad</h1>
  <form class="row g-3 needs-validation" method="post" enctype="multipart/form-data" id="edit-form">
    <div class="mb-3">
      <label for="name" class="form-label">Nombre</label>
      <input type="text" name="name" class="form-control" id="name" placeholder="Ir a caminar"
    </div>
    <div class="mb-3">
      <label for="description" class="form-label">Descripción</label>
      <textarea rows="4" name="description" cols="50" class="form-control" id="description" placeholder="Descripción de la actividad"></textarea>
    </div>
    <div class="col-md-4">
      <label for="date" class="form-label">Fecha</label>
      <input type="date" name="date" class="form-control" id="date" placeholder="dd/mm/yyyy"
    </div>
    <div class="col-md-4">
      <label for="price" class="form-label">Precio</label>
      <input type="text" name="price" class="form-control" id="price" placeholder="15,00"
    </div>
    <div class="col-md-4">
      <label for="picture" class="form-label">Foto</label>
      <input type="file" name="picture" class="form-control" id="picture">
    </div>
    <div class="col-12">
      <input type="submit" value="Enviar" id="edit-button"/>
    </div>
    <input type="hidden" name="id" value="<%= id %>"/>
  </form>
  <br/>
  <div id="result"></div>
</section>

Servlet para procesar la información

@WebServlet("/edit-activity")
@MultipartConfig
public class EditActivity extends HttpServlet {
 
  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    response.setContentType("text/html");
    response.setCharacterEncoding("UTF-8");
 
    HttpSession currentSession = request.getSession();
    if (currentSession.getAttribute("role") != null) {
      if (!currentSession.getAttribute("role").equals("admin")) {
        response.sendRedirect("/parque");
      }
    }
 
    try {
      if (hasValidationErrors(request, response))
        return;
 
      int id = 0;
      if (request.getParameter("id") != null) {
        id = Integer.parseInt(request.getParameter("id"));
      }
 
      String name = request.getParameter("name");
      String description = request.getParameter("description");
      Date date = DateUtils.parse(request.getParameter("date"));
      float price = CurrencyUtils.parse(request.getParameter("price"));
      Part picturePart = request.getPart("picture");
 
      // Guardar la imagen en disco
      String imagePath = request.getServletContext().getInitParameter("image-path");
      String filename = null;
      if (picturePart.getSize() == 0) {
        filename = "no-image.jpg";
      } else {
        filename = UUID.randomUUID() + ".jpg";
        InputStream fileStream = picturePart.getInputStream();
        Files.copy(fileStream, Path.of(imagePath + File.separator + filename));
      }
 
      Database.connect();
      final String finalFilename = filename;
      if (id == 0) {
        int affectedRows = Database.jdbi.withExtension(ActivityDao.class,
           dao -> dao.addActivity(name, description, date, price, finalFilename));
        sendMessage("Actividad registrada correctamente", response);
      } else {
        final int finalId = id;
        int affectedRows = Database.jdbi.withExtension(ActivityDao.class,
          dao -> dao.updateActivity(name, description, date, price, finalFilename, finalId));
        sendMessage("Actividad modificada correctamente", response);
      }
    } catch (ParseException pe) {
      pe.printStackTrace();
      sendError("El formato de fecha o precio no es correcto", response);
    } catch (ClassNotFoundException cnfe) {
      cnfe.printStackTrace();
      sendError("Internal Server Error", response);
    } catch (SQLException sqle) {
      sqle.printStackTrace();
      sendError("Error conectando con la base de datos", response);
    }
  }
 
  private boolean hasValidationErrors(HttpServletRequest request, HttpServletResponse response) throws IOException {
    boolean hasErrors = false;
 
    if (request.getParameter("name") == null) {
      sendError("El nombre es un campo obligatorio", response);
      hasErrors = true;
    }
    try {
      DateUtils.parse(request.getParameter("date"));
    } catch (ParseException pe) {
      sendError("Formato de fecha no válido", response);
      hasErrors = true;
    }
    try {
      float priceValue = CurrencyUtils.parse(request.getParameter("price"));
    } catch (ParseException pe) {
      sendError("Formato de precio no válido", response);
      hasErrors = true;
    }
 
    return hasErrors;
  }
}

Envío asíncrono del formulario

<script>
  $(document).ready(function () {
    $("#edit-button").click(function (event) {
      event.preventDefault();
      const form = $("#edit-form")[0];
      const data = new FormData(form);
 
      $("#edit-button").prop("disabled", true);
        $.ajax({
          type: "POST",
          enctype: "multipart/form-data",
          url: "edit-activity",
          data: data,
          processData: false,
          contentType: false,
          cache: false,
          timeout: 600000,
          success: function (data) {
            $("#result").html(data);
            $("#edit-button").prop("disabled", false);
          },
          error: function (error) {
            $("#result").html(error.responseText);
            $("#edit-button").prop("disabled", false);
          }
       });
    });
  });
</script>

Como se puede ver en el código, utilizamos la capa result para mostrar el resultado de procesar el formulario, tanto para caso de error como para cuando todo va bien. El Servlet nos enviará un pequeño fragmento html con el código para mostrar un alert siguiendo el diseño que proporciona Bootstrap. Al procesarlo de forma asíncrona, no es necesario que se recargue la web en ninguno de los casos.

Figure 2: Formulario procesado correctamente
Figure 3: Formulario con errores

Soporte para envío de imágenes en el formulario

Podemos dejar fijada en el fichero web.xml la ruta donde queremos que se almacenen las imágenes (o cualquier tipo de fichero) que queramos que los usuarios adjunten a través de un formulario.

web.xml
<?xml version="1.0" encoding="UTF-8" ?>
<web-app>
  <display-name>Parques Naturales</display-name>
 
  <context-param>
    <description>Ruta donde guardar las imagenes</description>
    <param-name>image-path</param-name>
    <param-value>/home/santi/apache-tomcat-9.0.86/webapps/parque_pictures</param-value>
  </context-param>
</web-app>

Hay que tener en cuenta que tendremos que definir nuestro formulario con enctype igual a multipart/from-data para que éste sea capaz de enviar ficheros (de cualquier tipo, incluidas las imágenes):

. . .
<form class="row g-3 needs-validation" method="post" enctype="multipart/form-data" id="edit-form">
. . .

En nuestro caso estamos aprendiendo a procesar los formularios de forma asíncrona utilizando Ajax. Un poco más arriba, en https://java.codeandcoke.com/apuntes:web#envio_asincrono_del_formulario, se puede encontrar un ejemplo de script Ajax para procesar formularios que incluyen algún tipo de fichero.

A la hora de procesar el formulario, en el Servlet que se encargue de él, podemos adjuntar un código como el siguiente para recoger la imagen enviada y copiarla a esa carpeta. Más adelante, podemos guardar el nombre del fichero en base de datos junto con el objeto que estemos registrando con el formulario. De esa manera podremos recuperarla más adelante para mostrarla en la web.

También podemos disponer de una imagen por defecto, en nuestro caso llamada no-image.jpg, para los casos en los que los usuarios no adjunten ninguna. Asi, les añadiremos una genérica que siempre será mejor que no disponer de ninguna.

. . .
// Recoge la imagen adjuntada en el formulario a través de un 'input' de tipo 'file' con el nombre 'picture'
Part picturePart = request.getPart("picture");
 
// Ruta base para las imágenes. Coge el valor que hay fijado en 'web.xml' con ese nombre
String imagePath = request.getServletContext().getInitParameter("image-path");
String filename = null;
if (picturePart.getSize() == 0) {
  // El usuario no adjunta foto al formulario. Se le asigna una que ya tenemos preparada por defecto
  filename = "no-image.jpg";
} else {
  // El usuario adjunta foto al formulario. Generamos nombre aleatorio y la movemos a la ruta destino que hemos configurado en 'web.xml'
  filename = UUID.randomUUID() + ".jpg";
  InputStream fileStream = picturePart.getInputStream();
  Files.copy(fileStream, Path.of(imagePath + File.separator + filename));
}
. . .

Crear una página JSP para mostrar información

JSTL: JavaServer Pages Standard Tag Library

<dependencies>
  . . .
  <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
  </dependency>
  . . .
</dependencies>
. . .
<%@ taglib uri = "http://java.sun.com/jsp/jstl/core" prefix = "c" %>
. . .
<%
  . . .
  pageContext.setAttribute("nombreAtributo", "valorAtributo");
  . . .
%>
. . .
<c:out value="${nombreAtributo}"/>
. . .
. . .
<c:if test="${nombreAtributo=='valorAtributo'}">
  <c:out value="El atributo 'nombreAtributo' es igual a 'valorAtributo'"/>
</c:if>
. . .

Uso de sesiones. Login y área privada

Inicio de sesión. Login

login.jsp
. . .
<script type="text/javascript">
  $(document).ready(function() {
    $("form").on("submit", function(event) {
      event.preventDefault();
      const formValue = $(this).serialize();
      $.ajax("login", {
        type: "POST",
        data: formValue,
        statusCode: {
          200: function(response) {
            if (response === "ok") {
              window.location.href = "/parque";
            } else {
              $("#result").html(response);
            }
          }
        }
      });
    });
  });
</script>
. . .
login.jsp
<main class="form-signin w-100 m-auto">
  <form>
    <h1 class="h3 mb-3 fw-normal">Iniciar sesión</h1>
    <div class="form-floating">
      <input type="text" name="username" class="form-control" id="floatingInput" placeholder="Usuario">
      <label for="floatingInput">Usuario</label>
    </div>
    <div class="form-floating">
      <input type="password" name="password" class="form-control" id="floatingPassword" placeholder="Contraseña">
      <label for="floatingPassword">Contraseña</label>
    </div>
 
    <button class="btn btn-primary w-100 py-2" type="submit">Iniciar sesión</button>
  </form>
  <br/>
  <div id="result"></div>
</main>
. . .
LoginServlet.java
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
 
  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    response.setContentType("text/html");
    response.setCharacterEncoding("UTF-8");
 
    String username = request.getParameter("username");
    String password = request.getParameter("password");
 
    try {
      Database.connect();
      User user = Database.jdbi.withExtension(UserDao.class,
        dao -> dao.getUser(username, password));
      if (user != null) {
        HttpSession session = request.getSession();
        session.setAttribute("username", user.getUsername());
        session.setAttribute("role", user.getRole());
        response.getWriter().print("ok");
      } else {
        sendError("El usuario no existe", response);
      }
    } catch (ClassNotFoundException cnfe) {
      cnfe.printStackTrace();
      sendError("Internal Server Error", response);
    } catch (SQLException sqle) {
      sqle.printStackTrace();
      sendError("Error conectando con la base de datos", response);
    }
  }
}

Mantenimiento de la sesión


© 2023-2024 Santiago Faci