/*
 * Copyright 2014-2018 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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.springframework.session.jdbc;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.dao.DataAccessException;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionOperations;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * A {@link org.springframework.session.SessionRepository} implementation that uses
 * Spring's {@link JdbcOperations} to store sessions in a relational database. This
 * implementation does not support publishing of session events.
 *

 * An example of how to create a new instance can be seen below:
 *
 *

 * JdbcTemplate jdbcTemplate = new JdbcTemplate();
 *
 * // ... configure jdbcTemplate ...
 *
 * PlatformTransactionManager transactionManager = new DataSourceTransactionManager();
 *
 * // ... configure transactionManager ...
 *
 * JdbcOperationsSessionRepository sessionRepository =
 *         new JdbcOperationsSessionRepository(jdbcTemplate, transactionManager);
 *

 *
 * For additional information on how to create and configure {@link JdbcTemplate} and
 * {@link PlatformTransactionManager}, refer to the
 *
 * Spring Framework Reference Documentation.
 *

 * By default, this implementation uses SPRING_SESSION and
 * SPRING_SESSION_ATTRIBUTES tables to store sessions. Note that the table
 * name can be customized using the {@link #setTableName(String)} method. In that case the
 * table used to store attributes will be named using the provided table name, suffixed
 * with _ATTRIBUTES.
 *
 * Depending on your database, the table definition can be described as below:
 *
 *

 * CREATE TABLE SPRING_SESSION (
 *   PRIMARY_ID CHAR(36) NOT NULL,
 *   SESSION_ID CHAR(36) NOT NULL,
 *   CREATION_TIME BIGINT NOT NULL,
 *   LAST_ACCESS_TIME BIGINT NOT NULL,
 *   MAX_INACTIVE_INTERVAL INT NOT NULL,
 *   EXPIRY_TIME BIGINT NOT NULL,
 *   PRINCIPAL_NAME VARCHAR(100),
 *   CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
 * );
 *
 * CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
 * CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (EXPIRY_TIME);
 * CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);
 *
 * CREATE TABLE SPRING_SESSION_ATTRIBUTES (
 *  SESSION_PRIMARY_ID CHAR(36) NOT NULL,
 *  ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
 *  ATTRIBUTE_BYTES BYTEA NOT NULL,
 *  CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
 *  CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
 * );
 *
 * CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_PRIMARY_ID);
 *

 *
 * Due to the differences between the various database vendors, especially when it comes
 * to storing binary data, make sure to use SQL script specific to your database. Scripts
 * for most major database vendors are packaged as
 * org/springframework/session/jdbc/schema-*.sql, where * is the
 * target database type.
 *
 * @author Vedran Pavic
 * @author Craig Andrews
 * @since 1.2.0
 */
public class JdbcOperationsSessionRepository implements
		FindByIndexNameSessionRepository<JdbcOperationsSessionRepository.JdbcSession> {

	/**
	 * The default name of database table used by Spring Session to store sessions.
	 */
	public static final String DEFAULT_TABLE_NAME = "SPRING_SESSION";

	private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";

	private static final String CREATE_SESSION_QUERY =
			"INSERT INTO %TABLE_NAME%(PRIMARY_ID, SESSION_ID, CREATION_TIME, LAST_ACCESS_TIME, MAX_INACTIVE_INTERVAL, EXPIRY_TIME, PRINCIPAL_NAME) " +
					"VALUES (?, ?, ?, ?, ?, ?, ?)";

	private static final String CREATE_SESSION_ATTRIBUTE_QUERY =
			"INSERT INTO %TABLE_NAME%_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) " +
					"SELECT PRIMARY_ID, ?, ? " +
					"FROM %TABLE_NAME% " +
					"WHERE SESSION_ID = ?";

	private static final String GET_SESSION_QUERY =
			"SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES " +
					"FROM %TABLE_NAME% S " +
					"LEFT OUTER JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID " +
					"WHERE S.SESSION_ID = ?";

	private static final String UPDATE_SESSION_QUERY =
			"UPDATE %TABLE_NAME% SET SESSION_ID = ?, LAST_ACCESS_TIME = ?, MAX_INACTIVE_INTERVAL = ?, EXPIRY_TIME = ?, PRINCIPAL_NAME = ? " +
					"WHERE PRIMARY_ID = ?";

	private static final String UPDATE_SESSION_ATTRIBUTE_QUERY =
			"UPDATE %TABLE_NAME%_ATTRIBUTES SET ATTRIBUTE_BYTES = ? " +
					"WHERE SESSION_PRIMARY_ID = ? " +
					"AND ATTRIBUTE_NAME = ?";

	private static final String DELETE_SESSION_ATTRIBUTE_QUERY =
			"DELETE FROM %TABLE_NAME%_ATTRIBUTES " +
					"WHERE SESSION_PRIMARY_ID = ? " +
					"AND ATTRIBUTE_NAME = ?";

	private static final String DELETE_SESSION_QUERY =
			"DELETE FROM %TABLE_NAME% " +
					"WHERE SESSION_ID = ?";

	private static final String LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY =
			"SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES " +
					"FROM %TABLE_NAME% S " +
					"LEFT OUTER JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID " +
					"WHERE S.PRINCIPAL_NAME = ?";

	private static final String DELETE_SESSIONS_BY_EXPIRY_TIME_QUERY =
			"DELETE FROM %TABLE_NAME% " +
					"WHERE EXPIRY_TIME < ?";

	private static final Log logger = LogFactory
			.getLog(JdbcOperationsSessionRepository.class);

	private static final PrincipalNameResolver PRINCIPAL_NAME_RESOLVER = new PrincipalNameResolver();

	private final JdbcOperations jdbcOperations;

	private final ResultSetExtractor<List<JdbcSession>> extractor = new SessionResultSetExtractor();

	private TransactionOperations transactionOperations = new TransactionOperations() {

		@Override
		public <T> T execute(TransactionCallback<T> action) throws TransactionException {
			return action.doInTransaction(null);
		}

	};

	/**
	 * The name of database table used by Spring Session to store sessions.
	 */
	private String tableName = DEFAULT_TABLE_NAME;

	private String createSessionQuery;

	private String createSessionAttributeQuery;

	private String getSessionQuery;

	private String updateSessionQuery;

	private String updateSessionAttributeQuery;

	private String deleteSessionAttributeQuery;

	private String deleteSessionQuery;

	private String listSessionsByPrincipalNameQuery;

	private String deleteSessionsByExpiryTimeQuery;

	/**
	 * If non-null, this value is used to override the default value for
	 * {@link JdbcSession#setMaxInactiveInterval(Duration)}.
	 */
	private Integer defaultMaxInactiveInterval;

	private ConversionService conversionService;

	private LobHandler lobHandler = new DefaultLobHandler();

	/**
	 * Create a new {@link JdbcOperationsSessionRepository} instance which uses the
	 * provided {@link JdbcOperations} to manage sessions.
	 *

	 * The created instance will execute all data access operations in a transaction with
	 * propagation level of {@link TransactionDefinition#PROPAGATION_REQUIRES_NEW}.
	 * @param jdbcOperations the {@link JdbcOperations} to use
	 * @param transactionManager the {@link PlatformTransactionManager} to use
	 */
	public JdbcOperationsSessionRepository(JdbcOperations jdbcOperations,
			PlatformTransactionManager transactionManager) {
		this(jdbcOperations);
		Assert.notNull(transactionManager, "TransactionManager must not be null");
		this.transactionOperations = createTransactionTemplate(transactionManager);
	}

	/**
	 * Create a new {@link JdbcOperationsSessionRepository} instance which uses the
	 * provided {@link JdbcOperations} to manage sessions.
	 *

	 * The created instance will not execute data access operations in a transaction.
	 * @param jdbcOperations the {@link JdbcOperations} to use
	 */
	public JdbcOperationsSessionRepository(JdbcOperations jdbcOperations) {
		Assert.notNull(jdbcOperations, "JdbcOperations must not be null");
		this.jdbcOperations = jdbcOperations;
		this.conversionService = createDefaultConversionService();
		prepareQueries();
	}

	/**
	 * Set the name of database table used to store sessions.
	 * @param tableName the database table name
	 */
	public void setTableName(String tableName) {
		Assert.hasText(tableName, "Table name must not be empty");
		this.tableName = tableName.trim();
		prepareQueries();
	}

	/**
	 * Set the custom SQL query used to create the session.
	 * @param createSessionQuery the SQL query string
	 */
	public void setCreateSessionQuery(String createSessionQuery) {
		Assert.hasText(createSessionQuery, "Query must not be empty");
		this.createSessionQuery = createSessionQuery;
	}

	/**
	 * Set the custom SQL query used to create the session attribute.
	 * @param createSessionAttributeQuery the SQL query string
	 */
	public void setCreateSessionAttributeQuery(String createSessionAttributeQuery) {
		Assert.hasText(createSessionAttributeQuery, "Query must not be empty");
		this.createSessionAttributeQuery = createSessionAttributeQuery;
	}

	/**
	 * Set the custom SQL query used to retrieve the session.
	 * @param getSessionQuery the SQL query string
	 */
	public void setGetSessionQuery(String getSessionQuery) {
		Assert.hasText(getSessionQuery, "Query must not be empty");
		this.getSessionQuery = getSessionQuery;
	}

	/**
	 * Set the custom SQL query used to update the session.
	 * @param updateSessionQuery the SQL query string
	 */
	public void setUpdateSessionQuery(String updateSessionQuery) {
		Assert.hasText(updateSessionQuery, "Query must not be empty");
		this.updateSessionQuery = updateSessionQuery;
	}

	/**
	 * Set the custom SQL query used to update the session attribute.
	 * @param updateSessionAttributeQuery the SQL query string
	 */
	public void setUpdateSessionAttributeQuery(String updateSessionAttributeQuery) {
		Assert.hasText(updateSessionAttributeQuery, "Query must not be empty");
		this.updateSessionAttributeQuery = updateSessionAttributeQuery;
	}

	/**
	 * Set the custom SQL query used to delete the session attribute.
	 * @param deleteSessionAttributeQuery the SQL query string
	 */
	public void setDeleteSessionAttributeQuery(String deleteSessionAttributeQuery) {
		Assert.hasText(deleteSessionAttributeQuery, "Query must not be empty");
		this.deleteSessionAttributeQuery = deleteSessionAttributeQuery;
	}

	/**
	 * Set the custom SQL query used to delete the session.
	 * @param deleteSessionQuery the SQL query string
	 */
	public void setDeleteSessionQuery(String deleteSessionQuery) {
		Assert.hasText(deleteSessionQuery, "Query must not be empty");
		this.deleteSessionQuery = deleteSessionQuery;
	}

	/**
	 * Set the custom SQL query used to retrieve the sessions by principal name.
	 * @param listSessionsByPrincipalNameQuery the SQL query string
	 */
	public void setListSessionsByPrincipalNameQuery(String listSessionsByPrincipalNameQuery) {
		Assert.hasText(listSessionsByPrincipalNameQuery, "Query must not be empty");
		this.listSessionsByPrincipalNameQuery = listSessionsByPrincipalNameQuery;
	}

	/**
	 * Set the custom SQL query used to delete the sessions by last access time.
	 * @param deleteSessionsByExpiryTimeQuery the SQL query string
	 */
	public void setDeleteSessionsByExpiryTimeQuery(String deleteSessionsByExpiryTimeQuery) {
		Assert.hasText(deleteSessionsByExpiryTimeQuery, "Query must not be empty");
		this.deleteSessionsByExpiryTimeQuery = deleteSessionsByExpiryTimeQuery;
	}

	/**
	 * Set the maximum inactive interval in seconds between requests before newly created
	 * sessions will be invalidated. A negative time indicates that the session will never
	 * timeout. The default is 1800 (30 minutes).
	 * @param defaultMaxInactiveInterval the maximum inactive interval in seconds
	 */
	public void setDefaultMaxInactiveInterval(Integer defaultMaxInactiveInterval) {
		this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
	}

	public void setLobHandler(LobHandler lobHandler) {
		Assert.notNull(lobHandler, "LobHandler must not be null");
		this.lobHandler = lobHandler;
	}

	/**
	 * Sets the {@link ConversionService} to use.
	 * @param conversionService the converter to set
	 */
	public void setConversionService(ConversionService conversionService) {
		Assert.notNull(conversionService, "conversionService must not be null");
		this.conversionService = conversionService;
	}

	@Override
	public JdbcSession createSession() {
		JdbcSession session = new JdbcSession();
		if (this.defaultMaxInactiveInterval != null) {
			session.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
		}
		return session;
	}

	@Override
	public void save(final JdbcSession session) {
		if (session.isNew()) {
			this.transactionOperations.execute(new TransactionCallbackWithoutResult() {

				@Override
				protected void doInTransactionWithoutResult(TransactionStatus status) {
					JdbcOperationsSessionRepository.this.jdbcOperations.update(
							JdbcOperationsSessionRepository.this.createSessionQuery,
							(ps) -> {
								ps.setString(1, session.primaryKey);
								ps.setString(2, session.getId());
								ps.setLong(3, session.getCreationTime().toEpochMilli());
								ps.setLong(4, session.getLastAccessedTime().toEpochMilli());
								ps.setInt(5, (int) session.getMaxInactiveInterval().getSeconds());
								ps.setLong(6, session.getExpiryTime().toEpochMilli());
								ps.setString(7, session.getPrincipalName());
							});
					Set<String> attributeNames = session.getAttributeNames();
					if (!attributeNames.isEmpty()) {
						insertSessionAttributes(session, new ArrayList<>(attributeNames));
					}
				}

			});
		}
		else {
			this.transactionOperations.execute(new TransactionCallbackWithoutResult() {

				@Override
				protected void doInTransactionWithoutResult(TransactionStatus status) {
					if (session.isChanged()) {
						JdbcOperationsSessionRepository.this.jdbcOperations.update(
								JdbcOperationsSessionRepository.this.updateSessionQuery,
								(ps) -> {
									ps.setString(1, session.getId());
									ps.setLong(2, session.getLastAccessedTime().toEpochMilli());
									ps.setInt(3, (int) session.getMaxInactiveInterval().getSeconds());
									ps.setLong(4, session.getExpiryTime().toEpochMilli());
									ps.setString(5, session.getPrincipalName());
									ps.setString(6, session.primaryKey);
								});
					}
					List<String> addedAttributeNames = session.delta.entrySet().stream()
							.filter((entry) -> entry.getValue() == DeltaValue.ADDED)
							.map(Map.Entry::getKey)
							.collect(Collectors.toList());
					if (!addedAttributeNames.isEmpty()) {
						insertSessionAttributes(session, addedAttributeNames);
					}
					List<String> updatedAttributeNames = session.delta.entrySet().stream()
							.filter((entry) -> entry.getValue() == DeltaValue.UPDATED)
							.map(Map.Entry::getKey)
							.collect(Collectors.toList());
					if (!updatedAttributeNames.isEmpty()) {
						updateSessionAttributes(session, updatedAttributeNames);
					}
					List<String> removedAttributeNames = session.delta.entrySet().stream()
							.filter((entry) -> entry.getValue() == DeltaValue.REMOVED)
							.map(Map.Entry::getKey)
							.collect(Collectors.toList());
					if (!removedAttributeNames.isEmpty()) {
						deleteSessionAttributes(session, removedAttributeNames);
					}
				}

			});
		}
		session.clearChangeFlags();
	}

	@Override
	public JdbcSession findById(final String id) {
		final JdbcSession session = this.transactionOperations.execute((status) -> {
			List<JdbcSession> sessions = JdbcOperationsSessionRepository.this.jdbcOperations.query(
					JdbcOperationsSessionRepository.this.getSessionQuery,
					(ps) -> ps.setString(1, id),
					JdbcOperationsSessionRepository.this.extractor
			);
			if (sessions.isEmpty()) {
				return null;
			}
			return sessions.get(0);
		});

		if (session != null) {
			if (session.isExpired()) {
				deleteById(id);
			}
			else {
				return session;
			}
		}
		return null;
	}

	@Override
	public void deleteById(final String id) {
		this.transactionOperations.execute(new TransactionCallbackWithoutResult() {

			@Override
			protected void doInTransactionWithoutResult(TransactionStatus status) {
				JdbcOperationsSessionRepository.this.jdbcOperations.update(
						JdbcOperationsSessionRepository.this.deleteSessionQuery, id);
			}

		});
	}

	@Override
	public Map<String, JdbcSession> findByIndexNameAndIndexValue(String indexName,
			final String indexValue) {
		if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
			return Collections.emptyMap();
		}

		List<JdbcSession> sessions = this.transactionOperations.execute((status) ->
				JdbcOperationsSessionRepository.this.jdbcOperations.query(
						JdbcOperationsSessionRepository.this.listSessionsByPrincipalNameQuery,
						(ps) -> ps.setString(1, indexValue),
						JdbcOperationsSessionRepository.this.extractor));

		Map<String, JdbcSession> sessionMap = new HashMap<>(
				sessions.size());

		for (JdbcSession session : sessions) {
			sessionMap.put(session.getId(), session);
		}

		return sessionMap;
	}

	private void insertSessionAttributes(JdbcSession session, List<String> attributeNames) {
		Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
		if (attributeNames.size() > 1) {
			this.jdbcOperations.batchUpdate(this.createSessionAttributeQuery, new BatchPreparedStatementSetter() {

						@Override
						public void setValues(PreparedStatement ps, int i) throws SQLException {
							String attributeName = attributeNames.get(i);
							ps.setString(1, attributeName);
							setObjectAsBlob(ps, 2, session.getAttribute(attributeName));
							ps.setString(3, session.getId());
						}

						@Override
						public int getBatchSize() {
							return attributeNames.size();
						}

			});
		}
		else {
			this.jdbcOperations.update(this.createSessionAttributeQuery, (ps) -> {
				String attributeName = attributeNames.get(0);
				ps.setString(1, attributeName);
				setObjectAsBlob(ps, 2, session.getAttribute(attributeName));
				ps.setString(3, session.getId());
			});
		}
	}

	private void updateSessionAttributes(JdbcSession session, List<String> attributeNames) {
		Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
		if (attributeNames.size() > 1) {
			this.jdbcOperations.batchUpdate(this.updateSessionAttributeQuery, new BatchPreparedStatementSetter() {

						@Override
						public void setValues(PreparedStatement ps, int i) throws SQLException {
							String attributeName = attributeNames.get(i);
							setObjectAsBlob(ps, 1, session.getAttribute(attributeName));
							ps.setString(2, session.primaryKey);
							ps.setString(3, attributeName);
						}

						@Override
						public int getBatchSize() {
							return attributeNames.size();
						}

			});
		}
		else {
			this.jdbcOperations.update(this.updateSessionAttributeQuery, (ps) -> {
				String attributeName = attributeNames.get(0);
				setObjectAsBlob(ps, 1, session.getAttribute(attributeName));
				ps.setString(2, session.primaryKey);
				ps.setString(3, attributeName);
			});
		}
	}

	private void deleteSessionAttributes(JdbcSession session, List<String> attributeNames) {
		Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
		if (attributeNames.size() > 1) {
			this.jdbcOperations.batchUpdate(this.deleteSessionAttributeQuery, new BatchPreparedStatementSetter() {

						@Override
						public void setValues(PreparedStatement ps, int i) throws SQLException {
							String attributeName = attributeNames.get(i);
							ps.setString(1, session.primaryKey);
							ps.setString(2, attributeName);
						}

						@Override
						public int getBatchSize() {
							return attributeNames.size();
						}

			});
		}
		else {
			this.jdbcOperations.update(this.deleteSessionAttributeQuery, (ps) -> {
				String attributeName = attributeNames.get(0);
				ps.setString(1, session.primaryKey);
				ps.setString(2, attributeName);
			});
		}
	}

	public void cleanUpExpiredSessions() {
		Integer deletedCount = this.transactionOperations.execute((status) ->
				JdbcOperationsSessionRepository.this.jdbcOperations.update(
						JdbcOperationsSessionRepository.this.deleteSessionsByExpiryTimeQuery,
						System.currentTimeMillis()));

		if (logger.isDebugEnabled()) {
			logger.debug("Cleaned up " + deletedCount + " expired sessions");
		}
	}

	private static TransactionTemplate createTransactionTemplate(
			PlatformTransactionManager transactionManager) {
		TransactionTemplate transactionTemplate = new TransactionTemplate(
				transactionManager);
		transactionTemplate.setPropagationBehavior(
				TransactionDefinition.PROPAGATION_REQUIRES_NEW);
		transactionTemplate.afterPropertiesSet();
		return transactionTemplate;
	}

	private static GenericConversionService createDefaultConversionService() {
		GenericConversionService converter = new GenericConversionService();
		converter.addConverter(Object.class, byte[].class,
				new SerializingConverter());
		converter.addConverter(byte[].class, Object.class,
				new DeserializingConverter());
		return converter;
	}

	private String getQuery(String base) {
		return StringUtils.replace(base, "%TABLE_NAME%", this.tableName);
	}

	private void prepareQueries() {
		this.createSessionQuery = getQuery(CREATE_SESSION_QUERY);
		this.createSessionAttributeQuery = getQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
		this.getSessionQuery = getQuery(GET_SESSION_QUERY);
		this.updateSessionQuery = getQuery(UPDATE_SESSION_QUERY);
		this.updateSessionAttributeQuery = getQuery(UPDATE_SESSION_ATTRIBUTE_QUERY);
		this.deleteSessionAttributeQuery = getQuery(DELETE_SESSION_ATTRIBUTE_QUERY);
		this.deleteSessionQuery = getQuery(DELETE_SESSION_QUERY);
		this.listSessionsByPrincipalNameQuery =
				getQuery(LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY);
		this.deleteSessionsByExpiryTimeQuery =
				getQuery(DELETE_SESSIONS_BY_EXPIRY_TIME_QUERY);
	}

	private void setObjectAsBlob(PreparedStatement ps, int paramIndex, Object object)
			throws SQLException {
		byte[] bytes = (byte[]) this.conversionService.convert(object,
				TypeDescriptor.valueOf(Object.class),
				TypeDescriptor.valueOf(byte[].class));
		this.lobHandler.getLobCreator().setBlobAsBytes(ps, paramIndex, bytes);
	}

	private Object getBlobAsObject(ResultSet rs, String columnName) throws SQLException {
		byte[] bytes = this.lobHandler.getBlobAsBytes(rs, columnName);
		return this.conversionService.convert(bytes, TypeDescriptor.valueOf(byte[].class),
				TypeDescriptor.valueOf(Object.class));
	}

	private enum DeltaValue {

		ADDED, UPDATED, REMOVED

	}

	private static <T> Supplier<T> value(T value) {
		return (value != null) ? () -> value : null;
	}

	private static <T> Supplier<T> lazily(Supplier<T> supplier) {
		Supplier<T> lazySupplier = new Supplier<T>() {

			private T value;

			@Override
			public T get() {
				if (this.value == null) {
					this.value = supplier.get();
				}
				return this.value;
			}

		};

		return (supplier != null) ? lazySupplier : null;
	}

	/**
	 * The {@link Session} to use for {@link JdbcOperationsSessionRepository}.
	 *
	 * @author Vedran Pavic
	 */
	final class JdbcSession implements Session {

		private final Session delegate;

		private final String primaryKey;

		private boolean isNew;

		private boolean changed;

		private Map<String, DeltaValue> delta = new HashMap<>();

		JdbcSession() {
			this.delegate = new MapSession();
			this.isNew = true;
			this.primaryKey = UUID.randomUUID().toString();
		}

		JdbcSession(String primaryKey, Session delegate) {
			Assert.notNull(primaryKey, "primaryKey cannot be null");
			Assert.notNull(delegate, "Session cannot be null");
			this.primaryKey = primaryKey;
			this.delegate = delegate;
		}

		boolean isNew() {
			return this.isNew;
		}

		boolean isChanged() {
			return this.changed;
		}

		Map<String, DeltaValue> getDelta() {
			return this.delta;
		}

		void clearChangeFlags() {
			this.isNew = false;
			this.changed = false;
			this.delta.clear();
		}

		String getPrincipalName() {
			return PRINCIPAL_NAME_RESOLVER.resolvePrincipal(this);
		}

		Instant getExpiryTime() {
			return getLastAccessedTime().plus(getMaxInactiveInterval());
		}

		@Override
		public String getId() {
			return this.delegate.getId();
		}

		@Override
		public String changeSessionId() {
			this.changed = true;
			return this.delegate.changeSessionId();
		}

		@Override
		public <T> T getAttribute(String attributeName) {
			Supplier<T> supplier = this.delegate.getAttribute(attributeName);
			return (supplier != null) ? supplier.get() : null;
		}

		@Override
		public Set<String> getAttributeNames() {
			return this.delegate.getAttributeNames();
		}

		@Override
		public void setAttribute(String attributeName, Object attributeValue) {
			boolean attributeExists = (this.delegate.getAttribute(attributeName) != null);
			boolean attributeRemoved = (attributeValue == null);
			if (!attributeExists && attributeRemoved) {
				return;
			}
			if (attributeExists) {
				if (attributeRemoved) {
					this.delta.merge(attributeName, DeltaValue.REMOVED, (oldDeltaValue,
							deltaValue) -> (oldDeltaValue == DeltaValue.ADDED) ? null
									: deltaValue);
				}
				else {
					this.delta.merge(attributeName, DeltaValue.UPDATED,
							(oldDeltaValue,
									deltaValue) -> (oldDeltaValue == DeltaValue.ADDED)
											? oldDeltaValue
											: deltaValue);
				}
			}
			else {
				this.delta.merge(attributeName, DeltaValue.ADDED,
						(oldDeltaValue, deltaValue) -> (oldDeltaValue == DeltaValue.ADDED)
								? oldDeltaValue
								: DeltaValue.UPDATED);
			}
			this.delegate.setAttribute(attributeName, value(attributeValue));
			if (PRINCIPAL_NAME_INDEX_NAME.equals(attributeName) ||
					SPRING_SECURITY_CONTEXT.equals(attributeName)) {
				this.changed = true;
			}
		}

		@Override
		public void removeAttribute(String attributeName) {
			setAttribute(attributeName, null);
		}

		@Override
		public Instant getCreationTime() {
			return this.delegate.getCreationTime();
		}

		@Override
		public void setLastAccessedTime(Instant lastAccessedTime) {
			this.delegate.setLastAccessedTime(lastAccessedTime);
			this.changed = true;
		}

		@Override
		public Instant getLastAccessedTime() {
			return this.delegate.getLastAccessedTime();
		}

		@Override
		public void setMaxInactiveInterval(Duration interval) {
			this.delegate.setMaxInactiveInterval(interval);
			this.changed = true;
		}

		@Override
		public Duration getMaxInactiveInterval() {
			return this.delegate.getMaxInactiveInterval();
		}

		@Override
		public boolean isExpired() {
			return this.delegate.isExpired();
		}

	}

	/**
	 * Resolves the Spring Security principal name.
	 *
	 * @author Vedran Pavic
	 */
	static class PrincipalNameResolver {

		private SpelExpressionParser parser = new SpelExpressionParser();

		public String resolvePrincipal(Session session) {
			String principalName = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);
			if (principalName != null) {
				return principalName;
			}
			Object authentication = session.getAttribute(SPRING_SECURITY_CONTEXT);
			if (authentication != null) {
				Expression expression = this.parser
						.parseExpression("authentication?.name");
				return expression.getValue(authentication, String.class);
			}
			return null;
		}

	}

	private class SessionResultSetExtractor implements ResultSetExtractor<List<JdbcSession>> {

		@Override
		public List<JdbcSession> extractData(ResultSet rs) throws SQLException, DataAccessException {
			List<JdbcSession> sessions = new ArrayList<>();
			while (rs.next()) {
				String id = rs.getString("SESSION_ID");
				JdbcSession session;
				if (sessions.size() > 0 && getLast(sessions).getId().equals(id)) {
					session = getLast(sessions);
				}
				else {
					MapSession delegate = new MapSession(id);
					String primaryKey = rs.getString("PRIMARY_ID");
					delegate.setCreationTime(Instant.ofEpochMilli(rs.getLong("CREATION_TIME")));
					delegate.setLastAccessedTime(Instant.ofEpochMilli(rs.getLong("LAST_ACCESS_TIME")));
					delegate.setMaxInactiveInterval(Duration.ofSeconds(rs.getInt("MAX_INACTIVE_INTERVAL")));
					session = new JdbcSession(primaryKey, delegate);
				}
				String attributeName = rs.getString("ATTRIBUTE_NAME");
				if (attributeName != null) {
					Object attributeValue = getBlobAsObject(rs, "ATTRIBUTE_BYTES");
					session.delegate.setAttribute(attributeName, lazily(() -> attributeValue));
				}
				sessions.add(session);
			}
			return sessions;
		}

		private JdbcSession getLast(List<JdbcSession> sessions) {
			return sessions.get(sessions.size() - 1);
		}

	}

}


/*
 * Copyright 2014-2017 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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.springframework.session.web.http;

import java.util.List;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

import org.springframework.context.ApplicationListener;
import org.springframework.session.Session;
import org.springframework.session.events.AbstractSessionEvent;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDestroyedEvent;
import org.springframework.web.context.ServletContextAware;

/**
 * Receives {@link SessionDestroyedEvent} and {@link SessionCreatedEvent} and translates
 * them into {@link HttpSessionEvent} and submits the {@link HttpSessionEvent} to every
 * registered {@link HttpSessionListener}.
 *
 * @author Rob Winch
 * @since 1.1
 */
public class SessionEventHttpSessionListenerAdapter
		implements ApplicationListener<AbstractSessionEvent>, ServletContextAware {
	private final List<HttpSessionListener> listeners;

	private ServletContext context;

	public SessionEventHttpSessionListenerAdapter(List<HttpSessionListener> listeners) {
		super();
		this.listeners = listeners;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.
	 * springframework.context.ApplicationEvent)
	 */
	@Override
	public void onApplicationEvent(AbstractSessionEvent event) {
		if (this.listeners.isEmpty()) {
			return;
		}

		HttpSessionEvent httpSessionEvent = createHttpSessionEvent(event);

		for (HttpSessionListener listener : this.listeners) {
			if (event instanceof SessionDestroyedEvent) {
				listener.sessionDestroyed(httpSessionEvent);
			}
			else if (event instanceof SessionCreatedEvent) {
				listener.sessionCreated(httpSessionEvent);
			}
		}
	}

	private HttpSessionEvent createHttpSessionEvent(AbstractSessionEvent event) {
		Session session = event.getSession();
		HttpSession httpSession = new HttpSessionAdapter<>(session,
				this.context);
		return new HttpSessionEvent(httpSession);
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * org.springframework.web.context.ServletContextAware#setServletContext(javax.servlet
	 * .ServletContext)
	 */
	@Override
	public void setServletContext(ServletContext servletContext) {
		this.context = servletContext;
	}
}


/*
 * Copyright 2014-2017 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 sample;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;

@Import(EmbeddedRedisConfig.class)
@EnableRedisHttpSession
public class Config {

	@Bean
	public LettuceConnectionFactory connectionFactory() {
		return new LettuceConnectionFactory();
	}

	// tag::cookie-serializer[]
	@Bean
	public CookieSerializer cookieSerializer() {
		DefaultCookieSerializer serializer = new DefaultCookieSerializer();
		serializer.setCookieName("JSESSIONID"); // <1>
		serializer.setCookiePath("/"); // <2>
		serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$"); // <3>
		return serializer;
	}
	// end::cookie-serializer[]
}


/*
 * Copyright 2014-2017 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 sample.config;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.security.jackson2.SecurityJackson2Modules;

/**
 * @author jitendra on 3/3/16.
 */
@Configuration
public class SessionConfig implements BeanClassLoaderAware {

	private ClassLoader loader;

	@Bean
	public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
		return new GenericJackson2JsonRedisSerializer(objectMapper());
	}

	/**
	 * Customized {@link ObjectMapper} to add mix-in for class that doesn't have default
	 * constructors
	 *
	 * @return the {@link ObjectMapper} to use
	 */
	private ObjectMapper objectMapper() {
		ObjectMapper mapper = new ObjectMapper();
		mapper.registerModules(SecurityJackson2Modules.getModules(this.loader));
		return mapper;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang
	 * .ClassLoader)
	 */
	@Override
	public void setBeanClassLoader(ClassLoader classLoader) {
		this.loader = classLoader;
	}
}


/*
 * Copyright 2014-2018 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 sample.session;

import java.io.IOException;
import java.net.InetAddress;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.maxmind.geoip2.DatabaseReader;
import com.maxmind.geoip2.model.CityResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

/**
 * Inserts the session details into the session for every request. Some users may prefer
 * to insert session details only after authentication. This is fine, but it may be
 * valuable to the most up to date information so that if someone stole the user's session
 * id it can be observed.
 *
 * @author Rob Winch
 *
 */
// tag::class[]
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 101)
public class SessionDetailsFilter extends OncePerRequestFilter {
	static final String UNKNOWN = "Unknown";

	private DatabaseReader reader;

	@Autowired
	public SessionDetailsFilter(DatabaseReader reader) {
		this.reader = reader;
	}

	// tag::dofilterinternal[]
	@Override
	public void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		chain.doFilter(request, response);

		HttpSession session = request.getSession(false);
		if (session != null) {
			String remoteAddr = getRemoteAddress(request);
			String geoLocation = getGeoLocation(remoteAddr);

			SessionDetails details = new SessionDetails();
			details.setAccessType(request.getHeader("User-Agent"));
			details.setLocation(remoteAddr + " " + geoLocation);

			session.setAttribute("SESSION_DETAILS", details);
		}
	}
	// end::dofilterinternal[]

	String getGeoLocation(String remoteAddr) {
		try {
			CityResponse city = this.reader.city(InetAddress.getByName(remoteAddr));
			String cityName = city.getCity().getName();
			String countryName = city.getCountry().getName();
			if (cityName == null && countryName == null) {
				return null;
			}
			else if (cityName == null) {
				return countryName;
			}
			else if (countryName == null) {
				return cityName;
			}
			return cityName + ", " + countryName;
		}
		catch (Exception ex) {
			return UNKNOWN;

		}
	}

	private String getRemoteAddress(HttpServletRequest request) {
		String remoteAddr = request.getHeader("X-FORWARDED-FOR");
		if (remoteAddr == null) {
			remoteAddr = request.getRemoteAddr();
		}
		else if (remoteAddr.contains(",")) {
			remoteAddr = remoteAddr.split(",")[0];
		}
		return remoteAddr;
	}
}
// end::class[]


/*
 * Copyright 2014-2018 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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.springframework.session.data.redis.config;

import java.util.Properties;

import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.redis.connection.RedisConnection;

/**
 *

 * Ensures that Redis Keyspace events for Generic commands and Expired events are enabled.
 * For example, it might set the following:
 *

 *
 *

 * config set notify-keyspace-events Egx
 *

 *
 *

 * This strategy will not work if the Redis instance has been properly secured. Instead,
 * the Redis instance should be configured externally and a Bean of type
 * {@link ConfigureRedisAction#NO_OP} should be exposed.
 *

 *
 * @author Rob Winch
 * @author Mark Paluch
 * @since 1.0.1
 */
public class ConfigureNotifyKeyspaceEventsAction implements ConfigureRedisAction {

	static final String CONFIG_NOTIFY_KEYSPACE_EVENTS = "notify-keyspace-events";

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * org.springframework.session.data.redis.config.ConfigureRedisAction#configure(org.
	 * springframework.data.redis.connection.RedisConnection)
	 */
	@Override
	public void configure(RedisConnection connection) {
		String notifyOptions = getNotifyOptions(connection);
		String customizedNotifyOptions = notifyOptions;
		if (!customizedNotifyOptions.contains("E")) {
			customizedNotifyOptions += "E";
		}
		boolean A = customizedNotifyOptions.contains("A");
		if (!(A || customizedNotifyOptions.contains("g"))) {
			customizedNotifyOptions += "g";
		}
		if (!(A || customizedNotifyOptions.contains("x"))) {
			customizedNotifyOptions += "x";
		}
		if (!notifyOptions.equals(customizedNotifyOptions)) {
			connection.setConfig(CONFIG_NOTIFY_KEYSPACE_EVENTS, customizedNotifyOptions);
		}
	}

	private String getNotifyOptions(RedisConnection connection) {
		try {
			Properties config = connection.getConfig(CONFIG_NOTIFY_KEYSPACE_EVENTS);
			if (config.isEmpty()) {
				return "";
			}
			return config.getProperty(config.stringPropertyNames().iterator().next());
		}
		catch (InvalidDataAccessApiUsageException ex) {
			throw new IllegalStateException(
					"Unable to configure Redis to keyspace notifications. See https://docs.spring.io/spring-session/docs/current/reference/html5/#api-redisoperationssessionrepository-sessiondestroyedevent",
					ex);
		}
	}

}


/*
 * Copyright 2014-2018 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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.springframework.session.config.annotation.web.server;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.web.server.session.SpringSessionWebSessionStore;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import org.springframework.web.server.session.DefaultWebSessionManager;
import org.springframework.web.server.session.WebSessionIdResolver;
import org.springframework.web.server.session.WebSessionManager;

/**
 * Wire up a {@link WebSessionManager} using a Reactive {@link ReactiveSessionRepository} from the application context.
 *
 * @author Greg Turnquist
 * @author Rob Winch
 * @since 2.0
 *
 * @see EnableSpringWebSession
 */
@Configuration
public class SpringWebSessionConfiguration {

	/**
	 * Optional override of default {@link WebSessionIdResolver}.
	 */
	@Autowired(required = false)
	private WebSessionIdResolver webSessionIdResolver;

	/**
	 * Configure a {@link WebSessionManager} using a provided {@link ReactiveSessionRepository}.
	 *
	 * @param repository a bean that implements {@link ReactiveSessionRepository}.
	 * @return a configured {@link WebSessionManager} registered with a preconfigured name.
	 */
	@Bean(WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME)
	public WebSessionManager webSessionManager(ReactiveSessionRepository<? extends Session> repository) {
		SpringSessionWebSessionStore<? extends Session> sessionStore = new SpringSessionWebSessionStore<>(repository);
		DefaultWebSessionManager manager = new DefaultWebSessionManager();
		manager.setSessionStore(sessionStore);

		if (this.webSessionIdResolver != null) {
			manager.setSessionIdResolver(this.webSessionIdResolver);
		}

		return manager;
	}
}


/*
 * Copyright 2014-2018 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 sample.security;

import java.util.Collection;

import sample.data.User;
import sample.data.UserRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

/**
 * @author Rob Winch
 *
 */
@Service
public class UserRepositoryUserDetailsService implements UserDetailsService {
	private final UserRepository userRepository;

	@Autowired
	public UserRepositoryUserDetailsService(UserRepository userRepository) {
		this.userRepository = userRepository;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * org.springframework.security.core.userdetails.UserDetailsService#loadUserByUsername
	 * (java.lang.String)
	 */
	@Override
	public UserDetails loadUserByUsername(String username)
			throws UsernameNotFoundException {
		User user = this.userRepository.findByEmail(username);
		if (user == null) {
			throw new UsernameNotFoundException("Could not find user " + username);
		}
		return new CustomUserDetails(user);
	}

	private static final class CustomUserDetails extends User implements UserDetails {

		private CustomUserDetails(User user) {
			super(user);
		}

		@Override
		public Collection<? extends GrantedAuthority> getAuthorities() {
			return AuthorityUtils.createAuthorityList("ROLE_USER");
		}

		@Override
		public String getUsername() {
			return getEmail();
		}

		@Override
		public boolean isAccountNonExpired() {
			return true;
		}

		@Override
		public boolean isAccountNonLocked() {
			return true;
		}

		@Override
		public boolean isCredentialsNonExpired() {
			return true;
		}

		@Override
		public boolean isEnabled() {
			return true;
		}

		private static final long serialVersionUID = 5639683223516504866L;
	}
}


/*
 * Copyright 2014-2016 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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.springframework.session.security.web.authentication;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.util.Assert;

/**
 * A {@link RememberMeServices} implementation that uses Spring Session backed
 * {@link HttpSession} to provide remember-me service capabilities.
 *
 * @author Vedran Pavic
 * @since 1.3.0
 */
public class SpringSessionRememberMeServices
		implements RememberMeServices, LogoutHandler {

	/**
	 * Remember-me login request attribute name.
	 */
	public static final String REMEMBER_ME_LOGIN_ATTR = SpringSessionRememberMeServices.class
			.getName() + "REMEMBER_ME_LOGIN_ATTR";

	private static final String DEFAULT_REMEMBERME_PARAMETER = "remember-me";

	private static final int THIRTY_DAYS_SECONDS = 2592000;

	private static final Log logger = LogFactory
			.getLog(SpringSessionRememberMeServices.class);

	private String rememberMeParameterName = DEFAULT_REMEMBERME_PARAMETER;

	private boolean alwaysRemember;

	private int validitySeconds = THIRTY_DAYS_SECONDS;

	private String sessionAttrToDeleteOnLoginFail = HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;

	@Override
	public final Authentication autoLogin(HttpServletRequest request,
			HttpServletResponse response) {
		return null;
	}

	@Override
	public final void loginFail(HttpServletRequest request,
			HttpServletResponse response) {
		logout(request);
	}

	@Override
	public final void loginSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication successfulAuthentication) {
		if (!this.alwaysRemember
				&& !rememberMeRequested(request, this.rememberMeParameterName)) {
			logger.debug("Remember-me login not requested.");
			return;
		}
		request.setAttribute(REMEMBER_ME_LOGIN_ATTR, true);
		request.getSession().setMaxInactiveInterval(this.validitySeconds);
	}

	/**
	 * Allows customization of whether a remember-me login has been requested. The default
	 * is to return {@code true} if the configured parameter name has been included in the
	 * request and is set to the value {@code true}.
	 * @param request the request submitted from an interactive login, which may include
	 * additional information indicating that a persistent login is desired.
	 * @param parameter the configured remember-me parameter name.
	 * @return true if the request includes information indicating that a persistent login
	 * has been requested.
	 */
	protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
		String rememberMe = request.getParameter(parameter);
		if (rememberMe != null) {
			if (rememberMe.equalsIgnoreCase("true") || rememberMe.equalsIgnoreCase("on")
					|| rememberMe.equalsIgnoreCase("yes") || rememberMe.equals("1")) {
				return true;
			}
		}
		if (logger.isDebugEnabled()) {
			logger.debug("Did not send remember-me cookie (principal did not set "
					+ "parameter '" + parameter + "')");
		}
		return false;
	}

	/**
	 * Set the name of the parameter which should be checked for to see if a remember-me
	 * has been requested during a login request. This should be the same name you assign
	 * to the checkbox in your login form.
	 * @param rememberMeParameterName the request parameter
	 */
	public void setRememberMeParameterName(String rememberMeParameterName) {
		Assert.hasText(rememberMeParameterName,
				"rememberMeParameterName cannot be empty or null");
		this.rememberMeParameterName = rememberMeParameterName;
	}

	public void setAlwaysRemember(boolean alwaysRemember) {
		this.alwaysRemember = alwaysRemember;
	}

	public void setValiditySeconds(int validitySeconds) {
		this.validitySeconds = validitySeconds;
	}

	@Override
	public void logout(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) {
		logout(request);
	}

	private void logout(HttpServletRequest request) {
		logger.debug("Interactive login attempt was unsuccessful.");
		HttpSession session = request.getSession(false);
		if (session != null) {
			session.removeAttribute(this.sessionAttrToDeleteOnLoginFail);
		}
	}
}


/*
 * Copyright 2014-2018 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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.springframework.session.security;

import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.util.Assert;

/**
 * A {@link SessionRegistry} that retrieves session information from Spring Session,
 * rather than maintaining it itself. This allows concurrent session management with
 * Spring Security in a clustered environment.
 *

 * Relies on being able to derive the same String-based representation of the principal
 * given to {@link #getAllSessions(Object, boolean)} as used by Spring Session in order to
 * look up the user's sessions.
 *

 * Does not support {@link #getAllPrincipals()}, since that information is not available.
 *
 * @param <S> the {@link Session} type.
 * @author Joris Kuipers
 * @author Vedran Pavic
 * @since 1.3
 */
public class SpringSessionBackedSessionRegistry<S extends Session>
		implements SessionRegistry {

	private final FindByIndexNameSessionRepository<S> sessionRepository;

	public SpringSessionBackedSessionRegistry(
			FindByIndexNameSessionRepository<S> sessionRepository) {
		Assert.notNull(sessionRepository, "sessionRepository cannot be null");
		this.sessionRepository = sessionRepository;
	}

	@Override
	public List<Object> getAllPrincipals() {
		throw new UnsupportedOperationException("SpringSessionBackedSessionRegistry does "
				+ "not support retrieving all principals, since Spring Session provides "
				+ "no way to obtain that information");
	}

	@Override
	public List<SessionInformation> getAllSessions(Object principal,
			boolean includeExpiredSessions) {
		Collection<S> sessions = this.sessionRepository
				.findByPrincipalName(name(principal)).values();
		List<SessionInformation> infos = new ArrayList<>();
		for (S session : sessions) {
			if (includeExpiredSessions || !Boolean.TRUE.equals(session
					.getAttribute(SpringSessionBackedSessionInformation.EXPIRED_ATTR))) {
				infos.add(new SpringSessionBackedSessionInformation<>(session,
						this.sessionRepository));
			}
		}
		return infos;
	}

	@Override
	public SessionInformation getSessionInformation(String sessionId) {
		S session = this.sessionRepository.findById(sessionId);
		if (session != null) {
			return new SpringSessionBackedSessionInformation<>(session,
					this.sessionRepository);
		}
		return null;
	}

	/*
	 * This is a no-op, as we don't administer sessions ourselves.
	 */
	@Override
	public void refreshLastRequest(String sessionId) {
	}

	/*
	 * This is a no-op, as we don't administer sessions ourselves.
	 */
	@Override
	public void registerNewSession(String sessionId, Object principal) {
	}

	/*
	 * This is a no-op, as we don't administer sessions ourselves.
	 */
	@Override
	public void removeSessionInformation(String sessionId) {
	}

	/**
	 * Derives a String name for the given principal.
	 *
	 * @param principal as provided by Spring Security
	 * @return name of the principal, or its {@code toString()} representation if no name
	 * could be derived
	 */
	protected String name(Object principal) {
		if (principal instanceof UserDetails) {
			return ((UserDetails) principal).getUsername();
		}
		if (principal instanceof Principal) {
			return ((Principal) principal).getName();
		}
		return principal.toString();
	}

}


/*
 * Copyright 2014-2016 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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.springframework.session.web.context;

import java.util.Arrays;
import java.util.EnumSet;

import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterRegistration.Dynamic;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;

import org.springframework.context.ApplicationContext;
import org.springframework.core.Conventions;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.util.Assert;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.AbstractContextLoaderInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.DelegatingFilterProxy;

/**
 * Registers the {@link DelegatingFilterProxy} to use the springSessionRepositoryFilter
 * before any other registered {@link Filter}. When used with
 * {@link #AbstractHttpSessionApplicationInitializer(Class...)}, it will also register a
 * {@link ContextLoaderListener}. When used with
 * {@link #AbstractHttpSessionApplicationInitializer()}, this class is typically used in
 * addition to a subclass of {@link AbstractContextLoaderInitializer}.
 *
 *

 * By default the {@link DelegatingFilterProxy} is registered with support for
 * asynchronous requests, but can be enabled by overriding
 * {@link #isAsyncSessionSupported()} and {@link #getSessionDispatcherTypes()}.
 *

 *
 *

 * Additional configuration before and after the springSecurityFilterChain can be added by
 * overriding {@link #afterSessionRepositoryFilter(ServletContext)}.
 *

 *
 *
 *
Caveats

 *

 * Subclasses of {@code AbstractDispatcherServletInitializer} will register their filters
 * before any other {@link Filter}. This means that you will typically want to ensure
 * subclasses of {@code AbstractDispatcherServletInitializer} are invoked first. This can
 * be done by ensuring the {@link Order} or {@link Ordered} of
 * {@code AbstractDispatcherServletInitializer} are sooner than subclasses of
 * {@code AbstractSecurityWebApplicationInitializer}.
 *

 *
 * @author Rob Winch
 *
 */
@Order(100)
public abstract class AbstractHttpSessionApplicationInitializer
		implements WebApplicationInitializer {

	private static final String SERVLET_CONTEXT_PREFIX = "org.springframework.web.servlet.FrameworkServlet.CONTEXT.";

	/**
	 * The default name for Spring Session's repository filter.
	 */
	public static final String DEFAULT_FILTER_NAME = "springSessionRepositoryFilter";

	private final Class<?>[] configurationClasses;

	/**
	 * Creates a new instance that assumes the Spring Session configuration is loaded by
	 * some other means than this class. For example, a user might create a
	 * {@link ContextLoaderListener} using a subclass of
	 * {@link AbstractContextLoaderInitializer}.
	 *
	 * @see ContextLoaderListener
	 */
	protected AbstractHttpSessionApplicationInitializer() {
		this.configurationClasses = null;
	}

	/**
	 * Creates a new instance that will instantiate the {@link ContextLoaderListener} with
	 * the specified classes.
	 *
	 * @param configurationClasses {@code @Configuration} classes that will be used to
	 * configure the context
	 */
	protected AbstractHttpSessionApplicationInitializer(
			Class<?>... configurationClasses) {
		this.configurationClasses = configurationClasses;
	}

	@Override
	public void onStartup(ServletContext servletContext) throws ServletException {
		beforeSessionRepositoryFilter(servletContext);
		if (this.configurationClasses != null) {
			AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext();
			rootAppContext.register(this.configurationClasses);
			servletContext.addListener(new ContextLoaderListener(rootAppContext));
		}
		insertSessionRepositoryFilter(servletContext);
		afterSessionRepositoryFilter(servletContext);
	}

	/**
	 * Registers the springSessionRepositoryFilter.
	 * @param servletContext the {@link ServletContext}
	 */
	private void insertSessionRepositoryFilter(ServletContext servletContext) {
		String filterName = DEFAULT_FILTER_NAME;
		DelegatingFilterProxy springSessionRepositoryFilter = new DelegatingFilterProxy(
				filterName);
		String contextAttribute = getWebApplicationContextAttribute();
		if (contextAttribute != null) {
			springSessionRepositoryFilter.setContextAttribute(contextAttribute);
		}
		registerFilter(servletContext, true, filterName, springSessionRepositoryFilter);
	}

	/**
	 * Inserts the provided {@link Filter}s before existing {@link Filter}s using default
	 * generated names, {@link #getSessionDispatcherTypes()}, and
	 * {@link #isAsyncSessionSupported()}.
	 *
	 * @param servletContext the {@link ServletContext} to use
	 * @param filters the {@link Filter}s to register
	 */
	protected final void insertFilters(ServletContext servletContext, Filter... filters) {
		registerFilters(servletContext, true, filters);
	}

	/**
	 * Inserts the provided {@link Filter}s after existing {@link Filter}s using default
	 * generated names, {@link #getSessionDispatcherTypes()}, and
	 * {@link #isAsyncSessionSupported()}.
	 *
	 * @param servletContext the {@link ServletContext} to use
	 * @param filters the {@link Filter}s to register
	 */
	protected final void appendFilters(ServletContext servletContext, Filter... filters) {
		registerFilters(servletContext, false, filters);
	}

	/**
	 * Registers the provided {@link Filter}s using default generated names,
	 * {@link #getSessionDispatcherTypes()}, and {@link #isAsyncSessionSupported()}.
	 *
	 * @param servletContext the {@link ServletContext} to use
	 * @param insertBeforeOtherFilters if true, will insert the provided {@link Filter}s
	 * before other {@link Filter}s. Otherwise, will insert the {@link Filter}s after
	 * other {@link Filter}s.
	 * @param filters the {@link Filter}s to register
	 */
	private void registerFilters(ServletContext servletContext,
			boolean insertBeforeOtherFilters, Filter... filters) {
		Assert.notEmpty(filters, "filters cannot be null or empty");

		for (Filter filter : filters) {
			if (filter == null) {
				throw new IllegalArgumentException(
						"filters cannot contain null values. Got "
								+ Arrays.asList(filters));
			}
			String filterName = Conventions.getVariableName(filter);
			registerFilter(servletContext, insertBeforeOtherFilters, filterName, filter);
		}
	}

	/**
	 * Registers the provided filter using the {@link #isAsyncSessionSupported()} and
	 * {@link #getSessionDispatcherTypes()}.
	 *
	 * @param servletContext the servlet context
	 * @param insertBeforeOtherFilters should this Filter be inserted before or after
	 * other {@link Filter}
	 * @param filterName the filter name
	 * @param filter the filter
	 */
	private void registerFilter(ServletContext servletContext,
			boolean insertBeforeOtherFilters, String filterName, Filter filter) {
		Dynamic registration = servletContext.addFilter(filterName, filter);
		if (registration == null) {
			throw new IllegalStateException(
					"Duplicate Filter registration for '" + filterName
							+ "'. Check to ensure the Filter is only configured once.");
		}
		registration.setAsyncSupported(isAsyncSessionSupported());
		EnumSet<DispatcherType> dispatcherTypes = getSessionDispatcherTypes();
		registration.addMappingForUrlPatterns(dispatcherTypes, !insertBeforeOtherFilters,
				"/*");
	}

	/**
	 * Returns the {@link DelegatingFilterProxy#getContextAttribute()} or null if the
	 * parent {@link ApplicationContext} should be used. The default behavior is to use
	 * the parent {@link ApplicationContext}.
	 *
	 *

	 * If {@link #getDispatcherWebApplicationContextSuffix()} is non-null the
	 * {@link WebApplicationContext} for the Dispatcher will be used. This means the child
	 * {@link ApplicationContext} is used to look up the springSessionRepositoryFilter
	 * bean.
	 *

	 *
	 * @return the {@link DelegatingFilterProxy#getContextAttribute()} or null if the
	 * parent {@link ApplicationContext} should be used
	 */
	private String getWebApplicationContextAttribute() {
		String dispatcherServletName = getDispatcherWebApplicationContextSuffix();
		if (dispatcherServletName == null) {
			return null;
		}
		return SERVLET_CONTEXT_PREFIX + dispatcherServletName;
	}

	/**
	 * Return the {@code <servlet-name>} to use the DispatcherServlet's
	 * {@link WebApplicationContext} to find the {@link DelegatingFilterProxy} or null to
	 * use the parent {@link ApplicationContext}.
	 *
	 *

	 * For example, if you are using AbstractDispatcherServletInitializer or
	 * AbstractAnnotationConfigDispatcherServletInitializer and using the provided Servlet
	 * name, you can return "dispatcher" from this method to use the DispatcherServlet's
	 * {@link WebApplicationContext}.
	 *

	 *
	 * @return the {@code <servlet-name>} of the DispatcherServlet to use its
	 * {@link WebApplicationContext} or null (default) to use the parent
	 * {@link ApplicationContext}.
	 */
	protected String getDispatcherWebApplicationContextSuffix() {
		return null;
	}

	/**
	 * Invoked before the springSessionRepositoryFilter is added.
	 * @param servletContext the {@link ServletContext}
	 */
	protected void beforeSessionRepositoryFilter(ServletContext servletContext) {

	}

	/**
	 * Invoked after the springSessionRepositoryFilter is added.
	 * @param servletContext the {@link ServletContext}
	 */
	protected void afterSessionRepositoryFilter(ServletContext servletContext) {

	}

	/**
	 * Get the {@link DispatcherType} for the springSessionRepositoryFilter.
	 * @return the {@link DispatcherType} for the filter
	 */
	protected EnumSet<DispatcherType> getSessionDispatcherTypes() {
		return EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR,
				DispatcherType.ASYNC);
	}

	/**
	 * Determine if the springSessionRepositoryFilter should be marked as supporting
	 * asynch. Default is true.
	 *
	 * @return true if springSessionRepositoryFilter should be marked as supporting asynch
	 */
	protected boolean isAsyncSessionSupported() {
		return true;
	}
}


/*
 * Copyright 2014-2018 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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.springframework.session.security;

import java.util.Date;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;

/**
 * Ensures that calling {@link #expireNow()} propagates to Spring Session, since this
 * session information contains only derived data and is not the authoritative source.
 *
 * @param <S> the {@link Session} type.
 * @author Joris Kuipers
 * @author Vedran Pavic
 * @since 1.3
 */
class SpringSessionBackedSessionInformation<S extends Session>
		extends SessionInformation {

	static final String EXPIRED_ATTR = SpringSessionBackedSessionInformation.class
			.getName() + ".EXPIRED";

	private static final Log logger = LogFactory
			.getLog(SpringSessionBackedSessionInformation.class);

	private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";

	private final SessionRepository<S> sessionRepository;

	SpringSessionBackedSessionInformation(S session,
			SessionRepository<S> sessionRepository) {
		super(resolvePrincipal(session), session.getId(),
				Date.from(session.getLastAccessedTime()));
		this.sessionRepository = sessionRepository;
		Boolean expired = session.getAttribute(EXPIRED_ATTR);
		if (Boolean.TRUE.equals(expired)) {
			super.expireNow();
		}
	}

	/**
	 * Tries to determine the principal's name from the given Session.
	 *
	 * @param session the session
	 * @return the principal's name, or empty String if it couldn't be determined
	 */
	private static String resolvePrincipal(Session session) {
		String principalName = session
				.getAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
		if (principalName != null) {
			return principalName;
		}
		SecurityContext securityContext = session
				.getAttribute(SPRING_SECURITY_CONTEXT);
		if (securityContext != null
				&& securityContext.getAuthentication() != null) {
			return securityContext.getAuthentication().getName();
		}
		return "";
	}

	@Override
	public void expireNow() {
		if (logger.isDebugEnabled()) {
			logger.debug("Expiring session " + getSessionId() + " for user '"
					+ getPrincipal() + "', presumably because maximum allowed concurrent "
					+ "sessions was exceeded");
		}
		super.expireNow();
		S session = this.sessionRepository.findById(getSessionId());
		if (session != null) {
			session.setAttribute(EXPIRED_ATTR, Boolean.TRUE);
			this.sessionRepository.save(session);
		}
		else {
			logger.info("Could not find Session with id " + getSessionId()
					+ " to mark as expired");
		}
	}

}


/*
 * Copyright 2014-2016 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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.springframework.session.web.http;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Allows for easily ensuring that a request is only invoked once per request. This is a
 * simplified version of spring-web's OncePerRequestFilter and copied to reduce the foot
 * print required to use the session support.
 *
 * @author Rob Winch
 * @since 1.0
 */
abstract class OncePerRequestFilter implements Filter {
	/**
	 * Suffix that gets appended to the filter name for the "already filtered" request
	 * attribute.
	 */
	public static final String ALREADY_FILTERED_SUFFIX = ".FILTERED";

	private String alreadyFilteredAttributeName = getClass().getName()
			.concat(ALREADY_FILTERED_SUFFIX);

	/**
	 * This {@code doFilter} implementation stores a request attribute for
	 * "already filtered", proceeding without filtering again if the attribute is already
	 * there.
	 * @param request the request
	 * @param response the response
	 * @param filterChain the filter chain
	 * @throws ServletException if request is not HTTP request
	 * @throws IOException in case of I/O operation exception
	 */
	@Override
	public final void doFilter(ServletRequest request, ServletResponse response,
			FilterChain filterChain) throws ServletException, IOException {

		if (!(request instanceof HttpServletRequest)
				|| !(response instanceof HttpServletResponse)) {
			throw new ServletException(
					"OncePerRequestFilter just supports HTTP requests");
		}
		HttpServletRequest httpRequest = (HttpServletRequest) request;
		HttpServletResponse httpResponse = (HttpServletResponse) response;
		boolean hasAlreadyFilteredAttribute = request
				.getAttribute(this.alreadyFilteredAttributeName) != null;

		if (hasAlreadyFilteredAttribute) {

			// Proceed without invoking this filter...
			filterChain.doFilter(request, response);
		}
		else {
			// Do invoke this filter...
			request.setAttribute(this.alreadyFilteredAttributeName, Boolean.TRUE);
			try {
				doFilterInternal(httpRequest, httpResponse, filterChain);
			}
			finally {
				// Remove the "already filtered" request attribute for this request.
				request.removeAttribute(this.alreadyFilteredAttributeName);
			}
		}
	}

	/**
	 * Same contract as for {@code doFilter}, but guaranteed to be just invoked once per
	 * request within a single request thread.
	 * <p>
	 * Provides HttpServletRequest and HttpServletResponse arguments instead of the
	 * default ServletRequest and ServletResponse ones.
	 *
	 * @param request the request
	 * @param response the response
	 * @param filterChain the FilterChain
	 * @throws ServletException thrown when a non-I/O exception has occurred
	 * @throws IOException thrown when an I/O exception of some sort has occurred
	 * @see Filter#doFilter
	 */
	protected abstract void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain filterChain)
					throws ServletException, IOException;

	@Override
	public void init(FilterConfig config) {
	}

	@Override
	public void destroy() {
	}
}