Salta al contenuto
Note per Studenti Servizi REST in Java - progettazione delle risorse e implementazione con servlet

Servizi REST in Java - progettazione delle risorse e implementazione con servlet

In questa pagina 8

Fin qui il server rispondeva con pagine HTML complete (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 →): ogni azione dell'utente ricaricava l'intera pagina. Con il Web 2.0 il server diventa un servizio che offre dati, e la pagina, già caricata nel browser, li chiede in background e li mostra (AJAX e fetch - applicazioni Web 2.0 e MVC rivistoAJAX (Asynchronous JavaScript and XML, oggi quasi sempre con JSON) permette a JavaScript di chiedere dati al server in background e aggiornare solo una parte del DOM, senza ricaricare la pagina. L'oggetto storico è XMLHttpRequest (open, send, eventi onload e onerror); l'API moderna è fetch, che restituisce una Promise di Response (ok, status, json()). Una Promise rappresenta un risultato futuro (in attesa, mantenuta, rifiutata) e si concatena con then, catch e finally; async/await è la sintassi che la fa leggere come codice sequenziale, e dopo ogni await il resto della funzione riparte quando la Promise si risolve (il codice sincrono finisce prima, poi vengono le promise, poi i timer). Attenzione: fetch rifiuta la Promise solo per errori di rete; un 404 o un 500 sono risposte e si controllano con response.ok. Il client delle aule ha api.js (funzioni che chiamano l'API REST e lanciano ErroreApi se il codice non è 2xx) e pagina.js (legge il form, chiama api.js, aggiorna la tabella con textContent, un solo gestore di click per i bottoni di cancellazione). Il paradigma MVC viene rivisto: il server resta con model e controller REST ma restituisce solo dati, la view è nel browser (DOM costruito da JavaScript) e il controller della pagina sono i gestori di eventi; lo stato dell'interfaccia sta nel client e il server è senza stato. La same-origin policy (origine = schema, host, porta) impedisce a uno script di leggere risposte di altre origini; CORS permette al server di autorizzarle con Access-Control-Allow-Origin, e le richieste non semplici (JSON, DELETE) sono precedute da una richiesta OPTIONS di preflight.AJAX e fetch - applicazioni Web 2.0 e MVC rivisto →). L'architettura REST (REpresentational State Transfer, Fielding 2000) dà le regole per progettare questo servizio. I vincoli (client-server, senza stato, cache, interfaccia uniforme, livelli) e le definizioni di risorsa e rappresentazione sono in Web service, XML, JSON, SOAP e RESTUn web service e' un servizio software accessibile via rete da altri programmi, con messaggi in formato standard su HTTP; i formati di dati sono XML (marcatori annidati, attributi, namespace, validazione con XSD) e JSON (RFC 8259: oggetti, array, stringhe, numeri, true, false, null; piu' compatto e diretto per i linguaggi di programmazione); SOAP e' un protocollo di messaggi XML (Envelope con Header facoltativo e Body, Fault per gli errori) inviati di solito con una POST HTTP e descritti da un file WSDL; REST e' uno stile architetturale (Fielding) in cui il servizio espone risorse identificate da URI e le manipola con i metodi HTTP: GET legge, POST crea, PUT sostituisce, PATCH modifica, DELETE elimina, con codici di stato significativi (200, 201 con Location, 204, 400, 404, 409), senza stato sul server, con cache e interfaccia uniforme.Web service, XML, JSON, SOAP e REST → e qui non si ripetono; questa nota riguarda come si progetta l'API dell'applicazione delle aule e come si realizza con servlet. La semantica dei metodi e dei codici di stato è quella di RFC 9110 (HTTP Semantics); le basi dei messaggi sono in HTTP 1.1 - connessioni persistenti, Content-Length e chunked transfer encodingHTTP/1.1 (oggi RFC 9110 e 9112) rende la connessione persistente di default (si chiude solo con "Connection: close"), rende obbligatorio l'header Host (virtual hosting) e introduce i nuovi metodi PUT, DELETE, OPTIONS, TRACE, Expect: 100-continue, richieste di intervalli (206) e Transfer-Encoding: chunked; con la connessione persistente il client deve sapere dove finisce ogni risposta: lunghezza del corpo nell'ordine HEAD/1xx/204/304 senza corpo, Transfer-Encoding chunked, Content-Length, altrimenti fino alla chiusura; il chunked divide il corpo in blocchi preceduti dalla lunghezza in esadecimale, termina con un chunk 0 e un trailer facoltativo, e si decodifica contando i byte dichiarati (non cercando CRLF).HTTP 1.1 - connessioni persistenti, Content-Length e chunked transfer encoding → e Caching, autenticazione, tipi MIME e URI in HTTPLa cache HTTP riusa le risposte senza contattare il server finche' sono fresche (Cache-Control: max-age, Expires; in mancanza una durata euristica pari a circa il 10% del tempo trascorso da Last-Modified) e, quando sono scadute, le rivalida con una richiesta condizionale (If-None-Match con ETag, If-Modified-Since con Last-Modified) alla quale il server risponde 304 senza corpo; no-store vieta di memorizzare, no-cache obbliga a rivalidare, private esclude le cache condivise. L'autenticazione usa 401 con WWW-Authenticate e la risposta Authorization: Basic (base64 di utente:password, solo sopra TLS) o Digest (hash con nonce); 407 vale per i proxy. Content-Type porta il tipo MIME (tipo/sottotipo; parametri come charset e boundary), Accept e gli altri header Accept-* guidano la negoziazione. Un URI (RFC 3986) e' scheme://userinfo@host:porta/percorso?query#frammento, con percent-encoding %HH per i byte non ammessi e regole per risolvere i riferimenti relativi.Caching, autenticazione, tipi MIME e URI in HTTP →.

Progettare le risorse

Il procedimento, applicato all'esempio (tabelle aula e prenotazione):

  1. Individuare i sostantivi del dominio. Aule e prenotazioni sono le cose che il client vuole leggere e modificare: ciascuna è un tipo di risorsa. Non tutto ciò che c'è nel database deve diventare una risorsa, e una risorsa non coincide per forza con una tabella.
  2. Collezione e elemento. Per ogni tipo, due URI: la collezione (/api/aule) e il singolo elemento (/api/aule/3, dove 3 è l'id). Nomi al plurale, minuscoli; l'identificatore è parte del path perché identifica la risorsa.
  3. Relazioni. Ogni prenotazione appartiene a un'aula (aula_id). Due modi di esporlo: annidare (/api/aule/1/prenotazioni) oppure tenere le risorse piatte e filtrare (/api/prenotazioni?aula=1). L'esempio sceglie le risorse piatte: la prenotazione ha un URI proprio e breve (/api/prenotazioni/7) e i filtri (aula, giorno) si combinano liberamente (il giorno senza l'aula, per tutte le aule). L'annidamento ha senso per le risorse che non esistono senza il genitore.
  4. Azioni. Si traducono in metodi HTTP sulle risorse, non in URI con verbi. "Prenotare" è creare un elemento della collezione (POST /api/prenotazioni), "annullare" è eliminare l'elemento (DELETE /api/prenotazioni/7). Niente /api/prenota né /api/cancellaPrenotazione?id=7. Una modifica di orario non è offerta: si cancella e si ricrea (così il controllo delle sovrapposizioni resta nel solo POST); un PUT completo o un PATCH sarebbero ammissibili con lo stesso controllo.
  5. Rappresentazioni e codici. Si fissano il formato dei dati (JSON, sotto) e gli esiti di ciascuna operazione (tabella dei codici).

Regole per gli URI: sostantivi e non verbi; plurali; minuscoli con trattini se serve; il filtro, l'ordinamento e la paginazione nella query string (?giorno=...&ordina=...), l'identità nel path; un prefisso (/api) che separa le risorse dei dati dalle pagine; nessuna estensione (.json), perché il formato si negozia con Accept e Content-Type; il numero di versione, se serve, nel prefisso (/api/v1/...), omesso nell'esempio.

L'API dell'esempio

Metodo URI Significato Successo Errori
GET /api/aule elenco delle aule 200 + array 500
GET /api/aule/{id} una aula 200 + oggetto 404
GET /api/prenotazioni?aula=&giorno= elenco, filtrabile per aula e/o giorno 200 + array (anche vuoto) 400 filtro malformato
GET /api/prenotazioni/{id} una prenotazione 200 + oggetto 404
POST /api/prenotazioni crea una prenotazione 201 + Location + oggetto creato 400 dati malformati, 415 non è JSON, 422 aula inesistente, 409 orario occupato
DELETE /api/prenotazioni/{id} annulla una prenotazione 204 senza corpo 404
altri qualunque URI sopra 405 con Allow

I metodi HTTP

RFC 9110 definisce per ogni metodo due proprietà che il progettista deve rispettare:

  • sicuro (safe): il client non chiede e non ottiene alcun cambiamento di stato sul server (letture). Un crawler o un'anteprima può chiamare liberamente i metodi sicuri;
  • idempotente: ripetere la stessa richiesta più volte ha lo stesso effetto di farla una volta; un client può quindi ritentare dopo un errore di rete senza paura.
Metodo Uso Sicuro Idempotente
GET leggere una risorsa o una collezione sì sì
POST creare un elemento nella collezione (il server sceglie l'id), o azioni senza altro metodo adatto no no
PUT sostituire per intero l'elemento, a un URI deciso dal client no sì
PATCH modificare solo alcuni campi no non garantito
DELETE eliminare no sì

DELETE è idempotente nell'effetto (dopo la prima richiesta l'elemento non c'è più) anche se la seconda risposta è 404. POST non lo è: ripetere la richiesta può creare due elementi; nell'esempio il controllo delle sovrapposizioni risponde 409 al secondo tentativo, quindi un ritentativo non duplica la prenotazione.

I codici di stato

I codici sono parte del contratto: il client decide che cosa fare guardando il codice, e il corpo spiega. Classi: 2xx riuscito, 4xx errore del client (sbaglia lui, ripetere uguale non serve), 5xx errore del server.

Codice Quando nell'esempio
200 OK lettura riuscita
201 Created prenotazione creata; l'header Location contiene l'URI della nuova risorsa (/api/prenotazioni/5) e il corpo la sua rappresentazione
204 No Content cancellazione riuscita, nessun corpo
400 Bad Request richiesta malformata: JSON non valido, campo mancante, data o ora in formato sbagliato, ora_inizio non precedente a ora_fine, filtro non numerico
404 Not Found l'URI non corrisponde a nessuna risorsa: id inesistente, path come /api/aule/abc
405 Method Not Allowed URI valido ma metodo non previsto (DELETE /api/aule/1): la risposta deve contenere Allow con i metodi ammessi
409 Conflict la richiesta è corretta ma contrasta con lo stato della risorsa: orario già occupato
415 Unsupported Media Type il corpo non è application/json
422 Unprocessable Content sintassi corretta ma contenuto non elaborabile: aula_id che non esiste
500 Internal Server Error guasto del server (database): il dettaglio va nel log, non nella risposta

Distinzioni che si chiedono all'esame: 400 o 422: 400 se la richiesta non si capisce (sintassi, tipi, campi), 422 se si capisce ma i dati hanno un significato impossibile; molte API usano solo 400, l'importante è la coerenza. 404 o 405: 404 se la risorsa non esiste, 405 se esiste ma non con quel metodo. 409: dipende dallo stato del server, e cambierebbe se qualcun altro cancellasse la prenotazione che blocca. Autenticazione e autorizzazione (401, 403) non sono nell'esempio: le credenziali viaggiano a ogni richiesta (header Authorization), mai in una sessione, perché il servizio è senza stato.

Le rappresentazioni JSON

JSON (RFC 8259, introdotto in Web service, XML, JSON, SOAP e RESTUn web service e' un servizio software accessibile via rete da altri programmi, con messaggi in formato standard su HTTP; i formati di dati sono XML (marcatori annidati, attributi, namespace, validazione con XSD) e JSON (RFC 8259: oggetti, array, stringhe, numeri, true, false, null; piu' compatto e diretto per i linguaggi di programmazione); SOAP e' un protocollo di messaggi XML (Envelope con Header facoltativo e Body, Fault per gli errori) inviati di solito con una POST HTTP e descritti da un file WSDL; REST e' uno stile architetturale (Fielding) in cui il servizio espone risorse identificate da URI e le manipola con i metodi HTTP: GET legge, POST crea, PUT sostituisce, PATCH modifica, DELETE elimina, con codici di stato significativi (200, 201 con Location, 204, 400, 404, 409), senza stato sul server, con cache e interfaccia uniforme.Web service, XML, JSON, SOAP e REST →) è il formato scelto. Una aula e una prenotazione:

json
{"id": 2, "nome": "Aula B2", "posti": 45}
json
{"id": 1, "aula_id": 1, "giorno": "2026-10-19", "ora_inizio": "09:00", "ora_fine": "11:00", "studente": "Rossi"}
  • I nomi dei campi sono quelli delle colonne (aula_id, ora_inizio, ora_fine), in snake_case. Date e ore sono stringhe nel formato ISO 8601 (aaaa-mm-gg, hh:mm): JSON non ha un tipo data.
  • Una collezione è un array, anche vuoto ([] con 200, non 404).
  • Un errore ha sempre lo stesso corpo, leggibile dalle macchine e dalle persone: {"errore": "l'aula è già prenotata in quell'orario"}.
  • In ingresso (POST) il client manda la prenotazione senza id: lo assegna il server. Se lo manda, viene ignorato.
  • L'header Content-Type: application/json dichiara il formato, in richiesta e in risposta; con JSON la codifica è sempre UTF-8.

L'implementazione con le servlet

Il model è lo stesso dell'applicazione MVC: Aula, Prenotazione, AulaDAO, PrenotazioneDAO (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 →). Cambia solo la view: invece di una JSP c'è un serializzatore JSON. Per convertire oggetti Java e JSON si usa una libreria; qui Gson, dipendenza nel pom. Le servlet dell'API stanno nel package it.unipd.aule.rest; hanno gli stessi nomi, AuleServlet e PrenotazioniServlet, di quelle MVC ma URL diversi (/api/...), quindi possono coesistere.

Una classe di utilità raccoglie ciò che serve a entrambe:

java
package it.unipd.aule.rest;

import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.Map;

/** Piccole utilità comuni alle servlet REST: scrivere JSON e leggere l'identificatore dal path. */
final class Json {
    // aulaId diventa "aula_id", oraInizio "ora_inizio": nel JSON i nomi sono quelli delle colonne
    static final Gson GSON = new GsonBuilder()
            .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
            .disableHtmlEscaping()                  // di serie Gson trasforma l'apostrofo e <, >, & in sequenze "uXXXX"
            .create();

    private Json() { }

    static void invia(HttpServletResponse resp, int stato, Object corpo) throws IOException {
        resp.setStatus(stato);
        resp.setContentType("application/json");
        resp.setCharacterEncoding("UTF-8");
        resp.getWriter().write(GSON.toJson(corpo));
    }

    static void errore(HttpServletResponse resp, int stato, String messaggio) throws IOException {
        invia(resp, stato, Map.of("errore", messaggio));
    }

    /** 405 con l'header Allow obbligatorio (RFC 9110, par. 15.5.6). */
    static void nonConsentito(HttpServletResponse resp, String allow) throws IOException {
        resp.setHeader("Allow", allow);
        errore(resp, HttpServletResponse.SC_METHOD_NOT_ALLOWED, "metodo non consentito su questa risorsa");
    }

    /** True se il path è la collezione: nessun path oppure solo "/". */
    static boolean eCollezione(String pathInfo) {
        return pathInfo == null || pathInfo.equals("/");
    }

    /** Estrae l'id da "/12". Lancia NumberFormatException per "/abc" o "/12/altro": la servlet risponde 404. */
    static int id(String pathInfo) {
        return Integer.parseInt(pathInfo.substring(1));
    }
}
  • FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES fa sì che aulaId diventi aula_id nel JSON, e viceversa in lettura: il codice Java resta in camelCase, l'API in snake_case.
  • invia imposta stato, Content-Type e codifica prima di scrivere il corpo (altrimenti la risposta è già committed).
  • nonConsentito produce il 405 con l'header Allow richiesto da RFC 9110.
  • id estrae il numero da "/12"; con "/abc" o "/12/altro" Integer.parseInt lancia NumberFormatException, che le servlet traducono in 404.

Il dispatch sul path

L'URL /api/prenotazioni/12 e /api/prenotazioni devono arrivare alla stessa servlet, che capisce da sola di che risorsa si tratta. Con il mapping a prefisso /api/prenotazioni/*:

URL getPathInfo() Risorsa
/api/prenotazioni null collezione
/api/prenotazioni/ "/" collezione
/api/prenotazioni/12 "/12" elemento 12
/api/prenotazioni/abc "/abc" non esiste: 404

Il container sceglie già il metodo (doGet, doPost, doDelete); dentro ciascuno si guarda getPathInfo(). Le aule, in sola lettura:

java
package it.unipd.aule.rest;

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

/** Risorse /api/aule (collezione) e /api/aule/{id} (singola aula): solo lettura. */
@WebServlet("/api/aule/*")
public class AuleServlet extends HttpServlet {
    private AulaDAO dao;

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

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        try {
            String path = req.getPathInfo();                     // ciò che segue /api/aule: null, "/" oppure "/3"
            if (Json.eCollezione(path)) {
                Json.invia(resp, HttpServletResponse.SC_OK, dao.tutte());
                return;
            }
            Aula aula = dao.perId(Json.id(path));
            if (aula == null) {
                Json.errore(resp, HttpServletResponse.SC_NOT_FOUND, "aula inesistente");
            } else {
                Json.invia(resp, HttpServletResponse.SC_OK, aula);
            }
        } catch (NumberFormatException e) {
            Json.errore(resp, HttpServletResponse.SC_NOT_FOUND, "risorsa inesistente");
        } catch (SQLException e) {
            log("errore SQL", e);
            Json.errore(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "errore interno");
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        Json.nonConsentito(resp, "GET");
    }

    @Override
    protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        Json.nonConsentito(resp, "GET");
    }
}

Le prenotazioni, con lettura, creazione e cancellazione:

java
package it.unipd.aule.rest;

import com.google.gson.JsonSyntaxException;
import it.unipd.aule.dao.PrenotazioneDAO;
import it.unipd.aule.dao.PrenotazioneException;
import it.unipd.aule.model.Prenotazione;
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;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;

/**
 * API REST delle prenotazioni. Una sola servlet per due tipi di risorsa, scelti dal path:
 *   /api/prenotazioni        collezione: GET (elenco, filtri ?aula= e ?giorno=), POST (crea)
 *   /api/prenotazioni/{id}   singola prenotazione: GET, DELETE
 */
@WebServlet("/api/prenotazioni/*")
public class PrenotazioniServlet extends HttpServlet {
    private PrenotazioneDAO dao;

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

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        try {
            String path = req.getPathInfo();
            if (Json.eCollezione(path)) {
                String aula = req.getParameter("aula");
                String giorno = req.getParameter("giorno");
                if (giorno != null) {
                    LocalDate.parse(giorno);                                  // solo per validarlo
                }
                Integer aulaId = aula == null ? null : Integer.valueOf(aula);
                Json.invia(resp, HttpServletResponse.SC_OK, dao.lista(aulaId, giorno));
                return;
            }
            Prenotazione p = dao.perId(Json.id(path));
            if (p == null) {
                Json.errore(resp, HttpServletResponse.SC_NOT_FOUND, "prenotazione inesistente");
            } else {
                Json.invia(resp, HttpServletResponse.SC_OK, p);
            }
        } catch (DateTimeParseException e) {
            Json.errore(resp, HttpServletResponse.SC_BAD_REQUEST, "giorno: formato aaaa-mm-gg");
        } catch (NumberFormatException e) {
            // "/abc" nel path è una risorsa che non esiste; "?aula=abc" è una richiesta malformata
            boolean nelPath = !Json.eCollezione(req.getPathInfo());
            Json.errore(resp, nelPath ? HttpServletResponse.SC_NOT_FOUND : HttpServletResponse.SC_BAD_REQUEST,
                        nelPath ? "risorsa inesistente" : "aula: numero intero");
        } catch (SQLException e) {
            erroreInterno(resp, e);
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        if (!Json.eCollezione(req.getPathInfo())) {
            Json.nonConsentito(resp, "GET, DELETE");
            return;
        }
        String tipo = req.getContentType();
        if (tipo == null || !tipo.startsWith("application/json")) {
            Json.errore(resp, HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, "serve Content-Type: application/json");
            return;
        }
        req.setCharacterEncoding("UTF-8");
        try {
            Prenotazione p = Json.GSON.fromJson(req.getReader(), Prenotazione.class);
            if (p == null) {
                Json.errore(resp, HttpServletResponse.SC_BAD_REQUEST, "corpo vuoto");
                return;
            }
            p.setId(0);                       // l'id lo assegna il server, qualunque cosa mandi il client
            p.valida();
            dao.crea(p);
            resp.setHeader("Location", req.getContextPath() + "/api/prenotazioni/" + p.getId());
            Json.invia(resp, HttpServletResponse.SC_CREATED, p);
        } catch (JsonSyntaxException e) {
            Json.errore(resp, HttpServletResponse.SC_BAD_REQUEST, "il corpo non è un JSON valido");
        } catch (IllegalArgumentException e) {
            Json.errore(resp, HttpServletResponse.SC_BAD_REQUEST, e.getMessage());          // dal metodo valida()
        } catch (PrenotazioneException e) {
            int stato = e.getTipo() == PrenotazioneException.Tipo.SOVRAPPOSIZIONE ? HttpServletResponse.SC_CONFLICT : 422;
            Json.errore(resp, stato, e.getMessage());
        } catch (SQLException e) {
            erroreInterno(resp, e);
        }
    }

    @Override
    protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        if (Json.eCollezione(req.getPathInfo())) {
            Json.nonConsentito(resp, "GET, POST");
            return;
        }
        try {
            if (dao.elimina(Json.id(req.getPathInfo()))) {
                resp.setStatus(HttpServletResponse.SC_NO_CONTENT);            // successo senza corpo
            } else {
                Json.errore(resp, HttpServletResponse.SC_NOT_FOUND, "prenotazione inesistente");
            }
        } catch (NumberFormatException e) {
            Json.errore(resp, HttpServletResponse.SC_NOT_FOUND, "risorsa inesistente");
        } catch (SQLException e) {
            erroreInterno(resp, e);
        }
    }

    private void erroreInterno(HttpServletResponse resp, SQLException e) throws IOException {
        log("errore SQL", e);                                                  // il dettaglio resta nel log del server
        Json.errore(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "errore interno");
    }
}

Punti da osservare:

  • Nessun HTML, nessuna sessione: la servlet usa solo la richiesta; per questo l'API è senza stato.
  • doGet ha due rami (collezione o elemento). I filtri aula e giorno sono convalidati prima di arrivare al DAO; un NumberFormatException vuol dire 404 se l'errore sta nel path, 400 se sta nel filtro. Un elemento che non c'è (null dal DAO) è 404.
  • doPost applica nell'ordine: il path (un POST su un elemento non ha senso: 405), il Content-Type (415), la lettura del JSON (getReader() e Gson; sintassi errata →\to 400), la validazione del modello (valida() →\to 400) e il DAO (crea: 409 o 422). Con p.setId(0) si ignora l'id mandato dal client. In caso di successo: Location e stato 201 con la prenotazione completa dell'id.
  • doDelete: 204 senza corpo se la riga c'era, 404 se no.
  • Gli errori dell'utente e i guasti si distinguono: SQLException è registrata con log e al client arriva solo {"errore":"errore interno"}.

Uno scambio completo

Le risposte qui sotto sono quelle prodotte dalle due servlet, provate con richieste simulate e DAO in memoria sugli stessi dati dell'esempio (gli header Date e simili sono omessi). Prima una lettura:

http
GET /api/prenotazioni?aula=1&giorno=2026-10-19 HTTP/1.1
Host: localhost:8080
Accept: application/json
http
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Content-Length: 207

[{"id":1,"aula_id":1,"giorno":"2026-10-19","ora_inizio":"09:00","ora_fine":"11:00","studente":"Rossi"},{"id":2,"aula_id":1,"giorno":"2026-10-19","ora_inizio":"14:00","ora_fine":"16:00","studente":"Bianchi"}]

Una creazione riuscita, poi un tentativo sovrapposto alla prima:

http
POST /api/prenotazioni HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Content-Length: 93

{"aula_id":1,"giorno":"2026-10-19","ora_inizio":"11:00","ora_fine":"12:30","studente":"Neri"}
http
HTTP/1.1 201
Location: /api/prenotazioni/5
Content-Type: application/json;charset=UTF-8
Content-Length: 100

{"id":5,"aula_id":1,"giorno":"2026-10-19","ora_inizio":"11:00","ora_fine":"12:30","studente":"Neri"}
http
POST /api/prenotazioni HTTP/1.1
Content-Type: application/json

{"aula_id":1,"giorno":"2026-10-19","ora_inizio":"10:00","ora_fine":"12:00","studente":"Neri"}
http
HTTP/1.1 409
Content-Type: application/json;charset=UTF-8

{"errore":"l'aula è già prenotata in quell'orario"}

(L'intervallo 10:00-12:00 si sovrappone sia a Rossi 09:00-11:00 sia alla nuova prenotazione di Neri 11:00-12:30.) Gli altri esiti:

Richiesta Risposta
GET /api/aule/9 404 {"errore":"aula inesistente"}
GET /api/aule/abc 404 {"errore":"risorsa inesistente"}
GET /api/prenotazioni?giorno=19-10-2026 400 {"errore":"giorno: formato aaaa-mm-gg"}
POST /api/prenotazioni con aula_id 99 422 {"errore":"aula inesistente"}
POST /api/prenotazioni con ora_inizio 13:00 e ora_fine 12:00 400 {"errore":"l'ora di inizio deve precedere quella di fine"}
POST /api/prenotazioni con Content-Type: text/plain 415 {"errore":"serve Content-Type: application/json"}
POST /api/prenotazioni/1 405 con Allow: GET, DELETE
DELETE /api/prenotazioni/5 204, senza corpo
DELETE /api/prenotazioni/5 (di nuovo) 404 {"errore":"prenotazione inesistente"}
DELETE /api/prenotazioni 405 con Allow: GET, POST

Dalla riga di comando il servizio si prova con curl:

bash
$ curl -i "http://localhost:8080/api/prenotazioni?giorno=2026-10-19"
$ curl -i -X POST http://localhost:8080/api/prenotazioni \
       -H "Content-Type: application/json" \
       -d '{"aula_id":1,"giorno":"2026-10-19","ora_inizio":"11:00","ora_fine":"12:30","studente":"Neri"}'
$ curl -i -X DELETE http://localhost:8080/api/prenotazioni/5

Quello che resta fuori

Errori tipici

  • URI con verbi (/api/prenotaAula) o GET che modifica lo stato.
  • Rispondere 200 con {"errore": ...} nel corpo invece del codice giusto: il client non può distinguere.
  • Non rispondere 201 e Location alla creazione, o 404 per una collezione vuota (è 200 con []).
  • Confondere 404 e 405; dimenticare Allow nel 405.
  • Fidarsi del JSON ricevuto: nessun controllo di tipo e di formato, oppure accettare l'id dal client.
  • Mandare al client il testo dell'eccezione o la stack trace.
  • Tenere uno stato di sessione nel servizio: non è più REST e non si può replicare.
  • Non impostare Content-Type: application/json, o scrivere il corpo prima dello stato.

Domande d'esame

1. Progettare l'API REST per le prenotazioni delle aule: risorse, URI, metodi e codici di stato. Traccia. Risorse aule e prenotazioni; collezioni /api/aule e /api/prenotazioni con filtri in query, elementi /api/aule/{id}, /api/prenotazioni/{id}. GET per leggere (200, 404), POST sulla collezione per creare (201 + Location; 400, 409 per sovrapposizione, 422 per aula inesistente), DELETE sull'elemento (204, 404). Metodi non previsti: 405 con Allow. JSON con i nomi delle colonne.

2. Perché una POST che crea una risorsa non è idempotente e DELETE sì? Che cosa risponde il servizio alla seconda DELETE della stessa prenotazione? Traccia. Due POST identiche possono creare due elementi; due DELETE portano allo stesso stato finale (l'elemento non c'è), anche se la seconda risposta è 404. Nell'esempio una seconda POST uguale dà 409.

3. Come fa una sola servlet mappata su /api/prenotazioni/* a gestire collezione ed elemento? Scrivere lo scheletro di doGet. Traccia. getPathInfo() è null o "/" per la collezione, "/12" per l'elemento. Collezione: leggere i filtri, validarli, dao.lista(aula, giorno), 200 con l'array. Elemento: Integer.parseInt dell'id (NumberFormatException →\to 404), dao.perId, 404 se null, altrimenti 200.

Versione ripasso

Idea. Il server offre risorse e dati (JSON), non pagine; la vista passa al client (AJAX e fetch - applicazioni Web 2.0 e MVC rivistoAJAX (Asynchronous JavaScript and XML, oggi quasi sempre con JSON) permette a JavaScript di chiedere dati al server in background e aggiornare solo una parte del DOM, senza ricaricare la pagina. L'oggetto storico è XMLHttpRequest (open, send, eventi onload e onerror); l'API moderna è fetch, che restituisce una Promise di Response (ok, status, json()). Una Promise rappresenta un risultato futuro (in attesa, mantenuta, rifiutata) e si concatena con then, catch e finally; async/await è la sintassi che la fa leggere come codice sequenziale, e dopo ogni await il resto della funzione riparte quando la Promise si risolve (il codice sincrono finisce prima, poi vengono le promise, poi i timer). Attenzione: fetch rifiuta la Promise solo per errori di rete; un 404 o un 500 sono risposte e si controllano con response.ok. Il client delle aule ha api.js (funzioni che chiamano l'API REST e lanciano ErroreApi se il codice non è 2xx) e pagina.js (legge il form, chiama api.js, aggiorna la tabella con textContent, un solo gestore di click per i bottoni di cancellazione). Il paradigma MVC viene rivisto: il server resta con model e controller REST ma restituisce solo dati, la view è nel browser (DOM costruito da JavaScript) e il controller della pagina sono i gestori di eventi; lo stato dell'interfaccia sta nel client e il server è senza stato. La same-origin policy (origine = schema, host, porta) impedisce a uno script di leggere risposte di altre origini; CORS permette al server di autorizzarle con Access-Control-Allow-Origin, e le richieste non semplici (JSON, DELETE) sono precedute da una richiesta OPTIONS di preflight.AJAX e fetch - applicazioni Web 2.0 e MVC rivisto →). Vincoli REST in Web service, XML, JSON, SOAP e RESTUn web service e' un servizio software accessibile via rete da altri programmi, con messaggi in formato standard su HTTP; i formati di dati sono XML (marcatori annidati, attributi, namespace, validazione con XSD) e JSON (RFC 8259: oggetti, array, stringhe, numeri, true, false, null; piu' compatto e diretto per i linguaggi di programmazione); SOAP e' un protocollo di messaggi XML (Envelope con Header facoltativo e Body, Fault per gli errori) inviati di solito con una POST HTTP e descritti da un file WSDL; REST e' uno stile architetturale (Fielding) in cui il servizio espone risorse identificate da URI e le manipola con i metodi HTTP: GET legge, POST crea, PUT sostituisce, PATCH modifica, DELETE elimina, con codici di stato significativi (200, 201 con Location, 204, 400, 404, 409), senza stato sul server, con cache e interfaccia uniforme.Web service, XML, JSON, SOAP e REST →; semantica di metodi e codici da RFC 9110. L'API è senza stato: niente sessione, ogni richiesta porta tutto.

Progettare le risorse: 1 sostantivi del dominio (aula, prenotazione); 2 collezione ed elemento (/api/aule, /api/aule/3); 3 relazioni: risorse piatte con filtri (/api/prenotazioni?aula=1&giorno=2026-10-19) invece dell'annidamento; 4 azioni = metodi HTTP, mai verbi nell'URI (prenotare = POST, annullare = DELETE); 5 rappresentazioni e codici. URI: plurali, minuscoli, senza estensioni, filtri in query, identità nel path, prefisso /api.

Metodo URI Successo Errori
GET /api/aule 200 + array 500
GET /api/aule/{id} 200 404
GET /api/prenotazioni?aula=&giorno= 200 + array (anche []) 400
GET /api/prenotazioni/{id} 200 404
POST /api/prenotazioni 201 + Location + oggetto 400, 415, 422 aula inesistente, 409 orario occupato
DELETE /api/prenotazioni/{id} 204 404
altri 405 + Allow

Metodi: sicuro = nessun cambiamento di stato; idempotente = ripetere ha lo stesso effetto. GET sicuro e idempotente; PUT e DELETE idempotenti; POST no (due richieste possono creare due elementi; qui la seconda dà 409). DELETE ripetuta: effetto uguale, risposta 404. PATCH non garantito.

Codici: 200 lettura; 201 + Location creazione; 204 senza corpo; 400 richiesta malformata (JSON non valido, campo mancante, formato data, ora_inizio non prima di ora_fine, filtro non numerico); 404 risorsa inesistente; 405 metodo non previsto, con Allow obbligatorio; 409 conflitto con lo stato (orario occupato); 415 non è application/json; 422 sintassi giusta ma contenuto impossibile (aula_id inesistente); 500 guasto, dettaglio solo nel log. 4xx = errore del client, 5xx = del server. 404 se la risorsa non esiste, 405 se esiste ma non con quel metodo.

JSON: nomi delle colonne in snake_case, date e ore ISO 8601 come stringhe, collezione = array, errore {"errore": "..."}, id assegnato dal server (ignorato se il client lo manda), UTF-8.

json
{"id": 1, "aula_id": 1, "giorno": "2026-10-19", "ora_inizio": "09:00", "ora_fine": "11:00", "studente": "Rossi"}

Implementazione. Model invariato (Aula, Prenotazione, DAO); la view è un serializzatore (Gson, dipendenza nel pom). Package it.unipd.aule.rest, servlet AuleServlet su /api/aule/* e PrenotazioniServlet su /api/prenotazioni/*. Json: Gson con FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES (aulaId →\to aula_id) e disableHtmlEscaping(); invia(resp, stato, corpo) imposta stato, Content-Type e codifica prima del corpo; nonConsentito = 405 + Allow; id("/12") con Integer.parseInt, NumberFormatException →\to 404.

Dispatch sul path: getPathInfo() = null o "/" →\to collezione; "/12" →\to elemento 12; "/abc" →\to 404. Il container sceglie doGet/doPost/doDelete, dentro si guarda il path.

java
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    try {
        String path = req.getPathInfo();
        if (Json.eCollezione(path)) {
            Integer aulaId = req.getParameter("aula") == null ? null : Integer.valueOf(req.getParameter("aula"));
            Json.invia(resp, 200, dao.lista(aulaId, req.getParameter("giorno")));
            return;
        }
        Prenotazione p = dao.perId(Json.id(path));
        if (p == null) Json.errore(resp, 404, "prenotazione inesistente");
        else Json.invia(resp, 200, p);
    } catch (NumberFormatException e) {
        Json.errore(resp, 404, "risorsa inesistente");
    } catch (SQLException e) {
        log("errore SQL", e);
        Json.errore(resp, 500, "errore interno");
    }
}

doPost, in ordine: getPathInfo() non è la collezione →\to 405 + Allow: GET, DELETE; Content-Type non JSON →\to 415; Json.GSON.fromJson(req.getReader(), Prenotazione.class), null →\to 400 "corpo vuoto", JsonSyntaxException →\to 400; p.setId(0); p.valida() →\to 400; dao.crea(p): PrenotazioneException SOVRAPPOSIZIONE →\to 409, AULA_INESISTENTE →\to 422; successo: header Location: /api/prenotazioni/{id} e 201 con l'oggetto. doDelete: path collezione →\to 405 + Allow: GET, POST; dao.elimina(id) vero →\to 204, falso →\to 404.

java
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    if (!Json.eCollezione(req.getPathInfo())) { Json.nonConsentito(resp, "GET, DELETE"); return; }
    String tipo = req.getContentType();
    if (tipo == null || !tipo.startsWith("application/json")) {
        Json.errore(resp, 415, "serve Content-Type: application/json");
        return;
    }
    req.setCharacterEncoding("UTF-8");
    try {
        Prenotazione p = Json.GSON.fromJson(req.getReader(), Prenotazione.class);
        if (p == null) { Json.errore(resp, 400, "corpo vuoto"); return; }
        p.setId(0);                       // l'id lo assegna il server
        p.valida();
        dao.crea(p);
        resp.setHeader("Location", req.getContextPath() + "/api/prenotazioni/" + p.getId());
        Json.invia(resp, 201, p);
    } catch (JsonSyntaxException e) {
        Json.errore(resp, 400, "il corpo non è un JSON valido");
    } catch (IllegalArgumentException e) {
        Json.errore(resp, 400, e.getMessage());
    } catch (PrenotazioneException e) {
        Json.errore(resp, e.getTipo() == PrenotazioneException.Tipo.SOVRAPPOSIZIONE ? 409 : 422, e.getMessage());
    } catch (SQLException e) {
        log("errore SQL", e);
        Json.errore(resp, 500, "errore interno");
    }
}
http
POST /api/prenotazioni HTTP/1.1
Content-Type: application/json

{"aula_id":1,"giorno":"2026-10-19","ora_inizio":"11:00","ora_fine":"12:30","studente":"Neri"}

HTTP/1.1 201
Location: /api/prenotazioni/5

{"id":5,"aula_id":1,"giorno":"2026-10-19","ora_inizio":"11:00","ora_fine":"12:30","studente":"Neri"}
  • Stessa richiesta con ora_inizio 10:00 e ora_fine 12:00 →\to 409 {"errore":"l'aula è già prenotata in quell'orario"}. GET /api/aule/9 →\to 404; GET /api/prenotazioni?giorno=19-10-2026 →\to 400; POST con aula_id 99 →\to 422; Content-Type: text/plain →\to 415; POST /api/prenotazioni/1 →\to 405 con Allow: GET, DELETE.
  • Prova da riga di comando: curl -i -X POST URL -H "Content-Type: application/json" -d '{...}'.

Fuori dall'esempio: HATEOAS (livello 3 di Richardson), autenticazione con Authorization e 401/403, CORS, paginazione, cache con ETag.

Errori tipici

  • Verbi negli URI; GET che modifica; 200 con l'errore nel corpo; [] sostituito da 404.
  • Dimenticare 201 + Location, o Allow nel 405; confondere 404 e 405.
  • Fidarsi del JSON (tipi, formato, id dal client); mostrare stack trace al client.
  • Stato di sessione nel servizio; corpo scritto prima dello stato.

Domande d'esame (tracce)

  • API delle prenotazioni: risorse aule e prenotazioni, GET, POST (201, 409, 422), DELETE (204, 404), 405 + Allow, JSON con i nomi delle colonne.
  • POST non idempotente (due elementi), DELETE idempotente (seconda risposta 404, stesso stato finale).
  • Una servlet su /api/prenotazioni/*: getPathInfo() nullo o "/" = collezione, "/12" = elemento.

Collegamenti: 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 →, 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 →, Esercizio - Progettazione dell'API REST delle prenotazioni (applicazione web).

Esercizi su questo argomento

Teoria collegata