Introduce SessionIdGenerationStrategy

Closes gh-11
This commit is contained in:
Marcus Da Coregio
2023-03-07 17:55:18 -03:00
committed by Marcus Hert Da Coregio
parent 2e1275333a
commit d547b33962
39 changed files with 1074 additions and 30 deletions

View File

@@ -31,6 +31,8 @@ import org.springframework.session.MapSession;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.SessionIdGenerationStrategy;
import org.springframework.session.UuidSessionIdGenerationStrategy;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -61,6 +63,8 @@ public class ReactiveRedisSessionRepository
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private SessionIdGenerationStrategy sessionIdGenerationStrategy = UuidSessionIdGenerationStrategy.getInstance();
/**
* Create a new {@link ReactiveRedisSessionRepository} instance.
* @param sessionRedisOperations the {@link ReactiveRedisOperations} to use for
@@ -120,7 +124,7 @@ public class ReactiveRedisSessionRepository
@Override
public Mono<RedisSession> createSession() {
return Mono.defer(() -> {
MapSession cached = new MapSession();
MapSession cached = new MapSession(this.sessionIdGenerationStrategy);
cached.setMaxInactiveInterval(this.defaultMaxInactiveInterval);
RedisSession session = new RedisSession(cached, true);
return Mono.just(session);
@@ -167,6 +171,16 @@ public class ReactiveRedisSessionRepository
return this.namespace + "sessions:" + sessionId;
}
/**
* Set the {@link SessionIdGenerationStrategy} to use to generate session ids.
* @param sessionIdGenerationStrategy the {@link SessionIdGenerationStrategy} to use
* @since 3.2
*/
public void setSessionIdGenerationStrategy(SessionIdGenerationStrategy sessionIdGenerationStrategy) {
Assert.notNull(sessionIdGenerationStrategy, "sessionIdGenerationStrategy cannot be null");
this.sessionIdGenerationStrategy = sessionIdGenerationStrategy;
}
/**
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
* basis for its mapping. It keeps track of any attributes that have changed. When
@@ -206,7 +220,9 @@ public class ReactiveRedisSessionRepository
@Override
public String changeSessionId() {
return this.cached.changeSessionId();
String newSessionId = ReactiveRedisSessionRepository.this.sessionIdGenerationStrategy.generate();
this.cached.setId(newSessionId);
return newSessionId;
}
@Override

View File

@@ -52,6 +52,8 @@ import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.SessionIdGenerationStrategy;
import org.springframework.session.UuidSessionIdGenerationStrategy;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionDestroyedEvent;
@@ -322,6 +324,8 @@ public class RedisIndexedSessionRepository
private ThreadPoolTaskScheduler taskScheduler;
private SessionIdGenerationStrategy sessionIdGenerationStrategy = UuidSessionIdGenerationStrategy.getInstance();
/**
* Creates a new instance. For an example, refer to the class level javadoc.
* @param sessionRedisOperations the {@link RedisOperations} to use for managing the
@@ -547,7 +551,7 @@ public class RedisIndexedSessionRepository
@Override
public RedisSession createSession() {
MapSession cached = new MapSession();
MapSession cached = new MapSession(this.sessionIdGenerationStrategy);
cached.setMaxInactiveInterval(this.defaultMaxInactiveInterval);
RedisSession session = new RedisSession(cached, true);
session.flushImmediateIfNecessary();
@@ -716,6 +720,16 @@ public class RedisIndexedSessionRepository
return RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName;
}
/**
* Set the {@link SessionIdGenerationStrategy} to use to generate session ids.
* @param sessionIdGenerationStrategy the {@link SessionIdGenerationStrategy} to use
* @since 3.2
*/
public void setSessionIdGenerationStrategy(SessionIdGenerationStrategy sessionIdGenerationStrategy) {
Assert.notNull(sessionIdGenerationStrategy, "sessionIdGenerationStrategy cannot be null");
this.sessionIdGenerationStrategy = sessionIdGenerationStrategy;
}
/**
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
* basis for its mapping. It keeps track of any attributes that have changed. When
@@ -780,7 +794,9 @@ public class RedisIndexedSessionRepository
@Override
public String changeSessionId() {
return this.cached.changeSessionId();
String newSessionId = RedisIndexedSessionRepository.this.sessionIdGenerationStrategy.generate();
this.cached.setId(newSessionId);
return newSessionId;
}
@Override

View File

@@ -27,7 +27,9 @@ import org.springframework.session.FlushMode;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.SessionIdGenerationStrategy;
import org.springframework.session.SessionRepository;
import org.springframework.session.UuidSessionIdGenerationStrategy;
import org.springframework.util.Assert;
/**
@@ -56,6 +58,8 @@ public class RedisSessionRepository implements SessionRepository<RedisSessionRep
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private SessionIdGenerationStrategy sessionIdGenerationStrategy = UuidSessionIdGenerationStrategy.getInstance();
/**
* Create a new {@link RedisSessionRepository} instance.
* @param sessionRedisOperations the {@link RedisOperations} to use for managing
@@ -106,7 +110,7 @@ public class RedisSessionRepository implements SessionRepository<RedisSessionRep
@Override
public RedisSession createSession() {
MapSession cached = new MapSession();
MapSession cached = new MapSession(this.sessionIdGenerationStrategy);
cached.setMaxInactiveInterval(this.defaultMaxInactiveInterval);
RedisSession session = new RedisSession(cached, true);
session.flushIfRequired();
@@ -162,6 +166,16 @@ public class RedisSessionRepository implements SessionRepository<RedisSessionRep
return RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName;
}
/**
* Set the {@link SessionIdGenerationStrategy} to use to generate session ids.
* @param sessionIdGenerationStrategy the {@link SessionIdGenerationStrategy} to use
* @since 3.2
*/
public void setSessionIdGenerationStrategy(SessionIdGenerationStrategy sessionIdGenerationStrategy) {
Assert.notNull(sessionIdGenerationStrategy, "sessionIdGenerationStrategy cannot be null");
this.sessionIdGenerationStrategy = sessionIdGenerationStrategy;
}
/**
* An internal {@link Session} implementation used by this {@link SessionRepository}.
*/
@@ -198,7 +212,9 @@ public class RedisSessionRepository implements SessionRepository<RedisSessionRep
@Override
public String changeSessionId() {
return this.cached.changeSessionId();
String newSessionId = RedisSessionRepository.this.sessionIdGenerationStrategy.generate();
this.cached.setId(newSessionId);
return newSessionId;
}
@Override

View File

@@ -19,6 +19,7 @@ package org.springframework.session.data.redis.config.annotation.web.http;
import java.time.Duration;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -27,6 +28,8 @@ import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.session.SessionIdGenerationStrategy;
import org.springframework.session.UuidSessionIdGenerationStrategy;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.StringUtils;
@@ -49,6 +52,8 @@ public class RedisHttpSessionConfiguration extends AbstractRedisHttpSessionConfi
private StringValueResolver embeddedValueResolver;
private SessionIdGenerationStrategy sessionIdGenerationStrategy = UuidSessionIdGenerationStrategy.getInstance();
@Bean
@Override
public RedisSessionRepository sessionRepository() {
@@ -60,6 +65,7 @@ public class RedisHttpSessionConfiguration extends AbstractRedisHttpSessionConfi
}
sessionRepository.setFlushMode(getFlushMode());
sessionRepository.setSaveMode(getSaveMode());
sessionRepository.setSessionIdGenerationStrategy(this.sessionIdGenerationStrategy);
getSessionRepositoryCustomizers()
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
@@ -87,4 +93,9 @@ public class RedisHttpSessionConfiguration extends AbstractRedisHttpSessionConfi
setSaveMode(attributes.getEnum("saveMode"));
}
@Autowired(required = false)
public void setSessionIdGenerationStrategy(SessionIdGenerationStrategy sessionIdGenerationStrategy) {
this.sessionIdGenerationStrategy = sessionIdGenerationStrategy;
}
}

View File

@@ -44,6 +44,8 @@ import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.session.IndexResolver;
import org.springframework.session.Session;
import org.springframework.session.SessionIdGenerationStrategy;
import org.springframework.session.UuidSessionIdGenerationStrategy;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction;
import org.springframework.session.data.redis.config.ConfigureRedisAction;
@@ -80,6 +82,8 @@ public class RedisIndexedHttpSessionConfiguration
private StringValueResolver embeddedValueResolver;
private SessionIdGenerationStrategy sessionIdGenerationStrategy = UuidSessionIdGenerationStrategy.getInstance();
@Bean
@Override
public RedisIndexedSessionRepository sessionRepository() {
@@ -101,6 +105,7 @@ public class RedisIndexedHttpSessionConfiguration
sessionRepository.setCleanupCron(this.cleanupCron);
int database = resolveDatabase();
sessionRepository.setDatabase(database);
sessionRepository.setSessionIdGenerationStrategy(this.sessionIdGenerationStrategy);
getSessionRepositoryCustomizers()
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
@@ -204,6 +209,11 @@ public class RedisIndexedHttpSessionConfiguration
return RedisIndexedSessionRepository.DEFAULT_DATABASE;
}
@Autowired(required = false)
public void setSessionIdGenerationStrategy(SessionIdGenerationStrategy sessionIdGenerationStrategy) {
this.sessionIdGenerationStrategy = sessionIdGenerationStrategy;
}
/**
* Ensures that Redis is configured to send keyspace notifications. This is important
* to ensure that expiration and deletion of sessions trigger SessionDestroyedEvents.

View File

@@ -39,6 +39,8 @@ import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.SessionIdGenerationStrategy;
import org.springframework.session.UuidSessionIdGenerationStrategy;
import org.springframework.session.config.ReactiveSessionRepositoryCustomizer;
import org.springframework.session.config.annotation.web.server.SpringWebSessionConfiguration;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
@@ -77,6 +79,8 @@ public class RedisWebSessionConfiguration implements BeanClassLoaderAware, Embed
private StringValueResolver embeddedValueResolver;
private SessionIdGenerationStrategy sessionIdGenerationStrategy = UuidSessionIdGenerationStrategy.getInstance();
@Bean
public ReactiveRedisSessionRepository sessionRepository() {
ReactiveRedisTemplate<String, Object> reactiveRedisTemplate = createReactiveRedisTemplate();
@@ -86,6 +90,7 @@ public class RedisWebSessionConfiguration implements BeanClassLoaderAware, Embed
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
}
sessionRepository.setSaveMode(this.saveMode);
sessionRepository.setSessionIdGenerationStrategy(this.sessionIdGenerationStrategy);
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
@@ -168,4 +173,9 @@ public class RedisWebSessionConfiguration implements BeanClassLoaderAware, Embed
return new ReactiveRedisTemplate<>(this.redisConnectionFactory, serializationContext);
}
@Autowired(required = false)
public void setSessionIdGenerationStrategy(SessionIdGenerationStrategy sessionIdGenerationStrategy) {
this.sessionIdGenerationStrategy = sessionIdGenerationStrategy;
}
}

View File

@@ -438,6 +438,46 @@ class ReactiveRedisSessionRepositoryTests {
verifyNoMoreInteractions(this.hashOperations);
}
@Test
void createSessionWhenSessionIdGenerationStrategyThenUses() {
this.repository.setSessionIdGenerationStrategy(() -> "test");
this.repository.createSession().as(StepVerifier::create).assertNext((redisSession) -> {
assertThat(redisSession.getId()).isEqualTo("test");
assertThat(redisSession.changeSessionId()).isEqualTo("test");
}).verifyComplete();
}
@Test
void setSessionIdGenerationStrategyWhenNullThenThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setSessionIdGenerationStrategy(null))
.withMessage("sessionIdGenerationStrategy cannot be null");
}
@Test
@SuppressWarnings("unchecked")
void findByIdWhenChangeSessionIdThenUsesSessionIdGenerationStrategy() {
this.repository.setSessionIdGenerationStrategy(() -> "changed-session-id");
given(this.redisOperations.opsForHash()).willReturn(this.hashOperations);
String attribute1 = "attribute1";
String attribute2 = "attribute2";
MapSession expected = new MapSession("test");
expected.setLastAccessedTime(Instant.now().minusSeconds(60));
expected.setAttribute(attribute1, "test");
expected.setAttribute(attribute2, null);
Map map = map(RedisSessionMapper.ATTRIBUTE_PREFIX + attribute1, expected.getAttribute(attribute1),
RedisSessionMapper.ATTRIBUTE_PREFIX + attribute2, expected.getAttribute(attribute2),
RedisSessionMapper.CREATION_TIME_KEY, expected.getCreationTime().toEpochMilli(),
RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) expected.getMaxInactiveInterval().getSeconds(),
RedisSessionMapper.LAST_ACCESSED_TIME_KEY, expected.getLastAccessedTime().toEpochMilli());
given(this.hashOperations.entries(anyString())).willReturn(Flux.fromIterable(map.entrySet()));
StepVerifier.create(this.repository.findById("test")).consumeNextWith((session) -> {
assertThat(session.getId()).isEqualTo(expected.getId());
assertThat(session.changeSessionId()).isEqualTo("changed-session-id");
}).verifyComplete();
}
private Map<String, Object> map(Object... objects) {
Map<String, Object> result = new HashMap<>();
if (objects == null) {

View File

@@ -910,6 +910,46 @@ class RedisIndexedSessionRepositoryTests {
assertThat(getDelta()).hasSize(3);
}
@Test
void createSessionWhenSessionIdGenerationStrategyThenUses() {
this.redisRepository.setSessionIdGenerationStrategy(() -> "test");
RedisSession session = this.redisRepository.createSession();
assertThat(session.getId()).isEqualTo("test");
assertThat(session.changeSessionId()).isEqualTo("test");
}
@Test
void setSessionIdGenerationStrategyWhenNullThenThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.redisRepository.setSessionIdGenerationStrategy(null))
.withMessage("sessionIdGenerationStrategy cannot be null");
}
@Test
void findByIdWhenChangeSessionIdThenUsesSessionIdGenerationStrategy() {
this.redisRepository.setSessionIdGenerationStrategy(() -> "test");
String attribute1 = "attribute1";
String attribute2 = "attribute2";
MapSession expected = new MapSession("original");
expected.setLastAccessedTime(Instant.now().minusSeconds(60));
expected.setAttribute(attribute1, "test");
expected.setAttribute(attribute2, null);
given(this.redisOperations.<String, Object>boundHashOps(getKey(expected.getId())))
.willReturn(this.boundHashOperations);
Map<String, Object> map = map(RedisIndexedSessionRepository.getSessionAttrNameKey(attribute1),
expected.getAttribute(attribute1), RedisIndexedSessionRepository.getSessionAttrNameKey(attribute2),
expected.getAttribute(attribute2), RedisSessionMapper.CREATION_TIME_KEY,
expected.getCreationTime().toEpochMilli(), RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
(int) expected.getMaxInactiveInterval().getSeconds(), RedisSessionMapper.LAST_ACCESSED_TIME_KEY,
expected.getLastAccessedTime().toEpochMilli());
given(this.boundHashOperations.entries()).willReturn(map);
RedisSession session = this.redisRepository.findById(expected.getId());
String oldSessionId = session.getId();
String newSessionId = session.changeSessionId();
assertThat(oldSessionId).isEqualTo("original");
assertThat(newSessionId).isEqualTo("test");
}
private String getKey(String id) {
return "spring:session:sessions:" + id;
}

View File

@@ -373,6 +373,35 @@ class RedisSessionRepositoryTests {
verifyNoMoreInteractions(this.sessionHashOperations);
}
@Test
void createSessionWhenSessionIdGenerationStrategyThenUses() {
this.sessionRepository.setSessionIdGenerationStrategy(() -> "test");
RedisSessionRepository.RedisSession session = this.sessionRepository.createSession();
assertThat(session.getId()).isEqualTo("test");
assertThat(session.changeSessionId()).isEqualTo("test");
}
@Test
void setSessionIdGenerationStrategyWhenNullThenThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.sessionRepository.setSessionIdGenerationStrategy(null))
.withMessage("sessionIdGenerationStrategy cannot be null");
}
@Test
void findByIdWhenChangeSessionIdThenUsesSessionIdGenerationStrategy() {
this.sessionRepository.setSessionIdGenerationStrategy(() -> "test");
Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
given(this.sessionHashOperations.entries(eq(TEST_SESSION_KEY)))
.willReturn(mapOf(RedisSessionMapper.CREATION_TIME_KEY, Instant.EPOCH.toEpochMilli(),
RedisSessionMapper.LAST_ACCESSED_TIME_KEY, now.toEpochMilli(),
RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS,
RedisSessionMapper.ATTRIBUTE_PREFIX + "attribute1", "value1"));
RedisSession session = this.sessionRepository.findById(TEST_SESSION_ID);
assertThat(session.getId()).isEqualTo(TEST_SESSION_ID);
assertThat(session.changeSessionId()).isEqualTo("test");
}
private static String getSessionKey(String sessionId) {
return "spring:session:sessions:" + sessionId;
}

View File

@@ -39,6 +39,8 @@ import org.springframework.data.redis.core.RedisOperations;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.session.FlushMode;
import org.springframework.session.SaveMode;
import org.springframework.session.SessionIdGenerationStrategy;
import org.springframework.session.UuidSessionIdGenerationStrategy;
import org.springframework.session.config.SessionRepositoryCustomizer;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
@@ -206,6 +208,22 @@ class RedisHttpsSessionConfigurationTests {
assertThat(sessionRepository).extracting("defaultMaxInactiveInterval").isEqualTo(Duration.ZERO);
}
@Test
void registerWhenSessionIdGenerationStrategyBeanThenUses() {
registerAndRefresh(RedisConfig.class, SessionIdGenerationStrategyConfiguration.class);
RedisSessionRepository sessionRepository = this.context.getBean(RedisSessionRepository.class);
assertThat(sessionRepository).extracting("sessionIdGenerationStrategy")
.isInstanceOf(TestSessionIdGenerationStrategy.class);
}
@Test
void registerWhenNoSessionIdGenerationStrategyBeanThenDefault() {
registerAndRefresh(RedisConfig.class, DefaultConfiguration.class);
RedisSessionRepository sessionRepository = this.context.getBean(RedisSessionRepository.class);
assertThat(sessionRepository).extracting("sessionIdGenerationStrategy")
.isInstanceOf(UuidSessionIdGenerationStrategy.class);
}
private void registerAndRefresh(Class<?>... annotatedClasses) {
this.context.register(annotatedClasses);
this.context.refresh();
@@ -381,4 +399,30 @@ class RedisHttpsSessionConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
@EnableRedisHttpSession
static class SessionIdGenerationStrategyConfiguration {
@Bean
SessionIdGenerationStrategy sessionIdGenerationStrategy() {
return new TestSessionIdGenerationStrategy();
}
}
@Configuration(proxyBeanMethods = false)
@EnableRedisHttpSession
static class DefaultConfiguration {
}
static class TestSessionIdGenerationStrategy implements SessionIdGenerationStrategy {
@Override
public String generate() {
return "test";
}
}
}

View File

@@ -43,6 +43,8 @@ import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.SessionIdGenerationStrategy;
import org.springframework.session.UuidSessionIdGenerationStrategy;
import org.springframework.session.config.SessionRepositoryCustomizer;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
@@ -240,6 +242,22 @@ class RedisIndexedHttpSessionConfigurationTests {
assertThat(sessionRepository).extracting("defaultMaxInactiveInterval").isEqualTo(Duration.ZERO);
}
@Test
void registerWhenSessionIdGenerationStrategyBeanThenUses() {
registerAndRefresh(RedisConfig.class, SessionIdGenerationStrategyConfiguration.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).extracting("sessionIdGenerationStrategy")
.isInstanceOf(TestSessionIdGenerationStrategy.class);
}
@Test
void registerWhenNoSessionIdGenerationStrategyBeanThenDefault() {
registerAndRefresh(RedisConfig.class, DefaultConfiguration.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).extracting("sessionIdGenerationStrategy")
.isInstanceOf(UuidSessionIdGenerationStrategy.class);
}
private void registerAndRefresh(Class<?>... annotatedClasses) {
this.context.register(annotatedClasses);
this.context.refresh();
@@ -444,4 +462,30 @@ class RedisIndexedHttpSessionConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
@EnableRedisIndexedHttpSession
static class SessionIdGenerationStrategyConfiguration {
@Bean
SessionIdGenerationStrategy sessionIdGenerationStrategy() {
return new TestSessionIdGenerationStrategy();
}
}
@Configuration(proxyBeanMethods = false)
@EnableRedisIndexedHttpSession
static class DefaultConfiguration {
}
static class TestSessionIdGenerationStrategy implements SessionIdGenerationStrategy {
@Override
public String generate() {
return "test";
}
}
}