Salta al contenuto
Note per Studenti Esercizio - Elenco delle aule con MVC e JSP (applicazione web)

Esercizio - Elenco delle aule con MVC e JSP (applicazione web)

Esercizio originale, nello stile della prova di programmazione. Teoria: JSP e pattern Model-View-ControllerScrivere l'HTML con println dentro la servlet mescola logica e presentazione. Una JSP (Jakarta Pages) è una pagina HTML con parti dinamiche che Jasper traduce in una servlet alla prima richiesta (quindi vale la stessa concorrenza). Si usano la direttiva page, le espressioni EL (Expression Language) per leggere attributi e proprietà dei JavaBean (la proprietà nome chiama getNome), gli oggetti impliciti (param, requestScope, sessionScope, pageContext) e le librerie JSTL core (c:out, c:if, c:choose, c:forEach); l'EL non fa escape, quindi i dati dell'utente passano da c:out. Il pattern Model-View-Controller separa il model (JavaBean e DAO: dati e regole), la view (JSP, che mostra) e il controller (servlet: legge la richiesta, chiama il model, mette i risultati negli attributi della richiesta e fa il forward alla view). Le JSP stanno in WEB-INF così non si raggiungono direttamente. Forward: stessa richiesta, stesso URL, attributi condivisi, lato server; redirect: risposta 302 con Location, nuova richiesta GET, URL cambiato, attributi persi. Il pattern Post/Redirect/Get risponde alla POST con un redirect a una GET, per evitare che il ricaricamento rinvii il form; il messaggio di conferma passa in sessione (flash) e si mostra una volta sola; in caso di errore invece si fa il forward alla stessa pagina con il form compilato e lo stato 4xx.JSP e pattern Model-View-Controller →, Java servlet e Apache Tomcat - ciclo di vita, richieste e sessioniUna servlet è una classe Java che un container (Apache Tomcat: connettore HTTP Coyote, contenitore Catalina, compilatore JSP Jasper) istanzia una sola volta e chiama per ogni richiesta, ciascuna in un thread del pool. Ciclo di vita: caricamento e costruzione, init() una volta, service() per ogni richiesta (HttpServlet lo smista su doGet, doPost, doPut, doDelete), destroy() allo spegnimento. La richiesta (HttpServletRequest) dà metodo, parametri (query string e corpo del form), header, path (contextPath, servletPath, pathInfo) e corpo; la risposta (HttpServletResponse) si costruisce con stato, header e corpo (PrintWriter), con sendRedirect (302 + Location) o sendError. Il mapping si dichiara con @WebServlet o in WEB-INF/web.xml (che prevale). Poiché esiste una sola istanza e molti thread, i campi di istanza sono condivisi: lo stato della richiesta sta in variabili locali, quello condiviso in oggetti thread-safe (AtomicInteger, DataSource). HTTP è senza stato, quindi si usano le sessioni: la servlet crea un HttpSession, tiene i dati sul server e manda al browser solo l'identificatore nel cookie JSESSIONID (Set-Cookie con HttpOnly, Secure, SameSite, Path secondo RFC 6265); al login si cambia l'identificatore (changeSessionId) contro la session fixation. I filtri (Filter, FilterChain) elaborano richiesta e risposta prima e dopo le servlet: codifica, autenticazione, log. Il war ha la struttura WEB-INF/classes, WEB-INF/lib, WEB-INF/web.xml, META-INF/context.xml; l'accesso ai dati usa un DataSource con pool di connessioni dichiarato in context.xml e cercato con JNDI, e un DAO con PreparedStatement e try-with-resources.Java servlet e Apache Tomcat - ciclo di vita, richieste e sessioni →. Il controller e il DAO sono stati compilati contro una copia delle sole firme dell'API Jakarta Servlet e provati con richieste simulate (parametro valido, assente, non valido, forward); la query sulle aule è stata provata con SQLite sui dati dell'esempio. La JSP non è stata eseguita (non c'è un container): è stata scritta e riletta seguendo la specifica EL e JSTL.

Testo

Nell'applicazione delle aule (tabella aula(id, nome, posti) con Aula A1 80, Aula B2 45, Lab Info 30) si vuole la pagina GET /aule/capienza?minimo=N:

  • elenca in una tabella le aule con almeno minimo posti, in ordine di nome; senza il parametro, tutte;
  • le aule da 60 posti in su hanno accanto la scritta "(grande)";
  • in fondo c'è il totale dei posti delle aule mostrate;
  • se nessuna aula soddisfa il filtro, un messaggio al posto della tabella;
  • sopra la tabella un form GET con il campo "Posti minimi" già compilato con il valore corrente;
  • minimo non numerico o negativo: risposta 400.

Si richiede di rispettare il pattern MVC: dire quali classi e file fanno da model, view, controller, scrivere il metodo del DAO, il controller e la JSP, e rispondere:

  1. Perché la JSP sta in WEB-INF/jsp/ e come ci si arriva?
  2. Qual è la differenza fra forward e redirect, e quale si usa qui?
  3. Se un'aula si chiamasse <b>Lab</b>, che cosa mostrerebbe la pagina scrivendo il nome con una semplice espressione EL e con <c:out>?

Soluzione

Ruoli

Ruolo Elemento
Model Aula (JavaBean), AulaDAO con il nuovo metodo conPostiAlmeno(int minimo)
Controller AuleCapienzaServlet (mappata su /aule/capienza)
View WEB-INF/jsp/aule-capienza.jsp

Il model non sa nulla di HTTP; la vista non contiene SQL né logica di accesso ai dati; il controller legge e valida il parametro, chiama il model, imposta gli attributi e fa il forward.

Il model: una query parametrizzata

java
/** Aule con almeno "minimo" posti, in ordine di nome. */
public List<Aula> conPostiAlmeno(int minimo) throws SQLException {
    String sql = "SELECT id, nome, posti FROM aula WHERE posti >= ? ORDER BY nome";
    try (Connection c = ds.getConnection();
         PreparedStatement ps = c.prepareStatement(sql)) {
        ps.setInt(1, minimo);
        try (ResultSet rs = ps.executeQuery()) {
            List<Aula> aule = new ArrayList<>();
            while (rs.next()) {
                aule.add(new Aula(rs.getInt("id"), rs.getString("nome"), rs.getInt("posti")));
            }
            return aule;
        }
    }
}

Il filtro sta nell'SQL (non si portano tutte le righe nell'applicazione per scartarle in Java: Organizzazione fisica dei dati e accesso programmatico alle basi di datiUn DBMS è formato da un gestore delle connessioni, un parser, un ottimizzatore che sceglie il piano di esecuzione usando le statistiche del catalogo, un esecutore, i metodi di accesso (heap, ordinato, hash, B-tree, indici), il buffer manager e il gestore delle transazioni (lock e log). Il costo si misura in accessi a blocchi del disco. L'ottimizzatore riscrive l'espressione algebrica (selezioni e proiezioni il prima possibile, prodotto con selezione = join, ordine dei join) e sceglie gli algoritmi: scansione o indice per le selezioni, nested loop, block nested loop, index nested loop, sort-merge o hash join per i join (con 1000 e 100 blocchi, 10 pagine di buffer: 11 mila accessi con block nested loop, 3300 con hash join). La progettazione fisica sceglie organizzazione dei file, indici ed eventuale denormalizzazione in base al carico. Le applicazioni accedono ai dati con SQL incorporato, interfacce a livello di chiamata (ODBC, JDBC, DB-API di Python), ORM o routine memorizzate: connessione, cursore, execute con parametri, fetch, commit o rollback. Mai concatenare l'input dell'utente nell'SQL (SQL injection): query parametrizzate. Un'applicazione web è a tre livelli: browser, server applicativo (HTTP senza stato, sessioni e cookie) e DBMS.Organizzazione fisica dei dati e accesso programmatico alle basi di dati →) e il valore passa come parametro ?. Prova sui dati dell'esempio:

minimo aule restituite totale posti
0 (o assente) Aula A1, Aula B2, Lab Info 155
45 Aula A1, Aula B2 125
50 Aula A1 80
100 nessuna 0

Il controller

java
package it.unipd.aule.mvc;

import it.unipd.aule.dao.AulaDAO;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.sql.SQLException;

/** Controller di /aule/capienza?minimo=N: le aule con almeno N posti (0 se il parametro manca). */
@WebServlet("/aule/capienza")
public class AuleCapienzaServlet extends HttpServlet {
    private AulaDAO aulaDAO;

    @Override
    public void init() {
        aulaDAO = (AulaDAO) getServletContext().getAttribute("aulaDAO");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String testo = req.getParameter("minimo");
        int minimo = 0;
        if (testo != null && !testo.isBlank()) {
            try {
                minimo = Integer.parseInt(testo.trim());
            } catch (NumberFormatException e) {
                minimo = -1;                                         // segnala "non valido" al controllo seguente
            }
            if (minimo < 0) {
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "minimo deve essere un intero non negativo");
                return;
            }
        }
        try {
            req.setAttribute("minimo", minimo);
            req.setAttribute("aule", aulaDAO.conPostiAlmeno(minimo));
            req.getRequestDispatcher("/WEB-INF/jsp/aule-capienza.jsp").forward(req, resp);
        } catch (SQLException e) {
            throw new ServletException(e);
        }
    }
}
  • getParameter restituisce sempre testo (o null): si converte con Integer.parseInt e si gestisce NumberFormatException. Un valore negativo non ha senso: stessa risposta 400 con sendError, e return (altrimenti si proseguirebbe e si farebbe anche il forward).
  • I due attributi minimo e aule sono i dati per la vista, valgono per questa richiesta.
  • getRequestDispatcher(...).forward(req, resp) passa il controllo alla JSP: dopo il forward il controller non scrive più nulla.
  • Provato con richieste simulate: minimo=45 →\to 200, 2 aule, forward a /WEB-INF/jsp/aule-capienza.jsp; 50 →\to 1 aula; 100 →\to lista vuota; parametro assente →\to 3 aule; abc e -5 →\to 400, nessun forward.

La view

jsp
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!DOCTYPE html>
<html lang="it">
<head>
  <meta charset="utf-8">
  <title>Aule con almeno ${minimo} posti</title>
</head>
<body>
  <h1>Aule con almeno ${minimo} posti</h1>

  <form method="get" action="${pageContext.request.contextPath}/aule/capienza">
    <label>Posti minimi <input type="number" name="minimo" min="0" value="${minimo}"></label>
    <button>Filtra</button>
  </form>

  <c:choose>
    <c:when test="${empty aule}">
      <p>Nessuna aula ha almeno ${minimo} posti.</p>
    </c:when>
    <c:otherwise>
      <table>
        <thead><tr><th>Nome</th><th>Posti</th></tr></thead>
        <tbody>
          <c:set var="totale" value="0"/>
          <c:forEach var="a" items="${aule}">
            <tr>
              <td><c:out value="${a.nome}"/><c:if test="${a.posti >= 60}"> (grande)</c:if></td>
              <td>${a.posti}</td>
            </tr>
            <c:set var="totale" value="${totale + a.posti}"/>
          </c:forEach>
        </tbody>
        <tfoot><tr><td>Totale</td><td>${totale}</td></tr></tfoot>
      </table>
    </c:otherwise>
  </c:choose>
</body>
</html>
  • taglib carica la libreria JSTL (con la dipendenza nel pom); la direttiva page fissa il tipo e la codifica della risposta.
  • L'espressione EL su minimo, nel titolo e nel campo del form, riscrive il valore dell'attributo della richiesta (è un intero, non servirebbe l'escape).
  • c:choose con c:when che testa empty aule: empty è vero per lista vuota o null.
  • Il ciclo c:forEach con var="a" sull'attributo aule scrive una riga per aula; a.posti nell'EL chiama getPosti().
  • La scritta "(grande)" è un c:if che testa a.posti >= 60, dentro la cella (JSTL non ha else; con più rami si usa c:choose).
  • Il totale è accumulato con un c:set su totale (valore: totale più a.posti) a ogni giro (la variabile parte da 0 prima del ciclo) e scritto nel tfoot. Con minimo = 45 la pagina mostra Aula A1 (80) con "(grande)", Aula B2 (45) senza, e Totale 125.
  • Non c'è Java nella pagina: nessuno scriptlet.

Risposte alle domande

1. WEB-INF/jsp/. Il contenuto di WEB-INF non viene mai servito direttamente ai client: nessun URL del tipo /WEB-INF/jsp/aule-capienza.jsp funziona dal browser. La JSP è quindi raggiungibile solo passando dal controller, con il forward interno, e non può essere chiamata senza gli attributi che il controller prepara (altrimenti mostrerebbe una pagina vuota o rotta). Il browser conosce solo /aule/capienza.

2. Forward o redirect. Il forward è un passaggio interno al server: stessa richiesta, stessi attributi, l'URL nel browser non cambia. Il redirect è una risposta 302 con Location: il browser fa una nuova GET e gli attributi della richiesta si perdono. Qui serve il forward, perché gli attributi aule e minimo devono arrivare alla vista, e perché la richiesta è una GET che non modifica niente. Il redirect si userebbe dopo una POST riuscita (pattern Post/Redirect/Get).

3. <b>Lab</b>. Con una semplice espressione EL sul nome il testo viene scritto così com'è nell'HTML e il browser lo interpreta: appare la parola "Lab" in grassetto; se invece il nome contenesse un elemento script, il codice verrebbe eseguito per chiunque guardi la pagina (XSS). Con <c:out> sul nome i caratteri speciali sono sostituiti con entità (&lt;b&gt;Lab&lt;/b&gt;) e la pagina mostra la stringa <b>Lab</b>. Per questo c:out si usa per ogni dato che può essere scritto da un utente; il numero a.posti è sicuro.

Versione ripasso

Richiesta. GET /aule/capienza?minimo=N: tabella delle aule con almeno minimo posti in ordine di nome (tutte se manca), "(grande)" da 60 posti in su, totale dei posti, messaggio se nessuna soddisfa il filtro, form GET con "Posti minimi" già compilato, 400 se minimo non è intero non negativo.

Ruolo Elemento
Model Aula (JavaBean), AulaDAO.conPostiAlmeno(int)
Controller AuleCapienzaServlet su /aule/capienza
View WEB-INF/jsp/aule-capienza.jsp

Model. Filtro nell'SQL con parametro ?:

java
String sql = "SELECT id, nome, posti FROM aula WHERE posti >= ? ORDER BY nome";
try (Connection c = ds.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
    ps.setInt(1, minimo);
    try (ResultSet rs = ps.executeQuery()) { /* una Aula per riga */ }
}

Risultati sui dati dell'esempio: 0 →\to 3 aule, totale 155; 45 →\to A1 e B2, 125; 50 →\to A1, 80; 100 →\to nessuna.

Controller.

java
String testo = req.getParameter("minimo");
int minimo = 0;
if (testo != null && !testo.isBlank()) {
    try { minimo = Integer.parseInt(testo.trim()); } catch (NumberFormatException e) { minimo = -1; }
    if (minimo < 0) {
        resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "minimo deve essere un intero non negativo");
        return;                                   // niente forward
    }
}
req.setAttribute("minimo", minimo);
req.setAttribute("aule", aulaDAO.conPostiAlmeno(minimo));
req.getRequestDispatcher("/WEB-INF/jsp/aule-capienza.jsp").forward(req, resp);

View.

jsp
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:choose>
  <c:when test="${empty aule}"><p>Nessuna aula ha almeno ${minimo} posti.</p></c:when>
  <c:otherwise>
    <table>
      <c:set var="totale" value="0"/>
      <c:forEach var="a" items="${aule}">
        <tr><td><c:out value="${a.nome}"/><c:if test="${a.posti >= 60}"> (grande)</c:if></td><td>${a.posti}</td></tr>
        <c:set var="totale" value="${totale + a.posti}"/>
      </c:forEach>
      <tfoot><tr><td>Totale</td><td>${totale}</td></tr></tfoot>
    </table>
  </c:otherwise>
</c:choose>
  • empty è vero per lista vuota o null; c:if non ha else (per più rami c:choose). Con minimo = 45: A1 (80) "(grande)", B2 (45), totale 125. Nessuno scriptlet.
  • Prova del controller con richieste simulate: 45 →\to 2 aule e forward; 50 →\to 1; 100 →\to vuota; assente →\to 3; abc e -5 →\to 400.

Risposte.

  • WEB-INF/jsp/: il contenuto di WEB-INF non è servito ai client; la JSP si raggiunge solo dal controller con il forward, che prepara gli attributi.
  • Forward: interno, stessa richiesta, attributi conservati, URL invariato. Redirect: 302 con Location, nuova GET, attributi persi. Qui forward (si passano dati alla vista, richiesta di sola lettura); redirect dopo una POST riuscita (PRG).
  • Aula <b>Lab</b>: con la semplice espressione EL il browser interpreta l'HTML (grassetto; con uno script, XSS); con <c:out> i caratteri sono sostituiti da entità e si legge la stringa.

Errori tipici

  • Dimenticare return dopo sendError (si fa anche il forward).
  • Dati dell'utente scritti con l'EL senza c:out; JSP fuori da WEB-INF; scriptlet nella vista.
  • Filtrare in Java invece che in SQL; concatenare minimo nella stringa SQL.
  • Aspettarsi gli attributi dopo un redirect; non gestire NumberFormatException.

Teoria: JSP e pattern Model-View-ControllerScrivere l'HTML con println dentro la servlet mescola logica e presentazione. Una JSP (Jakarta Pages) è una pagina HTML con parti dinamiche che Jasper traduce in una servlet alla prima richiesta (quindi vale la stessa concorrenza). Si usano la direttiva page, le espressioni EL (Expression Language) per leggere attributi e proprietà dei JavaBean (la proprietà nome chiama getNome), gli oggetti impliciti (param, requestScope, sessionScope, pageContext) e le librerie JSTL core (c:out, c:if, c:choose, c:forEach); l'EL non fa escape, quindi i dati dell'utente passano da c:out. Il pattern Model-View-Controller separa il model (JavaBean e DAO: dati e regole), la view (JSP, che mostra) e il controller (servlet: legge la richiesta, chiama il model, mette i risultati negli attributi della richiesta e fa il forward alla view). Le JSP stanno in WEB-INF così non si raggiungono direttamente. Forward: stessa richiesta, stesso URL, attributi condivisi, lato server; redirect: risposta 302 con Location, nuova richiesta GET, URL cambiato, attributi persi. Il pattern Post/Redirect/Get risponde alla POST con un redirect a una GET, per evitare che il ricaricamento rinvii il form; il messaggio di conferma passa in sessione (flash) e si mostra una volta sola; in caso di errore invece si fa il forward alla stessa pagina con il form compilato e lo stato 4xx.JSP e pattern Model-View-Controller →.

Teoria collegata