56. Web filters and File uploads#

This is the last post in this course. It deals with two loose ends: uploading files and intercepting requests.

56.1. Web filters#

Suppose you want to intercept requests for a (group of) servlets, to log what is requested and where it came from. For instance, to do analyse the web traffic later on in order to pinpoint the most used resources.
Or more relevant: intercept requests to asses authentication status of the user. And reject a request if the user is not authorized to see a resource.

56.1.1. A basic example#

Here is our old friend, the PhraseServlet (only relevant code show):

package nl.bioinf.wis_on_thymeleaf.servlets;

@WebServlet(name = "PhraseServlet", urlPatterns = "/give.phrase")
public class PhraseServlet extends HttpServlet {
    private TemplateEngine templateEngine;

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String phraseType = request.getParameter("phrase_category");
        Locale locale = request.getLocale();
        WebContext ctx = new WebContext(request, response, request.getServletContext(), locale);
        String phrase = PhraseFactory.getPhrase(phraseType);
        ctx.setVariable("phrase_type", phraseType);
        ctx.setVariable("phrase_num", phrase);
        templateEngine.process("phrase_of_the_day", ctx, response.getWriter());
    }
}

This is the way to keep track of requests for /give.phrase; a @WebFilter annotation on a class implementing javax.servlet.Filter does the trick:

package nl.bioinf.wis_on_thymeleaf.webfilters;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@WebFilter(urlPatterns = "/give.phrase")
public class PhraseServletFilter implements Filter{

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (request instanceof HttpServletRequest) {
            System.out.println("Request for " + ((HttpServletRequest)request).getRequestURL().toString());
            System.out.println("Query: " + ((HttpServletRequest)request).getQueryString());
            System.out.println("Remote address: " + request.getRemoteAddr());
        }
        try {
            //pass on the request after you are done
            chain.doFilter(request, response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        /*not interesting here*/
    }

    @Override
    public void destroy() {
        /*not interesting here*/
    }
}

Whe the /give.phrase?phrase_category=bullshit resource is requested, the Tomcat logs show

Request for http://localhost:8080/give.phrase
Query: phrase_category=bullshit
Remote address: 0:0:0:0:0:0:0:1

56.1.2. Check for authentication#

Suppose you have an application with a few public pages and a few pages for which authentication is required. This is a typical use case for WebFilter.

package nl.bioinf.wis_on_thymeleaf.webfilters;


import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@WebFilter(urlPatterns = {"/user.dashboard", "/list.secrets"}) //use "/*" for catch-all
public class AuthenticationFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (request instanceof HttpServletRequest) {
            HttpServletRequest req = ((HttpServletRequest)request);
            System.out.println("[AuthenticationFilter] Intercepted URL: " + req.getRequestURL().toString());
            final HttpSession session = req.getSession();
            if (session.getAttribute("user") == null) {
                System.out.println("[AuthenticationFilter] no authenticated user: redirecting to /login");
                ((HttpServletResponse)response).sendRedirect("/login");
            } else {
                System.out.println("[AuthenticationFilter] authenticated status checked; user= " + session.getAttribute("user"));
                try {
                    chain.doFilter(request, response);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        /*not interesting here*/
    }

    @Override
    public void destroy() {
        /*not interesting here*/
    }
}

Here is the output after a few requests

[AuthenticationFilter] Intercepted URL: http://localhost:8080/list.secrets 
[AuthenticationFilter] no authenticated user: redirecting to /login

<logged in as Henk>

[AuthenticationFilter] Intercepted URL: http://localhost:8080/list.secrets 
[AuthenticationFilter] authenticated status checked; user= User{name='Henk', email='henk@example.com', password='null', role=USER}
[AuthenticationFilter] Intercepted URL: http://localhost:8080/user.dashboard 
[AuthenticationFilter] authenticated status checked; user= User{name='Henk', email='henk@example.com', password='null', role=USER}

56.2. File uploads#

File Upload is an essential feature! Many websites provide some form of upload functionality:

  • Photos & Video

  • Data

  • Configuration

Fortunately, uploading is boilerplate code!

The harder thing is: what will you do with the file?

  • Into database

  • Store locally

  • Process directly

56.2.1. Upload form#

Suppose we want to offer a scv data upload. Here is the html for uploading csv data. Key are enctype="multipart/form-data" in the form tag and the <input type="file" name="scv_data" accept="text/csv"> tag.

<form th:action="@{'data_upload'}" method="post" enctype="multipart/form-data">
    <input type="file" name="scv_data" accept="text/csv">
    <input type="submit" value="Upload" />
</form>

The servlet handling the file upload needs to be annotated as such:

@MultipartConfig(location="/tmp",
        fileSizeThreshold = 1024 * 1024,
        maxFileSize = 1024 * 1024 * 5,
        maxRequestSize = 1024 * 1024 * 5 * 5)
@WebServlet(name = "FileUploadServlet", urlPatterns = "/data_upload")
public class FileUploadServlet extends HttpServlet {
    //class body
}

The @MultipartConfig annotation has several parameters:

  • location: An absolute path to a directory on the file system. This location is used to store files temporarily while the parts are processed or when the size of the file exceeds the specified fileSizeThreshold setting. The default location is “”.

  • fileSizeThreshold: The file size in bytes after which the file will be temporarily stored on disk. The default size is 0 bytes.

  • maxFileSize: The maximum size allowed for uploaded files, in bytes. If the size is greater, the container will throw an exception (IllegalStateException). The default size is unlimited.

  • maxRequestSize: The maximum size allowed for a multipart/form-data request, in bytes. The web container will throw an exception if the overall size of all uploaded files exceeds this threshold. The default size is unlimited.

Instead of hardcoding the constraints, you can also put them in web.xml:

    <servlet-mapping>
        <servlet-name>FileUploadServlet</servlet-name>
        <url-pattern>/data_upload</url-pattern>
    </servlet-mapping>
    <servlet>
        <servlet-name>FileUploadServlet</servlet-name>
        <servlet-class>nl.bioinf.wis_on_thymeleaf.servlets.FileUploadServlet</servlet-class>
        <init-param>
            <param-name>upload_dir</param-name>
            <param-value>/path/to/file/upload</param-value>
        </init-param>
        <multipart-config>
            <location>/path/to/tmp/tmp</location>
            <!-- all in bytes -->
            <max-file-size>20848820</max-file-size>
            <max-request-size>418018841</max-request-size>
            <file-size-threshold>1048576</file-size-threshold>
        </multipart-config>
    </servlet>

Here is the servlet dealing with the uploads. The final upload location is specified using an init-param in the web.xml deployment descriptor of course.

package nl.bioinf.wis_on_thymeleaf.servlets;

import nl.bioinf.wis_on_thymeleaf.config.WebConfig;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;

import javax.servlet.ServletException;
import javax.servlet.http.Part;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;

//moved to web.xml
//@MultipartConfig(location="/tmp",
//        fileSizeThreshold = 1024 * 1024,
//        maxFileSize = 1024 * 1024 * 5,
//        maxRequestSize = 1024 * 1024 * 5 * 5)
@WebServlet(name = "FileUploadServlet", urlPatterns = "/data_upload")
public class FileUploadServlet extends HttpServlet {

    private TemplateEngine templateEngine;
    private String uploadDir;

    @Override
    public void init() throws ServletException {
        this.templateEngine = WebConfig.getTemplateEngine();
        this.uploadDir = getInitParameter("upload_dir");

        //or, use relative to this app:
        // gets absolute path of the web application
        //String applicationPath = getServletContext().getRealPath("");
        //String uploadFilePath = applicationPath + File.separator + uploadDir;
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //simply go back to upload form
        WebContext ctx = new WebContext(request, response, request.getServletContext(), request.getLocale());
        templateEngine.process("upload_form", ctx, response.getWriter());
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        File fileSaveDir = new File(this.uploadDir);
        if (! fileSaveDir.exists()) {
            throw new IllegalStateException("Upload dir does not exist: " + this.uploadDir);
        }

        //Do this only if you are sure there won't be any file name conflicts!
        //An existing one will simply be overwritten
//        String fileName;
//        for (Part part : request.getParts()) {
//            fileName = part.getSubmittedFileName();
//            part.write(this.uploadDir + File.separator + fileName);
//        }

        //the safe way, with a generated file name becomes something like this
        //my_app_upload14260971264207930189.csv
        File generatedFile = File.createTempFile("my_app_upload", ".csv");
        for (Part part : request.getParts()) {
            part.write(this.uploadDir + File.separator + generatedFile.getName());
        }

        //go back to the upload form
        WebContext ctx = new WebContext(request, response, request.getServletContext(), request.getLocale());
        ctx.setVariable("message", "upload successfull, wanna do another on?");
        templateEngine.process("upload_form", ctx, response.getWriter());
    }
}

If you want to be sure files won’t get overwritten, you should use random generated file names, as in the above code example.