com.astamuse.asta4d.web.builtin.StaticResourceHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.astamuse.asta4d.web.builtin.StaticResourceHandler.java

Source

/*
 * Copyright 2014 astamuse company,Ltd.
 * 
 * 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.astamuse.asta4d.web.builtin;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

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

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.astamuse.asta4d.Configuration;
import com.astamuse.asta4d.Context;
import com.astamuse.asta4d.util.MemorySafeResourceCache;
import com.astamuse.asta4d.util.MemorySafeResourceCache.ResouceHolder;
import com.astamuse.asta4d.util.MultiSearchPathResourceLoader;
import com.astamuse.asta4d.util.i18n.LocalizeUtil;
import com.astamuse.asta4d.web.WebApplicationContext;
import com.astamuse.asta4d.web.dispatch.mapping.UrlMappingRule;
import com.astamuse.asta4d.web.dispatch.request.RequestHandler;
import com.astamuse.asta4d.web.dispatch.response.provider.BinaryDataProvider;
import com.astamuse.asta4d.web.dispatch.response.provider.HeaderInfoProvider;
import com.astamuse.asta4d.web.util.data.BinaryDataUtil;

/**
 * A static resouce handler for service static resources such as js, css or static html files.
 * 
 * The following path vars can be configured for specializing response headers:
 * 
 * <ul>
 * <li>{@link #VAR_CONTENT_TYPE}
 * <li>{@link #VAR_CONTENT_CACHE_SIZE_LIMIT_K}
 * <li>{@link #VAR_CACHE_TIME}
 * <li>{@link #VAR_LAST_MODIFIED}
 * </ul>
 * 
 * Or the following methods can be override for more complex calculations:
 * 
 * <ul>
 * <li>{@link #judgContentType(String)}
 * <li>{@link #getContentCacheSizeLimit(String)}
 * <li>{@link #decideCacheTime(String)}
 * <li>{@link #getLastModifiedTime(String)}
 * </ul>
 * 
 * @author e-ryu
 * 
 */
public class StaticResourceHandler extends AbstractGenericPathHandler {

    /**
     * see {@link #judgContentType(String)}
     */
    public final static String VAR_CONTENT_TYPE = StaticResourceHandler.class.getName() + "#content_type";

    /**
     * see {@link #getContentCacheSizeLimit(String)}
     */
    public final static String VAR_CONTENT_CACHE_SIZE_LIMIT_K = StaticResourceHandler.class.getName()
            + "#content_cache_size_limit_k";

    /**
     * see {@link #decideCacheTime(String)}
     */
    public final static String VAR_CACHE_TIME = StaticResourceHandler.class.getName() + "#cache_time";

    /**
     * see {@link #getLastModifiedTime(String)}
     */
    public final static String VAR_LAST_MODIFIED = StaticResourceHandler.class.getName() + "#last_modified";

    private final static Logger logger = LoggerFactory.getLogger(StaticResourceHandler.class);

    private final static long DefaultLastModified = DateTime.now().getMillis();
    // one hour
    private final static long DefaultCacheTime = 1000 * 60 * 60;

    protected final static class StaticFileInfo {
        String contentType;
        String actualPath;
        long lastModified;
        int cacheLimit;
        SoftReference<byte[]> content;
        InputStream firstTimeInput;
    }

    private final static MemorySafeResourceCache<String, StaticFileInfo> StaticFileInfoMap = new MemorySafeResourceCache<>();

    public StaticResourceHandler() {
        super();
    }

    public StaticResourceHandler(String basePath) {
        super(basePath);
    }

    private HeaderInfoProvider createSimpleHeaderResponse(int status) {
        HeaderInfoProvider provider = new HeaderInfoProvider(status);
        provider.setContinuable(false);
        return provider;
    }

    @RequestHandler
    public Object handler(HttpServletRequest request, HttpServletResponse response, ServletContext servletContext,
            UrlMappingRule currentRule) throws FileNotFoundException, IOException {
        String path = convertPath(currentRule);

        if (path == null) {
            return createSimpleHeaderResponse(404);
        }

        StaticFileInfo info = retrieveStaticFileInfo(servletContext, path);

        return response(request, response, servletContext, info, path);
    }

    protected StaticFileInfo retrieveStaticFileInfo(ServletContext servletContext, String path)
            throws FileNotFoundException, IOException {

        Locale locale = LocalizeUtil.defaultWhenNull(null);
        String staticFileInfoKey = LocalizeUtil.createLocalizedKey(path, locale);

        ResouceHolder<StaticFileInfo> cachedResource = Configuration.getConfiguration().isCacheEnable()
                ? StaticFileInfoMap.get(staticFileInfoKey)
                : null;

        StaticFileInfo info = null;

        if (cachedResource == null) {
            info = createInfo(servletContext, locale, path);
            StaticFileInfoMap.put(staticFileInfoKey, info);
        } else {
            if (cachedResource.exists()) {
                info = cachedResource.get();
            } else {
                info = null;
            }
        }
        return info;
    }

    protected Object response(HttpServletRequest request, HttpServletResponse response,
            ServletContext servletContext, StaticFileInfo info, String requiredPath)
            throws FileNotFoundException, IOException {

        if (info == null) {
            return createSimpleHeaderResponse(404);
        }

        if (Configuration.getConfiguration().isCacheEnable()) {
            long clientTime = retrieveClientCachedTime(request);
            if (clientTime >= info.lastModified) {
                return createSimpleHeaderResponse(304);
            }
        }

        // our header provider is not convenient for date header... hope we can
        // improve it in future
        long cacheTime = decideCacheTime(requiredPath, info.actualPath);
        response.setStatus(200);
        response.setHeader("Content-Type", info.contentType);
        response.setDateHeader("Expires", DateTime.now().getMillis() + cacheTime);
        response.setDateHeader("Last-Modified", info.lastModified);
        response.setHeader("Cache-control", "max-age=" + (cacheTime / 1000));

        // here we do not synchronize threads because we do not matter

        if (info.content == null && info.firstTimeInput != null) {
            // no cache, and we have opened it at first time
            InputStream input = info.firstTimeInput;
            info.firstTimeInput = null;
            return new BinaryDataProvider(input);
        } else if (info.content == null && info.firstTimeInput == null) {
            // no cache
            return new BinaryDataProvider(servletContext, this.getClass().getClassLoader(), info.actualPath);
        } else {
            // should cache
            byte[] data = null;
            data = info.content.get();
            if (data == null) {
                InputStream input = BinaryDataUtil.retrieveInputStreamByPath(servletContext,
                        this.getClass().getClassLoader(), info.actualPath);
                // if we went to here, which means we are not overing the cache size limit, so we do not need to check null.
                data = retrieveBytesFromInputStream(input, info.cacheLimit);
                info.content = new SoftReference<byte[]>(data);
            }
            return new BinaryDataProvider(data);
        }
    }

    private long retrieveClientCachedTime(HttpServletRequest request) {
        try {
            long time = request.getDateHeader("If-Modified-Since");
            return DateTimeZone.getDefault().convertLocalToUTC(time, false);
        } catch (Exception e) {
            logger.debug("retrieve If-Modified-Since failed", e);
            return -1;
        }
    }

    private StaticFileInfo createInfo(final ServletContext servletContext, Locale locale, String path)
            throws FileNotFoundException, IOException {

        MultiSearchPathResourceLoader<Pair<String, InputStream>> loader = new MultiSearchPathResourceLoader<Pair<String, InputStream>>() {
            @Override
            protected Pair<String, InputStream> loadResource(String name) {
                InputStream is = BinaryDataUtil.retrieveInputStreamByPath(servletContext,
                        this.getClass().getClassLoader(), name);
                if (is != null) {
                    return Pair.of(name, is);
                } else {
                    return null;
                }
            }
        };

        Pair<String, InputStream> foundResource = loader.searchResource("/",
                LocalizeUtil.getCandidatePaths(path, locale));

        if (foundResource == null) {
            return null;
        }

        StaticFileInfo info = new StaticFileInfo();
        info.contentType = judgContentType(path);
        info.actualPath = foundResource.getLeft();
        info.lastModified = getLastModifiedTime(path);

        // cut the milliseconds
        info.lastModified = info.lastModified / 1000 * 1000;

        info.cacheLimit = getContentCacheSizeLimit(path);

        if (info.cacheLimit == 0) {// don't cache
            info.content = null;
            // we will use the retrieved input stream at the first time for performance reason
            info.firstTimeInput = foundResource.getRight();
        } else {
            byte[] contentData = retrieveBytesFromInputStream(foundResource.getRight(), info.cacheLimit);
            if (contentData == null) {// we cannot cache it due to over limited size
                // fallback to no cache case
                info.content = null;
                info.firstTimeInput = null;
            } else {
                try {
                    info.content = new SoftReference<byte[]>(contentData);
                } finally {
                    foundResource.getRight().close();
                }
                info.firstTimeInput = null;
            }
        }
        return info;
    }

    private byte[] retrieveBytesFromInputStream(InputStream input, int cacheSize) throws IOException {
        byte[] b = new byte[cacheSize];
        int len = input.read(b);
        if (input.read() >= 0) {// over the limit of cache size
            return null;
        } else {
            if (len < cacheSize) {
                byte[] nb = new byte[len];
                System.arraycopy(b, 0, nb, 0, len);
                return nb;
            } else {// going to buy lottery
                return b;
            }
        }
    }

    private final static Map<String, String> MimeTypeMap = new HashMap<>();

    static {
        MimeTypeMap.put("js", "application/javascript");
        MimeTypeMap.put("css", "text/css");
        MimeTypeMap.put("ico", "image/x-icon");
    }

    /**
     * The header value of Content-Type
     * 
     * @param path
     * @return a guess of the content type by file name extension, "application/octet-stream" when not matched
     */
    protected String judgContentType(String path) {

        Context context = Context.getCurrentThreadContext();

        String forceContentType = context.getData(WebApplicationContext.SCOPE_PATHVAR, VAR_CONTENT_TYPE);
        if (forceContentType != null) {
            return forceContentType;
        }

        String fileName = FilenameUtils.getName(path);

        // guess the type by file name extension
        String type = URLConnection.guessContentTypeFromName(fileName);

        if (type == null) {
            type = MimeTypeMap.get(FilenameUtils.getExtension(fileName));
        }

        if (type == null) {
            type = "application/octet-stream";
        }
        return type;
    }

    /**
     * The header value of Cache-control and Expires.
     * 
     * override this method to supply the specialized cache time.
     * 
     * @param path
     * @return cache time in millisecond unit
     */
    protected long decideCacheTime(String requiredPath, String actualTargetFilePath) {
        Long varCacheTime = Context.getCurrentThreadContext().getData(WebApplicationContext.SCOPE_PATHVAR,
                VAR_CACHE_TIME);
        if (varCacheTime != null) {
            return varCacheTime;
        } else {
            return DefaultCacheTime;
        }
    }

    /**
     * 
     * The header value of Last-Modified.
     * 
     * override this method to supply the specialized last modified time
     * 
     * @param path
     * @return the time of last modified time in millisecond unit(In http protocol, the time unit should be second, but we will cope with
     *         this matter)
     */
    protected long getLastModifiedTime(String path) {
        WebApplicationContext context = Context.getCurrentThreadContext();
        Long varLastModified = context.getData(WebApplicationContext.SCOPE_PATHVAR, VAR_LAST_MODIFIED);
        if (varLastModified != null) {
            return varLastModified;
        } else {
            long retrieveTime = BinaryDataUtil.retrieveLastModifiedByPath(context.getServletContext(),
                    this.getClass().getClassLoader(), path);
            if (retrieveTime == 0L) {
                return DefaultLastModified;
            } else {
                return retrieveTime;
            }
        }
    }

    /**
     * Retrieve the max cachable size limit for a certain path in byte unit.Be care of that the path var is set by kilobyte unit for
     * convenience but this method will return in byte unit. <br>
     * This is a default implementation which does not see the path and will return 0 for not caching when path var is not set.
     * <p>
     * Note: we do not cache it by default because the resources in war should have been cached by the servlet container.
     * 
     * 
     * @param path
     * @return the max cachable size limit for a certain path in byte unit.
     */
    protected int getContentCacheSizeLimit(String path) {
        Integer varCacheSize = Context.getCurrentThreadContext().getData(WebApplicationContext.SCOPE_PATHVAR,
                VAR_CONTENT_CACHE_SIZE_LIMIT_K);
        if (varCacheSize != null) {
            return varCacheSize * 1000;
        } else {
            return 0;
        }
    }

}