Java tutorial
/* 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; } }