
com.semanticcms.core.servlet.CapturePage Maven / Gradle / Ivy
/*
* semanticcms-core-servlet - Java API for modeling web page content and relationships in a Servlet environment.
* Copyright (C) 2013, 2014, 2015, 2016 AO Industries, Inc.
* [email protected]
* 7262 Bull Pen Cir
* Mobile, AL 36695
*
* This file is part of semanticcms-core-servlet.
*
* semanticcms-core-servlet is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* semanticcms-core-servlet is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with semanticcms-core-servlet. If not, see .
*/
package com.semanticcms.core.servlet;
import com.aoindustries.lang.NullArgumentException;
import com.aoindustries.servlet.http.Dispatcher;
import com.aoindustries.servlet.http.NullHttpServletResponseWrapper;
import com.aoindustries.servlet.http.ServletUtil;
import com.semanticcms.core.model.Node;
import com.semanticcms.core.model.Page;
import com.semanticcms.core.model.PageRef;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.SkipPageException;
public class CapturePage {
private static final String CAPTURE_CONTEXT_REQUEST_ATTRIBUTE_NAME = CapturePage.class.getName()+".captureContext";
private static final String CAPTURE_PAGE_CACHE_REQUEST_ATTRIBUTE_NAME = CapturePage.class.getName()+".capturePageCache";
/**
* To speed up an export, the elements are cached between requests.
* The first non-exporting request will clear this cache, and it will also
* be remover after a given number of seconds.
*/
private static final String EXPORT_CAPTURE_PAGE_CACHE_CONTEXT_ATTRIBUTE_NAME = CapturePage.class.getName()+".exportCapturePageCache";
/**
* The number of milliseconds after the export cache is no longer considered valid.
*/
private static final long EXPORT_CAPTURE_PAGE_CACHE_TTL = 60 * 1000; // one minute
/**
* Gets the capture context or null
if none occurring.
*/
public static CapturePage getCaptureContext(ServletRequest request) {
return (CapturePage)request.getAttribute(CAPTURE_CONTEXT_REQUEST_ATTRIBUTE_NAME);
}
/**
* Caches pages that have been captured within the scope of a single request.
*
* IDEA: Could possibly substitute pages with higher level capture, such as META capture in place of PAGE capture request.
* IDEA: Could also cache over time, since there is currently no concept of a "user" (except whether request is trusted
* 127.0.0.1 or not).
*/
static class CapturePageCacheKey {
final PageRef pageRef;
final CaptureLevel level;
CapturePageCacheKey(
PageRef pageRef,
CaptureLevel level
) {
this.pageRef = pageRef;
assert level != CaptureLevel.BODY : "Body captures are not cached";
this.level = level;
}
@Override
public boolean equals(Object o) {
if(!(o instanceof CapturePageCacheKey)) return false;
CapturePageCacheKey other = (CapturePageCacheKey)o;
return
level==other.level
&& pageRef.equals(other.pageRef)
;
}
@Override
public int hashCode() {
int hash = level.hashCode();
hash = hash * 31 + pageRef.hashCode();
return hash;
}
@Override
public String toString() {
return '(' + level.toString() + ", " + pageRef.toString() + ')';
}
}
// TODO: Consider consequences of caching once we have a security model applied
static class ExportPageCache {
private final Object lock = new Object();
/**
* The time the cache will expire.
*/
private long cacheStart;
/**
* The currently active cache.
*/
private Map cache;
/**
* Invalidates the page cache if it has exceeded its TTL.
*/
void invalidateCache(long currentTime) {
synchronized(lock) {
if(
cache != null
&& (
currentTime >= (cacheStart + EXPORT_CAPTURE_PAGE_CACHE_TTL)
// Handle system time changes
|| currentTime <= (cacheStart - EXPORT_CAPTURE_PAGE_CACHE_TTL)
)
) {
cache = null;
}
}
}
/**
* Invalidates the cache, if needed, then gets the resulting cache.
*/
Map getCache(long currentTime) {
synchronized(lock) {
invalidateCache(currentTime);
if(cache == null) {
cacheStart = currentTime;
cache = new HashMap();
}
return cache;
}
}
}
/**
* Captures a page.
* The capture is always done with a request method of "GET", even when the enclosing request is a different method.
* Also validates parent-child and child-parent relationships if the other related pages happened to already be captured and cached.
*/
public static Page capturePage(
final ServletContext servletContext,
final HttpServletRequest request,
final HttpServletResponse response,
PageRef pageRef,
CaptureLevel level
) throws ServletException, IOException {
NullArgumentException.checkNotNull(level, "level");
// Don't use cache for full body captures
final boolean useCache = level != CaptureLevel.BODY;
// Find the cache to use
Map cache;
{
ExportPageCache exportCache = (ExportPageCache)servletContext.getAttribute(EXPORT_CAPTURE_PAGE_CACHE_CONTEXT_ATTRIBUTE_NAME);
if(Headers.isExporting(request)) {
// No harm done if two threads create two different caches inbetween check and set
if(exportCache == null) {
exportCache = new ExportPageCache();
servletContext.setAttribute(EXPORT_CAPTURE_PAGE_CACHE_CONTEXT_ATTRIBUTE_NAME, exportCache);
}
cache = exportCache.getCache(System.currentTimeMillis());
} else {
// Clean-up stale export cache
if(exportCache != null) {
exportCache.invalidateCache(System.currentTimeMillis());
}
// Request-level cache when not exporting
{
@SuppressWarnings("unchecked")
Map reqCache = (Map)request.getAttribute(CAPTURE_PAGE_CACHE_REQUEST_ATTRIBUTE_NAME);
cache = reqCache;
}
if(cache == null) {
cache = new HashMap();
request.setAttribute(CAPTURE_PAGE_CACHE_REQUEST_ATTRIBUTE_NAME, cache);
}
}
}
// cacheKey will be null when this capture is not to be cached
final CapturePageCacheKey cacheKey;
Page capturedPage;
if(useCache) {
// Check the cache
cacheKey = new CapturePageCacheKey(pageRef, level);
capturedPage = cache.get(cacheKey);
} else {
cacheKey = null;
capturedPage = null;
}
if(capturedPage == null) {
// Perform new capture
Node oldNode = CurrentNode.getCurrentNode(request);
Page oldPage = CurrentPage.getCurrentPage(request);
try {
// Clear request values that break captures
if(oldNode != null) CurrentNode.setCurrentNode(request, null);
if(oldPage != null) CurrentPage.setCurrentPage(request, null);
CaptureLevel oldCaptureLevel = CaptureLevel.getCaptureLevel(request);
CapturePage oldCaptureContext = CapturePage.getCaptureContext(request);
try {
// Set new capture context
CaptureLevel.setCaptureLevel(request, level);
CapturePage captureContext = new CapturePage();
request.setAttribute(CAPTURE_CONTEXT_REQUEST_ATTRIBUTE_NAME, captureContext);
// Include the page resource, discarding any direct output
final String capturePath = pageRef.getServletPath();
try {
// Clear PageContext on include
PageContext.newPageContextSkip(
null,
null,
null,
new PageContext.PageContextCallableSkip() {
@Override
public void call() throws ServletException, IOException, SkipPageException {
Dispatcher.include(
servletContext,
capturePath,
// Always capture as "GET" request
ServletUtil.METHOD_GET.equals(request.getMethod())
// Is already "GET"
? request
// Wrap to make "GET"
: new HttpServletRequestWrapper(request) {
@Override
public String getMethod() {
return ServletUtil.METHOD_GET;
}
},
new NullHttpServletResponseWrapper(response)
);
}
}
);
} catch(SkipPageException e) {
// An individual page may throw SkipPageException which only terminates
// the capture, not the request overall
}
capturedPage = captureContext.getCapturedPage();
if(capturedPage==null) throw new ServletException("No page captured, page=" + capturePath);
PageRef capturedPageRef = capturedPage.getPageRef();
if(!capturedPageRef.equals(pageRef)) throw new ServletException(
"Captured page has unexpected pageRef. Expected "
+ pageRef
+ " but got "
+ capturedPageRef
);
} finally {
// Restore previous capture context
CaptureLevel.setCaptureLevel(request, oldCaptureLevel);
request.setAttribute(CAPTURE_CONTEXT_REQUEST_ATTRIBUTE_NAME, oldCaptureContext);
}
} finally {
if(oldNode != null) CurrentNode.setCurrentNode(request, oldNode);
if(oldPage != null) CurrentPage.setCurrentPage(request, oldPage);
}
}
assert capturedPage != null;
if(useCache) {
// Add to cache
cache.put(cacheKey, capturedPage);
}
// Verify parents that happened to already be cached
if(!capturedPage.getAllowParentMismatch()) {
for(PageRef parentRef : capturedPage.getParentPages()) {
// Can't verify parent reference to missing book
if(parentRef.getBook() != null) {
// Check if parent in cache
Page parentPage = cache.get(new CapturePageCacheKey(parentRef, CaptureLevel.PAGE));
if(parentPage == null) parentPage = cache.get(new CapturePageCacheKey(parentRef, CaptureLevel.META));
if(parentPage != null) {
if(!parentPage.getChildPages().contains(pageRef)) {
throw new ServletException(
"The parent page does not have this as a child. this="
+ pageRef
+ ", parent="
+ parentRef
);
}
// Verify parent's children that happened to already be cached since captures can happen in any order.
/*
if(!parentPage.getAllowChildMismatch()) {
for(PageRef childRef : parentPage.getChildPages()) {
// Can't verify child reference to missing book
if(childRef.getBook() != null) {
// Check if child in cache
Page childPage = cache.get(new CapturePageCacheKey(childRef, CaptureLevel.PAGE));
if(childPage == null) childPage = cache.get(new CapturePageCacheKey(childRef, CaptureLevel.META));
if(childPage != null) {
if(!childPage.getParentPages().contains(parentRef)) {
throw new ServletException(
"The child page does not have this as a parent. this="
+ parentRef
+ ", child="
+ childRef
);
}
}
}
}
}
*/
}
}
}
}
// Verify children that happened to already be cached
if(!capturedPage.getAllowChildMismatch()) {
for(PageRef childRef : capturedPage.getChildPages()) {
// Can't verify child reference to missing book
if(childRef.getBook() != null) {
// Check if child in cache
Page childPage = cache.get(new CapturePageCacheKey(childRef, CaptureLevel.PAGE));
if(childPage == null) childPage = cache.get(new CapturePageCacheKey(childRef, CaptureLevel.META));
if(childPage != null) {
if(!childPage.getParentPages().contains(pageRef)) {
throw new ServletException(
"The child page does not have this as a parent. this="
+ pageRef
+ ", child="
+ childRef
);
}
// Verify children's parents that happened to already be cached since captures can happen in any order.
/*
if(!childPage.getAllowParentMismatch()) {
for(PageRef parentRef : childPage.getParentPages()) {
// Can't verify parent reference to missing book
if(parentRef.getBook() != null) {
// Check if parent in cache
Page parentPage = cache.get(new CapturePageCacheKey(parentRef, CaptureLevel.PAGE));
if(parentPage == null) parentPage = cache.get(new CapturePageCacheKey(parentRef, CaptureLevel.META));
if(parentPage != null) {
if(!parentPage.getChildPages().contains(childRef)) {
throw new ServletException(
"The parent page does not have this as a child. this="
+ childRef
+ ", parent="
+ parentRef
);
}
}
}
}
}*/
}
}
}
}
return capturedPage;
}
/**
* Captures a page in the current page context.
*
* @see #capturePage(javax.servlet.ServletContext, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, com.semanticcms.core.model.PageRef, com.aoindustries.web.page.servlet.CaptureLevel)
* @see PageContext
*/
public static Page capturePage(PageRef pageRef, CaptureLevel level) throws ServletException, IOException {
return capturePage(
PageContext.getServletContext(),
PageContext.getRequest(),
PageContext.getResponse(),
pageRef,
level
);
}
private CapturePage() {
}
private Page capturedPage;
public void setCapturedPage(Page capturedPage) {
NullArgumentException.checkNotNull(capturedPage, "page");
if(this.capturedPage != null) {
throw new IllegalStateException(
"Cannot capture more than one page: first page="
+ this.capturedPage.getPageRef()
+ ", second page=" + capturedPage.getPageRef()
);
}
this.capturedPage = capturedPage;
}
private Page getCapturedPage() {
return capturedPage;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy