Salta al contenuto
Note per Studenti Esercizio - Progettazione dell'API REST delle prenotazioni (applicazione web)

Esercizio - Progettazione dell'API REST delle prenotazioni (applicazione web)

Esercizio originale, nello stile di una domanda di progettazione seguita da una parte di codice. Teoria: Servizi REST in Java - progettazione delle risorse e implementazione con servletUn servizio REST espone risorse, non pagine: il server risponde con dati (JSON) e codici di stato, e la presentazione passa al client. Per progettare l'API si individuano i sostantivi del dominio (aula, prenotazione), si dà a collezioni ed elementi un URI (/api/aule, /api/aule/3, /api/prenotazioni, /api/prenotazioni/7: nomi al plurale, mai verbi), si usano i metodi HTTP con la semantica di RFC 9110 (GET sicuro e idempotente, POST crea e non è idempotente, PUT e DELETE idempotenti) e si filtrano le collezioni con la query (?aula=1&giorno=2026-10-19). I codici fanno parte del contratto: 200 lettura, 201 creazione con Location, 204 cancellazione, 400 richiesta malformata, 404 risorsa inesistente, 405 metodo non consentito con Allow, 409 conflitto (orario già occupato), 415 tipo di contenuto, 422 richiesta ben formata ma non elaborabile (aula inesistente), 500 guasto. Il JSON usa i nomi delle colonne (aula_id, ora_inizio, ora_fine) e gli errori hanno un corpo {"errore": "..."}. In Java una servlet mappata su /api/prenotazioni/* fa il dispatch sul path: getPathInfo() nullo o "/" indica la collezione, "/12" l'elemento; doGet, doPost e doDelete implementano i metodi, Gson converte oggetti e JSON, i metodi non previsti rispondono 405 con l'header Allow. Il servizio è senza stato: niente sessione, ogni richiesta porta tutto ciò che serve.Servizi REST in Java - progettazione delle risorse e implementazione con servlet →; vincoli REST: 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 codice Java è stato compilato contro una copia delle sole firme dell'API Jakarta Servlet e provato con richieste simulate (servlet con DAO in memoria sugli stessi dati e un piccolo Gson per la prova); la condizione SQL della modifica è stata provata con SQLite.

Testo

Sull'applicazione delle aule (aule Aula A1 80 posti, Aula B2 45, Lab Info 30; quattro prenotazioni di esempio, tra cui Rossi in A1 il 19 ottobre dalle 09:00 alle 11:00 e Bianchi in A1 dalle 14:00 alle 16:00) un client deve poter:

  1. leggere l'elenco delle aule e una singola aula;
  2. leggere le prenotazioni, filtrate per aula e/o per giorno, e una singola prenotazione;
  3. creare una prenotazione;
  4. cancellarla;
  5. spostarne l'orario (stessa aula e stesso giorno);
  6. conoscere le fasce libere di un'aula in un giorno, dentro l'orario di apertura 08:00-20:00.

Parte A. Progettare l'API: risorse, URI, metodi, codici di stato in caso di successo e di errore, con le scelte motivate (annidare o filtrare, che metodo per lo spostamento, come esporre le fasce libere). Mostrare il JSON di una risposta per il punto 6.

Parte B. Implementare con una servlet il punto 6 (GET), separando il calcolo dall'accesso al database.

Parte C. Per il punto 5, dire come si rende corretto il controllo delle sovrapposizioni: una prenotazione spostata non deve "scontrarsi con se stessa".

Soluzione

Parte A: tabella dell'API

# Metodo URI Successo Errori
1 GET /api/aule 200 array 500
1 GET /api/aule/{id} 200 404
2 GET /api/prenotazioni?aula=&giorno= 200 array (anche vuoto) 400 filtro malformato
2 GET /api/prenotazioni/{id} 200 404
3 POST /api/prenotazioni 201 + Location + corpo 400 dati o JSON malformati, 415 non JSON, 422 aula inesistente, 409 orario occupato
4 DELETE /api/prenotazioni/{id} 204 404
5 PUT /api/prenotazioni/{id} 200 + corpo aggiornato 400, 404, 415, 422, 409
6 GET /api/disponibilita?aula=&giorno= 200 array di fasce 400 parametri mancanti o malformati, 404 aula inesistente
tutti metodo non previsto 405 con Allow

Le scelte:

  • Filtrare, non annidare. Le prenotazioni hanno un URI proprio (/api/prenotazioni/7) e i filtri aula e giorno si combinano; un URI annidato (/api/aule/1/prenotazioni) obbligherebbe a passare sempre dall'aula, e non permette di chiedere "tutte le prenotazioni del giorno".
  • Spostamento con PUT. Cambia lo stato di una risorsa già identificata, quindi non è un POST (che crea). PUT sostituisce l'intera rappresentazione ed è idempotente: ripetere la stessa richiesta lascia la prenotazione nello stesso stato (e, con il controllo giusto della parte C, risponde ancora 200). PATCH con i soli campi ora_inizio e ora_fine sarebbe ammissibile ma non garantisce l'idempotenza; con cancella + ricrea si perderebbe l'identificatore. Non si usa un URI con verbo (/api/prenotazioni/7/sposta).
  • Le fasce libere sono una risorsa derivata, di sola lettura. Non esistono come righe di tabella, ma sono qualcosa che si può chiedere: un nome (disponibilita, la disponibilità) e una GET. Si usa la query (?aula=1&giorno=2026-10-19) perché identifica una vista calcolata su due parametri; l'alternativa /api/aule/{id}/disponibilita?giorno=... è equivalente e legittima. Mai POST: non modifica nulla e deve poter essere memorizzata e ritentata.
  • Errori e codici come nella nota: 400 richiesta che non si capisce; 422 si capisce ma ha un significato impossibile; 404 risorsa inesistente; 405 metodo non previsto sull'URI, con Allow; 409 conflitto con lo stato; 415 formato del corpo. Un'aula senza prenotazioni nel giorno non è un errore: 200 con una fascia che copre tutto il giorno; un'aula che non esiste è 404.

JSON della disponibilità. Aula A1 il 19 ottobre (prenotata dalle 09:00 alle 11:00 e dalle 14:00 alle 16:00):

http
GET /api/disponibilita?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

[{"ora_inizio":"08:00","ora_fine":"09:00"},{"ora_inizio":"11:00","ora_fine":"14:00"},{"ora_inizio":"16:00","ora_fine":"20:00"}]

Parte B: il calcolo e la servlet

Il calcolo delle fasce non dipende da HTTP né dal database: si scrive in una classe a parte, che prende la lista delle prenotazioni del giorno e restituisce le fasce. Si può provare senza server e il DAO resta quello già esistente (lista(aula, giorno)).

L'algoritmo: si ordinano le prenotazioni per ora d'inizio e si tiene un cursore (l'istante fino a cui tutto è stato esaminato), inizialmente l'apertura. Per ogni prenotazione: se comincia dopo il cursore, tra cursore e inizio c'è una fascia libera; il cursore avanza alla fine della prenotazione, ma solo se questa è più avanti del cursore (le prenotazioni di una stessa aula e giorno non si sovrappongono, ma il controllo rende il codice robusto anche con dati incoerenti). Alla fine, se il cursore è prima della chiusura, c'è l'ultima fascia. Gli estremi coincidenti non producono fasce vuote (isAfter è stretto): 11:00-13:00 subito dopo 09:00-11:00 non lascia buchi.

java
package it.unipd.aule.rest;

import it.unipd.aule.model.Prenotazione;

import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

/** Calcolo delle fasce libere di un'aula in un giorno, dentro l'orario di apertura. Nessun accesso al database. */
final class FasceLibere {
    /** Un intervallo libero, serializzato in JSON come {"ora_inizio": "...", "ora_fine": "..."}. */
    static final class Fascia {
        final String oraInizio;
        final String oraFine;

        Fascia(LocalTime inizio, LocalTime fine) {
            this.oraInizio = inizio.toString();
            this.oraFine = fine.toString();
        }
    }

    private FasceLibere() { }

    /** prenotazioni = quelle dell'aula nel giorno (in qualunque ordine); apertura e chiusura delimitano il giorno. */
    static List<Fascia> calcola(List<Prenotazione> prenotazioni, LocalTime apertura, LocalTime chiusura) {
        List<Prenotazione> ordinate = new ArrayList<>(prenotazioni);
        ordinate.sort(Comparator.comparing(p -> LocalTime.parse(p.getOraInizio())));
        List<Fascia> libere = new ArrayList<>();
        LocalTime cursore = apertura;                                    // fino a qui è tutto già considerato
        for (Prenotazione p : ordinate) {
            LocalTime inizio = LocalTime.parse(p.getOraInizio());
            LocalTime fine = LocalTime.parse(p.getOraFine());
            if (inizio.isAfter(cursore)) {                              // c'è un buco prima di questa prenotazione
                libere.add(new Fascia(cursore, inizio.isAfter(chiusura) ? chiusura : inizio));
            }
            if (fine.isAfter(cursore)) {
                cursore = fine;
            }
        }
        if (cursore.isBefore(chiusura)) {                               // buco finale
            libere.add(new Fascia(cursore, chiusura));
        }
        return libere;
    }
}
java
package it.unipd.aule.rest;

import it.unipd.aule.dao.AulaDAO;
import it.unipd.aule.dao.PrenotazioneDAO;
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.LocalTime;
import java.time.format.DateTimeParseException;

/** Risorsa derivata, di sola lettura: GET /api/disponibilita?aula=1&giorno=2026-10-19 elenca le fasce libere (08:00-20:00). */
@WebServlet("/api/disponibilita")
public class DisponibilitaServlet extends HttpServlet {
    private static final LocalTime APERTURA = LocalTime.of(8, 0);
    private static final LocalTime CHIUSURA = LocalTime.of(20, 0);

    private AulaDAO aulaDAO;
    private PrenotazioneDAO prenotazioneDAO;

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

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        try {
            String aula = req.getParameter("aula");
            String giorno = req.getParameter("giorno");
            if (aula == null || giorno == null) {
                Json.errore(resp, HttpServletResponse.SC_BAD_REQUEST, "servono i parametri aula e giorno");
                return;
            }
            int aulaId = Integer.parseInt(aula);
            LocalDate.parse(giorno);
            if (aulaDAO.perId(aulaId) == null) {
                Json.errore(resp, HttpServletResponse.SC_NOT_FOUND, "aula inesistente");
                return;
            }
            Json.invia(resp, HttpServletResponse.SC_OK,
                       FasceLibere.calcola(prenotazioneDAO.lista(aulaId, giorno), APERTURA, CHIUSURA));
        } catch (NumberFormatException | DateTimeParseException e) {
            Json.errore(resp, HttpServletResponse.SC_BAD_REQUEST, "aula intero, giorno aaaa-mm-gg");
        } catch (SQLException e) {
            log("errore SQL", e);
            Json.errore(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "errore interno");
        }
    }
}
  • Il campo Fascia.oraInizio diventa ora_inizio nel JSON per la stessa politica di denominazione delle altre servlet (Json.GSON).
  • La servlet fa soltanto: leggere e validare i parametri, controllare che l'aula esista (404), chiamare il DAO, chiamare il calcolo, rispondere. I parametri non numerici o in un formato sbagliato danno 400 (un unico catch).
  • doGet è il solo metodo ridefinito: gli altri metodi ricevono dal container 405.

Esiti provati con richieste simulate:

Richiesta Risposta
aula=1&giorno=2026-10-19 200, fasce 08:00-09:00, 11:00-14:00, 16:00-20:00
aula=2&giorno=2026-10-19 (nessuna prenotazione) 200, 08:00-20:00
aula=9&giorno=2026-10-19 404 {"errore":"aula inesistente"}
aula=1 (manca giorno) 400 {"errore":"servono i parametri aula e giorno"}
aula=x&giorno=2026-10-19 400

Parte C: spostare senza scontrarsi con se stessa

Il PUT sulla prenotazione 1, da 09:00-11:00 a 10:00-12:00, ha il corpo {"aula_id":1,"giorno":"2026-10-19","ora_inizio":"10:00","ora_fine":"12:00","studente":"Rossi"}. Se si riusasse tale e quale la query della creazione, la prenotazione 1 stessa (09:00-11:00) risulterebbe in conflitto con il nuovo intervallo (10:00-12:00), e lo spostamento verrebbe respinto con 409 anche se nessun altro occupa l'aula. Il rimedio è escludere la riga che si sta modificando:

sql
SELECT COUNT(*)
FROM prenotazione
WHERE aula_id = ? AND giorno = ?
  AND ora_inizio < ? AND ora_fine > ?
  AND id <> ?;                      -- id della prenotazione che si sposta

Prova in SQLite sui dati dell'esempio (la riga 1 è Rossi 09:00-11:00, la 2 Bianchi 14:00-16:00):

Spostamento della prenotazione 1 Conflitti senza id <> ? Conflitti con id <> ?
10:00-12:00 (libero, tranne se stessa) 1 (se stessa) 0
13:00-15:00 (tocca Bianchi 14:00-16:00) 1 (Bianchi) 1

Quindi il PUT da 10:00 a 12:00 risponde 200, mentre da 13:00 a 15:00 risponde 409. Il metodo del DAO è l'analogo di crea (transazione, blocco della riga dell'aula con FOR UPDATE, controllo con id <> ?, UPDATE ... WHERE id = ?), e la servlet gestisce /api/prenotazioni/{id} in doPut: 404 se l'id non esiste, 415/400/422 come per la creazione. Ripetere lo stesso PUT dà ancora 200 (la riga è esclusa dal controllo): è l'idempotenza.

Versione ripasso

Requisiti. Leggere aule (elenco e singola); leggere prenotazioni (filtro per aula e giorno, singola); creare; cancellare; spostare l'orario; conoscere le fasce libere di un'aula in un giorno (apertura 08:00-20:00).

# Metodo URI Successo Errori
1 GET /api/aule, /api/aule/{id} 200 404
2 GET /api/prenotazioni?aula=&giorno=, /api/prenotazioni/{id} 200 400, 404
3 POST /api/prenotazioni 201 + Location 400, 415, 422, 409
4 DELETE /api/prenotazioni/{id} 204 404
5 PUT /api/prenotazioni/{id} 200 + corpo 400, 404, 415, 422, 409
6 GET /api/disponibilita?aula=&giorno= 200 array di fasce 400, 404 aula inesistente
tutti altro metodo 405 + Allow

Scelte.

  • Filtrare e non annidare: URI proprio per la prenotazione, filtri combinabili (anche "tutte le prenotazioni del giorno").
  • Spostamento = PUT sull'URI della prenotazione (risorsa già identificata, rappresentazione completa, idempotente); PATCH ammissibile ma non idempotente garantito; mai /sposta.
  • Fasce libere = risorsa derivata, di sola lettura, GET con la query; mai POST. Aula senza prenotazioni →\to 200 con una fascia 08:00-20:00; aula inesistente →\to 404.
json
[{"ora_inizio":"08:00","ora_fine":"09:00"},{"ora_inizio":"11:00","ora_fine":"14:00"},{"ora_inizio":"16:00","ora_fine":"20:00"}]

(A1 il 19 ottobre, con Rossi 09:00-11:00 e Bianchi 14:00-16:00.)

Calcolo delle fasce in una classe separata (nessun HTTP né database): ordinare le prenotazioni per inizio, cursore = apertura; per ognuna, se l'inizio è dopo il cursore c'è una fascia libera fino all'inizio, poi il cursore passa alla fine; in fondo, se il cursore è prima della chiusura, ultima fascia. Gli estremi coincidenti non danno fasce vuote.

java
LocalTime cursore = apertura;
for (Prenotazione p : ordinate) {
    LocalTime inizio = LocalTime.parse(p.getOraInizio());
    LocalTime fine = LocalTime.parse(p.getOraFine());
    if (inizio.isAfter(cursore)) libere.add(new Fascia(cursore, inizio.isAfter(chiusura) ? chiusura : inizio));
    if (fine.isAfter(cursore)) cursore = fine;
}
if (cursore.isBefore(chiusura)) libere.add(new Fascia(cursore, chiusura));

Servlet DisponibilitaServlet su /api/disponibilita: legge aula e giorno (mancanti →\to 400), Integer.parseInt e LocalDate.parse (errore →\to 400), aulaDAO.perId nullo →\to 404, FasceLibere.calcola(prenotazioneDAO.lista(aulaId, giorno), APERTURA, CHIUSURA), risposta 200. Solo doGet; gli altri metodi →\to 405. Il campo oraInizio diventa ora_inizio con la politica di nomi di Gson.

Richiesta Risposta
aula=1&giorno=2026-10-19 200, 08:00-09:00, 11:00-14:00, 16:00-20:00
aula=2&giorno=2026-10-19 200, 08:00-20:00
aula=9&giorno=2026-10-19 404
aula=1 400
aula=x&giorno=2026-10-19 400

Spostare senza scontrarsi con se stessa. Riusare la query di creazione farebbe contare come conflitto la riga stessa (09:00-11:00 contro 10:00-12:00): 409 sbagliato. Si esclude la riga con id <> ?:

sql
SELECT COUNT(*) FROM prenotazione
WHERE aula_id = ? AND giorno = ?
  AND ora_inizio < ? AND ora_fine > ?
  AND id <> ?;
Spostamento della prenotazione 1 senza id <> ? con id <> ?
10:00-12:00 1 (se stessa) 0
13:00-15:00 (tocca Bianchi 14-16) 1 1

Quindi PUT a 10:00-12:00 →\to 200, a 13:00-15:00 →\to 409. Il metodo del DAO è l'analogo di crea (transazione, FOR UPDATE sull'aula, controllo con id <> ?, UPDATE). Ripetere lo stesso PUT dà ancora 200: idempotenza.

Errori tipici

  • Verbi nell'URI (/sposta); POST per una lettura calcolata.
  • Annidare tutto e non poter chiedere il giorno intero.
  • Dimenticare di escludere la riga stessa nel controllo di una modifica.
  • 404 per "nessuna fascia occupata" (è 200); fasce vuote tra prenotazioni consecutive.

Teoria: Servizi REST in Java - progettazione delle risorse e implementazione con servletUn servizio REST espone risorse, non pagine: il server risponde con dati (JSON) e codici di stato, e la presentazione passa al client. Per progettare l'API si individuano i sostantivi del dominio (aula, prenotazione), si dà a collezioni ed elementi un URI (/api/aule, /api/aule/3, /api/prenotazioni, /api/prenotazioni/7: nomi al plurale, mai verbi), si usano i metodi HTTP con la semantica di RFC 9110 (GET sicuro e idempotente, POST crea e non è idempotente, PUT e DELETE idempotenti) e si filtrano le collezioni con la query (?aula=1&giorno=2026-10-19). I codici fanno parte del contratto: 200 lettura, 201 creazione con Location, 204 cancellazione, 400 richiesta malformata, 404 risorsa inesistente, 405 metodo non consentito con Allow, 409 conflitto (orario già occupato), 415 tipo di contenuto, 422 richiesta ben formata ma non elaborabile (aula inesistente), 500 guasto. Il JSON usa i nomi delle colonne (aula_id, ora_inizio, ora_fine) e gli errori hanno un corpo {"errore": "..."}. In Java una servlet mappata su /api/prenotazioni/* fa il dispatch sul path: getPathInfo() nullo o "/" indica la collezione, "/12" l'elemento; doGet, doPost e doDelete implementano i metodi, Gson converte oggetti e JSON, i metodi non previsti rispondono 405 con l'header Allow. Il servizio è senza stato: niente sessione, ogni richiesta porta tutto ciò che serve.Servizi REST in Java - progettazione delle risorse e implementazione con servlet →.

Teoria collegata