com.threewks.thundr.route.staticResource.StaticResourceRouteResolver.java Source code

Java tutorial

Introduction

Here is the source code for com.threewks.thundr.route.staticResource.StaticResourceRouteResolver.java

Source

/*
 * This file is a component of thundr, a software library from 3wks.
 * Read more: http://3wks.github.io/thundr/
 * Copyright (C) 2015 3wks, <thundr@3wks.com.au>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.threewks.thundr.route.staticResource;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;

import com.threewks.thundr.Experimental;
import com.threewks.thundr.exception.BaseException;
import com.threewks.thundr.http.Header;
import com.threewks.thundr.http.StatusCode;
import com.threewks.thundr.logger.Logger;
import com.threewks.thundr.request.Request;
import com.threewks.thundr.request.Response;
import com.threewks.thundr.route.RouteResolver;
import com.threewks.thundr.route.RouteResolverException;

import jodd.io.StreamUtil;
import jodd.util.Wildcard;

// TODO - Better caching control:
// TODO - Vary: Accept-Encoding
@Experimental
public class StaticResourceRouteResolver implements RouteResolver<StaticResource> {
    private final String protectedPath = "/?WEB-INF/.*";

    private boolean gzipEnabled = true;

    private Map<String, String> defaultMimeTypes = new HashMap<String, String>();
    {
        defaultMimeTypes.put(".html", "text/html");
        defaultMimeTypes.put(".htm", "text/html");
        defaultMimeTypes.put(".css", "text/css");
        defaultMimeTypes.put(".gif", "image/gif");
        defaultMimeTypes.put(".ico", "image/vnd.microsoft.icon");
        defaultMimeTypes.put(".jpeg", "image/jpeg");
        defaultMimeTypes.put(".jpg", "image/jpeg");
        defaultMimeTypes.put(".js", "text/javascript");
        defaultMimeTypes.put(".png", "image/png");
        defaultMimeTypes.put(".htc", "text/x-component");
    }

    private Set<String> compressedMimeTypes = new HashSet<String>();
    {
        compressedMimeTypes.add("text/*");
    }

    private int cacheDuration = 24 * 60 * 60;
    private ServletContext servletContext;

    public StaticResourceRouteResolver(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

    @Override
    public Object resolve(StaticResource action, Request req, Response resp) throws RouteResolverException {
        try {
            serve(action, req, resp);
            return null;
        } catch (Exception e) {
            Throwable original = e.getCause() == null ? e : e.getCause();
            throw new BaseException(original, "Failed to load resource %s: %s", req.getRequestPath(),
                    original.getMessage());
        }
    }

    protected void serve(StaticResource action, Request request, Response response)
            throws ServletException, IOException {
        String resource = request.getRequestPath();
        URL resourceUrl = servletContext.getResource(resource);
        boolean allowed = isAllowed(resource);
        if (resourceUrl == null || !allowed) {
            response.withStatusCode(StatusCode.NotFound);
            Logger.info("%s -> %s not resolved: %s", resource, action, allowed ? "Not found" : "Not Permitted");
            return;
        }

        URLConnection urlConnection = resourceUrl.openConnection();
        String mimeType = deriveMimeType(resource);
        long contentLength = urlConnection.getContentLength();
        long lastModified = urlConnection.getLastModified();
        String acceptEncoding = request.getHeader(Header.AcceptEncoding);
        long cacheTimeSeconds = deriveCacheDuration(resource, mimeType);

        // TODO - v3 - Validate objects written to headers are serialized correctly - ints and dates mostly.
        // @formatter:off
        response.withContentType(mimeType)
                //response.setDateHeader(Header.Expires, System.currentTimeMillis() + cacheTimeSeconds * 1000L); // HTTP 1.0
                .withHeader(Header.Expires, System.currentTimeMillis() + cacheTimeSeconds * 1000L) // HTTP 1.0
                .withHeader(Header.CacheControl, String.format("max-age=%d, public", cacheTimeSeconds)) // HTTP 1.1
                .withHeader(Header.LastModified, lastModified);
        // @formatter:on

        OutputStream os = null;
        InputStream is = urlConnection.getInputStream();

        if (shouldZip(acceptEncoding, mimeType)) {
            HttpServletResponse resp = response.getRawResponse(HttpServletResponse.class);
            GzipResponseWrapper wrapper = new GzipResponseWrapper(resp);
            os = wrapper.getOutputStream();
            StreamUtil.copy(is, os);
            wrapper.finishResponse();
        } else {
            response.withHeader(Header.ContentLength, Long.toString(contentLength));
            os = response.getOutputStream();
            StreamUtil.copy(is, os);
            os.close();
        }

        response.withStatusCode(StatusCode.OK);
        Logger.debug("%s -> %s resolved as %s(%d bytes)", resource, action, mimeType, contentLength);
    }

    private long deriveCacheDuration(String resource, String mimeType) {
        return cacheDuration;
    }

    String deriveMimeType(String resource) {
        String mimeType = servletContext.getMimeType(resource);
        if (mimeType == null) {
            String extension = resource.substring(resource.lastIndexOf('.'));
            mimeType = defaultMimeTypes.get(extension);
        }
        return mimeType;
    }

    boolean shouldZip(String acceptEncoding, String mimeType) {
        return gzipEnabled && StringUtils.indexOf(acceptEncoding, "gzip") > -1
                && matchesCompressedMimeTypes(mimeType);
    }

    boolean matchesCompressedMimeTypes(String mimeType) {
        if (mimeType != null) {
            for (String compressedMimeType : compressedMimeTypes) {
                if (Wildcard.match(mimeType, compressedMimeType)) {
                    return true;
                }
            }
        }
        return false;
    }

    boolean isAllowed(String resourcePath) {
        return !resourcePath.matches(protectedPath);
    }

}