Skip to content

Commit

Permalink
Improve support for Hazelcast client-server topology
Browse files Browse the repository at this point in the history
This commit improves support for use of Spring Session with Hazelcast's client-server topology by ensuring SessionUpdateEntryProcessor is easier to serialize to the cluster. This is done by refactoring SessionUpdateEntryProcessor from static inner class of HazelcastSessionRepository to a dedicated class, therefore minimizing the dependencies to other Spring Session components.

Closes spring-projectsgh-1101
  • Loading branch information
vpavic committed Jul 17, 2018
1 parent 936fc85 commit 83cbff5
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 81 deletions.
1 change: 1 addition & 0 deletions spring-session-hazelcast/spring-session-hazelcast.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ dependencies {
testCompile "org.springframework.security:spring-security-core"

integrationTestCompile "com.hazelcast:hazelcast-client"
integrationTestCompile "org.testcontainers:testcontainers"
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,17 @@

import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;
import com.hazelcast.instance.HazelcastInstanceProxy;
import org.junit.Assume;
import org.junit.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.session.hazelcast.HazelcastSessionRepository.HazelcastSession;

Expand All @@ -34,8 +42,10 @@
*/
public abstract class AbstractHazelcastRepositoryITests {

private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";

@Autowired
private HazelcastInstance hazelcast;
private HazelcastInstance hazelcastInstance;

@Autowired
private HazelcastSessionRepository repository;
Expand All @@ -45,7 +55,7 @@ public void createAndDestroySession() {
HazelcastSession sessionToSave = this.repository.createSession();
String sessionId = sessionToSave.getId();

IMap<String, MapSession> hazelcastMap = this.hazelcast
IMap<String, MapSession> hazelcastMap = this.hazelcastInstance
.getMap(HazelcastSessionRepository.DEFAULT_SESSION_MAP_NAME);

assertThat(hazelcastMap.size()).isEqualTo(0);
Expand All @@ -61,7 +71,7 @@ public void createAndDestroySession() {
}

@Test
public void changeSessionIdWhenOnlyChangeId() throws Exception {
public void changeSessionIdWhenOnlyChangeId() {
String attrName = "changeSessionId";
String attrValue = "changeSessionId-value";
HazelcastSession toSave = this.repository.createSession();
Expand Down Expand Up @@ -90,7 +100,7 @@ public void changeSessionIdWhenOnlyChangeId() throws Exception {
}

@Test
public void changeSessionIdWhenChangeTwice() throws Exception {
public void changeSessionIdWhenChangeTwice() {
HazelcastSession toSave = this.repository.createSession();

this.repository.save(toSave);
Expand All @@ -109,7 +119,7 @@ public void changeSessionIdWhenChangeTwice() throws Exception {
}

@Test
public void changeSessionIdWhenSetAttributeOnChangedSession() throws Exception {
public void changeSessionIdWhenSetAttributeOnChangedSession() {
String attrName = "changeSessionId";
String attrValue = "changeSessionId-value";

Expand Down Expand Up @@ -138,10 +148,7 @@ public void changeSessionIdWhenSetAttributeOnChangedSession() throws Exception {
}

@Test
public void changeSessionIdWhenHasNotSaved() throws Exception {
String attrName = "changeSessionId";
String attrValue = "changeSessionId-value";

public void changeSessionIdWhenHasNotSaved() {
HazelcastSession toSave = this.repository.createSession();
String originalId = toSave.getId();
toSave.changeSessionId();
Expand All @@ -167,4 +174,57 @@ public void attemptToUpdateSessionAfterDelete() {
assertThat(this.repository.findById(sessionId)).isNull();
}

@Test
public void createAndUpdateSession() {
HazelcastSession session = this.repository.createSession();
String sessionId = session.getId();

this.repository.save(session);

session = this.repository.findById(sessionId);
session.setAttribute("attributeName", "attributeValue");

this.repository.save(session);

assertThat(this.repository.findById(sessionId)).isNotNull();
}

@Test
public void createSessionWithSecurityContextAndFindById() {
HazelcastSession session = this.repository.createSession();
String sessionId = session.getId();

Authentication authentication = new UsernamePasswordAuthenticationToken(
"saves-" + System.currentTimeMillis(), "password",
AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
session.setAttribute(SPRING_SECURITY_CONTEXT, securityContext);

this.repository.save(session);

assertThat(this.repository.findById(sessionId)).isNotNull();
}

@Test
public void createSessionWithSecurityContextAndFindByPrincipal() {
Assume.assumeTrue("Hazelcast runs in embedded server topology",
this.hazelcastInstance instanceof HazelcastInstanceProxy);

HazelcastSession session = this.repository.createSession();

String username = "saves-" + System.currentTimeMillis();
Authentication authentication = new UsernamePasswordAuthenticationToken(username,
"password", AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
session.setAttribute(SPRING_SECURITY_CONTEXT, securityContext);

this.repository.save(session);

assertThat(this.repository.findByIndexNameAndIndexValue(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username))
.isNotNull();
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 the original author or authors.
* 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.
Expand All @@ -22,14 +22,17 @@
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.runner.RunWith;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.GenericContainer;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.util.SocketUtils;

/**
* Integration tests that check the underlying data source - in this case Hazelcast
Expand All @@ -44,30 +47,37 @@
@WebAppConfiguration
public class HazelcastClientRepositoryITests extends AbstractHazelcastRepositoryITests {

private static final int PORT = SocketUtils.findAvailableTcpPort();

private static HazelcastInstance hazelcastInstance;
private static GenericContainer container = new GenericContainer<>(
"hazelcast/hazelcast:3.9.4")
.withExposedPorts(5701)
.withEnv("JAVA_OPTS",
"-Dhazelcast.config=/opt/hazelcast/config_ext/hazelcast.xml")
.withClasspathResourceMapping("/hazelcast-server.xml",
"/opt/hazelcast/config_ext/hazelcast.xml",
BindMode.READ_ONLY);

@BeforeClass
public static void setup() {
hazelcastInstance = HazelcastITestUtils.embeddedHazelcastServer(PORT);
public static void setUpClass() {
container.start();
}

@AfterClass
public static void teardown() {
if (hazelcastInstance != null) {
hazelcastInstance.shutdown();
}
public static void tearDownClass() {
container.stop();
}

@Configuration
@EnableHazelcastHttpSession
static class HazelcastSessionConfig {

@Bean
public HazelcastInstance embeddedHazelcastClient() {
public HazelcastInstance hazelcastInstance() {
ClientConfig clientConfig = new ClientConfig();
clientConfig.getNetworkConfig().addAddress("127.0.0.1:" + PORT);
clientConfig.getNetworkConfig().addAddress(container.getContainerIpAddress()
+ ":" + container.getFirstMappedPort());
clientConfig.getUserCodeDeploymentConfig().setEnabled(true)
.addClass(Session.class).addClass(MapSession.class)
.addClass(SessionUpdateEntryProcessor.class);
return HazelcastClient.newHazelcastClient(clientConfig);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 the original author or authors.
* 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.
Expand Down Expand Up @@ -43,7 +43,7 @@ public class HazelcastServerRepositoryITests extends AbstractHazelcastRepository
static class HazelcastSessionConfig {

@Bean
public HazelcastInstance embeddedHazelcastServer() {
public HazelcastInstance hazelcastInstance() {
return HazelcastITestUtils.embeddedHazelcastServer();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<hazelcast xmlns="http://www.hazelcast.com/schema/config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.9.xsd">

<user-code-deployment enabled="true">
<class-cache-mode>ETERNAL</class-cache-mode>
<provider-mode>LOCAL_AND_CACHED_CLASSES</provider-mode>
</user-code-deployment>

</hazelcast>
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@
import com.hazelcast.core.EntryEvent;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;
import com.hazelcast.map.AbstractEntryProcessor;
import com.hazelcast.map.EntryProcessor;
import com.hazelcast.map.listener.EntryAddedListener;
import com.hazelcast.map.listener.EntryEvictedListener;
import com.hazelcast.map.listener.EntryRemovedListener;
Expand Down Expand Up @@ -452,59 +450,4 @@ private void flushImmediateIfNecessary() {

}

/**
* Hazelcast {@link EntryProcessor} responsible for handling updates to session.
*
* @since 2.0.0
* @see #save(HazelcastSession)
*/
private static final class SessionUpdateEntryProcessor
extends AbstractEntryProcessor<String, MapSession> {

private Instant lastAccessedTime;

private Duration maxInactiveInterval;

private Map<String, Object> delta;

@Override
public Object process(Map.Entry<String, MapSession> entry) {
MapSession value = entry.getValue();
if (value == null) {
return Boolean.FALSE;
}
if (this.lastAccessedTime != null) {
value.setLastAccessedTime(this.lastAccessedTime);
}
if (this.maxInactiveInterval != null) {
value.setMaxInactiveInterval(this.maxInactiveInterval);
}
if (this.delta != null) {
for (final Map.Entry<String, Object> attribute : this.delta.entrySet()) {
if (attribute.getValue() != null) {
value.setAttribute(attribute.getKey(), attribute.getValue());
}
else {
value.removeAttribute(attribute.getKey());
}
}
}
entry.setValue(value);
return Boolean.TRUE;
}

public void setLastAccessedTime(Instant lastAccessedTime) {
this.lastAccessedTime = lastAccessedTime;
}

public void setMaxInactiveInterval(Duration maxInactiveInterval) {
this.maxInactiveInterval = maxInactiveInterval;
}

public void setDelta(Map<String, Object> delta) {
this.delta = delta;
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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
*
* 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.hazelcast;

import java.time.Duration;
import java.time.Instant;
import java.util.Map;

import com.hazelcast.map.AbstractEntryProcessor;
import com.hazelcast.map.EntryProcessor;

import org.springframework.session.MapSession;

/**
* Hazelcast {@link EntryProcessor} responsible for handling updates to session.
*
* @author Vedran Pavic
* @since 2.0.5
* @see HazelcastSessionRepository#save(HazelcastSessionRepository.HazelcastSession)
*/
class SessionUpdateEntryProcessor extends AbstractEntryProcessor<String, MapSession> {

private Instant lastAccessedTime;

private Duration maxInactiveInterval;

private Map<String, Object> delta;

@Override
public Object process(Map.Entry<String, MapSession> entry) {
MapSession value = entry.getValue();
if (value == null) {
return Boolean.FALSE;
}
if (this.lastAccessedTime != null) {
value.setLastAccessedTime(this.lastAccessedTime);
}
if (this.maxInactiveInterval != null) {
value.setMaxInactiveInterval(this.maxInactiveInterval);
}
if (this.delta != null) {
for (final Map.Entry<String, Object> attribute : this.delta.entrySet()) {
if (attribute.getValue() != null) {
value.setAttribute(attribute.getKey(), attribute.getValue());
}
else {
value.removeAttribute(attribute.getKey());
}
}
}
entry.setValue(value);
return Boolean.TRUE;
}

void setLastAccessedTime(Instant lastAccessedTime) {
this.lastAccessedTime = lastAccessedTime;
}

void setMaxInactiveInterval(Duration maxInactiveInterval) {
this.maxInactiveInterval = maxInactiveInterval;
}

void setDelta(Map<String, Object> delta) {
this.delta = delta;
}

}

0 comments on commit 83cbff5

Please sign in to comment.