Salta al contenuto
Note per Studenti Esercizio - Servlet delle prenotazioni con controllo delle sovrapposizioni (applicazione web)

Esercizio - Servlet delle prenotazioni con controllo delle sovrapposizioni (applicazione web)

Esercizio originale, nello stile della prova di programmazione (servlet con accesso al database). Teoria: 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 →; SQL: SQL - interrogazioni, join e aggregazioniUn'interrogazione SQL ha almeno SELECT e FROM; le altre clausole (WHERE, GROUP BY, HAVING, ORDER BY) sono opzionali e HAVING richiede GROUP BY. Ordine logico di valutazione: FROM, WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY. SELECT corrisponde a π (ma senza DISTINCT non elimina i duplicati), WHERE a σ, gli alias ad AS/ρ, i join a ⋈ (INNER, LEFT, RIGHT, FULL). WHERE seleziona le righe prima del raggruppamento, HAVING seleziona i gruppi dopo. Le funzioni COUNT, SUM, AVG, MIN, MAX ignorano i NULL (COUNT(*) conta le righe, DISTINCT toglie i duplicati dentro la funzione); con un aggregato o con GROUP BY nella SELECT possono comparire solo colonne di raggruppamento o aggregati. LIKE usa % (qualsiasi sequenza) e _ (un carattere). I confronti con NULL non sono veri: serve IS NULL. Un filtro sulla tabella di destra di un LEFT JOIN va nella clausola ON, non in WHERE, altrimenti il join diventa interno. Con interi COUNT(a)/COUNT(b) è una divisione intera: serve CAST.SQL - interrogazioni, join e aggregazioni →, SQL avanzato - indici, transazioni e triggerUn file di record può essere heap (inserimento in ordine, ricerca per uguaglianza in media n/2 accessi), ordinato (log2 n), hash (costante, solo uguaglianza) o B-tree (log_F n, anche intervalli). Un indice è una struttura ausiliaria di coppie (chiave K, puntatore P) ordinate, molto più piccola della tabella: l'indice primario è sul campo di ordinamento del file ordinato (una chiave per blocco, non denso, log2(m)+1 accessi), l'indice secondario è su un campo qualsiasi (denso). CREATE [UNIQUE] INDEX, anche su espressioni; PostgreSQL usa tabelle heap con indici secondari B-tree (predefinito) o hash, e PRIMARY KEY e UNIQUE creano un indice implicito. Gli indici velocizzano le letture ma costano nelle scritture e in spazio. Una transazione è una sequenza atomica di operazioni con proprietà ACID (atomicità, consistenza, isolamento, durabilità): START TRANSACTION, COMMIT, SAVEPOINT, ROLLBACK [TO SAVEPOINT]. Un trigger è una regola evento-condizione-azione (ECA): BEFORE/AFTER, ROW/STATEMENT, WHEN, con funzione PL/pgSQL che usa NEW e OLD; serve per vincoli complessi e attributi derivati.SQL avanzato - indici, transazioni e trigger →; JDBC: 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 →. La condizione SQL è stata provata con SQLite sui dati dell'esempio; il codice Java è stato compilato contro una copia delle sole firme dell'API Jakarta Servlet.

Testo

Schema e dati (esempio conduttore, vedi Architettura delle applicazioni web - browser, server web e livelliUn'applicazione web è distribuita su più livelli (tier): presentazione (browser: HTML, CSS, JavaScript), logica applicativa (server web e application server) e dati (DBMS). Il client invia richieste HTTP, il server risponde; HTTP è senza stato, quindi lo stato dell'utente viaggia in cookie e sessioni. Il web server serve file statici, l'application server (container, per esempio Tomcat per le servlet Java) esegue codice che genera risposte dinamiche; spesso il web server fa da reverse proxy davanti all'application server. Il browser trasforma i byte della risposta in una pagina: decodifica, tokenizzazione e costruzione del DOM, CSS che diventa CSSOM, render tree, layout (reflow), paint e composizione; un script senza defer o async blocca il parsing, il CSS blocca il rendering. Origine = schema + host + porta; la same-origin policy limita ciò che uno script può leggere da altre origini. Web 1.0: pagine generate dal server, ogni azione ricarica la pagina; Web 2.0: AJAX, contenuti degli utenti, servizi REST; Web 3.0: dati con significato esplicito (RDF, linked data).Architettura delle applicazioni web - browser, server web e livelli →):

aula.id nome posti
1 Aula A1 80
2 Aula B2 45
3 Lab Info 30
prenotazione.id aula_id giorno ora_inizio ora_fine studente
1 1 2026-10-19 09:00 11:00 Rossi
2 1 2026-10-19 14:00 16:00 Bianchi
3 2 2026-10-20 10:00 12:00 Verdi
4 3 2026-10-21 09:00 13:00 Rossi

Si vuole la servlet PrenotazioniServlet che riceve POST /prenotazioni con i parametri aula_id, giorno, ora_inizio, ora_fine (lo studente viene dalla sessione) e registra la prenotazione solo se l'aula esiste ed è libera nell'intervallo richiesto.

  1. Scrivere la condizione di sovrapposizione fra due intervalli e la query SQL che conta le prenotazioni in conflitto. Spiegare perché 11:00-13:00 dopo Rossi (09:00-11:00) è ammessa.
  2. Verificare la query su dodici richieste per l'Aula A1 e le altre aule, e mostrare perché una soluzione "ingenua" con BETWEEN è sbagliata.
  3. Scrivere il metodo crea del DAO: controllo e inserimento nella stessa transazione. Perché, senza blocchi, due richieste simultanee potrebbero sovrapporsi?
  4. Scrivere doPost con i codici HTTP corretti per: parametro non numerico, formato errato, aula inesistente, orario occupato, successo.
  5. Estensione: la servlet AnnullaServlet (POST /prenotazioni/annulla con id) cancella una prenotazione, ma solo se è dello studente in sessione.

Soluzione

1. La condizione di sovrapposizione

Due intervalli di tempo [a,b)[a,b) (esistente) e [c,d)[c,d) (richiesto), con l'estremo finale escluso (a 11:00 la prima prenotazione è finita e la sala è libera), si sovrappongono se e solo se ciascuno comincia prima che l'altro finisca:

a<dec<b.a<d \quad\text{e}\quad c<b .

Se d≤ad\le a la nuova finisce prima che l'esistente cominci; se b≤cb\le c l'esistente finisce prima che la nuova cominci: sono i due soli modi per non sovrapporsi. Con aa = ora_inizio, bb = ora_fine dell'esistente e cc, dd della richiesta:

sql
SELECT COUNT(*)
FROM prenotazione
WHERE aula_id = ?          -- stessa aula
  AND giorno = ?           -- stesso giorno
  AND ora_inizio < ?       -- a < d : parametro = fine richiesta
  AND ora_fine   > ?;      -- b > c : parametro = inizio richiesta

La richiesta è ammessa se il conteggio è 0. Per 11:00-13:00 dopo Rossi: l'inizio di Rossi (09:00) è prima della fine richiesta (13:00), vero; ma la fine di Rossi (11:00) non è dopo l'inizio richiesto (11:00): la disuguaglianza è stretta e risulta falsa, quindi nessun conflitto. È la scelta giusta perché gli intervalli sono semiaperti: due prenotazioni consecutive sono lecite.

2. Prova su dodici richieste

Per ogni richiesta sul database dell'esempio (colonna "ingenua" = la condizione sbagliata "l'inizio o la fine cadono dentro una prenotazione esistente", ? BETWEEN ora_inizio AND ora_fine):

Richiesta Corretta (conflitto?) Ingenua
A1, 19, 11:00-13:00 (subito dopo Rossi) no sì (errore)
A1, 19, 08:00-09:00 (subito prima di Rossi) no sì (errore)
A1, 19, 10:00-12:00 (si sovrappone in coda) sì sì
A1, 19, 08:00-10:00 (si sovrappone in testa) sì sì
A1, 19, 09:30-10:30 (dentro Rossi) sì sì
A1, 19, 08:00-12:00 (contiene Rossi) sì no (errore)
A1, 19, 09:00-11:00 (identica a Rossi) sì sì
A1, 19, 12:00-14:00 (tra Rossi e Bianchi) no sì (errore)
A1, 19, 15:00-17:00 (sovrappone Bianchi) sì sì
B2, 19, 10:00-12:00 (altra aula, giorno senza prenotazioni) no no
B2, 20, 11:00-12:30 (Verdi 10:00-12:00) sì sì
A1, 20, 09:00-11:00 (stesso orario, giorno dopo) no no

La colonna "Corretta" è l'uscita della query dello schema reale (sqlite3, ora_inizio < fine AND ora_fine > inizio). La soluzione ingenua sbaglia in tre modi: (i) conta come conflitto l'estremo comune (11:00-13:00 dopo 09:00-11:00), perché BETWEEN include gli estremi; (ii) non vede la richiesta che contiene un'esistente (08:00-12:00 ha inizio e fine fuori dalla prenotazione di Rossi); (iii) per lo stesso motivo 12:00-14:00 è respinta perché 14:00 coincide con l'inizio di Bianchi. La condizione corretta confronta inizio con fine dell'altro, non dentro/fuori.

3. Il metodo crea del DAO

Il controllo e l'inserimento devono essere un'unica operazione. Se si controllasse in una transazione e si inserisse in un'altra, due richieste contemporanee per la stessa aula (A e B, entrambe 12:00-14:00) farebbero: A controlla (libero), B controlla (libero), A inserisce, B inserisce: due prenotazioni sovrapposte (anomalia da check then act). Servono una transazione e un meccanismo che faccia aspettare la seconda richiesta. Qui: SELECT id FROM aula WHERE id = ? FOR UPDATE blocca la riga dell'aula fino al commit; la seconda transazione che prova a bloccare la stessa aula attende, poi vede la prenotazione già inserita e il suo controllo fallisce. (Richieste su aule diverse non si bloccano.) Il blocco di riga è un FOR UPDATE di PostgreSQL (SQL avanzato - indici, transazioni e triggerUn file di record può essere heap (inserimento in ordine, ricerca per uguaglianza in media n/2 accessi), ordinato (log2 n), hash (costante, solo uguaglianza) o B-tree (log_F n, anche intervalli). Un indice è una struttura ausiliaria di coppie (chiave K, puntatore P) ordinate, molto più piccola della tabella: l'indice primario è sul campo di ordinamento del file ordinato (una chiave per blocco, non denso, log2(m)+1 accessi), l'indice secondario è su un campo qualsiasi (denso). CREATE [UNIQUE] INDEX, anche su espressioni; PostgreSQL usa tabelle heap con indici secondari B-tree (predefinito) o hash, e PRIMARY KEY e UNIQUE creano un indice implicito. Gli indici velocizzano le letture ma costano nelle scritture e in spazio. Una transazione è una sequenza atomica di operazioni con proprietà ACID (atomicità, consistenza, isolamento, durabilità): START TRANSACTION, COMMIT, SAVEPOINT, ROLLBACK [TO SAVEPOINT]. Un trigger è una regola evento-condizione-azione (ECA): BEFORE/AFTER, ROW/STATEMENT, WHEN, con funzione PL/pgSQL che usa NEW e OLD; serve per vincoli complessi e attributi derivati.SQL avanzato - indici, transazioni e trigger →); SQLite non lo ha, e la prova della sezione 2 riguarda la sola query di controllo.

java
public Prenotazione crea(Prenotazione p) throws SQLException, PrenotazioneException {
    try (Connection c = ds.getConnection()) {
        c.setAutoCommit(false);                                     // inizia la transazione
        try {
            if (!bloccaAula(c, p.getAulaId())) {                    // SELECT ... FOR UPDATE
                throw new PrenotazioneException(PrenotazioneException.Tipo.AULA_INESISTENTE);
            }
            if (siSovrappone(c, p)) {                               // la COUNT(*) della sezione 1
                throw new PrenotazioneException(PrenotazioneException.Tipo.SOVRAPPOSIZIONE);
            }
            p.setId(inserisci(c, p));                               // INSERT ... RETURN_GENERATED_KEYS
            c.commit();                                             // rilascia il blocco
            return p;
        } catch (SQLException | PrenotazioneException | RuntimeException e) {
            c.rollback();                                           // annulla e rilascia il blocco
            throw e;
        }
    }
}

Il metodo completo, con bloccaAula, siSovrappone e inserisci, è in 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 →. In siSovrappone l'ordine dei parametri è quello della query: ? 3 = fine richiesta, ? 4 = inizio richiesta (un errore classico è scambiarli).

Alternativa dichiarativa. In PostgreSQL si può far applicare la regola al database con un vincolo di esclusione: EXCLUDE USING gist (aula_id WITH =, tsrange(giorno + ora_inizio, giorno + ora_fine) WITH &&) (richiede l'estensione btree_gist). Ogni inserimento sovrapposto fallisce con un errore di vincolo, a prova di concorrenza e di qualunque applicazione che scriva nella tabella. Qui è solo descritto: non è stato eseguito perché PostgreSQL non è disponibile in questo ambiente.

4. doPost con i codici HTTP

java
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    Prenotazione p = new Prenotazione();
    try {
        p.setAulaId(Integer.parseInt(req.getParameter("aula_id")));
    } catch (NumberFormatException e) {
        resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "aula non valida");       // 400
        return;
    }
    p.setGiorno(req.getParameter("giorno"));
    p.setOraInizio(req.getParameter("ora_inizio"));
    p.setOraFine(req.getParameter("ora_fine"));
    p.setStudente((String) req.getSession().getAttribute("studente"));               // dalla sessione, non dal form
    try {
        p.valida();                                                                  // formato e ora_inizio < ora_fine
        prenotazioneDAO.crea(p);
        resp.sendRedirect(req.getContextPath() + "/prenotazioni?giorno=" + p.getGiorno());   // successo: PRG
    } catch (IllegalArgumentException e) {
        resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());          // 400
    } catch (PrenotazioneException e) {
        int stato = e.getTipo() == PrenotazioneException.Tipo.SOVRAPPOSIZIONE ? 409 : 422;
        resp.sendError(stato, e.getMessage());                                       // 409 occupata, 422 aula inesistente
    } catch (SQLException e) {
        throw new ServletException("errore di accesso ai dati", e);                  // 500
    }
}
Situazione Esito
aula_id=abc 400 (NumberFormatException)
giorno=19/10/2026, ore mancanti, ora_inizio ≥\ge ora_fine 400 (valida())
aula_id=99 422 (AULA_INESISTENTE)
A1, 19, 10:00-12:00 409 (SOVRAPPOSIZIONE)
A1, 19, 11:00-13:00 redirect 302 a /prenotazioni?giorno=2026-10-19
guasto del database 500

Il giorno e le ore passano da valida() (che usa LocalDate.parse e LocalTime.parse) prima di arrivare al DAO; i valori arrivano al database solo come parametri ? (nessuna SQL injection).

5. Estensione: annullare solo le proprie prenotazioni

java
package it.unipd.aule.web;

import it.unipd.aule.dao.PrenotazioneDAO;
import it.unipd.aule.model.Prenotazione;
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;

/** Annulla una prenotazione, ma solo se è dello studente che ha fatto il login. */
@WebServlet("/prenotazioni/annulla")
public class AnnullaServlet extends HttpServlet {
    private PrenotazioneDAO dao;

    @Override
    public void init() {
        dao = (PrenotazioneDAO) getServletContext().getAttribute("prenotazioneDAO");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String studente = (String) req.getSession().getAttribute("studente");   // il filtro garantisce che non sia null
        try {
            Prenotazione p = dao.perId(Integer.parseInt(req.getParameter("id")));
            if (p == null) {
                resp.sendError(HttpServletResponse.SC_NOT_FOUND, "prenotazione inesistente");
            } else if (!p.getStudente().equals(studente)) {
                resp.sendError(HttpServletResponse.SC_FORBIDDEN, "puoi annullare solo le tue prenotazioni");
            } else {
                dao.elimina(p.getId());
                resp.sendRedirect(req.getContextPath() + "/prenotazioni?giorno=" + p.getGiorno());
            }
        } catch (NumberFormatException e) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "id non valido");
        } catch (SQLException e) {
            throw new ServletException(e);
        }
    }
}
  • Il nome dello studente è letto dalla sessione, non dal form: un parametro studente nel form potrebbe essere falsificato.
  • perId restituisce null per un id inesistente: 404. Se la prenotazione è di un altro studente: 403 Forbidden (la richiesta è chiara ma non permessa), a differenza di 401 (manca l'autenticazione).
  • AutenticazioneFilter su /prenotazioni/* si applica anche a /prenotazioni/annulla e garantisce che lo studente in sessione non sia nullo.
  • Il successo termina con un redirect (Post/Redirect/Get).
  • Il controllo del proprietario va fatto sul server: nascondere il pulsante "Cancella" nella pagina non basta.

Versione ripasso

Obiettivo. POST /prenotazioni con aula_id, giorno, ora_inizio, ora_fine (lo studente è nella sessione): registrare solo se l'aula esiste ed è libera.

Condizione di sovrapposizione. Intervalli semiaperti [a,b)[a,b) esistente e [c,d)[c,d) richiesto: si sovrappongono se e solo se a<da<d e c<bc<b (ciascuno comincia prima che l'altro finisca). Altrimenti d≤ad\le a oppure b≤cb\le c. Le prenotazioni consecutive sono lecite (11:00-13:00 dopo 09:00-11:00: b>cb>c è falso).

sql
SELECT COUNT(*) FROM prenotazione
WHERE aula_id = ? AND giorno = ?
  AND ora_inizio < ?   -- fine richiesta
  AND ora_fine   > ?;  -- inizio richiesta

Conteggio 0 →\to ammessa. Ordine dei parametri: 3 = fine richiesta, 4 = inizio richiesta.

Prova (A1 il 19 con Rossi 09-11 e Bianchi 14-16).

Richiesta Conflitto
11:00-13:00, 08:00-09:00, 12:00-14:00 no
10:00-12:00, 08:00-10:00, 09:30-10:30, 08:00-12:00, 09:00-11:00, 15:00-17:00 sì
B2 il 19 10:00-12:00; A1 il 20 09:00-11:00 no
B2 il 20 11:00-12:30 (Verdi 10-12) sì

Soluzione ingenua con BETWEEN: ? BETWEEN ora_inizio AND ora_fine sbaglia perché include gli estremi (blocca le prenotazioni consecutive e 12:00-14:00 che tocca Bianchi) e non vede la richiesta che contiene un'esistente (08:00-12:00).

DAO crea. Controllo e inserimento nella stessa transazione, altrimenti due richieste simultanee passano entrambe il controllo e inseriscono due prenotazioni sovrapposte (check then act). SELECT id FROM aula WHERE id = ? FOR UPDATE blocca la riga dell'aula fino al commit: la seconda richiesta attende e poi vede la prima.

java
c.setAutoCommit(false);
try {
    if (!bloccaAula(c, p.getAulaId())) throw new PrenotazioneException(Tipo.AULA_INESISTENTE);
    if (siSovrappone(c, p))            throw new PrenotazioneException(Tipo.SOVRAPPOSIZIONE);
    p.setId(inserisci(c, p));
    c.commit();
    return p;
} catch (SQLException | PrenotazioneException | RuntimeException e) {
    c.rollback();
    throw e;
}

Alternativa in PostgreSQL: EXCLUDE USING gist (aula_id WITH =, tsrange(giorno + ora_inizio, giorno + ora_fine) WITH &&) con btree_gist.

doPost e codici.

Situazione Esito
aula_id non numerico; formato errato; ora_inizio ≥\ge ora_fine 400
aula inesistente 422
orario occupato 409
successo redirect 302 a /prenotazioni?giorno=... (PRG)
guasto SQL ServletException, 500
java
p.setStudente((String) req.getSession().getAttribute("studente"));   // dalla sessione
try {
    p.valida();
    prenotazioneDAO.crea(p);
    resp.sendRedirect(req.getContextPath() + "/prenotazioni?giorno=" + p.getGiorno());
} catch (IllegalArgumentException e) {
    resp.sendError(400, e.getMessage());
} catch (PrenotazioneException e) {
    resp.sendError(e.getTipo() == PrenotazioneException.Tipo.SOVRAPPOSIZIONE ? 409 : 422, e.getMessage());
} catch (SQLException e) {
    throw new ServletException("errore di accesso ai dati", e);
}

AnnullaServlet (POST /prenotazioni/annulla, parametro id): studente dalla sessione; perId nullo →\to 404; studente diverso →\to 403; altrimenti dao.elimina(id) e redirect. Id non numerico →\to 400. Il filtro di autenticazione copre /prenotazioni/*. Il controllo del proprietario sta sul server.

Errori tipici

  • Condizione BETWEEN o inizio/fine scambiati nei parametri.
  • Controllo e inserimento in transazioni diverse, senza blocco.
  • Prendere lo studente da un campo del form.
  • 401 al posto di 403 (o viceversa); mostrare all'utente il testo dell'eccezione SQL.

Teoria: 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 →.

Teoria collegata