Servlet Router

Servlet router is simple and lightweight library for servlet routing. You can learn how to use it on this page. Czech version is available here.

Benefits of using servlet router library

Mapping URLs to servlets via the web.xml file is not the best way to perform routing in a web application. If we have a lot of servlets, it becomes quite challenging to navigate in such a file. We always have to first define the servlet there, which takes up 4 lines of code, and then we have to map it to some URL, which also takes at least 4 lines of code. And that's not even considering filters, which also need to be defined and mapped in the web.xml file. Specifying the path in the url-pattern element for mapping a servlet or filter is not very user-friendly, and it's not always easy to map the servlet to the URL we want to. Furthermore, I don't think routing for a web application should be configured somewhere. It should be hardcoded in the application code (unless we're talking about some CMS system or similar, where the user might create their own pages). These are the main reasons that led me to create a routing library and thus avoid these problems.

The advantages of using the Servlet Router library are summarized in the following list:

  • no clumsy URL mapping in the web.xml configuration file
  • just map one entry servlet for all requests, and routing depends on the application code itself (no need to configure the web.xml file intricately)
  • no use of filters
  • no confusing setting of URL patterns
  • support for path parameters
  • better code orientation (no need to struggle to figure out what is called for which URL)
  • easy implementation of the MVC architecture
  • built-in support for the HTTP PATCH method (the HttpServlet class does not support it by default)

Components

The Servlet Router library contains three fundamental components: Router, Controller, and Handler. We will now take a look at each of them gradually, and you will also learn about other components built upon them.

Router

The Router is a component responsible for mapping controllers and handlers to specific paths. Its base class is Router. This class is abstract. It can be extended and used for routing any type of ServletRequest and ServletResponse object. However, in 99.9 percent of cases, you will want to route HttpServletRequest and HttpServletResponse objects. In such cases, you can use HttpRouter.

You can create HttpRouter either directly and then map the corresponding controllers and handlers (which I do not recommend), or you can create a subclass and perform the mapping in the constructor. The following example demonstrates the first (not recommended) approach.

// create router
HttpRouter router = new HttpRouter();
// register controller
router.register("/", HomeController.class);
// register another router (handler)
router.register("/info", infoRouter);

The following example demonstrates a better way to create the router.

public class AppRouter extends HttpRouter {
    public AppRouter() {
        // register controller
        register("/", HomeController.class);
        // register another router (handler)
        register("/info", new InfoRouter());
    }
}

As you may have noticed in the previous examples, the method used for mapping (registering) controllers or handlers is called register. If you look into the JavaDoc documentation, you'll see that it's overloaded at least ten times. This is because it allows registering both controller and handler classes simultaneously. In Java, it's not straightforward to pass two different types as method parameters in a type-safe manner. So, I solved it by overloading the register method multiple times to make it more user-friendly and easier to use. This allows calling it with multiple handlers or controllers in succession, as shown in the following example. You'll find out why you might want to do this later on.

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

Another fundamental component is the controller. Its task is to handle requests to a specific (usually) path. Similar to the router, the basic class here is the Controller class. However, you'll most often work with the HTTP protocol and process objects of type HttpServletRequest and HttpServletResponse. Therefore, you'll extend the HttpController class for creating controllers. This class provides methods for handling requests sent using various methods (GET, POST, DELETE, etc.). The following example illustrates how a controller can look like.

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

The following table illustrates various methods of the HttpController class that we can override to handle different types of requests.

MethodDescription
handleIt processes requests and invokes corresponding methods based on the method they were sent with. This method can be overridden, for example, for a controller handling the 404 page to respond to requests sent with any method.
handleGetIt handles requests sent with the GET method.
handlePostIt handles requests sent with the POST method.
handlePutIt handles requests sent with the PUT method.
handlePatchIt handles requests sent with the PATCH method.
handleDeleteIt handles requests sent with the DELETE method.
handleHeadIt handles requests sent with the HEAD method.
handleOptionsIt handles requests sent with the OPTIONS method. This method should not typically be overridden.
handleTraceIt handles requests sent with the TRACE method. This method should not typically be overridden.

Except for the handleOptions and handleTrace methods, the aforementioned methods for handling requests sent with specific methods do not have a default implementation. By default, an error page with status code 405 (Method Not Allowed) is sent for them. If instead we want to skip unimplemented methods and continue routing the request to other handlers/controllers, we can set the property skipUnimplementedMethods to true, as shown in the following example.

public class ExampleController extends HttpController {
    public ExampleController() {
        // For unimplemented methods, an error page will not be sent;
        // instead, the request routing will continue to the next handlers/controllers.
        this.skipUnimplementedMethods = true;
    }

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

When a controller is found during the routing of a request to handle it, the request no longer traverses through additional handlers or another controller. If we want the routing of the request to continue further, we have the option to call the method continueHandlersChain, as shown in the following example. You may not yet understand precisely what I mean by routing the request, but once we start creating a sample project, you'll likely grasp it.

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();
        }
    }
}

One thing you typically wouldn't do with traditional servlets is directly retrieve a parameter from the URL path. However, with the Servlet Router library, it's straightforward. You just need to define a parameter in the path during its registration in the router by using the ":" character followed by a name. Then, you can retrieve the parameter in the controller using the getPathParam method. The following example demonstrates this more clearly.

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);
    }
}

A good practice is not to directly extend the HttpController class for creating controllers but to create your own base controller class. This way, you can define methods for operations commonly performed in controllers. The following example illustrates an example of such a basic controller.

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

The Router can register both controllers and handlers. You already know what a controller is, but we haven't discussed handlers yet. Handlers are a similar component to controllers, as they also serve to process requests. The difference is that with controllers, we register the controller class in the router, whereas with handlers, we register an instance of the class. Any class implementing the Handler interface can become a handler. This interface defines methods described in the following table.

MethodDescription
handle(Request request, Response response)It is called to process the request. It returns a boolean value indicating whether the routing of the request should continue (true) or stop (false).
matchesFullPath()The return value of this boolean-typed method determines whether the path must match the request path exactly or if it's sufficient for the beginning of the request path to match.
setPathParams(Map<String, String> pathParams)This method is called before invoking the handle method to set up a map containing parameters in the path (path parameters).

The following example illustrates an example of creating a handler.

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);
    }
}

Typically, you wouldn't directly implement the Handler interface for creating handlers. Instead, when creating handlers, you'll usually extend the HttpMiddleware class. You can find out more about what this entails a little further below.

The Handler interface is also implemented by the Router class, making the router essentially a handler itself. This allows us to nest routers within each other. An example is shown in the following demonstration.

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, like controllers, is used to process requests. However, it's typically employed just to perform some action on the request, and the routing of the request can continue. Middleware can be used, for example, to restrict access to selected controllers only for logged-in users, to set some attribute on the request, and so on. The base class is the Middleware class, but in the vast majority of cases, you'll want to work with the HTTP protocol, so you can inherit from the HttpMiddleware class when creating middleware.

The following example illustrates an example of middleware that checks whether the user is logged in. If so, it allows the request to proceed; otherwise, it redirects the user to the login page.

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

        // if the user is logged in, the request continues
        if (session.getAttribute("LOGGED_USER") != null) return true;

        // otherwise, the user is redirected to the login page
        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 is a handler and therefore implements the Handler interface. The setPathParams and matchesFullPath methods are already implemented for us. Storing parameters in the path is the same for every middleware, so it would be unnecessary to perform this in every middleware we create. Instead of retrieving parameters from the URL path, we can use the getPathParam method. The matchesFullPath method is already implemented because, for most middleware, we want only the beginning of the request path to match. If this doesn't suit our needs, we can override the matchesFullPath method.

The Servlet Router library provides a pre-built middleware that you'll likely want to register at the beginning of your application. It's called BaseURLAttributeSetter. Its purpose is to set an attribute containing the root URL of your application into the request. Therefore, you don't need to use relative paths when setting paths to various assets in JSP pages, which prevents those JSP pages from being used for URLs with different lengths. The following example shows how to register this middleware. The default name for the attribute is "BASE_URL", but you can change it by passing a name into the constructor.

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 {
        // forward request to JSP page
        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

The Router allows you to set a special controller designed to handle exceptions. You can register it using the registerErrorController method. If an error controller is set for the router, it will be called when an exception occurs. Otherwise, the router will throw the exception instead.

The base class for creating an error controller is the ErrorController class. However, when working with the HTTP protocol, we can utilize the HttpErrorController class to create an error controller. The following example illustrates how such an error controller might look. To retrieve the error in the handle method, we can use the getException method.

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

    @Override
    public boolean handle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // log error message to console
        System.out.println(getException().getMessage());

        // set 500 HTTP status
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        
        // display error page
        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);
    }
}

Integration of the library into the application

When using the Servlet Router library, you only need to create a single servlet, which you map for all incoming requests in the web.xml file. In this servlet, you typically call the handle method of an entry router of your web application when a request arrives for processing. The following example demonstrates how such a servlet might look.

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.");
        }
    }
}

How to map a servlet to all incoming requests in the web.xml file is shown in the following example. Additionally, it's important to determine the name of the folder containing static assets such as CSS files, images, etc., located in the webapp directory, and map its path to the default servlet. When all requests are directed to the servlet, access to content in the webapp directory (except JSP files - unfortunately...) is blocked. This limitation frustrates me about JSP. Even though access to the webapp folder is blocked in this way, users can still access JSP files. Therefore, JSP files must be placed in the WEB-INF folder, where direct access is not allowed. The default servlet is used to access static resources, and most web servers have it. For Tomcat, this servlet is named "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>

Example of creating a project

You might want to see how to use the Servlet Router library in a project. Let's dive into creating a sample application. It will be a simple website with a main page, a page displaying a list of products, and a product detail page. The administrator will have the ability to add products and will be able to log in. The following diagram shows what the structure of the website will look like.

Structure of web application

We'll start by creating a new Maven project, which we can name "servlet-router-example-app." In the pom.xml file, we'll add the Servlet Router library to the project as a dependency, along with servlets, as shown in the following example.

  • 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>

The first thing we can do is to create the root router of our application. We can name it something like "AppRouter." For now, it will be empty, but at the beginning, we'll register the BaseURLAttributeSetter for setting the attribute containing the root URL of our application. We can create a Java package for it, perhaps named com.example.app.routes, and create it there.

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());
    }
}

Now we can create the entry servlet for our application (perhaps in the package com.example.app). When a request arrives for processing, we'll pass it to our root router using the handle method. We need to wrap the call to this method in a try-catch block. This ensures that if any error occurs in the router, we catch it. Later, we'll also create a custom error controller, so there may not be any exceptions thrown to this try-catch block. However, for safety, we'll return an error page using the sendError method, in case an error does reach this point.

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, "Something went wrong.");
        }
    }
}

We'll map the created servlet for all incoming requests. In the webapp directory, we'll create a subdirectory named WEB-INF, and within it, a file named web.xml, where we'll map our servlet. The following example shows its content.

  • 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>

We have the basic setup of the project ready. Now let's start creating the homepage. But before we do that, let's create our own basic controller class instead of directly inheriting from the HttpController class. This is a good practice because in this base class, we can define methods for code that we may frequently use in multiple controllers. Let's create an abstract base class for controllers, which we can name something like ExampleAppController. It will inherit from the HttpController class, and in our example, it will be empty. However, it doesn't matter, as the main point is that we'll have the ability to extend the base class for controllers at any time without modifying existing controllers. We can create it in the root package, perhaps com.example.app.

package com.example.app;

import io.github.jirkasa.servletrouter.HttpController;

public abstract class ExampleAppController extends HttpController {}

After creating the base class, we can start creating the controller for the homepage. We'll create it in the package com.example.app.routes and name it something like HomeController. This controller will be very simple, as it will just pass the request for processing to a JSP file for rendering the HTML page.

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);
    }
}

Now let's create the JSP page. In the WEB-INF directory (so that users cannot directly access JSP pages), we'll create a folder named jsp, and within it, a file named "HomePage.jsp". In the following example, you can view its code. The main page will simply display a welcome message and show a link to the product list page. You may notice that in the link, we're using the BASE_URL attribute, which is set by the BaseURLAttributeSetter middleware. This attribute contains the root URL of our application.

  • 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>Welcome</h1>
    <p>This website represents a sample application for the Servlet Router library. By clicking on the link below, you can view the list of products.</p>
    <a href="${BASE_URL}/products" class="button">List of products</a>
<%@ include file="./includes/PageEnd.jsp" %>

In the previous code for the homepage, we used an include directive to include content from other files. However, we haven't created those files yet, so let's do that now. We're doing this because the beginning and end of the page will be common to all pages in our application.

The first file included at the beginning will contain the code for the start of the page. Let's create it in a folder named includes, which we'll create, and name it "PageStart.jsp". Its content is shown in the following example.

  • 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>Example app</title>
</head>
<body>
    <div class="page">

The file included at the end will contain the code for the end of the page. In the includes folder, let's create a file named "PageEnd.jsp". You can see its code in the example below.

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

The last thing we need to do for the homepage is register the controller in the router, as shown in the following example.

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);
    }
}

Now you can run the application, and after visiting http://localhost:8080/servlet-router-example-app/, you should see the page as shown in the following image.

Main page of application

Now let's style the page using CSS styles. This leads us to creating a folder in the webapp directory where we'll put things like images, JavaScript files, CSS styles, and so on. We need to map this folder to the default servlet; otherwise, all requests for these files would go to our servlet named "AppServlet". Let's create this folder and name it something like "static". Then we can create a file for CSS styles in it. The code for it is shown in the following example, so you can copy it.

  • 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;
}

In the PageStart.jsp file, which contains the code for the start of the page, we'll include the CSS style file. We'll use the BASE_URL attribute to make it work for any page where the PageStart.jsp file is included.

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

In the web.xml file, we'll map our newly created static folder to the default servlet, as shown in the following example.

  • 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>

After restarting the server and refreshing the page, you should see that the page has been styled.

Main page of application

We have the main page of our application. Now let's create pages for the admin. These will be handled by a separate router. Let's create it and place it in a new package, perhaps named com.example.app.routes.admin. It's up to you how you want to organize your classes into packages. In this sample application, I've decided to create separate packages for each router and its controllers.

package com.example.app.routes.admin;

import io.github.jirkasa.servletrouter.HttpRouter;

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

As the first step, let's create the home page for the admin. It will contain only a heading "Administration" and a link to the page for adding a new product. We'll need a controller and a JSP page for this. The following examples show them. The file name for the page is "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>Administration</h1>
    <a href="${BASE_URL}/admin/add-product" class="button">Add product</a>
<%@ include file="./includes/PageEnd.jsp" %>

In the admin router, we map the controller as shown in the following example.

package com.example.app.routes.admin;

import io.github.jirkasa.servletrouter.HttpRouter;

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

We also need to map our admin router in our root router. The following example shows the modified code.

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());
    }
}

If you now visit http://localhost:8080/servlet-router-example-app/admin/, you should see the page depicted in the following image.

Main page of administration

In our application, we don't want just anyone to be able to visit the administration pages. Therefore, let's secure them now. We'll create middleware that will check if the user is logged in as an admin. If they are, it will allow them to proceed; otherwise, it will display the login page.

We'll create a new Java package for middleware named something like com.example.app.middlewares. Inside this package, we'll create a class named RequireAdminLogin. The following example shows its code. If the middleware detects that the user is logged in (has the attribute ADMIN_LOGGED_IN set in the session), it will allow the request to proceed. Otherwise, it will display the login page.

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;
    }
}

The JSP code for the login page is as follows. It contains a form where the user can input their credentials and submit them.

  • src/main/webapp/WEB-INF/jsp
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ include file="./includes/PageStart.jsp" %>
    <h1>Administration - login</h1>
    <form action="${BASE_URL}/admin/login" method="POST">
        <input name="username" type="text" required>
        <input name="password" type="password" required>
        <button type="submit">Login</button>
    </form>
<%@ include file="./includes/PageEnd.jsp" %>

In the root router, we need to register the middleware before the admin router, as shown in the following example.

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());
    }
}

Now, after refreshing the admin home page, you should see the login page instead of the main admin page.

Administration login page

The login form on the login page is submitted via the POST method to /admin/login. Therefore, we will create a controller to handle this form. Let's name it, for example, AdminLoginController. Since we want to process the POST request, we will implement the handlePost method. In this method, we will check if the entered credentials are correct, and if so, we will log in the user (by setting a session attribute). Otherwise, we will simply redirect them back to the main admin page where the login form will be displayed again (we are not handling any validation messages, etc., in our example). The following code snippet shows the controller's code. The correct login credentials are directly specified in the code. In a real application, this would not be secure, but this is just a sample project.

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");
    }
}

Since we want any user to access the administration login controller, we cannot register it in the admin router. This router is protected by our middleware, which checks if the user is logged in as an administrator. The request would not reach the controller. Therefore, we will register the login controller directly in our root router, as shown in the following example. Routers and middlewares are invoked even if only the beginning of the request path matches, so we must register the controller before registering the admin router. Otherwise, the request would not reach the controller.

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());
    }
}

If you now try to correctly fill in and submit the login form (the username is "admin" and the password is "password"), you should successfully log in and see the main administration page.

Now, we'll create a page through which the administrator can add a new product. We'll need a controller and a JSP page with a form. The code for them is shown in the following example. The controller currently only implements the handleGet method and passes the request for processing to the JSP page named "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>Administration - add product</h1>
    <form action="${BASE_URL}/admin/add-product" method="POST">
        <label>Name:</label>
        <input name="name" type="text" required><br>
        <label>Price:</label>
        <input name="price" type="number" required><br>
        <button type="submit">Add</button>
    </form>
<%@ include file="./includes/PageEnd.jsp" %>

In the admin router, we'll register the newly created controller.

package com.example.app.routes.admin;

import io.github.jirkasa.servletrouter.HttpRouter;

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

If you now click on "add product" on the main administration page, you'll see the page with the form as shown in the following image.

Page to add product

The form on the page directs to the same controller that displays the page and is submitted using the POST method. In the controller, we'll implement the handlePost method to process the form and create a new product. This leads us to the point where we need to store the created products somehow. For our purposes, it will suffice to store them only in memory. The product in our application will be represented by the following class, which we can create in a new package, perhaps named "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;
    }
}

For adding and retrieving products, we'll use the following class, which you can also create in the "com.example.app.model" package. You don't need to worry about how it works. It contains three static methods. Using the addProduct method, we can add a new product, the getProducts method allows us to retrieve a list of all stored products, and the getProductById method enables us to retrieve a product by its 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;
    }
}

Now, in the controller for adding a product, let's implement the handlePost method. We'll use the class we just created to store the product created based on the form values. The following code snippet shows the handlePost method. We're not performing any validation, so if the user leaves any value blank, it's fine. After saving the product, we'll simply redirect the user to the main administration page for simplicity.

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");
    }
}

You can try adding a product now, but you won't see it anywhere yet. After submitting the form, you should simply be redirected to the main administration page.

We're done with the administration. Now, all that's left is to create a page displaying a list of products and a page showing the details of a product. For these pages, we'll create a router, which we'll place in a new package called "com.example.app.routes.products".

package com.example.app.routes.products;

import io.github.jirkasa.servletrouter.HttpRouter;

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

We'll start with the page displaying a list of products. The following examples show the controller and JSP page for it. In the controller, we'll retrieve the list of products, set them as an attribute in the request, and the JSP page will display them.

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>Products</h1>
    <table>
        <thead>
            <tr>
                <th>Name</th>
                <th>Price</th>
            </tr>
        </thead>
        <tbody>
            <% for (Product product : (List<Product>) request.getAttribute("products")) { %>
            <tr>
                <td><a href="${BASE_URL}/products/<%= product.getId() %>"><%= product.getName() %></a></td>
                <td><%= product.getPrice() %></td>
            </tr>
            <% } %>
        </tbody>
    </table>
<%@ include file="./includes/PageEnd.jsp" %>

In the router, we'll register the newly created controller as shown in the following example.

package com.example.app.routes.products;

import io.github.jirkasa.servletrouter.HttpRouter;

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

We must also remember to register our router in the root router of our application.

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("/products", new ProductsRouter());
        register("/admin/login", AdminLoginController.class);
        register("/admin", new RequireAdminLogin(), new AdminRouter());
    }
}

If you now click on the "list of products" button on the main page, you will see the page as shown in the following image. But first, of course, you will need to add some products in the administration section.

Page with list of products

We have the option to click on each item in the table and thus access the detail page. Each item has a link pointing to /products/:id. So, we'll create a controller that will display the detail page. The following example shows it, and below you can also see the code of the JSP page.

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>Product detail</h1>
    <p>Name: ${product.name}</p>
    <p>Price: ${product.price}</p>
    <p><a href="${BASE_URL}/products">back to list</a></p>
<%@ include file="./includes/PageEnd.jsp" %>

We'll register the created controller in the router.

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);
    }
}

If you click on any product now, the product detail page will open up.

Page with detail of product

Our application is essentially finished. However, we could still create an error controller to display an error page if an error occurs in any of our controllers. Additionally, we could add a 404 page in case no controller is found for a URL.

We'll start with the 404 page. In the "com.example.app.routes" package, let's create a controller named, for example, PageNotFoundController. This controller will be a bit different from the others because we want it to handle requests regardless of the method used. We'll override the handle method in it, where we'll set the HTTP status code to 404 (Page Not Found) and render a JSP page indicating that the page was not found. The following examples show the code for the controller and the JSP page it displays.

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>Page not found</h1>
    <p>The page unfortunately does not exist. Continue to <a href="${BASE_URL}">home page</a>.</p>
<%@ include file="./includes/PageEnd.jsp" %>

We'll register the controller as the last one in our root router for all requests. So, if no other controller is found to handle the request during routing, this one will be used.

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("/products", new ProductsRouter());
        register("/admin/login", AdminLoginController.class);
        register("/admin", new RequireAdminLogin(), new AdminRouter());
        register(PageNotFoundController.class);
    }
}

If you now visit a URL that is not mapped to any controller, you should see the page displayed in the following image.

Page not found

We will now create an error controller, which will display an error page when an error occurs, and with that, we will consider our application complete. We will create it in the package "com.example.app.routes" and name it something like "ServerErrorController". In the handle method, we will simply print the error message to the console, set the HTTP status code to 500, and render a page announcing that an error has occurred. The code for the controller and the JSP page is shown in the following examples.

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>An error occurred</h1>
    <p>Unfortunately, an error occurred.</p>
<%@ include file="./includes/PageEnd.jsp" %>

Finally, we will register our error controller in the root router of our application using the registerErrorController method, as shown in the following example.

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("/products", new ProductsRouter());
        register("/admin/login", AdminLoginController.class);
        register("/admin", new RequireAdminLogin(), new AdminRouter());
        register(PageNotFoundController.class);
        registerErrorController(ServerErrorController.class);
    }
}

If an error ever occurs in our application, the registered error controller should be called, printing the error to the console and displaying the error page. If you want to try it out, you can throw an exception in any controller.

Our sample application is complete. If you want to review its code, you can open the left panel in the following demonstration and view individual project files.

Extension libraries

It's easy to create various extension libraries for the Servlet Router library. An example could be middleware for protection against CSRF attacks.