grails.plugin.cache.web.PageInfo.java Source code

Java tutorial

Introduction

Here is the source code for grails.plugin.cache.web.PageInfo.java

Source

/* Copyright 2012-2013 SpringSource.
 *
 * 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 grails.plugin.cache.web;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

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

import org.codehaus.groovy.grails.plugins.web.api.ControllersApi;
import org.codehaus.groovy.grails.web.servlet.GrailsFlashScope;
import org.codehaus.groovy.grails.web.servlet.HttpHeaders;
import org.codehaus.groovy.grails.commons.*;
import org.slf4j.LoggerFactory;
import org.springframework.aop.PointcutAdvisor;
import org.springframework.aop.TargetSource;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.FlashMap;

/**
 * A Serializable representation of a {@link HttpServletResponse}.
 *
 * Based on net.sf.ehcache.constructs.web.PageInfo and grails.plugin.springcache.web.HeadersCategory.
 *
 * @author Adam Murdoch
 * @author Greg Luck
 * @author Rob Fletcher
 * @author Burt Beckwith
 */
public class PageInfo implements Serializable {
    private static final long serialVersionUID = 1;

    protected static final Pattern PATTERN_CACHE_DIRECTIVE = Pattern.compile("([\\w-]+)(?:=(.+))?");
    protected static final int FOUR_KB = 4196;
    protected static final int GZIP_MAGIC_NUMBER_BYTE_1 = 31;
    protected static final int GZIP_MAGIC_NUMBER_BYTE_2 = -117;
    protected static final long ONE_YEAR_IN_SECONDS = 60 * 60 * 24 * 365;
    protected static final java.util.Set<String> IGNORED_INTERFACES = new java.util.HashSet<String>() {
        {
            add("net.sf.cglib.proxy");
            add("javassist.util.proxy.ProxyObject");
        }
    };

    protected final HttpDateFormatter httpDateFormatter = new HttpDateFormatter();
    protected final List<Header<? extends Serializable>> responseHeaders = new ArrayList<Header<? extends Serializable>>();
    protected final List<SerializableCookie> serializableCookies = new ArrayList<SerializableCookie>();
    protected Map<String, Serializable> requestAttributes;
    protected String contentType;
    protected byte[] gzippedBody;
    protected byte[] ungzippedBody;
    protected int statusCode;
    protected boolean storeGzipped;
    protected Date created;
    protected long timeToLiveSeconds;

    /**
     * Creates a PageInfo object representing the "page".
     *
     * @param statusCode
     * @param contentType
     * @param cookies
     * @param body
     * @param storeGzipped set this to false for images and page fragments which should never
     * @param timeToLiveSeconds the time to Live in seconds. 0 means maximum, which is one year per RFC2616.
     * @param headers
     * @param cookies
     * @param requestAttributes
     * @throws AlreadyGzippedException
     */
    public PageInfo(final int statusCode, final String contentType, final byte[] body, boolean storeGzipped,
            long timeToLiveSeconds, final Collection<Header<? extends Serializable>> headers,
            @SuppressWarnings("unused") final Collection<Cookie> cookies,
            Map<String, Serializable> requestAttributes) throws AlreadyGzippedException {

        if (headers != null) {
            responseHeaders.addAll(headers);
        }
        setTimeToLiveWithCheckForNeverExpires(timeToLiveSeconds);

        created = new Date();
        this.contentType = contentType;
        this.storeGzipped = storeGzipped;
        this.statusCode = statusCode;
        setCacheableRequestAttributes(requestAttributes);

        // bug 2630970
        // extractCookies(cookies);

        try {
            if (storeGzipped) {
                // gunzip on demand
                ungzippedBody = null;
                if (isBodyParameterGzipped()) {
                    gzippedBody = body;
                } else {
                    gzippedBody = gzip(body);
                }
            } else {
                Assert.isTrue(!isBodyParameterGzipped(), "Non gzip content has been gzipped.");
                ungzippedBody = body;
            }
        } catch (IOException e) {
            LoggerFactory.getLogger(getClass()).error("Error ungzipping gzipped body", e);
        }
    }

    /**
     * See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html To mark a
     * response as "never expires," an origin server sends an Expires date
     * approximately one year from the time the response is sent. HTTP/1.1
     * servers SHOULD NOT send Expires dates more than one year in the future.
     *
     * @param ttlSeconds
     *           accepts 0, which means eternal. If the time is 0 or > one year,
     *           it is set to one year in accordance with the RFC.
     *           <p/>
     *           Note: PageInfo does not hold a reference to the Element
     *           and therefore does not know what the Element ttl is. It would
     *           normally make most sense to set the TTL to the same as the
     *           element expiry.
     */
    protected void setTimeToLiveWithCheckForNeverExpires(long ttlSeconds) {
        // 0 means eternal
        if (ttlSeconds == 0 || ttlSeconds > ONE_YEAR_IN_SECONDS) {
            timeToLiveSeconds = ONE_YEAR_IN_SECONDS;
        } else {
            timeToLiveSeconds = ttlSeconds;
        }
    }

    /**
     * @param ungzipped the bytes to be gzipped
     * @return gzipped bytes
     */
    protected byte[] gzip(byte[] ungzipped) throws IOException, AlreadyGzippedException {
        if (isGzipped(ungzipped)) {
            throw new AlreadyGzippedException("The byte[] is already gzipped. It should not be gzipped again.");
        }
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        GZIPOutputStream gzipOutputStream = new GZIPOutputStream(bytes);
        gzipOutputStream.write(ungzipped);
        gzipOutputStream.close();
        return bytes.toByteArray();
    }

    /**
     * The response body will be assumed to be gzipped if the GZIP header has been set.
     *
     * @return true if the body is gzipped
     */
    protected boolean isBodyParameterGzipped() {
        for (Header<? extends Serializable> header : responseHeaders) {
            if ("gzip".equals(header.getValue())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks the first two bytes of the candidate byte array for the magic
     * number 0x677a. This magic number was obtained from /usr/share/file/magic.
     * The line for gzip is:
     * <p/>
     * <code>
     * >>14    beshort 0x677a          (gzipped)
     * </code>
     *
     * @param candidate the byte array to check
     * @return true if gzipped, false if null, less than two bytes or not gzipped
     */
    public static boolean isGzipped(byte[] candidate) {
        if (candidate == null || candidate.length < 2) {
            return false;
        }
        return (candidate[0] == GZIP_MAGIC_NUMBER_BYTE_1 && candidate[1] == GZIP_MAGIC_NUMBER_BYTE_2);
    }

    /**
     * @return the content type of the response.
     */
    public String getContentType() {
        return contentType;
    }

    /**
     * @return the gzipped version of the body if the content is storeGzipped, otherwise null
     */
    public byte[] getGzippedBody() {
        return storeGzipped ? gzippedBody : null;
    }

    /**
     * @return All of the headers set on the page
     */
    public List<Header<? extends Serializable>> getHeaders() {
        return responseHeaders;
    }

    /**
     * Returns the cookies of the response.
     */
    public List<SerializableCookie> getSerializableCookies() {
        return serializableCookies;
    }

    /**
     * Returns the status code of the response.
     */
    public int getStatusCode() {
        return statusCode;
    }

    /**
     * @return the ungzipped version of the body. This gunzipped on demand when
     *         storedGzipped, otherwise the ungzipped body is returned.
     */
    public byte[] getUngzippedBody() throws IOException {
        return storeGzipped ? ungzip(gzippedBody) : ungzippedBody;
    }

    /**
     * A highly performant ungzip implementation. Do not refactor this without
     * taking new timings. See ElementTest for timings
     *
     * @param gzipped the gzipped content
     * @return an ungzipped byte[]
     * @throws IOException
     */
    protected byte[] ungzip(final byte[] gzipped) throws IOException {
        GZIPInputStream inputStream = new GZIPInputStream(new ByteArrayInputStream(gzipped));
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(gzipped.length);
        byte[] buffer = new byte[FOUR_KB];
        int bytesRead = 0;
        while (bytesRead != -1) {
            bytesRead = inputStream.read(buffer, 0, FOUR_KB);
            if (bytesRead != -1) {
                byteArrayOutputStream.write(buffer, 0, bytesRead);
            }
        }
        byte[] ungzipped = byteArrayOutputStream.toByteArray();
        inputStream.close();
        byteArrayOutputStream.close();
        return ungzipped;
    }

    /**
     * @return true if there is a non null gzipped body
     */
    public boolean hasGzippedBody() {
        return gzippedBody != null;
    }

    /**
     * @return true if there is a non null ungzipped body
     */
    public boolean hasUngzippedBody() {
        return ungzippedBody != null;
    }

    /**
     * Returns true if the response is Ok.
     *
     * @return true if the response code is 200.
     */
    public boolean isOk() {
        return statusCode == HttpServletResponse.SC_OK;
    }

    /**
     * The <code>Date</code> this PageInfo object was created
     */
    public Date getCreated() {
        return created;
    }

    /**
     * The time to live in seconds.
     *
     * @return the time to live, or 0 if the wrapping element is eternal
     */
    public long getTimeToLiveSeconds() {
        return timeToLiveSeconds;
    }

    public Map<String, Serializable> getRequestAttributes() {
        return Collections.unmodifiableMap(requestAttributes);
    }

    public String getHeader(String headerName) {
        for (Header<? extends Serializable> header : responseHeaders) {
            if (header.getName().equals(headerName)) {
                return (String) header.getValue();
            }
        }
        return null;
    }

    public long getDateHeader(String headerName) {
        String header = getHeader(headerName);
        if (!StringUtils.hasLength(header)) {
            return -1;
        }

        try {
            return Long.valueOf(header);
        } catch (NumberFormatException e) {
            return httpDateFormatter.parseDateFromHttpDate(header).getTime();
        }
    }

    /**
     * Returns true if the page's last-modified header indicates it is newer than
     * the copy held by the client as indicated by the request's if-modified-since header.
     */
    public boolean isModified(HttpServletRequest request) {
        long ifModifiedSince = request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE);
        long lastModified = getDateHeader(HttpHeaders.LAST_MODIFIED);
        if (ifModifiedSince == -1 || lastModified == -1) {
            return true;
        }
        return lastModified > ifModifiedSince;
    }

    /**
     * Returns true if the page's etag header indicates it is the same as the
     * copy held by the client as indicated by the request's if-none-match
     * header.
     */
    public boolean isMatch(HttpServletRequest request) {
        String ifNoneMatch = request.getHeader(HttpHeaders.IF_NONE_MATCH);
        String etag = getHeader(HttpHeaders.ETAG);
        if (!StringUtils.hasLength(ifNoneMatch) || !StringUtils.hasLength(etag)) {
            return false;
        }

        return ifNoneMatch == etag;
    }

    public Map<String, Object> getCacheDirectives() {
        String cacheControl = getHeader(HttpHeaders.CACHE_CONTROL);
        Map<String, Object> directives = new HashMap<String, Object>();
        if (StringUtils.hasLength(cacheControl)) {
            for (String directive : cacheControl.split(",\\s*")) {
                Matcher matcher = PATTERN_CACHE_DIRECTIVE.matcher(directive);
                if (!matcher.find()) {
                    continue;
                }

                String name = matcher.group(1);
                String value = matcher.group(2);
                if (StringUtils.hasLength(value)) {
                    try {
                        directives.put(name, Integer.valueOf(value));
                    } catch (NumberFormatException e) {
                        directives.put(name, value);
                    }
                } else {
                    directives.put(name, true);
                }
            }
        }
        return directives;
    }

    protected void setCacheableRequestAttributes(Map<String, Serializable> attributes) {
        requestAttributes = new HashMap<String, Serializable>();

        for (Map.Entry<String, Serializable> entry : attributes.entrySet()) {
            Serializable value = entry.getValue();

            if (value instanceof GrailsFlashScope) {
                continue;
            } else if (value instanceof FlashMap) {
                continue;
            } else if (value instanceof HttpServletResponse) {
                continue;
            } else if (value instanceof ControllersApi) {
                continue;
            } else if (value instanceof PointcutAdvisor || value instanceof PointcutAdvisor[]) {
                continue;
            } else if (value instanceof TargetSource) {
                continue;
            } else if (isInvalidObject(value)) {
                continue;
            }

            requestAttributes.put(entry.getKey(), value);
        }
    }

    protected boolean isInvalidObject(Object object) {
        Class[] interfaces = GrailsClassUtils.getAllInterfaces(object);
        for (Class i : interfaces) {
            if (IGNORED_INTERFACES.contains(i.getName())) {
                return true;
            }
        }
        return false;
    }
}