diff --git a/spring-geode-tests/smoke-tests/mock-session-caching/lombok.config b/spring-geode-tests/smoke-tests/mock-session-caching/lombok.config new file mode 100644 index 00000000..6aa51d71 --- /dev/null +++ b/spring-geode-tests/smoke-tests/mock-session-caching/lombok.config @@ -0,0 +1,2 @@ +# This file is generated by the 'io.freefair.lombok' Gradle plugin +config.stopBubbling = true diff --git a/spring-geode-tests/smoke-tests/mock-session-caching/spring-geode-smoke-tests-mock-session-caching.gradle b/spring-geode-tests/smoke-tests/mock-session-caching/spring-geode-smoke-tests-mock-session-caching.gradle new file mode 100644 index 00000000..3d68a05e --- /dev/null +++ b/spring-geode-tests/smoke-tests/mock-session-caching/spring-geode-smoke-tests-mock-session-caching.gradle @@ -0,0 +1,22 @@ +plugins { + id "io.freefair.lombok" version "5.2.1" +} + +apply plugin: 'io.spring.convention.spring-test' + +description = "Smoke Tests to assert (Spring) Session state caching using Apache Geode with Mock Objects auto-configured by Spring Boot." + +dependencies { + + implementation "org.assertj:assertj-core" + implementation "org.springframework.boot:spring-boot-starter-web" + + implementation project(':spring-geode-starter-session') + + implementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + + testImplementation project(":spring-geode-starter-test") + +} diff --git a/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/hamcrest/RegexMatcher.java b/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/hamcrest/RegexMatcher.java new file mode 100644 index 00000000..4f42ae4c --- /dev/null +++ b/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/hamcrest/RegexMatcher.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020 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.hamcrest; + +import java.util.regex.Pattern; + +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; + +/** + * A Hamcrest {@link Matcher} using a Java {@link Pattern} with a {@link String Regular Expression} + * to match a {@link String} argument. + * + * @author John Blum + * @see java.util.regex.Pattern + * @see org.hamcrest.BaseMatcher + * @see org.hamcrest.Matcher + * @since 1.4.0 + */ +public class RegexMatcher extends BaseMatcher { + + private final Pattern pattern; + + public static RegexMatcher from(@NonNull String regex) { + return new RegexMatcher(regex); + } + + public RegexMatcher(@NonNull String regex) { + + Assert.hasText(regex, () -> String.format("Regular Expression [%s] is required", regex)); + + this.pattern = Pattern.compile(regex); + } + + @Override + public void describeTo(Description description) { + description.appendText(String.format("Matches text to the Regular Expression (Pattern) [%s]", + this.pattern.toString())); + } + + @Override + public boolean matches(Object actual) { + return this.pattern.matcher(String.valueOf(actual)).matches(); + } +} diff --git a/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/security/AuthenticationException.java b/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/security/AuthenticationException.java new file mode 100644 index 00000000..5531bfe2 --- /dev/null +++ b/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/security/AuthenticationException.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020 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.security; + +/** + * {@link RuntimeException} implementation indicating a Security Authentication error. + * + * @author John Blum + * @see java.lang.Runtime + * @since 1.4.0 + */ +@SuppressWarnings("unused") +public class AuthenticationException extends RuntimeException { + + public AuthenticationException() { } + + public AuthenticationException(String message) { + super(message); + } + + public AuthenticationException(Throwable cause) { + super(cause); + } + + public AuthenticationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/session/web/servlet/http/HttpSessionAdapter.java b/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/session/web/servlet/http/HttpSessionAdapter.java new file mode 100644 index 00000000..3cd16475 --- /dev/null +++ b/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/session/web/servlet/http/HttpSessionAdapter.java @@ -0,0 +1,128 @@ +/* + * Copyright 2020 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.servlet.http; + +import java.time.Duration; +import java.util.Collections; +import java.util.Enumeration; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionContext; + +import org.springframework.lang.NonNull; +import org.springframework.session.Session; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.http.AbstractHttpSession; + +/** + * {@link HttpSession} implementation adapting the Spring Session {@link Session} interface. + * + * @author John Blum + * @see java.time.Duration + * @see javax.servlet.ServletContext + * @see javax.servlet.http.HttpSession + * @see org.springframework.session.Session + * @see org.springframework.web.servlet.http.AbstractHttpSession + * @since 1.4.0 + */ +@SuppressWarnings("unused") +public class HttpSessionAdapter extends AbstractHttpSession { + + private final ServletContext servletContext; + + private final Session session; + + public HttpSessionAdapter(@NonNull ServletContext servletContext, @NonNull Session session) { + + Assert.notNull(servletContext, "ServletContext must not be null"); + Assert.notNull(session, "Session must not be null"); + + this.session = session; + this.servletContext = servletContext; + } + + protected @NonNull Session getSession() { + return this.session; + } + + @Override + public String getId() { + return getSession().getId(); + } + + @Override + public long getCreationTime() { + return getSession().getCreationTime().toEpochMilli(); + } + + @Override + public long getLastAccessedTime() { + return getSession().getLastAccessedTime().toEpochMilli(); + } + + @Override + public void setMaxInactiveInterval(int interval) { + getSession().setMaxInactiveInterval(Duration.ofSeconds(interval)); + } + + @Override + public int getMaxInactiveInterval() { + return Long.valueOf(getSession().getMaxInactiveInterval().getSeconds()).intValue(); + } + + @Override + public @NonNull ServletContext getServletContext() { + return this.servletContext; + } + + @Override + @SuppressWarnings("deprecation") + public HttpSessionContext getSessionContext() { + throw new UnsupportedOperationException("Not Implemented"); + } + + @Override + public void setAttribute(String name, Object value) { + getSession().setAttribute(name, value); + } + + @Override + public void removeAttribute(String name) { + getSession().removeAttribute(name); + } + + @Override + public Object getAttribute(String name) { + return getSession().getAttribute(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(getSession().getAttributeNames()); + } + + @Override + public void invalidate() { + throw new UnsupportedOperationException("Not Implemented"); + } + + @Override + public boolean isNew() { + return !StringUtils.hasText(getId()); + } +} diff --git a/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/session/web/servlet/http/SessionUtils.java b/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/session/web/servlet/http/SessionUtils.java new file mode 100644 index 00000000..b9e42fc4 --- /dev/null +++ b/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/session/web/servlet/http/SessionUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020 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.servlet.http; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import org.springframework.geode.core.util.ObjectUtils; +import org.springframework.lang.Nullable; +import org.springframework.session.SessionRepository; + +/** + * Abstract utility class used to work with {@link HttpSession} objects. + * + * @author John Blum + * @see javax.servlet.http.HttpServletRequest + * @see javax.servlet.http.HttpSession + * @since 1.4.0 + */ +public abstract class SessionUtils { + + protected static final String CURRENT_SESSION_REQUEST_ATTRIBUTE = + SessionRepository.class.getName().concat(".CURRENT_SESSION"); + + protected static final String GET_CURRENT_SESSION_METHOD_NAME = "getCurrentSession"; + + public static @Nullable HttpSession resolveSession(HttpServletRequest servletRequest) { + + try { + return ObjectUtils.invoke(servletRequest, GET_CURRENT_SESSION_METHOD_NAME); + } + catch (IllegalArgumentException ignore) { + return (HttpSession) servletRequest.getAttribute(CURRENT_SESSION_REQUEST_ATTRIBUTE); + } + } +} diff --git a/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/session/web/servlet/http/SpringSessionSubstitutingSpyRequestPostProcessor.java b/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/session/web/servlet/http/SpringSessionSubstitutingSpyRequestPostProcessor.java new file mode 100644 index 00000000..7a2cd69f --- /dev/null +++ b/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/session/web/servlet/http/SpringSessionSubstitutingSpyRequestPostProcessor.java @@ -0,0 +1,73 @@ +/* + * Copyright 2020 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.servlet.http; + +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.spy; + +import java.util.concurrent.atomic.AtomicReference; + +import javax.servlet.http.HttpSession; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.session.Session; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.web.servlet.http.HttpSessionProxy; + +/** + * Spring Test Mock Web MVC framework {@link RequestPostProcessor} that substitutes the Spring Session {@link Session} + * for the {@link MockHttpSession}. + * + * @author John Blum + * @see javax.servlet.http.HttpSession + * @see org.springframework.mock.web.MockHttpServletRequest + * @see org.springframework.mock.web.MockHttpSession + * @see org.springframework.session.Session + * @see org.springframework.test.web.servlet.request.RequestPostProcessor + * @see org.springframework.web.servlet.http.HttpSessionProxy + * @since 1.4.0 + */ +public class SpringSessionSubstitutingSpyRequestPostProcessor implements RequestPostProcessor { + + private static final AtomicReference instance = new AtomicReference<>(null); + + public static SpringSessionSubstitutingSpyRequestPostProcessor create() { + return instance.updateAndGet(instance -> instance != null ? instance + : new SpringSessionSubstitutingSpyRequestPostProcessor()); + } + + /** + * @inheritDoc + */ + @Override + public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + + MockHttpServletRequest requestSpy = spy(request); + + doAnswer(invocation -> { + + HttpSession session = SessionUtils.resolveSession(request); + + return session != null + ? HttpSessionProxy.from(session) + : request.getSession(); + + }).when(requestSpy).getSession(); + + return requestSpy; + } +} diff --git a/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/web/servlet/http/AbstractHttpSession.java b/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/web/servlet/http/AbstractHttpSession.java new file mode 100644 index 00000000..974c5c3d --- /dev/null +++ b/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/web/servlet/http/AbstractHttpSession.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020 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.web.servlet.http; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Objects; + +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionContext; + +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +/** + * Abstract base class supporting implementations of the {@link HttpSession} interface. + * + * @author John Blum + * @see javax.servlet.http.HttpSession + * @since 1.4.0 + */ +@SuppressWarnings("deprecation") +public abstract class AbstractHttpSession implements HttpSession { + + @Override + public HttpSessionContext getSessionContext() { + throw new UnsupportedOperationException("Not Implemented"); + } + + @Override + public @Nullable Object getValue(String name) { + return getAttribute(name); + } + + @Override + public @NonNull String[] getValueNames() { + + List valueNames = new ArrayList<>(); + + Enumeration attributeNames = getAttributeNames(); + + if (Objects.nonNull(attributeNames)) { + while (attributeNames.hasMoreElements()) { + valueNames.add(attributeNames.nextElement()); + } + } + + return valueNames.toArray(new String[0]); + } + + @Override + public void putValue(String name, Object value) { + setAttribute(name, value); + } + + @Override + public void removeValue(String name) { + removeAttribute(name); + } +} diff --git a/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/web/servlet/http/HttpSessionProxy.java b/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/web/servlet/http/HttpSessionProxy.java new file mode 100644 index 00000000..e75c9ffa --- /dev/null +++ b/spring-geode-tests/smoke-tests/mock-session-caching/src/main/java/org/springframework/web/servlet/http/HttpSessionProxy.java @@ -0,0 +1,121 @@ +/* + * Copyright 2020 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.web.servlet.http; + +import java.util.Enumeration; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionContext; + +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link HttpSession} implementation wrapping and proxying for an existing {@link HttpSession} instance. + * + * @author John Blum + * @see javax.servlet.ServletContext + * @see javax.servlet.http.HttpSession + * @see org.springframework.web.servlet.http.AbstractHttpSession + * @since 1.4.0 + */ +public class HttpSessionProxy extends AbstractHttpSession { + + public static HttpSessionProxy from(HttpSession session) { + return new HttpSessionProxy(session); + } + + private final HttpSession session; + + private HttpSessionProxy(@NonNull HttpSession session) { + + Assert.notNull(session, "HttpSession must not be null"); + + this.session = session; + } + + protected @NonNull HttpSession getSession() { + return this.session; + } + + @Override + public String getId() { + return getSession().getId(); + } + + @Override + public long getCreationTime() { + return getSession().getCreationTime(); + } + + @Override + public long getLastAccessedTime() { + return getSession().getLastAccessedTime(); + } + + @Override + public void setMaxInactiveInterval(int interval) { + getSession().setMaxInactiveInterval(interval); + } + + @Override + public int getMaxInactiveInterval() { + return getSession().getMaxInactiveInterval(); + } + + @Override + public @NonNull ServletContext getServletContext() { + return getSession().getServletContext(); + } + + @Override + @SuppressWarnings("deprecation") + public HttpSessionContext getSessionContext() { + return getSession().getSessionContext(); + } + + @Override + public void setAttribute(String name, Object value) { + getSession().setAttribute(name, value); + } + + @Override + public @Nullable Object getAttribute(String name) { + return getSession().getAttribute(name); + } + + @Override + public Enumeration getAttributeNames() { + return getSession().getAttributeNames(); + } + + @Override + public void removeAttribute(String name) { + getSession().removeAttribute(name); + } + + @Override + public void invalidate() { + getSession().invalidate(); + } + + @Override + public boolean isNew() { + return getSession().isNew(); + } +} diff --git a/spring-geode-tests/smoke-tests/mock-session-caching/src/test/java/example/app/geode/caching/session/MockPersistentSessionAccessSmokeTests.java b/spring-geode-tests/smoke-tests/mock-session-caching/src/test/java/example/app/geode/caching/session/MockPersistentSessionAccessSmokeTests.java new file mode 100644 index 00000000..0eebc6b7 --- /dev/null +++ b/spring-geode-tests/smoke-tests/mock-session-caching/src/test/java/example/app/geode/caching/session/MockPersistentSessionAccessSmokeTests.java @@ -0,0 +1,145 @@ +/* + * Copyright 2020 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 example.app.geode.caching.session; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import javax.servlet.http.HttpSession; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.gemfire.tests.mock.annotation.EnableGemFireMockObjects; +import org.springframework.data.gemfire.tests.support.MapBuilder; +import org.springframework.hamcrest.RegexMatcher; +import org.springframework.http.MediaType; +import org.springframework.security.AuthenticationException; +import org.springframework.session.web.servlet.http.SpringSessionSubstitutingSpyRequestPostProcessor; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +/** + * Smoke Tests testing the persistent access of a (HTTP) Session stored and managed by Apache Geode using Mock Objects + * with Spring Session auto-configured with Spring Boot. + * + * @author John Blum + * @see javax.servlet.http.HttpSession + * @see org.junit.Test + * @see org.springframework.boot.autoconfigure.SpringBootApplication + * @see org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc + * @see org.springframework.boot.test.context.SpringBootTest + * @see org.springframework.data.gemfire.tests.mock.annotation.EnableGemFireMockObjects + * @see org.springframework.test.context.junit4.SpringRunner + * @see org.springframework.test.web.servlet.MockMvc + * @see org.springframework.web.bind.annotation.RestController + * @since 1.4.0 + */ +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = { + MockPersistentSessionAccessSmokeTests.TestSessionGeodeConfiguration.class, + MockPersistentSessionAccessSmokeTests.TestUserAccessController.class + }, + webEnvironment = SpringBootTest.WebEnvironment.MOCK +) +@AutoConfigureMockMvc +@SuppressWarnings("unused") +public class MockPersistentSessionAccessSmokeTests { + + @Autowired + private MockMvc mvc; + + @Test + public void persistentSessionAccessIsSuccessful() throws Exception { + + String username = "jonDoe"; + + this.mvc.perform(get("/users/{username}", username) + .param("password", "p@5$w0rd") + .with(SpringSessionSubstitutingSpyRequestPostProcessor.create())) + .andExpect(status().isOk()) + .andExpect(request().sessionAttribute(username, User.newUser(username))) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().string(RegexMatcher.from("\\{\"id\":\".*\",\"name\":\"jonDoe\"\\}"))); + } + + @SpringBootApplication + @EnableGemFireMockObjects + static class TestSessionGeodeConfiguration { } + + @RestController + static class TestUserAccessController { + + static final Map userAccessControlMap = MapBuilder.newMapBuilder() + .put("jonDoe", "p@5$w0rd") + .put("janeDoe", "s3cr3t!") + .build(); + + @GetMapping("/users/{username}") + public User login(HttpSession session, @PathVariable String username, + @RequestParam(required = false) String password) { + + return Optional.ofNullable(password) + .filter(StringUtils::hasText) + .filter(it -> String.valueOf(userAccessControlMap.get(username)).equals(password)) + .map(it -> User.newUser(username).identifiedBy(UUID.randomUUID().toString())) + .map(user -> { + session.setAttribute(user.getName(), user); + return user; + }) + .orElseThrow(() -> new AuthenticationException(String.format("User [%s] is not authorized", username))); + } + } + + @Getter + @ToString(of = "name") + @EqualsAndHashCode(of = "name") + @RequiredArgsConstructor(staticName = "newUser") + static class User { + + private String id; + + @NonNull + private final String name; + + public User identifiedBy(String id) { + this.id = id; + return this; + } + } +}