diff --git a/persistence-modules/pom.xml b/persistence-modules/pom.xml index 6609e619bf6c..79c485d55b96 100644 --- a/persistence-modules/pom.xml +++ b/persistence-modules/pom.xml @@ -139,6 +139,7 @@ hibernate-annotations-2 hibernate-reactive my-sql + spring-data-envers diff --git a/persistence-modules/spring-data-envers/pom.xml b/persistence-modules/spring-data-envers/pom.xml new file mode 100644 index 000000000000..3fa1141bd26a --- /dev/null +++ b/persistence-modules/spring-data-envers/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + spring-data-envers + spring-data-envers + + + com.baeldung + parent-boot-3 + 0.0.1-SNAPSHOT + ../../parent-boot-3 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.data + spring-data-envers + + + com.h2database + h2 + + + org.projectlombok + lombok + provided + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.xmlunit + xmlunit-core + + + + + + + diff --git a/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/CustomAuditApplication.java b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/CustomAuditApplication.java new file mode 100644 index 000000000000..150f0dcf8477 --- /dev/null +++ b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/CustomAuditApplication.java @@ -0,0 +1,7 @@ +package com.baeldung.envers.customrevision; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CustomAuditApplication { +} diff --git a/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/domain/CustomRevisionEntity.java b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/domain/CustomRevisionEntity.java new file mode 100644 index 000000000000..f5652d5aa4af --- /dev/null +++ b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/domain/CustomRevisionEntity.java @@ -0,0 +1,21 @@ +package com.baeldung.envers.customrevision.domain; + +import com.baeldung.envers.customrevision.service.CustomRevisionListener; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import lombok.*; +import org.hibernate.envers.DefaultRevisionEntity; +import org.hibernate.envers.RevisionEntity; + +@Entity +@RevisionEntity +@EqualsAndHashCode(callSuper = true) +@Getter +@Setter +@NoArgsConstructor +@EntityListeners(CustomRevisionListener.class) +public class CustomRevisionEntity extends DefaultRevisionEntity { + + private String remoteHost; + private String remoteUser; +} diff --git a/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/domain/Owner.java b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/domain/Owner.java new file mode 100644 index 000000000000..07816a7b7475 --- /dev/null +++ b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/domain/Owner.java @@ -0,0 +1,30 @@ +package com.baeldung.envers.customrevision.domain; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.envers.Audited; + +import java.util.List; + +@Entity +@Audited +@Data +public class Owner { + @Id + @GeneratedValue + private Long id; + + @Column(nullable = false) + private String name; + + @OneToMany(fetch = FetchType.EAGER) + private List pets; + + public static Owner forName(String name ) { + + var owner = new Owner(); + owner.setName(name); + return owner; + } + +} diff --git a/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/domain/Pet.java b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/domain/Pet.java new file mode 100644 index 000000000000..4f61fc07ea9c --- /dev/null +++ b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/domain/Pet.java @@ -0,0 +1,31 @@ +package com.baeldung.envers.customrevision.domain; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.envers.Audited; + +import java.time.LocalDate; +import java.util.UUID; + +@Entity +@Audited +@Data +public class Pet { + @Id + @GeneratedValue + private Long id; + + @Column(unique = true, nullable = false) + private UUID uuid; + + private String name; + + // A null ownes implies the pet is available for adoption + @ManyToOne + @JoinColumn(name = "owner_id", nullable = true) + private Owner owner; + + @ManyToOne + @JoinColumn(name = "species_id") + private Species species; +} diff --git a/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/domain/PetHistoryEntry.java b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/domain/PetHistoryEntry.java new file mode 100644 index 000000000000..27703ec3d439 --- /dev/null +++ b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/domain/PetHistoryEntry.java @@ -0,0 +1,18 @@ +package com.baeldung.envers.customrevision.domain; + +import org.springframework.data.history.RevisionMetadata; + +import java.time.Instant; +import java.util.UUID; + +public record PetHistoryEntry( + Instant eventDate, + RevisionMetadata.RevisionType revisionType, + UUID petUuid, + String speciesName, + String petName, + String ownerName, + String remoteHost, + String remoteUser +) { +} diff --git a/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/domain/PetLogInfo.java b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/domain/PetLogInfo.java new file mode 100644 index 000000000000..8b5f3a7bfbc1 --- /dev/null +++ b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/domain/PetLogInfo.java @@ -0,0 +1,16 @@ +package com.baeldung.envers.customrevision.domain; + +import org.springframework.data.history.RevisionMetadata; + +import java.time.Instant; +import java.util.UUID; + +public record PetLogInfo( + Instant eventDate, + RevisionMetadata.RevisionType revisionType, + UUID petUuid, + String speciesName, + String petName, + String ownerName +) { +} diff --git a/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/domain/Species.java b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/domain/Species.java new file mode 100644 index 000000000000..bfcadb7765a1 --- /dev/null +++ b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/domain/Species.java @@ -0,0 +1,25 @@ +package com.baeldung.envers.customrevision.domain; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.envers.Audited; + +@Entity +@Audited +@Data +@NoArgsConstructor +public class Species { + @Id + @GeneratedValue + private Long id; + + @Column(unique = true) + private String name; + + public static Species forName(String name) { + var s = new Species(); + s.setName(name); + return s; + } +} diff --git a/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/repository/OwnerRepository.java b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/repository/OwnerRepository.java new file mode 100644 index 000000000000..8ad3952d4d40 --- /dev/null +++ b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/repository/OwnerRepository.java @@ -0,0 +1,10 @@ +package com.baeldung.envers.customrevision.repository; + +import com.baeldung.envers.customrevision.domain.Owner; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface OwnerRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/repository/PetRepository.java b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/repository/PetRepository.java new file mode 100644 index 000000000000..87761a82702e --- /dev/null +++ b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/repository/PetRepository.java @@ -0,0 +1,15 @@ +package com.baeldung.envers.customrevision.repository; + +import com.baeldung.envers.customrevision.domain.Pet; +import com.baeldung.envers.customrevision.domain.Species; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.history.RevisionRepository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface PetRepository extends JpaRepository, RevisionRepository { + List findPetsByOwnerNullAndSpecies(Species species); + Optional findPetByUuid(UUID uuid); +} diff --git a/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/repository/SpeciesRepository.java b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/repository/SpeciesRepository.java new file mode 100644 index 000000000000..9ba9b8093569 --- /dev/null +++ b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/repository/SpeciesRepository.java @@ -0,0 +1,11 @@ +package com.baeldung.envers.customrevision.repository; + +import com.baeldung.envers.customrevision.domain.Species; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface SpeciesRepository extends JpaRepository { + + Optional findByName(String name); +} diff --git a/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/service/AdoptionService.java b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/service/AdoptionService.java new file mode 100644 index 000000000000..40364d395c57 --- /dev/null +++ b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/service/AdoptionService.java @@ -0,0 +1,99 @@ +package com.baeldung.envers.customrevision.service; + + +import com.baeldung.envers.customrevision.domain.CustomRevisionEntity; +import com.baeldung.envers.customrevision.domain.Pet; +import com.baeldung.envers.customrevision.domain.PetHistoryEntry; +import com.baeldung.envers.customrevision.repository.OwnerRepository; +import com.baeldung.envers.customrevision.repository.PetRepository; +import com.baeldung.envers.customrevision.repository.SpeciesRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class AdoptionService { + + private final OwnerRepository ownersRepo; + private final PetRepository petsRepo; + private final SpeciesRepository speciesRepo; + + /** + * Register a new pet for adoption. Initially, this pet will have no name or owner. + * @param speciesName + * @return The assigned UUID for this pet + */ + public UUID registerForAdoption( String speciesName) { + + var species = speciesRepo.findByName(speciesName) + .orElseThrow(() -> new IllegalArgumentException("Unknown Species: " + speciesName)); + + var pet = new Pet(); + pet.setSpecies(species); + pet.setUuid(UUID.randomUUID()); + petsRepo.save(pet); + return pet.getUuid(); + } + + + public List findPetsForAdoption(String speciesName) { + var species = speciesRepo.findByName(speciesName) + .orElseThrow(() -> new IllegalArgumentException("Unknown Species: " + speciesName)); + + return petsRepo.findPetsByOwnerNullAndSpecies(species); + } + + public Pet adoptPet(UUID petUuid, String ownerName, String petName) { + + var newOwner = ownersRepo.findByName(ownerName) + .orElseThrow(() -> new IllegalArgumentException("Unknown owner")); + + var pet = petsRepo.findPetByUuid(petUuid) + .orElseThrow(() -> new IllegalArgumentException("Unknown pet")); + + if ( pet.getOwner() != null) { + throw new IllegalArgumentException("Pet already adopted"); + } + + pet.setOwner(newOwner); + pet.setName(petName); + petsRepo.save(pet); + return pet; + } + + public Pet returnPet(UUID petUuid) { + var pet = petsRepo.findPetByUuid(petUuid) + .orElseThrow(() -> new IllegalArgumentException("Unknown pet")); + + pet.setOwner(null); + petsRepo.save(pet); + return pet; + } + + + public List listPetHistory(UUID petUuid) { + + var pet = petsRepo.findPetByUuid(petUuid) + .orElseThrow(() -> new IllegalArgumentException("No pet with UUID '" + petUuid + "' found")); + + return petsRepo.findRevisions(pet.getId()).stream() + .map(r -> { + CustomRevisionEntity rev = r.getMetadata().getDelegate(); + return new PetHistoryEntry(r.getRequiredRevisionInstant(), + r.getMetadata().getRevisionType(), + r.getEntity().getUuid(), + r.getEntity().getSpecies().getName(), + r.getEntity().getName(), + r.getEntity().getOwner() != null ? r.getEntity().getOwner().getName() : null, + rev.getRemoteHost(), + rev.getRemoteUser()); + }) + .toList(); + + } + + +} diff --git a/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/service/CustomRevisionListener.java b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/service/CustomRevisionListener.java new file mode 100644 index 000000000000..190161cf4724 --- /dev/null +++ b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/service/CustomRevisionListener.java @@ -0,0 +1,29 @@ +package com.baeldung.envers.customrevision.service; + +import com.baeldung.envers.customrevision.domain.CustomRevisionEntity; +import jakarta.persistence.PrePersist; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; +import java.util.function.Supplier; + +@Component +@RequiredArgsConstructor +public class CustomRevisionListener { + + private final Supplier> requestInfoSupplier; + + @PrePersist + private void onPersist(CustomRevisionEntity entity) { + + var info = requestInfoSupplier.get(); + if ( info.isEmpty()) { + return; + } + + entity.setRemoteHost(info.get().remoteHost()); + entity.setRemoteUser(info.get().remoteUser()); + + } +} diff --git a/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/service/RequestInfo.java b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/service/RequestInfo.java new file mode 100644 index 000000000000..95bd9c1e0959 --- /dev/null +++ b/persistence-modules/spring-data-envers/src/main/java/com/baeldung/envers/customrevision/service/RequestInfo.java @@ -0,0 +1,4 @@ +package com.baeldung.envers.customrevision.service; + +public record RequestInfo(String remoteHost, String remoteUser) { +} diff --git a/persistence-modules/spring-data-envers/src/main/resources/application.properties b/persistence-modules/spring-data-envers/src/main/resources/application.properties new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/persistence-modules/spring-data-envers/src/test/java/com/baeldung/envers/customrevision/CustomAuditApplicationUnitTest.java b/persistence-modules/spring-data-envers/src/test/java/com/baeldung/envers/customrevision/CustomAuditApplicationUnitTest.java new file mode 100644 index 000000000000..55ccf0e92ea4 --- /dev/null +++ b/persistence-modules/spring-data-envers/src/test/java/com/baeldung/envers/customrevision/CustomAuditApplicationUnitTest.java @@ -0,0 +1,89 @@ +package com.baeldung.envers.customrevision; + +import com.baeldung.envers.customrevision.domain.Owner; +import com.baeldung.envers.customrevision.domain.PetHistoryEntry; +import com.baeldung.envers.customrevision.domain.Species; +import com.baeldung.envers.customrevision.repository.OwnerRepository; +import com.baeldung.envers.customrevision.repository.SpeciesRepository; +import com.baeldung.envers.customrevision.service.AdoptionService; +import com.baeldung.envers.customrevision.service.RequestInfo; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +import static org.junit.jupiter.api.Assertions.*; + +@Slf4j +@SpringBootTest +public class CustomAuditApplicationUnitTest { + + @Autowired + AdoptionService adoptionService; + + @Test + void whenRegisterForAdoption_thenSuccess() { + + var generatedUuid = adoptionService.registerForAdoption("dog"); + assertNotNull(generatedUuid); + + var doggies = adoptionService.findPetsForAdoption("dog"); + assertNotNull(doggies); + assertEquals(1,doggies.size(), "should have one dog for adoption"); + assertEquals("dog", doggies.get(0).getSpecies().getName()); + assertNull(doggies.get(0).getOwner()); + } + + @Test + void whenAdoptPet_thenSuccess() { + + var petUuid = adoptionService.registerForAdoption("cat"); + var kitty = adoptionService.adoptPet(petUuid,"adam", "kitty"); + + List kittyHistory = adoptionService.listPetHistory(kitty.getUuid()); + assertNotNull(kittyHistory); + assertTrue(kittyHistory.size() > 0 , "kitty should have a history"); + for (PetHistoryEntry e : kittyHistory) { + log.info("Entry: {}", e); + } + + } + + @TestConfiguration + static class TestConfig { + + @Bean + Supplier> requestInfoSupplier() { + + return () -> Optional.of(new RequestInfo("example.com", "thomas")); + + } + + @Bean + CommandLineRunner populateShelter(SpeciesRepository speciesRepo, OwnerRepository ownerRepo) { + + return (args) -> { + // Add species + speciesRepo.saveAll(List.of( + Species.forName("dog"), + Species.forName("cat"), + Species.forName("chinchilla"))); + + // Add Owners + ownerRepo.saveAll( List.of( + Owner.forName("adam"), + Owner.forName("mary"), + Owner.forName("phil") + )); + }; + } + + } +} \ No newline at end of file diff --git a/persistence-modules/spring-data-envers/src/test/resources/application.properties b/persistence-modules/spring-data-envers/src/test/resources/application.properties new file mode 100644 index 000000000000..bd3d32502a3f --- /dev/null +++ b/persistence-modules/spring-data-envers/src/test/resources/application.properties @@ -0,0 +1,2 @@ +spring.jpa.hibernate.ddl-auto=create +logging.level.sql=TRACE \ No newline at end of file