Index: components/core/src/main/java/org/hippoecm/hst/core/container/PageCachingValve.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- components/core/src/main/java/org/hippoecm/hst/core/container/PageCachingValve.java	(revision )
+++ components/core/src/main/java/org/hippoecm/hst/core/container/PageCachingValve.java	(revision )
@@ -0,0 +1,455 @@
+/*
+ *  Copyright 2012 Hippo.
+ * 
+ *  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 org.hippoecm.hst.core.container;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.TreeSet;
+import java.util.zip.DataFormatException;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.hippoecm.hst.core.request.HstRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.sf.ehcache.Element;
+import net.sf.ehcache.constructs.blocking.BlockingCache;
+import net.sf.ehcache.constructs.blocking.LockTimeoutException;
+import net.sf.ehcache.constructs.web.AlreadyCommittedException;
+import net.sf.ehcache.constructs.web.GenericResponseWrapper;
+import net.sf.ehcache.constructs.web.Header;
+import net.sf.ehcache.constructs.web.PageInfo;
+import net.sf.ehcache.constructs.web.ResponseHeadersNotModifiableException;
+import net.sf.ehcache.constructs.web.ResponseUtil;
+import net.sf.ehcache.constructs.web.SerializableCookie;
+import net.sf.ehcache.constructs.web.filter.FilterNonReentrantException;
+
+/**
+ * PageCachingValve
+ *
+ */
+public class PageCachingValve extends AbstractValve {
+    
+    private static final Logger log = LoggerFactory
+            .getLogger(PageCachingValve.class);
+
+    /**
+     * The cache holding the web pages. Ensure that all threads for a given
+     * cache name are using the same instance of this.
+     */
+    protected BlockingCache blockingCache;
+
+    private final VisitLog visitLog = new VisitLog();
+
+    /**
+     * Whether the cache is enabled or not
+     */
+    private boolean enabled = true;
+
+    public void setBlockingCache(BlockingCache blockingCache) {
+        this.blockingCache = blockingCache;
+    }
+
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+    
+    @Override
+    public void invoke(ValveContext context) throws ContainerException
+    {
+        if (!enabled) {
+            context.invokeNext();
+            return;
+        }
+        if (context.getRequestKey().isUnique()) {
+            // request key is marked as unique, in other words, uncacheable
+            context.invokeNext();
+            return;
+        }
+
+        HstRequestContext requestContext = context.getRequestContext();
+        if (requestContext.isCmsRequest() || requestContext.getResolvedMount().getMount().isPreview()) {
+            // we do not cache cms context requests OR preview requests
+            context.invokeNext();
+            return;
+        }
+
+        HttpServletRequest request = context.getServletRequest();
+        HttpServletResponse response = context.getServletResponse();
+
+        try {
+            PageInfo pageInfo = buildPageInfo(request, response, context);
+            if (pageInfo.isOk()) {
+                if (response.isCommitted()) {
+                    throw new AlreadyCommittedException(
+                            "Response already committed after doing buildPage"
+                                    + " but before writing response from PageInfo.");
+                }
+                writeResponse(request, response, pageInfo);
+            }
+        } catch (Exception e) {
+            throw new ContainerException(e);
+        }
+
+
+    }
+
+    /**
+     * Pages are cached based on their key.
+     *
+     * Implementers should differentiate between GET and HEAD requests otherwise blank pages
+     * can result.
+     *
+     * @param httpRequest
+     * @param context
+     * @return the key, generally the URI plus request parameters
+     */
+    protected String calculateKey(HttpServletRequest httpRequest, final ValveContext context) {
+        RequestKey requestKey = context.getRequestKey();
+
+        // add the method used to the cachekey
+        requestKey.addContribution(String.valueOf(httpRequest.getMethod()));
+
+        // account for request URI
+        requestKey.addContribution(httpRequest.getRequestURI());
+
+        // account for request queryString
+        requestKey.addContribution(httpRequest.getQueryString());
+
+        // when the hst model is rebuild, all existing cache entries should be invalid.
+        // instead of flushing the cache, those old entries will just time out.
+        // We include the hst model by hashCode in the cache key
+        int modelHashCode = context.getRequestContext().getResolvedMount().getMount().getVirtualHost().getVirtualHosts().hashCode();
+        requestKey.addContribution(String.valueOf(modelHashCode));
+
+        String mountIdentifier = context.getRequestContext().getResolvedMount().getMount().getIdentifier();
+        // account for Mount identifier
+        requestKey.addContribution(mountIdentifier);
+
+        String sitemapId = context.getRequestContext().getResolvedSiteMapItem().getHstSiteMapItem().getId();
+        // account for match sitemapId
+        requestKey.addContribution(sitemapId);
+
+        return requestKey.getKey();
+    }
+
+    /**
+     * Build page info either using the cache or building the page directly.
+     */
+    protected PageInfo buildPageInfo(final HttpServletRequest request,
+                                     final HttpServletResponse response, final ValveContext context)
+            throws Exception {
+        // Look up the cached page
+        final String key = calculateKey(request, context);
+        PageInfo pageInfo = null;
+        try {
+            checkNoReentry(request);
+            Element element = blockingCache.get(key);
+            if (element == null || element.getObjectValue() == null) {
+                try {
+                    // Page is not cached - build the response, cache it, and
+                    // send to client
+                    pageInfo = buildPage(request, response, context);
+                    if (pageInfo.isOk()) {
+                        if (log.isDebugEnabled()) {
+                            log.debug("PageInfo ok. Adding to cache "
+                                    + blockingCache.getName() + " with key "
+                                    + key);
+                        }
+                        // check Pragma header: If it is present and has value 'no-cache', then 
+                        // we do not cache the pageInfo
+                        boolean shouldCache = true;
+                        final List<Header<? extends Serializable>> headers = pageInfo.getHeaders();
+                        for (Header<? extends Serializable> header  :headers) {
+                            if ("Pragma".equals(header.getName()) && "no-cache".equals(header.getValue())) {
+                                shouldCache = false;
+                                log.debug("We do not cache '{}' because header Pragma no-cache was set", request.getRequestURL());
+                                break;
+                            }
+                        }
+                        if (shouldCache) {
+                            blockingCache.put(new Element(key, pageInfo));
+                        }
+                    } else {
+                        if (log.isDebugEnabled()) {
+                            log.debug("PageInfo was not ok(200). Putting null into cache "
+                                    + blockingCache.getName()
+                                    + " with key "
+                                    + key);
+                        }
+                        blockingCache.put(new Element(key, null));
+                    }
+                } catch (final Throwable throwable) {
+                    // Must unlock the cache if the above fails.
+                    blockingCache.put(new Element(key, null));
+                    throw new Exception(throwable);
+                }
+            } else {
+                pageInfo = (PageInfo) element.getObjectValue();
+            }
+        } catch (LockTimeoutException e) {
+            // do not release the lock, because you never acquired it
+            throw e;
+        } finally {
+            // all done building page, reset the re-entrant flag
+            visitLog.clear();
+        }
+        return pageInfo;
+    }
+
+    /**
+     * Writes the response from a PageInfo object.
+     * <p/>
+     * Headers are set last so that there is an opportunity to override
+     *
+     * @param request
+     * @param response
+     * @param pageInfo
+     * @throws IOException
+     * @throws java.util.zip.DataFormatException
+     * @throws net.sf.ehcache.constructs.web.ResponseHeadersNotModifiableException
+     *
+     */
+    protected void writeResponse(final HttpServletRequest request,
+                                 final HttpServletResponse response, final PageInfo pageInfo)
+            throws IOException, DataFormatException,
+            ResponseHeadersNotModifiableException {
+
+        setStatus(response, pageInfo);
+        setContentType(response, pageInfo);
+        setCookies(pageInfo, response);
+        // do headers last so that users can override with their own header sets
+        setHeaders(pageInfo, response);
+        writeContent(request, response, pageInfo);
+    }
+
+    /**
+     * Set the content type.
+     *
+     * @param response
+     * @param pageInfo
+     */
+    protected void setContentType(final HttpServletResponse response,
+                                  final PageInfo pageInfo) {
+        String contentType = pageInfo.getContentType();
+        if (contentType != null && contentType.length() > 0) {
+            response.setContentType(contentType);
+        }
+    }
+
+    /**
+     * Set the serializableCookies
+     *
+     * @param pageInfo
+     * @param response
+     */
+    protected void setCookies(final PageInfo pageInfo,
+                              final HttpServletResponse response) {
+
+        final Collection cookies = pageInfo.getSerializableCookies();
+        for (Iterator iterator = cookies.iterator(); iterator.hasNext();) {
+            final Cookie cookie = ((SerializableCookie) iterator.next())
+                    .toCookie();
+            response.addCookie(cookie);
+        }
+    }
+
+    /**
+     * Status code
+     *
+     * @param response
+     * @param pageInfo
+     */
+    protected void setStatus(final HttpServletResponse response,
+                             final PageInfo pageInfo) {
+        response.setStatus(pageInfo.getStatusCode());
+    }
+
+    /**
+     * Set the headers in the response object
+     *
+     * @param pageInfo
+     * @param response
+     */
+    protected void setHeaders(final PageInfo pageInfo,
+                              final HttpServletResponse response) {
+
+        final Collection<Header<? extends Serializable>> headers = pageInfo
+                .getHeaders();
+
+        // Track which headers have been set so all headers of the same name
+        // after the first are added
+        final TreeSet<String> setHeaders = new TreeSet<String>(
+                String.CASE_INSENSITIVE_ORDER);
+
+        for (final Header<? extends Serializable> header : headers) {
+            final String name = header.getName();
+
+            switch (header.getType()) {
+                case STRING:
+                    if (setHeaders.contains(name)) {
+                        response.addHeader(name, (String) header.getValue());
+                    } else {
+                        setHeaders.add(name);
+                        response.setHeader(name, (String) header.getValue());
+                    }
+                    break;
+                case DATE:
+                    if (setHeaders.contains(name)) {
+                        response.addDateHeader(name, (Long) header.getValue());
+                    } else {
+                        setHeaders.add(name);
+                        response.setDateHeader(name, (Long) header.getValue());
+                    }
+                    break;
+                case INT:
+                    if (setHeaders.contains(name)) {
+                        response.addIntHeader(name, (Integer) header.getValue());
+                    } else {
+                        setHeaders.add(name);
+                        response.setIntHeader(name, (Integer) header.getValue());
+                    }
+                    break;
+                default:
+                    throw new IllegalArgumentException("No mapping for Header: "
+                            + header);
+            }
+        }
+    }
+
+    /**
+     * Writes the response content.
+     *
+     * @param response
+     * @param pageInfo
+     * @throws IOException
+     */
+    protected void writeContent(final HttpServletRequest request,
+                                final HttpServletResponse response, final PageInfo pageInfo)
+            throws IOException, ResponseHeadersNotModifiableException {
+        byte[] body;
+
+        boolean shouldBodyBeZero = ResponseUtil.shouldBodyBeZero(request,
+                pageInfo.getStatusCode());
+        if (shouldBodyBeZero) {
+            body = new byte[0];
+        } else {
+            body = pageInfo.getUngzippedBody();
+        }
+
+        response.setContentLength(body.length);
+        OutputStream out = new BufferedOutputStream(response.getOutputStream());
+        out.write(body);
+        out.flush();
+    }
+
+    /**
+     * Check that this caching valve is not being reentered by the same
+     * recursively. Recursive calls will block indefinitely because the first
+     * request has not yet unblocked the cache.
+     * <p/>
+     * This condition usually indicates an error in valve chaining or
+     * RequestDispatcher dispatching.
+     *
+     * @param httpRequest
+     * @throws net.sf.ehcache.constructs.web.filter.FilterNonReentrantException
+     *             if reentry is detected
+     */
+    protected void checkNoReentry(final HttpServletRequest httpRequest)
+            throws FilterNonReentrantException {
+        String filterName = getClass().getName();
+        if (visitLog.hasVisited()) {
+            throw new FilterNonReentrantException(
+                    "The request thread is attempting to reenter" + " filter "
+                            + filterName + ". URL: "
+                            + httpRequest.getRequestURL());
+        } else {
+            // mark this thread as already visited
+            visitLog.markAsVisited();
+            if (log.isDebugEnabled()) {
+                log.debug("Thread {}  has been marked as visited.", Thread
+                        .currentThread().getName());
+            }
+        }
+    }
+
+    /**
+     * Builds the PageInfo object by passing the request along the filter chain
+     *
+     *
+     * @param request
+     * @param response
+     * @param context
+     * @return a Serializable value object for the page or page fragment
+     * @throws Exception
+     */
+    protected PageInfo buildPage(final HttpServletRequest request,
+                                 final HttpServletResponse response, final ValveContext context)
+            throws Exception {
+
+        // Invoke the next entity in the chain
+        final ByteArrayOutputStream outstr = new ByteArrayOutputStream();
+        final GenericResponseWrapper wrapper = new GenericResponseWrapper(
+                response, outstr);
+
+        context.setHttpServletResponse(wrapper);
+        context.invokeNext();
+        wrapper.flush();
+
+        long timeToLiveSeconds = blockingCache.getCacheConfiguration()
+                .getTimeToLiveSeconds();
+
+        // Return the page info
+        boolean storeGzipped = false;
+        return new PageInfo(wrapper.getStatus(), wrapper.getContentType(),
+                wrapper.getCookies(), outstr.toByteArray(), storeGzipped,
+                timeToLiveSeconds, wrapper.getAllHeaders());
+    }
+
+
+    /**
+     * threadlocal class to check for reentry
+     *
+     */
+    private static class VisitLog extends ThreadLocal<Boolean> {
+        @Override
+        protected Boolean initialValue() {
+            return false;
+        }
+
+        public boolean hasVisited() {
+            return get();
+        }
+
+        public void markAsVisited() {
+            set(true);
+        }
+
+        public void clear() {
+            super.remove();
+        }
+    }
+
+}
