com.mastfrog.acteur.ClasspathResourcePage.java Source code

Java tutorial

Introduction

Here is the source code for com.mastfrog.acteur.ClasspathResourcePage.java

Source

/* 
 * The MIT License
 *
 * Copyright 2013 Tim Boudreau.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.mastfrog.acteur;

import com.mastfrog.acteur.headers.Headers;
import com.google.common.net.MediaType;
import com.google.inject.Inject;
import com.mastfrog.url.Path;
import com.mastfrog.util.Streams;
import com.mastfrog.util.streams.HashingInputStream;
import com.mastfrog.acteur.ResponseHeaders.ContentLengthProvider;
import com.mastfrog.acteur.ResponseHeaders.ETagProvider;
import com.mastfrog.acteur.util.CacheControlTypes;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.util.CharsetUtil;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.codec.binary.Base64;
import org.joda.time.DateTime;
import org.joda.time.Duration;

/**
 * A page which loads resources relative to itself on the classpath. Handles
 * caching headers as follows: ETag is generated (SHA-1) on first read; last
 * modified = server start time.
 * <p/>
 * Use this to embed resources inside application JARs - this is not for
 * serving flat files on disk.
 *
 * @author Tim Boudreau
 */
public abstract class ClasspathResourcePage extends Page implements ContentLengthProvider, ETagProvider {

    private static Map<Class<?>, Map<Path, Integer>> sizes = new HashMap<>();
    private static Map<Class<?>, Boolean> overridesProcessContent = new HashMap<>();
    private static Map<Class<?>, Map<Path, String>> etags = new HashMap<>();
    private static final Map<Class<?>, Map<Path, byte[]>> contentForPathForType = new HashMap<>();
    private final Path path;

    protected ClasspathResourcePage(final HttpEvent event, ActeurFactory f, DateTime serverStartTime,
            String... patterns) {
        this(null, event, f, serverStartTime, patterns);
    }

    @Deprecated
    @SuppressWarnings("LeakingThisInConstructor")
    protected ClasspathResourcePage(final Application app, final HttpEvent event, ActeurFactory f,
            DateTime serverStartTime, String... patterns) {
        this.path = event.getPath();
        responseHeaders.setLastModified(serverStartTime);
        responseHeaders.addCacheControl(CacheControlTypes.Public);
        responseHeaders.addCacheControl(CacheControlTypes.must_revalidate);
        responseHeaders.addCacheControl(CacheControlTypes.max_age, Duration.standardDays(100));
        responseHeaders.setContentLengthProvider(this);
        responseHeaders.setETagProvider(this);
        getResponseHeaders().setMaxAge(Duration.standardDays(100));
        getResponseHeaders().addVaryHeader(Headers.CONTENT_ENCODING);

        add(f.matchPath(patterns));
        add(f.matchMethods(com.mastfrog.acteur.headers.Method.GET, com.mastfrog.acteur.headers.Method.HEAD));
        add(HasStreamAction.class);

        add(f.sendNotModifiedIfETagHeaderMatches());
        add(f.sendNotModifiedIfIfModifiedSinceHeaderMatches());
        if (event.getMethod() != com.mastfrog.acteur.headers.Method.HEAD) {
            add(WriteBodyActeur.class);
        } else {
            add(f.responseCode(HttpResponseStatus.OK));
        }
    }

    protected MediaType getContentType(Path path) {
        MediaType type = responseHeaders.getContentType();
        if (type != null) {
            return type;
        }
        String pth = path.toString();
        if (pth.endsWith("svg")) {
            return MediaType.SVG_UTF_8;
        } else if (pth.endsWith("css")) {
            return MediaType.CSS_UTF_8;
        } else if (pth.endsWith("html")) {
            return MediaType.HTML_UTF_8;
        } else if (pth.endsWith("json")) {
            return MediaType.JSON_UTF_8;
        } else if (pth.endsWith("js")) {
            return MediaType.JAVASCRIPT_UTF_8;
        } else if (pth.endsWith("gif")) {
            return MediaType.GIF;
        } else if (pth.endsWith("jpg")) {
            return MediaType.JPEG;
        } else if (pth.endsWith("png")) {
            return MediaType.PNG;
        }
        return null;
    }

    private static class WriteBodyActeur extends Acteur {

        @Inject
        @SuppressWarnings("ArrayIsStoredDirectly")
        WriteBodyActeur(HttpEvent event, Page page) throws IOException {
            byte[] content = ((ClasspathResourcePage) page).getContent(event.getPath());
            setState(new RespondWith(HttpResponseStatus.OK));
            setResponseWriter(new BodyWriter(content, event.isKeepAlive()));
        }
    }

    private static class HasStreamAction extends Acteur {

        @Inject
        HasStreamAction(Page page, HttpEvent event) {
            boolean hasContent;
            Map<Path, byte[]> m = contentForPathForType.get(page.getClass());
            hasContent = (m != null && m.containsKey(event.getPath())
                    || getStream(event.getPath(), page.getClass()) != null);
            if (hasContent) {
                String cachedEtag = getCachedEtag(page.getClass(), event.getPath());
                if (cachedEtag != null) {
                    page.getResponseHeaders().setEtag(cachedEtag);
                }
                Long cachedSize = getCachedSize(page.getClass(), event.getPath());
                if (cachedSize != null) {
                    add(Headers.CONTENT_LENGTH, cachedSize);
                }
                setState(new ConsumedState());
            } else {
                setState(new RespondWith(HttpResponseStatus.NOT_FOUND, "No such page " + event.getPath()));
            }
        }
    }

    private static Long getCachedSize(Class<?> pageClass, Path path) {
        Map<Path, Integer> sz = sizes.get(pageClass);
        if (sz != null) {
            Integer val = sz.get(path);
            if (val != null) {
                return val.longValue();
            }
        }
        return null;
    }

    private static String getCachedEtag(Class<?> pageClass, Path path) {
        Map<Path, String> tags = etags.get(pageClass);
        if (tags != null) {
            return tags.get(path);
        }
        return null;
    }

    InputStream getStream(Path path) {
        return getStream(path, getClass());
    }

    protected static InputStream getStream(Path path, Class<?> type) {
        try {
            String name = URLDecoder.decode(path.getLastElement().toString(), "UTF-8");
            InputStream in = type.getResourceAsStream(name);
            return in;
        } catch (UnsupportedEncodingException ex) {
            throw new AssertionError(ex); //won't happen
        }
    }

    static class BodyWriter extends ResponseWriter {

        private final byte[] bytes;
        private volatile int offset = 0;
        private int chunksize = 256;
        private final boolean keepAlive;

        @SuppressWarnings("ArrayIsStoredDirectly")
        BodyWriter(byte[] content, boolean keepAlive) {
            bytes = content;
            this.keepAlive = keepAlive;
        }

        @Override
        public Status write(Event<?> evt, Output out, int iteration) throws Exception {
            int old = offset;
            int remaining = Math.min(chunksize, bytes.length - offset);
            offset += remaining;
            ByteBuf buf = Unpooled.wrappedBuffer(bytes, old, remaining);
            out.write(buf);
            return offset < bytes.length ? Status.NOT_DONE : Status.DONE;
        }
    }

    @Override
    public Long getContentLength() {
        long result = -1;
        if (!isDynamicContent()) {
            Map<Path, Integer> m = sizes.get(getClass());
            if (m == null) {
                sizes.put(getClass(), m = new HashMap<>());
            }
            getETag();
            Integer res = m.get(path);
            if (res != null) {
                result = res;
            }
        }
        return result == -1 ? null : result;
    }

    private boolean shouldCache(Path path) {
        return !isDynamicContent();
    }

    protected byte[] getContent(Path path) throws IOException {
        boolean cache = shouldCache(path);
        Map<Path, byte[]> cacheMap = contentForPathForType.get(getClass());
        if (cache && cacheMap != null) {
            byte[] res = cacheMap.get(path);
            if (res != null) {
                return res;
            }
        } else if (cache) {
            cacheMap = new HashMap<>();
            contentForPathForType.put(getClass(), cacheMap);
        }

        InputStream in = getStream(path);
        if (in == null) {
            return null;
        }
        byte[] result;
        if (!isDynamicContent()) {
            Map<Path, String> m = etags.get(getClass());
            String etag = null;
            if (m == null) {
                m = new HashMap<>();
                etags.put(getClass(), m);
            } else {
                etag = m.get(path);
            }
            if (etag == null) {
                HashingInputStream hin = HashingInputStream.sha1(in);
                ByteArrayOutputStream o = new ByteArrayOutputStream();
                int byteCount = Streams.copy(hin, o);
                hin.close();
                m.put(path, getHashString(hin));
                Map<Path, Integer> sz = sizes.get(getClass());
                if (sz == null) {
                    sz = new HashMap<>();
                    sizes.put(getClass(), sz);
                }
                sz.put(path, byteCount);
                result = o.toByteArray();
            } else {
                try {
                    ByteArrayOutputStream o = new ByteArrayOutputStream();
                    Streams.copy(in, o);
                    result = o.toByteArray();
                } finally {
                    in.close();
                }
            }
        } else {
            try {
                ByteArrayOutputStream o = new ByteArrayOutputStream();
                Streams.copy(in, o);
                result = o.toByteArray();
            } finally {
                in.close();
            }
        }
        if (cache) {
            cacheMap.put(path, result);
        }
        return result;
    }

    protected byte[] processContent(byte[] content) {
        return content;
    }

    private static Method findMethod(Class<?> on, String name, Class<?>... params) throws SecurityException {
        Class<?> curr = on;
        //NOTE:  Does not check interfaces
        while (curr != Object.class) {
            try {
                return curr.getDeclaredMethod(name, params);
            } catch (NoSuchMethodException ex) {
                //                Logger.getLogger(ClasspathResourcePage.class.getName()).log(Level.SEVERE, null, ex);
            } finally {
                curr = curr.getSuperclass();
            }
        }
        return null;
    }

    protected boolean isDynamicContent() {
        Boolean dynContent = overridesProcessContent.get(getClass());
        if (dynContent == null) {
            try {
                Method m = findMethod(getClass(), "processContent", String.class);
                dynContent = m == null ? false : m.getDeclaringClass() == ClasspathResourcePage.class;
                overridesProcessContent.put(getClass(), dynContent);
            } catch (SecurityException ex) {
                Logger.getLogger(ClasspathResourcePage.class.getName()).log(Level.SEVERE, null, ex);
                dynContent = true;
            }
        }
        return dynContent;
    }

    @Override
    public String getETag() {
        if (isDynamicContent()) {
            return null;
        }
        Map<Path, String> m = etags.get(getClass());
        if (m == null) {
            m = new HashMap<>();
            etags.put(getClass(), m);
        }
        String etag = m.get(path);
        if (etag == null) {
            InputStream in = getStream(path);
            if (in == null) {
                return null;
            }
            HashingInputStream hin = HashingInputStream.sha1(in);
            try {
                int byteCount = Streams.copy(hin, Streams.nullOutputStream());
                etag = getHashString(hin);
                Map<Path, Integer> sz = sizes.get(getClass());
                if (sz == null) {
                    sz = new HashMap<>();
                    sizes.put(getClass(), sz);
                }
                sz.put(path, byteCount);
                m.put(path, etag);
            } catch (IOException ex) {
                Logger.getLogger(ClasspathResourcePage.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        return etag;
    }

    String getHashString(HashingInputStream hin) throws IOException {
        hin.close();
        byte[] bytes = hin.getDigest();
        byte[] base64 = Base64.encodeBase64(bytes);
        return new String(base64, CharsetUtil.US_ASCII);
    }
}