diff --git a/libraries-data-2/pom.xml b/libraries-data-2/pom.xml
index e6106c0fe3..73c5452f77 100644
--- a/libraries-data-2/pom.xml
+++ b/libraries-data-2/pom.xml
@@ -128,6 +128,21 @@
${awaitility.version}
test
+
+ org.rosuda.REngine
+ Rserve
+ ${rserve.version}
+
+
+ com.github.jbytecode
+ RCaller
+ ${rcaller.version}
+
+
+ org.renjin
+ renjin-script-engine
+ ${renjin.version}
+
@@ -137,6 +152,13 @@
http://repo.numericalmethod.com/maven/
default
+
+
+
+ bedatadriven
+ bedatadriven public repo
+ https://nexus.bedatadriven.com/content/groups/public/
+
@@ -153,6 +175,27 @@
3.6.2
1.7.25
3.0.0
+ RELEASE
+ 3.0
+ 1.8.1
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ com/baeldung/r/FastRMean.java
+
+
+ com/baeldung/r/FastRMeanUnitTest.java
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libraries-data-2/src/main/java/com/baeldung/r/FastRMean.java b/libraries-data-2/src/main/java/com/baeldung/r/FastRMean.java
new file mode 100644
index 0000000000..8348bfa403
--- /dev/null
+++ b/libraries-data-2/src/main/java/com/baeldung/r/FastRMean.java
@@ -0,0 +1,33 @@
+package com.baeldung.r;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+/**
+ * FastR showcase.
+ *
+ * @author Donato Rimenti
+ */
+public class FastRMean {
+
+ /**
+ * Invokes the customMean R function passing the given values as arguments.
+ *
+ * @param values the input to the mean script
+ * @return the result of the R script
+ */
+ public double mean(int[] values) {
+ Context polyglot = Context.newBuilder()
+ .allowAllAccess(true)
+ .build();
+ String meanScriptContent = RUtils.getMeanScriptContent();
+ polyglot.eval("R", meanScriptContent);
+ Value rBindings = polyglot.getBindings("R");
+ Value rInput = rBindings.getMember("c")
+ .execute(values);
+ return rBindings.getMember("customMean")
+ .execute(rInput)
+ .asDouble();
+ }
+
+}
\ No newline at end of file
diff --git a/libraries-data-2/src/main/java/com/baeldung/r/RCallerMean.java b/libraries-data-2/src/main/java/com/baeldung/r/RCallerMean.java
new file mode 100644
index 0000000000..99edb8c043
--- /dev/null
+++ b/libraries-data-2/src/main/java/com/baeldung/r/RCallerMean.java
@@ -0,0 +1,37 @@
+package com.baeldung.r;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+import com.github.rcaller.rstuff.RCaller;
+import com.github.rcaller.rstuff.RCallerOptions;
+import com.github.rcaller.rstuff.RCode;
+
+/**
+ * RCaller showcase.
+ *
+ * @author Donato Rimenti
+ */
+public class RCallerMean {
+
+ /**
+ * Invokes the customMean R function passing the given values as arguments.
+ *
+ * @param values the input to the mean script
+ * @return the result of the R script
+ * @throws IOException if any error occurs
+ * @throws URISyntaxException if any error occurs
+ */
+ public double mean(int[] values) throws IOException, URISyntaxException {
+ String fileContent = RUtils.getMeanScriptContent();
+ RCode code = RCode.create();
+ code.addRCode(fileContent);
+ code.addIntArray("input", values);
+ code.addRCode("result <- customMean(input)");
+ RCaller caller = RCaller.create(code, RCallerOptions.create());
+ caller.runAndReturnResult("result");
+ return caller.getParser()
+ .getAsDoubleArray("result")[0];
+ }
+
+}
\ No newline at end of file
diff --git a/libraries-data-2/src/main/java/com/baeldung/r/RUtils.java b/libraries-data-2/src/main/java/com/baeldung/r/RUtils.java
new file mode 100644
index 0000000000..a9393cdcc2
--- /dev/null
+++ b/libraries-data-2/src/main/java/com/baeldung/r/RUtils.java
@@ -0,0 +1,33 @@
+package com.baeldung.r;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.stream.Collectors;
+
+/**
+ * Utility class for loading the script.R content.
+ *
+ * @author Donato Rimenti
+ */
+public class RUtils {
+
+ /**
+ * Loads the script.R and returns its content as a string.
+ *
+ * @return the script.R content as a string
+ * @throws IOException if any error occurs
+ * @throws URISyntaxException if any error occurs
+ */
+ static String getMeanScriptContent() throws IOException, URISyntaxException {
+ URI rScriptUri = RUtils.class.getClassLoader()
+ .getResource("script.R")
+ .toURI();
+ Path inputScript = Paths.get(rScriptUri);
+ return Files.lines(inputScript)
+ .collect(Collectors.joining());
+ }
+}
\ No newline at end of file
diff --git a/libraries-data-2/src/main/java/com/baeldung/r/RenjinMean.java b/libraries-data-2/src/main/java/com/baeldung/r/RenjinMean.java
new file mode 100644
index 0000000000..4576ec5fb4
--- /dev/null
+++ b/libraries-data-2/src/main/java/com/baeldung/r/RenjinMean.java
@@ -0,0 +1,36 @@
+package com.baeldung.r;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+import javax.script.ScriptException;
+
+import org.renjin.script.RenjinScriptEngine;
+import org.renjin.sexp.DoubleArrayVector;
+
+/**
+ * Renjin showcase.
+ *
+ * @author Donato Rimenti
+ */
+public class RenjinMean {
+
+ /**
+ * Invokes the customMean R function passing the given values as arguments.
+ *
+ * @param values the input to the mean script
+ * @return the result of the R script
+ * @throws IOException if any error occurs
+ * @throws URISyntaxException if any error occurs
+ * @throws ScriptException if any error occurs
+ */
+ public double mean(int[] values) throws IOException, URISyntaxException, ScriptException {
+ RenjinScriptEngine engine = new RenjinScriptEngine();
+ String meanScriptContent = RUtils.getMeanScriptContent();
+ engine.put("input", values);
+ engine.eval(meanScriptContent);
+ DoubleArrayVector result = (DoubleArrayVector) engine.eval("customMean(input)");
+ return result.asReal();
+ }
+
+}
\ No newline at end of file
diff --git a/libraries-data-2/src/main/java/com/baeldung/r/RserveMean.java b/libraries-data-2/src/main/java/com/baeldung/r/RserveMean.java
new file mode 100644
index 0000000000..1aaa7fa847
--- /dev/null
+++ b/libraries-data-2/src/main/java/com/baeldung/r/RserveMean.java
@@ -0,0 +1,30 @@
+package com.baeldung.r;
+
+import org.rosuda.REngine.REXPMismatchException;
+import org.rosuda.REngine.REngineException;
+import org.rosuda.REngine.Rserve.RConnection;
+
+/**
+ * Rserve showcase.
+ *
+ * @author Donato Rimenti
+ */
+public class RserveMean {
+
+ /**
+ * Connects to the Rserve istance listening on 127.0.0.1:6311 and invokes the
+ * customMean R function passing the given values as arguments.
+ *
+ * @param values the input to the mean script
+ * @return the result of the R script
+ * @throws REngineException if any error occurs
+ * @throws REXPMismatchException if any error occurs
+ */
+ public double mean(int[] values) throws REngineException, REXPMismatchException {
+ RConnection c = new RConnection();
+ c.assign("input", values);
+ return c.eval("customMean(input)")
+ .asDouble();
+ }
+
+}
\ No newline at end of file
diff --git a/libraries-data-2/src/test/java/com/baeldung/r/FastRMeanUnitTest.java b/libraries-data-2/src/test/java/com/baeldung/r/FastRMeanUnitTest.java
new file mode 100644
index 0000000000..4e7426b75a
--- /dev/null
+++ b/libraries-data-2/src/test/java/com/baeldung/r/FastRMeanUnitTest.java
@@ -0,0 +1,29 @@
+package com.baeldung.r;
+
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * Test for {@link FastRMean}.
+ *
+ * @author Donato Rimenti
+ */
+@Ignore
+public class FastRMeanUnitTest {
+
+ /**
+ * Object to test.
+ */
+ private FastRMean fastrMean = new FastRMean();
+
+ /**
+ * Test for {@link FastRMeanUnitTest#mean(int[])}.
+ */
+ @Test
+ public void givenValues_whenMean_thenCorrect() {
+ int[] input = { 1, 2, 3, 4, 5 };
+ double result = fastrMean.mean(input);
+ Assert.assertEquals(3.0, result, 0.000001);
+ }
+}
\ No newline at end of file
diff --git a/libraries-data-2/src/test/java/com/baeldung/r/RCallerMeanIntegrationTest.java b/libraries-data-2/src/test/java/com/baeldung/r/RCallerMeanIntegrationTest.java
new file mode 100644
index 0000000000..ce6b3a4332
--- /dev/null
+++ b/libraries-data-2/src/test/java/com/baeldung/r/RCallerMeanIntegrationTest.java
@@ -0,0 +1,37 @@
+package com.baeldung.r;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+import javax.script.ScriptException;
+
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * Test for {@link RCallerMean}.
+ *
+ * @author Donato Rimenti
+ */
+@Ignore
+public class RCallerMeanIntegrationTest {
+
+ /**
+ * Object to test.
+ */
+ private RCallerMean rcallerMean = new RCallerMean();
+
+ /**
+ * Test for {@link RCallerMeanIntegrationTest#mean(int[])}.
+ *
+ * @throws ScriptException if an error occurs
+ * @throws URISyntaxException if an error occurs
+ */
+ @Test
+ public void givenValues_whenMean_thenCorrect() throws IOException, URISyntaxException {
+ int[] input = { 1, 2, 3, 4, 5 };
+ double result = rcallerMean.mean(input);
+ Assert.assertEquals(3.0, result, 0.000001);
+ }
+}
\ No newline at end of file
diff --git a/libraries-data-2/src/test/java/com/baeldung/r/RenjinMeanUnitTest.java b/libraries-data-2/src/test/java/com/baeldung/r/RenjinMeanUnitTest.java
new file mode 100644
index 0000000000..f52d37d614
--- /dev/null
+++ b/libraries-data-2/src/test/java/com/baeldung/r/RenjinMeanUnitTest.java
@@ -0,0 +1,37 @@
+package com.baeldung.r;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+import javax.script.ScriptException;
+
+import org.junit.Test;
+
+import org.junit.Assert;
+
+/**
+ * Test for {@link RenjinMean}.
+ *
+ * @author Donato Rimenti
+ */
+public class RenjinMeanUnitTest {
+
+ /**
+ * Object to test.
+ */
+ private RenjinMean renjinMean = new RenjinMean();
+
+ /**
+ * Test for {@link RenjinMeanUnitTest#mean(int[])}.
+ *
+ * @throws ScriptException if an error occurs
+ * @throws URISyntaxException if an error occurs
+ * @throws IOException if an error occurs
+ */
+ @Test
+ public void givenValues_whenMean_thenCorrect() throws IOException, URISyntaxException, ScriptException {
+ int[] input = { 1, 2, 3, 4, 5 };
+ double result = renjinMean.mean(input);
+ Assert.assertEquals(3.0, result, 0.000001);
+ }
+}
\ No newline at end of file
diff --git a/libraries-data-2/src/test/java/com/baeldung/r/RserveMeanIntegrationTest.java b/libraries-data-2/src/test/java/com/baeldung/r/RserveMeanIntegrationTest.java
new file mode 100644
index 0000000000..23d42bd8e9
--- /dev/null
+++ b/libraries-data-2/src/test/java/com/baeldung/r/RserveMeanIntegrationTest.java
@@ -0,0 +1,34 @@
+package com.baeldung.r;
+
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.rosuda.REngine.REXPMismatchException;
+import org.rosuda.REngine.REngineException;
+
+/**
+ * Test for {@link RserveMean}.
+ *
+ * @author Donato Rimenti
+ */
+@Ignore
+public class RserveMeanIntegrationTest {
+
+ /**
+ * Object to test.
+ */
+ private RserveMean rserveMean = new RserveMean();
+
+ /**
+ * Test for {@link RserveMeanIntegrationTest#mean(int[])}.
+ *
+ * @throws REXPMismatchException if an error occurs
+ * @throws REngineException if an error occurs
+ */
+ @Test
+ public void givenValues_whenMean_thenCorrect() throws REngineException, REXPMismatchException {
+ int[] input = { 1, 2, 3, 4, 5 };
+ double result = rserveMean.mean(input);
+ Assert.assertEquals(3.0, result, 0.000001);
+ }
+}
\ No newline at end of file
diff --git a/libraries-data-2/src/test/resources/script.R b/libraries-data-2/src/test/resources/script.R
new file mode 100644
index 0000000000..08f859cc3d
--- /dev/null
+++ b/libraries-data-2/src/test/resources/script.R
@@ -0,0 +1,3 @@
+customMean <- function(vector) {
+ mean(vector)
+}
\ No newline at end of file
diff --git a/persistence-modules/spring-data-dynamodb/pom.xml b/persistence-modules/spring-data-dynamodb/pom.xml
index fceceb40ba..377e35b635 100644
--- a/persistence-modules/spring-data-dynamodb/pom.xml
+++ b/persistence-modules/spring-data-dynamodb/pom.xml
@@ -9,9 +9,9 @@
com.baeldung
- parent-boot-1
+ parent-boot-2
0.0.1-SNAPSHOT
- ../../parent-boot-1
+ ../../parent-boot-2
@@ -19,7 +19,7 @@
org.springframework.data
spring-data-releasetrain
- Hopper-SR10
+ Lovelace-SR16
pom
import
@@ -174,7 +174,7 @@
com.baeldung.Application
4.3.4.RELEASE
4.5.2
- 4.4.1
+ 5.1.0
1.11.64
3.3.7-1
1.0.392
diff --git a/persistence-modules/spring-data-dynamodb/src/main/java/com/baeldung/spring/data/dynamodb/config/DynamoDBConfig.java b/persistence-modules/spring-data-dynamodb/src/main/java/com/baeldung/spring/data/dynamodb/config/DynamoDBConfig.java
index 9278c0a12e..7e97e6b383 100644
--- a/persistence-modules/spring-data-dynamodb/src/main/java/com/baeldung/spring/data/dynamodb/config/DynamoDBConfig.java
+++ b/persistence-modules/spring-data-dynamodb/src/main/java/com/baeldung/spring/data/dynamodb/config/DynamoDBConfig.java
@@ -44,8 +44,8 @@ public class DynamoDBConfig {
return new BasicAWSCredentials(amazonAWSAccessKey, amazonAWSSecretKey);
}
- @Bean(name = "mvcHandlerMappingIntrospector")
- public HandlerMappingIntrospector mvcHandlerMappingIntrospector() {
+ @Bean(name = "mvcHandlerMappingIntrospectorCustom")
+ public HandlerMappingIntrospector mvcHandlerMappingIntrospectorCustom() {
return new HandlerMappingIntrospector(context);
}
}
diff --git a/persistence-modules/spring-data-dynamodb/src/main/java/com/baeldung/spring/data/dynamodb/repositories/ProductInfoRepository.java b/persistence-modules/spring-data-dynamodb/src/main/java/com/baeldung/spring/data/dynamodb/repositories/ProductInfoRepository.java
index da47f033b6..6e8b493c3b 100644
--- a/persistence-modules/spring-data-dynamodb/src/main/java/com/baeldung/spring/data/dynamodb/repositories/ProductInfoRepository.java
+++ b/persistence-modules/spring-data-dynamodb/src/main/java/com/baeldung/spring/data/dynamodb/repositories/ProductInfoRepository.java
@@ -1,12 +1,13 @@
package com.baeldung.spring.data.dynamodb.repositories;
-import com.baeldung.spring.data.dynamodb.model.ProductInfo;
+import java.util.Optional;
+
import org.socialsignin.spring.data.dynamodb.repository.EnableScan;
import org.springframework.data.repository.CrudRepository;
-import java.util.List;
+import com.baeldung.spring.data.dynamodb.model.ProductInfo;
@EnableScan
public interface ProductInfoRepository extends CrudRepository {
- List findById(String id);
+ Optional findById(String id);
}
diff --git a/pom.xml b/pom.xml
index 02d1ab6cd2..44a94095b3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -652,6 +652,7 @@
spring-core
spring-core-2
spring-core-3
+ spring-core-4
spring-cucumber
spring-data-rest
@@ -1157,6 +1158,7 @@
spring-core
spring-core-2
spring-core-3
+ spring-core-4
spring-cucumber
spring-data-rest
diff --git a/spring-boot-groovy/src/main/groovy/com/baeldung/springwithgroovy/SpringBootGroovyApplication.groovy b/spring-boot-groovy/src/main/groovy/com/baeldung/springwithgroovy/SpringBootGroovyApplication.groovy
new file mode 100644
index 0000000000..4912b75a66
--- /dev/null
+++ b/spring-boot-groovy/src/main/groovy/com/baeldung/springwithgroovy/SpringBootGroovyApplication.groovy
@@ -0,0 +1,13 @@
+package com.baeldung.springwithgroovy
+
+import org.springframework.boot.SpringApplication
+import org.springframework.boot.autoconfigure.SpringBootApplication
+
+import com.baeldung.springwithgroovy.SpringBootGroovyApplication
+
+@SpringBootApplication
+class SpringBootGroovyApplication {
+ static void main(String[] args) {
+ SpringApplication.run SpringBootGroovyApplication, args
+ }
+}
diff --git a/spring-boot-groovy/src/main/groovy/com/baeldung/springwithgroovy/controller/TodoController.groovy b/spring-boot-groovy/src/main/groovy/com/baeldung/springwithgroovy/controller/TodoController.groovy
new file mode 100644
index 0000000000..9c6aee20d3
--- /dev/null
+++ b/spring-boot-groovy/src/main/groovy/com/baeldung/springwithgroovy/controller/TodoController.groovy
@@ -0,0 +1,48 @@
+package com.baeldung.springwithgroovy.controller
+
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.web.bind.annotation.DeleteMapping
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.PathVariable
+import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.PutMapping
+import org.springframework.web.bind.annotation.RequestBody
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestMethod
+import org.springframework.web.bind.annotation.RestController
+
+import com.baeldung.springwithgroovy.entity.Todo
+import com.baeldung.springwithgroovy.service.TodoService
+
+@RestController
+@RequestMapping('todo')
+public class TodoController {
+
+ @Autowired
+ TodoService todoService
+
+ @GetMapping
+ List getAllTodoList(){
+ todoService.findAll()
+ }
+
+ @PostMapping
+ Todo saveTodo(@RequestBody Todo todo){
+ todoService.saveTodo todo
+ }
+
+ @PutMapping
+ Todo updateTodo(@RequestBody Todo todo){
+ todoService.updateTodo todo
+ }
+
+ @DeleteMapping('/{todoId}')
+ deleteTodo(@PathVariable Integer todoId){
+ todoService.deleteTodo todoId
+ }
+
+ @GetMapping('/{todoId}')
+ Todo getTodoById(@PathVariable Integer todoId){
+ todoService.findById todoId
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-groovy/src/main/groovy/com/baeldung/springwithgroovy/entity/Todo.groovy b/spring-boot-groovy/src/main/groovy/com/baeldung/springwithgroovy/entity/Todo.groovy
new file mode 100644
index 0000000000..000d981701
--- /dev/null
+++ b/spring-boot-groovy/src/main/groovy/com/baeldung/springwithgroovy/entity/Todo.groovy
@@ -0,0 +1,23 @@
+package com.baeldung.springwithgroovy.entity
+
+import javax.persistence.Column
+import javax.persistence.Entity
+import javax.persistence.GeneratedValue
+import javax.persistence.GenerationType
+import javax.persistence.Id
+import javax.persistence.Table
+
+@Entity
+@Table(name = 'todo')
+class Todo {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ Integer id
+
+ @Column
+ String task
+
+ @Column
+ Boolean isCompleted
+
+}
diff --git a/spring-boot-groovy/src/main/groovy/com/baeldung/springwithgroovy/repository/TodoRepository.groovy b/spring-boot-groovy/src/main/groovy/com/baeldung/springwithgroovy/repository/TodoRepository.groovy
new file mode 100644
index 0000000000..eb58cc0791
--- /dev/null
+++ b/spring-boot-groovy/src/main/groovy/com/baeldung/springwithgroovy/repository/TodoRepository.groovy
@@ -0,0 +1,9 @@
+package com.baeldung.springwithgroovy.repository
+
+import org.springframework.data.jpa.repository.JpaRepository
+import org.springframework.stereotype.Repository
+
+import com.baeldung.springwithgroovy.entity.Todo
+
+@Repository
+interface TodoRepository extends JpaRepository {}
\ No newline at end of file
diff --git a/spring-boot-groovy/src/main/groovy/com/baeldung/springwithgroovy/service/TodoService.groovy b/spring-boot-groovy/src/main/groovy/com/baeldung/springwithgroovy/service/TodoService.groovy
new file mode 100644
index 0000000000..c57af34cde
--- /dev/null
+++ b/spring-boot-groovy/src/main/groovy/com/baeldung/springwithgroovy/service/TodoService.groovy
@@ -0,0 +1,16 @@
+package com.baeldung.springwithgroovy.service
+
+import com.baeldung.springwithgroovy.entity.Todo
+
+interface TodoService {
+
+ List findAll()
+
+ Todo findById(Integer todoId)
+
+ Todo saveTodo(Todo todo)
+
+ Todo updateTodo(Todo todo)
+
+ Todo deleteTodo(Integer todoId)
+}
diff --git a/spring-boot-groovy/src/main/groovy/com/baeldung/springwithgroovy/service/impl/TodoServiceImpl.groovy b/spring-boot-groovy/src/main/groovy/com/baeldung/springwithgroovy/service/impl/TodoServiceImpl.groovy
new file mode 100644
index 0000000000..943c1c6944
--- /dev/null
+++ b/spring-boot-groovy/src/main/groovy/com/baeldung/springwithgroovy/service/impl/TodoServiceImpl.groovy
@@ -0,0 +1,40 @@
+package com.baeldung.springwithgroovy.service.impl
+
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.stereotype.Service
+
+import com.baeldung.springwithgroovy.entity.Todo
+import com.baeldung.springwithgroovy.repository.TodoRepository
+import com.baeldung.springwithgroovy.service.TodoService
+
+@Service
+class TodoServiceImpl implements TodoService {
+
+ @Autowired
+ TodoRepository todoRepository
+
+ @Override
+ List findAll() {
+ todoRepository.findAll()
+ }
+
+ @Override
+ Todo findById(Integer todoId) {
+ todoRepository.findById todoId get()
+ }
+
+ @Override
+ Todo saveTodo(Todo todo){
+ todoRepository.save todo
+ }
+
+ @Override
+ Todo updateTodo(Todo todo){
+ todoRepository.save todo
+ }
+
+ @Override
+ Todo deleteTodo(Integer todoId){
+ todoRepository.deleteById todoId
+ }
+}
diff --git a/spring-boot-groovy/src/test/groovy/com/baeldung/springwithgroovy/TodoAppUnitTest.groovy b/spring-boot-groovy/src/test/groovy/com/baeldung/springwithgroovy/TodoAppUnitTest.groovy
new file mode 100644
index 0000000000..bf8b0ff27f
--- /dev/null
+++ b/spring-boot-groovy/src/test/groovy/com/baeldung/springwithgroovy/TodoAppUnitTest.groovy
@@ -0,0 +1,97 @@
+package com.baeldung.springwithgroovy
+
+import static org.junit.jupiter.api.Assertions.assertEquals
+import static org.junit.jupiter.api.Assertions.assertTrue
+
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.http.HttpStatus
+import org.springframework.http.MediaType
+import org.springframework.test.context.event.annotation.BeforeTestClass
+import org.springframework.test.context.junit4.SpringRunner
+
+import com.baeldung.springwithgroovy.entity.Todo
+
+import io.restassured.RestAssured
+import io.restassured.response.Response
+
+class TodoAppUnitTest {
+ static API_ROOT = 'http://localhost:8081/todo'
+ static readingTodoId
+ static writingTodoId
+
+ @BeforeClass
+ static void populateDummyData() {
+ Todo readingTodo = new Todo(task: 'Reading', isCompleted: false)
+ Todo writingTodo = new Todo(task: 'Writing', isCompleted: false)
+
+ final Response readingResponse =
+ RestAssured.given()
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body(readingTodo).post(API_ROOT)
+
+ Todo cookingTodoResponse = readingResponse.as Todo.class
+ readingTodoId = cookingTodoResponse.getId()
+
+ final Response writingResponse =
+ RestAssured.given()
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body(writingTodo).post(API_ROOT)
+
+ Todo writingTodoResponse = writingResponse.as Todo.class
+ writingTodoId = writingTodoResponse.getId()
+ }
+
+ @Test
+ void whenGetAllTodoList_thenOk(){
+ final Response response = RestAssured.get(API_ROOT)
+
+ assertEquals HttpStatus.OK.value(),response.getStatusCode()
+ assertTrue response.as(List.class).size() > 0
+ }
+
+ @Test
+ void whenGetTodoById_thenOk(){
+ final Response response =
+ RestAssured.get("$API_ROOT/$readingTodoId")
+
+ assertEquals HttpStatus.OK.value(),response.getStatusCode()
+ Todo todoResponse = response.as Todo.class
+ assertEquals readingTodoId,todoResponse.getId()
+ }
+
+ @Test
+ void whenUpdateTodoById_thenOk(){
+ Todo todo = new Todo(id:readingTodoId, isCompleted: true)
+ final Response response =
+ RestAssured.given()
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body(todo).put(API_ROOT)
+
+ assertEquals HttpStatus.OK.value(),response.getStatusCode()
+ Todo todoResponse = response.as Todo.class
+ assertTrue todoResponse.getIsCompleted()
+ }
+
+ @Test
+ void whenDeleteTodoById_thenOk(){
+ final Response response =
+ RestAssured.given()
+ .delete("$API_ROOT/$writingTodoId")
+
+ assertEquals HttpStatus.OK.value(),response.getStatusCode()
+ }
+
+ @Test
+ void whenSaveTodo_thenOk(){
+ Todo todo = new Todo(task: 'Blogging', isCompleted: false)
+ final Response response =
+ RestAssured.given()
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body(todo).post(API_ROOT)
+
+ assertEquals HttpStatus.OK.value(),response.getStatusCode()
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-custom-starter/greeter-spring-boot-autoconfigure/src/test/java/org/baeldung/SpringContextTest.java b/spring-boot-modules/spring-boot-custom-starter/greeter-spring-boot-autoconfigure/src/test/java/com/baeldung/SpringContextTest.java
similarity index 100%
rename from spring-boot-modules/spring-boot-custom-starter/greeter-spring-boot-autoconfigure/src/test/java/org/baeldung/SpringContextTest.java
rename to spring-boot-modules/spring-boot-custom-starter/greeter-spring-boot-autoconfigure/src/test/java/com/baeldung/SpringContextTest.java
diff --git a/spring-boot-modules/spring-boot-custom-starter/greeter-spring-boot-sample-app/pom.xml b/spring-boot-modules/spring-boot-custom-starter/greeter-spring-boot-sample-app/pom.xml
index 818ce5c107..8d328b88be 100644
--- a/spring-boot-modules/spring-boot-custom-starter/greeter-spring-boot-sample-app/pom.xml
+++ b/spring-boot-modules/spring-boot-custom-starter/greeter-spring-boot-sample-app/pom.xml
@@ -8,9 +8,9 @@
com.baeldung
- parent-boot-1
+ parent-boot-2
0.0.1-SNAPSHOT
- ../../../parent-boot-1
+ ../../../parent-boot-2
diff --git a/spring-boot-modules/spring-boot-custom-starter/greeter-spring-boot-sample-app/src/test/java/org/baeldung/SpringContextTest.java b/spring-boot-modules/spring-boot-custom-starter/greeter-spring-boot-sample-app/src/test/java/com/baeldung/SpringContextTest.java
similarity index 100%
rename from spring-boot-modules/spring-boot-custom-starter/greeter-spring-boot-sample-app/src/test/java/org/baeldung/SpringContextTest.java
rename to spring-boot-modules/spring-boot-custom-starter/greeter-spring-boot-sample-app/src/test/java/com/baeldung/SpringContextTest.java
diff --git a/spring-boot-modules/spring-boot-custom-starter/greeter/pom.xml b/spring-boot-modules/spring-boot-custom-starter/greeter/pom.xml
index 89119e2e99..47296990aa 100644
--- a/spring-boot-modules/spring-boot-custom-starter/greeter/pom.xml
+++ b/spring-boot-modules/spring-boot-custom-starter/greeter/pom.xml
@@ -8,9 +8,9 @@
com.baeldung
- parent-boot-1
+ parent-boot-2
0.0.1-SNAPSHOT
- ../../../parent-boot-1
+ ../../../parent-boot-2
\ No newline at end of file
diff --git a/spring-core-4/README.md b/spring-core-4/README.md
new file mode 100644
index 0000000000..f882c77179
--- /dev/null
+++ b/spring-core-4/README.md
@@ -0,0 +1,7 @@
+## Spring Core
+
+This module contains articles about core Spring functionality
+
+## Relevant Articles:
+
+- More articles: [[<-- prev]](/spring-core-3)
diff --git a/spring-core-4/pom.xml b/spring-core-4/pom.xml
new file mode 100644
index 0000000000..53f7ca6912
--- /dev/null
+++ b/spring-core-4/pom.xml
@@ -0,0 +1,63 @@
+
+
+ 4.0.0
+ spring-core-4
+ spring-core-4
+
+
+ com.baeldung
+ parent-spring-5
+ 0.0.1-SNAPSHOT
+ ../parent-spring-5
+
+
+
+
+ org.springframework
+ spring-context
+ ${spring.version}
+
+
+ org.springframework
+ spring-core
+ ${spring.version}
+
+
+ org.springframework
+ spring-test
+ ${spring.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit-jupiter.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit-jupiter.version}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ ${maven.surefire.version}
+
+
+
+
+
+ 2.22.1
+ 1.3.2
+ 2.2.2.RELEASE
+
+
+
\ No newline at end of file
diff --git a/spring-core-4/src/main/java/com/baeldung/factorymethod/Bar.java b/spring-core-4/src/main/java/com/baeldung/factorymethod/Bar.java
new file mode 100644
index 0000000000..22ef5b3429
--- /dev/null
+++ b/spring-core-4/src/main/java/com/baeldung/factorymethod/Bar.java
@@ -0,0 +1,18 @@
+package com.baeldung.factorymethod;
+
+public class Bar {
+
+ private String name;
+
+ public Bar(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/spring-core-4/src/main/java/com/baeldung/factorymethod/Foo.java b/spring-core-4/src/main/java/com/baeldung/factorymethod/Foo.java
new file mode 100644
index 0000000000..54bd0c9ff4
--- /dev/null
+++ b/spring-core-4/src/main/java/com/baeldung/factorymethod/Foo.java
@@ -0,0 +1,5 @@
+package com.baeldung.factorymethod;
+
+public class Foo {
+
+}
diff --git a/spring-core-4/src/main/java/com/baeldung/factorymethod/InstanceBarFactory.java b/spring-core-4/src/main/java/com/baeldung/factorymethod/InstanceBarFactory.java
new file mode 100644
index 0000000000..f834b82aee
--- /dev/null
+++ b/spring-core-4/src/main/java/com/baeldung/factorymethod/InstanceBarFactory.java
@@ -0,0 +1,8 @@
+package com.baeldung.factorymethod;
+
+public class InstanceBarFactory {
+
+ public Bar createInstance(String name) {
+ return new Bar(name);
+ }
+}
diff --git a/spring-core-4/src/main/java/com/baeldung/factorymethod/InstanceFooFactory.java b/spring-core-4/src/main/java/com/baeldung/factorymethod/InstanceFooFactory.java
new file mode 100644
index 0000000000..c3125d3339
--- /dev/null
+++ b/spring-core-4/src/main/java/com/baeldung/factorymethod/InstanceFooFactory.java
@@ -0,0 +1,8 @@
+package com.baeldung.factorymethod;
+
+public class InstanceFooFactory {
+
+ public Foo createInstance() {
+ return new Foo();
+ }
+}
diff --git a/spring-core-4/src/main/java/com/baeldung/factorymethod/SingletonBarFactory.java b/spring-core-4/src/main/java/com/baeldung/factorymethod/SingletonBarFactory.java
new file mode 100644
index 0000000000..93802819b1
--- /dev/null
+++ b/spring-core-4/src/main/java/com/baeldung/factorymethod/SingletonBarFactory.java
@@ -0,0 +1,11 @@
+package com.baeldung.factorymethod;
+
+public class SingletonBarFactory {
+
+ private static final Bar INSTANCE = new Bar("unnamed");
+
+ public static Bar createInstance(String name) {
+ INSTANCE.setName(name);
+ return INSTANCE;
+ }
+}
diff --git a/spring-core-4/src/main/java/com/baeldung/factorymethod/SingletonFooFactory.java b/spring-core-4/src/main/java/com/baeldung/factorymethod/SingletonFooFactory.java
new file mode 100644
index 0000000000..77d56cc7f6
--- /dev/null
+++ b/spring-core-4/src/main/java/com/baeldung/factorymethod/SingletonFooFactory.java
@@ -0,0 +1,10 @@
+package com.baeldung.factorymethod;
+
+public class SingletonFooFactory {
+
+ private static final Foo INSTANCE = new Foo();
+
+ public static Foo createInstance() {
+ return INSTANCE;
+ }
+}
diff --git a/spring-core-4/src/test/java/com/baeldung/factorymethod/InstanceBarFactoryIntegrationTest.java b/spring-core-4/src/test/java/com/baeldung/factorymethod/InstanceBarFactoryIntegrationTest.java
new file mode 100644
index 0000000000..b5728316e7
--- /dev/null
+++ b/spring-core-4/src/test/java/com/baeldung/factorymethod/InstanceBarFactoryIntegrationTest.java
@@ -0,0 +1,24 @@
+package com.baeldung.factorymethod;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration("/factorymethod/instance-bar-config.xml")
+public class InstanceBarFactoryIntegrationTest {
+
+ @Autowired
+ private Bar instance;
+
+ @Test
+ public void givenValidInstanceFactoryConfig_whenCreateInstance_thenNameIsCorrect() {
+ assertNotNull(instance);
+ assertEquals("someName", instance.getName());
+ }
+}
diff --git a/spring-core-4/src/test/java/com/baeldung/factorymethod/InstanceFooFactoryIntegrationTest.java b/spring-core-4/src/test/java/com/baeldung/factorymethod/InstanceFooFactoryIntegrationTest.java
new file mode 100644
index 0000000000..6b1857c2f2
--- /dev/null
+++ b/spring-core-4/src/test/java/com/baeldung/factorymethod/InstanceFooFactoryIntegrationTest.java
@@ -0,0 +1,22 @@
+package com.baeldung.factorymethod;
+
+import static org.junit.Assert.assertNotNull;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration("/factorymethod/instance-foo-config.xml")
+public class InstanceFooFactoryIntegrationTest {
+
+ @Autowired
+ private Foo foo;
+
+ @Test
+ public void givenValidInstanceFactoryConfig_whenCreateFooInstance_thenInstanceIsNotNull() {
+ assertNotNull(foo);
+ }
+}
diff --git a/spring-core-4/src/test/java/com/baeldung/factorymethod/SingletonBarFactoryIntegrationTest.java b/spring-core-4/src/test/java/com/baeldung/factorymethod/SingletonBarFactoryIntegrationTest.java
new file mode 100644
index 0000000000..403b46156b
--- /dev/null
+++ b/spring-core-4/src/test/java/com/baeldung/factorymethod/SingletonBarFactoryIntegrationTest.java
@@ -0,0 +1,24 @@
+package com.baeldung.factorymethod;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration("/factorymethod/static-bar-config.xml")
+public class SingletonBarFactoryIntegrationTest {
+
+ @Autowired
+ private Bar instance;
+
+ @Test
+ public void givenValidStaticFactoryConfig_whenCreateInstance_thenNameIsCorrect() {
+ assertNotNull(instance);
+ assertEquals("someName", instance.getName());
+ }
+}
diff --git a/spring-core-4/src/test/java/com/baeldung/factorymethod/SingletonFooFactoryIntegrationTest.java b/spring-core-4/src/test/java/com/baeldung/factorymethod/SingletonFooFactoryIntegrationTest.java
new file mode 100644
index 0000000000..52154b81ab
--- /dev/null
+++ b/spring-core-4/src/test/java/com/baeldung/factorymethod/SingletonFooFactoryIntegrationTest.java
@@ -0,0 +1,22 @@
+package com.baeldung.factorymethod;
+
+import static org.junit.Assert.assertNotNull;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration("/factorymethod/static-foo-config.xml")
+public class SingletonFooFactoryIntegrationTest {
+
+ @Autowired
+ private Foo singleton;
+
+ @Test
+ public void givenValidStaticFactoryConfig_whenCreateInstance_thenInstanceIsNotNull() {
+ assertNotNull(singleton);
+ }
+}
diff --git a/spring-core-4/src/test/resources/factorymethod/instance-bar-config.xml b/spring-core-4/src/test/resources/factorymethod/instance-bar-config.xml
new file mode 100644
index 0000000000..a4281aee4e
--- /dev/null
+++ b/spring-core-4/src/test/resources/factorymethod/instance-bar-config.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-core-4/src/test/resources/factorymethod/instance-foo-config.xml b/spring-core-4/src/test/resources/factorymethod/instance-foo-config.xml
new file mode 100644
index 0000000000..0f21f06f5a
--- /dev/null
+++ b/spring-core-4/src/test/resources/factorymethod/instance-foo-config.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-core-4/src/test/resources/factorymethod/static-bar-config.xml b/spring-core-4/src/test/resources/factorymethod/static-bar-config.xml
new file mode 100644
index 0000000000..2cacc293bc
--- /dev/null
+++ b/spring-core-4/src/test/resources/factorymethod/static-bar-config.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-core-4/src/test/resources/factorymethod/static-foo-config.xml b/spring-core-4/src/test/resources/factorymethod/static-foo-config.xml
new file mode 100644
index 0000000000..ffe1480638
--- /dev/null
+++ b/spring-core-4/src/test/resources/factorymethod/static-foo-config.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-security-modules/spring-security-core/src/main/java/com/baeldung/app/controller/TaskController.java b/spring-security-modules/spring-security-core/src/main/java/com/baeldung/app/controller/TaskController.java
index a084f14eca..67072b5d61 100644
--- a/spring-security-modules/spring-security-core/src/main/java/com/baeldung/app/controller/TaskController.java
+++ b/spring-security-modules/spring-security-core/src/main/java/com/baeldung/app/controller/TaskController.java
@@ -1,8 +1,15 @@
package com.baeldung.app.controller;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@@ -10,6 +17,8 @@ import org.springframework.web.bind.annotation.RequestMethod;
import com.baeldung.app.entity.Task;
import com.baeldung.app.service.TaskService;
+import javax.servlet.http.HttpServletRequest;
+
@Controller
@RequestMapping("api/tasks")
public class TaskController {
@@ -17,6 +26,9 @@ public class TaskController {
@Autowired
private TaskService taskService;
+ @Autowired(required = false)
+ private UserDetailsService userDetailsService;
+
@RequestMapping(method = RequestMethod.GET)
public ResponseEntity> findAllTasks() {
Iterable tasks = taskService.findAll();
@@ -30,4 +42,62 @@ public class TaskController {
return ResponseEntity.ok().body(tasks);
}
+
+ /**
+ * Example of restricting specific endpoints to specific roles using @PreAuthorize.
+ */
+ @GetMapping("/manager")
+ @PreAuthorize("hasRole('ROLE_MANAGER')")
+ public ResponseEntity> getAlManagerTasks() {
+ Iterable tasks = taskService.findAll();
+
+ return ResponseEntity.ok().body(tasks);
+ }
+
+ /**
+ * Example of restricting specific endpoints to specific roles using SecurityContext.
+ */
+ @GetMapping("/actuator")
+ public ResponseEntity> getAlActuatorTasks() {
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+ if (auth != null && auth.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ACTUATOR")))
+ {
+ return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
+ }
+
+ Iterable tasks = taskService.findAll();
+
+ return ResponseEntity.ok().body(tasks);
+ }
+
+ /**
+ * Example of restricting specific endpoints to specific roles using UserDetailsService.
+ */
+ @GetMapping("/admin")
+ public ResponseEntity> getAlAdminTasks() {
+ if(userDetailsService != null) {
+ UserDetails details = userDetailsService.loadUserByUsername("pam");
+ if (details != null && details.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ADMIN"))) {
+ return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
+ }
+ }
+
+ Iterable tasks = taskService.findAll();
+
+ return ResponseEntity.ok().body(tasks);
+ }
+
+ /**
+ * Example of restricting specific endpoints to specific roles using HttpServletRequest.
+ */
+ @GetMapping("/admin2")
+ public ResponseEntity> getAlAdminTasksUsingServlet(HttpServletRequest request) {
+ if (!request.isUserInRole("ROLE_ADMIN")) {
+ return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
+ }
+
+ Iterable tasks = taskService.findAll();
+
+ return ResponseEntity.ok().body(tasks);
+ }
}
diff --git a/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/CustomLogoutApplication.java b/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/CustomLogoutApplication.java
new file mode 100644
index 0000000000..39d867b1f4
--- /dev/null
+++ b/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/CustomLogoutApplication.java
@@ -0,0 +1,13 @@
+package com.baeldung.customlogouthandler;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class CustomLogoutApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(CustomLogoutApplication.class, args);
+ }
+
+}
diff --git a/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/MvcConfiguration.java b/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/MvcConfiguration.java
new file mode 100644
index 0000000000..c363effb4e
--- /dev/null
+++ b/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/MvcConfiguration.java
@@ -0,0 +1,55 @@
+package com.baeldung.customlogouthandler;
+
+import javax.sql.DataSource;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
+
+import com.baeldung.customlogouthandler.web.CustomLogoutHandler;
+
+@Configuration
+@EnableWebSecurity
+public class MvcConfiguration extends WebSecurityConfigurerAdapter {
+
+ @Autowired
+ private DataSource dataSource;
+
+ @Autowired
+ private CustomLogoutHandler logoutHandler;
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http.httpBasic()
+ .and()
+ .authorizeRequests()
+ .antMatchers(HttpMethod.GET, "/user/**")
+ .hasRole("USER")
+ .and()
+ .logout()
+ .logoutUrl("/user/logout")
+ .addLogoutHandler(logoutHandler)
+ .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))
+ .permitAll()
+ .and()
+ .csrf()
+ .disable()
+ .formLogin()
+ .disable();
+ }
+
+ @Override
+ protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+ auth.jdbcAuthentication()
+ .dataSource(dataSource)
+ .usersByUsernameQuery("select login, password, true from users where login=?")
+ .authoritiesByUsernameQuery("select login, role from users where login=?");
+ }
+
+}
diff --git a/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/services/UserCache.java b/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/services/UserCache.java
new file mode 100644
index 0000000000..b86edc0dee
--- /dev/null
+++ b/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/services/UserCache.java
@@ -0,0 +1,35 @@
+package com.baeldung.customlogouthandler.services;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+
+import org.springframework.stereotype.Service;
+
+import com.baeldung.customlogouthandler.user.User;
+
+@Service
+public class UserCache {
+
+ @PersistenceContext
+ private EntityManager entityManager;
+
+ private final ConcurrentMap store = new ConcurrentHashMap<>(256);
+
+ public User getByUserName(String userName) {
+ return store.computeIfAbsent(userName, k -> entityManager.createQuery("from User where login=:login", User.class)
+ .setParameter("login", k)
+ .getSingleResult());
+ }
+
+ public void evictUser(String userName) {
+ store.remove(userName);
+ }
+
+ public int size() {
+ return store.size();
+ }
+
+}
diff --git a/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/user/User.java b/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/user/User.java
new file mode 100644
index 0000000000..ca3a998c5c
--- /dev/null
+++ b/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/user/User.java
@@ -0,0 +1,61 @@
+package com.baeldung.customlogouthandler.user;
+
+import javax.persistence.*;
+
+@Entity
+@Table(name = "users")
+public class User {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Integer id;
+
+ @Column(unique = true)
+ private String login;
+
+ private String password;
+
+ private String role;
+
+ private String language;
+
+ public Integer getId() {
+ return id;
+ }
+
+ public void setId(Integer id) {
+ this.id = id;
+ }
+
+ public String getLogin() {
+ return login;
+ }
+
+ public void setLogin(String login) {
+ this.login = login;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getRole() {
+ return role;
+ }
+
+ public void setRole(String role) {
+ this.role = role;
+ }
+
+ public String getLanguage() {
+ return language;
+ }
+
+ public void setLanguage(String language) {
+ this.language = language;
+ }
+}
diff --git a/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/user/UserUtils.java b/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/user/UserUtils.java
new file mode 100644
index 0000000000..aa9a521b01
--- /dev/null
+++ b/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/user/UserUtils.java
@@ -0,0 +1,14 @@
+package com.baeldung.customlogouthandler.user;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+public class UserUtils {
+
+ public static String getAuthenticatedUserName() {
+ Authentication auth = SecurityContextHolder.getContext()
+ .getAuthentication();
+ return auth != null ? ((org.springframework.security.core.userdetails.User) auth.getPrincipal()).getUsername() : null;
+ }
+
+}
diff --git a/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/web/CustomLogoutHandler.java b/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/web/CustomLogoutHandler.java
new file mode 100644
index 0000000000..a89c9a570d
--- /dev/null
+++ b/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/web/CustomLogoutHandler.java
@@ -0,0 +1,28 @@
+package com.baeldung.customlogouthandler.web;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.logout.LogoutHandler;
+import org.springframework.stereotype.Service;
+
+import com.baeldung.customlogouthandler.services.UserCache;
+import com.baeldung.customlogouthandler.user.UserUtils;
+
+@Service
+public class CustomLogoutHandler implements LogoutHandler {
+
+ private final UserCache userCache;
+
+ public CustomLogoutHandler(UserCache userCache) {
+ this.userCache = userCache;
+ }
+
+ @Override
+ public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
+ String userName = UserUtils.getAuthenticatedUserName();
+ userCache.evictUser(userName);
+ }
+
+}
diff --git a/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/web/UserController.java b/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/web/UserController.java
new file mode 100644
index 0000000000..b2d332a1bb
--- /dev/null
+++ b/spring-security-modules/spring-security-mvc-boot-2/src/main/java/com/baeldung/customlogouthandler/web/UserController.java
@@ -0,0 +1,30 @@
+package com.baeldung.customlogouthandler.web;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import com.baeldung.customlogouthandler.services.UserCache;
+import com.baeldung.customlogouthandler.user.User;
+import com.baeldung.customlogouthandler.user.UserUtils;
+
+@Controller
+@RequestMapping(path = "/user")
+public class UserController {
+
+ private final UserCache userCache;
+
+ public UserController(UserCache userCache) {
+ this.userCache = userCache;
+ }
+
+ @GetMapping(path = "/language")
+ @ResponseBody
+ public String getLanguage() {
+ String userName = UserUtils.getAuthenticatedUserName();
+ User user = userCache.getByUserName(userName);
+ return user.getLanguage();
+ }
+
+}
diff --git a/spring-security-modules/spring-security-mvc-boot-2/src/main/resources/application-customlogouthandler.properties b/spring-security-modules/spring-security-mvc-boot-2/src/main/resources/application-customlogouthandler.properties
new file mode 100644
index 0000000000..2cb766378d
--- /dev/null
+++ b/spring-security-modules/spring-security-mvc-boot-2/src/main/resources/application-customlogouthandler.properties
@@ -0,0 +1,5 @@
+spring.datasource.url=jdbc:postgresql://localhost:5432/test
+spring.datasource.username=test
+spring.datasource.password=test
+
+spring.jpa.hibernate.ddl-auto=create
\ No newline at end of file
diff --git a/spring-security-modules/spring-security-mvc-boot-2/src/test/java/com/baeldung/customlogouthandler/CustomLogoutHandlerIntegrationTest.java b/spring-security-modules/spring-security-mvc-boot-2/src/test/java/com/baeldung/customlogouthandler/CustomLogoutHandlerIntegrationTest.java
new file mode 100644
index 0000000000..cd8a1a72d6
--- /dev/null
+++ b/spring-security-modules/spring-security-mvc-boot-2/src/test/java/com/baeldung/customlogouthandler/CustomLogoutHandlerIntegrationTest.java
@@ -0,0 +1,108 @@
+package com.baeldung.customlogouthandler;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.boot.web.server.LocalServerPort;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.jdbc.SqlGroup;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+import com.baeldung.customlogouthandler.services.UserCache;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@SpringBootTest(classes = { CustomLogoutApplication.class }, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@SqlGroup({ @Sql(value = "classpath:customlogouthandler/before.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), @Sql(value = "classpath:customlogouthandler/after.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) })
+@TestPropertySource(locations="classpath:customlogouthandler/application.properties")
+class CustomLogoutHandlerIntegrationTest {
+
+ @Autowired
+ private TestRestTemplate restTemplate;
+
+ @Autowired
+ private UserCache userCache;
+
+ @LocalServerPort
+ private int port;
+
+ @Test
+ public void whenLogin_thenUseUserCache() {
+ // User cache should be empty on start
+ assertThat(userCache.size()).isEqualTo(0);
+
+ // Request using first login
+ ResponseEntity response = restTemplate.withBasicAuth("user", "pass")
+ .getForEntity(getLanguageUrl(), String.class);
+
+ assertThat(response.getBody()).contains("english");
+
+ // User cache must contain the user
+ assertThat(userCache.size()).isEqualTo(1);
+
+ // Getting the session cookie
+ HttpHeaders requestHeaders = new HttpHeaders();
+ requestHeaders.add("Cookie", response.getHeaders()
+ .getFirst(HttpHeaders.SET_COOKIE));
+
+ // Request with the session cookie
+ response = restTemplate.exchange(getLanguageUrl(), HttpMethod.GET, new HttpEntity(requestHeaders), String.class);
+ assertThat(response.getBody()).contains("english");
+
+ // Logging out using the session cookies
+ response = restTemplate.exchange(getLogoutUrl(), HttpMethod.GET, new HttpEntity(requestHeaders), String.class);
+ assertThat(response.getStatusCode()
+ .value()).isEqualTo(200);
+ }
+
+ @Test
+ public void whenLogout_thenCacheIsEmpty() {
+ // User cache should be empty on start
+ assertThat(userCache.size()).isEqualTo(0);
+
+ // Request using first login
+ ResponseEntity response = restTemplate.withBasicAuth("user", "pass")
+ .getForEntity(getLanguageUrl(), String.class);
+
+ assertThat(response.getBody()).contains("english");
+
+ // User cache must contain the user
+ assertThat(userCache.size()).isEqualTo(1);
+
+ // Getting the session cookie
+ HttpHeaders requestHeaders = new HttpHeaders();
+ requestHeaders.add("Cookie", response.getHeaders()
+ .getFirst(HttpHeaders.SET_COOKIE));
+
+ // Logging out using the session cookies
+ response = restTemplate.exchange(getLogoutUrl(), HttpMethod.GET, new HttpEntity(requestHeaders), String.class);
+ assertThat(response.getStatusCode()
+ .value()).isEqualTo(200);
+
+ // User cache must be empty now
+ // this is the reaction on custom logout filter execution
+ assertThat(userCache.size()).isEqualTo(0);
+
+ // Assert unauthorized request
+ response = restTemplate.exchange(getLanguageUrl(), HttpMethod.GET, new HttpEntity(requestHeaders), String.class);
+ assertThat(response.getStatusCode()
+ .value()).isEqualTo(401);
+ }
+
+ private String getLanguageUrl() {
+ return "http://localhost:" + port + "/user/language";
+ }
+
+ private String getLogoutUrl() {
+ return "http://localhost:" + port + "/user/logout";
+ }
+
+}
diff --git a/spring-security-modules/spring-security-mvc-boot-2/src/test/resources/customlogouthandler/after.sql b/spring-security-modules/spring-security-mvc-boot-2/src/test/resources/customlogouthandler/after.sql
new file mode 100644
index 0000000000..df6f312987
--- /dev/null
+++ b/spring-security-modules/spring-security-mvc-boot-2/src/test/resources/customlogouthandler/after.sql
@@ -0,0 +1 @@
+delete from users;
\ No newline at end of file
diff --git a/spring-security-modules/spring-security-mvc-boot-2/src/test/resources/customlogouthandler/application.properties b/spring-security-modules/spring-security-mvc-boot-2/src/test/resources/customlogouthandler/application.properties
new file mode 100644
index 0000000000..9edd853f2c
--- /dev/null
+++ b/spring-security-modules/spring-security-mvc-boot-2/src/test/resources/customlogouthandler/application.properties
@@ -0,0 +1,5 @@
+spring.datasource.url=jdbc:postgresql://localhost:5432/test
+spring.datasource.username=test
+spring.datasource.password=test
+
+spring.jpa.hibernate.ddl-auto=create
diff --git a/spring-security-modules/spring-security-mvc-boot-2/src/test/resources/customlogouthandler/before.sql b/spring-security-modules/spring-security-mvc-boot-2/src/test/resources/customlogouthandler/before.sql
new file mode 100644
index 0000000000..bb0a85f613
--- /dev/null
+++ b/spring-security-modules/spring-security-mvc-boot-2/src/test/resources/customlogouthandler/before.sql
@@ -0,0 +1 @@
+insert into users (login, password, role, language) values ('user', '{noop}pass', 'ROLE_USER', 'english');
\ No newline at end of file