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
minimoposti, 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
GETcon il campo "Posti minimi" già compilato con il valore corrente; minimonon numerico o negativo: risposta400.
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:
- Perché la JSP sta in
WEB-INF/jsp/e come ci si arriva? - Qual è la differenza fra
forwarderedirect, e quale si usa qui? - 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
/** 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
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);
}
}
}getParameterrestituisce sempre testo (onull): si converte conInteger.parseInte si gestisceNumberFormatException. Un valore negativo non ha senso: stessa risposta400consendError, ereturn(altrimenti si proseguirebbe e si farebbe anche il forward).- I due attributi
minimoeaulesono 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=45200, 2 aule, forward a/WEB-INF/jsp/aule-capienza.jsp;501 aula;100lista vuota; parametro assente 3 aule;abce-5400, nessun forward.
La view
<%@ 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>taglibcarica la libreria JSTL (con la dipendenza nel pom); la direttivapagefissa 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:chooseconc:whenche testaempty aule:emptyè vero per lista vuota onull.- Il ciclo
c:forEachconvar="a"sull'attributoaulescrive una riga per aula;a.postinell'EL chiamagetPosti(). - La scritta "(grande)" è un
c:ifche testaa.posti >= 60, dentro la cella (JSTL non haelse; con più rami si usac:choose). - Il totale è accumulato con un
c:setsutotale(valore: totale piùa.posti) a ogni giro (la variabile parte da 0 prima del ciclo) e scritto neltfoot. Conminimo= 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à (<b>Lab</b>) 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 ?:
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 3 aule, totale 155; 45 A1 e B2, 125; 50 A1, 80; 100 nessuna.
Controller.
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.
<%@ 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 onull;c:ifnon haelse(per più ramic:choose). Conminimo= 45: A1 (80) "(grande)", B2 (45), totale 125. Nessuno scriptlet.- Prova del controller con richieste simulate:
452 aule e forward;501;100vuota; assente 3;abce-5400.
Risposte.
WEB-INF/jsp/: il contenuto diWEB-INFnon è 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:
302conLocation, nuovaGET, attributi persi. Qui forward (si passano dati alla vista, richiesta di sola lettura); redirect dopo unaPOSTriuscita (PRG). - Aula
<b>Lab</b>: con la semplice espressione EL il browser interpreta l'HTML (grassetto; con unoscript, XSS); con<c:out>i caratteri sono sostituiti da entità e si legge la stringa.
Errori tipici
- Dimenticare
returndoposendError(si fa anche il forward). - Dati dell'utente scritti con l'EL senza
c:out; JSP fuori daWEB-INF; scriptlet nella vista. - Filtrare in Java invece che in SQL; concatenare
minimonella stringa SQL. - Aspettarsi gli attributi dopo un redirect; non gestire
NumberFormatException.