diff --git a/jpa/boot-data-envers/pom.xml b/jpa/boot-data-envers/pom.xml
index 69ac4006f..0344a52b8 100644
--- a/jpa/boot-data-envers/pom.xml
+++ b/jpa/boot-data-envers/pom.xml
@@ -7,7 +7,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.2.4
+ 3.3.0-M3
com.example.envers
@@ -223,11 +223,11 @@
- 2.39.0
+ 2.40.0
-
-
-
+
+
+
@@ -336,4 +336,24 @@
+
+
+ spring-milestones
+ Spring Milestones
+ https://repo.spring.io/milestone
+
+ false
+
+
+
+
+
+ spring-milestones
+ Spring Milestones
+ https://repo.spring.io/milestone
+
+ false
+
+
+
diff --git a/jpa/boot-data-envers/src/main/java/com/example/envers/config/WebMvcConfig.java b/jpa/boot-data-envers/src/main/java/com/example/envers/config/WebMvcConfig.java
index 524db2a3f..38963390a 100644
--- a/jpa/boot-data-envers/src/main/java/com/example/envers/config/WebMvcConfig.java
+++ b/jpa/boot-data-envers/src/main/java/com/example/envers/config/WebMvcConfig.java
@@ -8,14 +8,16 @@
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
+
private final ApplicationProperties properties;
@Override
public void addCorsMappings(CorsRegistry registry) {
- registry.addMapping(properties.getCors().getPathPattern())
- .allowedMethods(properties.getCors().getAllowedMethods())
- .allowedHeaders(properties.getCors().getAllowedHeaders())
- .allowedOriginPatterns(properties.getCors().getAllowedOriginPatterns())
- .allowCredentials(properties.getCors().isAllowCredentials());
+ ApplicationProperties.Cors propertiesCors = properties.getCors();
+ registry.addMapping(propertiesCors.getPathPattern())
+ .allowedMethods(propertiesCors.getAllowedMethods())
+ .allowedHeaders(propertiesCors.getAllowedHeaders())
+ .allowedOriginPatterns(propertiesCors.getAllowedOriginPatterns())
+ .allowCredentials(propertiesCors.isAllowCredentials());
}
}
diff --git a/jpa/boot-data-envers/src/main/java/com/example/envers/entities/Customer.java b/jpa/boot-data-envers/src/main/java/com/example/envers/entities/Customer.java
index 573f4ce22..8c3c5bac1 100644
--- a/jpa/boot-data-envers/src/main/java/com/example/envers/entities/Customer.java
+++ b/jpa/boot-data-envers/src/main/java/com/example/envers/entities/Customer.java
@@ -11,7 +11,6 @@
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
-import lombok.Setter;
import lombok.ToString;
import org.hibernate.Hibernate;
import org.hibernate.envers.Audited;
@@ -19,7 +18,6 @@
@Entity
@Table(name = "customers")
@Getter
-@Setter
@NoArgsConstructor
@AllArgsConstructor
@Audited
@@ -36,7 +34,27 @@ public class Customer {
private String address;
@Version
- Short version = 0;
+ Short version;
+
+ public Customer setId(Long id) {
+ this.id = id;
+ return this;
+ }
+
+ public Customer setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Customer setAddress(String address) {
+ this.address = address;
+ return this;
+ }
+
+ public Customer setVersion(Short version) {
+ this.version = version;
+ return this;
+ }
@Override
public boolean equals(Object o) {
diff --git a/jpa/boot-data-envers/src/main/java/com/example/envers/services/CustomerService.java b/jpa/boot-data-envers/src/main/java/com/example/envers/services/CustomerService.java
index b6e6f5529..1b15f654b 100644
--- a/jpa/boot-data-envers/src/main/java/com/example/envers/services/CustomerService.java
+++ b/jpa/boot-data-envers/src/main/java/com/example/envers/services/CustomerService.java
@@ -18,6 +18,8 @@
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
+import org.springframework.data.history.Revision;
+import org.springframework.data.history.RevisionSort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -65,6 +67,35 @@ public List findCustomerRevisionsById(Long id) {
return revisionDtoCF.stream().map(CompletableFuture::join).toList();
}
+ public PagedResult findCustomerHistoryById(Long id, Pageable pageRequest) {
+ if (customerRepository.findById(id).isEmpty()) {
+ throw new CustomerNotFoundException(id);
+ }
+
+ RevisionSort sortDir;
+ Optional direction =
+ pageRequest.getSort().stream().map(Sort.Order::getDirection).findFirst();
+ if (direction.isPresent()) {
+ if (Sort.Direction.ASC.name().equalsIgnoreCase(direction.get().name())) {
+ sortDir = RevisionSort.asc();
+ } else {
+ sortDir = RevisionSort.desc();
+ }
+ } else {
+ sortDir = RevisionSort.desc();
+ }
+
+ Pageable pageable = PageRequest.of(pageRequest.getPageNumber(), pageRequest.getPageSize(), sortDir);
+ Page> customerRevisions = customerRepository.findRevisions(id, pageable);
+ List> revisionCFResultList = customerRevisions.getContent().stream()
+ .map(customerRevision -> CompletableFuture.supplyAsync(
+ () -> customerRevisionToRevisionDTOMapper.convert(customerRevision)))
+ .toList();
+ List revisionResultList =
+ revisionCFResultList.stream().map(CompletableFuture::join).toList();
+ return new PagedResult<>(customerRevisions, revisionResultList);
+ }
+
@Transactional
public CustomerResponse saveCustomer(CustomerRequest customerRequest) {
Customer customer = customerMapper.toEntity(customerRequest);
diff --git a/jpa/boot-data-envers/src/main/java/com/example/envers/web/controllers/CustomerController.java b/jpa/boot-data-envers/src/main/java/com/example/envers/web/controllers/CustomerController.java
index 8dcfefb01..b07ffbc4e 100644
--- a/jpa/boot-data-envers/src/main/java/com/example/envers/web/controllers/CustomerController.java
+++ b/jpa/boot-data-envers/src/main/java/com/example/envers/web/controllers/CustomerController.java
@@ -8,10 +8,12 @@
import com.example.envers.model.response.RevisionResult;
import com.example.envers.services.CustomerService;
import com.example.envers.utils.AppConstants;
+import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import java.net.URI;
import java.util.List;
import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
@@ -55,6 +57,12 @@ public ResponseEntity> findCustomerRevisionsById(@PathVaria
return ResponseEntity.ok(customerService.findCustomerRevisionsById(id));
}
+ @Operation(summary = "Get Customer Version History By Id")
+ @GetMapping("/{id}/history")
+ public PagedResult getCustomerHistoryById(@PathVariable("id") Long id, Pageable pageable) {
+ return customerService.findCustomerHistoryById(id, pageable);
+ }
+
@PostMapping
public ResponseEntity createCustomer(@RequestBody @Validated CustomerRequest customerRequest) {
CustomerResponse response = customerService.saveCustomer(customerRequest);
diff --git a/jpa/boot-data-envers/src/test/java/com/example/envers/ApplicationIntegrationTest.java b/jpa/boot-data-envers/src/test/java/com/example/envers/ApplicationIntegrationTest.java
index 3acbe750f..e4e25aa6c 100644
--- a/jpa/boot-data-envers/src/test/java/com/example/envers/ApplicationIntegrationTest.java
+++ b/jpa/boot-data-envers/src/test/java/com/example/envers/ApplicationIntegrationTest.java
@@ -19,29 +19,27 @@ class ApplicationIntegrationTest extends AbstractIntegrationTest {
@Test
void initialRevision() {
- Customer cust = new Customer();
- cust.setName("junit");
- cust.setAddress("address");
- Customer customer = customerRepository.save(cust);
+ Customer cust = new Customer().setName("junitName").setAddress("junitAddress");
+ Customer savedCustomer = customerRepository.save(cust);
- Revisions revisions = customerRepository.findRevisions(customer.getId());
+ Revisions revisions = customerRepository.findRevisions(savedCustomer.getId());
assertThat(revisions).isNotEmpty().allSatisfy(revision -> assertThat(revision.getEntity())
.extracting(Customer::getId, Customer::getName, Customer::getVersion)
- .containsExactly(customer.getId(), customer.getName(), customer.getVersion()));
+ .containsExactly(savedCustomer.getId(), savedCustomer.getName(), null));
}
@Test
void updateIncreasesRevisionNumber() {
- var cust = new Customer();
- cust.setName("text");
+ Customer cust = new Customer().setName("text");
Customer customer = customerRepository.save(cust);
customer.setName("If");
- customerRepository.save(customer);
+ Customer updatedCustomer = customerRepository.save(customer);
- Optional> revision = customerRepository.findLastChangeRevision(customer.getId());
+ Optional> revision =
+ customerRepository.findLastChangeRevision(updatedCustomer.getId());
assertThat(revision)
.isPresent()
@@ -55,8 +53,7 @@ void updateIncreasesRevisionNumber() {
@Test
void deletedItemWillHaveRevisionRetained() {
- var cust = new Customer();
- cust.setName("junit");
+ Customer cust = new Customer().setName("junitName").setAddress("junitAddress");
Customer customer = customerRepository.save(cust);
customerRepository.delete(customer);
@@ -71,11 +68,11 @@ void deletedItemWillHaveRevisionRetained() {
Revision finalRevision = iterator.next();
assertThat(initialRevision).satisfies(rev -> assertThat(rev.getEntity())
- .extracting(Customer::getId, Customer::getName, Customer::getVersion)
- .containsExactly(customer.getId(), customer.getName(), customer.getVersion()));
+ .extracting(Customer::getId, Customer::getName, Customer::getAddress, Customer::getVersion)
+ .containsExactly(customer.getId(), customer.getName(), customer.getAddress(), null));
assertThat(finalRevision).satisfies(rev -> assertThat(rev.getEntity())
- .extracting(Customer::getId, Customer::getName, Customer::getVersion)
- .containsExactly(customer.getId(), null, (short) 0));
+ .extracting(Customer::getId, Customer::getName, Customer::getAddress, Customer::getVersion)
+ .containsExactly(customer.getId(), null, null, null));
}
}
diff --git a/jpa/boot-data-envers/src/test/java/com/example/envers/web/controllers/CustomerControllerIT.java b/jpa/boot-data-envers/src/test/java/com/example/envers/web/controllers/CustomerControllerIT.java
index a455c2b6d..ef996c6f4 100644
--- a/jpa/boot-data-envers/src/test/java/com/example/envers/web/controllers/CustomerControllerIT.java
+++ b/jpa/boot-data-envers/src/test/java/com/example/envers/web/controllers/CustomerControllerIT.java
@@ -37,9 +37,9 @@ void setUp() {
customerRepository.deleteAllInBatch();
customerList = new ArrayList<>();
- customerList.add(new Customer(null, "First Customer", "Junit Address", (short) 0));
- customerList.add(new Customer(null, "Second Customer", "Junit Address", (short) 0));
- customerList.add(new Customer(null, "Third Customer", "Junit Address", (short) 0));
+ customerList.add(new Customer().setName("First Customer").setAddress("Junit Address"));
+ customerList.add(new Customer().setName("First Customer").setAddress("Junit Address"));
+ customerList.add(new Customer().setName("First Customer").setAddress("Junit Address"));
customerList = customerRepository.saveAll(customerList);
}
@@ -88,6 +88,44 @@ void shouldFindCustomerRevisionsById() throws Exception {
.andExpect(jsonPath("$[0].revisionNumber", notNullValue()))
.andExpect(jsonPath("$[0].revisionType", is("INSERT")));
}
+
+ @Test
+ void shouldFindCustomerHistoryById() throws Exception {
+ Customer customer = customerList.getFirst();
+ customerRepository.saveAndFlush(customer.setAddress("newAddress"));
+ Long customerId = customer.getId();
+
+ mockMvc.perform(get("/api/customers/{id}/history?page=0&size=10&sort=revision_Number,desc", customerId))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.data.size()", is(2)))
+ .andExpect(jsonPath("$.totalElements", is(2)))
+ .andExpect(jsonPath("$.pageNumber", is(1)))
+ .andExpect(jsonPath("$.totalPages", is(1)))
+ .andExpect(jsonPath("$.isFirst", is(true)))
+ .andExpect(jsonPath("$.isLast", is(true)))
+ .andExpect(jsonPath("$.hasNext", is(false)))
+ .andExpect(jsonPath("$.hasPrevious", is(false)))
+ .andExpect(jsonPath("$.data[0].entity.id", is(customer.getId()), Long.class))
+ .andExpect(jsonPath("$.data[0].entity.name", is(customer.getName())))
+ .andExpect(jsonPath("$.data[0].entity.address", is(customer.getAddress())))
+ .andExpect(jsonPath("$.data[0].revisionNumber", notNullValue()))
+ .andExpect(jsonPath("$.data[0].revisionType", is("UPDATE")))
+ .andExpect(jsonPath("$.data[0].revisionInstant", notNullValue()));
+ }
+
+ @Test
+ void cantFindCustomerHistoryById() throws Exception {
+ Customer customer = customerList.getFirst();
+ Long customerId = customer.getId() + 10_000;
+
+ mockMvc.perform(get("/api/customers/{id}/history?page=0&size=10&sort=revision_Number,asc", customerId))
+ .andExpect(status().isNotFound())
+ .andExpect(header().string(HttpHeaders.CONTENT_TYPE, is(MediaType.APPLICATION_PROBLEM_JSON_VALUE)))
+ .andExpect(jsonPath("$.type", is("http://api.boot-data-envers.com/errors/not-found")))
+ .andExpect(jsonPath("$.title", is("Not Found")))
+ .andExpect(jsonPath("$.status", is(404)))
+ .andExpect(jsonPath("$.detail").value("Customer with Id '%d' not found".formatted(customerId)));
+ }
}
@Test
@@ -113,7 +151,7 @@ void shouldReturn400WhenCreateNewCustomerWithoutName() throws Exception {
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(customerRequest)))
.andExpect(status().isBadRequest())
- .andExpect(header().string("Content-Type", is("application/problem+json")))
+ .andExpect(header().string(HttpHeaders.CONTENT_TYPE, is(MediaType.APPLICATION_PROBLEM_JSON_VALUE)))
.andExpect(jsonPath("$.type", is("about:blank")))
.andExpect(jsonPath("$.title", is("Constraint Violation")))
.andExpect(jsonPath("$.status", is(400)))
diff --git a/jpa/boot-data-envers/src/test/java/com/example/envers/web/controllers/CustomerControllerTest.java b/jpa/boot-data-envers/src/test/java/com/example/envers/web/controllers/CustomerControllerTest.java
index 423093317..1e08e961b 100644
--- a/jpa/boot-data-envers/src/test/java/com/example/envers/web/controllers/CustomerControllerTest.java
+++ b/jpa/boot-data-envers/src/test/java/com/example/envers/web/controllers/CustomerControllerTest.java
@@ -107,7 +107,7 @@ void shouldReturn404WhenFetchingNonExistingCustomer() throws Exception {
mockMvc.perform(get("/api/customers/{id}", customerId))
.andExpect(status().isNotFound())
- .andExpect(header().string("Content-Type", is(MediaType.APPLICATION_PROBLEM_JSON_VALUE)))
+ .andExpect(header().string(HttpHeaders.CONTENT_TYPE, is(MediaType.APPLICATION_PROBLEM_JSON_VALUE)))
.andExpect(jsonPath("$.type", is("http://api.boot-data-envers.com/errors/not-found")))
.andExpect(jsonPath("$.title", is("Not Found")))
.andExpect(jsonPath("$.status", is(404)))
@@ -143,7 +143,7 @@ void shouldReturn400WhenCreateNewCustomerWithoutName() throws Exception {
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(customerRequest)))
.andExpect(status().isBadRequest())
- .andExpect(header().string("Content-Type", is("application/problem+json")))
+ .andExpect(header().string(HttpHeaders.CONTENT_TYPE, is("application/problem+json")))
.andExpect(jsonPath("$.type", is("about:blank")))
.andExpect(jsonPath("$.title", is("Constraint Violation")))
.andExpect(jsonPath("$.status", is(400)))
@@ -187,7 +187,7 @@ void shouldReturn404WhenUpdatingNonExistingCustomer() throws Exception {
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(customerRequest)))
.andExpect(status().isNotFound())
- .andExpect(header().string("Content-Type", is(MediaType.APPLICATION_PROBLEM_JSON_VALUE)))
+ .andExpect(header().string(HttpHeaders.CONTENT_TYPE, is(MediaType.APPLICATION_PROBLEM_JSON_VALUE)))
.andExpect(jsonPath("$.type", is("http://api.boot-data-envers.com/errors/not-found")))
.andExpect(jsonPath("$.title", is("Not Found")))
.andExpect(jsonPath("$.status", is(404)))
@@ -217,7 +217,7 @@ void shouldReturn404WhenDeletingNonExistingCustomer() throws Exception {
given(customerService.findCustomerById(customerId)).willReturn(Optional.empty());
mockMvc.perform(delete("/api/customers/{id}", customerId))
- .andExpect(header().string("Content-Type", is(MediaType.APPLICATION_PROBLEM_JSON_VALUE)))
+ .andExpect(header().string(HttpHeaders.CONTENT_TYPE, is(MediaType.APPLICATION_PROBLEM_JSON_VALUE)))
.andExpect(jsonPath("$.type", is("http://api.boot-data-envers.com/errors/not-found")))
.andExpect(jsonPath("$.title", is("Not Found")))
.andExpect(jsonPath("$.status", is(404)))