Servlet for serving static content

177,151

Solution 1

I ended up rolling my own StaticServlet. It supports If-Modified-Since, gzip encoding and it should be able to serve static files from war-files as well. It is not very difficult code, but it is not entirely trivial either.

The code is available: StaticServlet.java. Feel free to comment.

Update: Khurram asks about the ServletUtils class which is referenced in StaticServlet. It is simply a class with auxiliary methods that I used for my project. The only method you need is coalesce (which is identical to the SQL function COALESCE). This is the code:

public static <T> T coalesce(T...ts) {
    for(T t: ts)
        if(t != null)
            return t;
    return null;
}

Solution 2

I came up with a slightly different solution. It's a bit hack-ish, but here is the mapping:

<servlet-mapping>   
    <servlet-name>default</servlet-name>
    <url-pattern>*.html</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.jpg</url-pattern>
</servlet-mapping>
<servlet-mapping>
 <servlet-name>default</servlet-name>
    <url-pattern>*.png</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.css</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.js</url-pattern>
</servlet-mapping>

<servlet-mapping>
    <servlet-name>myAppServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

This basically just maps all content files by extension to the default servlet, and everything else to "myAppServlet".

It works in both Jetty and Tomcat.

Solution 3

There is no need for completely custom implementation of the default servlet in this case, you can use this simple servlet to wrap request to the container's implementation:


package com.example;

import java.io.*;

import javax.servlet.*;
import javax.servlet.http.*;

public class DefaultWrapperServlet extends HttpServlet
{   
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {
        RequestDispatcher rd = getServletContext().getNamedDispatcher("default");

        HttpServletRequest wrapped = new HttpServletRequestWrapper(req) {
            public String getServletPath() { return ""; }
        };

        rd.forward(wrapped, resp);
    }
}

Solution 4

I've had good results with FileServlet, as it supports pretty much all of HTTP (etags, chunking, etc.).

Solution 5

Abstract template for a static resource servlet

Partly based on this blog from 2007, here's a modernized and highly reusable abstract template for a servlet which properly deals with caching, ETag, If-None-Match and If-Modified-Since (but no Gzip and Range support; just to keep it simple; Gzip could be done with a filter or via container configuration).

public abstract class StaticResourceServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;
    private static final long ONE_SECOND_IN_MILLIS = TimeUnit.SECONDS.toMillis(1);
    private static final String ETAG_HEADER = "W/\"%s-%s\"";
    private static final String CONTENT_DISPOSITION_HEADER = "inline;filename=\"%1$s\"; filename*=UTF-8''%1$s";

    public static final long DEFAULT_EXPIRE_TIME_IN_MILLIS = TimeUnit.DAYS.toMillis(30);
    public static final int DEFAULT_STREAM_BUFFER_SIZE = 102400;

    @Override
    protected void doHead(HttpServletRequest request, HttpServletResponse response) throws ServletException ,IOException {
        doRequest(request, response, true);
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doRequest(request, response, false);
    }

    private void doRequest(HttpServletRequest request, HttpServletResponse response, boolean head) throws IOException {
        response.reset();
        StaticResource resource;

        try {
            resource = getStaticResource(request);
        }
        catch (IllegalArgumentException e) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }

        if (resource == null) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        String fileName = URLEncoder.encode(resource.getFileName(), StandardCharsets.UTF_8.name());
        boolean notModified = setCacheHeaders(request, response, fileName, resource.getLastModified());

        if (notModified) {
            response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
            return;
        }

        setContentHeaders(response, fileName, resource.getContentLength());

        if (head) {
            return;
        }

        writeContent(response, resource);
    }

    /**
     * Returns the static resource associated with the given HTTP servlet request. This returns <code>null</code> when
     * the resource does actually not exist. The servlet will then return a HTTP 404 error.
     * @param request The involved HTTP servlet request.
     * @return The static resource associated with the given HTTP servlet request.
     * @throws IllegalArgumentException When the request is mangled in such way that it's not recognizable as a valid
     * static resource request. The servlet will then return a HTTP 400 error.
     */
    protected abstract StaticResource getStaticResource(HttpServletRequest request) throws IllegalArgumentException;

    private boolean setCacheHeaders(HttpServletRequest request, HttpServletResponse response, String fileName, long lastModified) {
        String eTag = String.format(ETAG_HEADER, fileName, lastModified);
        response.setHeader("ETag", eTag);
        response.setDateHeader("Last-Modified", lastModified);
        response.setDateHeader("Expires", System.currentTimeMillis() + DEFAULT_EXPIRE_TIME_IN_MILLIS);
        return notModified(request, eTag, lastModified);
    }

    private boolean notModified(HttpServletRequest request, String eTag, long lastModified) {
        String ifNoneMatch = request.getHeader("If-None-Match");

        if (ifNoneMatch != null) {
            String[] matches = ifNoneMatch.split("\\s*,\\s*");
            Arrays.sort(matches);
            return (Arrays.binarySearch(matches, eTag) > -1 || Arrays.binarySearch(matches, "*") > -1);
        }
        else {
            long ifModifiedSince = request.getDateHeader("If-Modified-Since");
            return (ifModifiedSince + ONE_SECOND_IN_MILLIS > lastModified); // That second is because the header is in seconds, not millis.
        }
    }

    private void setContentHeaders(HttpServletResponse response, String fileName, long contentLength) {
        response.setHeader("Content-Type", getServletContext().getMimeType(fileName));
        response.setHeader("Content-Disposition", String.format(CONTENT_DISPOSITION_HEADER, fileName));

        if (contentLength != -1) {
            response.setHeader("Content-Length", String.valueOf(contentLength));
        }
    }

    private void writeContent(HttpServletResponse response, StaticResource resource) throws IOException {
        try (
            ReadableByteChannel inputChannel = Channels.newChannel(resource.getInputStream());
            WritableByteChannel outputChannel = Channels.newChannel(response.getOutputStream());
        ) {
            ByteBuffer buffer = ByteBuffer.allocateDirect(DEFAULT_STREAM_BUFFER_SIZE);
            long size = 0;

            while (inputChannel.read(buffer) != -1) {
                buffer.flip();
                size += outputChannel.write(buffer);
                buffer.clear();
            }

            if (resource.getContentLength() == -1 && !response.isCommitted()) {
                response.setHeader("Content-Length", String.valueOf(size));
            }
        }
    }

}

Use it together with the below interface representing a static resource.

interface StaticResource {

    /**
     * Returns the file name of the resource. This must be unique across all static resources. If any, the file
     * extension will be used to determine the content type being set. If the container doesn't recognize the
     * extension, then you can always register it as <code>&lt;mime-type&gt;</code> in <code>web.xml</code>.
     * @return The file name of the resource.
     */
    public String getFileName();

    /**
     * Returns the last modified timestamp of the resource in milliseconds.
     * @return The last modified timestamp of the resource in milliseconds.
     */
    public long getLastModified();

    /**
     * Returns the content length of the resource. This returns <code>-1</code> if the content length is unknown.
     * In that case, the container will automatically switch to chunked encoding if the response is already
     * committed after streaming. The file download progress may be unknown.
     * @return The content length of the resource.
     */
    public long getContentLength();

    /**
     * Returns the input stream with the content of the resource. This method will be called only once by the
     * servlet, and only when the resource actually needs to be streamed, so lazy loading is not necessary.
     * @return The input stream with the content of the resource.
     * @throws IOException When something fails at I/O level.
     */
    public InputStream getInputStream() throws IOException;

}

All you need is just extending from the given abstract servlet and implementing the getStaticResource() method according the javadoc.

Concrete example serving from file system:

Here's a concrete example which serves it via an URL like /files/foo.ext from the local disk file system:

@WebServlet("/files/*")
public class FileSystemResourceServlet extends StaticResourceServlet {

    private File folder;

    @Override
    public void init() throws ServletException {
        folder = new File("/path/to/the/folder");
    }

    @Override
    protected StaticResource getStaticResource(HttpServletRequest request) throws IllegalArgumentException {
        String pathInfo = request.getPathInfo();

        if (pathInfo == null || pathInfo.isEmpty() || "/".equals(pathInfo)) {
            throw new IllegalArgumentException();
        }

        String name = URLDecoder.decode(pathInfo.substring(1), StandardCharsets.UTF_8.name());
        final File file = new File(folder, Paths.get(name).getFileName().toString());

        return !file.exists() ? null : new StaticResource() {
            @Override
            public long getLastModified() {
                return file.lastModified();
            }
            @Override
            public InputStream getInputStream() throws IOException {
                return new FileInputStream(file);
            }
            @Override
            public String getFileName() {
                return file.getName();
            }
            @Override
            public long getContentLength() {
                return file.length();
            }
        };
    }

}

Concrete example serving from database:

Here's a concrete example which serves it via an URL like /files/foo.ext from the database via an EJB service call which returns your entity having a byte[] content property:

@WebServlet("/files/*")
public class YourEntityResourceServlet extends StaticResourceServlet {

    @EJB
    private YourEntityService yourEntityService;

    @Override
    protected StaticResource getStaticResource(HttpServletRequest request) throws IllegalArgumentException {
        String pathInfo = request.getPathInfo();

        if (pathInfo == null || pathInfo.isEmpty() || "/".equals(pathInfo)) {
            throw new IllegalArgumentException();
        }

        String name = URLDecoder.decode(pathInfo.substring(1), StandardCharsets.UTF_8.name());
        final YourEntity yourEntity = yourEntityService.getByName(name);

        return (yourEntity == null) ? null : new StaticResource() {
            @Override
            public long getLastModified() {
                return yourEntity.getLastModified();
            }
            @Override
            public InputStream getInputStream() throws IOException {
                return new ByteArrayInputStream(yourEntityService.getContentById(yourEntity.getId()));
            }
            @Override
            public String getFileName() {
                return yourEntity.getName();
            }
            @Override
            public long getContentLength() {
                return yourEntity.getContentLength();
            }
        };
    }

}
Share:
177,151

Related videos on Youtube

Bruno De Fraine
Author by

Bruno De Fraine

Updated on July 06, 2020

Comments

  • Bruno De Fraine
    Bruno De Fraine almost 4 years

    I deploy a webapp on two different containers (Tomcat and Jetty), but their default servlets for serving the static content have a different way of handling the URL structure I want to use (details).

    I am therefore looking to include a small servlet in the webapp to serve its own static content (images, CSS, etc.). The servlet should have the following properties:

    • No external dependencies
    • Simple and reliable
    • Support for If-Modified-Since header (i.e. custom getLastModified method)
    • (Optional) support for gzip encoding, etags,...

    Is such a servlet available somewhere? The closest I can find is example 4-10 from the servlet book.

    Update: The URL structure I want to use - in case you are wondering - is simply:

        <servlet-mapping>
                <servlet-name>main</servlet-name>
                <url-pattern>/*</url-pattern>
        </servlet-mapping>
        <servlet-mapping>
                <servlet-name>default</servlet-name>
                <url-pattern>/static/*</url-pattern>
        </servlet-mapping>
    

    So all requests should be passed to the main servlet, unless they are for the static path. The problem is that Tomcat's default servlet does not take the ServletPath into account (so it looks for the static files in the main folder), while Jetty does (so it looks in the static folder).

    • Stu Thompson
      Stu Thompson over 15 years
      Could you elaborate on the "URL structure" you want to use? Rolling your own, based on the linked example 4-10, seems like a trivial effort. I've done it myself plenty of times...
    • Stephen
      Stephen over 15 years
      Why don't you use the webserver for static content?
    • Bruno De Fraine
      Bruno De Fraine over 15 years
      @Stephen: because there is not always an Apache in front of the Tomcat/Jetty. And to avoid the hassle of a separate configuration. But you are right, I could consider that option.
    • Maciek Kreft
      Maciek Kreft over 12 years
      I just can't understand, why you didn't use mapping like this <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> to serve static content
  • Bruno De Fraine
    Bruno De Fraine over 15 years
    It seems to depend on a lot of stuff from org.apache.*. How can you use it with Jetty?
  • Panagiotis Korros
    Panagiotis Korros over 15 years
    You are right, this version has too many depedencies to the Tomcat (caand it also supports many things you might not want. I will edit my answer.
  • Leonel
    Leonel over 15 years
    Don't name your inner class Error. That might cause confusion as you can mistake it for java.lang.Error Also, is your web.xml the same ?
  • Bruno De Fraine
    Bruno De Fraine over 15 years
    Thanks for the Error warning. web.xml is the same, with "default" replaced by the name of the StaticServlet.
  • Bruno De Fraine
    Bruno De Fraine about 13 years
    That's also my understanding from svn diff -c1056763 http://svn.apache.org/repos/asf/tomcat/tc6.0.x/trunk/. At long last, after marking this WONTFIX +3 years ago!
  • Mike Minicki
    Mike Minicki almost 13 years
    As for the coalesce method, it can be replaced (inside the Servlet class) by commons-lang StringUtils.defaultString(String, String)
  • Fareed Alnamrouti
    Fareed Alnamrouti over 12 years
    actually you can add more than one url-pattern tag inside the servelet-mapping ;)
  • vivid_voidgroup
    vivid_voidgroup almost 12 years
    Servlet 2.5 and newer support multiple url-pattern tags inside servlet-mapping
  • Yossi Shasho
    Yossi Shasho over 11 years
    Thanks! hours of failed attempts and bad answers, and this solved my problem
  • Yossi Shasho
    Yossi Shasho over 11 years
    Though in order to serve content from a folder outside the app (I use it to server a folder from the disk, say C:\resources) I modified the this row: this.basePath = getServletContext().getRealPath(getInitParameter("basePath")‌​); And replaced it with: this.basePath = getInitParameter("basePath");
  • David Carboni
    David Carboni over 11 years
    This question has a neat way of mapping / to a controller and /static to static content using a filter. Check the upvoted answer after the accepted one: stackoverflow.com/questions/870150/…
  • Gedrox
    Gedrox over 11 years
    Seems this is not a valid config.
  • Gerrit Brink
    Gerrit Brink over 9 years
    The transferStreams() method can also be replaced with Files.copy(is ,os);
  • Andres
    Andres over 8 years
    Just be careful with index files (index.html) since they may take precedence over your servlet.
  • febot
    febot about 8 years
  • koppor
    koppor over 7 years
    An updated version is available at showcase.omnifaces.org/servlets/FileServlet
  • Platon Efimov
    Platon Efimov almost 7 years
    I think it's bad idea use *.sth. If somebody will get url example.com/index.jsp?g=.sth he will get the source of jsp file. Or I'm wrong? (I'm new in Java EE) I usually use url pattern /css/* and etc.
  • Cristian Arteaga
    Cristian Arteaga almost 6 years
    Dear @BalusC I think your approach is is vulnerable to a hacker who sending the following request could navigate trough the file system: files/%2e%2e/mysecretfile.txt. This request produces files/../mysecretfile.txt. I tested it on Tomcat 7.0.55. They call it a directory climbing: owasp.org/index.php/Path_Traversal
  • BalusC
    BalusC almost 6 years
    @Cristian: Yup, possible. I updated the example to show how to prevent that.
  • Leonhard Printz
    Leonhard Printz about 4 years
    This should not get upvotes. Serving static files for a webpage with Servlet like this is a recipe for disaster security wise. All such problems have already been solved, and there's no reason to implement a new Custom way with likely more undiscovered security time bombs to go off. The correct path is to configure Tomcat/GlassFish/Jetty etc to serve the content, or even better to use a dedicated fileserver like NGinX.
  • Leonhard Printz
    Leonhard Printz about 4 years
    Why is this approach so popular? Why are people reimplementing static file servers like this? There's so many security holes just waiting to be discovered, and so many features of real static file servers that aren't implemented.
  • BalusC
    BalusC about 4 years
    @LeonhardPrintz: I'll delete the answer and report back to my friends at Tomcat once you point out security issues. No problem.
  • scharette
    scharette about 2 years
    this looked promising but didn't work for me. In tomcat9 getRelativePath() is not invoked when I try to access a resource.