clusterIp = dbServer.getClusterIp();
+ assertEquals(3, clusterIp.size());
+ assertTrue(clusterIp.contains("127.0.0.1"));
+ assertTrue(clusterIp.contains("127.0.0.2"));
+ assertTrue(clusterIp.contains("127.0.0.3"));
+
+ }
+
+ // Mock Configuration within the test class
+ @Configuration
+ @PropertySource({"classpath:/server/db.properties", "classpath:/server/file.properties"})
+ static class TestConfig {
+ @Bean
+ public DatabaseProperties dbServer() {
+ return new DatabaseProperties();
+ }
+
+ @Bean
+ public FileProperties fileServer() {
+ return new FileProperties();
+ }
+
+ // supports List from .properties file.
+ @Bean
+ public ConversionService conversionService() {
+ return new DefaultConversionService();
+ }
+ }
+
+}
diff --git a/spring-boot-externalize-config-4/README.md b/spring-boot-externalize-config-4/README.md
new file mode 100644
index 0000000..924c333
--- /dev/null
+++ b/spring-boot-externalize-config-4/README.md
@@ -0,0 +1,16 @@
+# Spring @Value default value
+* [https://mkyong.com/spring3/spring-value-default-value/](https://mkyong.com/spring3/spring-value-default-value/)
+
+# Spring @TestPropertySource example
+* [https://mkyong.com/spring-boot/spring-testpropertysource-example/](https://mkyong.com/spring-boot/spring-testpropertysource-example/)
+
+## 1. How to start
+```
+$ git clone https://github.com/mkyong/spring-boot.git
+
+$ cd spring-boot-externalize-config-4
+
+$ mvn test
+
+$ mvn spring-boot:run
+```
\ No newline at end of file
diff --git a/spring-boot-externalize-config-4/pom.xml b/spring-boot-externalize-config-4/pom.xml
new file mode 100644
index 0000000..9608568
--- /dev/null
+++ b/spring-boot-externalize-config-4/pom.xml
@@ -0,0 +1,54 @@
+
+
+ 4.0.0
+
+ spring-boot-externalize-config-4
+ jar
+ https://mkyong.com
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.1.2
+
+
+
+
+ 17
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ ${java.version}
+ ${java.version}
+
+
+
+
+
diff --git a/spring-boot-externalize-config-4/src/main/java/com/mkyong/Application.java b/spring-boot-externalize-config-4/src/main/java/com/mkyong/Application.java
new file mode 100644
index 0000000..8c65539
--- /dev/null
+++ b/spring-boot-externalize-config-4/src/main/java/com/mkyong/Application.java
@@ -0,0 +1,23 @@
+package com.mkyong;
+
+import com.mkyong.service.DatabaseService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application implements CommandLineRunner {
+
+ @Autowired
+ private DatabaseService dbConfig;
+
+ @Override
+ public void run(String... args) throws Exception {
+ System.out.println(dbConfig);
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-externalize-config-4/src/main/java/com/mkyong/service/DatabaseService.java b/spring-boot-externalize-config-4/src/main/java/com/mkyong/service/DatabaseService.java
new file mode 100644
index 0000000..f3ccd6b
--- /dev/null
+++ b/spring-boot-externalize-config-4/src/main/java/com/mkyong/service/DatabaseService.java
@@ -0,0 +1,38 @@
+package com.mkyong.service;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Service
+public class DatabaseService {
+
+ @Value("${db.name:hello}")
+ private String name; // if db.name doesn't exist, we get the default hello
+
+ @Value("${db.thread-pool:3}")
+ private Integer threadPool; // if db.thread-pool doesn't exist, we get the default 3
+
+ @Override
+ public String toString() {
+ return "DatabaseService{" +
+ "name='" + name + '\'' +
+ ", threadPool=" + threadPool +
+ '}';
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Integer getThreadPool() {
+ return threadPool;
+ }
+
+ public void setThreadPool(Integer threadPool) {
+ this.threadPool = threadPool;
+ }
+}
diff --git a/spring-boot-externalize-config-4/src/main/resources/application.properties b/spring-boot-externalize-config-4/src/main/resources/application.properties
new file mode 100644
index 0000000..cf0cb60
--- /dev/null
+++ b/spring-boot-externalize-config-4/src/main/resources/application.properties
@@ -0,0 +1,7 @@
+# Logging
+logging.level.org.springframework.web=ERROR
+logging.level.com.mkyong=DEBUG
+
+# Database
+db.name=mkyong
+db.thread-pool=5
\ No newline at end of file
diff --git a/spring-boot-externalize-config-4/src/test/java/com/mkyong/ApplicationTest.java b/spring-boot-externalize-config-4/src/test/java/com/mkyong/ApplicationTest.java
new file mode 100644
index 0000000..30bbca3
--- /dev/null
+++ b/spring-boot-externalize-config-4/src/test/java/com/mkyong/ApplicationTest.java
@@ -0,0 +1,43 @@
+package com.mkyong;
+
+import com.mkyong.service.DatabaseService;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+// easy but load entire context, no good.
+// @SpringBootTest
+
+// Keep test in minimal configuration, only `test.properties` is loaded.
+@SpringJUnitConfig
+@TestPropertySource("classpath:test.properties")
+public class ApplicationTest {
+
+ @Autowired
+ private DatabaseService dbServer;
+
+ @Test
+ public void testDefaultDatabaseName() {
+ assertEquals("hello", dbServer.getName());
+ }
+
+ @Test
+ public void testDatabaseThreadPool() {
+ assertEquals(10, dbServer.getThreadPool());
+ }
+
+ // Mock Configuration within the test class
+ @Configuration
+ static class TestConfig {
+ @Bean
+ public DatabaseService dbServer() {
+ return new DatabaseService();
+ }
+ }
+
+}
diff --git a/spring-boot-externalize-config-4/src/test/java/com/mkyong/ApplicationTest2.java b/spring-boot-externalize-config-4/src/test/java/com/mkyong/ApplicationTest2.java
new file mode 100644
index 0000000..5a5c6f7
--- /dev/null
+++ b/spring-boot-externalize-config-4/src/test/java/com/mkyong/ApplicationTest2.java
@@ -0,0 +1,44 @@
+package com.mkyong;
+
+import com.mkyong.service.DatabaseService;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.TestPropertySource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Simulate Spring Boot Application behaviour of loading properties
+ * 1. Properties from @TestPropertySource
+ * 2. The application.properties or application.yml from the src/test/resources directory.
+ * 3. The application.properties or application.yml from the src/main/resources directory.
+ *
+ * In this case, application.properties from the src/main/resources is loaded.
+ */
+@SpringBootTest
+@TestPropertySource("classpath:test.properties")
+// inline properties
+//@TestPropertySource(properties = {"db.thread-pool=10"})
+/* multiple inline properties
+@TestPropertySource(properties = {
+ "db.thread-pool=10",
+ "db.name=mkyong"
+})*/
+public class ApplicationTest2 {
+
+ @Autowired
+ private DatabaseService dbServer;
+
+ @Test
+ public void testDatabaseName() {
+ assertEquals("mkyong", dbServer.getName());
+ }
+
+ @Test
+ public void testDatabaseThreadPool() {
+ assertEquals(10, dbServer.getThreadPool());
+ }
+
+}
+
diff --git a/spring-boot-externalize-config-4/src/test/resources/test.properties b/spring-boot-externalize-config-4/src/test/resources/test.properties
new file mode 100644
index 0000000..3a34fa4
--- /dev/null
+++ b/spring-boot-externalize-config-4/src/test/resources/test.properties
@@ -0,0 +1,3 @@
+# comment out to test the default value
+# db.name=mkyong
+db.thread-pool=10
\ No newline at end of file
diff --git a/spring-boot-externalize-config/README.md b/spring-boot-externalize-config/README.md
new file mode 100644
index 0000000..a7bf7f2
--- /dev/null
+++ b/spring-boot-externalize-config/README.md
@@ -0,0 +1,15 @@
+# Spring Boot @ConfigurationProperties example
+
+Article link
+[https://www.mkyong.com/spring-boot/spring-boot-configurationproperties-example/](https://www.mkyong.com/spring-boot/spring-boot-configurationproperties-example/)
+
+## 1. How to start
+```
+$ git clone https://github.com/mkyong/spring-boot.git
+
+$ cd spring-boot-externalize-config
+
+$ mvn spring-boot:run
+
+curl localhost:8080/
+```
\ No newline at end of file
diff --git a/spring-boot-externalize-config/pom.xml b/spring-boot-externalize-config/pom.xml
new file mode 100644
index 0000000..093bc07
--- /dev/null
+++ b/spring-boot-externalize-config/pom.xml
@@ -0,0 +1,68 @@
+
+
+ 4.0.0
+
+ spring-boot-externalize-config
+ jar
+ https://www.mkyong.com
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.1.2
+
+
+
+
+ 17
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ jakarta.validation
+ jakarta.validation-api
+ 3.0.2
+
+
+
+
+ org.hibernate
+ hibernate-validator
+ 8.0.1.Final
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ ${java.version}
+ ${java.version}
+
+
+
+
+
diff --git a/spring-boot-externalize-config/src/main/java/com/mkyong/MainController.java b/spring-boot-externalize-config/src/main/java/com/mkyong/MainController.java
new file mode 100644
index 0000000..d15ed71
--- /dev/null
+++ b/spring-boot-externalize-config/src/main/java/com/mkyong/MainController.java
@@ -0,0 +1,31 @@
+package com.mkyong;
+
+import com.mkyong.global.AppProperties;
+import com.mkyong.global.GlobalProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class MainController {
+
+ private static final Logger logger = LoggerFactory.getLogger(MainController.class);
+
+ @Autowired
+ private AppProperties app;
+ @Autowired
+ private GlobalProperties global;
+
+ @GetMapping("/")
+ public AppProperties main() {
+ return app;
+ }
+
+ @GetMapping("/global")
+ public GlobalProperties global() {
+ return global;
+ }
+
+}
\ No newline at end of file
diff --git a/spring-boot-externalize-config/src/main/java/com/mkyong/SpringBootWebApplication.java b/spring-boot-externalize-config/src/main/java/com/mkyong/SpringBootWebApplication.java
new file mode 100644
index 0000000..aaa9ded
--- /dev/null
+++ b/spring-boot-externalize-config/src/main/java/com/mkyong/SpringBootWebApplication.java
@@ -0,0 +1,13 @@
+package com.mkyong;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class SpringBootWebApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootWebApplication.class, args);
+ }
+
+}
\ No newline at end of file
diff --git a/externalize-config-properties-yaml/src/main/java/com/mkyong/AppProperties.java b/spring-boot-externalize-config/src/main/java/com/mkyong/global/AppProperties.java
similarity index 91%
rename from externalize-config-properties-yaml/src/main/java/com/mkyong/AppProperties.java
rename to spring-boot-externalize-config/src/main/java/com/mkyong/global/AppProperties.java
index d9c8e10..2b80a9f 100644
--- a/externalize-config-properties-yaml/src/main/java/com/mkyong/AppProperties.java
+++ b/spring-boot-externalize-config/src/main/java/com/mkyong/global/AppProperties.java
@@ -1,4 +1,4 @@
-package com.mkyong;
+package com.mkyong.global;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@@ -6,10 +6,13 @@
import java.util.ArrayList;
import java.util.List;
-// By default, Spring boot it loads property from application.properties, we can use @PropertySource to load other .properties files.
+// By default, Spring boot it loads property from application.properties,
+// we can use @PropertySource to load other .properties files.
//@PropertySource("classpath:custom.properties")
+
+// This component maps value from application.properties to object via @ConfigurationProperties
@Component
-@ConfigurationProperties("app")
+@ConfigurationProperties("app") // prefix app, find app.* values
public class AppProperties {
private String error;
diff --git a/externalize-config-properties-yaml/src/main/java/com/mkyong/GlobalProperties.java b/spring-boot-externalize-config/src/main/java/com/mkyong/global/GlobalProperties.java
similarity index 67%
rename from externalize-config-properties-yaml/src/main/java/com/mkyong/GlobalProperties.java
rename to spring-boot-externalize-config/src/main/java/com/mkyong/global/GlobalProperties.java
index cf70594..22b14ac 100644
--- a/externalize-config-properties-yaml/src/main/java/com/mkyong/GlobalProperties.java
+++ b/spring-boot-externalize-config/src/main/java/com/mkyong/global/GlobalProperties.java
@@ -1,21 +1,20 @@
-package com.mkyong;
-
-//import org.hibernate.validator.constraints.NotEmpty;
+package com.mkyong.global;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotEmpty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
-import javax.validation.constraints.Max;
-import javax.validation.constraints.Min;
-import javax.validation.constraints.NotEmpty;
-
+// This component maps value from application.properties to object via @ConfigurationProperties
@Component
-@ConfigurationProperties
+@ConfigurationProperties // no prefix, find root level values.
@Validated
public class GlobalProperties {
- //@Value("${thread-pool}")
+ // @Value("${thread-pool}")
+ // access the value from application.properties via @Value
@Max(5)
@Min(0)
private int threadPool;
diff --git a/externalize-config-properties-yaml/src/main/resources/application.properties b/spring-boot-externalize-config/src/main/resources/application.properties
similarity index 73%
rename from externalize-config-properties-yaml/src/main/resources/application.properties
rename to spring-boot-externalize-config/src/main/resources/application.properties
index 912d1af..7242bc8 100644
--- a/externalize-config-properties-yaml/src/main/resources/application.properties
+++ b/spring-boot-externalize-config/src/main/resources/application.properties
@@ -1,12 +1,12 @@
#Logging
-logging.level.org.springframework.web=DEBUG
+logging.level.org.springframework.web=ERROR
logging.level.com.mkyong=DEBUG
-#Global
+# Test @Value
email=test@mkyong.com
thread-pool=5
-#App
+#Below properties mapped to AppProperties.java
app.menus[0].title=Home
app.menus[0].name=Home
app.menus[0].path=/
diff --git a/externalize-config-properties-yaml/src/main/resources/application.yml.bk b/spring-boot-externalize-config/src/main/resources/application.yml.bk
similarity index 94%
rename from externalize-config-properties-yaml/src/main/resources/application.yml.bk
rename to spring-boot-externalize-config/src/main/resources/application.yml.bk
index dcb8b64..caeae5d 100644
--- a/externalize-config-properties-yaml/src/main/resources/application.yml.bk
+++ b/spring-boot-externalize-config/src/main/resources/application.yml.bk
@@ -3,7 +3,7 @@ logging:
org.springframework.web: ERROR
com.mkyong: DEBUG
email: test@mkyong.com
-thread-pool: 10
+thread-pool: 5
app:
menus:
- title: Home
diff --git a/spring-boot-externalize-config/src/test/java/com/mkyong/MainControllerTest.java b/spring-boot-externalize-config/src/test/java/com/mkyong/MainControllerTest.java
new file mode 100644
index 0000000..21127ea
--- /dev/null
+++ b/spring-boot-externalize-config/src/test/java/com/mkyong/MainControllerTest.java
@@ -0,0 +1,49 @@
+package com.mkyong;
+
+import com.mkyong.global.AppProperties;
+import com.mkyong.global.GlobalProperties;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+
+import static org.hamcrest.Matchers.is;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+// @SpringBootTest
+// @AutoConfigureMockMvc
+
+@WebMvcTest(MainController.class)
+@EnableConfigurationProperties({AppProperties.class, GlobalProperties.class})
+public class MainControllerTest {
+
+ @Autowired
+ private MockMvc mvc;
+
+ @Test
+ public void testGlobalProperties() throws Exception {
+ mvc.perform(MockMvcRequestBuilders.get("/global")
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ // has field name `threadPool` of with value of 5
+ .andExpect(jsonPath("$.threadPool", is(5)))
+ .andExpect(jsonPath("$.email", is("test@mkyong.com")));
+ }
+
+ @Test
+ public void testAppProperties() throws Exception {
+ mvc.perform(MockMvcRequestBuilders.get("/")
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.error", is("/error/")))
+ .andExpect(jsonPath("$.compiler.timeout", is("5")))
+ .andExpect(jsonPath("$.compiler.outputFolder", is("/temp/")))
+ .andExpect(jsonPath("$.menus[0].title", is("Home")))
+ .andExpect(jsonPath("$.menus[1].title", is("Login")));
+ }
+
+}
diff --git a/spring-boot-hello-world/README.md b/spring-boot-hello-world/README.md
new file mode 100644
index 0000000..b8e954f
--- /dev/null
+++ b/spring-boot-hello-world/README.md
@@ -0,0 +1,20 @@
+# Spring Boot Hello World Example
+Get started with the Spring Boot application, a hello world example.
+
+https://mkyong.com/spring-boot/spring-boot-hello-world-example/
+
+## 1. How to start
+```bash
+$ git clone [https://github.com/mkyong/spring-boot.git](https://github.com/mkyong/spring-boot.git)
+
+$ cd spring-boot-hello-world
+
+# Tomcat started at 8080
+$ mvn spring-boot:run
+
+# test
+curl localhost:8080
+
+```
+
+
diff --git a/spring-boot-hello-world/pom.xml b/spring-boot-hello-world/pom.xml
new file mode 100644
index 0000000..45b65be
--- /dev/null
+++ b/spring-boot-hello-world/pom.xml
@@ -0,0 +1,56 @@
+
+
+ 4.0.0
+
+ spring-boot-hello-world
+ jar
+ Spring Boot Hello World Example
+ 1.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.1.2
+
+
+
+
+ 17
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ ${java.version}
+ ${java.version}
+
+
+
+
+
+
+
diff --git a/spring-boot-hello-world/src/main/java/com/mkyong/HelloController.java b/spring-boot-hello-world/src/main/java/com/mkyong/HelloController.java
new file mode 100644
index 0000000..9ca30fa
--- /dev/null
+++ b/spring-boot-hello-world/src/main/java/com/mkyong/HelloController.java
@@ -0,0 +1,14 @@
+package com.mkyong;
+
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class HelloController {
+
+ @RequestMapping("/")
+ String hello() {
+ return "Hello World, Spring Boot!";
+ }
+
+}
\ No newline at end of file
diff --git a/spring-boot-hello-world/src/main/java/com/mkyong/MainApplication.java b/spring-boot-hello-world/src/main/java/com/mkyong/MainApplication.java
new file mode 100644
index 0000000..9ca90e1
--- /dev/null
+++ b/spring-boot-hello-world/src/main/java/com/mkyong/MainApplication.java
@@ -0,0 +1,13 @@
+package com.mkyong;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class MainApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(MainApplication.class, args);
+ }
+
+}
\ No newline at end of file
diff --git a/spring-boot-hello-world/src/main/resources/application.properties b/spring-boot-hello-world/src/main/resources/application.properties
new file mode 100644
index 0000000..e69de29
diff --git a/spring-boot-hello-world/src/test/java/com/mkyong/HelloControllerTests.java b/spring-boot-hello-world/src/test/java/com/mkyong/HelloControllerTests.java
new file mode 100644
index 0000000..3373aae
--- /dev/null
+++ b/spring-boot-hello-world/src/test/java/com/mkyong/HelloControllerTests.java
@@ -0,0 +1,29 @@
+package com.mkyong;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+public class HelloControllerTests {
+
+ @Autowired
+ private MockMvc mvc;
+
+ @Test
+ public void welcome_ok() throws Exception {
+ mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(content().string(equalTo("Hello World, Spring Boot!")));
+ }
+
+}
diff --git a/spring-boot-hello-world/src/test/java/com/mkyong/HelloControllerTests2.java b/spring-boot-hello-world/src/test/java/com/mkyong/HelloControllerTests2.java
new file mode 100644
index 0000000..6b45309
--- /dev/null
+++ b/spring-boot-hello-world/src/test/java/com/mkyong/HelloControllerTests2.java
@@ -0,0 +1,25 @@
+package com.mkyong;
+
+import org.junit.jupiter.api.Test;
+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.http.ResponseEntity;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+
+//@SpringBootTest(classes = MainApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+public class HelloControllerTests2 {
+
+ @Autowired
+ private TestRestTemplate template;
+
+ @Test
+ public void hello_ok() throws Exception {
+ ResponseEntity response = template.getForEntity("/", String.class);
+ assertThat(response.getBody()).isEqualTo("Hello World, Spring Boot!");
+ }
+
+}
+
diff --git a/spring-boot-jobrunr/README.md b/spring-boot-jobrunr/README.md
new file mode 100644
index 0000000..c47bd8e
--- /dev/null
+++ b/spring-boot-jobrunr/README.md
@@ -0,0 +1,12 @@
+# Spring Boot JobRunr examples
+
+https://mkyong.com/spring-boot/spring-boot-jobrunr-examples/
+
+## How to run this?
+```bash
+$ git clone https://github.com/mkyong/spring-boot.git
+
+$ cd spring-boot-jobrunr
+
+$ mvn spring-boot:run
+```
\ No newline at end of file
diff --git a/spring-boot-jobrunr/pom.xml b/spring-boot-jobrunr/pom.xml
new file mode 100644
index 0000000..f1472c5
--- /dev/null
+++ b/spring-boot-jobrunr/pom.xml
@@ -0,0 +1,84 @@
+
+
+ 4.0.0
+
+ com.mkyong
+ spring-boot-jobrunr
+ 1.0
+
+ spring-boot-jobrunr
+ https://mkyong.com
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.3.12.RELEASE
+
+
+
+ UTF-8
+ UTF-8
+ 11
+ 11
+ 11
+ 3.1.2
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.jobrunr
+ jobrunr-spring-boot-starter
+ ${jobrunr.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.junit.vintage
+ junit-vintage-engine
+
+
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+
+
+
+
+ spring-boot-web
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ ${java.version}
+ ${java.version}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-boot-jobrunr/src/main/java/com/mkyong/MainApplication.java b/spring-boot-jobrunr/src/main/java/com/mkyong/MainApplication.java
new file mode 100644
index 0000000..75fa1c9
--- /dev/null
+++ b/spring-boot-jobrunr/src/main/java/com/mkyong/MainApplication.java
@@ -0,0 +1,15 @@
+package com.mkyong;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Import;
+
+@SpringBootApplication
+@Import(MainConfiguration.class)
+public class MainApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(MainApplication.class, args);
+ }
+
+}
\ No newline at end of file
diff --git a/spring-boot-jobrunr/src/main/java/com/mkyong/MainConfiguration.java b/spring-boot-jobrunr/src/main/java/com/mkyong/MainConfiguration.java
new file mode 100644
index 0000000..8ecd952
--- /dev/null
+++ b/spring-boot-jobrunr/src/main/java/com/mkyong/MainConfiguration.java
@@ -0,0 +1,21 @@
+package com.mkyong;
+
+import org.jobrunr.jobs.mappers.JobMapper;
+import org.jobrunr.storage.InMemoryStorageProvider;
+import org.jobrunr.storage.StorageProvider;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class MainConfiguration {
+
+ // InMemoryStorageProvider to store the job details
+ // The`spring-boot-starter-web` provides Jackson as JobMapper
+ @Bean
+ public StorageProvider storageProvider(JobMapper jobMapper) {
+ InMemoryStorageProvider storageProvider = new InMemoryStorageProvider();
+ storageProvider.setJobMapper(jobMapper);
+ return storageProvider;
+ }
+
+}
\ No newline at end of file
diff --git a/spring-boot-jobrunr/src/main/java/com/mkyong/api/JobController.java b/spring-boot-jobrunr/src/main/java/com/mkyong/api/JobController.java
new file mode 100644
index 0000000..2ba1212
--- /dev/null
+++ b/spring-boot-jobrunr/src/main/java/com/mkyong/api/JobController.java
@@ -0,0 +1,51 @@
+package com.mkyong.api;
+
+import com.mkyong.job.SampleJobService;
+import org.jobrunr.scheduling.JobScheduler;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.Duration;
+import java.time.Instant;
+
+@RestController
+public class JobController {
+
+ @Autowired
+ private JobScheduler jobScheduler;
+
+ @Autowired
+ private SampleJobService sampleJobService;
+
+ @GetMapping("/run-job")
+ public String runJob(
+ @RequestParam(value = "name", defaultValue = "Hello World") String name) {
+
+ jobScheduler.enqueue(() -> sampleJobService.execute(name));
+ return "Job is enqueued.";
+
+ }
+
+ @GetMapping("/schedule-job")
+ public String scheduleJob(
+ @RequestParam(value = "name", defaultValue = "Hello World") String name,
+ @RequestParam(value = "when", defaultValue = "PT3H") String when) {
+
+ // old API, job first followed by time
+ /*
+ jobScheduler.schedule(() -> sampleJobService.execute(name),
+ Instant.now().plus(Duration.parse(when)));
+ */
+
+ // new API, time first followed by job
+ jobScheduler.schedule(
+ Instant.now().plus(Duration.parse(when)),
+ () -> sampleJobService.execute(name)
+ );
+
+ return "Job is scheduled.";
+ }
+
+}
diff --git a/spring-boot-jobrunr/src/main/java/com/mkyong/job/SampleJobService.java b/spring-boot-jobrunr/src/main/java/com/mkyong/job/SampleJobService.java
new file mode 100644
index 0000000..7cbf264
--- /dev/null
+++ b/spring-boot-jobrunr/src/main/java/com/mkyong/job/SampleJobService.java
@@ -0,0 +1,30 @@
+package com.mkyong.job;
+
+import org.jobrunr.jobs.annotations.Job;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+@Service
+public class SampleJobService {
+
+ private Logger logger = LoggerFactory.getLogger(getClass());
+
+ @Job(name = "The sample job without variable")
+ public void execute() {
+ execute("Hello world!");
+ }
+
+ @Job(name = "The sample job with variable %0")
+ public void execute(String input) {
+ logger.info("The sample job has begun. The variable you passed is {}", input);
+ try {
+ Thread.sleep(3000);
+ } catch (InterruptedException e) {
+ logger.error("Error while executing sample job", e);
+ } finally {
+ logger.info("Sample job has finished...");
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/spring-boot-jobrunr/src/main/resources/application.properties b/spring-boot-jobrunr/src/main/resources/application.properties
new file mode 100644
index 0000000..54b92ff
--- /dev/null
+++ b/spring-boot-jobrunr/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+org.jobrunr.background-job-server.enabled=true
+org.jobrunr.dashboard.enabled=true
\ No newline at end of file
diff --git a/spring-boot-jobrunr/src/test/java/com.mkyong/JobEndpointTest.java b/spring-boot-jobrunr/src/test/java/com.mkyong/JobEndpointTest.java
new file mode 100644
index 0000000..771fc05
--- /dev/null
+++ b/spring-boot-jobrunr/src/test/java/com.mkyong/JobEndpointTest.java
@@ -0,0 +1,62 @@
+package com.mkyong;
+
+import org.jobrunr.jobs.states.StateName;
+import org.jobrunr.storage.StorageProvider;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT;
+
+@SpringBootTest(webEnvironment = DEFINED_PORT)
+public class JobEndpointTest {
+
+ @Autowired
+ TestRestTemplate restTemplate;
+
+ @Autowired
+ StorageProvider storageProvider;
+
+ @Test
+ @DisplayName("Test job enqueued.")
+ public void givenEndpoint_whenJobEnqueued_thenJobIsProcessedWithin30Seconds() {
+ String response = runJobViaRest("from-test");
+ assertEquals("Job is enqueued.", response);
+
+ await()
+ .atMost(30, TimeUnit.SECONDS)
+ .until(() -> storageProvider.countJobs(StateName.SUCCEEDED) == 1);
+ }
+
+ @Test
+ @DisplayName("Test job scheduled.")
+ public void givenEndpoint_whenJobScheduled_thenJobIsScheduled() {
+ String response = scheduleJobViaRest("from-test", Duration.ofHours(3));
+ assertEquals("Job is scheduled.", response);
+
+ await()
+ .atMost(30, TimeUnit.SECONDS)
+ .until(() -> storageProvider.countJobs(StateName.SCHEDULED) == 1);
+ }
+
+ private String runJobViaRest(String input) {
+ return restTemplate.getForObject(
+ "http://localhost:8080/run-job?name=" + input,
+ String.class);
+ }
+
+ private String scheduleJobViaRest(String input, Duration duration) {
+ return restTemplate.getForObject(
+ "http://localhost:8080/schedule-job?name=" + input
+ + "&when=" + duration.toString(),
+ String.class);
+ }
+
+}
\ No newline at end of file
diff --git a/spring-boot-logging-slf4j-logback/README.md b/spring-boot-logging-slf4j-logback/README.md
new file mode 100644
index 0000000..35d4bc9
--- /dev/null
+++ b/spring-boot-logging-slf4j-logback/README.md
@@ -0,0 +1,17 @@
+# Spring Boot Logback SLF4j Examples
+
+This is the source code the article [Spring Boot Logging Using Logback Examples](https://mkyong.com/spring-boot/spring-boot-logging-example/s)
+
+# How to run it?
+
+```bash
+$ git clone https://github.com/mkyong/spring-boot.git
+
+$ cd spring-boot-logging-slf4j-logback
+
+$ mvn spring-boot:run
+
+$ curl localhost:8080 , review the log in console
+
+$ curl localhost:8080/hello/your-name , review the log in console
+```
\ No newline at end of file
diff --git a/spring-boot-logging-slf4j-logback/pom.xml b/spring-boot-logging-slf4j-logback/pom.xml
new file mode 100644
index 0000000..361123e
--- /dev/null
+++ b/spring-boot-logging-slf4j-logback/pom.xml
@@ -0,0 +1,52 @@
+
+
+ 4.0.0
+
+ spring-boot-slf4j
+ jar
+ Spring Boot Logging SLF4j and Logback
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.2.2
+
+
+
+
+ 17
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ ${java.version}
+ ${java.version}
+
+
+
+
+
diff --git a/spring-boot-logging-slf4j-logback/src/main/java/com/mkyong/HelloController.java b/spring-boot-logging-slf4j-logback/src/main/java/com/mkyong/HelloController.java
new file mode 100644
index 0000000..38351b8
--- /dev/null
+++ b/spring-boot-logging-slf4j-logback/src/main/java/com/mkyong/HelloController.java
@@ -0,0 +1,41 @@
+package com.mkyong;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class HelloController {
+
+ // Get the SLF4J logger interface, default Logback, a SLF4J implementation
+ private static final Logger logger = LoggerFactory.getLogger(HelloController.class);
+
+ @GetMapping("/")
+ public String hello() {
+
+ logger.debug("Debug level - Hello Logback");
+
+ logger.info("Info level - Hello Logback");
+
+ logger.error("Error level - Hello Logback");
+
+ return "Hello SLF4J";
+ }
+
+ // Log variables
+ @GetMapping("/hello/{name}")
+ String find(@PathVariable String name) {
+
+ logger.debug("Debug level - Hello Logback {}", name);
+
+ logger.info("Info level - Hello Logback {}", name);
+
+ logger.error("Error level - Hello Logback {}", name);
+
+ return "Hello SLF4J" + name;
+
+ }
+
+}
\ No newline at end of file
diff --git a/logging-slf4j-logback/src/main/java/com/mkyong/StartWebApplication.java b/spring-boot-logging-slf4j-logback/src/main/java/com/mkyong/MainApplication.java
similarity index 69%
rename from logging-slf4j-logback/src/main/java/com/mkyong/StartWebApplication.java
rename to spring-boot-logging-slf4j-logback/src/main/java/com/mkyong/MainApplication.java
index fe21f6a..b5f3dfe 100644
--- a/logging-slf4j-logback/src/main/java/com/mkyong/StartWebApplication.java
+++ b/spring-boot-logging-slf4j-logback/src/main/java/com/mkyong/MainApplication.java
@@ -4,10 +4,10 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
-public class StartWebApplication {
+public class MainApplication {
public static void main(String[] args) {
- SpringApplication.run(StartWebApplication.class, args);
+ SpringApplication.run(MainApplication.class, args);
}
}
\ No newline at end of file
diff --git a/spring-boot-logging-slf4j-logback/src/main/resources/application.properties b/spring-boot-logging-slf4j-logback/src/main/resources/application.properties
new file mode 100644
index 0000000..a15d919
--- /dev/null
+++ b/spring-boot-logging-slf4j-logback/src/main/resources/application.properties
@@ -0,0 +1,34 @@
+# logging level
+#logging.level.org.springframework=ERROR
+#logging.level.com.mkyong=DEBUG
+
+# logging.file was deprecated and renamed to logging.file.name in 2.2. It was then removed in 2.3.
+
+### 1. file output
+#logging.file.name=logs/app.log
+#logging.file.path=
+
+### 2. file rotation
+#The filename pattern used to create log archives.
+#logging.logback.rollingpolicy.file-name-pattern=logs/%d{yyyy-MM, aux}/app.%d{yyyy-MM-dd}.%i.log
+
+#If log archive cleanup should occur when the application starts.
+#logging.logback.rollingpolicy.clean-history-on-start=true
+
+#The maximum size of log file before it is archived.
+#logging.logback.rollingpolicy.max-file-size=100MB
+
+#The maximum amount of size log archives can take before being deleted.
+#logging.logback.rollingpolicy.total-size-cap=10GB
+
+#The maximum number of archive log files to keep (defaults to 7).
+#logging.logback.rollingpolicy.max-history=10
+
+# logging.pattern.file=%d %p %c{1.} [%t] %m%n
+# logging.pattern.console=%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
+
+## if no active profile, default is 'default'
+# spring.profiles.active=prod
+
+# root level
+#logging.level.=INFO
\ No newline at end of file
diff --git a/logging-slf4j-logback/src/main/resources/bk/logback-spring.xml b/spring-boot-logging-slf4j-logback/src/main/resources/bk/logback-spring.xml
similarity index 75%
rename from logging-slf4j-logback/src/main/resources/bk/logback-spring.xml
rename to spring-boot-logging-slf4j-logback/src/main/resources/bk/logback-spring.xml
index 4876886..86372ca 100644
--- a/logging-slf4j-logback/src/main/resources/bk/logback-spring.xml
+++ b/spring-boot-logging-slf4j-logback/src/main/resources/bk/logback-spring.xml
@@ -2,21 +2,19 @@
-
+
-
-
-
+
- app.log
+ /Users/mkyong/logs/prod.log
- logs/archived/app.%d{yyyy-MM-dd}.%i.log
+ /Users/mkyong/logs/%d{yyyy-MM, aux}/prod.%d{yyyy-MM-dd}.%i.log
10MB
@@ -26,11 +24,11 @@
- %d %p %c{1.} [%t] %m%n
+ %d %p %c [%t] %m%n
-
+
diff --git a/logging-slf4j-logback/src/main/resources/bk/logback.xml b/spring-boot-logging-slf4j-logback/src/main/resources/bk/logback.xml
similarity index 91%
rename from logging-slf4j-logback/src/main/resources/bk/logback.xml
rename to spring-boot-logging-slf4j-logback/src/main/resources/bk/logback.xml
index 32df67f..d7e7098 100644
--- a/logging-slf4j-logback/src/main/resources/bk/logback.xml
+++ b/spring-boot-logging-slf4j-logback/src/main/resources/bk/logback.xml
@@ -7,7 +7,7 @@
${HOME_LOG}
- logs/archived/app.%d{yyyy-MM-dd}.%i.log
+ logs/%d{yyyy-MM, aux}/app.%d{yyyy-MM-dd}.%i.log
10MB
diff --git a/spring-boot-test-json/README.md b/spring-boot-test-json/README.md
new file mode 100644
index 0000000..c3f6597
--- /dev/null
+++ b/spring-boot-test-json/README.md
@@ -0,0 +1,15 @@
+# Testing JSON in Spring Boot
+
+Article link
+[https://mkyong.com/spring-boot/testing-json-in-spring-boot/](https://mkyong.com/spring-boot/testing-json-in-spring-boot/)
+
+## 1. How to start
+```
+$ git clone [https://github.com/mkyong/spring-boot.git](https://github.com/mkyong/spring-boot.git)
+
+$ cd spring-boot-test-json
+
+$ mvn test
+
+$ mvn spring-boot:run
+```
\ No newline at end of file
diff --git a/spring-boot-test-json/pom.xml b/spring-boot-test-json/pom.xml
new file mode 100644
index 0000000..0a6120c
--- /dev/null
+++ b/spring-boot-test-json/pom.xml
@@ -0,0 +1,60 @@
+
+
+ 4.0.0
+
+ spring-boot-test-json
+ jar
+ https://mkyong.com
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.1.2
+
+
+
+
+ 17
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ ${java.version}
+ ${java.version}
+
+
+
+
+
diff --git a/spring-boot-test-json/src/main/java/com/mkyong/SpringBootWebApplication.java b/spring-boot-test-json/src/main/java/com/mkyong/SpringBootWebApplication.java
new file mode 100644
index 0000000..aaa9ded
--- /dev/null
+++ b/spring-boot-test-json/src/main/java/com/mkyong/SpringBootWebApplication.java
@@ -0,0 +1,13 @@
+package com.mkyong;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class SpringBootWebApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootWebApplication.class, args);
+ }
+
+}
\ No newline at end of file
diff --git a/spring-boot-test-json/src/main/java/com/mkyong/WebController.java b/spring-boot-test-json/src/main/java/com/mkyong/WebController.java
new file mode 100644
index 0000000..630fdd4
--- /dev/null
+++ b/spring-boot-test-json/src/main/java/com/mkyong/WebController.java
@@ -0,0 +1,55 @@
+package com.mkyong;
+
+import com.mkyong.model.Author;
+import com.mkyong.model.Book;
+import com.mkyong.model.SimpleBook;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+public class WebController {
+
+ private static final Logger logger =
+ LoggerFactory.getLogger(WebController.class);
+
+ @GetMapping("/")
+ public SimpleBook main() {
+ return new SimpleBook("Hello World");
+ }
+
+ @GetMapping("/book")
+ public Book returnBook() {
+
+ Author obj1 = new Author(1L, "Raoul-Gabriel Urma", "111-1111111");
+ Author obj2 = new Author(2L, "Mario Fusco", "222-2222222");
+ Author obj3 = new Author(3L, "Alan Mycroft", "333-3333333");
+
+ Book book = new Book();
+ book.setId(1L);
+ book.setTitle("Modern Java in Action");
+ book.setAuthors(List.of(obj1, obj2, obj3));
+ book.setTags(List.of("Java", "Java 8"));
+ book.setPublishedDate(LocalDate.of(2018, 11, 15));
+ book.setMeta(Map.of("isbn-10", "1617293563", "isbn-13", "978-1617293566"));
+
+ return book;
+
+ }
+
+ @GetMapping("/list")
+ public List returnList() {
+ return List.of("Java", "React", "JavaScript");
+ }
+
+ @GetMapping("/map")
+ public Map returnMap() {
+ return Map.of("key1", "a", "key2", "b", "key3", "c");
+ }
+
+}
\ No newline at end of file
diff --git a/spring-boot-test-json/src/main/java/com/mkyong/model/Author.java b/spring-boot-test-json/src/main/java/com/mkyong/model/Author.java
new file mode 100644
index 0000000..1f9e60a
--- /dev/null
+++ b/spring-boot-test-json/src/main/java/com/mkyong/model/Author.java
@@ -0,0 +1,66 @@
+package com.mkyong.model;
+
+import java.util.Objects;
+
+public class Author {
+
+ private long id;
+ private String name;
+ private String phoneNo;
+
+ public Author() {
+ }
+
+ // test array or list objects need equals and hashCode
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Author author = (Author) o;
+ return id == author.id && Objects.equals(name, author.name) && Objects.equals(phoneNo, author.phoneNo);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, name, phoneNo);
+ }
+
+ public Author(long id, String name, String phoneNo) {
+ this.id = id;
+ this.name = name;
+ this.phoneNo = phoneNo;
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getPhoneNo() {
+ return phoneNo;
+ }
+
+ public void setPhoneNo(String phoneNo) {
+ this.phoneNo = phoneNo;
+ }
+
+ @Override
+ public String toString() {
+ return "Author{" +
+ "id=" + id +
+ ", name='" + name + '\'' +
+ ", phoneNo='" + phoneNo + '\'' +
+ '}';
+ }
+}
diff --git a/spring-boot-test-json/src/main/java/com/mkyong/model/Book.java b/spring-boot-test-json/src/main/java/com/mkyong/model/Book.java
new file mode 100644
index 0000000..a3eea62
--- /dev/null
+++ b/spring-boot-test-json/src/main/java/com/mkyong/model/Book.java
@@ -0,0 +1,87 @@
+package com.mkyong.model;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Map;
+
+public class Book {
+
+ private long id;
+ private String title;
+ private List tags;
+ private List authors;
+ private LocalDate publishedDate;
+ private Map meta;
+
+ public Book() {
+ }
+
+ public Book(long id, String title, List tags, List authors, LocalDate publishedDate, Map meta) {
+ this.id = id;
+ this.title = title;
+ this.tags = tags;
+ this.authors = authors;
+ this.publishedDate = publishedDate;
+ this.meta = meta;
+ }
+
+ @Override
+ public String toString() {
+ return "Book{" +
+ "id=" + id +
+ ", title='" + title + '\'' +
+ ", tags=" + tags +
+ ", authors=" + authors +
+ ", publishedDate=" + publishedDate +
+ ", meta=" + meta +
+ '}';
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public List getTags() {
+ return tags;
+ }
+
+ public void setTags(List tags) {
+ this.tags = tags;
+ }
+
+ public List getAuthors() {
+ return authors;
+ }
+
+ public void setAuthors(List authors) {
+ this.authors = authors;
+ }
+
+ public LocalDate getPublishedDate() {
+ return publishedDate;
+ }
+
+ public void setPublishedDate(LocalDate publishedDate) {
+ this.publishedDate = publishedDate;
+ }
+
+ public Map getMeta() {
+ return meta;
+ }
+
+ public void setMeta(Map meta) {
+ this.meta = meta;
+ }
+}
diff --git a/spring-boot-test-json/src/main/java/com/mkyong/model/SimpleBook.java b/spring-boot-test-json/src/main/java/com/mkyong/model/SimpleBook.java
new file mode 100644
index 0000000..8ca4ca4
--- /dev/null
+++ b/spring-boot-test-json/src/main/java/com/mkyong/model/SimpleBook.java
@@ -0,0 +1,17 @@
+package com.mkyong.model;
+
+public class SimpleBook {
+ private String title;
+
+ public SimpleBook(String title) {
+ this.title = title;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+}
diff --git a/spring-boot-test-json/src/main/resources/application.properties b/spring-boot-test-json/src/main/resources/application.properties
new file mode 100644
index 0000000..3cc183f
--- /dev/null
+++ b/spring-boot-test-json/src/main/resources/application.properties
@@ -0,0 +1,3 @@
+#Logging
+logging.level.org.springframework.web=ERROR
+logging.level.com.mkyong=ERROR
diff --git a/spring-boot-test-json/src/test/java/com/mkyong/WebControllerTest.java b/spring-boot-test-json/src/test/java/com/mkyong/WebControllerTest.java
new file mode 100644
index 0000000..9685a4d
--- /dev/null
+++ b/spring-boot-test-json/src/test/java/com/mkyong/WebControllerTest.java
@@ -0,0 +1,199 @@
+package com.mkyong;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.mkyong.model.Author;
+import com.mkyong.model.Book;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+// too heavy to load entire spring context, uses @WebMvcTest
+//@SpringBootTest
+//@AutoConfigureMockMvc
+
+@WebMvcTest(WebController.class)
+public class WebControllerTest {
+
+ @Autowired
+ private MockMvc mvc;
+
+ @Test
+ public void testHello() throws Exception {
+
+ mvc.perform(get("/")
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ // has field name "$title" with a value of "Hello World"
+ .andExpect(jsonPath("$.title").value("Hello World"));
+
+ }
+
+ /**
+ * {
+ * "id" : 1,
+ * "title" : "Modern Java in Action",
+ * "tags" : [ "Java", "Java 8" ],
+ * "authors" : [ {
+ * "id" : 1,
+ * "name" : "Raoul-Gabriel Urma",
+ * "phoneNo" : "111-1111111"
+ * }, {
+ * "id" : 2,
+ * "name" : "Mario Fusco",
+ * "phoneNo" : "222-2222222"
+ * }, {
+ * "id" : 3,
+ * "name" : "Alan Mycroft",
+ * "phoneNo" : "333-3333333"
+ * } ],
+ * "publishedDate" : "2018-11-15",
+ * "meta" : {
+ * "isbn-10" : "1617293563",
+ * "isbn-13" : "978-1617293566"
+ * }
+ * }
+ */
+ @Test
+ public void testBook() throws Exception {
+
+ mvc.perform(MockMvcRequestBuilders.get("/book")
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id", is(1)))
+ .andExpect(jsonPath("$.id").isNumber())
+ .andExpect(jsonPath("$.title").exists())
+ .andExpect(jsonPath("$.title", is("Modern Java in Action")))
+ .andExpect(jsonPath("$.bookName").doesNotExist())
+ .andExpect(jsonPath("$.bookName").doesNotExist())
+ .andExpect(jsonPath("$.tags").isArray())
+ .andExpect(jsonPath("$.tags", hasSize(2)))
+ .andExpect(jsonPath("$.tags", hasItem("Java"))) // order not fix, check with contains
+ .andExpect(jsonPath("$.tags", hasItem("Java 8")))
+ .andExpect(jsonPath("$.publishedDate", is(LocalDate.of(2018, 11, 15).toString())))
+ .andExpect(jsonPath("$.authors", hasSize(3)))
+ .andExpect(jsonPath("$.meta.isbn-10", is("1617293563")))
+ .andExpect(jsonPath("$.meta.isbn-13", is("978-1617293566")))
+ // better convert to list of objects and test it, see below testBookAuthor
+ .andExpect(jsonPath("$.authors[*].id", hasItem(1)))
+ .andExpect(jsonPath("$.authors[*].id", containsInAnyOrder(3, 1, 2)))
+ .andExpect(jsonPath("$.authors[*].name", hasItem("Raoul-Gabriel Urma")))
+ .andExpect(jsonPath("$.authors[*].phoneNo", hasItem("111-1111111")));
+
+ /*.andExpect(jsonPath("$.authors[0].id").value(1)) // first author of the book
+ .andExpect(jsonPath("$.authors[0].name").value("Raoul-Gabriel Urma"))
+ .andExpect(jsonPath("$.authors[0].phoneNo").value("111-1111111"))
+
+ .andExpect(jsonPath("$.authors[1].id").value(2)) // second author of the book
+ .andExpect(jsonPath("$.authors[2].name").value("Mario Fusco"))
+ .andExpect(jsonPath("$.authors[3].phoneNo").value("222-2222222")
+ );*/
+
+ }
+
+ // Better convert to list of objects and test it
+ @Test
+ public void testBookAuthor() throws Exception {
+
+ MvcResult mvcResult = mvc.perform(get("/book")
+ .accept(MediaType.APPLICATION_JSON)).andReturn();
+
+ ObjectMapper mapper = new ObjectMapper();
+ // supports Java 8 date time
+ mapper.registerModule(new JavaTimeModule());
+
+ Book book = mapper.readValue(mvcResult.getResponse().getContentAsString(), Book.class);
+
+ List authors = book.getAuthors();
+
+ Author obj1 = new Author(1L, "Raoul-Gabriel Urma", "111-1111111");
+ Author obj2 = new Author(2L, "Mario Fusco", "222-2222222");
+ Author obj3 = new Author(3L, "Alan Mycroft", "333-3333333");
+
+ assertThat(authors.size(), is(3));
+
+ assertThat(authors, hasItem(obj1));
+ assertThat(authors, hasItem(obj2));
+ assertThat(authors, hasItem(obj3));
+
+ // need exactly list item but in any order
+ assertThat(authors, containsInAnyOrder(obj3, obj1, obj2));
+
+ }
+
+
+ /**
+ * [
+ * "Java",
+ * "React",
+ * "JavaScript"
+ * ]
+ */
+ @Test
+ public void testList() throws Exception {
+
+ mvc.perform(get("/list")
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ // $ refer to root element
+ .andExpect(jsonPath("$", hasSize(3)))
+ // $[0] refer to first element of the list
+ .andExpect(jsonPath("$[0]").value("Java"))
+ .andExpect(jsonPath("$[1]").value("React"))
+ .andExpect(jsonPath("$[2]").value("JavaScript"))
+
+ // normally list order is not fix, better use hasItem
+ // if contains a specific value
+ .andExpect(jsonPath("$", hasItem("React")));
+
+ }
+
+ /**
+ * {
+ * "key1": "a",
+ * "key2": "b",
+ * "key3": "c"
+ * }
+ */
+ @Test
+ public void testMap() throws Exception {
+
+ mvc.perform(get("/map")
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.key1").value("a"))
+ .andExpect(jsonPath("$.key2").value("b"))
+ .andExpect(jsonPath("$.key3").value("c"));
+
+ // Deserialize and assert to test the map size, is there a better way?
+ MvcResult result = mvc.perform(get("/map")
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // convert JSON to Map object
+ String content = result.getResponse().getContentAsString();
+ Map resultMap = new ObjectMapper().readValue(content, new TypeReference<>() {
+ });
+
+ assertEquals(3, resultMap.size());
+
+ }
+
+}
diff --git a/spring-boot-testcontainers/.mvn/wrapper/maven-wrapper.jar b/spring-boot-testcontainers/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 0000000..cb28b0e
Binary files /dev/null and b/spring-boot-testcontainers/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/spring-boot-testcontainers/.mvn/wrapper/maven-wrapper.properties b/spring-boot-testcontainers/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..6f40a26
--- /dev/null
+++ b/spring-boot-testcontainers/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
diff --git a/spring-boot-testcontainers/README.md b/spring-boot-testcontainers/README.md
new file mode 100644
index 0000000..001b70d
--- /dev/null
+++ b/spring-boot-testcontainers/README.md
@@ -0,0 +1,22 @@
+# Spring Boot and Testcontainers Example
+
+Related Articles
+* [Spring Boot Testcontainers example](https://mkyong.com/spring-boot/spring-boot-testcontainers-example/)
+
+## Technologies:
+* Spring Boot 3.1.2 (Spring Web MVC, Spring Data JPA and Spring Test)
+* Testcontainers 1.19.0
+* PostgreSQL 15, Alpine Linux base image `postgres:15-alpine`
+* Java 17
+* JUnt 5
+
+## How to start
+```
+$ git clone https://github.com/mkyong/spring-boot.git
+
+$ cd spring-boot-testcontainers
+
+$ ./mvnw test
+
+$ ./mvnw spring-boot:run
+```
\ No newline at end of file
diff --git a/spring-boot-testcontainers/mvnw b/spring-boot-testcontainers/mvnw
new file mode 100755
index 0000000..8d937f4
--- /dev/null
+++ b/spring-boot-testcontainers/mvnw
@@ -0,0 +1,308 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.2.0
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /usr/local/etc/mavenrc ] ; then
+ . /usr/local/etc/mavenrc
+ fi
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "$(uname)" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
+ else
+ JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=$(java-config --jre-home)
+ fi
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
+ JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="$(which javac)"
+ if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=$(which readlink)
+ if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
+ if $darwin ; then
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
+ else
+ javaExecutable="$(readlink -f "\"$javaExecutable\"")"
+ fi
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaHome=$(expr "$javaHome" : '\(.*\)/bin')
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=$(cd "$wdir/.." || exit 1; pwd)
+ fi
+ # end of workaround
+ done
+ printf '%s' "$(cd "$basedir" || exit 1; pwd)"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ # Remove \r in case we run on Windows within Git Bash
+ # and check out the repository with auto CRLF management
+ # enabled. Otherwise, we may read lines that are delimited with
+ # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
+ # splitting rules.
+ tr -s '\r\n' ' ' < "$1"
+ fi
+}
+
+log() {
+ if [ "$MVNW_VERBOSE" = true ]; then
+ printf '%s\n' "$1"
+ fi
+}
+
+BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
+log "$MAVEN_PROJECTBASEDIR"
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
+if [ -r "$wrapperJarPath" ]; then
+ log "Found $wrapperJarPath"
+else
+ log "Couldn't find $wrapperJarPath, downloading it ..."
+
+ if [ -n "$MVNW_REPOURL" ]; then
+ wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ else
+ wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ fi
+ while IFS="=" read -r key value; do
+ # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
+ safeValue=$(echo "$value" | tr -d '\r')
+ case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
+ esac
+ done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+ log "Downloading from: $wrapperUrl"
+
+ if $cygwin; then
+ wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
+ fi
+
+ if command -v wget > /dev/null; then
+ log "Found wget ... using wget"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ else
+ wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ log "Found curl ... using curl"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ else
+ curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ fi
+ else
+ log "Falling back to using Java to download"
+ javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaSource=$(cygpath --path --windows "$javaSource")
+ javaClass=$(cygpath --path --windows "$javaClass")
+ fi
+ if [ -e "$javaSource" ]; then
+ if [ ! -e "$javaClass" ]; then
+ log " - Compiling MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/javac" "$javaSource")
+ fi
+ if [ -e "$javaClass" ]; then
+ log " - Running MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+# If specified, validate the SHA-256 sum of the Maven wrapper jar file
+wrapperSha256Sum=""
+while IFS="=" read -r key value; do
+ case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
+ esac
+done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+if [ -n "$wrapperSha256Sum" ]; then
+ wrapperSha256Result=false
+ if command -v sha256sum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ elif command -v shasum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
+ echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
+ exit 1
+ fi
+ if [ $wrapperSha256Result = false ]; then
+ echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
+ echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
+ echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+# shellcheck disable=SC2086 # safe args
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ $MAVEN_DEBUG_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/spring-boot-testcontainers/mvnw.cmd b/spring-boot-testcontainers/mvnw.cmd
new file mode 100644
index 0000000..f80fbad
--- /dev/null
+++ b/spring-boot-testcontainers/mvnw.cmd
@@ -0,0 +1,205 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.2.0
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
+if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %WRAPPER_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
+SET WRAPPER_SHA_256_SUM=""
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
+)
+IF NOT %WRAPPER_SHA_256_SUM%=="" (
+ powershell -Command "&{"^
+ "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
+ "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
+ " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
+ " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
+ " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
+ " exit 1;"^
+ "}"^
+ "}"
+ if ERRORLEVEL 1 goto error
+)
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% ^
+ %JVM_CONFIG_MAVEN_PROPS% ^
+ %MAVEN_OPTS% ^
+ %MAVEN_DEBUG_OPTS% ^
+ -classpath %WRAPPER_JAR% ^
+ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
+ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
+if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%"=="on" pause
+
+if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
+
+cmd /C exit /B %ERROR_CODE%
diff --git a/spring-boot-testcontainers/pom.xml b/spring-boot-testcontainers/pom.xml
new file mode 100644
index 0000000..653e612
--- /dev/null
+++ b/spring-boot-testcontainers/pom.xml
@@ -0,0 +1,111 @@
+
+
+ 4.0.0
+
+ spring-boot-testcontainers
+ jar
+ https://mkyong.com
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.1.2
+
+
+
+
+ 17
+ 1.19.0
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ org.springframework.boot
+ spring-boot-testcontainers
+ test
+
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+
+
+ org.testcontainers
+ testcontainers
+ test
+
+
+
+ org.testcontainers
+ postgresql
+ test
+
+
+
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+
+
+
+
+ org.testcontainers
+ testcontainers-bom
+ ${testcontainers.version}
+ pom
+ import
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ ${java.version}
+ ${java.version}
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-boot-testcontainers/src/main/java/com/mkyong/Application.java b/spring-boot-testcontainers/src/main/java/com/mkyong/Application.java
new file mode 100644
index 0000000..3fdafd6
--- /dev/null
+++ b/spring-boot-testcontainers/src/main/java/com/mkyong/Application.java
@@ -0,0 +1,18 @@
+package com.mkyong;
+
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application implements CommandLineRunner {
+
+ @Override
+ public void run(String... args) throws Exception {
+ System.out.println("Starts Spring Boot Testcontainers application...");
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-testcontainers/src/main/java/com/mkyong/book/Book.java b/spring-boot-testcontainers/src/main/java/com/mkyong/book/Book.java
new file mode 100644
index 0000000..410320c
--- /dev/null
+++ b/spring-boot-testcontainers/src/main/java/com/mkyong/book/Book.java
@@ -0,0 +1,65 @@
+package com.mkyong.book;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import jakarta.persistence.Id;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+
+@Entity
+@Table(name = "books")
+public class Book {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(nullable = false)
+ private String name;
+
+ @Column(nullable = false, unique = true)
+ private String isbn;
+
+ public Book() {
+ }
+
+ public Book(Long id, String name, String isbn) {
+ this.id = id;
+ this.name = name;
+ this.isbn = isbn;
+ }
+
+ @Override
+ public String toString() {
+ return "Book{" +
+ "id=" + id +
+ ", name='" + name + '\'' +
+ ", isbn='" + isbn + '\'' +
+ '}';
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getIsbn() {
+ return isbn;
+ }
+
+ public void setIsbn(String isbn) {
+ this.isbn = isbn;
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-testcontainers/src/main/java/com/mkyong/book/BookController.java b/spring-boot-testcontainers/src/main/java/com/mkyong/book/BookController.java
new file mode 100644
index 0000000..42b4f21
--- /dev/null
+++ b/spring-boot-testcontainers/src/main/java/com/mkyong/book/BookController.java
@@ -0,0 +1,29 @@
+package com.mkyong.book;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/books")
+public class BookController {
+
+ @Autowired
+ private BookRepository bookRepository;
+
+ @GetMapping
+ public List getAll() {
+ return bookRepository.findAll();
+ }
+
+ @PostMapping
+ public Book create(@RequestBody Book book) {
+ return bookRepository.save(book);
+ }
+
+ @GetMapping("/{id}")
+ public Book getById(@PathVariable Long id) {
+ return bookRepository.findById(id).orElse(null);
+ }
+}
diff --git a/spring-boot-testcontainers/src/main/java/com/mkyong/book/BookRepository.java b/spring-boot-testcontainers/src/main/java/com/mkyong/book/BookRepository.java
new file mode 100644
index 0000000..1cea0ca
--- /dev/null
+++ b/spring-boot-testcontainers/src/main/java/com/mkyong/book/BookRepository.java
@@ -0,0 +1,11 @@
+package com.mkyong.book;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface BookRepository extends JpaRepository {
+
+ Optional findByIsbn(String isbn);
+
+}
diff --git a/spring-boot-testcontainers/src/main/resources/application.properties b/spring-boot-testcontainers/src/main/resources/application.properties
new file mode 100644
index 0000000..ecf73c8
--- /dev/null
+++ b/spring-boot-testcontainers/src/main/resources/application.properties
@@ -0,0 +1,5 @@
+# Logging
+logging.level.org.springframework.web=ERROR
+logging.level.com.mkyong=DEBUG
+
+spring.sql.init.mode=always
\ No newline at end of file
diff --git a/spring-boot-testcontainers/src/main/resources/schema.sql b/spring-boot-testcontainers/src/main/resources/schema.sql
new file mode 100644
index 0000000..e6ed74b
--- /dev/null
+++ b/spring-boot-testcontainers/src/main/resources/schema.sql
@@ -0,0 +1,7 @@
+create table if not exists books (
+ id bigserial not null,
+ name varchar not null,
+ isbn varchar not null,
+ primary key (id),
+ UNIQUE (isbn)
+);
\ No newline at end of file
diff --git a/spring-boot-testcontainers/src/test/java/com/mkyong/book/BookControllerOldWayTest.java b/spring-boot-testcontainers/src/test/java/com/mkyong/book/BookControllerOldWayTest.java
new file mode 100644
index 0000000..2293312
--- /dev/null
+++ b/spring-boot-testcontainers/src/test/java/com/mkyong/book/BookControllerOldWayTest.java
@@ -0,0 +1,94 @@
+package com.mkyong.book;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+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.test.web.server.LocalServerPort;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.testcontainers.containers.PostgreSQLContainer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+public class BookControllerOldWayTest {
+
+ @LocalServerPort
+ private Integer port;
+
+ @Autowired
+ private TestRestTemplate restTemplate;
+
+ @Autowired
+ private BookRepository bookRepository;
+
+ @BeforeAll
+ static void beforeAll() {
+ postgres.start();
+ }
+
+ @AfterAll
+ static void afterAll() {
+ postgres.stop();
+ }
+
+ /* static, all testes share this container */
+ /**
+ * postgres:15-alpine
+ * PostgreSQL version 15 using the lightweight Alpine Linux as the base image
+ */
+ static PostgreSQLContainer> postgres = new PostgreSQLContainer<>(
+ "postgres:15-alpine"
+ );
+
+ @DynamicPropertySource
+ static void configureProperties(DynamicPropertyRegistry registry) {
+ registry.add("spring.datasource.url", postgres::getJdbcUrl);
+ registry.add("spring.datasource.username", postgres::getUsername);
+ registry.add("spring.datasource.password", postgres::getPassword);
+ }
+
+ @Test
+ public void testBookEndpoints() {
+
+ // Create a new book
+ Book book = new Book();
+ book.setName("Is Java Dead?");
+ book.setIsbn("111-111");
+
+ ResponseEntity createResponse =
+ restTemplate.postForEntity("/books", book, Book.class);
+ assertEquals(HttpStatus.OK, createResponse.getStatusCode());
+ Book savedBook = createResponse.getBody();
+
+ assert savedBook != null;
+
+ // Retrieve
+ ResponseEntity getResponse =
+ restTemplate.getForEntity("/books/" + savedBook.getId(), Book.class);
+ assertEquals(HttpStatus.OK, getResponse.getStatusCode());
+
+ Book bookFromGet = getResponse.getBody();
+
+ assert bookFromGet != null;
+
+ assertEquals("Is Java Dead?", bookFromGet.getName());
+ assertEquals("111-111", bookFromGet.getIsbn());
+
+ // Retrieve All
+ ResponseEntity getAllResponse =
+ restTemplate.getForEntity("/books", Book[].class);
+ assertEquals(HttpStatus.OK, getAllResponse.getStatusCode());
+
+ Book[] bookFromGetAll = getAllResponse.getBody();
+ assert bookFromGetAll != null;
+
+ assertEquals(1, bookFromGetAll.length);
+ }
+
+}
\ No newline at end of file
diff --git a/spring-boot-testcontainers/src/test/java/com/mkyong/book/BookControllerTest.java b/spring-boot-testcontainers/src/test/java/com/mkyong/book/BookControllerTest.java
new file mode 100644
index 0000000..4abc921
--- /dev/null
+++ b/spring-boot-testcontainers/src/test/java/com/mkyong/book/BookControllerTest.java
@@ -0,0 +1,101 @@
+package com.mkyong.book;
+
+import org.junit.jupiter.api.Test;
+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.test.web.server.LocalServerPort;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@Testcontainers
+public class BookControllerTest {
+
+ @LocalServerPort
+ private Integer port;
+
+ @Autowired
+ private TestRestTemplate restTemplate;
+
+ @Autowired
+ private BookRepository bookRepository;
+
+ // no need this, the @Testcontainers and @Container will auto start and stop the container.
+ /*@BeforeAll
+ static void beforeAll() {
+ postgres.start();
+ }
+
+ @AfterAll
+ static void afterAll() {
+ postgres.stop();
+ }*/
+
+ /**
+ * The Testcontainers JUnit 5 Extension will take care of starting the container before tests and stopping it after tests.
+ * If the container is a static field then it will be started once before all the tests and stopped after all the tests.
+ * If it is a non-static field then the container will be started before each test and stopped after each test.
+ */
+ @Container
+ @ServiceConnection
+ static PostgreSQLContainer> postgres = new PostgreSQLContainer<>(
+ "postgres:15-alpine"
+ );
+
+ /**
+ * With Spring Boot 3.1, we use @ServiceConnection, no need @DynamicPropertySource
+ */
+ /*
+ @DynamicPropertySource
+ static void configureProperties(DynamicPropertyRegistry registry) {
+ registry.add("spring.datasource.url", postgres::getJdbcUrl);
+ registry.add("spring.datasource.username", postgres::getUsername);
+ registry.add("spring.datasource.password", postgres::getPassword);
+ }*/
+
+ @Test
+ public void testBookEndpoints() {
+
+ // Create a new book
+ Book book = new Book();
+ book.setName("Is Java Dead?");
+ book.setIsbn("111-111");
+
+ ResponseEntity createResponse =
+ restTemplate.postForEntity("/books", book, Book.class);
+ assertEquals(HttpStatus.OK, createResponse.getStatusCode());
+ Book savedBook = createResponse.getBody();
+
+ assert savedBook != null;
+
+ // Retrieve
+ ResponseEntity getResponse =
+ restTemplate.getForEntity("/books/" + savedBook.getId(), Book.class);
+ assertEquals(HttpStatus.OK, getResponse.getStatusCode());
+
+ Book bookFromGet = getResponse.getBody();
+
+ assert bookFromGet != null;
+
+ assertEquals("Is Java Dead?", bookFromGet.getName());
+ assertEquals("111-111", bookFromGet.getIsbn());
+
+ // Retrieve All
+ ResponseEntity getAllResponse =
+ restTemplate.getForEntity("/books", Book[].class);
+ assertEquals(HttpStatus.OK, getAllResponse.getStatusCode());
+
+ Book[] bookFromGetAll = getAllResponse.getBody();
+ assert bookFromGetAll != null;
+
+ assertEquals(1, bookFromGetAll.length);
+ }
+
+}
\ No newline at end of file
diff --git a/spring-boot-testcontainers/src/test/java/com/mkyong/book/BookRepositoryDynamicPropertyTest.java b/spring-boot-testcontainers/src/test/java/com/mkyong/book/BookRepositoryDynamicPropertyTest.java
new file mode 100644
index 0000000..266a664
--- /dev/null
+++ b/spring-boot-testcontainers/src/test/java/com/mkyong/book/BookRepositoryDynamicPropertyTest.java
@@ -0,0 +1,60 @@
+package com.mkyong.book;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@DataJpaTest
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+//@ContextConfiguration(initializers = BookRepositoryDynamicPropertyTest.Initializer.class)
+@Testcontainers
+public class BookRepositoryDynamicPropertyTest {
+
+ @Autowired
+ private BookRepository bookRepository;
+
+ @Container
+ @ServiceConnection
+ static PostgreSQLContainer> postgres = new PostgreSQLContainer<>(
+ "postgres:15-alpine"
+ );
+
+ @DynamicPropertySource
+ static void configureProperties(DynamicPropertyRegistry registry) {
+ registry.add("spring.datasource.url", postgres::getJdbcUrl);
+ registry.add("spring.datasource.username", postgres::getUsername);
+ registry.add("spring.datasource.password", postgres::getPassword);
+ }
+
+ // Spring 5.2.5 introduced @DynamicPropertySource, no more ApplicationContextInitializer
+ /*static class Initializer implements ApplicationContextInitializer {
+ @Override
+ public void initialize(ConfigurableApplicationContext applicationContext) {
+ TestPropertyValues.of(
+ "spring.datasource.url=" + postgres.getJdbcUrl(),
+ "spring.datasource.username=" + postgres.getUsername(),
+ "spring.datasource.password=" + postgres.getPassword()
+ ).applyTo(applicationContext);
+
+ }
+ }*/
+
+ @Test
+ public void testEmptyList() {
+
+ List result = bookRepository.findAll();
+ assertEquals(0, result.size());
+
+ }
+}
diff --git a/spring-boot-testcontainers/src/test/java/com/mkyong/book/BookRepositoryServiceConnectionTest.java b/spring-boot-testcontainers/src/test/java/com/mkyong/book/BookRepositoryServiceConnectionTest.java
new file mode 100644
index 0000000..dfe9413
--- /dev/null
+++ b/spring-boot-testcontainers/src/test/java/com/mkyong/book/BookRepositoryServiceConnectionTest.java
@@ -0,0 +1,44 @@
+package com.mkyong.book;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@SpringBootTest
+@Testcontainers
+public class BookRepositoryServiceConnectionTest {
+
+ @Autowired
+ private BookRepository bookRepository;
+
+ @Container
+ // When using Testcontainers, connection details can be automatically created for a service running in a container
+ @ServiceConnection
+ static PostgreSQLContainer> postgres = new PostgreSQLContainer<>(
+ "postgres:15-alpine"
+ );
+
+ // With Spring Boot 3.1 and @ServiceConnection, no need this @DynamicPropertySource
+ /*@DynamicPropertySource
+ static void configureProperties(DynamicPropertyRegistry registry) {
+ registry.add("spring.datasource.url", postgres::getJdbcUrl);
+ registry.add("spring.datasource.username", postgres::getUsername);
+ registry.add("spring.datasource.password", postgres::getPassword);
+ }*/
+
+ @Test
+ public void testEmptyList() {
+
+ List result = bookRepository.findAll();
+ assertEquals(0, result.size());
+
+ }
+}
diff --git a/spring-boot-testcontainers/src/test/java/com/mkyong/book/BookRepositoryTest.java b/spring-boot-testcontainers/src/test/java/com/mkyong/book/BookRepositoryTest.java
new file mode 100644
index 0000000..5a98d1e
--- /dev/null
+++ b/spring-boot-testcontainers/src/test/java/com/mkyong/book/BookRepositoryTest.java
@@ -0,0 +1,97 @@
+package com.mkyong.book;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@DataJpaTest
+// do not replace the testcontainer data source
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+@Testcontainers
+public class BookRepositoryTest {
+
+ @Autowired
+ private BookRepository bookRepository;
+
+ @Container
+ @ServiceConnection
+ static PostgreSQLContainer> postgres = new PostgreSQLContainer<>(
+ "postgres:15-alpine"
+ );
+
+ @Test
+ public void testBookSaveAndFindById() {
+
+ // Create a new book
+ Book book = new Book();
+ book.setName("Is Java Dead?");
+ book.setIsbn("111-111");
+
+ // save book
+ bookRepository.save(book);
+
+ // find book
+ Optional result = bookRepository.findById(book.getId());
+ assertTrue(result.isPresent());
+
+ Book bookFromGet = result.get();
+
+ assertEquals("Is Java Dead?", bookFromGet.getName());
+ assertEquals("111-111", bookFromGet.getIsbn());
+
+ }
+
+ @Test
+ public void testBookCRUD() {
+
+ Book book = new Book();
+ book.setName("Is Java Dead?");
+ book.setIsbn("111-111");
+
+ // save book
+ bookRepository.save(book);
+
+ // find book by isbn
+ Optional result = bookRepository.findByIsbn(book.getIsbn());
+ assertTrue(result.isPresent());
+
+ Book bookFromGet = result.get();
+
+ Long bookId = bookFromGet.getId();
+
+ assertEquals("Is Java Dead?", bookFromGet.getName());
+ assertEquals("111-111", bookFromGet.getIsbn());
+
+ // update book
+ book.setName("Java still relevant in 2050");
+ bookRepository.save(book);
+
+ // find book by id
+ Optional result2 = bookRepository.findById(bookId);
+ assertTrue(result2.isPresent());
+
+ Book bookFromGet2 = result2.get();
+
+ assertEquals("Java still relevant in 2050", bookFromGet2.getName());
+ assertEquals("111-111", bookFromGet2.getIsbn());
+
+ // delete a book
+ bookRepository.delete(book);
+
+ // should be empty
+ assertTrue(bookRepository.findById(bookId).isEmpty());
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/spring-data-jpa-mysql/.mvn/wrapper/maven-wrapper.jar b/spring-data-jpa-mysql/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 0000000..cb28b0e
Binary files /dev/null and b/spring-data-jpa-mysql/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/spring-data-jpa-mysql/.mvn/wrapper/maven-wrapper.properties b/spring-data-jpa-mysql/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..6f40a26
--- /dev/null
+++ b/spring-data-jpa-mysql/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
diff --git a/spring-data-jpa-mysql/README.md b/spring-data-jpa-mysql/README.md
index 3d211d7..a2491b3 100644
--- a/spring-data-jpa-mysql/README.md
+++ b/spring-data-jpa-mysql/README.md
@@ -1,3 +1,32 @@
-# Spring Boot + Spring data JPA + MySQL example
+# Spring Boot + Spring Data JPA + MySQL example
+
+Article link : https://mkyong.com/spring-boot/spring-boot-spring-data-jpa-mysql-example/
+
+## Technologies used:
+* Spring Boot 3.1.2
+* Spring Data JPA (Hibernate 6 is the default JPA implementation)
+* MySQL 8
+* Java 17
+* Maven 3
+* JUnit 5
+* Spring Test using TestRestTemplate
+* Docker, [Testcontainers](https://testcontainers.com/) (for Spring integration tests using a MySQL container)
+
+## How to run it
+```
+
+$ git clone [https://github.com/mkyong/spring-boot.git](https://github.com/mkyong/spring-boot.git)
+
+$ cd spring-data-jpa-mysql
+
+# Run MySQL container for testing
+$ docker run --name c1 -p 3306:3306 -e MYSQL_USER=mkyong -e MYSQL_PASSWORD=password -e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=mydb -d mysql:8.1
+
+# Skip test, the Testcontainers takes time
+$ ./mvnw clean package -Dmaven.test.skip=true
+
+$ ./mvnw spring-boot:run
+
+```
+
-Article link : https://www.mkyong.com/spring-boot/spring-boot-spring-data-jpa-mysql-example/
\ No newline at end of file
diff --git a/spring-data-jpa-mysql/mvnw b/spring-data-jpa-mysql/mvnw
new file mode 100755
index 0000000..8d937f4
--- /dev/null
+++ b/spring-data-jpa-mysql/mvnw
@@ -0,0 +1,308 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.2.0
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /usr/local/etc/mavenrc ] ; then
+ . /usr/local/etc/mavenrc
+ fi
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "$(uname)" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
+ else
+ JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=$(java-config --jre-home)
+ fi
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
+ JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="$(which javac)"
+ if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=$(which readlink)
+ if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
+ if $darwin ; then
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
+ else
+ javaExecutable="$(readlink -f "\"$javaExecutable\"")"
+ fi
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaHome=$(expr "$javaHome" : '\(.*\)/bin')
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=$(cd "$wdir/.." || exit 1; pwd)
+ fi
+ # end of workaround
+ done
+ printf '%s' "$(cd "$basedir" || exit 1; pwd)"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ # Remove \r in case we run on Windows within Git Bash
+ # and check out the repository with auto CRLF management
+ # enabled. Otherwise, we may read lines that are delimited with
+ # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
+ # splitting rules.
+ tr -s '\r\n' ' ' < "$1"
+ fi
+}
+
+log() {
+ if [ "$MVNW_VERBOSE" = true ]; then
+ printf '%s\n' "$1"
+ fi
+}
+
+BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
+log "$MAVEN_PROJECTBASEDIR"
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
+if [ -r "$wrapperJarPath" ]; then
+ log "Found $wrapperJarPath"
+else
+ log "Couldn't find $wrapperJarPath, downloading it ..."
+
+ if [ -n "$MVNW_REPOURL" ]; then
+ wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ else
+ wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ fi
+ while IFS="=" read -r key value; do
+ # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
+ safeValue=$(echo "$value" | tr -d '\r')
+ case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
+ esac
+ done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+ log "Downloading from: $wrapperUrl"
+
+ if $cygwin; then
+ wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
+ fi
+
+ if command -v wget > /dev/null; then
+ log "Found wget ... using wget"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ else
+ wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ log "Found curl ... using curl"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ else
+ curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ fi
+ else
+ log "Falling back to using Java to download"
+ javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaSource=$(cygpath --path --windows "$javaSource")
+ javaClass=$(cygpath --path --windows "$javaClass")
+ fi
+ if [ -e "$javaSource" ]; then
+ if [ ! -e "$javaClass" ]; then
+ log " - Compiling MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/javac" "$javaSource")
+ fi
+ if [ -e "$javaClass" ]; then
+ log " - Running MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+# If specified, validate the SHA-256 sum of the Maven wrapper jar file
+wrapperSha256Sum=""
+while IFS="=" read -r key value; do
+ case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
+ esac
+done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+if [ -n "$wrapperSha256Sum" ]; then
+ wrapperSha256Result=false
+ if command -v sha256sum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ elif command -v shasum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
+ echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
+ exit 1
+ fi
+ if [ $wrapperSha256Result = false ]; then
+ echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
+ echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
+ echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+# shellcheck disable=SC2086 # safe args
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ $MAVEN_DEBUG_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/spring-data-jpa-mysql/mvnw.cmd b/spring-data-jpa-mysql/mvnw.cmd
new file mode 100644
index 0000000..f80fbad
--- /dev/null
+++ b/spring-data-jpa-mysql/mvnw.cmd
@@ -0,0 +1,205 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.2.0
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
+if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %WRAPPER_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
+SET WRAPPER_SHA_256_SUM=""
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
+)
+IF NOT %WRAPPER_SHA_256_SUM%=="" (
+ powershell -Command "&{"^
+ "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
+ "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
+ " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
+ " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
+ " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
+ " exit 1;"^
+ "}"^
+ "}"
+ if ERRORLEVEL 1 goto error
+)
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% ^
+ %JVM_CONFIG_MAVEN_PROPS% ^
+ %MAVEN_OPTS% ^
+ %MAVEN_DEBUG_OPTS% ^
+ -classpath %WRAPPER_JAR% ^
+ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
+ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
+if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%"=="on" pause
+
+if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
+
+cmd /C exit /B %ERROR_CODE%
diff --git a/spring-data-jpa-mysql/pom.xml b/spring-data-jpa-mysql/pom.xml
index 944717b..2ff4d39 100644
--- a/spring-data-jpa-mysql/pom.xml
+++ b/spring-data-jpa-mysql/pom.xml
@@ -1,51 +1,101 @@
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
spring-data-jpa-mysql
jar
- Spring Boot Spring Data JPA MYSQL
+ Spring Data JPA MySQL
1.0
org.springframework.boot
spring-boot-starter-parent
- 2.1.2.RELEASE
+ 3.1.2
+
- 1.8
- true
- true
+ 17
-
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
org.springframework.boot
spring-boot-starter-data-jpa
-
-
+
+
+
+
+
+
+ com.mysql
+ mysql-connector-j
+ runtime
+
+
+
+
org.springframework.boot
spring-boot-starter-test
test
+
+
+ org.springframework.boot
+ spring-boot-testcontainers
+ test
+
+
+
+ org.testcontainers
+ testcontainers
+ test
+
+
+
+ org.testcontainers
+ mysql
+ test
+
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
-
org.springframework.boot
spring-boot-maven-plugin
@@ -53,11 +103,14 @@
org.apache.maven.plugins
- maven-surefire-plugin
- 2.22.0
+ maven-compiler-plugin
+ 3.11.0
+
+ ${java.version}
+ ${java.version}
+
-
-
+
diff --git a/spring-data-jpa-mysql/src/main/java/com/mkyong/Book.java b/spring-data-jpa-mysql/src/main/java/com/mkyong/Book.java
deleted file mode 100644
index 97a19f0..0000000
--- a/spring-data-jpa-mysql/src/main/java/com/mkyong/Book.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.mkyong;
-
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-
-@Entity
-public class Book {
-
- @Id
- @GeneratedValue(strategy = GenerationType.AUTO)
- private Long id;
- private String name;
-
- public Book() {
- }
-
- public Book(String name) {
- this.name = name;
- }
-
- @Override
- public String toString() {
- return "Book{" +
- "id=" + id +
- ", name='" + name + '\'' +
- '}';
- }
-
- public Long getId() {
- return id;
- }
-
- public void setId(Long id) {
- this.id = id;
- }
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-}
diff --git a/spring-data-jpa-mysql/src/main/java/com/mkyong/BookRepository.java b/spring-data-jpa-mysql/src/main/java/com/mkyong/BookRepository.java
deleted file mode 100644
index 43043a3..0000000
--- a/spring-data-jpa-mysql/src/main/java/com/mkyong/BookRepository.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.mkyong;
-
-import org.springframework.data.repository.CrudRepository;
-
-import java.util.List;
-
-public interface BookRepository extends CrudRepository {
-
- List findByName(String name);
-
-}
diff --git a/spring-data-jpa-mysql/src/main/java/com/mkyong/StartApplication.java b/spring-data-jpa-mysql/src/main/java/com/mkyong/StartApplication.java
index be37d64..ddfe97b 100644
--- a/spring-data-jpa-mysql/src/main/java/com/mkyong/StartApplication.java
+++ b/spring-data-jpa-mysql/src/main/java/com/mkyong/StartApplication.java
@@ -2,41 +2,16 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
-public class StartApplication implements CommandLineRunner {
+public class StartApplication {
private static final Logger log = LoggerFactory.getLogger(StartApplication.class);
- @Autowired
- private BookRepository repository;
-
public static void main(String[] args) {
SpringApplication.run(StartApplication.class, args);
}
- @Override
- public void run(String... args) {
-
- log.info("StartApplication...");
-
- repository.save(new Book("Java"));
- repository.save(new Book("Node"));
- repository.save(new Book("Python"));
-
- System.out.println("\nfindAll()");
- repository.findAll().forEach(x -> System.out.println(x));
-
- System.out.println("\nfindById(1L)");
- repository.findById(1l).ifPresent(x -> System.out.println(x));
-
- System.out.println("\nfindByName('Node')");
- repository.findByName("Node").forEach(x -> System.out.println(x));
-
- }
-
}
\ No newline at end of file
diff --git a/spring-data-jpa-mysql/src/main/java/com/mkyong/book/Book.java b/spring-data-jpa-mysql/src/main/java/com/mkyong/book/Book.java
new file mode 100644
index 0000000..9ba2823
--- /dev/null
+++ b/spring-data-jpa-mysql/src/main/java/com/mkyong/book/Book.java
@@ -0,0 +1,72 @@
+package com.mkyong.book;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+@Entity
+public class Book {
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private Long id;
+ private String title;
+ private BigDecimal price;
+ private LocalDate publishDate;
+
+ // for JPA only, no use
+ public Book() {
+ }
+
+ public Book(String title, BigDecimal price, LocalDate publishDate) {
+ this.title = title;
+ this.price = price;
+ this.publishDate = publishDate;
+ }
+
+ @Override
+ public String toString() {
+ return "Book{" +
+ "id=" + id +
+ ", title='" + title + '\'' +
+ ", price=" + price +
+ ", publishDate=" + publishDate +
+ '}';
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public BigDecimal getPrice() {
+ return price;
+ }
+
+ public void setPrice(BigDecimal price) {
+ this.price = price;
+ }
+
+ public LocalDate getPublishDate() {
+ return publishDate;
+ }
+
+ public void setPublishDate(LocalDate publishDate) {
+ this.publishDate = publishDate;
+ }
+}
+
diff --git a/spring-data-jpa-mysql/src/main/java/com/mkyong/book/BookController.java b/spring-data-jpa-mysql/src/main/java/com/mkyong/book/BookController.java
new file mode 100644
index 0000000..ea16807
--- /dev/null
+++ b/spring-data-jpa-mysql/src/main/java/com/mkyong/book/BookController.java
@@ -0,0 +1,61 @@
+package com.mkyong.book;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Optional;
+
+@RestController
+@RequestMapping("/books")
+public class BookController {
+
+ @Autowired
+ private BookService bookService;
+
+ @GetMapping
+ public List findAll() {
+ return bookService.findAll();
+ }
+
+ @GetMapping("/{id}")
+ public Optional findById(@PathVariable Long id) {
+ return bookService.findById(id);
+ }
+
+ // create a book
+ @ResponseStatus(HttpStatus.CREATED) // 201
+ @PostMapping
+ public Book create(@RequestBody Book book) {
+ return bookService.save(book);
+ }
+
+ // update a book
+ @PutMapping
+ public Book update(@RequestBody Book book) {
+ return bookService.save(book);
+ }
+
+ // delete a book
+ @ResponseStatus(HttpStatus.NO_CONTENT) // 204
+ @DeleteMapping("/{id}")
+ public void deleteById(@PathVariable Long id) {
+ bookService.deleteById(id);
+ }
+
+ @GetMapping("/find/title/{title}")
+ public List findByTitle(@PathVariable String title) {
+ return bookService.findByTitle(title);
+ }
+
+ @GetMapping("/find/date-after/{date}")
+ public List findByPublishedDateAfter(
+ @PathVariable @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
+ return bookService.findByPublishedDateAfter(date);
+ }
+
+}
+
diff --git a/spring-data-jpa-mysql/src/main/java/com/mkyong/book/BookRepository.java b/spring-data-jpa-mysql/src/main/java/com/mkyong/book/BookRepository.java
new file mode 100644
index 0000000..f754eda
--- /dev/null
+++ b/spring-data-jpa-mysql/src/main/java/com/mkyong/book/BookRepository.java
@@ -0,0 +1,19 @@
+package com.mkyong.book;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.time.LocalDate;
+import java.util.List;
+
+// Spring Data JPA creates CRUD implementation at runtime automatically.
+public interface BookRepository extends JpaRepository {
+
+ List findByTitle(String title);
+
+ // Custom query
+ @Query("SELECT b FROM Book b WHERE b.publishDate > :date")
+ List findByPublishedDateAfter(@Param("date") LocalDate date);
+
+}
\ No newline at end of file
diff --git a/spring-data-jpa-mysql/src/main/java/com/mkyong/book/BookService.java b/spring-data-jpa-mysql/src/main/java/com/mkyong/book/BookService.java
new file mode 100644
index 0000000..44f17c0
--- /dev/null
+++ b/spring-data-jpa-mysql/src/main/java/com/mkyong/book/BookService.java
@@ -0,0 +1,39 @@
+package com.mkyong.book;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Optional;
+
+@Service
+public class BookService {
+
+ @Autowired
+ private BookRepository bookRepository;
+
+ public List findAll() {
+ return bookRepository.findAll();
+ }
+
+ public Optional findById(Long id) {
+ return bookRepository.findById(id);
+ }
+
+ public Book save(Book book) {
+ return bookRepository.save(book);
+ }
+
+ public void deleteById(Long id) {
+ bookRepository.deleteById(id);
+ }
+
+ public List findByTitle(String title) {
+ return bookRepository.findByTitle(title);
+ }
+
+ public List findByPublishedDateAfter(LocalDate date) {
+ return bookRepository.findByPublishedDateAfter(date);
+ }
+}
diff --git a/spring-data-jpa-mysql/src/main/resources/application.properties b/spring-data-jpa-mysql/src/main/resources/application.properties
index 91bc1bc..6658caf 100644
--- a/spring-data-jpa-mysql/src/main/resources/application.properties
+++ b/spring-data-jpa-mysql/src/main/resources/application.properties
@@ -1,20 +1,20 @@
logging.level.org.springframework=INFO
logging.level.com.mkyong=INFO
-logging.level.com.zaxxer=DEBUG
+logging.level.com.zaxxer=ERROR
logging.level.root=ERROR
spring.datasource.hikari.connectionTimeout=20000
spring.datasource.hikari.maximumPoolSize=5
-logging.pattern.console=%-5level %logger{36} - %msg%n
+# logging.pattern.console=%-5level %logger{36} - %msg%n
## MySQL
-spring.datasource.url=jdbc:mysql://192.168.1.4:3306/wordpress
+spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=mkyong
spring.datasource.password=password
-#`hibernate_sequence' doesn't exist
-spring.jpa.hibernate.use-new-id-generator-mappings=false
+# hibernate_sequence' doesn't exist
+# spring.jpa.hibernate.use-new-id-generator-mappings=false
-#drop n create table again, good for testing, comment this in production
-spring.jpa.hibernate.ddl-auto=create
\ No newline at end of file
+# drop n create table again, good for testing, comment this in production
+spring.jpa.hibernate.ddl-auto=create-drop
\ No newline at end of file
diff --git a/spring-data-jpa-mysql/src/test/java/com/mkyong/BookControllerTest.java b/spring-data-jpa-mysql/src/test/java/com/mkyong/BookControllerTest.java
new file mode 100644
index 0000000..26f99d3
--- /dev/null
+++ b/spring-data-jpa-mysql/src/test/java/com/mkyong/BookControllerTest.java
@@ -0,0 +1,256 @@
+package com.mkyong;
+
+import com.mkyong.book.Book;
+import com.mkyong.book.BookRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+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.test.web.server.LocalServerPort;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.*;
+import org.springframework.test.context.TestPropertySource;
+import org.testcontainers.containers.MySQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Testing with TestRestTemplate and @Testcontainers (image mysql:8.0-debian)
+ */
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+// activate automatic startup and stop of containers
+@Testcontainers
+// JPA drop and create table, good for testing
+@TestPropertySource(properties = {"spring.jpa.hibernate.ddl-auto=create-drop"})
+public class BookControllerTest {
+
+ @LocalServerPort
+ private Integer port;
+
+ @Autowired
+ private TestRestTemplate restTemplate;
+
+ private String BASEURI;
+
+ @Autowired
+ BookRepository bookRepository;
+
+ // static, all tests share this postgres container
+ @Container
+ @ServiceConnection
+ static MySQLContainer> postgres = new MySQLContainer<>(
+ "mysql:8.0-debian"
+ );
+
+ @BeforeEach
+ void testSetUp() {
+
+ BASEURI = "http://localhost:" + port;
+
+ bookRepository.deleteAll();
+
+ Book b1 = new Book("Book A",
+ BigDecimal.valueOf(9.99),
+ LocalDate.of(2023, 8, 31));
+ Book b2 = new Book("Book B",
+ BigDecimal.valueOf(19.99),
+ LocalDate.of(2023, 7, 31));
+ Book b3 = new Book("Book C",
+ BigDecimal.valueOf(29.99),
+ LocalDate.of(2023, 6, 10));
+ Book b4 = new Book("Book D",
+ BigDecimal.valueOf(39.99),
+ LocalDate.of(2023, 5, 5));
+
+ bookRepository.saveAll(List.of(b1, b2, b3, b4));
+ }
+
+ @Test
+ void testFindAll() {
+
+ // ResponseEntity response = restTemplate.getForEntity(BASEURI + "/books", List.class);
+
+ // find all books and return List
+ ParameterizedTypeReference> typeRef = new ParameterizedTypeReference<>() {
+ };
+ ResponseEntity> response = restTemplate.exchange(
+ BASEURI + "/books",
+ HttpMethod.GET,
+ null,
+ typeRef
+ );
+
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+ assertEquals(4, response.getBody().size());
+
+ }
+
+ @Test
+ void testFindByTitle() {
+ String title = "Book C";
+ ParameterizedTypeReference> typeRef = new ParameterizedTypeReference<>() {
+ };
+
+ // find Book C
+ ResponseEntity> response = restTemplate.exchange(
+ BASEURI + "/books/find/title/" + title,
+ HttpMethod.GET,
+ null,
+ typeRef
+ );
+
+ // test response code
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+
+ List list = response.getBody();
+ assert list != null;
+
+ assertEquals(1, list.size());
+
+ // Test Book C details
+ Book book = list.get(0);
+ assertEquals("Book C", book.getTitle());
+ assertEquals(BigDecimal.valueOf(29.99), book.getPrice());
+ assertEquals(LocalDate.of(2023, 6, 10), book.getPublishDate());
+
+ }
+
+ @Test
+ void testFindByPublishedDateAfter() {
+
+ String date = "2023-07-01";
+ ParameterizedTypeReference> typeRef = new ParameterizedTypeReference<>() {
+ };
+
+ // find Book C
+ ResponseEntity> response = restTemplate.exchange(
+ BASEURI + "/books/find/date-after/" + date,
+ HttpMethod.GET,
+ null,
+ typeRef
+ );
+
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+
+ // test list of objects
+ List result = response.getBody();
+ assert result != null;
+
+ assertEquals(2, result.size());
+
+ assertThat(result).extracting(Book::getTitle)
+ .containsExactlyInAnyOrder(
+ "Book A", "Book B");
+ assertThat(result).extracting(Book::getPrice)
+ .containsExactlyInAnyOrder(
+ new BigDecimal("9.99"), new BigDecimal("19.99"));
+ assertThat(result).extracting(Book::getPublishDate)
+ .containsExactlyInAnyOrder
+ (LocalDate.parse("2023-08-31"), LocalDate.parse("2023-07-31"));
+ }
+
+ @Test
+ public void testDeleteById() {
+
+ List list = bookRepository.findByTitle("Book A");
+ Book bookA = list.get(0);
+
+ // get Book A id
+ Long id = bookA.getId();
+
+ // delete by id
+ ResponseEntity response = restTemplate.exchange(
+ BASEURI + "/books/" + id,
+ HttpMethod.DELETE,
+ null,
+ Void.class
+ );
+
+ // test 204
+ assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());
+
+ // find Book A again, ensure no result
+ List listAgain = bookRepository.findByTitle("Book A");
+ assertEquals(0, listAgain.size());
+
+ }
+
+ @Test
+ public void testCreate() {
+
+ // Create a new Book E
+ Book newBook = new Book("Book E", new BigDecimal("9.99"), LocalDate.parse("2023-09-14"));
+ HttpHeaders headers = new HttpHeaders();
+ headers.add("Content-Type", "application/json");
+ HttpEntity request = new HttpEntity<>(newBook, headers);
+
+ // test POST save
+ ResponseEntity responseEntity =
+ restTemplate.postForEntity(BASEURI + "/books", request, Book.class);
+
+ assertEquals(HttpStatus.CREATED, responseEntity.getStatusCode());
+
+ // find Book E
+ List list = bookRepository.findByTitle("Book E");
+
+ // Test Book E details
+ Book book = list.get(0);
+ assertEquals("Book E", book.getTitle());
+ assertEquals(BigDecimal.valueOf(9.99), book.getPrice());
+ assertEquals(LocalDate.of(2023, 9, 14), book.getPublishDate());
+
+ }
+
+ /**
+ * Book b4 = new Book("Book D",
+ * BigDecimal.valueOf(39.99),
+ * LocalDate.of(2023, 5, 5));
+ */
+ @Test
+ public void testUpdate() {
+ // Find Book D
+ Book bookD = bookRepository.findByTitle("Book D").get(0);
+ Long id = bookD.getId();
+
+ // Update the book details
+ bookD.setTitle("Book DDD");
+ bookD.setPrice(new BigDecimal("199.99"));
+ bookD.setPublishDate(LocalDate.of(2024, 1, 31));
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.add("Content-Type", "application/json");
+
+ // put the updated book in HttpEntity
+ HttpEntity request = new HttpEntity<>(bookD, headers);
+
+ // Perform the PUT request to update the book
+ ResponseEntity responseEntity = restTemplate.exchange(
+ "http://localhost:" + port + "/books",
+ HttpMethod.PUT,
+ request,
+ Book.class
+ );
+
+ // ensure OK
+ assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
+
+ // verify the updated book
+ Book updatedBook = bookRepository.findById(id).orElseThrow();
+
+ assertEquals(id, updatedBook.getId());
+ assertEquals("Book DDD", updatedBook.getTitle());
+ assertEquals(BigDecimal.valueOf(199.99), updatedBook.getPrice());
+ assertEquals(LocalDate.of(2024, 1, 31), updatedBook.getPublishDate());
+
+ }
+
+}
\ No newline at end of file
diff --git a/spring-data-jpa-mysql/src/test/java/com/mkyong/BookRepositoryTest.java b/spring-data-jpa-mysql/src/test/java/com/mkyong/BookRepositoryTest.java
deleted file mode 100644
index d8920ed..0000000
--- a/spring-data-jpa-mysql/src/test/java/com/mkyong/BookRepositoryTest.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package com.mkyong;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
-import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
-import org.springframework.test.context.junit4.SpringRunner;
-
-import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.assertEquals;
-
-@RunWith(SpringRunner.class)
-@DataJpaTest
-public class BookRepositoryTest {
-
- @Autowired
- private TestEntityManager entityManager;
-
- @Autowired
- private BookRepository repository;
-
- @Test
- public void testFindByName() {
-
- entityManager.persist(new Book("C++"));
-
- List books = repository.findByName("C++");
- assertEquals(1, books.size());
-
- assertThat(books).extracting(Book::getName).containsOnly("C++");
-
- }
-
-}
diff --git a/spring-data-jpa-paging-sorting/.mvn/wrapper/maven-wrapper.jar b/spring-data-jpa-paging-sorting/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 0000000..cb28b0e
Binary files /dev/null and b/spring-data-jpa-paging-sorting/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/spring-data-jpa-paging-sorting/.mvn/wrapper/maven-wrapper.properties b/spring-data-jpa-paging-sorting/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..6f40a26
--- /dev/null
+++ b/spring-data-jpa-paging-sorting/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
diff --git a/spring-data-jpa-paging-sorting/README.md b/spring-data-jpa-paging-sorting/README.md
new file mode 100644
index 0000000..cd19277
--- /dev/null
+++ b/spring-data-jpa-paging-sorting/README.md
@@ -0,0 +1,30 @@
+# Spring Boot + Spring Data JPA + Paging And Sorting example
+
+Article link : https://mkyong.com/spring-boot/spring-data-jpa-paging-and-sorting-example/
+
+## Technologies used:
+* Spring Boot 3.1.2
+* Spring Data JPA
+* H2 in-memory database
+* Java 17
+* Maven 3
+* JUnit 5
+* Spring Integration Tests with TestRestTemplate
+* Unit Tests with Mocking (Mockito)
+
+## How to run it
+```
+
+$ git clone [https://github.com/mkyong/spring-boot.git](https://github.com/mkyong/spring-boot.git)
+
+$ cd spring-data-jpa-paging-sorting
+
+$ ./mvnw spring-boot:run
+
+$ curl -s "http://localhost:8080/books"
+
+$ curl -s "http://localhost:8080/books?pageNo=1&pageSize=4&sortBy=title&sortDirection=desc" | python3 -m json.tool
+
+```
+
+
diff --git a/spring-data-jpa-paging-sorting/mvnw b/spring-data-jpa-paging-sorting/mvnw
new file mode 100755
index 0000000..8d937f4
--- /dev/null
+++ b/spring-data-jpa-paging-sorting/mvnw
@@ -0,0 +1,308 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.2.0
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /usr/local/etc/mavenrc ] ; then
+ . /usr/local/etc/mavenrc
+ fi
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "$(uname)" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
+ else
+ JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=$(java-config --jre-home)
+ fi
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
+ JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="$(which javac)"
+ if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=$(which readlink)
+ if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
+ if $darwin ; then
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
+ else
+ javaExecutable="$(readlink -f "\"$javaExecutable\"")"
+ fi
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaHome=$(expr "$javaHome" : '\(.*\)/bin')
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=$(cd "$wdir/.." || exit 1; pwd)
+ fi
+ # end of workaround
+ done
+ printf '%s' "$(cd "$basedir" || exit 1; pwd)"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ # Remove \r in case we run on Windows within Git Bash
+ # and check out the repository with auto CRLF management
+ # enabled. Otherwise, we may read lines that are delimited with
+ # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
+ # splitting rules.
+ tr -s '\r\n' ' ' < "$1"
+ fi
+}
+
+log() {
+ if [ "$MVNW_VERBOSE" = true ]; then
+ printf '%s\n' "$1"
+ fi
+}
+
+BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
+log "$MAVEN_PROJECTBASEDIR"
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
+if [ -r "$wrapperJarPath" ]; then
+ log "Found $wrapperJarPath"
+else
+ log "Couldn't find $wrapperJarPath, downloading it ..."
+
+ if [ -n "$MVNW_REPOURL" ]; then
+ wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ else
+ wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ fi
+ while IFS="=" read -r key value; do
+ # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
+ safeValue=$(echo "$value" | tr -d '\r')
+ case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
+ esac
+ done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+ log "Downloading from: $wrapperUrl"
+
+ if $cygwin; then
+ wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
+ fi
+
+ if command -v wget > /dev/null; then
+ log "Found wget ... using wget"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ else
+ wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ log "Found curl ... using curl"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ else
+ curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ fi
+ else
+ log "Falling back to using Java to download"
+ javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaSource=$(cygpath --path --windows "$javaSource")
+ javaClass=$(cygpath --path --windows "$javaClass")
+ fi
+ if [ -e "$javaSource" ]; then
+ if [ ! -e "$javaClass" ]; then
+ log " - Compiling MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/javac" "$javaSource")
+ fi
+ if [ -e "$javaClass" ]; then
+ log " - Running MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+# If specified, validate the SHA-256 sum of the Maven wrapper jar file
+wrapperSha256Sum=""
+while IFS="=" read -r key value; do
+ case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
+ esac
+done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+if [ -n "$wrapperSha256Sum" ]; then
+ wrapperSha256Result=false
+ if command -v sha256sum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ elif command -v shasum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
+ echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
+ exit 1
+ fi
+ if [ $wrapperSha256Result = false ]; then
+ echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
+ echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
+ echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+# shellcheck disable=SC2086 # safe args
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ $MAVEN_DEBUG_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/spring-data-jpa-paging-sorting/mvnw.cmd b/spring-data-jpa-paging-sorting/mvnw.cmd
new file mode 100644
index 0000000..f80fbad
--- /dev/null
+++ b/spring-data-jpa-paging-sorting/mvnw.cmd
@@ -0,0 +1,205 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.2.0
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
+if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %WRAPPER_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
+SET WRAPPER_SHA_256_SUM=""
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
+)
+IF NOT %WRAPPER_SHA_256_SUM%=="" (
+ powershell -Command "&{"^
+ "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
+ "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
+ " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
+ " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
+ " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
+ " exit 1;"^
+ "}"^
+ "}"
+ if ERRORLEVEL 1 goto error
+)
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% ^
+ %JVM_CONFIG_MAVEN_PROPS% ^
+ %MAVEN_OPTS% ^
+ %MAVEN_DEBUG_OPTS% ^
+ -classpath %WRAPPER_JAR% ^
+ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
+ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
+if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%"=="on" pause
+
+if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
+
+cmd /C exit /B %ERROR_CODE%
diff --git a/spring-data-jpa-paging-sorting/pom.xml b/spring-data-jpa-paging-sorting/pom.xml
new file mode 100644
index 0000000..a257933
--- /dev/null
+++ b/spring-data-jpa-paging-sorting/pom.xml
@@ -0,0 +1,78 @@
+
+
+ 4.0.0
+
+ spring-data-jpa-paging-sorting
+ jar
+ Spring Data JPA Paging And Sorting
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.1.2
+
+
+
+
+ 17
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+
+ com.h2database
+ h2
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ ${java.version}
+ ${java.version}
+
+
+
+
+
+
diff --git a/spring-data-jpa-paging-sorting/src/main/java/com/mkyong/StartApplication.java b/spring-data-jpa-paging-sorting/src/main/java/com/mkyong/StartApplication.java
new file mode 100644
index 0000000..80f2e9c
--- /dev/null
+++ b/spring-data-jpa-paging-sorting/src/main/java/com/mkyong/StartApplication.java
@@ -0,0 +1,58 @@
+package com.mkyong;
+
+import com.mkyong.book.Book;
+import com.mkyong.book.BookRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+
+@SpringBootApplication
+public class StartApplication {
+
+ private static final Logger log = LoggerFactory.getLogger(StartApplication.class);
+
+ public static void main(String[] args) {
+ SpringApplication.run(StartApplication.class, args);
+ }
+
+ @Autowired
+ BookRepository bookRepository;
+
+ @Bean
+ public CommandLineRunner startup() {
+
+ return args -> {
+
+ Book b1 = new Book("Book A",
+ BigDecimal.valueOf(9.99),
+ LocalDate.of(2023, 8, 31));
+ Book b2 = new Book("Book B",
+ BigDecimal.valueOf(19.99),
+ LocalDate.of(2023, 7, 31));
+ Book b3 = new Book("Book C",
+ BigDecimal.valueOf(29.99),
+ LocalDate.of(2023, 6, 10));
+ Book b4 = new Book("Book D",
+ BigDecimal.valueOf(39.99),
+ LocalDate.of(2023, 5, 5));
+ Book b5 = new Book("Book E",
+ BigDecimal.valueOf(49.99),
+ LocalDate.of(2023, 4, 1));
+ Book b6 = new Book("Book F",
+ BigDecimal.valueOf(59.99),
+ LocalDate.of(2023, 3, 1));
+
+ bookRepository.saveAll(List.of(b1, b2, b3, b4, b5, b6));
+
+ };
+
+ }
+}
\ No newline at end of file
diff --git a/spring-data-jpa-paging-sorting/src/main/java/com/mkyong/book/Book.java b/spring-data-jpa-paging-sorting/src/main/java/com/mkyong/book/Book.java
new file mode 100644
index 0000000..a0e05ee
--- /dev/null
+++ b/spring-data-jpa-paging-sorting/src/main/java/com/mkyong/book/Book.java
@@ -0,0 +1,79 @@
+package com.mkyong.book;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+@Entity
+public class Book {
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private Long id;
+ private String title;
+ private BigDecimal price;
+ private LocalDate publishDate;
+
+ // for JPA only, no use
+ public Book() {
+ }
+
+ public Book(String title, BigDecimal price, LocalDate publishDate) {
+ this.title = title;
+ this.price = price;
+ this.publishDate = publishDate;
+ }
+
+ public Book(Long id, String title, BigDecimal price, LocalDate publishDate) {
+ this.id = id;
+ this.title = title;
+ this.price = price;
+ this.publishDate = publishDate;
+ }
+
+ @Override
+ public String toString() {
+ return "Book{" +
+ "id=" + id +
+ ", title='" + title + '\'' +
+ ", price=" + price +
+ ", publishDate=" + publishDate +
+ '}';
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public BigDecimal getPrice() {
+ return price;
+ }
+
+ public void setPrice(BigDecimal price) {
+ this.price = price;
+ }
+
+ public LocalDate getPublishDate() {
+ return publishDate;
+ }
+
+ public void setPublishDate(LocalDate publishDate) {
+ this.publishDate = publishDate;
+ }
+}
+
diff --git a/spring-data-jpa-paging-sorting/src/main/java/com/mkyong/book/BookController.java b/spring-data-jpa-paging-sorting/src/main/java/com/mkyong/book/BookController.java
new file mode 100644
index 0000000..4b8a5bc
--- /dev/null
+++ b/spring-data-jpa-paging-sorting/src/main/java/com/mkyong/book/BookController.java
@@ -0,0 +1,29 @@
+package com.mkyong.book;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+public class BookController {
+
+ @Autowired
+ private BookService bookService;
+
+ @GetMapping("/books")
+ public List findAll(
+ @RequestParam(defaultValue = "0") int pageNo,
+ @RequestParam(defaultValue = "10") int pageSize,
+ @RequestParam(defaultValue = "id") String sortBy,
+ @RequestParam(defaultValue = "ASC") String sortDirection) {
+ Page result = bookService.findAll(pageNo, pageSize, sortBy, sortDirection);
+ return result.getContent();
+
+ }
+
+}
+
diff --git a/spring-data-jpa-paging-sorting/src/main/java/com/mkyong/book/BookRepository.java b/spring-data-jpa-paging-sorting/src/main/java/com/mkyong/book/BookRepository.java
new file mode 100644
index 0000000..d1b745e
--- /dev/null
+++ b/spring-data-jpa-paging-sorting/src/main/java/com/mkyong/book/BookRepository.java
@@ -0,0 +1,9 @@
+package com.mkyong.book;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+// JpaRepository extends ListPagingAndSortingRepository
+// ListPagingAndSortingRepository extends PagingAndSortingRepository
+public interface BookRepository extends JpaRepository {
+
+}
\ No newline at end of file
diff --git a/spring-data-jpa-paging-sorting/src/main/java/com/mkyong/book/BookService.java b/spring-data-jpa-paging-sorting/src/main/java/com/mkyong/book/BookService.java
new file mode 100644
index 0000000..8e2e47b
--- /dev/null
+++ b/spring-data-jpa-paging-sorting/src/main/java/com/mkyong/book/BookService.java
@@ -0,0 +1,23 @@
+package com.mkyong.book;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Service;
+
+@Service
+public class BookService {
+
+ @Autowired
+ private BookRepository bookRepository;
+
+ public Page findAll(int pageNo, int pageSize, String sortBy, String sortDirection) {
+ Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), sortBy);
+ Pageable pageable = PageRequest.of(pageNo, pageSize, sort);
+
+ return bookRepository.findAll(pageable);
+ }
+
+}
diff --git a/spring-data-jpa-paging-sorting/src/main/resources/application.properties b/spring-data-jpa-paging-sorting/src/main/resources/application.properties
new file mode 100644
index 0000000..1ef09c4
--- /dev/null
+++ b/spring-data-jpa-paging-sorting/src/main/resources/application.properties
@@ -0,0 +1,6 @@
+logging.level.org.springframework=INFO
+logging.level.com.mkyong=INFO
+logging.level.com.zaxxer=ERROR
+logging.level.root=ERROR
+
+# logging.pattern.console=%-5level %logger{36} - %msg%n
\ No newline at end of file
diff --git a/spring-data-jpa-paging-sorting/src/test/java/com/mkyong/BookControllerIntegrationTest.java b/spring-data-jpa-paging-sorting/src/test/java/com/mkyong/BookControllerIntegrationTest.java
new file mode 100644
index 0000000..cce942a
--- /dev/null
+++ b/spring-data-jpa-paging-sorting/src/test/java/com/mkyong/BookControllerIntegrationTest.java
@@ -0,0 +1,131 @@
+package com.mkyong;
+
+import com.mkyong.book.Book;
+import com.mkyong.book.BookRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+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.test.web.server.LocalServerPort;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+// Integration test using TestRestTemplate
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+public class BookControllerIntegrationTest {
+
+ @LocalServerPort
+ private Integer port;
+
+ @Autowired
+ private TestRestTemplate restTemplate;
+
+ private String BASEURI;
+
+ @Autowired
+ BookRepository bookRepository;
+
+ // pre-populated with books for test
+ @BeforeEach
+ void testSetUp() {
+
+ BASEURI = "http://localhost:" + port;
+
+ bookRepository.deleteAll();
+
+ Book b1 = new Book("Book A",
+ BigDecimal.valueOf(9.99),
+ LocalDate.of(2023, 8, 31));
+ Book b2 = new Book("Book B",
+ BigDecimal.valueOf(19.99),
+ LocalDate.of(2023, 7, 31));
+ Book b3 = new Book("Book C",
+ BigDecimal.valueOf(29.99),
+ LocalDate.of(2023, 6, 10));
+ Book b4 = new Book("Book D",
+ BigDecimal.valueOf(39.99),
+ LocalDate.of(2023, 5, 5));
+ Book b5 = new Book("Book E",
+ BigDecimal.valueOf(49.99),
+ LocalDate.of(2023, 4, 1));
+ Book b6 = new Book("Book F",
+ BigDecimal.valueOf(59.99),
+ LocalDate.of(2023, 3, 1));
+
+ bookRepository.saveAll(List.of(b1, b2, b3, b4, b5, b6));
+ }
+
+ @Test
+ void testFindAllWithPagingAndSorting() {
+
+ ParameterizedTypeReference> typeRef = new ParameterizedTypeReference<>() {
+ };
+
+ // sort by price, desc, get page 0 , size = 4
+ ResponseEntity> response = restTemplate.exchange(
+ BASEURI + "/books?pageNo=0&pageSize=4&sortBy=title&sortDirection=desc",
+ HttpMethod.GET,
+ null,
+ typeRef
+ );
+
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+
+ // test list of objects
+ List result = response.getBody();
+ assert result != null;
+
+ assertEquals(4, result.size());
+
+ // Get Book C, D, E, F
+ assertThat(result).extracting(Book::getTitle)
+ .containsExactlyInAnyOrder(
+ "Book C", "Book D", "Book E", "Book F");
+ assertThat(result).extracting(Book::getPrice)
+ .containsExactlyInAnyOrder(
+ new BigDecimal("59.99"),
+ new BigDecimal("49.99"),
+ new BigDecimal("39.99"),
+ new BigDecimal("29.99")
+ );
+
+
+ // sort by price, desc, get page 1 , size = 4
+ ResponseEntity> response2 = restTemplate.exchange(
+ BASEURI + "/books?pageNo=1&pageSize=4&sortBy=title&sortDirection=desc",
+ HttpMethod.GET,
+ null,
+ typeRef
+ );
+
+ assertEquals(HttpStatus.OK, response2.getStatusCode());
+
+ // test list of objects
+ List result2 = response2.getBody();
+ assert result2 != null;
+
+ assertEquals(2, result2.size());
+
+ // Get Book A, B
+ assertThat(result2).extracting(Book::getTitle)
+ .containsExactlyInAnyOrder(
+ "Book A", "Book B");
+ assertThat(result2).extracting(Book::getPrice)
+ .containsExactlyInAnyOrder(
+ new BigDecimal("9.99"),
+ new BigDecimal("19.99")
+ );
+
+ }
+
+}
\ No newline at end of file
diff --git a/spring-data-jpa-paging-sorting/src/test/java/com/mkyong/BookControllerTest.java b/spring-data-jpa-paging-sorting/src/test/java/com/mkyong/BookControllerTest.java
new file mode 100644
index 0000000..c175e36
--- /dev/null
+++ b/spring-data-jpa-paging-sorting/src/test/java/com/mkyong/BookControllerTest.java
@@ -0,0 +1,113 @@
+package com.mkyong;
+
+import com.mkyong.book.Book;
+import com.mkyong.book.BookController;
+import com.mkyong.book.BookRepository;
+import com.mkyong.book.BookService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Sort;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultActions;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.hasSize;
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+// Testing BookController with mocking
+@WebMvcTest(controllers = BookController.class)
+public class BookControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @MockBean
+ BookRepository bookRepository;
+
+ @MockBean
+ BookService bookService;
+
+ private List books;
+
+ private Page bookPage;
+ private PageRequest pageRequest;
+ private PageRequest pageRequestWithSorting;
+
+ @BeforeEach
+ void setUp() {
+
+ Book b1 = new Book(1L, "Book A",
+ BigDecimal.valueOf(9.99),
+ LocalDate.of(2023, 8, 31));
+ Book b2 = new Book(2L, "Book B",
+ BigDecimal.valueOf(19.99),
+ LocalDate.of(2023, 7, 31));
+ Book b3 = new Book(3L, "Book C",
+ BigDecimal.valueOf(29.99),
+ LocalDate.of(2023, 6, 10));
+ Book b4 = new Book(4L, "Book D",
+ BigDecimal.valueOf(39.99),
+ LocalDate.of(2023, 5, 5));
+ Book b5 = new Book(5L, "Book E",
+ BigDecimal.valueOf(49.99),
+ LocalDate.of(2023, 4, 1));
+ Book b6 = new Book(6L, "Book F",
+ BigDecimal.valueOf(59.99),
+ LocalDate.of(2023, 3, 1));
+
+ books = List.of(b1, b2, b3, b4, b5, b6);
+
+ bookPage = new PageImpl<>(books);
+ pageRequest = PageRequest.of(0, 2, Sort.by(Sort.Direction.DESC, "name"));
+
+ }
+
+ @Test
+ void testFindAllDefault() throws Exception {
+
+ when(bookService
+ .findAll(0, 10, "id", "ASC"))
+ .thenReturn(bookPage);
+
+ ResultActions result = mockMvc.perform(get("/books"));
+
+ result.andExpect(status().isOk()).andDo(print());
+
+ verify(bookService, times(1)).findAll(0, 10, "id", "ASC");
+
+ }
+
+ @Test
+ void testFindAllDefault2() throws Exception {
+
+ when(bookService
+ .findAll(0, 10, "id", "asc"))
+ .thenReturn(bookPage);
+
+ ResultActions result = mockMvc
+ .perform(get("/books?pageNo=0&pageSize=10&sortBy=id&sortDirection=asc"));
+
+ result.andExpect(status().isOk())
+ .andExpect(jsonPath("$", hasSize(6)))
+ .andExpect(jsonPath("$.[*].title",
+ containsInAnyOrder("Book A", "Book B", "Book C",
+ "Book D", "Book E", "Book F")))
+ .andDo(print());
+
+ }
+
+}
\ No newline at end of file
diff --git a/spring-data-jpa-paging-sorting/src/test/java/com/mkyong/BookRepositoryTest.java b/spring-data-jpa-paging-sorting/src/test/java/com/mkyong/BookRepositoryTest.java
new file mode 100644
index 0000000..ae1fc71
--- /dev/null
+++ b/spring-data-jpa-paging-sorting/src/test/java/com/mkyong/BookRepositoryTest.java
@@ -0,0 +1,86 @@
+package com.mkyong;
+
+import com.mkyong.book.Book;
+import com.mkyong.book.BookRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@DataJpaTest
+public class BookRepositoryTest {
+
+ @Autowired
+ private BookRepository bookRepository;
+
+ @BeforeEach
+ void setUp() {
+
+ bookRepository.deleteAll();
+
+ Book b1 = new Book("Book A",
+ BigDecimal.valueOf(9.99),
+ LocalDate.of(2023, 8, 31));
+ Book b2 = new Book("Book B",
+ BigDecimal.valueOf(19.99),
+ LocalDate.of(2023, 7, 31));
+ Book b3 = new Book("Book C",
+ BigDecimal.valueOf(29.99),
+ LocalDate.of(2023, 6, 10));
+ Book b4 = new Book("Book D",
+ BigDecimal.valueOf(39.99),
+ LocalDate.of(2023, 5, 5));
+ Book b5 = new Book("Book E",
+ BigDecimal.valueOf(49.99),
+ LocalDate.of(2023, 4, 1));
+ Book b6 = new Book("Book F",
+ BigDecimal.valueOf(59.99),
+ LocalDate.of(2023, 3, 1));
+
+ bookRepository.saveAllAndFlush(List.of(b1, b2, b3, b4, b5, b6));
+ }
+
+
+ @Test
+ public void testFindAll_Paging_Sorting() {
+
+ // page 1, size 4, sort by title, desc
+ Sort sort = Sort.by(Sort.Direction.DESC, "title");
+ Pageable pageable = PageRequest.of(0, 4, sort);
+
+ Page result = bookRepository.findAll(pageable);
+
+ List books = result.getContent();
+
+ assertEquals(4, books.size());
+
+ assertThat(result).extracting(Book::getTitle)
+ .containsExactlyInAnyOrder(
+ "Book C", "Book D", "Book E", "Book F");
+
+ // page 2, size 4, sort by title, desc
+ Pageable pageable2 = PageRequest.of(1, 4, sort);
+
+ Page result2 = bookRepository.findAll(pageable2);
+
+ List books2 = result2.getContent();
+
+ assertEquals(2, books2.size());
+
+ assertThat(result2).extracting(Book::getTitle)
+ .containsExactlyInAnyOrder(
+ "Book A", "Book B");
+ }
+
+}
diff --git a/spring-data-jpa-postgresql/.mvn/wrapper/maven-wrapper.jar b/spring-data-jpa-postgresql/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 0000000..cb28b0e
Binary files /dev/null and b/spring-data-jpa-postgresql/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/spring-data-jpa-postgresql/.mvn/wrapper/maven-wrapper.properties b/spring-data-jpa-postgresql/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..6f40a26
--- /dev/null
+++ b/spring-data-jpa-postgresql/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
diff --git a/spring-data-jpa-postgresql/README.md b/spring-data-jpa-postgresql/README.md
index 7fbcfec..a8f39dd 100644
--- a/spring-data-jpa-postgresql/README.md
+++ b/spring-data-jpa-postgresql/README.md
@@ -1,3 +1,29 @@
# Spring Boot + Spring data JPA + PostgreSQL example
-Article link : https://www.mkyong.com/spring-boot/spring-boot/spring-boot-spring-data-jpa-postgresql/
+Article link : https://mkyong.com/spring-boot/spring-boot/spring-boot-spring-data-jpa-postgresql/
+
+## Technologies used:
+Technologies used:
+* Spring Boot 3.1.2
+* Spring Data JPA (Hibernate 6 is the default JPA implementation)
+* PostgreSQL 15
+* Maven
+* Java 17
+* JUnit 5
+* Docker
+* [REST Assured](https://rest-assured.io/) and [Testcontainers](https://testcontainers.com/) (for Spring integration tests using a container)
+
+## How to run it
+```
+
+$ git clone [https://github.com/mkyong/spring-boot.git](https://github.com/mkyong/spring-boot.git)
+
+$ cd spring-data-jpa-postgresql
+
+$ ./mvnw clean package -Dmaven.test.skip=true
+
+$ docker run --name pg1 -p 5432:5432 -e POSTGRES_USER=mkyong -e POSTGRES_PASSWORD=password -e POSTGRES_DB=mydb -d postgres:15-alpine
+
+$ ./mvnw spring-boot:run
+
+```
\ No newline at end of file
diff --git a/spring-data-jpa-postgresql/mvnw b/spring-data-jpa-postgresql/mvnw
new file mode 100755
index 0000000..8d937f4
--- /dev/null
+++ b/spring-data-jpa-postgresql/mvnw
@@ -0,0 +1,308 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.2.0
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /usr/local/etc/mavenrc ] ; then
+ . /usr/local/etc/mavenrc
+ fi
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "$(uname)" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
+ else
+ JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=$(java-config --jre-home)
+ fi
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
+ JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="$(which javac)"
+ if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=$(which readlink)
+ if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
+ if $darwin ; then
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
+ else
+ javaExecutable="$(readlink -f "\"$javaExecutable\"")"
+ fi
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaHome=$(expr "$javaHome" : '\(.*\)/bin')
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=$(cd "$wdir/.." || exit 1; pwd)
+ fi
+ # end of workaround
+ done
+ printf '%s' "$(cd "$basedir" || exit 1; pwd)"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ # Remove \r in case we run on Windows within Git Bash
+ # and check out the repository with auto CRLF management
+ # enabled. Otherwise, we may read lines that are delimited with
+ # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
+ # splitting rules.
+ tr -s '\r\n' ' ' < "$1"
+ fi
+}
+
+log() {
+ if [ "$MVNW_VERBOSE" = true ]; then
+ printf '%s\n' "$1"
+ fi
+}
+
+BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
+log "$MAVEN_PROJECTBASEDIR"
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
+if [ -r "$wrapperJarPath" ]; then
+ log "Found $wrapperJarPath"
+else
+ log "Couldn't find $wrapperJarPath, downloading it ..."
+
+ if [ -n "$MVNW_REPOURL" ]; then
+ wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ else
+ wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ fi
+ while IFS="=" read -r key value; do
+ # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
+ safeValue=$(echo "$value" | tr -d '\r')
+ case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
+ esac
+ done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+ log "Downloading from: $wrapperUrl"
+
+ if $cygwin; then
+ wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
+ fi
+
+ if command -v wget > /dev/null; then
+ log "Found wget ... using wget"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ else
+ wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ log "Found curl ... using curl"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ else
+ curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ fi
+ else
+ log "Falling back to using Java to download"
+ javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaSource=$(cygpath --path --windows "$javaSource")
+ javaClass=$(cygpath --path --windows "$javaClass")
+ fi
+ if [ -e "$javaSource" ]; then
+ if [ ! -e "$javaClass" ]; then
+ log " - Compiling MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/javac" "$javaSource")
+ fi
+ if [ -e "$javaClass" ]; then
+ log " - Running MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+# If specified, validate the SHA-256 sum of the Maven wrapper jar file
+wrapperSha256Sum=""
+while IFS="=" read -r key value; do
+ case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
+ esac
+done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+if [ -n "$wrapperSha256Sum" ]; then
+ wrapperSha256Result=false
+ if command -v sha256sum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ elif command -v shasum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
+ echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
+ exit 1
+ fi
+ if [ $wrapperSha256Result = false ]; then
+ echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
+ echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
+ echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+# shellcheck disable=SC2086 # safe args
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ $MAVEN_DEBUG_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/spring-data-jpa-postgresql/mvnw.cmd b/spring-data-jpa-postgresql/mvnw.cmd
new file mode 100644
index 0000000..f80fbad
--- /dev/null
+++ b/spring-data-jpa-postgresql/mvnw.cmd
@@ -0,0 +1,205 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.2.0
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
+if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %WRAPPER_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
+SET WRAPPER_SHA_256_SUM=""
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
+)
+IF NOT %WRAPPER_SHA_256_SUM%=="" (
+ powershell -Command "&{"^
+ "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
+ "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
+ " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
+ " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
+ " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
+ " exit 1;"^
+ "}"^
+ "}"
+ if ERRORLEVEL 1 goto error
+)
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% ^
+ %JVM_CONFIG_MAVEN_PROPS% ^
+ %MAVEN_OPTS% ^
+ %MAVEN_DEBUG_OPTS% ^
+ -classpath %WRAPPER_JAR% ^
+ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
+ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
+if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%"=="on" pause
+
+if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
+
+cmd /C exit /B %ERROR_CODE%
diff --git a/spring-data-jpa-postgresql/pom.xml b/spring-data-jpa-postgresql/pom.xml
index ca88798..57158c4 100644
--- a/spring-data-jpa-postgresql/pom.xml
+++ b/spring-data-jpa-postgresql/pom.xml
@@ -4,48 +4,109 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
- spring-boot-data-jpa
+ spring-data-jpa-postgres
jar
- Spring Boot Spring Data JPA
+ Spring Data JPA and PostgreSQL
1.0
org.springframework.boot
spring-boot-starter-parent
- 2.1.2.RELEASE
+ 3.1.2
+
- 1.8
+ 17
+ 1.19.0
true
true
-
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
org.springframework.boot
spring-boot-starter-data-jpa
-
+
org.postgresql
postgresql
+ runtime
+
org.springframework.boot
spring-boot-starter-test
test
+
+
+ org.springframework.boot
+ spring-boot-testcontainers
+ test
+
+
+
+ org.testcontainers
+ testcontainers
+ test
+
+
+
+ org.testcontainers
+ postgresql
+ test
+
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+ true
+
+
+
+
+
+ org.testcontainers
+ testcontainers-bom
+ ${testcontainers.version}
+ pom
+ import
+
+
+
+
-
org.springframework.boot
spring-boot-maven-plugin
@@ -53,11 +114,13 @@
org.apache.maven.plugins
- maven-surefire-plugin
- 2.22.0
+ maven-compiler-plugin
+ 3.11.0
+
+ ${java.version}
+ ${java.version}
+
-
-
-
+
\ No newline at end of file
diff --git a/spring-data-jpa-postgresql/src/main/java/com/mkyong/Book.java b/spring-data-jpa-postgresql/src/main/java/com/mkyong/Book.java
deleted file mode 100644
index 97a19f0..0000000
--- a/spring-data-jpa-postgresql/src/main/java/com/mkyong/Book.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.mkyong;
-
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-
-@Entity
-public class Book {
-
- @Id
- @GeneratedValue(strategy = GenerationType.AUTO)
- private Long id;
- private String name;
-
- public Book() {
- }
-
- public Book(String name) {
- this.name = name;
- }
-
- @Override
- public String toString() {
- return "Book{" +
- "id=" + id +
- ", name='" + name + '\'' +
- '}';
- }
-
- public Long getId() {
- return id;
- }
-
- public void setId(Long id) {
- this.id = id;
- }
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-}
diff --git a/spring-data-jpa-postgresql/src/main/java/com/mkyong/BookRepository.java b/spring-data-jpa-postgresql/src/main/java/com/mkyong/BookRepository.java
deleted file mode 100644
index 43043a3..0000000
--- a/spring-data-jpa-postgresql/src/main/java/com/mkyong/BookRepository.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.mkyong;
-
-import org.springframework.data.repository.CrudRepository;
-
-import java.util.List;
-
-public interface BookRepository extends CrudRepository {
-
- List findByName(String name);
-
-}
diff --git a/spring-data-jpa-postgresql/src/main/java/com/mkyong/MainApplication.java b/spring-data-jpa-postgresql/src/main/java/com/mkyong/MainApplication.java
new file mode 100644
index 0000000..093dc17
--- /dev/null
+++ b/spring-data-jpa-postgresql/src/main/java/com/mkyong/MainApplication.java
@@ -0,0 +1,56 @@
+package com.mkyong;
+
+import com.mkyong.model.Book;
+import com.mkyong.repository.BookRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+
+@SpringBootApplication
+public class MainApplication {
+
+ private static final Logger log = LoggerFactory.getLogger(MainApplication.class);
+
+ public static void main(String[] args) {
+ SpringApplication.run(MainApplication.class, args);
+ }
+
+ @Autowired
+ BookRepository bookRepository;
+
+ // Run this if app.db.init.enabled = true
+ @Bean
+ @ConditionalOnProperty(prefix = "app", name = "db.init.enabled", havingValue = "true")
+ public CommandLineRunner demoCommandLineRunner() {
+ return args -> {
+
+ System.out.println("Running.....");
+
+ Book b1 = new Book("Book A",
+ BigDecimal.valueOf(9.99),
+ LocalDate.of(2023, 8, 31));
+ Book b2 = new Book("Book B",
+ BigDecimal.valueOf(19.99),
+ LocalDate.of(2023, 7, 31));
+ Book b3 = new Book("Book C",
+ BigDecimal.valueOf(29.99),
+ LocalDate.of(2023, 6, 10));
+ Book b4 = new Book("Book D",
+ BigDecimal.valueOf(39.99),
+ LocalDate.of(2023, 5, 5));
+
+ bookRepository.saveAll(List.of(b1, b2, b3, b4));
+
+ };
+ }
+
+}
\ No newline at end of file
diff --git a/spring-data-jpa-postgresql/src/main/java/com/mkyong/StartApplication.java b/spring-data-jpa-postgresql/src/main/java/com/mkyong/StartApplication.java
deleted file mode 100644
index be37d64..0000000
--- a/spring-data-jpa-postgresql/src/main/java/com/mkyong/StartApplication.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.mkyong;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.CommandLineRunner;
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-
-@SpringBootApplication
-public class StartApplication implements CommandLineRunner {
-
- private static final Logger log = LoggerFactory.getLogger(StartApplication.class);
-
- @Autowired
- private BookRepository repository;
-
- public static void main(String[] args) {
- SpringApplication.run(StartApplication.class, args);
- }
-
- @Override
- public void run(String... args) {
-
- log.info("StartApplication...");
-
- repository.save(new Book("Java"));
- repository.save(new Book("Node"));
- repository.save(new Book("Python"));
-
- System.out.println("\nfindAll()");
- repository.findAll().forEach(x -> System.out.println(x));
-
- System.out.println("\nfindById(1L)");
- repository.findById(1l).ifPresent(x -> System.out.println(x));
-
- System.out.println("\nfindByName('Node')");
- repository.findByName("Node").forEach(x -> System.out.println(x));
-
- }
-
-}
\ No newline at end of file
diff --git a/spring-data-jpa-postgresql/src/main/java/com/mkyong/controller/BookController.java b/spring-data-jpa-postgresql/src/main/java/com/mkyong/controller/BookController.java
new file mode 100644
index 0000000..3e76a3e
--- /dev/null
+++ b/spring-data-jpa-postgresql/src/main/java/com/mkyong/controller/BookController.java
@@ -0,0 +1,62 @@
+package com.mkyong.controller;
+
+import com.mkyong.model.Book;
+import com.mkyong.service.BookService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Optional;
+
+@RestController
+@RequestMapping("/books")
+public class BookController {
+
+ @Autowired
+ private BookService bookService;
+
+ @GetMapping
+ public List findAll() {
+ return bookService.findAll();
+ }
+
+ @GetMapping("/{id}")
+ public Optional findById(@PathVariable Long id) {
+ return bookService.findById(id);
+ }
+
+ // create a book
+ @ResponseStatus(HttpStatus.CREATED) // 201
+ @PostMapping
+ public Book create(@RequestBody Book book) {
+ return bookService.save(book);
+ }
+
+ // update a book
+ @PutMapping
+ public Book update(@RequestBody Book book) {
+ return bookService.save(book);
+ }
+
+ // delete a book
+ @ResponseStatus(HttpStatus.NO_CONTENT) // 204
+ @DeleteMapping("/{id}")
+ public void deleteById(@PathVariable Long id) {
+ bookService.deleteById(id);
+ }
+
+ @GetMapping("/find/title/{title}")
+ public List findByTitle(@PathVariable String title) {
+ return bookService.findByTitle(title);
+ }
+
+ @GetMapping("/find/date-after/{date}")
+ public List findByPublishedDateAfter(
+ @PathVariable @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
+ return bookService.findByPublishedDateAfter(date);
+ }
+
+}
diff --git a/spring-data-jpa-postgresql/src/main/java/com/mkyong/model/Book.java b/spring-data-jpa-postgresql/src/main/java/com/mkyong/model/Book.java
new file mode 100644
index 0000000..3c1bc2c
--- /dev/null
+++ b/spring-data-jpa-postgresql/src/main/java/com/mkyong/model/Book.java
@@ -0,0 +1,71 @@
+package com.mkyong.model;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+@Entity
+public class Book {
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private Long id;
+ private String title;
+ private BigDecimal price;
+ private LocalDate publishDate;
+
+ // for JPA only, no use
+ public Book() {
+ }
+
+ public Book(String title, BigDecimal price, LocalDate publishDate) {
+ this.title = title;
+ this.price = price;
+ this.publishDate = publishDate;
+ }
+
+ @Override
+ public String toString() {
+ return "Book{" +
+ "id=" + id +
+ ", title='" + title + '\'' +
+ ", price=" + price +
+ ", publishDate=" + publishDate +
+ '}';
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public BigDecimal getPrice() {
+ return price;
+ }
+
+ public void setPrice(BigDecimal price) {
+ this.price = price;
+ }
+
+ public LocalDate getPublishDate() {
+ return publishDate;
+ }
+
+ public void setPublishDate(LocalDate publishDate) {
+ this.publishDate = publishDate;
+ }
+}
diff --git a/spring-data-jpa-postgresql/src/main/java/com/mkyong/repository/BookRepository.java b/spring-data-jpa-postgresql/src/main/java/com/mkyong/repository/BookRepository.java
new file mode 100644
index 0000000..1bedd04
--- /dev/null
+++ b/spring-data-jpa-postgresql/src/main/java/com/mkyong/repository/BookRepository.java
@@ -0,0 +1,20 @@
+package com.mkyong.repository;
+
+import com.mkyong.model.Book;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.time.LocalDate;
+import java.util.List;
+
+// Spring Data JPA creates CRUD implementation at runtime automatically.
+public interface BookRepository extends JpaRepository {
+
+ List findByTitle(String title);
+
+ // Custom query
+ @Query("SELECT b FROM Book b WHERE b.publishDate > :date")
+ List findByPublishedDateAfter(@Param("date") LocalDate date);
+
+}
diff --git a/spring-data-jpa-postgresql/src/main/java/com/mkyong/service/BookService.java b/spring-data-jpa-postgresql/src/main/java/com/mkyong/service/BookService.java
new file mode 100644
index 0000000..deb92b2
--- /dev/null
+++ b/spring-data-jpa-postgresql/src/main/java/com/mkyong/service/BookService.java
@@ -0,0 +1,41 @@
+package com.mkyong.service;
+
+import com.mkyong.model.Book;
+import com.mkyong.repository.BookRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Optional;
+
+@Service
+public class BookService {
+
+ @Autowired
+ private BookRepository bookRepository;
+
+ public List findAll() {
+ return bookRepository.findAll();
+ }
+
+ public Optional findById(Long id) {
+ return bookRepository.findById(id);
+ }
+
+ public Book save(Book book) {
+ return bookRepository.save(book);
+ }
+
+ public void deleteById(Long id) {
+ bookRepository.deleteById(id);
+ }
+
+ public List findByTitle(String title) {
+ return bookRepository.findByTitle(title);
+ }
+
+ public List findByPublishedDateAfter(LocalDate date) {
+ return bookRepository.findByPublishedDateAfter(date);
+ }
+}
diff --git a/spring-data-jpa-postgresql/src/main/resources/application.properties b/spring-data-jpa-postgresql/src/main/resources/application.properties
index 3cfdd02..546ac11 100644
--- a/spring-data-jpa-postgresql/src/main/resources/application.properties
+++ b/spring-data-jpa-postgresql/src/main/resources/application.properties
@@ -1,19 +1,23 @@
logging.level.org.springframework=INFO
logging.level.com.mkyong=INFO
-logging.level.com.zaxxer=DEBUG
+logging.level.com.zaxxer=INFO
logging.level.root=ERROR
+logging.pattern.console=%-5level %logger{36} - %msg%n
+## Testing only
spring.datasource.hikari.connectionTimeout=20000
spring.datasource.hikari.maximumPoolSize=5
-logging.pattern.console=%-5level %logger{36} - %msg%n
-
## PostgreSQL
-spring.datasource.url=jdbc:postgresql://192.168.1.4:5432/postgres
-spring.datasource.username=postgres
+spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
+spring.datasource.username=mkyong
spring.datasource.password=password
-#drop n create table again, good for testing, comment this in production
-spring.jpa.hibernate.ddl-auto=create
+# create and drop table, good for testing, production set to none or comment it
+spring.jpa.hibernate.ddl-auto=create-drop
+
+# app custom property, if true, insert data for testing
+app.db.init.enabled=true
-spring.jpa.show-sql=true
\ No newline at end of file
+# enable query logging
+# spring.jpa.show-sql=true
\ No newline at end of file
diff --git a/spring-data-jpa-postgresql/src/test/java/com/mkyong/BookControllerTest.java b/spring-data-jpa-postgresql/src/test/java/com/mkyong/BookControllerTest.java
new file mode 100644
index 0000000..ab6e2d3
--- /dev/null
+++ b/spring-data-jpa-postgresql/src/test/java/com/mkyong/BookControllerTest.java
@@ -0,0 +1,216 @@
+package com.mkyong;
+
+import com.mkyong.model.Book;
+import com.mkyong.repository.BookRepository;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import io.restassured.response.Response;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.test.context.TestPropertySource;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+
+import static io.restassured.RestAssured.given;
+import static io.restassured.config.JsonConfig.jsonConfig;
+import static io.restassured.path.json.config.JsonPathConfig.NumberReturnType.BIG_DECIMAL;
+import static org.hamcrest.Matchers.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+// activate automatic startup and stop of containers
+@Testcontainers
+// JPA drop and create table, good for testing
+@TestPropertySource(properties = {"spring.jpa.hibernate.ddl-auto=create-drop"})
+public class BookControllerTest {
+
+ @LocalServerPort
+ private Integer port;
+
+ @Autowired
+ BookRepository bookRepository;
+
+ // static, all tests share this postgres container
+ @Container
+ @ServiceConnection
+ static PostgreSQLContainer> postgres = new PostgreSQLContainer<>(
+ "postgres:15-alpine"
+ );
+
+ @BeforeEach
+ void setUp() {
+ RestAssured.baseURI = "http://localhost:" + port;
+ bookRepository.deleteAll();
+
+ Book b1 = new Book("Book A",
+ BigDecimal.valueOf(9.99),
+ LocalDate.of(2023, 8, 31));
+ Book b2 = new Book("Book B",
+ BigDecimal.valueOf(19.99),
+ LocalDate.of(2023, 7, 31));
+ Book b3 = new Book("Book C",
+ BigDecimal.valueOf(29.99),
+ LocalDate.of(2023, 6, 10));
+ Book b4 = new Book("Book D",
+ BigDecimal.valueOf(39.99),
+ LocalDate.of(2023, 5, 5));
+
+ bookRepository.saveAll(List.of(b1, b2, b3, b4));
+ }
+
+ @Test
+ void testFindAll() {
+
+ given()
+ .contentType(ContentType.JSON)
+ .when()
+ .get("/books")
+ .then()
+ .statusCode(200) // expecting HTTP 200 OK
+ .contentType(ContentType.JSON) // expecting JSON response content
+ .body(".", hasSize(4));
+
+ }
+
+ @Test
+ void testFindByTitle() {
+
+ String title = "Book C";
+
+ given()
+ //Returning floats and doubles as BigDecimal
+ .config(RestAssured.config().jsonConfig(jsonConfig().numberReturnType(BIG_DECIMAL)))
+ .contentType(ContentType.JSON)
+ .pathParam("title", title)
+ .when()
+ .get("/books/find/title/{title}")
+ .then()
+ .statusCode(200)
+ .contentType(ContentType.JSON)
+ .body(
+ ".", hasSize(1),
+ "[0].title", equalTo("Book C"),
+ "[0].price", is(new BigDecimal("29.99")),
+ "[0].publishDate", equalTo("2023-06-10")
+ );
+ }
+
+ @Test
+ void testFindByPublishedDateAfter() {
+
+ String date = "2023-07-01";
+
+ Response result = given()
+ //Returning floats and doubles as BigDecimal
+ .config(RestAssured.config().jsonConfig(jsonConfig().numberReturnType(BIG_DECIMAL)))
+ .contentType(ContentType.JSON)
+ .pathParam("date", date)
+ .when()
+ .get("/books/find/date-after/{date}")
+ .then()
+ .statusCode(200)
+ .contentType(ContentType.JSON)
+ .body(
+ ".", hasSize(2),
+ "title", hasItems("Book A", "Book B"),
+ "price", hasItems(new BigDecimal("9.99"), new BigDecimal("19.99")),
+ "publishDate", hasItems("2023-08-31", "2023-07-31")
+ )
+ .extract().response();
+
+ // get the response and print it out
+ System.out.println(result.asString());
+
+ }
+
+
+ @Test
+ public void testDeleteById() {
+ Long id = 1L; // replace with a valid ID
+ given()
+ .pathParam("id", id)
+ .when()
+ .delete("/books/{id}")
+ .then()
+ .statusCode(204); // expecting HTTP 204 No Content
+ }
+
+ @Test
+ public void testCreate() {
+
+ given()
+ .contentType(ContentType.JSON)
+ .body("{ \"title\": \"Book E\", \"price\": \"9.99\", \"publishDate\": \"2023-09-14\" }")
+ .when()
+ .post("/books")
+ .then()
+ .statusCode(201) // expecting HTTP 201 Created
+ .contentType(ContentType.JSON); // expecting JSON response content
+
+ // find the new saved book
+ given()
+ //Returning floats and doubles as BigDecimal
+ .config(RestAssured.config().jsonConfig(jsonConfig().numberReturnType(BIG_DECIMAL)))
+ .contentType(ContentType.JSON)
+ .pathParam("title", "Book E")
+ .when()
+ .get("/books/find/title/{title}")
+ .then()
+ .statusCode(200)
+ .contentType(ContentType.JSON)
+ .body(
+ ".", hasSize(1),
+ "[0].title", equalTo("Book E"),
+ "[0].price", is(new BigDecimal("9.99")),
+ "[0].publishDate", equalTo("2023-09-14")
+ );
+ }
+
+ /**
+ * Book b4 = new Book("Book D",
+ * BigDecimal.valueOf(39.99),
+ * LocalDate.of(2023, 5, 5));
+ */
+ @Test
+ public void testUpdate() {
+
+ Book bookD = bookRepository.findByTitle("Book D").get(0);
+ System.out.println(bookD);
+
+ Long id = bookD.getId();
+
+ bookD.setTitle("Book E");
+ bookD.setPrice(new BigDecimal("199.99"));
+ bookD.setPublishDate(LocalDate.of(2024, 1, 31));
+
+ given()
+ .contentType(ContentType.JSON)
+ .body(bookD)
+ .when()
+ .put("/books")
+ .then()
+ .statusCode(200)
+ .contentType(ContentType.JSON);
+
+ // get the updated book
+ Book updatedBook = bookRepository.findById(id).orElseThrow();
+ System.out.println(updatedBook);
+
+ assertEquals(id, updatedBook.getId());
+ assertEquals("Book E", updatedBook.getTitle());
+ assertEquals(new BigDecimal("199.99"), updatedBook.getPrice());
+ assertEquals(LocalDate.of(2024, 1, 31), updatedBook.getPublishDate());
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/spring-data-jpa-postgresql/src/test/java/com/mkyong/BookRepositoryTest.java b/spring-data-jpa-postgresql/src/test/java/com/mkyong/BookRepositoryTest.java
index d8920ed..41c3248 100644
--- a/spring-data-jpa-postgresql/src/test/java/com/mkyong/BookRepositoryTest.java
+++ b/spring-data-jpa-postgresql/src/test/java/com/mkyong/BookRepositoryTest.java
@@ -1,37 +1,189 @@
-package com.mkyong;
+/*package com.mkyong;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import com.mkyong.model.Book;
+import com.mkyong.repository.BookRepository;
+import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
-import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
-import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import java.math.BigDecimal;
+import java.time.LocalDate;
import java.util.List;
+import java.util.Optional;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.assertEquals;
+import static org.junit.jupiter.api.Assertions.*;*/
-@RunWith(SpringRunner.class)
+/**
+ * @DataJpaTest 1. It scans the `@Entity` classes and Spring Data JPA repositories.
+ * 2. Set the `spring.jpa.show-sql` property to true and enable the SQL queries logging.
+ * 3. Default, JPA test data are transactional and roll back at the end of each test;
+ * it means we do not need to clean up saved or modified table data after each test.
+ * 4. Replace the application DataSource, run and configure the embed database on classpath.
+ */
+/*
@DataJpaTest
+// We provide the `test containers` as DataSource, don't replace it.
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+// activate automatic startup and stop of containers
+@Testcontainers
public class BookRepositoryTest {
@Autowired
- private TestEntityManager entityManager;
+ private BookRepository bookRepository;
- @Autowired
- private BookRepository repository;
+ // static, all tests share this
+ @Container
+ @ServiceConnection
+ static PostgreSQLContainer> postgres = new PostgreSQLContainer<>(
+ "postgres:15-alpine"
+ );
+
+ @Test
+ public void testSave() {
+
+ Book b1 = new Book("Book A",
+ BigDecimal.valueOf(9.99),
+ LocalDate.of(2023, 8, 31));
+
+ //testEM.persistAndFlush(b1); the same
+ bookRepository.save(b1);
+
+ Long savedBookID = b1.getId();
+
+ Book book = bookRepository.findById(savedBookID).orElseThrow();
+ // Book book = testEM.find(Book.class, savedBookID);
+
+ assertEquals(savedBookID, book.getId());
+ assertEquals("Book A", book.getTitle());
+ assertEquals(BigDecimal.valueOf(9.99), book.getPrice());
+ assertEquals(LocalDate.of(2023, 8, 31), book.getPublishDate());
+
+
+ }
+
+ @Test
+ public void testUpdate() {
+
+ Book b1 = new Book("Book A",
+ BigDecimal.valueOf(9.99),
+ LocalDate.of(2023, 8, 31));
+
+ //testEM.persistAndFlush(b1);
+ bookRepository.save(b1);
+
+ // update price from 9.99 to 19.99
+ b1.setPrice(BigDecimal.valueOf(19.99));
+
+ // update
+ bookRepository.save(b1);
+
+ List result = bookRepository.findByTitle("Book A");
+
+ assertEquals(1, result.size());
+
+ Book book = result.get(0);
+ assertNotNull(book.getId());
+ assertTrue(book.getId() > 0);
+
+ assertEquals("Book A", book.getTitle());
+ assertEquals(BigDecimal.valueOf(19.99), book.getPrice());
+ assertEquals(LocalDate.of(2023, 8, 31), book.getPublishDate());
+
+ }
+
+ @Test
+ public void testFindByTitle() {
+
+ Book b1 = new Book("Book A",
+ BigDecimal.valueOf(9.99),
+ LocalDate.of(2023, 8, 31));
+ bookRepository.save(b1);
+
+ List result = bookRepository.findByTitle("Book A");
+
+ assertEquals(1, result.size());
+ Book book = result.get(0);
+ assertNotNull(book.getId());
+ assertTrue(book.getId() > 0);
+
+ assertEquals("Book A", book.getTitle());
+ assertEquals(BigDecimal.valueOf(9.99), book.getPrice());
+ assertEquals(LocalDate.of(2023, 8, 31), book.getPublishDate());
+
+ }
+
+ @Test
+ public void testFindAll() {
+
+ Book b1 = new Book("Book A",
+ BigDecimal.valueOf(9.99),
+ LocalDate.of(2023, 8, 31));
+ Book b2 = new Book("Book B",
+ BigDecimal.valueOf(19.99),
+ LocalDate.of(2023, 7, 31));
+ Book b3 = new Book("Book C",
+ BigDecimal.valueOf(29.99),
+ LocalDate.of(2023, 6, 10));
+ Book b4 = new Book("Book D",
+ BigDecimal.valueOf(39.99),
+ LocalDate.of(2023, 5, 5));
+
+ bookRepository.saveAll(List.of(b1, b2, b3, b4));
+
+ List result = bookRepository.findAll();
+ assertEquals(4, result.size());
+
+ }
+
+ @Test
+ public void testFindByPublishedDateAfter() {
+
+ Book b1 = new Book("Book A",
+ BigDecimal.valueOf(9.99),
+ LocalDate.of(2023, 8, 31));
+ Book b2 = new Book("Book B",
+ BigDecimal.valueOf(19.99),
+ LocalDate.of(2023, 7, 31));
+ Book b3 = new Book("Book C",
+ BigDecimal.valueOf(29.99),
+ LocalDate.of(2023, 6, 10));
+ Book b4 = new Book("Book D",
+ BigDecimal.valueOf(39.99),
+ LocalDate.of(2023, 5, 5));
+
+ bookRepository.saveAll(List.of(b1, b2, b3, b4));
+
+ List result = bookRepository.findByPublishedDateAfter(
+ LocalDate.of(2023, 7, 1));
+
+ // b1 and b2
+ assertEquals(2, result.size());
+
+ }
@Test
- public void testFindByName() {
+ public void testDeleteById() {
+
+ Book b1 = new Book("Book A",
+ BigDecimal.valueOf(9.99),
+ LocalDate.of(2023, 8, 31));
+ bookRepository.save(b1);
+
+ Long savedBookID = b1.getId();
- entityManager.persist(new Book("C++"));
+ // Book book = bookRepository.findById(savedBookID).orElseThrow();
+ // Book book = testEM.find(Book.class, savedBookID);
- List books = repository.findByName("C++");
- assertEquals(1, books.size());
+ bookRepository.deleteById(savedBookID);
- assertThat(books).extracting(Book::getName).containsOnly("C++");
+ Optional result = bookRepository.findById(savedBookID);
+ assertTrue(result.isEmpty());
}
-}
+}*/
\ No newline at end of file
diff --git a/spring-data-jpa-test/.mvn/wrapper/maven-wrapper.jar b/spring-data-jpa-test/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 0000000..cb28b0e
Binary files /dev/null and b/spring-data-jpa-test/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/spring-data-jpa-test/.mvn/wrapper/maven-wrapper.properties b/spring-data-jpa-test/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..6f40a26
--- /dev/null
+++ b/spring-data-jpa-test/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
diff --git a/spring-data-jpa-test/README.md b/spring-data-jpa-test/README.md
new file mode 100644
index 0000000..0129f59
--- /dev/null
+++ b/spring-data-jpa-test/README.md
@@ -0,0 +1,20 @@
+# Spring data JPA + @DataJpaTest
+
+This is the source code for [Testing Spring Data JPA with @DataJpaTest](https://mkyong.com/spring-boot/testing-spring-data-jpa-with-datajpatest/)
+
+## Technologies used:
+* Spring Boot 3.1.2
+* Spring Data JPA (Hibernate 6 is the default JPA implementation)
+* H2 in-memory database
+* Maven
+* Java 17
+* JUnit 5
+
+## How to run it
+```
+$ git clone https://github.com/mkyong/spring-boot.git
+
+$ cd spring-data-jpa-test
+
+$ ./mvnw test
+```
\ No newline at end of file
diff --git a/spring-data-jpa-test/mvnw b/spring-data-jpa-test/mvnw
new file mode 100755
index 0000000..8d937f4
--- /dev/null
+++ b/spring-data-jpa-test/mvnw
@@ -0,0 +1,308 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.2.0
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /usr/local/etc/mavenrc ] ; then
+ . /usr/local/etc/mavenrc
+ fi
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "$(uname)" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
+ else
+ JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=$(java-config --jre-home)
+ fi
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
+ JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="$(which javac)"
+ if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=$(which readlink)
+ if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
+ if $darwin ; then
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
+ else
+ javaExecutable="$(readlink -f "\"$javaExecutable\"")"
+ fi
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaHome=$(expr "$javaHome" : '\(.*\)/bin')
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=$(cd "$wdir/.." || exit 1; pwd)
+ fi
+ # end of workaround
+ done
+ printf '%s' "$(cd "$basedir" || exit 1; pwd)"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ # Remove \r in case we run on Windows within Git Bash
+ # and check out the repository with auto CRLF management
+ # enabled. Otherwise, we may read lines that are delimited with
+ # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
+ # splitting rules.
+ tr -s '\r\n' ' ' < "$1"
+ fi
+}
+
+log() {
+ if [ "$MVNW_VERBOSE" = true ]; then
+ printf '%s\n' "$1"
+ fi
+}
+
+BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
+log "$MAVEN_PROJECTBASEDIR"
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
+if [ -r "$wrapperJarPath" ]; then
+ log "Found $wrapperJarPath"
+else
+ log "Couldn't find $wrapperJarPath, downloading it ..."
+
+ if [ -n "$MVNW_REPOURL" ]; then
+ wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ else
+ wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ fi
+ while IFS="=" read -r key value; do
+ # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
+ safeValue=$(echo "$value" | tr -d '\r')
+ case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
+ esac
+ done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+ log "Downloading from: $wrapperUrl"
+
+ if $cygwin; then
+ wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
+ fi
+
+ if command -v wget > /dev/null; then
+ log "Found wget ... using wget"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ else
+ wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ log "Found curl ... using curl"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ else
+ curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ fi
+ else
+ log "Falling back to using Java to download"
+ javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaSource=$(cygpath --path --windows "$javaSource")
+ javaClass=$(cygpath --path --windows "$javaClass")
+ fi
+ if [ -e "$javaSource" ]; then
+ if [ ! -e "$javaClass" ]; then
+ log " - Compiling MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/javac" "$javaSource")
+ fi
+ if [ -e "$javaClass" ]; then
+ log " - Running MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+# If specified, validate the SHA-256 sum of the Maven wrapper jar file
+wrapperSha256Sum=""
+while IFS="=" read -r key value; do
+ case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
+ esac
+done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+if [ -n "$wrapperSha256Sum" ]; then
+ wrapperSha256Result=false
+ if command -v sha256sum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ elif command -v shasum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
+ echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
+ exit 1
+ fi
+ if [ $wrapperSha256Result = false ]; then
+ echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
+ echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
+ echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+# shellcheck disable=SC2086 # safe args
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ $MAVEN_DEBUG_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/spring-data-jpa-test/mvnw.cmd b/spring-data-jpa-test/mvnw.cmd
new file mode 100644
index 0000000..f80fbad
--- /dev/null
+++ b/spring-data-jpa-test/mvnw.cmd
@@ -0,0 +1,205 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.2.0
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
+if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %WRAPPER_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
+SET WRAPPER_SHA_256_SUM=""
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
+)
+IF NOT %WRAPPER_SHA_256_SUM%=="" (
+ powershell -Command "&{"^
+ "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
+ "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
+ " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
+ " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
+ " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
+ " exit 1;"^
+ "}"^
+ "}"
+ if ERRORLEVEL 1 goto error
+)
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% ^
+ %JVM_CONFIG_MAVEN_PROPS% ^
+ %MAVEN_OPTS% ^
+ %MAVEN_DEBUG_OPTS% ^
+ -classpath %WRAPPER_JAR% ^
+ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
+ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
+if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%"=="on" pause
+
+if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
+
+cmd /C exit /B %ERROR_CODE%
diff --git a/spring-data-jpa-test/pom.xml b/spring-data-jpa-test/pom.xml
new file mode 100644
index 0000000..4bef679
--- /dev/null
+++ b/spring-data-jpa-test/pom.xml
@@ -0,0 +1,68 @@
+
+
+ 4.0.0
+
+ spring-data-jpa-test
+ jar
+ Spring Boot Spring Data JPA
+ https://mkyong.com
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.1.2
+
+
+
+ 17
+ true
+ true
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+
+ com.h2database
+ h2
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ ${java.version}
+ ${java.version}
+
+
+
+
+
+
+
diff --git a/spring-data-jpa-test/src/main/java/com/mkyong/Book.java b/spring-data-jpa-test/src/main/java/com/mkyong/Book.java
new file mode 100644
index 0000000..09245b4
--- /dev/null
+++ b/spring-data-jpa-test/src/main/java/com/mkyong/Book.java
@@ -0,0 +1,72 @@
+package com.mkyong;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+@Entity
+public class Book {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private Long id;
+ private String title;
+ private BigDecimal price;
+ private LocalDate publishDate;
+
+ // for JPA only, no use
+ protected Book() {
+ }
+
+ public Book(String title, BigDecimal price, LocalDate publishDate) {
+ this.title = title;
+ this.price = price;
+ this.publishDate = publishDate;
+ }
+
+ @Override
+ public String toString() {
+ return "Book{" +
+ "id=" + id +
+ ", title='" + title + '\'' +
+ ", price=" + price +
+ ", publishDate=" + publishDate +
+ '}';
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public BigDecimal getPrice() {
+ return price;
+ }
+
+ public void setPrice(BigDecimal price) {
+ this.price = price;
+ }
+
+ public LocalDate getPublishDate() {
+ return publishDate;
+ }
+
+ public void setPublishDate(LocalDate publishDate) {
+ this.publishDate = publishDate;
+ }
+}
diff --git a/spring-data-jpa-test/src/main/java/com/mkyong/BookRepository.java b/spring-data-jpa-test/src/main/java/com/mkyong/BookRepository.java
new file mode 100644
index 0000000..3f8751a
--- /dev/null
+++ b/spring-data-jpa-test/src/main/java/com/mkyong/BookRepository.java
@@ -0,0 +1,20 @@
+package com.mkyong;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.time.LocalDate;
+import java.util.List;
+
+//Spring Data JPA creates CRUD implementation at runtime automatically.
+public interface BookRepository extends JpaRepository {
+
+ // match the book field name
+ List findByTitle(String title);
+
+ // Custom Query
+ @Query("SELECT b FROM Book b WHERE b.publishDate > :date")
+ List findByPublishedDateAfter(@Param("date") LocalDate date);
+
+}
diff --git a/spring-data-jpa-test/src/main/java/com/mkyong/BookService.java b/spring-data-jpa-test/src/main/java/com/mkyong/BookService.java
new file mode 100644
index 0000000..2fc9b79
--- /dev/null
+++ b/spring-data-jpa-test/src/main/java/com/mkyong/BookService.java
@@ -0,0 +1,41 @@
+package com.mkyong;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Optional;
+
+@Service
+public class BookService {
+
+ @Autowired
+ private BookRepository bookRepository;
+
+ public List findAll() {
+
+ // convert to List
+ /*Iterable booksIterable = bookRepository.findAll();
+ List booksList = new ArrayList<>();
+ booksIterable.forEach(booksList::add);*/
+ return bookRepository.findAll();
+
+ }
+
+ public Optional findById(Long id) {
+ return bookRepository.findById(id);
+ }
+
+ public Book save(Book book) {
+ return bookRepository.save(book);
+ }
+
+ public void deleteById(Long id) {
+ bookRepository.deleteById(id);
+ }
+
+ public List findByPublishedDateAfter(LocalDate date) {
+ return bookRepository.findByPublishedDateAfter(date);
+ }
+}
diff --git a/spring-data-jpa-test/src/main/java/com/mkyong/MainApplication.java b/spring-data-jpa-test/src/main/java/com/mkyong/MainApplication.java
new file mode 100644
index 0000000..16ee662
--- /dev/null
+++ b/spring-data-jpa-test/src/main/java/com/mkyong/MainApplication.java
@@ -0,0 +1,17 @@
+package com.mkyong;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class MainApplication {
+
+ private static final Logger log = LoggerFactory.getLogger(MainApplication.class);
+
+ public static void main(String[] args) {
+ SpringApplication.run(MainApplication.class, args);
+ }
+
+}
\ No newline at end of file
diff --git a/spring-data-jpa-test/src/main/resources/application.properties b/spring-data-jpa-test/src/main/resources/application.properties
new file mode 100644
index 0000000..4433737
--- /dev/null
+++ b/spring-data-jpa-test/src/main/resources/application.properties
@@ -0,0 +1,10 @@
+logging.level.org.springframework=INFO
+logging.level.com.mkyong=INFO
+logging.level.com.zaxxer=DEBUG
+logging.level.root=ERROR
+
+spring.datasource.hikari.connectionTimeout=20000
+spring.datasource.hikari.maximumPoolSize=5
+spring.datasource.hikari.poolName=HikariPoolZZZ
+
+logging.pattern.console=%-5level %logger{36} - %msg%n
\ No newline at end of file
diff --git a/spring-data-jpa-test/src/test/java/com/mkyong/BookRepositoryMockTest.java b/spring-data-jpa-test/src/test/java/com/mkyong/BookRepositoryMockTest.java
new file mode 100644
index 0000000..b6b3f98
--- /dev/null
+++ b/spring-data-jpa-test/src/test/java/com/mkyong/BookRepositoryMockTest.java
@@ -0,0 +1,84 @@
+package com.mkyong;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.*;
+
+public class BookRepositoryMockTest {
+
+ @InjectMocks
+ private BookService bookService;
+
+ @Mock
+ private BookRepository bookRepository;
+
+ @BeforeEach
+ public void setUp() {
+ MockitoAnnotations.openMocks(this); // init the mocks
+ }
+
+ @Test
+ public void testSave() {
+
+ Book book = new Book();
+ book.setTitle("Hello Java");
+
+ when(bookRepository.save(book)).thenReturn(book);
+
+ Book result = bookService.save(book);
+ assertEquals("Hello Java", result.getTitle());
+
+ verify(bookRepository, times(1)).save(book);
+ }
+
+ @Test
+ public void testFindById() {
+
+ Book book = new Book();
+ book.setId(1L);
+ book.setTitle("Hello Java");
+
+ when(bookRepository.findById(1L)).thenReturn(Optional.of(book));
+
+ Optional result = bookService.findById(1L);
+ assertTrue(result.isPresent());
+ assertEquals("Hello Java", result.get().getTitle());
+
+ verify(bookRepository, times(1)).findById(1L);
+ }
+
+ @Test
+ public void testFindAll() {
+ Book book1 = new Book();
+ book1.setTitle("Book A");
+
+ Book book2 = new Book();
+ book2.setTitle("Book B");
+
+ when(bookRepository.findAll()).thenReturn(List.of(book1, book2));
+
+ List results = bookService.findAll();
+ assertEquals(2, results.size());
+
+ verify(bookRepository, times(1)).findAll();
+ }
+
+ @Test
+ public void testDeleteById() {
+
+ Long bookId = 1L;
+ bookService.deleteById(1L);
+
+ verify(bookRepository, times(1)).deleteById(bookId);
+ }
+
+}
\ No newline at end of file
diff --git a/spring-data-jpa-test/src/test/java/com/mkyong/BookRepositoryTest.java b/spring-data-jpa-test/src/test/java/com/mkyong/BookRepositoryTest.java
new file mode 100644
index 0000000..98e5f5f
--- /dev/null
+++ b/spring-data-jpa-test/src/test/java/com/mkyong/BookRepositoryTest.java
@@ -0,0 +1,175 @@
+package com.mkyong;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+// Default, JPA tests data are transactional and roll back at the end of each test.
+// @DataJpaTest
+// Add below will disabled the roll back
+// @Transactional(propagation = Propagation.NOT_SUPPORTED)
+
+// Default is true, set showSql to false will disable the SQL queries logging
+// @DataJpaTest(showSql = false)
+@DataJpaTest
+public class BookRepositoryTest {
+
+ // Alternative for EntityManager
+ // Optional in this case, we can use bookRepository to do the same stuff
+ @Autowired
+ private TestEntityManager testEM;
+ @Autowired
+ private BookRepository bookRepository;
+
+ @Test
+ public void testSave() {
+
+ Book b1 = new Book("Book A",
+ BigDecimal.valueOf(9.99),
+ LocalDate.of(2023, 8, 31));
+
+ //testEM.persistAndFlush(b1); the same
+ bookRepository.save(b1);
+
+ Long savedBookID = b1.getId();
+
+ Book book = bookRepository.findById(savedBookID).orElseThrow();
+ // Book book = testEM.find(Book.class, savedBookID);
+
+ assertEquals(savedBookID, book.getId());
+ assertEquals("Book A", book.getTitle());
+ assertEquals(BigDecimal.valueOf(9.99), book.getPrice());
+ assertEquals(LocalDate.of(2023, 8, 31), book.getPublishDate());
+
+
+ }
+
+ @Test
+ public void testUpdate() {
+
+ Book b1 = new Book("Book A",
+ BigDecimal.valueOf(9.99),
+ LocalDate.of(2023, 8, 31));
+
+ //testEM.persistAndFlush(b1);
+ bookRepository.save(b1);
+
+ // update price from 9.99 to 19.99
+ b1.setPrice(BigDecimal.valueOf(19.99));
+
+ // update
+ bookRepository.save(b1);
+
+ List result = bookRepository.findByTitle("Book A");
+
+ assertEquals(1, result.size());
+
+ Book book = result.get(0);
+ assertNotNull(book.getId());
+ assertTrue(book.getId() > 0);
+
+ assertEquals("Book A", book.getTitle());
+ assertEquals(BigDecimal.valueOf(19.99), book.getPrice());
+ assertEquals(LocalDate.of(2023, 8, 31), book.getPublishDate());
+
+
+ }
+
+ @Test
+ public void testFindByTitle() {
+
+ Book b1 = new Book("Book A",
+ BigDecimal.valueOf(9.99),
+ LocalDate.of(2023, 8, 31));
+ bookRepository.save(b1);
+
+ List result = bookRepository.findByTitle("Book A");
+
+ assertEquals(1, result.size());
+ Book book = result.get(0);
+ assertNotNull(book.getId());
+ assertTrue(book.getId() > 0);
+
+ assertEquals("Book A", book.getTitle());
+ assertEquals(BigDecimal.valueOf(9.99), book.getPrice());
+ assertEquals(LocalDate.of(2023, 8, 31), book.getPublishDate());
+
+ }
+
+ @Test
+ public void testFindAll() {
+
+ Book b1 = new Book("Book A",
+ BigDecimal.valueOf(9.99),
+ LocalDate.of(2023, 8, 31));
+ Book b2 = new Book("Book B",
+ BigDecimal.valueOf(19.99),
+ LocalDate.of(2023, 7, 31));
+ Book b3 = new Book("Book C",
+ BigDecimal.valueOf(29.99),
+ LocalDate.of(2023, 6, 10));
+ Book b4 = new Book("Book D",
+ BigDecimal.valueOf(39.99),
+ LocalDate.of(2023, 5, 5));
+
+ bookRepository.saveAll(List.of(b1, b2, b3, b4));
+
+ List result = bookRepository.findAll();
+ assertEquals(4, result.size());
+
+ }
+
+ @Test
+ public void testFindByPublishedDateAfter() {
+
+ Book b1 = new Book("Book A",
+ BigDecimal.valueOf(9.99),
+ LocalDate.of(2023, 8, 31));
+ Book b2 = new Book("Book B",
+ BigDecimal.valueOf(19.99),
+ LocalDate.of(2023, 7, 31));
+ Book b3 = new Book("Book C",
+ BigDecimal.valueOf(29.99),
+ LocalDate.of(2023, 6, 10));
+ Book b4 = new Book("Book D",
+ BigDecimal.valueOf(39.99),
+ LocalDate.of(2023, 5, 5));
+
+ bookRepository.saveAll(List.of(b1, b2, b3, b4));
+
+ List result = bookRepository.findByPublishedDateAfter(
+ LocalDate.of(2023, 7, 1));
+ // b1 and b2
+ assertEquals(2, result.size());
+
+ }
+
+ @Test
+ public void testDeleteById() {
+
+ Book b1 = new Book("Book A",
+ BigDecimal.valueOf(9.99),
+ LocalDate.of(2023, 8, 31));
+ bookRepository.save(b1);
+
+ Long savedBookID = b1.getId();
+
+ // Book book = bookRepository.findById(savedBookID).orElseThrow();
+ // Book book = testEM.find(Book.class, savedBookID);
+
+ bookRepository.deleteById(savedBookID);
+
+ Optional result = bookRepository.findById(savedBookID);
+ assertTrue(result.isEmpty());
+
+ }
+
+}
\ No newline at end of file
diff --git a/spring-data-jpa/.mvn/wrapper/maven-wrapper.jar b/spring-data-jpa/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 0000000..cb28b0e
Binary files /dev/null and b/spring-data-jpa/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/spring-data-jpa/.mvn/wrapper/maven-wrapper.properties b/spring-data-jpa/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..6f40a26
--- /dev/null
+++ b/spring-data-jpa/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
diff --git a/spring-data-jpa/README.md b/spring-data-jpa/README.md
index 3269928..df1bd98 100644
--- a/spring-data-jpa/README.md
+++ b/spring-data-jpa/README.md
@@ -1,3 +1,22 @@
-# Spring Boot + Spring data JPA
+# Spring Boot + Spring Data JPA example
+
+This is the source code for [Spring Boot + Spring Data JPA example](https://mkyong.com/spring-boot/spring-boot-spring-data-jpa/)
+
+## Technologies used:
+* Spring Boot 3.1.2
+* Spring Data JPA (Hibernate 6 is the default JPA implementation)
+* H2 in-memory database
+* Maven
+* Java 17
+* JUnit 5
+
+## How to run it
+```
+$ git clone https://github.com/mkyong/spring-boot.git
+
+$ cd spring-data-jpa
+
+$ ./mvnw spring-boot:run
+```
+
-Article link : https://www.mkyong.com/spring-boot/spring-boot-spring-data-jpa/
\ No newline at end of file
diff --git a/spring-data-jpa/mvnw b/spring-data-jpa/mvnw
new file mode 100755
index 0000000..8d937f4
--- /dev/null
+++ b/spring-data-jpa/mvnw
@@ -0,0 +1,308 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.2.0
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /usr/local/etc/mavenrc ] ; then
+ . /usr/local/etc/mavenrc
+ fi
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "$(uname)" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
+ else
+ JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=$(java-config --jre-home)
+ fi
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
+ JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="$(which javac)"
+ if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=$(which readlink)
+ if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
+ if $darwin ; then
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
+ else
+ javaExecutable="$(readlink -f "\"$javaExecutable\"")"
+ fi
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaHome=$(expr "$javaHome" : '\(.*\)/bin')
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=$(cd "$wdir/.." || exit 1; pwd)
+ fi
+ # end of workaround
+ done
+ printf '%s' "$(cd "$basedir" || exit 1; pwd)"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ # Remove \r in case we run on Windows within Git Bash
+ # and check out the repository with auto CRLF management
+ # enabled. Otherwise, we may read lines that are delimited with
+ # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
+ # splitting rules.
+ tr -s '\r\n' ' ' < "$1"
+ fi
+}
+
+log() {
+ if [ "$MVNW_VERBOSE" = true ]; then
+ printf '%s\n' "$1"
+ fi
+}
+
+BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
+log "$MAVEN_PROJECTBASEDIR"
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
+if [ -r "$wrapperJarPath" ]; then
+ log "Found $wrapperJarPath"
+else
+ log "Couldn't find $wrapperJarPath, downloading it ..."
+
+ if [ -n "$MVNW_REPOURL" ]; then
+ wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ else
+ wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ fi
+ while IFS="=" read -r key value; do
+ # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
+ safeValue=$(echo "$value" | tr -d '\r')
+ case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
+ esac
+ done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+ log "Downloading from: $wrapperUrl"
+
+ if $cygwin; then
+ wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
+ fi
+
+ if command -v wget > /dev/null; then
+ log "Found wget ... using wget"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ else
+ wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ log "Found curl ... using curl"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ else
+ curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ fi
+ else
+ log "Falling back to using Java to download"
+ javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaSource=$(cygpath --path --windows "$javaSource")
+ javaClass=$(cygpath --path --windows "$javaClass")
+ fi
+ if [ -e "$javaSource" ]; then
+ if [ ! -e "$javaClass" ]; then
+ log " - Compiling MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/javac" "$javaSource")
+ fi
+ if [ -e "$javaClass" ]; then
+ log " - Running MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+# If specified, validate the SHA-256 sum of the Maven wrapper jar file
+wrapperSha256Sum=""
+while IFS="=" read -r key value; do
+ case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
+ esac
+done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+if [ -n "$wrapperSha256Sum" ]; then
+ wrapperSha256Result=false
+ if command -v sha256sum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ elif command -v shasum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
+ echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
+ exit 1
+ fi
+ if [ $wrapperSha256Result = false ]; then
+ echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
+ echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
+ echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+# shellcheck disable=SC2086 # safe args
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ $MAVEN_DEBUG_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/spring-data-jpa/mvnw.cmd b/spring-data-jpa/mvnw.cmd
new file mode 100644
index 0000000..f80fbad
--- /dev/null
+++ b/spring-data-jpa/mvnw.cmd
@@ -0,0 +1,205 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.2.0
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
+if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %WRAPPER_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
+SET WRAPPER_SHA_256_SUM=""
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
+)
+IF NOT %WRAPPER_SHA_256_SUM%=="" (
+ powershell -Command "&{"^
+ "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
+ "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
+ " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
+ " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
+ " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
+ " exit 1;"^
+ "}"^
+ "}"
+ if ERRORLEVEL 1 goto error
+)
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% ^
+ %JVM_CONFIG_MAVEN_PROPS% ^
+ %MAVEN_OPTS% ^
+ %MAVEN_DEBUG_OPTS% ^
+ -classpath %WRAPPER_JAR% ^
+ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
+ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
+if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%"=="on" pause
+
+if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
+
+cmd /C exit /B %ERROR_CODE%
diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml
index e091887..c19b3e9 100644
--- a/spring-data-jpa/pom.xml
+++ b/spring-data-jpa/pom.xml
@@ -7,34 +7,36 @@
spring-data-jpa
jar
Spring Boot Spring Data JPA
+ https://mkyong.com
1.0
org.springframework.boot
spring-boot-starter-parent
- 2.1.2.RELEASE
+ 3.1.2
- 1.8
+ 17
true
true
-
+
org.springframework.boot
spring-boot-starter-data-jpa
-
+
com.h2database
h2
+
org.springframework.boot
spring-boot-starter-test
@@ -45,7 +47,6 @@
-
org.springframework.boot
spring-boot-maven-plugin
@@ -53,11 +54,15 @@
org.apache.maven.plugins
- maven-surefire-plugin
- 2.22.0
+ maven-compiler-plugin
+ 3.11.0
+
+ ${java.version}
+ ${java.version}
+
-
-
+
+
\ No newline at end of file
diff --git a/spring-data-jpa/src/main/java/com/mkyong/Book.java b/spring-data-jpa/src/main/java/com/mkyong/Book.java
index 97a19f0..09245b4 100644
--- a/spring-data-jpa/src/main/java/com/mkyong/Book.java
+++ b/spring-data-jpa/src/main/java/com/mkyong/Book.java
@@ -1,9 +1,12 @@
package com.mkyong;
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
@Entity
public class Book {
@@ -11,20 +14,27 @@ public class Book {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
- private String name;
+ private String title;
+ private BigDecimal price;
+ private LocalDate publishDate;
- public Book() {
+ // for JPA only, no use
+ protected Book() {
}
- public Book(String name) {
- this.name = name;
+ public Book(String title, BigDecimal price, LocalDate publishDate) {
+ this.title = title;
+ this.price = price;
+ this.publishDate = publishDate;
}
@Override
public String toString() {
return "Book{" +
"id=" + id +
- ", name='" + name + '\'' +
+ ", title='" + title + '\'' +
+ ", price=" + price +
+ ", publishDate=" + publishDate +
'}';
}
@@ -36,11 +46,27 @@ public void setId(Long id) {
this.id = id;
}
- public String getName() {
- return name;
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public BigDecimal getPrice() {
+ return price;
+ }
+
+ public void setPrice(BigDecimal price) {
+ this.price = price;
+ }
+
+ public LocalDate getPublishDate() {
+ return publishDate;
}
- public void setName(String name) {
- this.name = name;
+ public void setPublishDate(LocalDate publishDate) {
+ this.publishDate = publishDate;
}
}
diff --git a/spring-data-jpa/src/main/java/com/mkyong/BookRepository.java b/spring-data-jpa/src/main/java/com/mkyong/BookRepository.java
index 43043a3..9a46e82 100644
--- a/spring-data-jpa/src/main/java/com/mkyong/BookRepository.java
+++ b/spring-data-jpa/src/main/java/com/mkyong/BookRepository.java
@@ -1,11 +1,20 @@
package com.mkyong;
-import org.springframework.data.repository.CrudRepository;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import java.time.LocalDate;
import java.util.List;
-public interface BookRepository extends CrudRepository {
+//Spring Data JPA creates CRUD implementation at runtime automatically.
+public interface BookRepository extends JpaRepository {
- List findByName(String name);
+ // it works if it matches the book field name
+ List findByTitle(String title);
+
+ // Custom Query
+ @Query("SELECT b FROM Book b WHERE b.publishDate > :date")
+ List findByPublishedDateAfter(@Param("date") LocalDate date);
}
diff --git a/spring-data-jpa/src/main/java/com/mkyong/BookService.java b/spring-data-jpa/src/main/java/com/mkyong/BookService.java
new file mode 100644
index 0000000..2fc9b79
--- /dev/null
+++ b/spring-data-jpa/src/main/java/com/mkyong/BookService.java
@@ -0,0 +1,41 @@
+package com.mkyong;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Optional;
+
+@Service
+public class BookService {
+
+ @Autowired
+ private BookRepository bookRepository;
+
+ public List findAll() {
+
+ // convert to List
+ /*Iterable booksIterable = bookRepository.findAll();
+ List booksList = new ArrayList<>();
+ booksIterable.forEach(booksList::add);*/
+ return bookRepository.findAll();
+
+ }
+
+ public Optional findById(Long id) {
+ return bookRepository.findById(id);
+ }
+
+ public Book save(Book book) {
+ return bookRepository.save(book);
+ }
+
+ public void deleteById(Long id) {
+ bookRepository.deleteById(id);
+ }
+
+ public List findByPublishedDateAfter(LocalDate date) {
+ return bookRepository.findByPublishedDateAfter(date);
+ }
+}
diff --git a/spring-data-jpa/src/main/java/com/mkyong/MainApplication.java b/spring-data-jpa/src/main/java/com/mkyong/MainApplication.java
new file mode 100644
index 0000000..6a4f500
--- /dev/null
+++ b/spring-data-jpa/src/main/java/com/mkyong/MainApplication.java
@@ -0,0 +1,87 @@
+package com.mkyong;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Optional;
+
+@SpringBootApplication
+public class MainApplication {
+
+ private static final Logger log = LoggerFactory.getLogger(MainApplication.class);
+
+ public static void main(String[] args) {
+ SpringApplication.run(MainApplication.class, args);
+ }
+
+ // Spring runs CommandLineRunner bean when Spring Boot App starts
+ @Bean
+ public CommandLineRunner demo(BookRepository bookRepository) {
+ return (args) -> {
+
+ Book b1 = new Book("Book A", BigDecimal.valueOf(9.99), LocalDate.of(2023, 8, 31));
+ Book b2 = new Book("Book B", BigDecimal.valueOf(19.99), LocalDate.of(2023, 7, 31));
+ Book b3 = new Book("Book C", BigDecimal.valueOf(29.99), LocalDate.of(2023, 6, 10));
+ Book b4 = new Book("Book D", BigDecimal.valueOf(39.99), LocalDate.of(2023, 5, 5));
+
+ // save a few books, ID auto increase, expect 1, 2, 3, 4
+ bookRepository.save(b1);
+ bookRepository.save(b2);
+ bookRepository.save(b3);
+ bookRepository.save(b4);
+
+ // find all books
+ log.info("findAll(), expect 4 books");
+ log.info("-------------------------------");
+ for (Book book : bookRepository.findAll()) {
+ log.info(book.toString());
+ }
+ log.info("\n");
+
+ // find book by ID
+ Optional optionalBook = bookRepository.findById(1L);
+ optionalBook.ifPresent(obj -> {
+ log.info("Book found with findById(1L):");
+ log.info("--------------------------------");
+ log.info(obj.toString());
+ log.info("\n");
+ });
+
+ // find book by title
+ log.info("Book found with findByTitle('Book B')");
+ log.info("--------------------------------------------");
+ bookRepository.findByTitle("Book C").forEach(b -> {
+ log.info(b.toString());
+ log.info("\n");
+ });
+
+ // find book by published date after
+ log.info("Book found with findByPublishedDateAfter(), after 2023/7/1");
+ log.info("--------------------------------------------");
+ bookRepository.findByPublishedDateAfter(LocalDate.of(2023, 7, 1)).forEach(b -> {
+ log.info(b.toString());
+ log.info("\n");
+ });
+
+ // delete a book
+ bookRepository.deleteById(2L);
+ log.info("Book delete where ID = 2L");
+ log.info("--------------------------------------------");
+ // find all books
+ log.info("findAll() again, expect 3 books");
+ log.info("-------------------------------");
+ for (Book book : bookRepository.findAll()) {
+ log.info(book.toString());
+ }
+ log.info("\n");
+
+ };
+ }
+
+}
\ No newline at end of file
diff --git a/spring-data-jpa/src/main/java/com/mkyong/StartApplication.java b/spring-data-jpa/src/main/java/com/mkyong/StartApplication.java
deleted file mode 100644
index be37d64..0000000
--- a/spring-data-jpa/src/main/java/com/mkyong/StartApplication.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.mkyong;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.CommandLineRunner;
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-
-@SpringBootApplication
-public class StartApplication implements CommandLineRunner {
-
- private static final Logger log = LoggerFactory.getLogger(StartApplication.class);
-
- @Autowired
- private BookRepository repository;
-
- public static void main(String[] args) {
- SpringApplication.run(StartApplication.class, args);
- }
-
- @Override
- public void run(String... args) {
-
- log.info("StartApplication...");
-
- repository.save(new Book("Java"));
- repository.save(new Book("Node"));
- repository.save(new Book("Python"));
-
- System.out.println("\nfindAll()");
- repository.findAll().forEach(x -> System.out.println(x));
-
- System.out.println("\nfindById(1L)");
- repository.findById(1l).ifPresent(x -> System.out.println(x));
-
- System.out.println("\nfindByName('Node')");
- repository.findByName("Node").forEach(x -> System.out.println(x));
-
- }
-
-}
\ No newline at end of file
diff --git a/spring-data-jpa/src/test/java/com/mkyong/BookRepositoryMockTest.java b/spring-data-jpa/src/test/java/com/mkyong/BookRepositoryMockTest.java
new file mode 100644
index 0000000..b6b3f98
--- /dev/null
+++ b/spring-data-jpa/src/test/java/com/mkyong/BookRepositoryMockTest.java
@@ -0,0 +1,84 @@
+package com.mkyong;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.*;
+
+public class BookRepositoryMockTest {
+
+ @InjectMocks
+ private BookService bookService;
+
+ @Mock
+ private BookRepository bookRepository;
+
+ @BeforeEach
+ public void setUp() {
+ MockitoAnnotations.openMocks(this); // init the mocks
+ }
+
+ @Test
+ public void testSave() {
+
+ Book book = new Book();
+ book.setTitle("Hello Java");
+
+ when(bookRepository.save(book)).thenReturn(book);
+
+ Book result = bookService.save(book);
+ assertEquals("Hello Java", result.getTitle());
+
+ verify(bookRepository, times(1)).save(book);
+ }
+
+ @Test
+ public void testFindById() {
+
+ Book book = new Book();
+ book.setId(1L);
+ book.setTitle("Hello Java");
+
+ when(bookRepository.findById(1L)).thenReturn(Optional.of(book));
+
+ Optional result = bookService.findById(1L);
+ assertTrue(result.isPresent());
+ assertEquals("Hello Java", result.get().getTitle());
+
+ verify(bookRepository, times(1)).findById(1L);
+ }
+
+ @Test
+ public void testFindAll() {
+ Book book1 = new Book();
+ book1.setTitle("Book A");
+
+ Book book2 = new Book();
+ book2.setTitle("Book B");
+
+ when(bookRepository.findAll()).thenReturn(List.of(book1, book2));
+
+ List results = bookService.findAll();
+ assertEquals(2, results.size());
+
+ verify(bookRepository, times(1)).findAll();
+ }
+
+ @Test
+ public void testDeleteById() {
+
+ Long bookId = 1L;
+ bookService.deleteById(1L);
+
+ verify(bookRepository, times(1)).deleteById(bookId);
+ }
+
+}
\ No newline at end of file
diff --git a/spring-data-jpa/src/test/java/com/mkyong/BookRepositoryTest.java b/spring-data-jpa/src/test/java/com/mkyong/BookRepositoryTest.java
index d8920ed..0173aaa 100644
--- a/spring-data-jpa/src/test/java/com/mkyong/BookRepositoryTest.java
+++ b/spring-data-jpa/src/test/java/com/mkyong/BookRepositoryTest.java
@@ -1,37 +1,153 @@
package com.mkyong;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
-import org.springframework.test.context.junit4.SpringRunner;
+import java.math.BigDecimal;
+import java.time.LocalDate;
import java.util.List;
+import java.util.Optional;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.assertEquals;
+import static org.junit.jupiter.api.Assertions.*;
-@RunWith(SpringRunner.class)
+// Default, JPA tests data are transactional and roll back at the end of each test.
@DataJpaTest
public class BookRepositoryTest {
+ // Alternative for EntityManager
+ // Optional in this case, we can use bookRepository to do the same stuff
@Autowired
- private TestEntityManager entityManager;
-
+ private TestEntityManager testEM;
@Autowired
- private BookRepository repository;
+ private BookRepository bookRepository;
+
+ // Need clean up if the MainApplication CommandLineRunner bean inserted some data
+ // empty table.
+ @BeforeEach
+ void cleanup() {
+ bookRepository.deleteAll();
+ bookRepository.flush();
+ testEM.clear();
+ }
+
+ @Test
+ public void testSave() {
+
+ Book b1 = new Book("Book A", BigDecimal.valueOf(9.99), LocalDate.of(2023, 8, 31));
+
+ //testEM.persistAndFlush(b1); the same
+ bookRepository.save(b1);
+
+ Long savedBookID = b1.getId();
+
+ Book book = bookRepository.findById(savedBookID).orElseThrow();
+ // Book book = testEM.find(Book.class, savedBookID);
+
+ assertEquals(savedBookID, book.getId());
+ assertEquals("Book A", book.getTitle());
+ assertEquals(BigDecimal.valueOf(9.99), book.getPrice());
+ assertEquals(LocalDate.of(2023, 8, 31), book.getPublishDate());
+
+
+ }
+
+ @Test
+ public void testUpdate() {
+
+ Book b1 = new Book("Book A", BigDecimal.valueOf(9.99), LocalDate.of(2023, 8, 31));
+
+ //testEM.persistAndFlush(b1);
+ bookRepository.save(b1);
+
+ // update price from 9.99 to 19.99
+ b1.setPrice(BigDecimal.valueOf(19.99));
+
+ bookRepository.save(b1);
+
+ List result = bookRepository.findByTitle("Book A");
+
+ assertEquals(1, result.size());
+
+ Book book = result.get(0);
+ assertNotNull(book.getId());
+ assertTrue(book.getId() > 0);
+
+ assertEquals("Book A", book.getTitle());
+ assertEquals(BigDecimal.valueOf(19.99), book.getPrice());
+ assertEquals(LocalDate.of(2023, 8, 31), book.getPublishDate());
+
+
+ }
@Test
- public void testFindByName() {
+ public void testFindByTitle() {
+
+ Book b1 = new Book("Book A", BigDecimal.valueOf(9.99), LocalDate.of(2023, 8, 31));
+ bookRepository.save(b1);
+
+ List result = bookRepository.findByTitle("Book A");
+
+ assertEquals(1, result.size());
+ Book book = result.get(0);
+ assertNotNull(book.getId());
+ assertTrue(book.getId() > 0);
+
+ assertEquals("Book A", book.getTitle());
+ assertEquals(BigDecimal.valueOf(9.99), book.getPrice());
+ assertEquals(LocalDate.of(2023, 8, 31), book.getPublishDate());
+
+
+ }
+
+ @Test
+ public void testFindAll() {
+
+ Book b1 = new Book("Book A", BigDecimal.valueOf(9.99), LocalDate.of(2023, 8, 31));
+ Book b2 = new Book("Book B", BigDecimal.valueOf(19.99), LocalDate.of(2023, 7, 31));
+ Book b3 = new Book("Book C", BigDecimal.valueOf(29.99), LocalDate.of(2023, 6, 10));
+ Book b4 = new Book("Book D", BigDecimal.valueOf(39.99), LocalDate.of(2023, 5, 5));
+
+ bookRepository.saveAll(List.of(b1, b2, b3, b4));
+
+ List result = bookRepository.findAll();
+ assertEquals(4, result.size());
+
+ }
+
+ @Test
+ public void testFindByPublishedDateAfter() {
+
+ Book b1 = new Book("Book A", BigDecimal.valueOf(9.99), LocalDate.of(2023, 8, 31));
+ Book b2 = new Book("Book B", BigDecimal.valueOf(19.99), LocalDate.of(2023, 7, 31));
+ Book b3 = new Book("Book C", BigDecimal.valueOf(29.99), LocalDate.of(2023, 6, 10));
+ Book b4 = new Book("Book D", BigDecimal.valueOf(39.99), LocalDate.of(2023, 5, 5));
+
+ bookRepository.saveAll(List.of(b1, b2, b3, b4));
+
+ List result = bookRepository.findByPublishedDateAfter(LocalDate.of(2023, 7, 1));
+ assertEquals(2, result.size());
+
+ }
+
+ @Test
+ public void testDeleteById() {
+
+ Book b1 = new Book("Book A", BigDecimal.valueOf(9.99), LocalDate.of(2023, 8, 31));
+ bookRepository.save(b1);
+
+ Long savedBookID = b1.getId();
- entityManager.persist(new Book("C++"));
+ // Book book = bookRepository.findById(savedBookID).orElseThrow();
+ // Book book = testEM.find(Book.class, savedBookID);
- List books = repository.findByName("C++");
- assertEquals(1, books.size());
+ bookRepository.deleteById(savedBookID);
- assertThat(books).extracting(Book::getName).containsOnly("C++");
+ Optional result = bookRepository.findById(savedBookID);
+ assertTrue(result.isEmpty());
}
-}
+}
\ No newline at end of file
diff --git a/web-thymeleaf/src/main/java/com/mkyong/controller/WelcomeController.java b/web-thymeleaf/src/main/java/com/mkyong/controller/WelcomeController.java
index 389841e..5fcfa68 100644
--- a/web-thymeleaf/src/main/java/com/mkyong/controller/WelcomeController.java
+++ b/web-thymeleaf/src/main/java/com/mkyong/controller/WelcomeController.java
@@ -1,6 +1,7 @@
package com.mkyong.controller;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@@ -12,6 +13,7 @@
@Controller
public class WelcomeController {
+ TomcatServletWebServerFactory
// inject via application.properties
@Value("${welcome.message}")
private String message;