/**
* Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright ownership. Apereo
* licenses this file to you 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 the
* following location:
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apereo.portal.portlet.container.cache;
import java.io.Serializable;
import java.util.Locale;
import java.util.Map;
import javax.portlet.CacheControl;
import javax.portlet.MimeResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;
import net.sf.ehcache.config.CacheConfiguration;
import org.apache.pluto.container.om.portlet.PortletDefinition;
import org.apereo.portal.portlet.om.IPortletDefinitionId;
import org.apereo.portal.portlet.om.IPortletEntity;
import org.apereo.portal.portlet.om.IPortletEntityId;
import org.apereo.portal.portlet.om.IPortletWindow;
import org.apereo.portal.portlet.om.IPortletWindowId;
import org.apereo.portal.portlet.registry.IPortletDefinitionRegistry;
import org.apereo.portal.portlet.registry.IPortletWindowRegistry;
import org.apereo.portal.portlet.rendering.PortletRenderResult;
import org.apereo.portal.url.IPortalRequestInfo;
import org.apereo.portal.url.IUrlSyntaxProvider;
import org.apereo.portal.utils.cache.TaggedCacheEntryPurger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.support.RequestContextUtils;
/**
* Default implementation of {@link IPortletCacheControlService}. {@link CacheControl}s are stored
* in a {@link Map} stored as a {@link HttpServletRequest} attribute.
*
*/
@Service
public class PortletCacheControlServiceImpl implements IPortletCacheControlService {
private static final String IF_NONE_MATCH = "If-None-Match";
private static final String IF_MODIFIED_SINCE = "If-Modified-Since";
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
private TaggedCacheEntryPurger taggedCacheEntryPurger;
private IPortletWindowRegistry portletWindowRegistry;
private IPortletDefinitionRegistry portletDefinitionRegistry;
private IUrlSyntaxProvider urlSyntaxProvider;
private Ehcache privateScopePortletRenderHeaderOutputCache;
private Ehcache publicScopePortletRenderHeaderOutputCache;
private Ehcache privateScopePortletRenderOutputCache;
private Ehcache publicScopePortletRenderOutputCache;
private Ehcache privateScopePortletResourceOutputCache;
private Ehcache publicScopePortletResourceOutputCache;
// default to 100 KB
private int cacheSizeThreshold = 102400;
@Autowired
public void setTaggedCacheEntryPurger(TaggedCacheEntryPurger taggedCacheEntryPurger) {
this.taggedCacheEntryPurger = taggedCacheEntryPurger;
}
@Autowired
@Qualifier(
"org.apereo.portal.portlet.container.cache.PortletCacheControlServiceImpl.privateScopePortletRenderHeaderOutputCache")
public void setPrivateScopePortletRenderHeaderOutputCache(
Ehcache privateScopePortletRenderHeaderOutputCache) {
this.privateScopePortletRenderHeaderOutputCache =
privateScopePortletRenderHeaderOutputCache;
}
@Autowired
@Qualifier(
"org.apereo.portal.portlet.container.cache.PortletCacheControlServiceImpl.publicScopePortletRenderHeaderOutputCache")
public void setPublicScopePortletRenderHeaderOutputCache(
Ehcache publicScopePortletRenderHeaderOutputCache) {
this.publicScopePortletRenderHeaderOutputCache = publicScopePortletRenderHeaderOutputCache;
}
@Autowired
@Qualifier(
"org.apereo.portal.portlet.container.cache.PortletCacheControlServiceImpl.privateScopePortletRenderOutputCache")
public void setPrivateScopePortletRenderOutputCache(
Ehcache privateScopePortletRenderOutputCache) {
this.privateScopePortletRenderOutputCache = privateScopePortletRenderOutputCache;
}
@Autowired
@Qualifier(
"org.apereo.portal.portlet.container.cache.PortletCacheControlServiceImpl.publicScopePortletRenderOutputCache")
public void setPublicScopePortletRenderOutputCache(
Ehcache publicScopePortletRenderOutputCache) {
this.publicScopePortletRenderOutputCache = publicScopePortletRenderOutputCache;
}
@Autowired
@Qualifier(
"org.apereo.portal.portlet.container.cache.PortletCacheControlServiceImpl.privateScopePortletResourceOutputCache")
public void setPrivateScopePortletResourceOutputCache(
Ehcache privateScopePortletResourceOutputCache) {
this.privateScopePortletResourceOutputCache = privateScopePortletResourceOutputCache;
}
@Autowired
@Qualifier(
"org.apereo.portal.portlet.container.cache.PortletCacheControlServiceImpl.publicScopePortletResourceOutputCache")
public void setPublicScopePortletResourceOutputCache(
Ehcache publicScopePortletResourceOutputCache) {
this.publicScopePortletResourceOutputCache = publicScopePortletResourceOutputCache;
}
/** @param cacheSizeThreshold the cacheSizeThreshold to set in bytes */
@Value(
"${org.apereo.portal.portlet.container.cache.PortletCacheControlServiceImpl.cacheSizeThreshold:102400}")
public void setCacheSizeThreshold(int cacheSizeThreshold) {
this.cacheSizeThreshold = cacheSizeThreshold;
}
@Override
public int getCacheSizeThreshold() {
return cacheSizeThreshold;
}
@Autowired
public void setPortletWindowRegistry(IPortletWindowRegistry portletWindowRegistry) {
this.portletWindowRegistry = portletWindowRegistry;
}
@Autowired
public void setPortletDefinitionRegistry(IPortletDefinitionRegistry portletDefinitionRegistry) {
this.portletDefinitionRegistry = portletDefinitionRegistry;
}
@Autowired
public void setUrlSyntaxProvider(IUrlSyntaxProvider urlSyntaxProvider) {
this.urlSyntaxProvider = urlSyntaxProvider;
}
@Override
public CacheState<CachedPortletData<PortletRenderResult>, PortletRenderResult>
getPortletRenderHeaderState(
HttpServletRequest request, IPortletWindowId portletWindowId) {
final IPortletWindow portletWindow =
this.portletWindowRegistry.getPortletWindow(request, portletWindowId);
if (portletWindow == null) {
logger.warn(
"portletWindowRegistry returned null for {}, returning default cacheControl and no cached portlet data",
portletWindowId);
return new CacheState<CachedPortletData<PortletRenderResult>, PortletRenderResult>();
}
//Generate the public render-header cache key
final IPortalRequestInfo portalRequestInfo =
this.urlSyntaxProvider.getPortalRequestInfo(request);
final Locale locale = RequestContextUtils.getLocale(request);
final PublicPortletCacheKey publicCacheKey =
PublicPortletCacheKey.createPublicPortletRenderHeaderCacheKey(
portletWindow, portalRequestInfo, locale);
return this.<CachedPortletData<PortletRenderResult>, PortletRenderResult>getPortletState(
request,
portletWindow,
publicCacheKey,
this.publicScopePortletRenderHeaderOutputCache,
this.privateScopePortletRenderHeaderOutputCache,
false);
}
@Override
public CacheState<CachedPortletData<PortletRenderResult>, PortletRenderResult>
getPortletRenderState(HttpServletRequest request, IPortletWindowId portletWindowId) {
final IPortletWindow portletWindow =
this.portletWindowRegistry.getPortletWindow(request, portletWindowId);
if (portletWindow == null) {
logger.warn(
"portletWindowRegistry returned null for {}, returning default cacheControl and no cached portlet data",
portletWindowId);
return new CacheState<CachedPortletData<PortletRenderResult>, PortletRenderResult>();
}
//Generate the public render cache key
final IPortalRequestInfo portalRequestInfo =
this.urlSyntaxProvider.getPortalRequestInfo(request);
final Locale locale = RequestContextUtils.getLocale(request);
final PublicPortletCacheKey publicCacheKey =
PublicPortletCacheKey.createPublicPortletRenderCacheKey(
portletWindow, portalRequestInfo, locale);
return this.<CachedPortletData<PortletRenderResult>, PortletRenderResult>getPortletState(
request,
portletWindow,
publicCacheKey,
this.publicScopePortletRenderOutputCache,
this.privateScopePortletRenderOutputCache,
false);
}
@Override
public CacheState<CachedPortletResourceData<Long>, Long> getPortletResourceState(
HttpServletRequest request, IPortletWindowId portletWindowId) {
final IPortletWindow portletWindow =
this.portletWindowRegistry.getPortletWindow(request, portletWindowId);
if (portletWindow == null) {
logger.warn(
"portletWindowRegistry returned null for {}, returning default cacheControl and no cached portlet data",
portletWindowId);
return new CacheState<CachedPortletResourceData<Long>, Long>();
}
//Generate the public resource cache key
final IPortalRequestInfo portalRequestInfo =
this.urlSyntaxProvider.getPortalRequestInfo(request);
final Locale locale = RequestContextUtils.getLocale(request);
final PublicPortletCacheKey publicCacheKey =
PublicPortletCacheKey.createPublicPortletResourceCacheKey(
portletWindow, portalRequestInfo, locale);
return this.<CachedPortletResourceData<Long>, Long>getPortletState(
request,
portletWindow,
publicCacheKey,
this.publicScopePortletResourceOutputCache,
this.privateScopePortletResourceOutputCache,
true);
}
private <D extends CachedPortletResultHolder<T>, T extends Serializable>
CacheState<D, T> getPortletState(
HttpServletRequest request,
IPortletWindow portletWindow,
PublicPortletCacheKey publicCacheKey,
Ehcache publicOutputCache,
Ehcache privateOutputCache,
boolean useHttpHeaders) {
//See if there is any cached data for the portlet header request
final CacheState<D, T> cacheState =
this.<D, T>getPortletCacheState(
request,
portletWindow,
publicCacheKey,
publicOutputCache,
privateOutputCache);
String etagHeader = null;
final D cachedPortletData = cacheState.getCachedPortletData();
if (cachedPortletData != null) {
if (useHttpHeaders) {
//Browser headers being used, check ETag and Last Modified
etagHeader = request.getHeader(IF_NONE_MATCH);
if (etagHeader != null && etagHeader.equals(cachedPortletData.getEtag())) {
//ETag is valid, mark the browser data as matching
cacheState.setBrowserDataMatches(true);
} else {
long ifModifiedSince = request.getDateHeader(IF_MODIFIED_SINCE);
if (ifModifiedSince >= 0
&& cachedPortletData.getTimeStored() <= ifModifiedSince) {
//Cached content hasn't been modified since header date, mark the browser data as matching
cacheState.setBrowserDataMatches(true);
}
}
}
final long expirationTime = cachedPortletData.getExpirationTime();
if (expirationTime == -1 || expirationTime > System.currentTimeMillis()) {
//Cached data exists, see if it can be used with no additional work
//Cached data is not expired, check if browser data should be used
cacheState.setUseCachedData(true);
//Copy browser-data-matching flag to the user-browser-data flag
cacheState.setUseBrowserData(cacheState.isBrowserDataMatches());
//No browser side data to be used, return the cached data for replay
return cacheState;
}
}
//Build CacheControl structure
final CacheControl cacheControl = cacheState.getCacheControl();
//Get the portlet descriptor
final IPortletEntity entity = portletWindow.getPortletEntity();
final IPortletDefinitionId definitionId = entity.getPortletDefinitionId();
final PortletDefinition portletDescriptor =
this.portletDefinitionRegistry.getParentPortletDescriptor(definitionId);
//Set the default scope
final String cacheScopeValue = portletDescriptor.getCacheScope();
if (MimeResponse.PUBLIC_SCOPE.equalsIgnoreCase(cacheScopeValue)) {
cacheControl.setPublicScope(true);
}
//Set the default expiration time
cacheControl.setExpirationTime(portletDescriptor.getExpirationCache());
// Use the request etag if it exists (implies useHttpHeaders==true)
if (etagHeader != null) {
cacheControl.setETag(etagHeader);
cacheState.setBrowserSetEtag(true);
}
// No browser-set etag, use the cached etag value if there is cached data
else if (cachedPortletData != null) {
logger.debug(
"setting cacheControl.eTag from cached data to {}",
cachedPortletData.getEtag());
cacheControl.setETag(cachedPortletData.getEtag());
}
return cacheState;
}
/**
* Get the cached portlet data looking in both the public and then private caches returning the
* first found
*
* @param request The current request
* @param portletWindow The window to get data for
* @param publicCacheKey The public cache key
* @param publicOutputCache The public cache
* @param privateOutputCache The private cache
*/
@SuppressWarnings("unchecked")
protected <D extends CachedPortletResultHolder<T>, T extends Serializable>
CacheState<D, T> getPortletCacheState(
HttpServletRequest request,
IPortletWindow portletWindow,
PublicPortletCacheKey publicCacheKey,
Ehcache publicOutputCache,
Ehcache privateOutputCache) {
final CacheState<D, T> cacheState = new CacheState<D, T>();
cacheState.setPublicPortletCacheKey(publicCacheKey);
final IPortletWindowId portletWindowId = portletWindow.getPortletWindowId();
//Check for publicly cached data
D cachedPortletData =
(D) this.getCachedPortletData(publicCacheKey, publicOutputCache, portletWindow);
if (cachedPortletData != null) {
cacheState.setCachedPortletData(cachedPortletData);
return cacheState;
}
//Generate private cache key
final HttpSession session = request.getSession();
final String sessionId = session.getId();
final IPortletEntityId entityId = portletWindow.getPortletEntityId();
final PrivatePortletCacheKey privateCacheKey =
new PrivatePortletCacheKey(sessionId, portletWindowId, entityId, publicCacheKey);
cacheState.setPrivatePortletCacheKey(privateCacheKey);
//Check for privately cached data
cachedPortletData =
(D) this.getCachedPortletData(privateCacheKey, privateOutputCache, portletWindow);
if (cachedPortletData != null) {
cacheState.setCachedPortletData(cachedPortletData);
return cacheState;
}
return cacheState;
}
/**
* Get the cached portlet data for the key, cache and window. If there is {@link
* CachedPortletData} in the cache it will only be returned if {@link
* CachedPortletData#isExpired()} is false or {@link CachedPortletData#getEtag()} is not null.
*
* @param cacheKey The cache key
* @param outputCache The cache
* @param portletWindow The portlet window the lookup is for
* @return The cache data for the portlet window
*/
@SuppressWarnings("unchecked")
protected <T extends Serializable> CachedPortletResultHolder<T> getCachedPortletData(
Serializable cacheKey, Ehcache outputCache, IPortletWindow portletWindow) {
final Element publicCacheElement = outputCache.get(cacheKey);
if (publicCacheElement == null) {
logger.debug("No cached output for key {}", cacheKey);
return null;
}
final CachedPortletResultHolder<T> cachedPortletData =
(CachedPortletResultHolder<T>) publicCacheElement.getObjectValue();
if (publicCacheElement.isExpired() && cachedPortletData.getEtag() == null) {
logger.debug("Cached output for key {} is expired", cacheKey);
outputCache.remove(cacheKey);
return null;
}
logger.debug("Returning cached output with key {} for {}", cacheKey, portletWindow);
return (CachedPortletResultHolder<T>) publicCacheElement.getObjectValue();
}
@Override
public boolean shouldOutputBeCached(CacheControl cacheControl) {
if (cacheControl.getExpirationTime() != 0) {
return true;
} else {
return false;
}
}
@Override
public void cachePortletRenderHeaderOutput(
IPortletWindowId portletWindowId,
HttpServletRequest httpRequest,
CacheState<CachedPortletData<PortletRenderResult>, PortletRenderResult> cacheState,
CachedPortletData<PortletRenderResult> cachedPortletData) {
cachePortletOutput(
portletWindowId,
httpRequest,
cacheState,
cachedPortletData,
this.publicScopePortletRenderHeaderOutputCache,
this.privateScopePortletRenderHeaderOutputCache);
}
@Override
public void cachePortletRenderOutput(
IPortletWindowId portletWindowId,
HttpServletRequest httpRequest,
CacheState<CachedPortletData<PortletRenderResult>, PortletRenderResult> cacheState,
CachedPortletData<PortletRenderResult> cachedPortletData) {
cachePortletOutput(
portletWindowId,
httpRequest,
cacheState,
cachedPortletData,
this.publicScopePortletRenderOutputCache,
this.privateScopePortletRenderOutputCache);
}
@Override
public void cachePortletResourceOutput(
IPortletWindowId portletWindowId,
HttpServletRequest httpRequest,
CacheState<CachedPortletResourceData<Long>, Long> cacheState,
CachedPortletResourceData<Long> cachedPortletResourceData) {
cachePortletOutput(
portletWindowId,
httpRequest,
cacheState,
cachedPortletResourceData,
this.publicScopePortletResourceOutputCache,
this.privateScopePortletResourceOutputCache);
}
private <D extends CachedPortletResultHolder<T>, T extends Serializable>
void cachePortletOutput(
IPortletWindowId portletWindowId,
HttpServletRequest httpRequest,
CacheState<D, T> cacheState,
D cachedPortletData,
Ehcache publicOutputCache,
Ehcache privateOutputCache) {
final IPortletWindow portletWindow =
this.portletWindowRegistry.getPortletWindow(httpRequest, portletWindowId);
final CacheControl cacheControl = cacheState.getCacheControl();
if (cacheControl.isPublicScope()) {
final PublicPortletCacheKey publicCacheKey = cacheState.getPublicPortletCacheKey();
this.cacheElement(publicOutputCache, publicCacheKey, cachedPortletData, cacheControl);
logger.debug("Cached public data under key {} for {}", publicCacheKey, portletWindow);
} else {
PrivatePortletCacheKey privateCacheKey = cacheState.getPrivatePortletCacheKey();
//Private key can be null if getPortletState found publicly cached data but the portlet's response is now privately scoped
if (privateCacheKey == null) {
final HttpSession session = httpRequest.getSession();
final String sessionId = session.getId();
final IPortletEntityId entityId = portletWindow.getPortletEntityId();
final PublicPortletCacheKey publicCacheKey = cacheState.getPublicPortletCacheKey();
privateCacheKey =
new PrivatePortletCacheKey(
sessionId, portletWindowId, entityId, publicCacheKey);
}
this.cacheElement(privateOutputCache, privateCacheKey, cachedPortletData, cacheControl);
logger.debug("Cached private data under key {} for {}", privateCacheKey, portletWindow);
}
}
/**
* Construct an appropriate Cache {@link Element} for the cacheKey and data. The element's ttl
* will be set depending on whether expiration or validation method is indicated from the
* CacheControl and the cache's configuration.
*/
protected void cacheElement(
Ehcache cache,
Serializable cacheKey,
CachedPortletResultHolder<?> data,
CacheControl cacheControl) {
// using validation method, ignore expirationTime and defer to cache configuration
if (cacheControl.getETag() != null) {
final Element element = new Element(cacheKey, data);
cache.put(element);
return;
}
// using expiration method, -1 for CacheControl#expirationTime means "forever" (e.g. ignore and defer to cache configuration)
final int expirationTime = cacheControl.getExpirationTime();
if (expirationTime == -1) {
final Element element = new Element(cacheKey, data);
cache.put(element);
return;
}
// using expiration method with a positive expiration, set that value as the element's TTL if it is lower than the configured cache TTL
final CacheConfiguration cacheConfiguration = cache.getCacheConfiguration();
final Element element = new Element(cacheKey, data);
final long cacheTTL = cacheConfiguration.getTimeToLiveSeconds();
if (expirationTime < cacheTTL) {
element.setTimeToLive(expirationTime);
}
cache.put(element);
}
@Override
public boolean purgeCachedPortletData(
IPortletWindowId portletWindowId, HttpServletRequest httpRequest) {
final IPortletWindow portletWindow =
this.portletWindowRegistry.getPortletWindow(httpRequest, portletWindowId);
final IPortletEntity entity = portletWindow.getPortletEntity();
final IPortletDefinitionId definitionId = entity.getPortletDefinitionId();
final HttpSession session = httpRequest.getSession();
int purgeCount = 0;
//Remove all publicly cached data
purgeCount +=
this.taggedCacheEntryPurger.purgeCacheEntries(
PublicPortletCacheKey.createTag(definitionId));
//Remove all privately cached data
purgeCount +=
this.taggedCacheEntryPurger.purgeCacheEntries(
PrivatePortletCacheKey.createTag(session.getId(), portletWindowId));
logger.debug("Purging all cached data for {} removed {} keys", portletWindow, purgeCount);
return purgeCount != 0;
}
}