/**
* 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.services;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apereo.portal.concurrency.FunctionWithoutResult;
import org.apereo.portal.concurrency.locking.ClusterMutex;
import org.apereo.portal.concurrency.locking.IClusterLockService;
import org.apereo.portal.concurrency.locking.IClusterLockService.LockStatus;
import org.apereo.portal.concurrency.locking.IClusterLockService.TryLockFunctionResult;
import org.apereo.portal.concurrency.locking.LockOptions;
import org.apereo.portal.portlet.dao.IPortletCookieDao;
import org.apereo.portal.portlet.om.IPortalCookie;
import org.apereo.portal.portlet.om.IPortletCookie;
import org.apereo.portal.portlet.om.IPortletWindowId;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.util.WebUtils;
/**
* {@link Service} bean to encapsulate business logic regarding portlet cookie persistence.
*
*/
@Service("portletCookieService")
public class PortletCookieServiceImpl implements IPortletCookieService, ServletContextAware {
/**
* Name of the {@link HttpSession} attribute used for storing a concurrent map of portlet
* cookies that do not need to be persisted.
*/
private static final String SESSION_ATTRIBUTE__SESSION_ONLY_COOKIE_MAP =
PortletCookieServiceImpl.class.getName() + ".SESSION_ONLY_COOKIE_MAP";
/**
* Name of the {@link HttpSession} attribute used to track the value of the {@link
* IPortalCookie} (useful if the client does not accept cookies). Note: Package access for the
* test class convenience.
*/
/*private*/ static final String SESSION_ATTRIBUTE__PORTAL_COOKIE_ID =
PortletCookieServiceImpl.class.getName() + ".PORTAL_COOKIE_ID";
private static final String PURGE_LOCK_NAME =
PortletCookieServiceImpl.class.getName() + ".PURGE_LOCK";
protected final Logger logger = LoggerFactory.getLogger(getClass());
private IPortletCookieDao portletCookieDao;
private IClusterLockService clusterLockService;
protected static final int DEFAULT_MAX_AGE = (int) TimeUnit.DAYS.toSeconds(365);
private String cookieName = DEFAULT_PORTAL_COOKIE_NAME;
private String comment = DEFAULT_PORTAL_COOKIE_COMMENT;
private String domain = null;
private String path = "/";
private int maxAge = DEFAULT_MAX_AGE;
private int maxAgeUpdateInterval = (int) TimeUnit.MINUTES.toMillis(5);
private boolean portalCookieAlwaysSecure = false;
private long purgeExpiredCookiesPeriod = 0;
@Autowired
public void setPortletCookieDao(IPortletCookieDao portletCookieDao) {
this.portletCookieDao = portletCookieDao;
}
@Autowired
public void setClusterLockService(IClusterLockService clusterLockService) {
this.clusterLockService = clusterLockService;
}
@Value(
"${org.apereo.portal.portlet.container.services.PortletCookieServiceImpl.purgeExpiredCookiesPeriod}")
public void setPurgeExpiredCookiesPeriod(long purgeExpiredCookiesPeriod) {
this.purgeExpiredCookiesPeriod = purgeExpiredCookiesPeriod;
}
@Override
public void setServletContext(ServletContext servletContext) {
this.path = servletContext.getContextPath() + "/";
}
/**
* @param maxAge The max number of seconds the portal cookie should live for. Defaults to 365
* days.
*/
public void setMaxAge(int maxAge) {
this.maxAge = maxAge;
}
/**
* @param cookieName The name of the cookie to set on the browser. Defaults to {@link
* #DEFAULT_PORTAL_COOKIE_NAME} WARNING if you change this in an existing deployment all
* existing portal cookies will be orphaned.
*/
public void setCookieName(String cookieName) {
this.cookieName = cookieName;
}
/**
* @param comment The comment for the cookie that is set. Defaults to {@link
* #DEFAULT_PORTAL_COOKIE_COMMENT}
*/
public void setComment(String comment) {
this.comment = comment;
}
/** @param domain The domain to set, it is recommended to leave this null. */
public void setDomain(String domain) {
this.domain = domain;
}
/**
* @param maxAgeUpdateInterval How frequently (in ms) the maxAge date on the portal cookie
* should be updated. Defaults to 5 minutes. Only portal cookies older than 5 minutes will
* be updated in the client's browser and the db with a new maxAge
*/
public void setMaxAgeUpdateInterval(int maxAgeUpdateInterval) {
this.maxAgeUpdateInterval = maxAgeUpdateInterval;
}
/**
* @param portalCookieAlwaysSecure Set a value of true to set the portal cookie's secure flag to
* 'true' regardless of the request's secure flag.
*/
@Value(
"${org.apereo.portal.portlet.container.services.PortletCookieServiceImpl.portalCookieAlwaysSecure}")
public void setPortalCookieAlwaysSecure(boolean portalCookieAlwaysSecure) {
this.portalCookieAlwaysSecure = portalCookieAlwaysSecure;
}
/**
* (non-Javadoc)
*
* @see
* org.apereo.portal.portlet.container.services.IPortletCookieService#updatePortalCookie(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/
@Override
public void updatePortalCookie(HttpServletRequest request, HttpServletResponse response) {
//Get the portal cookie object
final IPortalCookie portalCookie = this.getOrCreatePortalCookie(request);
//Create the browser cookie
final Cookie cookie =
this.convertToCookie(
portalCookie, this.portalCookieAlwaysSecure || request.isSecure());
//Update the expiration date of the portal cookie stored in the DB if the update interval has passed
final DateTime expires = portalCookie.getExpires();
if (DateTime.now()
.minusMillis(this.maxAgeUpdateInterval)
.isAfter(expires.minusSeconds(this.maxAge))) {
try {
this.portletCookieDao.updatePortalCookieExpiration(
portalCookie, cookie.getMaxAge());
} catch (HibernateOptimisticLockingFailureException e) {
// Especially with ngPortal UI multiple requests for individual portlet content may come at
// the same time. Sometimes another thread updated the portal cookie between our dao fetch and
// dao update. If this happens, simply ignore the update since another thread has already
// made the update.
logger.debug(
"Attempted to update expired portal cookie but another thread beat me to it."
+ " Ignoring update since the other thread handled it.");
return;
}
// Update expiration dates of portlet cookies stored in session
removeExpiredPortletCookies(request);
}
//Update the cookie in the users browser
response.addCookie(cookie);
}
/**
* Remove expired session only portlet cookies.
*
* @param request
*/
protected void removeExpiredPortletCookies(HttpServletRequest request) {
Map<String, SessionOnlyPortletCookieImpl> sessionOnlyCookies =
getSessionOnlyPortletCookieMap(request);
for (Entry<String, SessionOnlyPortletCookieImpl> entry : sessionOnlyCookies.entrySet()) {
String key = entry.getKey();
SessionOnlyPortletCookieImpl sessionOnlyCookie = entry.getValue();
if (sessionOnlyCookie.getExpires().isBeforeNow()) {
sessionOnlyCookies.remove(key);
}
}
}
@Override
public Cookie[] getAllPortletCookies(
HttpServletRequest request, IPortletWindowId portletWindowId) {
final IPortalCookie portalCookie = this.getPortalCookie(request);
//Get the cookies from the servlet request
Cookie[] servletCookies = request.getCookies();
if (servletCookies == null) {
servletCookies = new Cookie[0];
} else if (portalCookie != null) {
for (int i = 0; i < servletCookies.length; i++) {
if (servletCookies[i].getName().equals(this.cookieName)) {
// replace cookie in the array with converted IPortalCookie (so secure, domain, path, maxAge are set)
servletCookies[i] =
convertToCookie(
portalCookie,
this.portalCookieAlwaysSecure || request.isSecure());
}
}
}
//Get cookies that have been set by portlets, suppressing expired
Set<IPortletCookie> portletCookies = new HashSet<IPortletCookie>();
if (portalCookie != null) {
for (IPortletCookie portletCookie : portalCookie.getPortletCookies()) {
if (portletCookie.getExpires().isAfterNow()) {
portletCookies.add(portletCookie);
}
}
}
// finally get portlet cookies from session (all maxAge -1)
Map<String, SessionOnlyPortletCookieImpl> sessionOnlyPortletCookieMap =
getSessionOnlyPortletCookieMap(request);
Collection<SessionOnlyPortletCookieImpl> sessionOnlyCookies =
sessionOnlyPortletCookieMap.values();
//Merge into a single array
final Cookie[] cookies =
new Cookie
[servletCookies.length + portletCookies.size() + sessionOnlyCookies.size()];
System.arraycopy(servletCookies, 0, cookies, 0, servletCookies.length);
int cookieIdx = servletCookies.length;
for (final IPortletCookie portletCookie : portletCookies) {
final Cookie cookie = portletCookie.toCookie();
cookies[cookieIdx++] = cookie;
}
for (SessionOnlyPortletCookieImpl sessionOnlyCookie : sessionOnlyCookies) {
cookies[cookieIdx++] = sessionOnlyCookie.toCookie();
}
return cookies;
}
@Override
public void addCookie(
HttpServletRequest request, IPortletWindowId portletWindowId, Cookie cookie) {
final IPortalCookie portalCookie = this.getOrCreatePortalCookie(request);
if (cookie.getMaxAge() < 0) {
// persist only in the session
Map<String, SessionOnlyPortletCookieImpl> sessionOnlyPortletCookies =
getSessionOnlyPortletCookieMap(request);
SessionOnlyPortletCookieImpl sessionOnlyCookie =
new SessionOnlyPortletCookieImpl(cookie);
sessionOnlyPortletCookies.put(cookie.getName(), sessionOnlyCookie);
} else if (cookie.getMaxAge() == 0) {
// delete the cookie from the session, if present
Map<String, SessionOnlyPortletCookieImpl> sessionOnlyPortletCookies =
getSessionOnlyPortletCookieMap(request);
SessionOnlyPortletCookieImpl existing =
sessionOnlyPortletCookies.remove(cookie.getName());
if (null == existing) {
// returning null from map#remove means cookie wasn't in the session, trigger portletCookieDao update
this.portletCookieDao.addOrUpdatePortletCookie(portalCookie, cookie);
}
} else {
Map<String, SessionOnlyPortletCookieImpl> sessionOnlyPortletCookies =
getSessionOnlyPortletCookieMap(request);
sessionOnlyPortletCookies.remove(cookie.getName());
// update the portletCookieDao regardless
this.portletCookieDao.addOrUpdatePortletCookie(portalCookie, cookie);
}
}
@Override
public boolean purgeExpiredCookies() {
try {
final long purgeExpiredLastRunDelay = (long) (purgeExpiredCookiesPeriod * .95);
final TryLockFunctionResult<Object> result =
this.clusterLockService.doInTryLock(
PURGE_LOCK_NAME,
LockOptions.builder().lastRunDelay(purgeExpiredLastRunDelay),
new FunctionWithoutResult<ClusterMutex>() {
@Override
protected void applyWithoutResult(ClusterMutex input) {
portletCookieDao.purgeExpiredCookies(maxAge);
}
});
return result.getLockStatus() == LockStatus.EXECUTED;
} catch (InterruptedException e) {
logger.warn("Interrupted while purging expired cookies", e);
Thread.currentThread().interrupt();
return false;
}
}
/**
* Get the {@link Map} of {@link SessionOnlyPortletCookieImpl}s stored in the {@link
* HttpSession} specifically used for storing {@link SessionOnlyPortletCookieImpl}s with a
* maxAge equal to -1. Will create the map if it doesn't yet exist.
*
* @param request
* @return
*/
@SuppressWarnings("unchecked")
protected Map<String, SessionOnlyPortletCookieImpl> getSessionOnlyPortletCookieMap(
final HttpServletRequest request) {
final HttpSession session = request.getSession();
synchronized (WebUtils.getSessionMutex(session)) {
Map<String, SessionOnlyPortletCookieImpl> sessionOnlyPortletCookies =
(Map<String, SessionOnlyPortletCookieImpl>)
session.getAttribute(SESSION_ATTRIBUTE__SESSION_ONLY_COOKIE_MAP);
if (sessionOnlyPortletCookies == null) {
sessionOnlyPortletCookies =
new ConcurrentHashMap<String, SessionOnlyPortletCookieImpl>();
session.setAttribute(
SESSION_ATTRIBUTE__SESSION_ONLY_COOKIE_MAP, sessionOnlyPortletCookies);
}
return sessionOnlyPortletCookies;
}
}
/**
* Convert the {@link IPortalCookie} into a servlet {@link Cookie}.
*
* @param portalCookie
* @return
*/
protected Cookie convertToCookie(IPortalCookie portalCookie, boolean secure) {
final Cookie cookie = new Cookie(this.cookieName, portalCookie.getValue());
//Set the cookie's fields
cookie.setComment(this.comment);
cookie.setMaxAge(this.maxAge);
cookie.setSecure(secure);
cookie.setHttpOnly(true);
if (this.domain != null) {
cookie.setDomain(this.domain);
}
cookie.setPath(this.path);
return cookie;
}
/**
* Check the {@link HttpSession} for the ID of the Portal Cookie. This is useful if the customer
* does not wish to accept cookies.
*
* @param session
* @return
*/
protected IPortalCookie locatePortalCookieInSession(HttpSession session) {
synchronized (WebUtils.getSessionMutex(session)) {
final String portalCookieId =
(String) session.getAttribute(SESSION_ATTRIBUTE__PORTAL_COOKIE_ID);
if (portalCookieId == null) {
return null;
}
IPortalCookie portalCookie = this.portletCookieDao.getPortalCookie(portalCookieId);
return portalCookie;
}
}
/**
* Locate the existing {@link IPortalCookie} with the request, or create a new one.
*
* @param request
* @return the {@link IPortalCookie} - never null
*/
protected IPortalCookie getOrCreatePortalCookie(HttpServletRequest request) {
IPortalCookie result = null;
// first check in request
final Cookie cookie = this.getCookieFromRequest(this.cookieName, request);
if (cookie != null) {
// found a potential cookie, call off to the dao
final String value = cookie.getValue();
result = this.portletCookieDao.getPortalCookie(value);
}
// still null? check in the session
if (result == null) {
result = locatePortalCookieInSession(request.getSession());
}
// if by this point we still haven't found the portal cookie, create one
if (result == null) {
result = this.portletCookieDao.createPortalCookie(this.maxAge);
// store the portal cookie value value in the session
HttpSession session = request.getSession();
synchronized (WebUtils.getSessionMutex(session)) {
session.setAttribute(SESSION_ATTRIBUTE__PORTAL_COOKIE_ID, result.getValue());
}
}
return result;
}
/**
* Get THE {@link IPortalCookie} from the {@link HttpServletRequest}, if it exists. Gracefully
* returns null if not in the request.
*
* @param request
* @return
*/
protected IPortalCookie getPortalCookie(HttpServletRequest request) {
final Cookie cookie = this.getCookieFromRequest(this.cookieName, request);
if (cookie == null) {
// check the session
IPortalCookie portalCookieInSession = locatePortalCookieInSession(request.getSession());
if (null != portalCookieInSession) {
return portalCookieInSession;
}
return null;
}
final String value = cookie.getValue();
return this.portletCookieDao.getPortalCookie(value);
}
/**
* Attempts to retrieve the {@link Cookie} with the specified name from the {@link
* HttpServletRequest}.
*
* <p>Returns the {@link Cookie} if a match is found in the request, otherwise gracefully
* returns null.
*
* @param name
* @param request
* @return
*/
protected Cookie getCookieFromRequest(String name, HttpServletRequest request) {
final Cookie[] cookies = request.getCookies();
if (cookies == null) { // getCookies() returns null if there aren't any
return null;
}
for (final Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
return cookie;
}
}
return null;
}
}