diff --git a/pom.xml b/pom.xml index fb38cc3db..9bbe1c7d6 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.1 + 3.2.2 org.springframework.samples spring-petclinic @@ -41,122 +41,93 @@ - - org.springframework.boot - spring-boot-starter-actuator + org.springframework.boot + spring-boot-starter-web + + - org.springframework.boot - spring-boot-starter-cache + org.springframework.boot + spring-boot-starter-thymeleaf + + - org.springframework.boot - spring-boot-starter-data-jpa + org.springframework.boot + spring-boot-starter-data-jpa + + - org.springframework.boot - spring-boot-starter-thymeleaf + org.springframework.boot + spring-boot-starter-validation + + - org.springframework.boot - spring-boot-starter-validation + org.springframework.boot + spring-boot-starter-aop + + - org.springframework.boot - spring-boot-starter-webmvc + org.springframework.boot + spring-boot-starter-cache + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + com.h2database + h2 + runtime - javax.cache - cache-api + org.postgresql + postgresql + runtime + + - jakarta.xml.bind - jakarta.xml.bind-api + org.springframework.boot + spring-boot-devtools + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.testcontainers + junit-jupiter + test - com.h2database - h2 - runtime - - - com.github.ben-manes.caffeine - caffeine - runtime - - - com.mysql - mysql-connector-j - runtime - - - org.postgresql - postgresql - runtime - - - org.webjars - webjars-locator-lite - ${webjars-locator.version} - runtime - - - org.webjars.npm - bootstrap - ${webjars-bootstrap.version} - runtime - - - org.webjars.npm - font-awesome - ${webjars-font-awesome.version} - runtime + org.testcontainers + mysql + test - org.springframework.boot - spring-boot-devtools - true + jakarta.xml.bind + jakarta.xml.bind-api - - org.springframework.boot - spring-boot-starter-data-jpa-test - test - - - org.springframework.boot - spring-boot-starter-restclient-test - test - - - org.springframework.boot - spring-boot-starter-webmvc-test - test - - - org.springframework.boot - spring-boot-testcontainers - test - - - org.springframework.boot - spring-boot-docker-compose - test - - - org.testcontainers - testcontainers-junit-jupiter - test - - - org.testcontainers - testcontainers-mysql - test - - + @@ -193,6 +164,30 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + true + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + true + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/src/main/java/org/springframework/samples/petclinic/feature/FeatureFlag.java b/src/main/java/org/springframework/samples/petclinic/feature/FeatureFlag.java new file mode 100644 index 000000000..f09275763 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/feature/FeatureFlag.java @@ -0,0 +1,108 @@ +package org.springframework.samples.petclinic.feature; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +@Entity +@Table(name = "feature_flags", uniqueConstraints = @UniqueConstraint(columnNames = "name")) +public class FeatureFlag { + + public enum Strategy { + + BOOLEAN, PERCENTAGE, WHITELIST, BLACKLIST + + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @NotEmpty + @Column(nullable = false, length = 100) + private String name; + + @Column(nullable = false) + private boolean enabled = false; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Strategy strategy = Strategy.BOOLEAN; + + @Column + private int percentage = 100; + + @Column(length = 2000) + private String userList; + + @Column(length = 500) + private String description; + + // Getters & Setters + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Strategy getStrategy() { + return strategy; + } + + public void setStrategy(Strategy strategy) { + this.strategy = strategy; + } + + public int getPercentage() { + return percentage; + } + + public void setPercentage(int percentage) { + this.percentage = Math.max(0, Math.min(100, percentage)); + } + + public String getUserList() { + return userList; + } + + public void setUserList(String userList) { + this.userList = userList; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + +} diff --git a/src/main/java/org/springframework/samples/petclinic/feature/FeatureFlagController.java b/src/main/java/org/springframework/samples/petclinic/feature/FeatureFlagController.java new file mode 100644 index 000000000..35e962559 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/feature/FeatureFlagController.java @@ -0,0 +1,43 @@ +package org.springframework.samples.petclinic.feature; + +import java.util.List; + +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/feature-flags") +public class FeatureFlagController { + + private final FeatureFlagService service; + + public FeatureFlagController(FeatureFlagService service) { + this.service = service; + } + + @GetMapping + public List getAll() { + return service.findAll(); + } + + @GetMapping("/{id}") + public FeatureFlag get(@PathVariable Integer id) { + return service.findById(id).orElseThrow(); + } + + @PostMapping + public FeatureFlag create(@RequestBody FeatureFlag flag) { + return service.save(flag); + } + + @PutMapping("/{id}") + public FeatureFlag update(@PathVariable Integer id, @RequestBody FeatureFlag flag) { + flag.setId(id); + return service.save(flag); + } + + @DeleteMapping("/{id}") + public void delete(@PathVariable Integer id) { + service.deleteById(id); + } + +} diff --git a/src/main/java/org/springframework/samples/petclinic/feature/FeatureFlagRepository.java b/src/main/java/org/springframework/samples/petclinic/feature/FeatureFlagRepository.java new file mode 100644 index 000000000..e16fd32de --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/feature/FeatureFlagRepository.java @@ -0,0 +1,11 @@ +package org.springframework.samples.petclinic.feature; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface FeatureFlagRepository extends JpaRepository { + + Optional findByName(String name); + +} diff --git a/src/main/java/org/springframework/samples/petclinic/feature/FeatureFlagService.java b/src/main/java/org/springframework/samples/petclinic/feature/FeatureFlagService.java new file mode 100644 index 000000000..32b6544a2 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/feature/FeatureFlagService.java @@ -0,0 +1,62 @@ +package org.springframework.samples.petclinic.feature; + +import java.util.List; +import java.util.Optional; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class FeatureFlagService { + + private final FeatureFlagRepository repository; + + public FeatureFlagService(FeatureFlagRepository repository) { + this.repository = repository; + } + + /* ===================== CRUD ===================== */ + + public List findAll() { + return repository.findAll(); + } + + public Optional findById(Integer id) { + return repository.findById(id); + } + + public FeatureFlag save(FeatureFlag flag) { + return repository.save(flag); + } + + public void deleteById(Integer id) { + repository.deleteById(id); + } + + /* ===================== FEATURE CHECK ===================== */ + + @Transactional(readOnly = true) + public boolean isEnabled(String flagName, String user) { + + Optional flagOpt = repository.findByName(flagName); + + if (flagOpt.isEmpty()) { + return false; + } + + FeatureFlag flag = flagOpt.get(); + + if (!flag.isEnabled()) { + return false; + } + + return switch (flag.getStrategy()) { + case BOOLEAN -> true; + case PERCENTAGE -> Math.abs(user.hashCode()) % 100 < flag.getPercentage(); + case WHITELIST -> flag.getUserList() != null && flag.getUserList().contains(user); + case BLACKLIST -> flag.getUserList() == null || !flag.getUserList().contains(user); + }; + } + +} diff --git a/src/main/java/org/springframework/samples/petclinic/feature/FeatureToggle.java b/src/main/java/org/springframework/samples/petclinic/feature/FeatureToggle.java new file mode 100644 index 000000000..9175c611a --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/feature/FeatureToggle.java @@ -0,0 +1,11 @@ +package org.springframework.samples.petclinic.feature; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface FeatureToggle { + + String value(); + +} diff --git a/src/main/java/org/springframework/samples/petclinic/feature/FeatureToggleAspect.java b/src/main/java/org/springframework/samples/petclinic/feature/FeatureToggleAspect.java new file mode 100644 index 000000000..91b923bee --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/feature/FeatureToggleAspect.java @@ -0,0 +1,29 @@ +package org.springframework.samples.petclinic.feature; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ResponseStatusException; + +@Aspect +@Component +public class FeatureToggleAspect { + + private final FeatureFlagService featureFlagService; + + public FeatureToggleAspect(FeatureFlagService featureFlagService) { + this.featureFlagService = featureFlagService; + } + + @Before("@annotation(featureToggle)") + public void checkFeature(JoinPoint joinPoint, FeatureToggle featureToggle) { + String user = "anonymous"; // can be replaced later + if (!featureFlagService.isEnabled(featureToggle.value(), user)) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, + "Feature " + featureToggle.value() + " is disabled"); + } + } + +} diff --git a/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java b/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java index 199ca3611..e5b327e0d 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java @@ -23,7 +23,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.GetMapping; @@ -37,6 +36,10 @@ import org.springframework.web.servlet.ModelAndView; import jakarta.validation.Valid; import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.samples.petclinic.feature.FeatureFlagService; +import org.springframework.ui.Model; /** * @author Juergen Hoeller @@ -52,8 +55,11 @@ class OwnerController { private final OwnerRepository owners; - public OwnerController(OwnerRepository owners) { + private final FeatureFlagService featureFlagService; + + public OwnerController(OwnerRepository owners, FeatureFlagService featureFlagService) { this.owners = owners; + this.featureFlagService = featureFlagService; } @InitBinder @@ -92,30 +98,24 @@ class OwnerController { } @GetMapping("/owners") - public String processFindForm(@RequestParam(defaultValue = "1") int page, Owner owner, BindingResult result, - Model model) { - // allow parameterless GET request for /owners to return all records - String lastName = owner.getLastName(); - if (lastName == null) { - lastName = ""; // empty string signifies broadest possible search + public String processFindForm(Owner owner, BindingResult result, Model model) { + + if (!featureFlagService.isEnabled("OWNER_SEARCH", "anonymous")) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Owner search feature is disabled"); } - // find owners by last name - Page ownersResults = findPaginatedForOwnersLastName(page, lastName); - if (ownersResults.isEmpty()) { - // no owners found + if (owner.getLastName() == null) { + owner.setLastName(""); + } + + List results = this.owners.findByLastNameContaining(owner.getLastName()); + if (results.isEmpty()) { result.rejectValue("lastName", "notFound", "not found"); return "owners/findOwners"; } - if (ownersResults.getTotalElements() == 1) { - // 1 owner found - owner = ownersResults.iterator().next(); - return "redirect:/owners/" + owner.getId(); - } - - // multiple owners found - return addPaginationModel(page, model, ownersResults); + model.addAttribute("selections", results); + return "owners/ownersList"; } private String addPaginationModel(int page, Model model, Page paginated) { diff --git a/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java b/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java index d2b3dde40..56902e9bb 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java @@ -20,6 +20,7 @@ import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; /** * Repository class for Owner domain objects. All method names are compliant @@ -59,4 +60,6 @@ public interface OwnerRepository extends JpaRepository { */ Optional findById(Integer id); + List findByLastNameContaining(String lastName); + } diff --git a/src/main/java/org/springframework/samples/petclinic/owner/PetController.java b/src/main/java/org/springframework/samples/petclinic/owner/PetController.java index 8398e4f13..7861572c8 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/PetController.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/PetController.java @@ -1,18 +1,3 @@ -/* - * Copyright 2012-2025 the original author or authors. - * - * Licensed 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 - * - * https://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. - */ package org.springframework.samples.petclinic.owner; import java.time.LocalDate; @@ -20,22 +5,18 @@ import java.util.Collection; import java.util.Objects; import java.util.Optional; +import jakarta.validation.Valid; + import org.springframework.stereotype.Controller; -import org.springframework.ui.ModelMap; +import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.InitBinder; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; - -import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import org.springframework.samples.petclinic.feature.FeatureToggle; /** * @author Juergen Hoeller @@ -58,6 +39,10 @@ class PetController { this.types = types; } + /* + * ========================= MODEL ATTRIBUTES ========================= + */ + @ModelAttribute("types") public Collection populatePetTypes() { return this.types.findPetTypes(); @@ -65,10 +50,8 @@ class PetController { @ModelAttribute("owner") public Owner findOwner(@PathVariable("ownerId") int ownerId) { - Optional optionalOwner = this.owners.findById(ownerId); - Owner owner = optionalOwner.orElseThrow(() -> new IllegalArgumentException( - "Owner not found with id: " + ownerId + ". Please ensure the ID is correct ")); - return owner; + return this.owners.findById(ownerId) + .orElseThrow(() -> new IllegalArgumentException("Owner not found with id: " + ownerId)); } @ModelAttribute("pet") @@ -79,12 +62,20 @@ class PetController { return new Pet(); } - Optional optionalOwner = this.owners.findById(ownerId); - Owner owner = optionalOwner.orElseThrow(() -> new IllegalArgumentException( - "Owner not found with id: " + ownerId + ". Please ensure the ID is correct ")); - return owner.getPet(petId); + Owner owner = findOwner(ownerId); + Pet pet = owner.getPet(petId); + + if (pet == null) { + throw new IllegalArgumentException("Pet not found with id: " + petId); + } + + return pet; } + /* + * ========================= BINDERS ========================= + */ + @InitBinder("owner") public void initOwnerBinder(WebDataBinder dataBinder) { dataBinder.setDisallowedFields("id"); @@ -95,35 +86,36 @@ class PetController { dataBinder.setValidator(new PetValidator()); } + /* + * ========================= CREATE PET ========================= + */ + @GetMapping("/pets/new") - public String initCreationForm(Owner owner, ModelMap model) { + public String initCreationForm(Owner owner, Model model) { Pet pet = new Pet(); - owner.addPet(pet); + model.addAttribute("pet", pet); return VIEWS_PETS_CREATE_OR_UPDATE_FORM; } + @FeatureToggle("ADD_PET") @PostMapping("/pets/new") - public String processCreationForm(Owner owner, @Valid Pet pet, BindingResult result, - RedirectAttributes redirectAttributes) { - - if (StringUtils.hasText(pet.getName()) && pet.isNew() && owner.getPet(pet.getName(), true) != null) - result.rejectValue("name", "duplicate", "already exists"); - - LocalDate currentDate = LocalDate.now(); - if (pet.getBirthDate() != null && pet.getBirthDate().isAfter(currentDate)) { - result.rejectValue("birthDate", "typeMismatch.birthDate"); - } + public String processCreationForm(@Valid Pet pet, BindingResult result, Owner owner, Model model) { if (result.hasErrors()) { + model.addAttribute("pet", pet); return VIEWS_PETS_CREATE_OR_UPDATE_FORM; } owner.addPet(pet); this.owners.save(owner); - redirectAttributes.addFlashAttribute("message", "New Pet has been Added"); + return "redirect:/owners/{ownerId}"; } + /* + * ========================= UPDATE PET ========================= + */ + @GetMapping("/pets/{petId}/edit") public String initUpdateForm() { return VIEWS_PETS_CREATE_OR_UPDATE_FORM; @@ -135,7 +127,6 @@ class PetController { String petName = pet.getName(); - // checking if the pet name already exists for the owner if (StringUtils.hasText(petName)) { Pet existingPet = owner.getPet(petName, false); if (existingPet != null && !Objects.equals(existingPet.getId(), pet.getId())) { @@ -143,8 +134,7 @@ class PetController { } } - LocalDate currentDate = LocalDate.now(); - if (pet.getBirthDate() != null && pet.getBirthDate().isAfter(currentDate)) { + if (pet.getBirthDate() != null && pet.getBirthDate().isAfter(LocalDate.now())) { result.rejectValue("birthDate", "typeMismatch.birthDate"); } @@ -153,21 +143,21 @@ class PetController { } updatePetDetails(owner, pet); - redirectAttributes.addFlashAttribute("message", "Pet details has been edited"); + redirectAttributes.addFlashAttribute("message", "Pet details updated"); + return "redirect:/owners/{ownerId}"; } - /** - * Updates the pet details if it exists or adds a new pet to the owner. - * @param owner The owner of the pet - * @param pet The pet with updated details + /* + * ========================= HELPER ========================= */ + private void updatePetDetails(Owner owner, Pet pet) { Integer id = pet.getId(); Assert.state(id != null, "'pet.getId()' must not be null"); + Pet existingPet = owner.getPet(id); if (existingPet != null) { - // Update existing pet's properties existingPet.setName(pet.getName()); existingPet.setBirthDate(pet.getBirthDate()); existingPet.setType(pet.getType()); @@ -175,6 +165,7 @@ class PetController { else { owner.addPet(pet); } + this.owners.save(owner); } diff --git a/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java b/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java index cc3e3ce1a..8910ac95c 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java @@ -29,6 +29,10 @@ import org.springframework.web.bind.annotation.PostMapping; import jakarta.validation.Valid; import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.samples.petclinic.feature.FeatureToggle; +import org.springframework.samples.petclinic.feature.FeatureFlagService; /** * @author Juergen Hoeller @@ -88,16 +92,21 @@ class VisitController { // Spring MVC calls method loadPetWithVisit(...) before processNewVisitForm is // called + @FeatureToggle("ADD_VISIT") @PostMapping("/owners/{ownerId}/pets/{petId}/visits/new") - public String processNewVisitForm(@ModelAttribute Owner owner, @PathVariable int petId, @Valid Visit visit, - BindingResult result, RedirectAttributes redirectAttributes) { + public String processNewVisitForm(@PathVariable("ownerId") int ownerId, @PathVariable("petId") int petId, + @Valid Visit visit, BindingResult result) { + if (result.hasErrors()) { return "pets/createOrUpdateVisitForm"; } + Owner owner = this.owners.findById(ownerId) + .orElseThrow(() -> new IllegalArgumentException("Owner not found with id: " + ownerId)); + owner.addVisit(petId, visit); this.owners.save(owner); - redirectAttributes.addFlashAttribute("message", "Your visit has been booked"); + return "redirect:/owners/{ownerId}"; } diff --git a/src/main/java/org/springframework/samples/petclinic/system/CacheConfiguration.java b/src/main/java/org/springframework/samples/petclinic/system/CacheConfiguration.java deleted file mode 100644 index 13cb74301..000000000 --- a/src/main/java/org/springframework/samples/petclinic/system/CacheConfiguration.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2012-2025 the original author or authors. - * - * Licensed 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 - * - * https://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. - */ - -package org.springframework.samples.petclinic.system; - -import org.springframework.boot.cache.autoconfigure.JCacheManagerCustomizer; -import org.springframework.cache.annotation.EnableCaching; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import javax.cache.configuration.MutableConfiguration; - -/** - * Cache configuration intended for caches providing the JCache API. This configuration - * creates the used cache for the application and enables statistics that become - * accessible via JMX. - */ -@Configuration(proxyBeanMethods = false) -@EnableCaching -class CacheConfiguration { - - @Bean - public JCacheManagerCustomizer petclinicCacheConfigurationCustomizer() { - return cm -> cm.createCache("vets", cacheConfiguration()); - } - - /** - * Create a simple configuration that enable statistics via the JCache programmatic - * configuration API. - *

- * Within the configuration object that is provided by the JCache API standard, there - * is only a very limited set of configuration options. The really relevant - * configuration options (like the size limit) must be set via a configuration - * mechanism that is provided by the selected JCache implementation. - */ - private javax.cache.configuration.Configuration cacheConfiguration() { - return new MutableConfiguration<>().setStatisticsEnabled(true); - } - -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 630c1145a..61e6d7c4f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,26 +1,53 @@ -# database init, supports mysql too +# =============================== +# DATABASE INIT +# =============================== database=h2 -spring.sql.init.schema-locations=classpath*:db/${database}/schema.sql -spring.sql.init.data-locations=classpath*:db/${database}/data.sql +spring.sql.init.mode=always +spring.sql.init.schema-locations=classpath:db/${database}/schema.sql +spring.sql.init.data-locations=classpath:db/${database}/data.sql -# Web -spring.thymeleaf.mode=HTML +# =============================== +# H2 DATABASE +# =============================== +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= -# JPA +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console + +# =============================== +# JPA / HIBERNATE (IMPORTANT) +# =============================== spring.jpa.hibernate.ddl-auto=none spring.jpa.open-in-view=false -spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategySnakeCaseImpl +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -# Internationalization +# ✅ MUST BE SINGLE LINE +spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy + +# =============================== +# THYMELEAF +# =============================== +spring.thymeleaf.mode=HTML + +# =============================== +# I18N +# =============================== spring.messages.basename=messages/messages -# Actuator +# =============================== +# ACTUATOR +# =============================== management.endpoints.web.exposure.include=* -# Logging +# =============================== +# LOGGING +# =============================== logging.level.org.springframework=INFO -# logging.level.org.springframework.web=DEBUG -# logging.level.org.springframework.context.annotation=TRACE -# Maximum time static resources should be cached +# =============================== +# STATIC CACHE +# =============================== spring.web.resources.cache.cachecontrol.max-age=12h diff --git a/src/main/resources/db/h2/data.sql b/src/main/resources/db/h2/data.sql index f232b1361..f120aaf2c 100644 --- a/src/main/resources/db/h2/data.sql +++ b/src/main/resources/db/h2/data.sql @@ -51,3 +51,10 @@ INSERT INTO visits VALUES (default, 7, '2013-01-01', 'rabies shot'); INSERT INTO visits VALUES (default, 8, '2013-01-02', 'rabies shot'); INSERT INTO visits VALUES (default, 8, '2013-01-03', 'neutered'); INSERT INTO visits VALUES (default, 7, '2013-01-04', 'spayed'); + +INSERT INTO feature_flags (name, enabled, strategy, percentage, user_list, description) +VALUES +('ADD_PET', true, 'BOOLEAN', 100, NULL, 'Enable add pet feature'), +('ADD_VISIT', true, 'BOOLEAN', 100, NULL, 'Enable add visit feature'), +('OWNER_SEARCH', true, 'BOOLEAN', 100, NULL, 'Enable owner search'); + diff --git a/src/main/resources/db/h2/schema.sql b/src/main/resources/db/h2/schema.sql index 4a6c322cb..b053b7509 100644 --- a/src/main/resources/db/h2/schema.sql +++ b/src/main/resources/db/h2/schema.sql @@ -62,3 +62,12 @@ CREATE TABLE visits ( ); ALTER TABLE visits ADD CONSTRAINT fk_visits_pets FOREIGN KEY (pet_id) REFERENCES pets (id); CREATE INDEX visits_pet_id ON visits (pet_id); +CREATE TABLE feature_flags ( + id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE, + enabled BOOLEAN NOT NULL, + strategy VARCHAR(50) NOT NULL, + percentage INTEGER, + user_list VARCHAR(2000), + description VARCHAR(255) +); diff --git a/vectonAi2026-02-08 231720.mp4 b/vectonAi2026-02-08 231720.mp4 new file mode 100644 index 000000000..fedb32479 Binary files /dev/null and b/vectonAi2026-02-08 231720.mp4 differ