diff --git a/etc/checkstyle/suppressions.xml b/etc/checkstyle/suppressions.xml index 915c20fb..a30987bc 100644 --- a/etc/checkstyle/suppressions.xml +++ b/etc/checkstyle/suppressions.xml @@ -4,6 +4,7 @@ + diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 0e442d3d..ae53a79e 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -12,6 +12,7 @@ dependencyManagement { entry 'hazelcast-client' } + dependency 'ch.qos.logback:logback-classic:1.2.3' dependency 'com.fasterxml.jackson.core:jackson-databind:2.9.0.pr4' dependency 'com.h2database:h2:1.4.196' dependency 'com.maxmind.geoip2:geoip2:2.3.1' @@ -29,6 +30,10 @@ dependencyManagement { dependency 'org.hsqldb:hsqldb:2.4.0' dependency 'org.mockito:mockito-core:2.8.47' dependency 'org.seleniumhq.selenium:htmlunit-driver:2.27' + dependency 'org.slf4j:jcl-over-slf4j:1.7.25' + dependency 'org.slf4j:log4j-over-slf4j:1.7.25' + dependency 'org.thymeleaf.extras:thymeleaf-extras-java8time:3.0.0.RELEASE' + dependency 'org.thymeleaf:thymeleaf-spring5:3.0.7.RC1' dependency 'org.webjars:bootstrap:2.3.2' dependency 'org.webjars:html5shiv:3.7.3' dependency 'org.webjars:jquery:1.9.0' diff --git a/samples/javaconfig/webflux/spring-session-sample-javaconfig-webflux.gradle b/samples/javaconfig/webflux/spring-session-sample-javaconfig-webflux.gradle new file mode 100644 index 00000000..cd67790e --- /dev/null +++ b/samples/javaconfig/webflux/spring-session-sample-javaconfig-webflux.gradle @@ -0,0 +1,22 @@ +apply plugin: 'io.spring.convention.spring-sample' + +dependencies { + compile project(':spring-session-core') + compile 'io.lettuce:lettuce-core' + compile 'io.netty:netty-buffer' + compile 'io.projectreactor.ipc:reactor-netty' + compile 'org.springframework:spring-context' + compile 'org.springframework:spring-web' + compile 'org.springframework:spring-webflux' + compile 'org.thymeleaf.extras:thymeleaf-extras-java8time' + compile 'org.thymeleaf:thymeleaf-spring5' + compile 'org.webjars:bootstrap' + compile 'org.webjars:webjars-taglib' + compile jstlDependencies + compile slf4jDependencies + + testCompile 'junit:junit' + testCompile 'org.assertj:assertj-core' + + integrationTestCompile seleniumDependencies +} diff --git a/samples/javaconfig/webflux/src/integration-test/java/sample/AttributeTests.java b/samples/javaconfig/webflux/src/integration-test/java/sample/AttributeTests.java new file mode 100644 index 00000000..94bf1f71 --- /dev/null +++ b/samples/javaconfig/webflux/src/integration-test/java/sample/AttributeTests.java @@ -0,0 +1,90 @@ +/* + * 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 + * + * 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 sample; + + +import java.util.List; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.htmlunit.HtmlUnitDriver; +import sample.pages.HomePage; +import sample.pages.HomePage.Attribute; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Eddú Meléndez + * @author Rob Winch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = HelloWebfluxApplication.class) +@TestPropertySource(properties = "server.port=0") +public class AttributeTests { + @Value("#{@nettyContext.address().getPort()}") + int port; + + private WebDriver driver; + + @Before + public void setup() { + this.driver = new HtmlUnitDriver(); + } + + @After + public void tearDown() { + this.driver.quit(); + } + + @Test + public void home() { + HomePage home = HomePage.go(this.driver, this.port); + home.assertAt(); + } + + @Test + public void noAttributes() { + HomePage home = HomePage.go(this.driver, this.port); + assertThat(home.attributes()).isEmpty(); + } + + @Test + public void createAttribute() { + HomePage home = HomePage.go(this.driver, this.port); + // @formatter:off + home = home.form() + .attributeName("a") + .attributeValue("b") + .submit(HomePage.class); + // @formatter:on + + List attributes = home.attributes(); + assertThat(attributes).hasSize(1); + Attribute row = attributes.get(0); + assertThat(row.getAttributeName()).isEqualTo("a"); + assertThat(row.getAttributeValue()).isEqualTo("b"); + } + +} diff --git a/samples/javaconfig/webflux/src/integration-test/java/sample/pages/HomePage.java b/samples/javaconfig/webflux/src/integration-test/java/sample/pages/HomePage.java new file mode 100644 index 00000000..2b0dbbee --- /dev/null +++ b/samples/javaconfig/webflux/src/integration-test/java/sample/pages/HomePage.java @@ -0,0 +1,135 @@ +/* + * 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 + * + * 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 sample.pages; + +import java.util.ArrayList; +import java.util.List; + +import org.openqa.selenium.SearchContext; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.PageFactory; +import org.openqa.selenium.support.pagefactory.DefaultElementLocatorFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Eddú Meléndez + * @author Rob Winch + */ +public class HomePage { + + private WebDriver driver; + + @FindBy(css = "form") + WebElement form; + + @FindBy(css = "table tbody tr") + List trs; + + List attributes; + + public HomePage(WebDriver driver) { + this.driver = driver; + this.attributes = new ArrayList<>(); + } + + private static void get(WebDriver driver, int port, String get) { + String baseUrl = "http://localhost:" + port; + driver.get(baseUrl + get); + } + + public static HomePage go(WebDriver driver, int port) { + get(driver, port, "/"); + return PageFactory.initElements(driver, HomePage.class); + } + + public void assertAt() { + assertThat(this.driver.getTitle()).isEqualTo("Session Attributes"); + } + + public List attributes() { + List rows = new ArrayList<>(); + for (WebElement tr : this.trs) { + rows.add(new Attribute(tr)); + } + this.attributes.addAll(rows); + return this.attributes; + } + + public Form form() { + return new Form(this.form); + } + + public class Form { + @FindBy(name = "attributeName") + WebElement attributeName; + + @FindBy(name = "attributeValue") + WebElement attributeValue; + + @FindBy(css = "input[type=\"submit\"]") + WebElement submit; + + public Form(SearchContext context) { + PageFactory.initElements(new DefaultElementLocatorFactory(context), this); + } + + public Form attributeName(String text) { + this.attributeName.sendKeys(text); + return this; + } + + public Form attributeValue(String text) { + this.attributeValue.sendKeys(text); + return this; + } + + public T submit(Class page) { + this.submit.click(); + return PageFactory.initElements(HomePage.this.driver, page); + } + } + + public static class Attribute { + @FindBy(xpath = ".//td[1]") + WebElement attributeName; + + @FindBy(xpath = ".//td[2]") + WebElement attributeValue; + + public Attribute(SearchContext context) { + PageFactory.initElements(new DefaultElementLocatorFactory(context), this); + } + + /** + * @return the attributeName + */ + public String getAttributeName() { + return this.attributeName.getText(); + } + + /** + * @return the attributeValue + */ + public String getAttributeValue() { + return this.attributeValue.getText(); + } + } + +} diff --git a/samples/javaconfig/webflux/src/main/java/sample/Config.java b/samples/javaconfig/webflux/src/main/java/sample/Config.java new file mode 100644 index 00000000..8851b14f --- /dev/null +++ b/samples/javaconfig/webflux/src/main/java/sample/Config.java @@ -0,0 +1,36 @@ +/* + * 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 + * + * 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 sample; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.session.MapReactorSessionRepository; +import org.springframework.session.web.server.session.SpringSessionWebSessionManager; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; +import org.springframework.web.server.session.WebSessionManager; + + +// tag::class[] +@Configuration +public class Config { + + @Bean(WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME) + public WebSessionManager webSessionManager() { + return new SpringSessionWebSessionManager(new MapReactorSessionRepository()); + } +} +// end::class[] diff --git a/samples/javaconfig/webflux/src/main/java/sample/HelloWebfluxApplication.java b/samples/javaconfig/webflux/src/main/java/sample/HelloWebfluxApplication.java new file mode 100644 index 00000000..b69b2d3c --- /dev/null +++ b/samples/javaconfig/webflux/src/main/java/sample/HelloWebfluxApplication.java @@ -0,0 +1,60 @@ +/* + * 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 + * + * 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 sample; + +import reactor.ipc.netty.NettyContext; +import reactor.ipc.netty.http.server.HttpServer; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; + +/** + * @author Rob Winch + * @since 5.0 + */ +@Configuration +@EnableWebFlux +@ComponentScan +public class HelloWebfluxApplication { + @Value("${server.port:8080}") + private int port = 8080; + + public static void main(String[] args) throws Exception { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + HelloWebfluxApplication.class)) { + context.getBean(NettyContext.class).onClose().block(); + } + } + + @Profile("default") + @Bean + public NettyContext nettyContext(ApplicationContext context) { + HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build(); + ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); + HttpServer httpServer = HttpServer.create("localhost", this.port); + return httpServer.newHandler(adapter).block(); + } +} diff --git a/samples/javaconfig/webflux/src/main/java/sample/SSEFluxWebConfig.java b/samples/javaconfig/webflux/src/main/java/sample/SSEFluxWebConfig.java new file mode 100644 index 00000000..8a0e31a8 --- /dev/null +++ b/samples/javaconfig/webflux/src/main/java/sample/SSEFluxWebConfig.java @@ -0,0 +1,91 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2014, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 sample; + +import org.thymeleaf.spring5.ISpringWebFluxTemplateEngine; +import org.thymeleaf.spring5.SpringWebFluxTemplateEngine; +import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver; +import org.thymeleaf.spring5.view.reactive.ThymeleafReactiveViewResolver; +import org.thymeleaf.templatemode.TemplateMode; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SSEFluxWebConfig { + + // TODO * Once there is a Spring Boot starter for thymeleaf-spring5, there would be no + // need to have + // TODO that @EnableConfigurationProperties annotation or use it for declaring the + // beans down in the + // TODO "thymeleaf" section below. + + private ApplicationContext applicationContext; + + public SSEFluxWebConfig(final ApplicationContext applicationContext) { + super(); + this.applicationContext = applicationContext; + } + + /* + * -------------------------------------- THYMELEAF CONFIGURATION + * -------------------------------------- + */ + + // TODO * If there was a Spring Boot starter for thymeleaf-spring5 most probably some + // or all of these + // TODO resolver and engine beans would not need to be specifically declared here. + + @Bean + public SpringResourceTemplateResolver thymeleafTemplateResolver() { + + final SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver(); + resolver.setApplicationContext(this.applicationContext); + resolver.setPrefix("classpath:/templates/"); + resolver.setSuffix(".html"); + resolver.setTemplateMode(TemplateMode.HTML); + resolver.setCacheable(false); + resolver.setCheckExistence(true); + return resolver; + + } + + @Bean + public ISpringWebFluxTemplateEngine thymeleafTemplateEngine() { + // We override here the SpringTemplateEngine instance that would otherwise be + // instantiated by + // Spring Boot because we want to apply the SpringWebFlux-specific context + // factory, link builder... + final SpringWebFluxTemplateEngine templateEngine = new SpringWebFluxTemplateEngine(); + templateEngine.setTemplateResolver(thymeleafTemplateResolver()); + return templateEngine; + } + + @Bean + public ThymeleafReactiveViewResolver thymeleafChunkedAndDataDrivenViewResolver() { + final ThymeleafReactiveViewResolver viewResolver = new ThymeleafReactiveViewResolver(); + viewResolver.setTemplateEngine(thymeleafTemplateEngine()); + viewResolver.setOrder(1); + viewResolver.setResponseMaxChunkSizeBytes(8192); // OUTPUT BUFFER size limit + return viewResolver; + } + +} diff --git a/samples/javaconfig/webflux/src/main/java/sample/SessionAttributeForm.java b/samples/javaconfig/webflux/src/main/java/sample/SessionAttributeForm.java new file mode 100644 index 00000000..d649bbb4 --- /dev/null +++ b/samples/javaconfig/webflux/src/main/java/sample/SessionAttributeForm.java @@ -0,0 +1,43 @@ +/* + * 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 + * + * 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 sample; + +/** + * @author Rob Winch + * @since 5.0 + */ +public class SessionAttributeForm { + private String attributeName; + + private String attributeValue; + + public String getAttributeName() { + return this.attributeName; + } + + public void setAttributeName(String attributeName) { + this.attributeName = attributeName; + } + + public String getAttributeValue() { + return this.attributeValue; + } + + public void setAttributeValue(String attributeValue) { + this.attributeValue = attributeValue; + } +} diff --git a/samples/javaconfig/webflux/src/main/java/sample/SessionController.java b/samples/javaconfig/webflux/src/main/java/sample/SessionController.java new file mode 100644 index 00000000..f2571792 --- /dev/null +++ b/samples/javaconfig/webflux/src/main/java/sample/SessionController.java @@ -0,0 +1,44 @@ +/* + * 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 sample; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.server.WebSession; + +// tag::class[] +@Controller +public class SessionController { + + @PostMapping("/session") + public String setAttribute(@ModelAttribute SessionAttributeForm sessionAttributeForm, WebSession session) { + session.getAttributes().put(sessionAttributeForm.getAttributeName(), sessionAttributeForm.getAttributeValue()); + return "redirect:/"; + } + + @GetMapping("/") + public String index(Model model, WebSession webSession) { + model.addAttribute("webSession", webSession); + return "index"; + } + + private static final long serialVersionUID = 2878267318695777395L; +} +// tag::end[] diff --git a/samples/javaconfig/webflux/src/main/java/sample/ThymeleafWebfluxConfig.java b/samples/javaconfig/webflux/src/main/java/sample/ThymeleafWebfluxConfig.java new file mode 100644 index 00000000..c7b350ea --- /dev/null +++ b/samples/javaconfig/webflux/src/main/java/sample/ThymeleafWebfluxConfig.java @@ -0,0 +1,44 @@ +/* + * 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 + * + * 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 sample; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.config.ViewResolverRegistry; +import org.springframework.web.reactive.config.WebFluxConfigurer; +import org.springframework.web.reactive.result.view.ViewResolver; + +/** + * @author Rob Winch + * @since 5.0 + */ +@Configuration +public class ThymeleafWebfluxConfig implements WebFluxConfigurer { + + @Autowired(required = false) + List views = new ArrayList<>(); + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + for (ViewResolver view : this.views) { + registry.viewResolver(view); + } + } +} diff --git a/samples/javaconfig/webflux/src/main/resources/logback.xml b/samples/javaconfig/webflux/src/main/resources/logback.xml new file mode 100644 index 00000000..20e90221 --- /dev/null +++ b/samples/javaconfig/webflux/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + diff --git a/samples/javaconfig/webflux/src/main/resources/templates/index.html b/samples/javaconfig/webflux/src/main/resources/templates/index.html new file mode 100644 index 00000000..adaf4727 --- /dev/null +++ b/samples/javaconfig/webflux/src/main/resources/templates/index.html @@ -0,0 +1,42 @@ + + + + + + Session Attributes + + + +
+

Description

+

This application demonstrates how to use a Redis instance to back your session. Notice that there is no JSESSIONID cookie. We are also able to customize the way of identifying what the requested session id is.

+ +

Try it

+ +
+ + + + + +
+ +
+ + + + + + + + + + + + + + +
Attribute NameAttribute Value
+
+ + \ No newline at end of file