/*
* Copyright 2000-2013 Enonic AS
* http://www.enonic.com/license
*/
package com.enonic.cms.web.portal.page;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.Interval;
import com.google.common.base.Preconditions;
import com.enonic.esl.util.DigestUtil;
import com.enonic.cms.framework.util.HttpCacheControlSettings;
import com.enonic.cms.framework.util.HttpServletUtil;
import com.enonic.cms.api.plugin.ext.http.HttpProcessor;
import com.enonic.cms.api.plugin.ext.http.HttpResponseFilter;
import com.enonic.cms.core.SiteBasePath;
import com.enonic.cms.core.SiteBasePathAndSitePath;
import com.enonic.cms.core.SiteBasePathAndSitePathToStringBuilder;
import com.enonic.cms.core.SiteBasePathResolver;
import com.enonic.cms.core.portal.PortalRenderingException;
import com.enonic.cms.core.portal.PortalRequest;
import com.enonic.cms.core.portal.PortalResponse;
import com.enonic.cms.core.portal.RedirectInstruction;
import com.enonic.cms.core.portal.livetrace.PortalRequestTrace;
import com.enonic.cms.core.structure.SitePath;
import com.enonic.cms.web.portal.SiteRedirectAndForwardHelper;
import com.enonic.cms.web.portal.instanttrace.InstantTraceId;
import com.enonic.cms.web.portal.instanttrace.InstantTraceResponseWriter;
import com.enonic.cms.web.portal.instanttrace.InstantTraceSessionInspector;
import com.enonic.cms.web.portal.instanttrace.InstantTraceSessionObject;
public class PortalResponseProcessor
{
private final static String EXECUTED_PLUGINS = "EXECUTED_PLUGINS";
private static final int SECOND_IN_MILLIS = 1000;
private SiteRedirectAndForwardHelper siteRedirectAndForwardHelper;
private List<HttpResponseFilter> responseFilters;
private PortalRequest request;
private PortalResponse response;
private HttpSession httpSession;
private HttpServletResponse httpResponse;
private HttpServletRequest httpRequest;
private boolean inPreview;
private boolean renderTraceOn;
private boolean cacheHeadersEnabledForSite = false;
private boolean forceNoCacheForSite = false;
private boolean deviceClassificationEnabled = false;
private boolean localizationEnabled = false;
private boolean instantTraceEnabled = false;
private PortalRequestTrace currentPortalRequestTrace;
private boolean encodeRedirectUrl;
private String doctypeHandler;
public void serveResponse()
throws Exception
{
if ( response.hasRedirectInstruction() )
{
serveRedirect();
}
else if ( response.isForwardToSitePath() )
{
serveForwardToSitePathResponse();
}
else
{
servePageResponse();
}
}
private void servePageResponse()
throws IOException
{
HttpServletUtil.setDateHeader( httpResponse, request.getRequestTime().toDate() );
boolean forceNoCache = false;
if ( inPreview || renderTraceOn )
{
forceNoCache = true;
final DateTime expirationTime = request.getRequestTime();
setHttpCacheHeaders( request.getRequestTime(), expirationTime, forceNoCache );
}
else if ( cacheHeadersEnabledForSite )
{
final DateTime expirationTime = resolveExpirationTime( request.getRequestTime(), response.getExpirationTime() );
setHttpCacheHeaders( request.getRequestTime(), expirationTime, forceNoCache );
}
// filter response with any response plugins
String content = filterResponseWithPlugins( response.getContent(), response.getHttpContentType() );
response.setContent( content );
boolean isHeadRequest = "HEAD".compareToIgnoreCase( httpRequest.getMethod() ) == 0;
boolean writeContent = !isHeadRequest;
boolean handleEtagLogic = cacheHeadersEnabledForSite && !forceNoCacheForSite && !instantTraceEnabled;
if ( handleEtagLogic && !StringUtils.isEmpty( content ) ) // resolveEtag does not like empty strings
{
// Handling etag logic if cache headers are enabled
final String etagFromContent = resolveEtag( content );
HttpServletUtil.setEtag( httpResponse, etagFromContent );
if ( !isContentModified( etagFromContent ) )
{
httpResponse.setStatus( HttpServletResponse.SC_NOT_MODIFIED );
writeContent = false;
}
}
if ( instantTraceEnabled && currentPortalRequestTrace != null )
{
final InstantTraceSessionObject instantTraceSessionObject =
InstantTraceSessionInspector.getInstantTraceSessionObject( httpSession );
final InstantTraceId instantTraceId = new InstantTraceId( currentPortalRequestTrace.getCompletedNumber() );
instantTraceSessionObject.addTrace( instantTraceId, currentPortalRequestTrace );
InstantTraceResponseWriter.applyInstantTraceId( httpResponse, instantTraceId );
}
httpResponse.setContentType( response.getHttpContentType() );
if ( isHeadRequest )
{
httpResponse.setContentLength( response.getContentAsBytes().length );
}
if ( writeContent )
{
writeContent( response.getContentAsBytes() );
}
}
private String resolveEtag( String content )
{
Preconditions.checkArgument( StringUtils.isNotEmpty( content ) );
return "content_" + DigestUtil.generateSHA( content );
}
private boolean isContentModified( String etagFromContent )
{
return HttpServletUtil.isContentModifiedAccordingToIfNoneMatchHeader( httpRequest, etagFromContent );
}
private void writeContent( byte[] content )
throws IOException
{
httpResponse.setContentLength( content.length );
OutputStream out = httpResponse.getOutputStream();
out.write( content );
}
private void setHttpCacheHeaders( final DateTime requestTime, final DateTime expirationTime, final boolean forceNoCache )
{
final Interval maxAge = new Interval( requestTime, expirationTime );
@SuppressWarnings({"UnnecessaryLocalVariable"}) boolean notCachableByClient = forceNoCache;
if ( notCachableByClient )
{
HttpServletUtil.setCacheControlNoCache( httpResponse );
}
else
{
HttpCacheControlSettings cacheControlSettings = new HttpCacheControlSettings();
// To eliminate proxy caching of pages (decided by TSI)
cacheControlSettings.publicAccess = false;
boolean setCacheTimeToZero = dynamicResolversEnabled();
if ( setCacheTimeToZero )
{
cacheControlSettings.maxAgeSecondsToLive = new Long( 0 );
HttpServletUtil.setExpiresHeader( httpResponse, requestTime.toDate() );
}
else
{
cacheControlSettings.maxAgeSecondsToLive = maxAge.toDurationMillis() / SECOND_IN_MILLIS;
HttpServletUtil.setExpiresHeader( httpResponse, expirationTime.toDate() );
}
HttpServletUtil.setCacheControl( httpResponse, cacheControlSettings );
}
}
private boolean dynamicResolversEnabled()
{
return deviceClassificationEnabled || localizationEnabled;
}
private void serveRedirect()
throws IOException
{
RedirectInstruction redirectInstruction = response.getRedirectInstruction();
int redirectStatus =
redirectInstruction.isPermanentRedirect() ? HttpServletResponse.SC_MOVED_PERMANENTLY : HttpServletResponse.SC_MOVED_TEMPORARILY;
if ( redirectInstruction.hasRedirectSitePath() )
{
serveRedirectToSitePath( redirectInstruction.getRedirectSitePath(), redirectStatus );
}
else if ( redirectInstruction.hasRedirectUrl() )
{
serveRedirectResponse( redirectInstruction.getRedirectUrl(), redirectStatus );
}
else
{
throw new IllegalStateException( "Redirect must have target url or sitepath set" );
}
}
private void serveRedirectToSitePath( final SitePath toSitePath, final int redirectStatus )
throws IOException
{
SiteBasePath siteBasePath = SiteBasePathResolver.resolveSiteBasePath( httpRequest, toSitePath.getSiteKey() );
SiteBasePathAndSitePath siteBasePathAndSitePath = new SiteBasePathAndSitePath( siteBasePath, toSitePath );
SiteBasePathAndSitePathToStringBuilder siteBasePathAndSitePathToStringBuilder = new SiteBasePathAndSitePathToStringBuilder();
siteBasePathAndSitePathToStringBuilder.setEncoding( "UTF-8" );
siteBasePathAndSitePathToStringBuilder.setHtmlEscapeParameterAmps( false );
siteBasePathAndSitePathToStringBuilder.setIncludeFragment( true );
siteBasePathAndSitePathToStringBuilder.setIncludeParamsInPath( true );
siteBasePathAndSitePathToStringBuilder.setUrlEncodePath( true );
String redirectUrl = siteBasePathAndSitePathToStringBuilder.toString( siteBasePathAndSitePath );
sendRedirectResponse( redirectUrl, redirectStatus );
}
private void sendRedirectResponse( final String redirectUrl, final int redirectStatus )
{
if ( redirectStatus == HttpServletResponse.SC_MOVED_PERMANENTLY )
{
httpResponse.setStatus( redirectStatus );
}
else
{
httpResponse.setStatus( HttpServletResponse.SC_MOVED_TEMPORARILY );
}
final String location = this.encodeRedirectUrl ? httpResponse.encodeRedirectURL( redirectUrl ) : redirectUrl;
httpResponse.setHeader( "Location", location );
}
private void serveForwardToSitePathResponse()
throws Exception
{
siteRedirectAndForwardHelper.forward( httpRequest, httpResponse, response.getForwardToSitePath() );
}
private void serveRedirectResponse( final String redirectUrl, final int redirectStatus )
{
sendRedirectResponse( redirectUrl, redirectStatus );
}
private DateTime resolveExpirationTime( final DateTime requestTime, final DateTime expirationTime )
{
if ( expirationTime == null )
{
return requestTime;
}
if ( expirationTime.isBefore( requestTime ) )
{
return requestTime;
}
return expirationTime;
}
private String filterResponseWithPlugins( String response, final String contentType )
{
try
{
// internal filter. needs for doctypeHandler
response = new HTML5HttpResponseFilter( doctypeHandler ).filterResponse( httpRequest, response, contentType );
//noinspection unchecked
Set<HttpProcessor> executedPlugins = (Set<HttpProcessor>) httpRequest.getAttribute( EXECUTED_PLUGINS );
if ( executedPlugins == null )
{
executedPlugins = new HashSet<HttpProcessor>();
httpRequest.setAttribute( EXECUTED_PLUGINS, executedPlugins );
}
for ( HttpResponseFilter plugin : responseFilters )
{
if ( !executedPlugins.contains( plugin ) )
{
response = plugin.filterResponse( httpRequest, response, contentType );
executedPlugins.add( plugin );
}
}
return response;
}
catch ( Exception e )
{
throw new PortalRenderingException( "Response filter plugin failed: " + e.getMessage(), e );
}
}
public void setSiteRedirectAndForwardHelper( final SiteRedirectAndForwardHelper siteRedirectAndForwardHelper )
{
this.siteRedirectAndForwardHelper = siteRedirectAndForwardHelper;
}
public void setRequest( final PortalRequest request )
{
this.request = request;
}
public void setResponse( final PortalResponse response )
{
this.response = response;
}
public void setHttpSession( final HttpSession httpSession )
{
this.httpSession = httpSession;
}
public void setHttpResponse( final HttpServletResponse httpResponse )
{
this.httpResponse = httpResponse;
}
public void setHttpRequest( final HttpServletRequest httpRequest )
{
this.httpRequest = httpRequest;
}
public void setInPreview( final boolean inPreview )
{
this.inPreview = inPreview;
}
public void setRenderTraceOn( final boolean renderTraceOn )
{
this.renderTraceOn = renderTraceOn;
}
public void setCacheHeadersEnabledForSite( final boolean cacheHeadersEnabledForSite )
{
this.cacheHeadersEnabledForSite = cacheHeadersEnabledForSite;
}
public void setForceNoCacheForSite( final boolean forceNoCacheForSite )
{
this.forceNoCacheForSite = forceNoCacheForSite;
}
public void setDeviceClassificationEnabled( final boolean deviceClassificationEnabled )
{
this.deviceClassificationEnabled = deviceClassificationEnabled;
}
public void setLocalizationEnabled( final boolean localizationEnabled )
{
this.localizationEnabled = localizationEnabled;
}
public void setResponseFilters( final List<HttpResponseFilter> responseFilters )
{
this.responseFilters = responseFilters;
}
public void setInstantTraceEnabled( final boolean instantTraceEnabled )
{
this.instantTraceEnabled = instantTraceEnabled;
}
public void setCurrentPortalRequestTrace( final PortalRequestTrace currentPortalRequestTrace )
{
this.currentPortalRequestTrace = currentPortalRequestTrace;
}
public void setEncodeRedirectUrl( final boolean encodeRedirectUrl )
{
this.encodeRedirectUrl = encodeRedirectUrl;
}
public void setDoctypeHandler( final String doctypeHandler )
{
this.doctypeHandler = doctypeHandler;
}
public String getDoctypeHandler()
{
return doctypeHandler;
}
}