Servlet Router

V tomto článku se můžete naučit pracovat s knihovnou Servlet Router. Jedná se o jednoduchou knihovnu, kterou jsem se rozhodl sám vytvořit a slouží pro routování requestů v servletech. Tato stránka pro ni v podstatě představuje českou dokumentaci. Verze v angličtině je k dispozici zde.

Proč knihovnu Servlet Router použít

Pravděpodobně vás zajímá, proč byste vůbec nějakou knihovnu pro routování, která je navíc vytvořená mnou samotným, měli chtít použít. Důvodů je více.

Mapování URL na servlety prostřednictvím web.xml souboru za mě nepředstavuje zrovna nejlepší způsob, jak ve webové aplikaci provádět routování. Pokud máme hodně servletů, tak se stává dost obtížné se v takovém souboru orientovat. Vždy si tam nejprve musíme servlet nadefinovat, což zabere 4 řádky kódu, a poté jej musíme namapovat na nějakou URL, což zabere také nejmíň 4 řádky kódu. A to ještě nebereme v potaz filtry, které je potřeba v souboru web.xml také nadefinovat a namapovat. Specifikování cesty v elementu url-pattern pro namapování servletu nebo filtru také není moc uživatelsky přívětivé a ne vždy se nám snadno podaří servlet namapovat na URL, na kterou jej namapovat chceme. Dále si nemyslím, že routování pro webovou aplikaci by se mělo někde konfigurovat. Mělo by to být napevno nastaveno v kódu aplikace (pokud se tedy nebavíme o nějakém CMS systému a podobně, kde si třeba uživatel vytváří vlastní stránky). Toto jsou hlavní důvody, které mě přiměly knihovnu pro routování vytvořit a těmto problémům se tak vyhnout.

Výhody proč knihovnu Servlet Router použít shrnuje následující seznam:

  • žádné neohrabané URL mapování v konfiguračním souboru web.xml
  • stačí namapovat jeden vstupní servlet pro všechny requesty a dále je routování závislé již na samotném kódu aplikace (není potřeba složitě konfigurovat soubor web.xml)
  • žádné použití filtrů
  • žádné zmatené nastavovaní URL patternů
  • podpora parametrů přímo v URL cestě (path parametry)
  • lepší orientace v kódu (nemusíte složitě patrát, pro jakou URL se co volá)
  • snadná implementace MVC architektury
  • vestavěná podpora HTTP PATCH metody (třída HttpServlet ji v základu nepodporuje)

Komponenty knihovny

Knihovna Servlet Router obsahuje takové tři základní komponenty: Router, Controller a Handler. Postupně se na ně nyní podíváme a dozvíte se i o jiných komponentách, které na nich staví.

Router

Router je komponenta, která má za úkol mapovat controllery a handlery na specifické cesty. Základní třídou je Router. Tato třída je abstraktní. Může být rozšířena a použita pro routování jakéhokoliv typu ServletRequest a ServletResponse objektu. V 99.9 procentech případů ale budete chtít routovat HttpServletRequest a HttpServletResponse objekty. V takovém případě můžete použít HttpRouter.

HttpRouter můžete vytvořit buď napřímo a poté namapovat příslušné controllery a handlery (což nedoporučuji), nebo můžete vytvořit podtřídu a mapování provést v konstruktoru. Následující ukázka ukazuje první (nedoporučený) postup.

// vytvoření routeru
HttpRouter router = new HttpRouter();
// namapování controlleru
router.register("/", HomeController.class);
// namapování jiného routeru (handleru)
router.register("/info", infoRouter);

Další ukázka ukazuje lepší způsob jak router vytvořit.

public class AppRouter extends HttpRouter {
    public AppRouter() {
        // namapování controlleru
        register("/", HomeController.class);
        // namapování jiného routeru (handleru)
        register("/info", new InfoRouter());
    }
}

Jak jste si v předchozích ukázkách mohli všimnout, tak pro namapování (registraci) controlleru nebo handleru se používá metoda register. Pokud se podíváte do JavaDoc dokumentace, tak uvidíte, že je nejmíň desetkrát přetížená. To je proto, aby se mohly registrovat třídy controllerů i handlery zároveň. V Javě nejde moc jednoduše a typově bezpečně zařídit, aby se mohli jako parametr metody předávat dva různé typy. Vyřešil jsem to tedy tak, že jsem metodu register mnohokrát přetížil, aby byla uživatelsky přívětivá a jednodušeji se používala. Díky tomu se může volat s více handlery nebo controllery jdoucími po sobě, jak ukazuje následující ukázka. Proč byste to měli chtít dělat se později dozvíte.

public class AppRouter extends HttpRouter {
    public AppRouter() {
        register("/", HomeController.class);
        register("/info", new InfoRouter());
        register("/account", new RequireAuthenticationMiddleware(), new AccountRouter());
        register("/admin", new RequireAuthenticationMiddleware(), new RequireAdminPermissionMiddleware(), new AdminRouter());
        register(PageNotFoundController.class);
    }
}

Controller

Další základní komponentou je controller. Ten má za úkol zpracovávat requesty na nějaké konkrétní (většinou) cestě. Podobně jako u routeru, tak i zde je základní třídou třída Controller. Nejčastěji ale budete pracovat s protokolem HTTP a zpracovávat objekty typu HttpServletRequest a HttpServletResponse, takže budete pro tvorbu controlleru rozšiřovat třídu HttpController. Ta poskytuje metody pro zpracování requestů poslaných různými metodami (GET, POST, DELETE, atd.). Následující ukázka ukazuje, jak může controller vypadat.

public class ExampleController extends HttpController {
    @Override
    protected void handleGet(HttpServletRequest request, HttpServletResponse response) throws Exception {
        forwardTo("/WEB-INF/jsp/ExamplePage.jsp", request, response);
    }
}

Následující tabulka ukazuje různé metody třídy HttpController, které můžeme přepsat a zpracovávat tak různé druhy requestů.

MetodaPopis
handleZpracovává requesty a podle metody kterou byly poslány volá příslušné další metody. Tato metoda může být přepsána například pro controller, který řeší 404 stránku, aby reagoval na requesty poslané jakoukoliv metodou.
handleGetZpracovává requesty poslané metodou GET.
handlePostZpracovává requesty poslané metodou POST.
handlePutZpracovává requesty poslané metodou PUT.
handlePatchZpracovává requesty poslané metodou PATCH.
handleDeleteZpracovává requesty poslané metodou DELETE.
handleHeadZpracovává requesty poslané metodou HEAD.
handleOptionsZpracovává requesty poslané metodou OPTIONS. Tato metoda by běžně neměla být přepisována.
handleTraceZpracovává requesty poslané metodou TRACE. Tato metoda by běžně neměla být přepisována.

Kromě metod handleOptions a handleTrace nemají výše zmíněné metody pro zpracování requestů poslaných specifickými metodami defaultní implementaci. Defaultně se pro ně posílá error stránka se status kódem 405 (metoda není podporována). Pokud namísto toho chceme neimplementované metody přeskočit a pokračovat v routování requestu do dalších handlerů/controllerů, tak můžeme nastavit vlastnost skipUnimplementedMethods na true, jak ukazuje následující ukázka.

public class ExampleController extends HttpController {
    public ExampleController() {
        // pro neimplementované metody se nebude posílat error stránka, ale
        // bude se pokračovat v routování requestu na další handlery/controllery
        this.skipUnimplementedMethods = true;
    }

    @Override
    protected void handleGet(HttpServletRequest request, HttpServletResponse response) throws Exception {
        forwardTo("/WEB-INF/jsp/ExamplePage.jsp", request, response);
    }
}

Když se při routování requestu najde controller, který se použije pro zpracování requestu, tak již request neputuje dál do dalších handlerů nebo jiného controlleru. Pokud chceme, aby se s routováním requestu pokračovalo dál, tak máme možnost zavolat metodu continueHandlersChain, jak ukazuje následující ukázka. Možná zatím nevíte co mám tím routováním requestu přesně na mysli, ale až se pustíme do tvorby ukázkového projektu, tak to pravděpodobně pochopíte.

public class ProductController extends HttpController {
    @Override
    protected void handleGet(HttpServletRequest request, HttpServletResponse response) throws Exception {
        String productId = req.getParameter("productId");

        Product product = DBUtil.loadProductById(productId);

        if (product) {
            request.setAttribute("product", product);
            forwardTo("/WEB-INF/jsp/ProductPage.jsp", request, response);
        } else {
            continueHandlersChain();
        }
    }
}

Věc, kterou u klasických servletů běžně neuděláte, je získání parametru přímo z URL cesty. S knihovnou Servlet Router je to ale jednoduché. Stačí si do cesty při její registraci v routeru nadefinovat parametr zapsáním znaku ":" a nějakého názvu. Poté můžeme parametr v controlleru získat pomocí getPathParam metody. Lépe to ukazuje následující ukázka.

public class MyRouter extends HttpRouter {
    public MyRouter() {
        register("/example/:myParam", MyController.class);
    }
}
public class MyController extends HttpController {
    @Override
    protected void handleGet(HttpServletRequest request, HttpServletResponse response) throws Exception {
        String param = getPathParam("myParam");
        request.setAttribute("myPathParameter", param);
        forwardTo("/WEB-INF/jsp/ExamplePage.jsp", request, response);
    }
}

Dobrou praktikou je nerozšiřovat pro tvorbu controllerů přímo třídu HttpController, ale vytvořit si svoji vlastní základní controller třídu. V té si totiž poté můžeme vytvářet metody pro operace, které často v controllerech provádíme. Následující ukázka ukazuje příklad takového základní controlleru.

public abstract class MyAppController extends HttpController {

    protected void login(HttpServletRequest request, User user) {
        HttpSession session = request.getSession();
        session.setAttribute("LOGGED_USER", user);
    }

    protected void logout(HttpServletRequest request) {
        HttpSession session = request.getSession();
        session.removeAttribute("LOGGED_USER");
    }

}

Handler

Router může registrovat controllery a handlery. Co je to controller již víte, ale ještě jsme se nezabývali handlerem. Jedná se o podobnou komponentu jako controller, jelikož také slouží pro zpracovávání requestů. Rozdíl je v tom, že u controlleru v routeru registrujeme třídu controlleru, ale u handleru instanci třídy. Handlerem se může stát jakákoliv třída implementující rozhraní Handler. To definuje metody, které popisuje následující tabulka.

MetodaPopis
handle(Request request, Response response)Volá se pro zpracování requestu. Jako návratovou hodnotu vrací boolean, signalizující, zda se má s routováním requestu pokračovat dál, nebo přestat.
matchesFullPath()Návratová hodnota této metody, která je typu boolean, určuje, zda musí být cesta shodná s request cestou nebo stačí, když je shodný začátek request cesty.
setPathParams(Map<String, String> pathParams)Tato metoda se volá před zavoláním metody handle k nastavení mapy, obsahující parametry v cestě (path parametry).

Následující ukázka ukazuje příklad vytvoření handleru.

public class MyHandler implements Handler<HttpServletRequest, HttpServletResponse> {
    private Map<String, String> pathParams;

    @Override
    public boolean handle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request.setAttribute("test", pathParams.get("test"));
        return true;
    }

    @Override
    public void matchesFullPath() {
        return false;
    }

    @Override
    public void setPathParams(Map<String, String> pathParams) {
        this.pathParams = pathParams;
    }
}
public class MyRouter extends HttpRouter {
    public MyRouter() {
        register("/example/:test", new MyHandler(), MyController.class);
    }
}

Běžně pro tvorbu handlerů nebudete rozhraní Handler implementovat napřímo. Namísto toho většinou budete při tvorbě handlerů rozšiřovat třídu HttpMiddleware. O co se jedná se můžete dozvědět o něco níže.

Rozhraní Handler implementuje také třída Router, takže router je ve své podstatě také handler. Díky tomu můžeme routery do sebe různě vnořovat. Příklad ukazuje následující ukázka.

public class AppRouter extends HttpRouter {
    public AppRouter() {
        register("/", HomeController.class);
        register("/info", new InfoRouter());
        register("/products", new ProductsRouter());
    }
}
public class InfoRouter extends HttpRouter {
    public InfoRouter() {
        register("/about", AboutController.class);
        register("/about-products", AboutProductsController.class);
    }
}
public class ProductsRouter extends HttpRouter {
    public ProductsRouter() {
        register("/glasses", GlassesController.class);
        register("/computers", ComputersController.class);
    }
}

Middleware

Middleware, stejně jako controller, slouží ke zpracování requestů. Typicky se ale používá jen pro provedení nějaké akce pro request a routování requestu může pokračovat dál. Middleware můžeme například použít k omezení přístupu k vybraným controllerům jen pro přihlášené uživatele, k nastavování nějakého atributu na request, a tak podobně. Základní třídou je třída Middleware, ale v naprosté většině případů budete chtít pracovat s protokolem HTTP, takže můžete při tvorbě middlewaru dědit od třídy HttpMiddleware.

Následující ukázka ukazuje příklad middlewaru, který kontroluje, zda je uživatel přihlášen. Pokud ano, tak request pustí dál. V opačném případě jej přesměruje na stránku pro přihlášení.

public class RequireLoginMiddleware extends HttpMiddleware {
    @Override
    public boolean handle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpSession session = request.getSession();

        // pokud je uživatel přihlášený, request pokračuje dál
        if (session.getAttribute("LOGGED_USER") != null) return true;

        // jinak je uživatel přesměrován na login stránku
        response.sendRedirect(request.getAttribute("BASE_URL") + "/login");
        return false;
    }
}
public class AppRouter extends HttpRouter {
    public AppRouter() {
        register(new BaseURLAttributeSetter());
        register("/", HomeController.class);
        register("/login", LoginController.class);
        register("/games", new RequireLoginMiddleware(), new GamesRouter());
    }
}

Middleware je handler a implementuje tedy rozhraní Handler. Metody setPathParams a matchesFullPath implementuje již za nás. Ukládání parametrů v cestě je totiž stejné pro každý middleware, takže by bylo zbytečné to provádět v každém middlewaru, který bychom vytvářeli. Na získávání parametrů z URL cesty namísto toho můžeme použít metodu getPathParam. Metoda matchesFullPath je již implementována proto, že pro většinu middlewaru budeme chtít, aby se s cestou shodoval jen začátek request cesty. Pokud by nám to nevyhovovalo, můžeme metodu matchesFullPath přepsat.

Knihovna Servlet Router poskytuje jeden již předpřipravený middleware, který ve své aplikace budete pravděpodobně chtít na začátku zaregistrovat. Jedná se o BaseURLAttributeSetter. Slouží k nastavení atributu, který obsahuje kořenovou URL vaší aplikace, do requestu. Nemusíte tedy při nastavování cest k různým assetům v JSP stránkách používat relativní cesty, což vede k tomu, že se potom JSP stránka nedá použít pro jinak dlouhé URL. Následující ukázka ukazuje, jak tento middleware můžete zaregistrovat. Defaultní název pro atribut je "BASE_URL". To se ale dá změnit předáním názvu do konstruktoru.

public class AppRouter extends HttpRouter {
    public AppRouter() {
        register(new BaseURLAttributeSetter());
        register("/", HomeController.class);
    }
}
public class HomeController extends HttpController {
    @Override
    protected void handleGet(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // forwardnutí requestu na JSP stránku
        forwardTo("/WEB-INF/jsp/HomePage.jsp", request, response);
    }
}
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Home Page</title>

    <link rel="icon" type="image/svg+xml" href="${BASE_URL}/static/img/favicon.svg">
    <link href="${BASE_URL}/static/css/style.css" rel="stylesheet">
</head>
<body>
    <h1>Welcome!</h1>
</body>
</html>

Error controller

Router umožňuje nastavit speciální controller, sloužící k zachycení vyjímek. Můžeme jej zaregistrovat pomocí metody registerErrorController. Pokud pro router error controller je nastaven, tak se bude volat, když dojde k vyjímce. V opačném případě router vyjímku namísto toho vyhodí.

Základní třídou pro tvorbu error controlleru je třída ErrorController. Pokud ale pracujeme s HTTP protokolem, tak pro vytvoření error controlleru můžeme využít třídu HttpErrorController. Následující ukázka ukazuje, jak takový error controller může vypadat. K získání chyby v handle metodě můžeme použít metodu getException.

public class MyErrorController extends HttpErrorController {
    public MyErrorController(Exception exception) {
        super(exception);
    }

    @Override
    public boolean handle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // vypsání chyby do konzole
        System.out.println(getException().getMessage());

        // nastavení HTTP status kódu na 500
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        
        // zobrazení chybové stránky
        forwardTo("/WEB-INF/jsp/ErrorPage.jsp", request, response);

        return false;
    }
}
public class AppRouter extends HttpRouter {
    public AppRouter() {
        register(new BaseURLAttributeSetter());
        register("/", HomeController.class);

        registerErrorController(MyErrorController.class);
    }
}

Integrace knihovny do aplikace

Při použití knihovny Servlet Router vám stačí vytvořit jen jeden servlet, který v souboru web.xml namapujete pro všechny příchozí requesty. V tomto servletu poté většinou voláte metodu handle nějakého vstupního routeru vaší webové aplikace, když na servlet dorazí request ke zpracování. Jak může takový servlet vypadat ukazuje následující ukázka.

public class AppServlet extends HttpServlet {
    AppRouter appRouter = new AppRouter();
    
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
        try {
            appRouter.handle(req, res);
        } catch (Exception e) {
            System.out.println(e);
            res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Something went wrong.");
        }
    }
}

Jak můžete v souboru web.xml servlet namapovat na všechny příchozí requesty ukazuje následující ukázka. Kromě toho je také důležité určit si, jak se bude jmenovat složka, která se bude nacházet ve složce webapp a obsahovat statické assety jako jsou CSS soubory, obrázky, a tak podobně, a namapovat cestu k ní na defaultní servlet. Když jsou totiž všechny requesty směrovány na servlet, tak je tím pádem zablokovaný přístup k obsahu ve složce webapp (kromě JSP souborů - bohužel.. - Toto mě na JSP štve. I když takto zablokujeme přístup ke složce webapp, tak se uživatel stejně k JSP souborům dostane. Takže JSP soubory stejně musíte vkládat do složky WEB-INF, kde k nim není přímý přístup.). Defaultní servlet slouží k přístupu ke statickým zdrojům a většina webových serverů jej má. Pro Tomcat má tento servlet název "default".

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://Java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 
id="WebApp_ID" version="3.0">
    <servlet>
        <servlet-name>AppServlet</servlet-name>
        <servlet-class>com.example.app.AppServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>AppServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/static/*</url-pattern>
    </servlet-mapping>
</web-app>

Ukázka tvorby projektu

Možná byste rádi viděli, jak vypadá použití knihovny Servlet Router v nějakém projektu. Pustíme se tedy do tvorby ukázkové aplikace. Bude to jednoduchý web, který bude mít hlavní stránku, stránku zobrazující nějaký seznam produktů a stránku s detailem produktu. Produkty bude mít možnost přidávat administrátor, který se bude moci přihlásit. Následující diagram ukazuje, jak bude vypadat struktura webu.

Struktura webu ukázkové aplikace

Začneme tím, že si založíme nový Maven projekt, který můžeme pojmenovat třeba jako "servlet-router-example-app". V souboru pom.xml si do projektu knihovnu Servlet Router přidáme jako dependency, společně se servlety, jak ukazuje následující ukázka.

  • src
    • main
      • java
      • resources
      • webapp
    • test
      • java
      • resources
  • target
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>io.github.jirkasa</groupId>
    <artifactId>servlet-router-example-app</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>servlet-router-example-app</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>17</java.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>io.github.jirkasa</groupId>
            <artifactId>servlet-router</artifactId>
            <version>1.0.1</version>
        </dependency>
    </dependencies>
</project>

První věc, kterou můžeme udělat, je, že si založíme kořenový router naší aplikace. Můžeme jej pojmenovat třeba jako AppRouter. Zatím bude prázdný, akorát na začátku zaregistrujeme BaseURLAttributeSetter pro nastavování atributu obsahující kořenovou URL naší aplikace. Můžeme si pro něj vytvořit java balíček, který pojmenujeme třeba jako com.example.app.routes, a vytvořit jej tam.

package com.example.app.routes;

import io.github.jirkasa.servletrouter.BaseURLAttributeSetter;
import io.github.jirkasa.servletrouter.HttpRouter;

public class AppRouter extends HttpRouter {
    public AppRouter() {
        register(new BaseURLAttributeSetter());
    }
}

Teď si můžeme založit vstupní servlet pro naši aplikaci (třeba v balíčku com.example.app). Když na něj dorazí ke zpracování request, tak jej předáme do našeho kořenového routeru metodou handle. Volání této metody musíme obalit do try-catch bloku. Tím zajistíme, že když v routeru dojde k jakékoliv chybě tak ji zachytíme. Později si vytvoříme i vlastní error controller, takže k vyhození vyjímky až do tohoto try-catch bloku pravděpodobně vůbec nebude docházet, ale pro jistotu budeme vracet chybovou stránku metodou sendError, kdyby přece jenom chyba doputovala až sem.

package com.example.app;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.example.app.routes.AppRouter;

public class AppServlet extends HttpServlet {
    AppRouter appRouter = new AppRouter();
    
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
        try {
            appRouter.handle(req, res);
        } catch (Exception e) {
            System.out.println(e);
            res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Bohužel došlo k chybě.");
        }
    }
}

Vytvořený servlet si namapujeme pro všechny příchozí requesty. Ve složce webapp si tedy založíme podsložku WEB-INF a v ní soubor web.xml, ve kterém náš servlet namapujeme. Následující ukázka ukazuje jeho obsah.

  • src/main/webapp/WEB-INF
  • src/main/webapp/WEB-INF
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://Java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 
id="WebApp_ID" version="3.0">
    <servlet>
        <servlet-name>AppServlet</servlet-name>
        <servlet-class>com.example.app.AppServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>AppServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

Základní přípravu projektu máme hotovou. Teď se pustíme do tvorby domovské stránky. Než to ale uděláme, tak si vytvoříme svoji vlastní základní controller třídu, namísto toho abychom u controllerů dědili přímo třídu HttpController. To je dobrá praktika, protože si v této základní třídě potom můžeme definovat metody pro kód, který třeba často spouštíme ve více controllerech. Vytvoříme si tedy základní abstraktní třídu pro controllery, kterou můžeme pojmenovat třeba jako ExampleAppController. Bude dědit od třídy HttpController a v našem příkladu bude prázdná. To ale nevadí, hlavní je, že budeme mít možnost si základní třídu pro controllery kdykoliv rozšířit, bez modifikace již existujících controllerů. Vytvoříme ji třeba v kořenovém balíčku com.example.app.

package com.example.app;

import io.github.jirkasa.servletrouter.HttpController;

public abstract class ExampleAppController extends HttpController {}

Po vytvoření základní třídy můžeme začít s tvorbou controlleru pro domovskou stránku. Vytvoříme jej v balíčku com.example.app.routes a pojmenujeme jej třeba jako HomeController. Tento controller bude úplně jednoduchý, jelikož akorát předá request ke zpracování do JSP souboru k vyrenderování HTML stránky.

package com.example.app.routes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.example.app.ExampleAppController;

public class HomeController extends ExampleAppController {
    @Override
    protected void handleGet(HttpServletRequest request, HttpServletResponse response) throws Exception {
        forwardTo("/WEB-INF/jsp/HomePage.jsp", request, response);
    }
}

Teď tedy vytvoříme JSP stránku. Ve složce WEB-INF (aby se k JSP stránkám napřímo nedostali uživatelé) si založíme složku jsp a v ní soubor "HomePage.jsp". V následující ukázce si můžete prohlédnout jeho kód. Hlavní stránka akorát vypíše nějaký uvítací text a zobrazí odkaz na stránku se seznamem produktů. Můžete si všimnout, že v odkazu používáme atribut BASE_URL, který nám nastavuje middleware BaseURLAttributeSetter. Tento atribut obsahuje kořenovou URL naší aplikace.

  • src/main/webapp/WEB-INF/jsp
  • src/main/webapp/WEB-INF/jsp
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ include file="./includes/PageStart.jsp" %>
    <h1>Vítejte</h1>
    <p>Tento web představuje ukázkovou aplikaci pro knihovnu Servlet Router. Kliknutím na odkaz níže si můžete prohlédnout seznam produktů.</p>
    <a href="${BASE_URL}/produkty" class="button">Seznam produktů</a>
<%@ include file="./includes/PageEnd.jsp" %>

V předchozím kódu pro domovskou stránku používáme include direktivu k připojení obsahu jiných souborů. Ty jsme ale zatím ještě nevytvořili, takže to uděláme teď. Děláme to tak proto, že začátek a konec stránky bude společný pro všechny stránky v naší aplikaci.

První soubor, který se připojuje na začátku, bude obsahovat kód pro začátek stránky. Vytvoříme jej ve složce includes, kterou založíme, a pojmenujeme jej jako "PageStart.jsp". Jeho obsah ukazuje následující ukázka.

  • src/main/webapp/WEB-INF/jsp/includes
  • src/main/webapp/WEB-INF/jsp/includes
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Ukázková aplikace</title>
</head>
<body>
    <div class="page">

Soubor, který se připojuje na konci, bude obsahovat kód pro konec stránky. Ve složce includes si vytvoříme soubor "PageEnd.jsp". Kód pro něj můžete vidět v ukázce.

  • src/main/webapp/WEB-INF/jsp/includes
<%@page contentType="text/html" pageEncoding="UTF-8"%>
    </div>
</body>
</html>

Poslední věcí, kterou musíme pro domovoskou stránku udělat, je zaregistrovat controller v routeru, jak ukazuje následující ukázka.

package com.example.app.routes;

import io.github.jirkasa.servletrouter.BaseURLAttributeSetter;
import io.github.jirkasa.servletrouter.HttpRouter;

public class AppRouter extends HttpRouter {
    public AppRouter() {
        register(new BaseURLAttributeSetter());
        register("/", HomeController.class);
    }
}

Nyní si můžete aplikaci spustit a po navštívení http://localhost:8080/servlet-router-example-app/ byste měli vidět stránku, kterou ukazuje následující obrázek.

Hlavní stránka aplikace

Nevypadá to moc pěkně, takže si teď stránku nastylujeme pomocí CSS stylů. Tím se dostáváme k tomu, že si ve složce webapp musíme vytvořit nějakou složku, do které budeme dávat věci jako jsou obrázky, JavaScript soubory, CSS styly, a tak podobně. Musíme na ni totiž namapovat defaultní servlet, protože jinak by všechny requesty pro tyto soubory putovali do našeho servletu, který jsme pojmenovali jako "AppServlet". Tuto složku tedy založíme a nazveme ji třeba jako "static". Poté v ní můžeme vytvořit soubor s CSS styly. Kód pro něj ukazuje následující ukázka, takže si jej můžete zkopírovat.

  • src/main/webapp/static
  • src/main/webapp/static
*, *::before, *::after {
    margin: 0;
    padding: 0;
    box-sizing: inherit;
}

html {
    font-size: 62.5%; /* 1rem = 10px */
}

body {
    box-sizing: border-box;
    overflow-x: hidden;
    
    padding: 1.6rem;
    background-color: #F4EFEA;
}

.page {
    width: 100%;
    max-width: 80rem;
    background-color: #FFFFFF;
    
    margin: 0 auto;
    padding: 1.6rem;
}

h1 {
    font-size: 3.2rem;
    line-height: 1;
    
    color: #292726;
    
    margin-bottom: .8rem;
}

p {
    font-size: 1.6rem;
    color: #5B5854;
    
    margin-bottom: .8rem;
}

.button, .button:link, .button:visited {
    font-family: inherit;
    font-size: 1.6rem;
    font-weight: 500;
    line-height: 2rem;
    
    display: inline-block;
    
    text-transform: uppercase;
    text-decoration: none;
    color: #292726;
    background-color: #F8E4B9;
    border: none;
    border-radius: .4rem;

    padding: 1.2rem 1.6rem;

    cursor: pointer;
}

table {
	font-size: 1.6rem;
	border-collapse: collapse;
	width: 100%;
}
td, th {
	border: 1px solid #292726;
	padding: .8rem 1.2rem;
}

V souboru PageStart.jsp, který obsahuje kód pro začátek stránky, si soubor s CSS styly připojíme. Použijeme atribut BASE_URL, aby to fungovalo pro jakoukoliv stránku, ve které se soubor PageStart.jsp includuje.

  • src/main/webapp/WEB-INF/jsp/includes
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Ukázková aplikace</title>
    
    <link href="${BASE_URL}/static/style.css" rel="stylesheet">
</head>
<body>
    <div class="page">

V souboru web.xml naši nově vytvořenou složku static namapujeme na defaultní servlet, jak ukazuje následující ukázka.

  • src/main/webapp/WEB-INF
  • src/main/webapp/WEB-INF
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://Java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 
id="WebApp_ID" version="3.0">
    <servlet>
        <servlet-name>AppServlet</servlet-name>
        <servlet-class>com.example.app.AppServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>AppServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/static/*</url-pattern>
    </servlet-mapping>
</web-app>

Po restartu serveru a obnovení stránky byste měli vidět, že se stránka nastylovala.

Hlavní stránka aplikace

Hlavní stránku naší aplikace máme. Teď se pustíme do tvorby stránek pro admina. Ty bude mít na starosti samostatný router. Vytvoříme si jej tedy a umístíme jej do nového balíčku, který nazveme třeba com.example.app.routes.admin. Záleží na vás jak si budete chtít jednotlivé třídy rozdělovat do balíčků. V této ukázkové aplikaci jsem se to rozhodl udělat tak, že pro každý router a jeho controllery máme samostatný balíček.

package com.example.app.routes.admin;

import io.github.jirkasa.servletrouter.HttpRouter;

public class AdminRouter extends HttpRouter {
    public AdminRouter() {
        
    }
}

Jako první si vytvoříme domovskou stránku pro admina. Na té se bude nacházet jen nadpis "Administrace" a odkaz na stránku pro přidání nového produktu. Budeme potřebovat controller a JSP stránku. Ukazují je následující ukázky. Název souboru pro stránku je "AdminHomePage.jsp".

package com.example.app.routes.admin;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.example.app.ExampleAppController;

public class AdminHomeController extends ExampleAppController {
    @Override
    protected void handleGet(HttpServletRequest request, HttpServletResponse response) throws Exception {
        forwardTo("/WEB-INF/jsp/AdminHomePage.jsp", request, response);
    }
}
  • src/main/webapp/WEB-INF/jsp
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ include file="./includes/PageStart.jsp" %>
    <h1>Administrace</h1>
    <a href="${BASE_URL}/admin/pridat-produkt" class="button">Přidat produkt</a>
<%@ include file="./includes/PageEnd.jsp" %>

V admin routeru controller namapujeme, jak ukazuje následující ukázka.

package com.example.app.routes.admin;

import io.github.jirkasa.servletrouter.HttpRouter;

public class AdminRouter extends HttpRouter {
    public AdminRouter() {
        register("/", AdminHomeController.class);
    }
}

Náš admin router také musíme namapovat v našem kořenovém routeru. Následující ukázka ukazuje upravený kód.

package com.example.app.routes;

import com.example.app.routes.admin.AdminRouter;

import io.github.jirkasa.servletrouter.BaseURLAttributeSetter;
import io.github.jirkasa.servletrouter.HttpRouter;

public class AppRouter extends HttpRouter {
    public AppRouter() {
        register(new BaseURLAttributeSetter());
        register("/", HomeController.class);
        register("/admin", new AdminRouter());
    }
}

Pokud nyní navštívíte http://localhost:8080/servlet-router-example-app/admin/, tak uvidíte stránku, kterou ukazuje následující obrázek.

Hlavní stránka administrace

V naší aplikaci nechceme, aby mohl kdokoliv stránky administrace navštívit. Proto si je nyní zabezpečíme. Vytvoříme si middleware, který bude kontrolovat, zda je uživatel přihlášen jako admin. Pokud ano, tak jej pustí dál, jinak zobrazí přihlašovací stránku.

Na tvorbu middlewarů si vytvoříme nový java balíček, který pojmenujeme třeba jako com.example.app.middlewares. V něm poté vytvoříme třídu, kterou nazveme například jako RequireAdminLogin. Následující ukázka ukazuje její kód. Pokud v middlewaru zjistíme, že je uživatel přihlášen (má v session nastaven atribut ADMIN_LOGGED_IN), tak pustíme request dál. V opačném případě zobrazíme stránku pro přihlášení.

package com.example.app.middlewares;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import io.github.jirkasa.servletrouter.HttpMiddleware;

public class RequireAdminLogin extends HttpMiddleware {
    @Override
    public boolean handle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpSession session = request.getSession();
        
        if (session.getAttribute("ADMIN_LOGGED_IN") != null) return true;
        
        request.getRequestDispatcher("/WEB-INF/jsp/AdminLoginPage.jsp").forward(request, response);
        return false;
    }
}

Kód JSP stránky pro přihlášení ukazuje následující ukázka. Nachází se na ní formulář, který uživatel může vyplnit a poslat.

  • src/main/webapp/WEB-INF/jsp
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ include file="./includes/PageStart.jsp" %>
    <h1>Administrace - přihlášení</h1>
    <form action="${BASE_URL}/admin/login" method="POST">
        <input name="username" type="text" required>
        <input name="password" type="password" required>
        <button type="submit">Přihlásit se</button>
    </form>
<%@ include file="./includes/PageEnd.jsp" %>

V kořenovém routeru musíme middleware zaregistrovat před admin routerem, jak ukazuje následující ukázka.

package com.example.app.routes;

import com.example.app.middlewares.RequireAdminLogin;
import com.example.app.routes.admin.AdminRouter;

import io.github.jirkasa.servletrouter.BaseURLAttributeSetter;
import io.github.jirkasa.servletrouter.HttpRouter;

public class AppRouter extends HttpRouter {
    public AppRouter() {
        register(new BaseURLAttributeSetter());
        register("/", HomeController.class);
        register("/admin", new RequireAdminLogin(), new AdminRouter());
    }
}

Teď byste již po novém načtení hlavní stránky administrace měli vidět, že se vám namísto ní zobrazila stránka pro přihlášení.

Stránka pro přihlášení do administrace

Formulář na stránce pro přihlášení se posílá metodou POST na /admin/login. Vytvoříme si tedy controller, který tento formulář bude zpracovávat. Nazveme jej třeba jako AdminLoginController. Jelikož budeme chtít zpracovávat POST request, tak implementujeme metodu handlePost. V této metodě zjistíme, zda jsou zadané údaje správně a pokud ano, tak uživatele přihlásíme (nastavíme mu do session atribut). V opačném případě jej jen znovu přesměrujeme na hlavní admin stránku, kde se mu opět zobrazí přihlašovací formulář (žádné validační zprávy apod. v našem příkladu neřešíme). Následující ukázka ukazuje kód controlleru. Správné údaje pro přihlášení máme zadané přímo v kódu. V reálné aplikaci by to samozřejmě nebylo bezpečné, ale toto je jen ukázkový projekt.

package com.example.app.routes.admin;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.example.app.ExampleAppController;

public class AdminLoginController extends ExampleAppController {
    private String CORRECT_USERNAME = "admin";
    private String CORRECT_PASSWORD = "password";
    
    @Override
    protected void handlePost(HttpServletRequest request, HttpServletResponse response) throws Exception {
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        
        if (
            username != null
            && password != null
            && CORRECT_USERNAME.equals(username)
            && CORRECT_PASSWORD.equals(password)
        ) {
            HttpSession session = request.getSession();
            session.setAttribute("ADMIN_LOGGED_IN", true);
        }
        
        response.sendRedirect(request.getAttribute("BASE_URL") + "/admin");
    }
}

Jelikož chceme, aby ke controlleru pro přihlášení do administrace měl přístup jakýkoliv uživatel, tak jej nemůžeme zaregistrovat v admin routeru. Ten je totiž chráněn naším middlewarem, který kontroluje, zda je uživatel přihlášený jako administrátor. Request by se do controlleru nedostal. Proto controller pro přihlášení zaregistrujeme přímo v našem kořenovém routeru, jak ukazuje následující ukázka. Routery a middlewary se volají, i když je pro ně shodný jen začátek request cesty a proto musíme controller zaregistrovat ještě před registrací admin routeru. Jinak by se request do controlleru nedostal.

package com.example.app.routes;

import com.example.app.middlewares.RequireAdminLogin;
import com.example.app.routes.admin.AdminLoginController;
import com.example.app.routes.admin.AdminRouter;

import io.github.jirkasa.servletrouter.BaseURLAttributeSetter;
import io.github.jirkasa.servletrouter.HttpRouter;

public class AppRouter extends HttpRouter {
    public AppRouter() {
        register(new BaseURLAttributeSetter());
        register("/", HomeController.class);
        register("/admin/login", AdminLoginController.class);
        register("/admin", new RequireAdminLogin(), new AdminRouter());
    }
}

Pokud nyní zkusíte přihlašovací formulář správně vyplnit a poslat (uživatelské jméno je "admin" a heslo "password"), tak byste se měli úspěšně přihlásit a vidět hlavní stránku administrace.

Teď si vytvoříme stránku, pomocí které bude mít administrátor možnost přidat nový produkt. Budeme potřebovat controller a JSP stránku s formulářem. Kód pro ně ukazuje následující ukázka. Controller zatím implementuje jen metodu handleGet a předává request ke zpracování do JSP stránky jménem "AdminAddProductPage.jsp".

package com.example.app.routes.admin;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.example.app.ExampleAppController;

public class AddProductController extends ExampleAppController {
    @Override
    protected void handleGet(HttpServletRequest request, HttpServletResponse response) throws Exception {
        forwardTo("/WEB-INF/jsp/AdminAddProductPage.jsp", request, response);
    }
}
  • src/main/webapp/WEB-INF/jsp
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ include file="./includes/PageStart.jsp" %>
    <h1>Administrace - přidat produkt</h1>
    <form action="${BASE_URL}/admin/pridat-produkt" method="POST">
        <label>Název:</label>
        <input name="name" type="text" required><br>
        <label>Cena:</label>
        <input name="price" type="number" required><br>
        <button type="submit">Přidat</button>
    </form>
<%@ include file="./includes/PageEnd.jsp" %>

V admin routeru nově vytvořený controller zaregistrujeme.

package com.example.app.routes.admin;

import io.github.jirkasa.servletrouter.HttpRouter;

public class AdminRouter extends HttpRouter {
    public AdminRouter() {
        register("/", AdminHomeController.class);
        register("/pridat-produkt", AddProductController.class);
    }
}

Pokud teď na hlavní stránce administrace kliknete na "přidat produkt", tak se vám zobrazí stránka s formulářem, kterou ukazuje následující obrázek.

Stránka pro přidání produktu

Formulář, který se na stránce nachází, směřuje na ten samý controller, který stránku zobrazuje, a posílá se metodou POST. V controlleru tedy implementujeme metodu handlePost, zpracujeme formulář a vytvoříme nový produkt. Tím se dostáváme k tomu, že si musíme vytvořené produkty nějak ukládat. Pro naše účely bude stačit, když si je uložíme jen v paměti. Produkt bude v naší aplikaci reprezentovat následující třída, kterou můžeme vytvořit třeba v novém balíčku, který nazveme například jako "com.example.app.model".

package com.example.app.model;

public class Product {
    private int id;
    private String name;
    private double price;
    
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    
    public double getPrice() {
        return price;
    }
    public void setPrice(double price) {
        this.price = price;
    }
}

Pro přidávání a získávání produktů budeme používat následující třídu, kterou můžete vytvořit také v balíčku "com.example.app.model". Nemusíte se zabývat tím, jak to funguje. Obsahuje tři statické metody. Pomocí metody addProduct máme možnost přidat nový produkt, metodou getProducts můžeme získat seznam všech uložených produktů a metodou getProductById můžeme získat produkt podle ID.

package com.example.app.model;

import java.util.LinkedList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;

public class ProductsDatabase {
    private ProductsDatabase() {}
    
    private static int counter = 0;
    
    public static void addProduct(HttpServletRequest request, Product product) {
        List<Product> products = (List<Product>) request.getServletContext().getAttribute("PRODUCTS");
        if (products == null) {
            products = new LinkedList<Product>();
            request.getServletContext().setAttribute("PRODUCTS", products);
        }
        
        product.setId(counter);
        counter++;
        
        products.add(product);
    }
    
    public static List<Product> getProducts(HttpServletRequest request) {
        List<Product> products = (List<Product>) request.getServletContext().getAttribute("PRODUCTS");
        if (products == null) products = new LinkedList<Product>();
        return products;
    }
    
    public static Product getProductById(HttpServletRequest request, int id) {
        List<Product> products = (List<Product>) request.getServletContext().getAttribute("PRODUCTS");
        if (products == null) products = new LinkedList<Product>();
        
        for (Product product : products) {
            if (product.getId() == id) return product;
        }
        
        return null;
    }
}

Teď tedy v controlleru pro přidání produktu implementujeme metodu handlePost a pomocí třídy, kterou jsme si právě vytvořili, uložíme produkt, který vytvoříme podle hodnot z formuláře. Následující ukázka kód metody handlePost ukazuje. Nezabýváme se žádnou validací, takže pokud uživatel třeba nějakou hodnotu nezadá, tak je nám to jedno. Pro jednoduchost po uložení produktu uživatele jen přesměrujeme na hlavní stránku administrace.

package com.example.app.routes.admin;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.example.app.ExampleAppController;
import com.example.app.model.Product;
import com.example.app.model.ProductsDatabase;

public class AddProductController extends ExampleAppController {
    @Override
    protected void handleGet(HttpServletRequest request, HttpServletResponse response) throws Exception {
        forwardTo("/WEB-INF/jsp/AdminAddProductPage.jsp", request, response);
    }

    @Override
    protected void handlePost(HttpServletRequest request, HttpServletResponse response) throws Exception {
        String name = (String) request.getParameter("name");
        double price = Double.parseDouble((String) request.getParameter("price"));
        
        Product product = new Product();
        product.setName(name);
        product.setPrice(price);
        
        ProductsDatabase.addProduct(request, product);
        
        response.sendRedirect(request.getAttribute("BASE_URL") + "/admin");
    }
}

Můžete si teď produkt klidně zkusit přidat, ale zatím jej samozřejmě nikde neuvidíte. Po odeslání formuláře byste jen měli být přesměrováni na hlavní stránku administrace.

S administrací jsme hotovi. Teď nám již zbývá jen vytvořit stránku zobrazující seznam produktů a stránku zobrazující detail produktu. Pro tyto stránky si založíme router, který vytvoříme v novém balíčku, který můžeme nazvat jako "com.example.app.routes.products".

package com.example.app.routes.products;

import io.github.jirkasa.servletrouter.HttpRouter;

public class ProductsRouter extends HttpRouter {
    public ProductsRouter() {
        
    }
}

Začneme se stránkou, zobrazující seznam produktů. Následující ukázky pro ni ukazující controller a JSP stránku. V controlleru získáme seznam produktů, nastavíme je jako atribut do requestu a JSP stránka je zobrazí.

package com.example.app.routes.products;

import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.example.app.ExampleAppController;
import com.example.app.model.Product;
import com.example.app.model.ProductsDatabase;

public class ProductsController extends ExampleAppController {
    @Override
    protected void handleGet(HttpServletRequest request, HttpServletResponse response) throws Exception {
        List<Product> products = ProductsDatabase.getProducts(request);
        
        request.setAttribute("products", products);
        
        forwardTo("/WEB-INF/jsp/ProductsListPage.jsp", request, response);
    }
}
  • src/main/webapp/WEB-INF/jsp
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ page import="com.example.app.model.Product" %>
<%@ page import="java.util.List" %>
<%@ include file="./includes/PageStart.jsp" %>
    <h1>Produkty</h1>
    <table>
        <thead>
            <tr>
                <th>Název</th>
                <th>Cena</th>
            </tr>
        </thead>
        <tbody>
            <% for (Product product : (List<Product>) request.getAttribute("products")) { %>
            <tr>
                <td><a href="${BASE_URL}/produkty/<%= product.getId() %>"><%= product.getName() %></a></td>
                <td><%= product.getPrice() %></td>
            </tr>
            <% } %>
        </tbody>
    </table>
<%@ include file="./includes/PageEnd.jsp" %>

V routeru nově vytvořený controller zaregistrujeme, jak ukazuje následující ukázka.

package com.example.app.routes.products;

import io.github.jirkasa.servletrouter.HttpRouter;

public class ProductsRouter extends HttpRouter {
    public ProductsRouter() {
        register("/", ProductsController.class);
    }
}

Nesmíme také zapomenout náš router zaregistrovat v kořenovém routeru naší aplikace.

package com.example.app.routes;

import com.example.app.middlewares.RequireAdminLogin;
import com.example.app.routes.admin.AdminLoginController;
import com.example.app.routes.admin.AdminRouter;
import com.example.app.routes.products.ProductsRouter;

import io.github.jirkasa.servletrouter.BaseURLAttributeSetter;
import io.github.jirkasa.servletrouter.HttpRouter;

public class AppRouter extends HttpRouter {
    public AppRouter() {
        register(new BaseURLAttributeSetter());
        register("/", HomeController.class);
        register("/produkty", new ProductsRouter());
        register("/admin/login", AdminLoginController.class);
        register("/admin", new RequireAdminLogin(), new AdminRouter());
    }
}

Pokud teď na hlavní stránce kliknete na tlačítko "seznam produktů", tak se vám zobrazí stránka, kterou ukazuje následující obrázek. Nejdříve si ale samozřejmě budete muset nějaké produkty přidat v administraci.

Stránka se seznamem produktů

Na každou položku v tabulce máme možnost kliknout a dostat se tak na stránku s detailem. U každé položky je odkaz který směřuje na /produkty/:id. Vytvoříme tedy controller, který bude stránku s detailem zobrazovat. Následující ukázka jej ukazuje a pod ní se můžete podívat také na kód JSP stránky.

package com.example.app.routes.products;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.example.app.ExampleAppController;
import com.example.app.model.Product;
import com.example.app.model.ProductsDatabase;

public class ProductController extends ExampleAppController {
    @Override
    protected void handleGet(HttpServletRequest request, HttpServletResponse response) throws Exception {
        String idParam = getPathParam("id");
        int id = Integer.parseInt(idParam);
        
        Product product = ProductsDatabase.getProductById(request, id);
        request.setAttribute("product", product);
        
        forwardTo("/WEB-INF/jsp/ProductDetailPage.jsp", request, response);
    }
}
  • src/main/webapp/WEB-INF/jsp
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ page import="com.example.app.model.Product" %>
<%@ include file="./includes/PageStart.jsp" %>
    <h1>Detail produktu</h1>
    <p>Název: ${product.name}</p>
    <p>Cena: ${product.price}</p>
    <p><a href="${BASE_URL}/produkty">zpět na seznam</a></p>
<%@ include file="./includes/PageEnd.jsp" %>

Vytvořený controller zaregistrujeme v routeru.

package com.example.app.routes.products;

import io.github.jirkasa.servletrouter.HttpRouter;

public class ProductsRouter extends HttpRouter {
    public ProductsRouter() {
        register("/", ProductsController.class);
        register("/:id", ProductController.class);
    }
}

Pokud teď na nějaký produkt kliknete, tak se vám stránka s detailem produktu otevře.

Stránka s detailem produktu

Naše aplikace je v podstatě hotová. Mohli bychom si ale ještě vytvořit error controller, který by zobrazil chybovou stránku, pokud by náhodou v nějakém našem controlleru došlo k chybě. Kromě toho bychom také mohli přidat 404 stránku pro případ, že se pro URL nenajde žádný controller.

Začneme s 404 stránkou. V balíčku "com.example.app.routes" si vytvoříme controller, který můžeme nazvat třeba jako PageNotFoundController. Tento controller bude trochu jiný než ostatní, jelikož budeme chtít, aby zpracovával requesty bez závislosti na tom, jakou metodou byly poslány. Přepíšeme v něm tedy metodu handle, ve které nastavíme HTTP status kód na 404 (stránka nenalezena) a vyrenderujeme JSP stránku, na které bude napsáno, že se stránka nenašla. Následující ukázky ukazují kód controlleru a JSP stránky, kterou zobrazuje.

package com.example.app.routes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.example.app.ExampleAppController;

public class PageNotFoundController extends ExampleAppController {
    @Override
    public boolean handle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        response.setStatus(HttpServletResponse.SC_NOT_FOUND);
        forwardTo("/WEB-INF/jsp/PageNotFoundPage.jsp", request, response);
        return false;
    }
}
  • src/main/webapp/WEB-INF/jsp
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ include file="./includes/PageStart.jsp" %>
    <h1>Stránka nenalezena</h1>
    <p>Stránka bohužel neexistuje. Pokračujte na <a href="${BASE_URL}">úvodní stránku</a>.</p>
<%@ include file="./includes/PageEnd.jsp" %>

Controller zaregistrujeme jako poslední controller v našem kořenovém routeru pro všechny requesty. Pokud se tedy při routování nenajde jiný controller, který by request zpracoval, tak se použije.

package com.example.app.routes;

import com.example.app.middlewares.RequireAdminLogin;
import com.example.app.routes.admin.AdminLoginController;
import com.example.app.routes.admin.AdminRouter;
import com.example.app.routes.products.ProductsRouter;

import io.github.jirkasa.servletrouter.BaseURLAttributeSetter;
import io.github.jirkasa.servletrouter.HttpRouter;

public class AppRouter extends HttpRouter {
    public AppRouter() {
        register(new BaseURLAttributeSetter());
        register("/", HomeController.class);
        register("/produkty", new ProductsRouter());
        register("/admin/login", AdminLoginController.class);
        register("/admin", new RequireAdminLogin(), new AdminRouter());
        register(PageNotFoundController.class);
    }
}

Pokud nyní navštívíte nějakou URL, která není namapována na žádný controller, tak by se vám měla zobrazit stránka, kterou ukazuje následující obrázek.

Stránka nenalezena

Teď si tedy ještě vytvoříme error controller, který zobrazí chybovou stránku, když dojde k nějaké chybě, a tím budeme naši aplikaci považovat za hotovou. Vytvoříme jej v balíčku "com.example.app.routes" a pojmenujeme jej třeba jako "ServerErrorController". V metodě handle si akorát vypíšeme chybovou zprávu do konzole, nastavíme HTTP status kód na 500 a vyrenderujeme stránku, oznamující že došlo k chybě. Kód controlleru i JSP stránky ukazující následující ukázky.

package com.example.app.routes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import io.github.jirkasa.servletrouter.HttpErrorController;

public class ServerErrorController extends HttpErrorController {
    public ServerErrorController(Exception exception) {
        super(exception);
    }

    @Override
    public boolean handle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println(getException().getMessage());
        
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        
        forwardTo("/WEB-INF/jsp/ErrorPage.jsp", request, response);
        return false;
    }
    
}
  • src/main/webapp/WEB-INF/jsp
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ include file="./includes/PageStart.jsp" %>
    <h1>Došlo k chybě</h1>
    <p>Bohužel došlo k chybě.</p>
<%@ include file="./includes/PageEnd.jsp" %>

Na závěr náš error controller zaregistrujeme v kořenovém routeru naší aplikace pomocí metody registerErrorController, jak ukazuje následující ukázka.

package com.example.app.routes;

import com.example.app.middlewares.RequireAdminLogin;
import com.example.app.routes.admin.AdminLoginController;
import com.example.app.routes.admin.AdminRouter;
import com.example.app.routes.products.ProductsRouter;

import io.github.jirkasa.servletrouter.BaseURLAttributeSetter;
import io.github.jirkasa.servletrouter.HttpRouter;

public class AppRouter extends HttpRouter {
    public AppRouter() {
        register(new BaseURLAttributeSetter());
        register("/", HomeController.class);
        register("/produkty", new ProductsRouter());
        register("/admin/login", AdminLoginController.class);
        register("/admin", new RequireAdminLogin(), new AdminRouter());
        register(PageNotFoundController.class);
        registerErrorController(ServerErrorController.class);
    }
}

Pokud by v naší aplikaci někdy došlo k chybě, tak by se měl zaregistrovaný error controller zavolat, vypsat chybu do konzole a zobrazit chybovou stránku. Pokud si to chcete vyzkoušet, tak si můžete v nějakém controlleru zkusit vyhodit vyjímku.

Naše ukázková aplikace je hotová. Pokud si ještě chcete prohlédnout její kód, tak si můžete v následující ukázce otevřít levý panel a zobrazovat si jednotlivé soubory projektu.

Rozšiřující knihovny pro Servlet Router

Pro knihovnu Servlet Router se snadno vytváří různé rozšiřující knihovny. Příkladem může být třeba middleware pro ochranu proti CSRF útokům o kterém píšu zde.