blob: 7dad3df84125697d8100a8b80b64b3f5fb056395 [file]
/* 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;
}
}