diff --git a/README.md b/README.md
index 38d9f05ee..6ecf16ec1 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
# Spring Framework 7: Beginner to Guru
+## Spring 7 Rest MVC
This repository is for an example application built in my [Spring Framework 7 - Beginner to Guru](https://www.udemy.com/course/spring-framework-6-beginner-to-guru/?referralCode=2BD0B7B7B6B511D699A9) online course
@@ -9,20 +10,20 @@ As you work through the course, please feel free to fork this repository to your
to source code changes. If you encounter a problem you can compare your code to the lesson code. [See this link for help with compares](https://github.com/springframeworkguru/spring5webapp/wiki#getting-an-error-but-cannot-find-what-is-different-from-lesson-source-code)
## Spring Framework 7: Beginner to Guru Course Wiki
-Got a question about your Spring Framework 7 course? [Checkout these FAQs!](https://github.com/springframeworkguru/spring5webapp/wiki)
+Got a question about your Spring Framework 6 course? [Checkout these FAQs!](https://github.com/springframeworkguru/spring5webapp/wiki)
+## Getting Your Development Environment Setup
### Recommended Versions
-| Recommended | Reference | Notes |
-|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| Oracle Java 25 JDK | [Download](https://www.oracle.com/java/technologies/downloads/#java25) | Java 17 or higher is required for Spring Framework 6+. Java 25 is recommended for the course. |
-| IntelliJ 2024 or Higher | [Download](https://www.jetbrains.com/idea/download/) | Ultimate Edition recommended. Students can get a free 120 trial license [here](https://github.com/springframeworkguru/spring5webapp/wiki/Which-IDE-to-Use%3F#how-do-i-get-the-free-120-day-trial-to-intellij-ultimate) |
-| Maven 3.9.11 or higher | [Download](https://maven.apache.org/download.cgi) | [Installation Instructions](https://maven.apache.org/install.html) |
-| Gradle 8.14 or higher | [Download](https://gradle.org/install/) | |
-| Git 2.39 or higher | [Download](https://git-scm.com/downloads) | |
-| Git GUI Clients | [Downloads](https://git-scm.com/downloads/guis) | Not required. But can be helpful if new to Git. SourceTree is a good option for Mac and Windows users. |
+| Recommended | Reference | Notes |
+|-------------------------|------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Oracle Java 25 JDK | [Download](https://www.oracle.com/java/technologies/downloads/#java21) | Java 17 or higher is required for Spring Framework 6+. Java 25 is recommended for the course. |
+| IntelliJ 2025 or Higher | [Download](https://www.jetbrains.com/idea/download/) | Ultimate Edition recommended. Students can get a free 120 trial license [here](https://github.com/springframeworkguru/spring5webapp/wiki/Which-IDE-to-Use%3F#how-do-i-get-the-free-120-day-trial-to-intellij-ultimate) |
+| Maven 3.9.11 or higher | [Download](https://maven.apache.org/download.cgi) | [Installation Instructions](https://maven.apache.org/install.html) |
+| Gradle 8.14 or higher | [Download](https://gradle.org/install/) | |
+| Git 2.39 or higher | [Download](https://git-scm.com/downloads) | |
+| Git GUI Clients | [Downloads](https://git-scm.com/downloads/guis) | Not required. But can be helpful if new to Git. SourceTree is a good option for Mac and Windows users. |
## All Spring Framework Guru Courses
-
### AI Courses
* [Spring AI: Beginner to Guru](https://www.udemy.com/course/spring-ai-beginner-to-guru/?referralCode=EF8DB31C723FFC8E2751)
* [Vibe Coding FullStake with Spring Boot and React Using Junie](https://www.udemy.com/course/jetbrains-junie/?referralCode=74BE8C5825CB296D2C57)
@@ -32,6 +33,7 @@ Got a question about your Spring Framework 7 course? [Checkout these FAQs!](http
### Spring Framework 6
* [Spring AI: Beginner to Guru](https://www.udemy.com/course/spring-ai-beginner-to-guru/?referralCode=EF8DB31C723FFC8E2751)
+* [Spring AI: Beginner to Guru](https://www.udemy.com/course/spring-ai-beginner-to-guru/?referralCode=EF8DB31C723FFC8E2751)
* [Hibernate and Spring Data JPA: Beginner to Guru](https://www.udemy.com/course/hibernate-and-spring-data-jpa-beginner-to-guru/?referralCode=251C4C865302C7B1BB8F)
* [API First Engineering with Spring Boot](https://www.udemy.com/course/api-first-engineering-with-spring-boot/?referralCode=C6DAEE7338215A2CF276)
* [Introduction to Kafka with Spring Boot](https://www.udemy.com/course/introduction-to-kafka-with-spring-boot/?referralCode=15118530CA63AD1AF16D)
diff --git a/pom.xml b/pom.xml
index 6905f5a7d..19b0f9b4e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,33 +9,30 @@
guru.springframework
- spring-7-webapp
+ spring-7-rest-mvc
0.0.1-SNAPSHOT
- spring-7-webapp
- Spring 7 Web App
-
+ spring-7-rest-mvc
+ Spring 7 Rest MVC
25
-
-
- org.springframework.boot
- spring-boot-starter-data-jpa
-
org.springframework.boot
spring-boot-starter-webmvc
+
- com.h2database
- h2
+ org.springframework.boot
+ spring-boot-devtools
runtime
+ true
+
- org.springframework.boot
- spring-boot-starter-data-jpa-test
- test
+ org.projectlombok
+ lombok
+ true
org.springframework.boot
@@ -46,10 +43,45 @@
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
org.springframework.boot
spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
diff --git a/src/main/java/guru/springframework/spring7webapp/Spring7WebappApplication.java b/src/main/java/guru/springframework/spring7restmvc/Spring7RestMvcApplication.java
similarity index 56%
rename from src/main/java/guru/springframework/spring7webapp/Spring7WebappApplication.java
rename to src/main/java/guru/springframework/spring7restmvc/Spring7RestMvcApplication.java
index b885e0ea3..46846b83d 100644
--- a/src/main/java/guru/springframework/spring7webapp/Spring7WebappApplication.java
+++ b/src/main/java/guru/springframework/spring7restmvc/Spring7RestMvcApplication.java
@@ -1,13 +1,13 @@
-package guru.springframework.spring7webapp;
+package guru.springframework.spring7restmvc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
-public class Spring7WebappApplication {
+public class Spring7RestMvcApplication {
public static void main(String[] args) {
- SpringApplication.run(Spring7WebappApplication.class, args);
+ SpringApplication.run(Spring7RestMvcApplication.class, args);
}
}
diff --git a/src/main/java/guru/springframework/spring7restmvc/controller/BeerController.java b/src/main/java/guru/springframework/spring7restmvc/controller/BeerController.java
new file mode 100644
index 000000000..3f7b23679
--- /dev/null
+++ b/src/main/java/guru/springframework/spring7restmvc/controller/BeerController.java
@@ -0,0 +1,78 @@
+package guru.springframework.spring7restmvc.controller;
+
+import guru.springframework.spring7restmvc.services.BeerService;
+import guru.springframework.spring7restmvc.model.Beer;
+import lombok.AllArgsConstructor;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Created by jt, Spring Framework Guru.
+ */
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+public class BeerController {
+
+ public static final String BEER_PATH = "/api/v1/beer";
+ public static final String BEER_PATH_ID = BEER_PATH + "/{beerId}";
+
+ private final BeerService beerService;
+
+ @PatchMapping(BEER_PATH_ID)
+ public ResponseEntity updateBeerPatchById(@PathVariable("beerId")UUID beerId, @RequestBody Beer beer){
+
+ beerService.patchBeerById(beerId, beer);
+
+ return new ResponseEntity(HttpStatus.NO_CONTENT);
+ }
+
+ @DeleteMapping(BEER_PATH_ID)
+ public ResponseEntity deleteById(@PathVariable("beerId") UUID beerId){
+
+ beerService.deleteById(beerId);
+
+ return new ResponseEntity(HttpStatus.NO_CONTENT);
+ }
+
+ @PutMapping(BEER_PATH_ID)
+ public ResponseEntity updateById(@PathVariable("beerId")UUID beerId, @RequestBody Beer beer){
+
+ beerService.updateBeerById(beerId, beer);
+
+ return new ResponseEntity(HttpStatus.NO_CONTENT);
+ }
+
+ @PostMapping(BEER_PATH)
+ public ResponseEntity handlePost(@RequestBody Beer beer){
+
+ Beer savedBeer = beerService.saveNewBeer(beer);
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.add("Location", BEER_PATH + "/" + savedBeer.getId().toString());
+
+ return new ResponseEntity(headers, HttpStatus.CREATED);
+ }
+
+ @GetMapping(value = BEER_PATH)
+ public List listBeers(){
+ return beerService.listBeers();
+ }
+
+
+ @GetMapping(value = BEER_PATH_ID)
+ public Beer getBeerById(@PathVariable("beerId") UUID beerId){
+
+ log.debug("Get Beer by Id - in controller");
+
+ return beerService.getBeerById(beerId).orElseThrow(NotFoundException::new);
+ }
+
+}
diff --git a/src/main/java/guru/springframework/spring7restmvc/controller/CustomerController.java b/src/main/java/guru/springframework/spring7restmvc/controller/CustomerController.java
new file mode 100644
index 000000000..4b5da43e8
--- /dev/null
+++ b/src/main/java/guru/springframework/spring7restmvc/controller/CustomerController.java
@@ -0,0 +1,72 @@
+package guru.springframework.spring7restmvc.controller;
+
+import guru.springframework.spring7restmvc.model.Customer;
+import guru.springframework.spring7restmvc.services.CustomerService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Created by jt, Spring Framework Guru.
+ */
+
+@RequiredArgsConstructor
+@RestController
+public class CustomerController {
+ public static final String CUSTOMER_PATH = "/api/v1/customer";
+ public static final String CUSTOMER_PATH_ID = CUSTOMER_PATH + "/{customerId}";
+
+ private final CustomerService customerService;
+
+ @PatchMapping(CUSTOMER_PATH_ID)
+ public ResponseEntity patchCustomerById(@PathVariable("customerId") UUID customerId,
+ @RequestBody Customer customer){
+
+ customerService.patchCustomerById(customerId, customer);
+
+ return new ResponseEntity(HttpStatus.NO_CONTENT);
+ }
+
+ @DeleteMapping(CUSTOMER_PATH_ID)
+ public ResponseEntity deleteCustomerById(@PathVariable("customerId") UUID customerId){
+
+ customerService.deleteCustomerById(customerId);
+
+ return new ResponseEntity(HttpStatus.NO_CONTENT);
+ }
+
+ @PutMapping(CUSTOMER_PATH_ID)
+ public ResponseEntity updateCustomerByID(@PathVariable("customerId") UUID customerId,
+ @RequestBody Customer customer){
+
+ customerService.updateCustomerById(customerId, customer);
+
+ return new ResponseEntity(HttpStatus.NO_CONTENT);
+ }
+
+ @PostMapping(CUSTOMER_PATH)
+ public ResponseEntity handlePost(@RequestBody Customer customer){
+ Customer savedCustomer = customerService.saveNewCustomer(customer);
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.add("Location", CUSTOMER_PATH + "/" + savedCustomer.getId().toString());
+
+ return new ResponseEntity(headers, HttpStatus.CREATED);
+ }
+
+ @GetMapping(CUSTOMER_PATH)
+ public List listAllCustomers(){
+ return customerService.getAllCustomers();
+ }
+
+ @GetMapping(value = CUSTOMER_PATH_ID)
+ public Customer getCustomerById(@PathVariable("customerId") UUID id){
+ return customerService.getCustomerById(id).orElseThrow(NotFoundException::new);
+ }
+
+}
diff --git a/src/main/java/guru/springframework/spring7restmvc/controller/ExceptionController.java b/src/main/java/guru/springframework/spring7restmvc/controller/ExceptionController.java
new file mode 100644
index 000000000..07d499f67
--- /dev/null
+++ b/src/main/java/guru/springframework/spring7restmvc/controller/ExceptionController.java
@@ -0,0 +1,16 @@
+package guru.springframework.spring7restmvc.controller;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+
+/**
+ * Created by jt, Spring Framework Guru.
+ */
+@ControllerAdvice
+public class ExceptionController {
+ @ExceptionHandler(NotFoundException.class)
+ public ResponseEntity handleNotFoundException(){
+ return ResponseEntity.notFound().build();
+ }
+}
diff --git a/src/main/java/guru/springframework/spring7restmvc/controller/NotFoundException.java b/src/main/java/guru/springframework/spring7restmvc/controller/NotFoundException.java
new file mode 100644
index 000000000..dc00e05ce
--- /dev/null
+++ b/src/main/java/guru/springframework/spring7restmvc/controller/NotFoundException.java
@@ -0,0 +1,29 @@
+package guru.springframework.spring7restmvc.controller;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+/**
+ * Created by jt, Spring Framework Guru.
+ */
+@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Value Not Found")
+public class NotFoundException extends RuntimeException {
+ public NotFoundException() {
+ }
+
+ public NotFoundException(String message) {
+ super(message);
+ }
+
+ public NotFoundException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public NotFoundException(Throwable cause) {
+ super(cause);
+ }
+
+ public NotFoundException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+}
diff --git a/src/main/java/guru/springframework/spring7restmvc/model/Beer.java b/src/main/java/guru/springframework/spring7restmvc/model/Beer.java
new file mode 100644
index 000000000..0c572a053
--- /dev/null
+++ b/src/main/java/guru/springframework/spring7restmvc/model/Beer.java
@@ -0,0 +1,43 @@
+package guru.springframework.spring7restmvc.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Builder;
+import lombok.Data;
+import tools.jackson.databind.annotation.JsonDeserialize;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+/**
+ * Created by jt, Spring Framework Guru.
+ */
+@JsonDeserialize(builder = Beer.BeerBuilder.class)
+@Builder
+@Data
+public class Beer {
+
+ @JsonProperty("id")
+ private UUID id;
+
+ @JsonProperty("version")
+ private Integer version;
+
+ @JsonProperty("beerName")
+ private String beerName;
+
+ @JsonProperty("beerStyle")
+ private BeerStyle beerStyle;
+
+ @JsonProperty("upc")
+ private String upc;
+
+ @JsonProperty("quantityOnHand")
+ private Integer quantityOnHand;
+
+ @JsonProperty("price")
+ private BigDecimal price;
+
+ private LocalDateTime createdDate;
+ private LocalDateTime updateDate;
+}
diff --git a/src/main/java/guru/springframework/spring7restmvc/model/BeerStyle.java b/src/main/java/guru/springframework/spring7restmvc/model/BeerStyle.java
new file mode 100644
index 000000000..d0f5d858a
--- /dev/null
+++ b/src/main/java/guru/springframework/spring7restmvc/model/BeerStyle.java
@@ -0,0 +1,8 @@
+package guru.springframework.spring7restmvc.model;
+
+/**
+ * Created by jt, Spring Framework Guru.
+ */
+public enum BeerStyle {
+ LAGER, PILSNER, STOUT, GOSE, PORTER, ALE, WHEAT, IPA, PALE_ALE, SAISON
+}
diff --git a/src/main/java/guru/springframework/spring7restmvc/model/Customer.java b/src/main/java/guru/springframework/spring7restmvc/model/Customer.java
new file mode 100644
index 000000000..90a91c17f
--- /dev/null
+++ b/src/main/java/guru/springframework/spring7restmvc/model/Customer.java
@@ -0,0 +1,30 @@
+package guru.springframework.spring7restmvc.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Builder;
+import lombok.Data;
+import tools.jackson.databind.annotation.JsonDeserialize;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+/**
+ * Created by jt, Spring Framework Guru.
+ */
+@JsonDeserialize(builder = Customer.CustomerBuilder.class)
+@Data
+@Builder
+public class Customer {
+
+ @JsonProperty("name")
+ private String name;
+
+ @JsonProperty("id")
+ private UUID id;
+
+ @JsonProperty("version")
+ private Integer version;
+
+ private LocalDateTime createdDate;
+ private LocalDateTime updateDate;
+}
diff --git a/src/main/java/guru/springframework/spring7restmvc/services/BeerService.java b/src/main/java/guru/springframework/spring7restmvc/services/BeerService.java
new file mode 100644
index 000000000..8ba8cd2ff
--- /dev/null
+++ b/src/main/java/guru/springframework/spring7restmvc/services/BeerService.java
@@ -0,0 +1,25 @@
+package guru.springframework.spring7restmvc.services;
+
+import guru.springframework.spring7restmvc.model.Beer;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * Created by jt, Spring Framework Guru.
+ */
+public interface BeerService {
+
+ List listBeers();
+
+ Optional getBeerById(UUID id);
+
+ Beer saveNewBeer(Beer beer);
+
+ void updateBeerById(UUID beerId, Beer beer);
+
+ void deleteById(UUID beerId);
+
+ void patchBeerById(UUID beerId, Beer beer);
+}
diff --git a/src/main/java/guru/springframework/spring7restmvc/services/BeerServiceImpl.java b/src/main/java/guru/springframework/spring7restmvc/services/BeerServiceImpl.java
new file mode 100644
index 000000000..67456d6f6
--- /dev/null
+++ b/src/main/java/guru/springframework/spring7restmvc/services/BeerServiceImpl.java
@@ -0,0 +1,154 @@
+package guru.springframework.spring7restmvc.services;
+
+import guru.springframework.spring7restmvc.model.Beer;
+import guru.springframework.spring7restmvc.model.BeerStyle;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.*;
+
+/**
+ * Created by jt, Spring Framework Guru.
+ */
+@Slf4j
+@Service
+public class BeerServiceImpl implements BeerService {
+
+ private Map beerMap;
+
+ public BeerServiceImpl() {
+ this.beerMap = new HashMap<>();
+
+ Beer beer1 = Beer.builder()
+ .id(UUID.randomUUID())
+ .version(1)
+ .beerName("Galaxy Cat")
+ .beerStyle(BeerStyle.PALE_ALE)
+ .upc("12356")
+ .price(new BigDecimal("12.99"))
+ .quantityOnHand(122)
+ .createdDate(LocalDateTime.now())
+ .updateDate(LocalDateTime.now())
+ .build();
+
+ Beer beer2 = Beer.builder()
+ .id(UUID.randomUUID())
+ .version(1)
+ .beerName("Crank")
+ .beerStyle(BeerStyle.PALE_ALE)
+ .upc("12356222")
+ .price(new BigDecimal("11.99"))
+ .quantityOnHand(392)
+ .createdDate(LocalDateTime.now())
+ .updateDate(LocalDateTime.now())
+ .build();
+
+ Beer beer3 = Beer.builder()
+ .id(UUID.randomUUID())
+ .version(1)
+ .beerName("Sunshine City")
+ .beerStyle(BeerStyle.IPA)
+ .upc("12356")
+ .price(new BigDecimal("13.99"))
+ .quantityOnHand(144)
+ .createdDate(LocalDateTime.now())
+ .updateDate(LocalDateTime.now())
+ .build();
+
+ beerMap.put(beer1.getId(), beer1);
+ beerMap.put(beer2.getId(), beer2);
+ beerMap.put(beer3.getId(), beer3);
+ }
+
+ @Override
+ public void patchBeerById(UUID beerId, Beer beer) {
+ Beer existing = beerMap.get(beerId);
+
+ if (StringUtils.hasText(beer.getBeerName())){
+ existing.setBeerName(beer.getBeerName());
+ }
+
+ if (beer.getBeerStyle() != null) {
+ existing.setBeerStyle(beer.getBeerStyle());
+ }
+
+ if (beer.getPrice() != null) {
+ existing.setPrice(beer.getPrice());
+ }
+
+ if (beer.getQuantityOnHand() != null){
+ existing.setQuantityOnHand(beer.getQuantityOnHand());
+ }
+
+ if (StringUtils.hasText(beer.getUpc())) {
+ existing.setUpc(beer.getUpc());
+ }
+ }
+
+ @Override
+ public void deleteById(UUID beerId) {
+ beerMap.remove(beerId);
+ }
+
+ @Override
+ public void updateBeerById(UUID beerId, Beer beer) {
+ Beer existing = beerMap.get(beerId);
+ existing.setBeerName(beer.getBeerName());
+ existing.setPrice(beer.getPrice());
+ existing.setUpc(beer.getUpc());
+ existing.setQuantityOnHand(beer.getQuantityOnHand());
+ }
+
+ @Override
+ public List listBeers(){
+ return new ArrayList<>(beerMap.values());
+ }
+
+ @Override
+ public Optional getBeerById(UUID id) {
+
+ log.debug("Get Beer by Id - in service. Id: " + id.toString());
+
+ return Optional.of(beerMap.get(id));
+ }
+
+ @Override
+ public Beer saveNewBeer(Beer beer) {
+
+ Beer savedBeer = Beer.builder()
+ .id(UUID.randomUUID())
+ .version(1)
+ .createdDate(LocalDateTime.now())
+ .updateDate(LocalDateTime.now())
+ .beerName(beer.getBeerName())
+ .beerStyle(beer.getBeerStyle())
+ .quantityOnHand(beer.getQuantityOnHand())
+ .upc(beer.getUpc())
+ .price(beer.getPrice())
+ .build();
+
+ beerMap.put(savedBeer.getId(), savedBeer);
+
+ return savedBeer;
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/guru/springframework/spring7restmvc/services/CustomerService.java b/src/main/java/guru/springframework/spring7restmvc/services/CustomerService.java
new file mode 100644
index 000000000..54e67aa18
--- /dev/null
+++ b/src/main/java/guru/springframework/spring7restmvc/services/CustomerService.java
@@ -0,0 +1,25 @@
+package guru.springframework.spring7restmvc.services;
+
+import guru.springframework.spring7restmvc.model.Customer;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * Created by jt, Spring Framework Guru.
+ */
+public interface CustomerService {
+
+ Optional getCustomerById(UUID uuid);
+
+ List getAllCustomers();
+
+ Customer saveNewCustomer(Customer customer);
+
+ void updateCustomerById(UUID customerId, Customer customer);
+
+ void deleteCustomerById(UUID customerId);
+
+ void patchCustomerById(UUID customerId, Customer customer);
+}
diff --git a/src/main/java/guru/springframework/spring7restmvc/services/CustomerServiceImpl.java b/src/main/java/guru/springframework/spring7restmvc/services/CustomerServiceImpl.java
new file mode 100644
index 000000000..23a829fc5
--- /dev/null
+++ b/src/main/java/guru/springframework/spring7restmvc/services/CustomerServiceImpl.java
@@ -0,0 +1,105 @@
+package guru.springframework.spring7restmvc.services;
+
+import guru.springframework.spring7restmvc.model.Customer;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.*;
+
+/**
+ * Created by jt, Spring Framework Guru.
+ */
+@Service
+public class CustomerServiceImpl implements CustomerService {
+
+ private Map customerMap;
+
+ public CustomerServiceImpl() {
+ Customer customer1 = Customer.builder()
+ .id(UUID.randomUUID())
+ .name("Customer 1")
+ .version(1)
+ .createdDate(LocalDateTime.now())
+ .updateDate(LocalDateTime.now())
+ .build();
+
+ Customer customer2 = Customer.builder()
+ .id(UUID.randomUUID())
+ .name("Customer 2")
+ .version(1)
+ .createdDate(LocalDateTime.now())
+ .updateDate(LocalDateTime.now())
+ .build();
+
+ Customer customer3 = Customer.builder()
+ .id(UUID.randomUUID())
+ .name("Customer 3")
+ .version(1)
+ .createdDate(LocalDateTime.now())
+ .updateDate(LocalDateTime.now())
+ .build();
+
+ customerMap = new HashMap<>();
+ customerMap.put(customer1.getId(), customer1);
+ customerMap.put(customer2.getId(), customer2);
+ customerMap.put(customer3.getId(), customer3);
+ }
+
+ @Override
+ public void patchCustomerById(UUID customerId, Customer customer) {
+ Customer existing = customerMap.get(customerId);
+
+ if (StringUtils.hasText(customer.getName())) {
+ existing.setName(customer.getName());
+ }
+ }
+
+ @Override
+ public void deleteCustomerById(UUID customerId) {
+ customerMap.remove(customerId);
+ }
+
+ @Override
+ public void updateCustomerById(UUID customerId, Customer customer) {
+ Customer existing = customerMap.get(customerId);
+ existing.setName(customer.getName());
+ }
+
+ @Override
+ public Customer saveNewCustomer(Customer customer) {
+
+ Customer savedCustomer = Customer.builder()
+ .id(UUID.randomUUID())
+ .version(1)
+ .updateDate(LocalDateTime.now())
+ .createdDate(LocalDateTime.now())
+ .name(customer.getName())
+ .build();
+
+ customerMap.put(savedCustomer.getId(), savedCustomer);
+
+ return savedCustomer;
+ }
+
+ @Override
+ public Optional getCustomerById(UUID uuid) {
+ return Optional.of(customerMap.get(uuid));
+ }
+
+ @Override
+ public List getAllCustomers() {
+ return new ArrayList<>(customerMap.values());
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 8b1378917..d0afd80b5 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1 +1,2 @@
+logging.level.guru.springframework=debug
diff --git a/src/test/java/guru/springframework/spring7webapp/Spring7WebappApplicationTests.java b/src/test/java/guru/springframework/spring7restmvc/Spring7RestMvcApplicationTests.java
similarity index 65%
rename from src/test/java/guru/springframework/spring7webapp/Spring7WebappApplicationTests.java
rename to src/test/java/guru/springframework/spring7restmvc/Spring7RestMvcApplicationTests.java
index 98527369f..55dd2aec5 100644
--- a/src/test/java/guru/springframework/spring7webapp/Spring7WebappApplicationTests.java
+++ b/src/test/java/guru/springframework/spring7restmvc/Spring7RestMvcApplicationTests.java
@@ -1,10 +1,10 @@
-package guru.springframework.spring7webapp;
+package guru.springframework.spring7restmvc;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
-class Spring7WebappApplicationTests {
+class Spring7RestMvcApplicationTests {
@Test
void contextLoads() {
diff --git a/src/test/java/guru/springframework/spring7restmvc/controller/BeerControllerTest.java b/src/test/java/guru/springframework/spring7restmvc/controller/BeerControllerTest.java
new file mode 100644
index 000000000..58a62253e
--- /dev/null
+++ b/src/test/java/guru/springframework/spring7restmvc/controller/BeerControllerTest.java
@@ -0,0 +1,152 @@
+package guru.springframework.spring7restmvc.controller;
+
+import guru.springframework.spring7restmvc.model.Beer;
+import guru.springframework.spring7restmvc.services.BeerService;
+import guru.springframework.spring7restmvc.services.BeerServiceImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.web.servlet.MockMvc;
+import tools.jackson.databind.ObjectMapper;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verify;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@WebMvcTest(BeerController.class)
+@ExtendWith(MockitoExtension.class)
+class BeerControllerTest {
+
+ @Autowired
+ MockMvc mockMvc;
+
+ @Autowired
+ ObjectMapper objectMapper;
+
+ @MockitoBean
+ BeerService beerService;
+
+ BeerServiceImpl beerServiceImpl;
+
+ @Captor
+ ArgumentCaptor uuidArgumentCaptor;
+
+ @Captor
+ ArgumentCaptor beerArgumentCaptor;
+
+ @BeforeEach
+ void setUp() {
+ beerServiceImpl = new BeerServiceImpl();
+ }
+
+ @Test
+ void testPatchBeer() throws Exception {
+ Beer beer = beerServiceImpl.listBeers().get(0);
+
+ Map beerMap = new HashMap<>();
+ beerMap.put("beerName", "New Name");
+
+ mockMvc.perform(patch(BeerController.BEER_PATH_ID, beer.getId())
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(beerMap)))
+ .andExpect(status().isNoContent());
+
+ verify(beerService).patchBeerById(uuidArgumentCaptor.capture(), beerArgumentCaptor.capture());
+
+ assertThat(beer.getId()).isEqualTo(uuidArgumentCaptor.getValue());
+ assertThat(beerMap.get("beerName")).isEqualTo(beerArgumentCaptor.getValue().getBeerName());
+ }
+
+ @Test
+ void testDeleteBeer() throws Exception {
+ Beer beer = beerServiceImpl.listBeers().get(0);
+
+ mockMvc.perform(delete(BeerController.BEER_PATH_ID, beer.getId())
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isNoContent());
+
+ verify(beerService).deleteById(uuidArgumentCaptor.capture());
+
+ assertThat(beer.getId()).isEqualTo(uuidArgumentCaptor.getValue());
+ }
+
+ @Test
+ void testUpdateBeer() throws Exception {
+ Beer beer = beerServiceImpl.listBeers().get(0);
+
+ mockMvc.perform(put(BeerController.BEER_PATH_ID, beer.getId())
+ .accept(MediaType.APPLICATION_JSON)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(beer)))
+ .andExpect(status().isNoContent());
+
+ verify(beerService).updateBeerById(any(UUID.class), any(Beer.class));
+ }
+
+ @Test
+ void testCreateNewBeer() throws Exception {
+ Beer beer = beerServiceImpl.listBeers().get(0);
+ beer.setVersion(null);
+ beer.setId(null);
+
+ given(beerService.saveNewBeer(any(Beer.class))).willReturn(beerServiceImpl.listBeers().get(1));
+
+ mockMvc.perform(post(BeerController.BEER_PATH)
+ .accept(MediaType.APPLICATION_JSON)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(beer)))
+ .andExpect(status().isCreated())
+ .andExpect(header().exists("Location"));
+ }
+
+ @Test
+ void testListBeers() throws Exception {
+ given(beerService.listBeers()).willReturn(beerServiceImpl.listBeers());
+
+ mockMvc.perform(get(BeerController.BEER_PATH)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$.length()", is(3)));
+ }
+
+ @Test
+ void getBeerByIdNotFound() throws Exception {
+
+ given(beerService.getBeerById(any(UUID.class))).willReturn(Optional.empty());
+
+ mockMvc.perform(get(BeerController.BEER_PATH_ID, UUID.randomUUID()))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ void getBeerById() throws Exception {
+ Beer testBeer = beerServiceImpl.listBeers().get(0);
+
+ given(beerService.getBeerById(testBeer.getId())).willReturn(Optional.of(testBeer));
+
+ mockMvc.perform(get(BeerController.BEER_PATH_ID, testBeer.getId())
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$.id", is(testBeer.getId().toString())))
+ .andExpect(jsonPath("$.beerName", is(testBeer.getBeerName())));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/guru/springframework/spring7restmvc/controller/CustomerControllerTest.java b/src/test/java/guru/springframework/spring7restmvc/controller/CustomerControllerTest.java
new file mode 100644
index 000000000..f14f94d6f
--- /dev/null
+++ b/src/test/java/guru/springframework/spring7restmvc/controller/CustomerControllerTest.java
@@ -0,0 +1,164 @@
+package guru.springframework.spring7restmvc.controller;
+
+import guru.springframework.spring7restmvc.model.Customer;
+import guru.springframework.spring7restmvc.services.CustomerService;
+import guru.springframework.spring7restmvc.services.CustomerServiceImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.web.servlet.MockMvc;
+import tools.jackson.databind.ObjectMapper;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verify;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@WebMvcTest(CustomerController.class)
+@ExtendWith(MockitoExtension.class)
+class CustomerControllerTest {
+
+ @MockitoBean
+ CustomerService customerService;
+
+ @Autowired
+ MockMvc mockMvc;
+
+ @Autowired
+ ObjectMapper objectMapper;
+
+ CustomerServiceImpl customerServiceImpl;
+
+ @BeforeEach
+ void setUp() {
+ customerServiceImpl = new CustomerServiceImpl();
+ }
+
+ @Captor
+ ArgumentCaptor uuidArgumentCaptor;
+
+ @Captor
+ ArgumentCaptor customerArgumentCaptor;
+
+ @Test
+ void testPatchCustomer() throws Exception {
+ Customer customer = customerServiceImpl.getAllCustomers().get(0);
+
+ Map customerMap = new HashMap<>();
+ customerMap.put("name", "New Name");
+
+ mockMvc.perform(patch( CustomerController.CUSTOMER_PATH_ID, customer.getId())
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(customerMap)))
+ .andExpect(status().isNoContent());
+
+ verify(customerService).patchCustomerById(uuidArgumentCaptor.capture(),
+ customerArgumentCaptor.capture());
+
+ assertThat(uuidArgumentCaptor.getValue()).isEqualTo(customer.getId());
+ assertThat(customerArgumentCaptor.getValue().getName())
+ .isEqualTo(customerMap.get("name"));
+ }
+
+ @Test
+ void testDeleteCustomer() throws Exception {
+ Customer customer = customerServiceImpl.getAllCustomers().get(0);
+
+ mockMvc.perform(delete(CustomerController.CUSTOMER_PATH_ID, customer.getId())
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isNoContent());
+
+ verify(customerService).deleteCustomerById(uuidArgumentCaptor.capture());
+
+ assertThat(customer.getId()).isEqualTo(uuidArgumentCaptor.getValue());
+ }
+
+ @Test
+ void testUpdateCustomer() throws Exception {
+ Customer customer = customerServiceImpl.getAllCustomers().get(0);
+
+ mockMvc.perform(put(CustomerController.CUSTOMER_PATH_ID, customer.getId())
+ .content(objectMapper.writeValueAsString(customer))
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isNoContent());
+
+ verify(customerService).updateCustomerById(uuidArgumentCaptor.capture(), any(Customer.class));
+
+ assertThat(customer.getId()).isEqualTo(uuidArgumentCaptor.getValue());
+ }
+
+ @Test
+ void testCreateCustomer() throws Exception {
+ Customer customer = customerServiceImpl.getAllCustomers().get(0);
+ customer.setId(null);
+ customer.setVersion(null);
+
+ given(customerService.saveNewCustomer(any(Customer.class)))
+ .willReturn(customerServiceImpl.getAllCustomers().get(1));
+
+ mockMvc.perform(post(CustomerController.CUSTOMER_PATH).contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(customer)))
+ .andExpect(status().isCreated())
+ .andExpect(header().exists("Location"));
+ }
+
+ @Test
+ void listAllCustomers() throws Exception {
+ given(customerService.getAllCustomers()).willReturn(customerServiceImpl.getAllCustomers());
+
+ mockMvc.perform(get(CustomerController.CUSTOMER_PATH)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$.length()", is(3)));
+ }
+
+ @Test
+ void getCustomerByIdNotFound() throws Exception {
+
+ given(customerService.getCustomerById(any(UUID.class))).willReturn(Optional.empty());
+
+ mockMvc.perform(get(CustomerController.CUSTOMER_PATH_ID, UUID.randomUUID()))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ void getCustomerById() throws Exception {
+ Customer customer = customerServiceImpl.getAllCustomers().get(0);
+
+ given(customerService.getCustomerById(customer.getId())).willReturn(Optional.of(customer));
+
+ mockMvc.perform(get(CustomerController.CUSTOMER_PATH_ID, customer.getId())
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$.name", is(customer.getName())));
+ }
+}
+
+
+
+
+
+
+
+
+
+