diff --git a/spring-session/src/main/java/org/springframework/session/SafeRetrievingSessionRepository.java b/spring-session/src/main/java/org/springframework/session/SafeRetrievingSessionRepository.java new file mode 100644 index 000000000..35a35f88f --- /dev/null +++ b/spring-session/src/main/java/org/springframework/session/SafeRetrievingSessionRepository.java @@ -0,0 +1,185 @@ +/* + * 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 + * + * 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.springframework.session; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.Assert; + +/** + * A {@link FindByIndexNameSessionRepository} that decorates the delegating + * {@link SessionRepository}, ignoring the configured set of exceptions during retrieval + * of a {@link Session} from the underlying store. Useful with handling exceptions + * during session deserialization (for example, when serialization UID changes) in + * scenarios where {@link SessionRepository} consumer wants to treat the session that + * wasn't deserializable as non-existing. + *

+ * By default, this implementation will delete the session whose retrieval using + * {@link #getSession(String)} has failed with the configured ignored exception. This + * behavior can be changed using {@link #setDeleteOnIgnoredException(boolean)} method. + * + * @param the type of {@link Session} being managed + * @author Vedran Pavic + * @since 1.3.0 + */ +public class SafeRetrievingSessionRepository + implements FindByIndexNameSessionRepository { + + private static final Log logger = LogFactory.getLog(SafeRetrievingSessionRepository.class); + + private final FindByIndexNameSessionRepository delegate; + + private final Set> ignoredExceptions; + + private boolean deleteOnIgnoredException = true; + + private boolean logIgnoredException; + + /** + * Create a new {@link SafeRetrievingSessionRepository} instance backed by a + * {@link FindByIndexNameSessionRepository} delegate. + * @param delegate the {@link FindByIndexNameSessionRepository} delegate + * @param ignoredExceptions the set of exceptions to ignore + */ + public SafeRetrievingSessionRepository( + FindByIndexNameSessionRepository delegate, + Set> ignoredExceptions) { + Assert.notNull(delegate, "Delegate must not be null"); + Assert.notEmpty(ignoredExceptions, "Ignored exceptions must not be empty"); + this.delegate = delegate; + this.ignoredExceptions = ignoredExceptions; + } + + /** + * Create a new {@link SafeRetrievingSessionRepository} instance backed by a + * {@link SessionRepository} delegate. + * @param delegate the {@link SessionRepository} delegate + * @param ignoredExceptions the set of exceptions to ignore + */ + public SafeRetrievingSessionRepository(SessionRepository delegate, + Set> ignoredExceptions) { + this(new FindByIndexNameSessionRepositoryAdapter(delegate), ignoredExceptions); + } + + /** + * Set whether session should be deleted after ignored exception has occurred during + * retrieval. + * @param deleteOnIgnoredException the flag to indicate whether session should be + * deleted + */ + public void setDeleteOnIgnoredException(boolean deleteOnIgnoredException) { + this.deleteOnIgnoredException = deleteOnIgnoredException; + } + + /** + * Set whether ignored exception should be logged. + * @param logIgnoredException the flag to indicate whether to log ignored exceptions + */ + public void setLogIgnoredException(boolean logIgnoredException) { + this.logIgnoredException = logIgnoredException; + } + + public S createSession() { + return this.delegate.createSession(); + } + + public void save(S session) { + this.delegate.save(session); + } + + public S getSession(String id) { + try { + return this.delegate.getSession(id); + } + catch (RuntimeException e) { + if (isIgnoredException(e)) { + if (this.logIgnoredException) { + logger.warn("Error occurred while retrieving session " + id + ": " + + e); + } + if (this.deleteOnIgnoredException) { + this.delegate.delete(id); + } + return null; + } + throw e; + } + } + + public void delete(String id) { + this.delegate.delete(id); + } + + public Map findByIndexNameAndIndexValue(String indexName, String indexValue) { + try { + return this.delegate.findByIndexNameAndIndexValue(indexName, indexValue); + } + catch (RuntimeException e) { + if (this.logIgnoredException) { + logger.warn("Error occurred while retrieving sessions with index name '" + + indexName + "' and value '" + indexValue + "': " + e); + } + if (isIgnoredException(e)) { + return Collections.emptyMap(); + } + throw e; + } + } + + public boolean isIgnoredException(RuntimeException e) { + return this.ignoredExceptions.contains(e.getClass()); + } + + private static class FindByIndexNameSessionRepositoryAdapter + implements FindByIndexNameSessionRepository { + + private final SessionRepository delegate; + + FindByIndexNameSessionRepositoryAdapter(SessionRepository delegate) { + Assert.notNull(delegate, "Delegate must not be null"); + this.delegate = delegate; + } + + public S createSession() { + return this.delegate.createSession(); + } + + public void save(S session) { + this.delegate.save(session); + } + + public S getSession(String id) { + return this.delegate.getSession(id); + } + + public void delete(String id) { + this.delegate.delete(id); + } + + public Map findByIndexNameAndIndexValue(String indexName, + String indexValue) { + throw new UnsupportedOperationException(); + } + + } + +} diff --git a/spring-session/src/test/java/org/springframework/session/SafeRetrievingSessionRepositoryTests.java b/spring-session/src/test/java/org/springframework/session/SafeRetrievingSessionRepositoryTests.java new file mode 100644 index 000000000..ee3fbf86e --- /dev/null +++ b/spring-session/src/test/java/org/springframework/session/SafeRetrievingSessionRepositoryTests.java @@ -0,0 +1,213 @@ +/* + * 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 + * + * 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.springframework.session; + +import java.util.Collections; +import java.util.UUID; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Tests for {@link SafeRetrievingSessionRepository}. + * + * @author Vedran Pavic + */ +@RunWith(MockitoJUnitRunner.class) +public class SafeRetrievingSessionRepositoryTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Mock + private FindByIndexNameSessionRepository delegate; + + @Test + public void createWithNullDelegateFails() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Delegate must not be null"); + new SafeRetrievingSessionRepository(null, + Collections.>singleton(RuntimeException.class)); + } + + @Test + public void createWithNullIgnoredExceptionsFails() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Ignored exceptions must not be empty"); + new SafeRetrievingSessionRepository(this.delegate, null); + } + + @Test + public void createWithEmptyIgnoredExceptionsFails() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Ignored exceptions must not be empty"); + new SafeRetrievingSessionRepository(this.delegate, + Collections.>emptySet()); + } + + @Test + public void createSession() { + SafeRetrievingSessionRepository repository = + new SafeRetrievingSessionRepository(this.delegate, + Collections.>singleton(RuntimeException.class)); + repository.createSession(); + verify(this.delegate, times(1)).createSession(); + verifyZeroInteractions(this.delegate); + } + + @Test + public void saveSession() { + SafeRetrievingSessionRepository repository = + new SafeRetrievingSessionRepository(this.delegate, + Collections.>singleton(RuntimeException.class)); + repository.save(new MapSession()); + verify(this.delegate, times(1)).save(any(ExpiringSession.class)); + verifyZeroInteractions(this.delegate); + } + + @Test + public void getSession() { + ExpiringSession session = mock(ExpiringSession.class); + given(this.delegate.getSession(anyString())).willReturn(session); + SafeRetrievingSessionRepository repository = + new SafeRetrievingSessionRepository(this.delegate, + Collections.>singleton(RuntimeException.class)); + assertThat(repository.getSession(UUID.randomUUID().toString())) + .isEqualTo(session); + verify(this.delegate, times(1)).getSession(anyString()); + verifyZeroInteractions(this.delegate); + } + + @Test + @SuppressWarnings("unchecked") + public void getSessionThrowsIgnoredException() { + given(this.delegate.getSession(anyString())).willThrow(RuntimeException.class); + SafeRetrievingSessionRepository repository = + new SafeRetrievingSessionRepository(this.delegate, + Collections.>singleton(RuntimeException.class)); + assertThat(repository.getSession(UUID.randomUUID().toString())).isNull(); + verify(this.delegate, times(1)).getSession(anyString()); + verify(this.delegate, times(1)).delete(anyString()); + verifyZeroInteractions(this.delegate); + } + + @Test + @SuppressWarnings("unchecked") + public void getSessionThrowsIgnoredExceptionWithDeletionDisabled() { + given(this.delegate.getSession(anyString())).willThrow(RuntimeException.class); + SafeRetrievingSessionRepository repository = + new SafeRetrievingSessionRepository(this.delegate, + Collections.>singleton(RuntimeException.class)); + repository.setDeleteOnIgnoredException(false); + assertThat(repository.getSession(UUID.randomUUID().toString())).isNull(); + verify(this.delegate, times(1)).getSession(anyString()); + verifyZeroInteractions(this.delegate); + } + + @Test + @SuppressWarnings("unchecked") + public void getSessionThrowsNotIgnoredException() { + this.thrown.expect(RuntimeException.class); + given(this.delegate.getSession(anyString())).willThrow(RuntimeException.class); + SafeRetrievingSessionRepository repository = + new SafeRetrievingSessionRepository(this.delegate, + Collections.>singleton(IllegalStateException.class)); + repository.getSession(UUID.randomUUID().toString()); + verify(this.delegate, times(1)).getSession(anyString()); + verifyZeroInteractions(this.delegate); + } + + @Test + public void deleteSession() { + SafeRetrievingSessionRepository repository = + new SafeRetrievingSessionRepository(this.delegate, + Collections.>singleton(RuntimeException.class)); + repository.delete(UUID.randomUUID().toString()); + verify(this.delegate, times(1)).delete(anyString()); + verifyZeroInteractions(this.delegate); + } + + @Test + public void findByIndexNameAndIndexValue() { + ExpiringSession session = mock(ExpiringSession.class); + given(this.delegate.findByIndexNameAndIndexValue(anyString(), anyString())) + .willReturn(Collections.singletonMap("name", session)); + SafeRetrievingSessionRepository repository = + new SafeRetrievingSessionRepository(this.delegate, + Collections.>singleton(RuntimeException.class)); + assertThat(repository.findByIndexNameAndIndexValue("name", "value").get("name")) + .isEqualTo(session); + verify(this.delegate, times(1)).findByIndexNameAndIndexValue(anyString(), + anyString()); + verifyZeroInteractions(this.delegate); + } + + @Test + @SuppressWarnings("unchecked") + public void findByIndexNameAndIndexValueThrowsIgnoredException() { + given(this.delegate.findByIndexNameAndIndexValue(anyString(), anyString())) + .willThrow(RuntimeException.class); + SafeRetrievingSessionRepository repository = + new SafeRetrievingSessionRepository(this.delegate, + Collections.>singleton(RuntimeException.class)); + assertThat(repository.findByIndexNameAndIndexValue("name", "value")).isEmpty(); + verify(this.delegate, times(1)).findByIndexNameAndIndexValue(anyString(), + anyString()); + verifyZeroInteractions(this.delegate); + } + + @Test + @SuppressWarnings("unchecked") + public void findByIndexNameAndIndexValueThrowsNotIgnoredException() { + this.thrown.expect(RuntimeException.class); + given(this.delegate.findByIndexNameAndIndexValue(anyString(), anyString())) + .willThrow(RuntimeException.class); + SafeRetrievingSessionRepository repository = + new SafeRetrievingSessionRepository(this.delegate, + Collections.>singleton(IllegalStateException.class)); + repository.findByIndexNameAndIndexValue("name", "value"); + verify(this.delegate, times(1)).findByIndexNameAndIndexValue(anyString(), + anyString()); + verifyZeroInteractions(this.delegate); + } + + @Test + @SuppressWarnings("unchecked") + public void findByIndexNameAndIndexValueOnSessionRepositoryThrowsException() { + this.thrown.expect(UnsupportedOperationException.class); + SessionRepository delegate = mock(SessionRepository.class); + SafeRetrievingSessionRepository repository = + new SafeRetrievingSessionRepository(delegate, + Collections.>singleton(IllegalStateException.class)); + repository.findByIndexNameAndIndexValue("name", "value"); + verifyZeroInteractions(delegate); + } + +}