<commit_before>/**
 * 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:
 *
 *   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.jasig.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.jasig.portal.concurrency.FunctionWithoutResult;
import org.jasig.portal.concurrency.locking.ClusterMutex;
import org.jasig.portal.concurrency.locking.IClusterLockService;
import org.jasig.portal.concurrency.locking.IClusterLockService.LockStatus;
import org.jasig.portal.concurrency.locking.IClusterLockService.TryLockFunctionResult;
import org.jasig.portal.concurrency.locking.LockOptions;
import org.jasig.portal.portlet.dao.IPortletCookieDao;
import org.jasig.portal.portlet.om.IPortalCookie;
import org.jasig.portal.portlet.om.IPortletCookie;
import org.jasig.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.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.
 * 
 * @author <NAME>
 * @version $Revision$
 */
@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.
	 */
	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).
	 */
	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.jasig.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.
	 */
	public void setPortalCookieAlwaysSecure(boolean portalCookieAlwaysSecure) {
		this.portalCookieAlwaysSecure = portalCookieAlwaysSecure;
	}

	/**
     * 
     * (non-Javadoc)
     * @see org.jasig.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))) {
            this.portletCookieDao.updatePortalCookieExpiration(portalCookie, cookie.getMaxAge());
            
            // 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);<commit_msg>UP-4573 Fix issue with multiple simultaneous HTTP requests failing when updating portal cookie<commit_after>/**
 * 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:
 *
 *   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.jasig.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.jasig.portal.concurrency.FunctionWithoutResult;
import org.jasig.portal.concurrency.locking.ClusterMutex;
import org.jasig.portal.concurrency.locking.IClusterLockService;
import org.jasig.portal.concurrency.locking.IClusterLockService.LockStatus;
import org.jasig.portal.concurrency.locking.IClusterLockService.TryLockFunctionResult;
import org.jasig.portal.concurrency.locking.LockOptions;
import org.jasig.portal.portlet.dao.IPortletCookieDao;
import org.jasig.portal.portlet.om.IPortalCookie;
import org.jasig.portal.portlet.om.IPortletCookie;
import org.jasig.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.
 * 
 * @author <NAME>
 * @version $Revision$
 */
@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.jasig.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.
	 */
	public void setPortalCookieAlwaysSecure(boolean portalCookieAlwaysSecure) {
		this.portalCookieAlwaysSecure = portalCookieAlwaysSecure;
	}

	/**
     * 
     * (non-Javadoc)
     * @see org.jasig.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);
{'fraction_non_alphanumeric': 0.07767162066525124, 'fraction_numerical': 0.0021821184241566406, 'mean_word_length': 3.955289304500292, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5633781', 'n_tokens_mistral': 4713, 'n_tokens_neox': 4584, 'n_words': 1318}