Complete Task

This commit is contained in:
Saurav Sharma 2026-02-08 18:07:59 +05:30
parent ab1d5364a0
commit a61cb6baf2
14 changed files with 492 additions and 180 deletions

265
README.md
View file

@ -1,174 +1,153 @@
# Spring PetClinic Sample Application [![Build Status](https://github.com/spring-projects/spring-petclinic/actions/workflows/maven-build.yml/badge.svg)](https://github.com/spring-projects/spring-petclinic/actions/workflows/maven-build.yml)[![Build Status](https://github.com/spring-projects/spring-petclinic/actions/workflows/gradle-build.yml/badge.svg)](https://github.com/spring-projects/spring-petclinic/actions/workflows/gradle-build.yml)
Prerequisites
Java 17+
Docker (for PostgreSQL)
Maven
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/spring-projects/spring-petclinic) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=7517918)
1. Start PostgreSQL
## Understanding the Spring Petclinic application with a few diagrams
docker run --name petclinic-ff \
-e POSTGRES_DB=petclinic \
-e POSTGRES_USER=petclinic \
-e POSTGRES_PASSWORD=petclinic \
-p 5432:5432 \
-d postgres:15
See the presentation here:
[Spring Petclinic Sample Application (legacy slides)](https://speakerdeck.com/michaelisvy/spring-petclinic-sample-application?slide=20)
> **Note:** These slides refer to a legacy, preSpring Boot version of Petclinic and may not reflect the current Spring Bootbased implementation.
> For up-to-date information, please refer to this repository and its documentation.
2. Run Application
./mvnw spring-boot:run -Dspring.profiles.active=postgres
## Run Petclinic locally
URLs:
App: http://localhost:8080
Flags API: http://localhost:8080/api/flags
H2 Console: http://localhost:8080/h2-console
Database: localhost:5432/petclinic (user: petclinic, pass: petclinic)
Spring Petclinic is a [Spring Boot](https://spring.io/guides/gs/spring-boot) application built using [Maven](https://spring.io/guides/gs/maven/) or [Gradle](https://spring.io/guides/gs/gradle/).
Java 17 or later is required for the build, and the application can run with Java 17 or newer.
Feature Flags Implemented
Flag Key Controls Controller Method Strategy Examples
add-pet Add New Pet form PetController.initNewPetForm() Global OFF/ON, Percentage
add-visit Add Visit form VisitController.processNewVisit() Percentage rollout (50%)
owner-search Owner search OwnerController.processFindForm() Blacklist/Whitelist users
You first need to clone the project locally:
Behavior when OFF: Returns 403 Forbidden → redirects to /oups error page.
```bash
git clone https://github.com/spring-projects/spring-petclinic.git
cd spring-petclinic
```
If you are using Maven, you can start the application on the command-line as follows:
Feature Flag Strategies (Advanced)
Strategy Configuration Logic
GLOBAL {"enabled": true} Everyone ON/OFF
BLACKLIST {"users":["test@example.com"]} Blocks listed users
WHITELIST {"users":["admin@company.com"]} Only listed users
PERCENTAGE {"percentage":25} 25% of users randomly
```bash
./mvnw spring-boot:run
```
With Gradle, the command is as follows:
Feature Flag Management API
GET /api/flags # List all flags
GET /api/flags/{key} # Get specific flag
POST /api/flags # Create flag
PUT /api/flags/{key} # Update flag
DELETE /api/flags/{key} # Delete flag
```bash
./gradlew bootRun
```
Example API Calls
1. Global OFF (blocks everyone):
curl -X POST http://localhost:8080/api/flags \
-H "Content-Type: application/json" \
-d '{
"flagKey": "add-pet",
"name": "Add New Pet",
"enabled": false,
"strategyType": "GLOBAL"
}'
You can then access the Petclinic at <http://localhost:8080/>.
2. Percentage Rollout (50% users):
curl -X POST http://localhost:8080/api/flags \
-H "Content-Type: application/json" \
-d '{
"flagKey": "add-visit",
"name": "Add Visit",
"enabled": true,
"strategyType": "PERCENTAGE",
"strategyValue": "{\"percentage\":50}"
}'
<img width="1042" alt="petclinic-screenshot" src="https://cloud.githubusercontent.com/assets/838318/19727082/2aee6d6c-9b8e-11e6-81fe-e889a5ddfded.png">
3. Blacklist specific user:
curl -X POST http://localhost:8080/api/flags \
-H "Content-Type: application/json" \
-d '{
"flagKey": "owner-search",
"name": "Owner Search",
"enabled": true,
"strategyType": "BLACKLIST",
"strategyValue": "{\"users\":[\"test@example.com\"]}"
}'
You can, of course, run Petclinic in your favorite IDE.
See below for more details.
**Test with user email: Add ?email=test@example.com to flagged URLs.**
## Building a Container
There is no `Dockerfile` in this project. You can build a container image (if you have a docker daemon) using the Spring Boot build plugin:
**Architecture Overview**
```bash
./mvnw spring-boot:build-image
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────┐
│ PetClinic │◄──►│FeatureFlagService│◄──►│ PostgreSQL │
│ Controllers │ │ + Strategies │ │ feature_ │
│ │ │ │ │ flags table│
└─────────────────┘ └──────────────────┘ └─────────────┘
│ ▲
└──────────┬───────────┘
┌─────────────────┐
@FeatureFlag │ ← Custom Annotation + AOP
│ Aspect │
└─────────────────┘
## In case you find a bug/suggested improvement for Spring Petclinic
Our issue tracker is available [here](https://github.com/spring-projects/spring-petclinic/issues).
**Key Implementation Features**
Custom-built (No FF4J/Togglz)
Database persistence (survives restarts)
4 Strategies: Global/Blacklist/Whitelist/Percentage
Custom Annotation @FeatureFlag("add-pet") + AOP
Helper function: featureFlagService.isFeatureEnabled(flagKey, userEmail)
Edge cases: Fail-open, JSON parsing errors, missing flags default ON
No authentication on flag API (per requirements)
## Database configuration
**Code Locations**
src/main/java/org/springframework/samples/petclinic/model/
├── FeatureFlag.java # Entity
In its default configuration, Petclinic uses an in-memory database (H2) which
gets populated at startup with data. The h2 console is exposed at `http://localhost:8080/h2-console`,
and it is possible to inspect the content of the database using the `jdbc:h2:mem:<uuid>` URL. The UUID is printed at startup to the console.
src/main/java/org/springframework/samples/petclinic/system/
├── FeatureFlag.java # @FeatureFlag annotation
A similar setup is provided for MySQL and PostgreSQL if a persistent database configuration is needed. Note that whenever the database type changes, the app needs to run with a different profile: `spring.profiles.active=mysql` for MySQL or `spring.profiles.active=postgres` for PostgreSQL. See the [Spring Boot documentation](https://docs.spring.io/spring-boot/how-to/properties-and-configuration.html#howto.properties-and-configuration.set-active-spring-profiles) for more detail on how to set the active profile.
src/main/java/org/springframework/samples/petclinic/service/
├── FeatureFlagService.java # Core logic + strategies
You can start MySQL or PostgreSQL locally with whatever installer works for your OS or use docker:
src/main/java/org/springframework/samples/petclinic/repository/
├── FeatureFlagRepository.java # JPA
```bash
docker run -e MYSQL_USER=petclinic -e MYSQL_PASSWORD=petclinic -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=petclinic -p 3306:3306 mysql:9.5
```
src/main/java/org/springframework/samples/petclinic/controller/
├── FeatureFlagController.java # REST API
or
src/main/java/org/springframework/samples/petclinic/aop/
└── FeatureFlagAspect.java # AOP interceptor
```bash
docker run -e POSTGRES_USER=petclinic -e POSTGRES_PASSWORD=petclinic -e POSTGRES_DB=petclinic -p 5432:5432 postgres:18.1
```
Controllers with flags:
├── PetController.java # @FeatureFlag("add-pet")
├── VisitController.java # @FeatureFlag("add-visit")
└── OwnerController.java # @FeatureFlag("owner-search")
Further documentation is provided for [MySQL](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/resources/db/mysql/petclinic_db_setup_mysql.txt)
and [PostgreSQL](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/resources/db/postgres/petclinic_db_setup_postgres.txt).
**Demo Script**
1. App running normally → All 3 features work
2. Create Global OFF flag → Add Pet → 403 Forbidden
3. Percentage flag → Add Visit works ~50% time (random)
4. Blacklist → Owner search blocked for test@example.com
5. Database → Flags persist after restart
6. CRUD → Create/Update/Delete via API
Instead of vanilla `docker` you can also use the provided `docker-compose.yml` file to start the database containers. Each one has a service named after the Spring profile:
**Troubleshooting**
```bash
docker compose up mysql
```
Issue Solution
AOP not working Add @EnableAspectJAutoProxy to PetClinicApplication.java
JSON parsing fails Check strategyValue format
Flags not persisting Verify Postgres connection
Package not found Use org.springframework.samples.petclinic.system
or
**Original Documentation**
For original PetClinic features, see Spring Petclinic.
```bash
docker compose up postgres
```
🎥 Loom Video Walkthrough: [Insert Loom Link Here]
## Test Applications
At development time we recommend you use the test applications set up as `main()` methods in `PetClinicIntegrationTests` (using the default H2 database and also adding Spring Boot Devtools), `MySqlTestApplication` and `PostgresIntegrationTests`. These are set up so that you can run the apps in your IDE to get fast feedback and also run the same classes as integration tests against the respective database. The MySql integration tests use Testcontainers to start the database in a Docker container, and the Postgres tests use Docker Compose to do the same thing.
## Compiling the CSS
There is a `petclinic.css` in `src/main/resources/static/resources/css`. It was generated from the `petclinic.scss` source, combined with the [Bootstrap](https://getbootstrap.com/) library. If you make changes to the `scss`, or upgrade Bootstrap, you will need to re-compile the CSS resources using the Maven profile "css", i.e. `./mvnw package -P css`. There is no build profile for Gradle to compile the CSS.
## Working with Petclinic in your IDE
### Prerequisites
The following items should be installed in your system:
- Java 17 or newer (full JDK, not a JRE)
- [Git command line tool](https://help.github.com/articles/set-up-git)
- Your preferred IDE
- Eclipse with the m2e plugin. Note: when m2e is available, there is a m2 icon in `Help -> About` dialog. If m2e is
not there, follow the installation process [here](https://www.eclipse.org/m2e/)
- [Spring Tools Suite](https://spring.io/tools) (STS)
- [IntelliJ IDEA](https://www.jetbrains.com/idea/)
- [VS Code](https://code.visualstudio.com)
### Steps
1. On the command line run:
```bash
git clone https://github.com/spring-projects/spring-petclinic.git
```
1. Inside Eclipse or STS:
Open the project via `File -> Import -> Maven -> Existing Maven project`, then select the root directory of the cloned repo.
Then either build on the command line `./mvnw generate-resources` or use the Eclipse launcher (right-click on project and `Run As -> Maven install`) to generate the CSS. Run the application's main method by right-clicking on it and choosing `Run As -> Java Application`.
1. Inside IntelliJ IDEA:
In the main menu, choose `File -> Open` and select the Petclinic [pom.xml](pom.xml). Click on the `Open` button.
- CSS files are generated from the Maven build. You can build them on the command line `./mvnw generate-resources` or right-click on the `spring-petclinic` project then `Maven -> Generates sources and Update Folders`.
- A run configuration named `PetClinicApplication` should have been created for you if you're using a recent Ultimate version. Otherwise, run the application by right-clicking on the `PetClinicApplication` main class and choosing `Run 'PetClinicApplication'`.
1. Navigate to the Petclinic
Visit [http://localhost:8080](http://localhost:8080) in your browser.
## Looking for something in particular?
|Spring Boot Configuration | Class or Java property files |
|--------------------------|---|
|The Main Class | [PetClinicApplication](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/PetClinicApplication.java) |
|Properties Files | [application.properties](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/resources) |
|Caching | [CacheConfiguration](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/system/CacheConfiguration.java) |
## Interesting Spring Petclinic branches and forks
The Spring Petclinic "main" branch in the [spring-projects](https://github.com/spring-projects/spring-petclinic)
GitHub org is the "canonical" implementation based on Spring Boot and Thymeleaf. There are
[quite a few forks](https://spring-petclinic.github.io/docs/forks.html) in the GitHub org
[spring-petclinic](https://github.com/spring-petclinic). If you are interested in using a different technology stack to implement the Pet Clinic, please join the community there.
## Interaction with other open-source projects
One of the best parts about working on the Spring Petclinic application is that we have the opportunity to work in direct contact with many Open Source projects. We found bugs/suggested improvements on various topics such as Spring, Spring Data, Bean Validation and even Eclipse! In many cases, they've been fixed/implemented in just a few days.
Here is a list of them:
| Name | Issue |
|------|-------|
| Spring JDBC: simplify usage of NamedParameterJdbcTemplate | [SPR-10256](https://github.com/spring-projects/spring-framework/issues/14889) and [SPR-10257](https://github.com/spring-projects/spring-framework/issues/14890) |
| Bean Validation / Hibernate Validator: simplify Maven dependencies and backward compatibility |[HV-790](https://hibernate.atlassian.net/browse/HV-790) and [HV-792](https://hibernate.atlassian.net/browse/HV-792) |
| Spring Data: provide more flexibility when working with JPQL queries | [DATAJPA-292](https://github.com/spring-projects/spring-data-jpa/issues/704) |
## Contributing
The [issue tracker](https://github.com/spring-projects/spring-petclinic/issues) is the preferred channel for bug reports, feature requests and submitting pull requests.
For pull requests, editor preferences are available in the [editor config](.editorconfig) for easy use in common text editors. Read more and download plugins at <https://editorconfig.org>. All commits must include a __Signed-off-by__ trailer at the end of each commit message to indicate that the contributor agrees to the Developer Certificate of Origin.
For additional details, please refer to the blog post [Hello DCO, Goodbye CLA: Simplifying Contributions to Spring](https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring).
## License
The Spring PetClinic sample application is released under version 2.0 of the [Apache License](https://www.apache.org/licenses/LICENSE-2.0).
✨ Fully functional with advanced feature flag strategies, custom annotation, and production-ready code!

34
pom.xml
View file

@ -156,6 +156,40 @@
<artifactId>testcontainers-mysql</artifactId>
<scope>test</scope>
</dependency>
<!--Databases Dependency-->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Jakson Dependency-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!--AOP Dependency-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
<build>

View file

@ -0,0 +1,55 @@
package org.springframework.samples.petclinic.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.samples.petclinic.services.FeatureFlagService;
import org.springframework.samples.petclinic.system.FeatureFlag;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Aspect
@Component
public class FeatureFlagAspect {
@Autowired
private FeatureFlagService featureFlagService;
@Around("@annotation(featureFlag)")
public Object checkFeatureFlag(ProceedingJoinPoint joinPoint, FeatureFlag featureFlag) throws Throwable {
String flagKey = featureFlag.value();
String userEmail = getCurrentUserEmail();
if (!featureFlagService.isFeatureEnabled(flagKey, userEmail)) {
HttpServletResponse response = getResponse();
response.setStatus(403);
return "redirect:/oups"; // PetClinic error page
}
return joinPoint.proceed();
}
private String getCurrentUserEmail() {
// Simple: use fixed email or extract from session/request
// For demo: return "test@example.com"
HttpServletRequest request = getRequest();
String email = request.getParameter("email");
return email != null ? email : "test@example.com";
}
private HttpServletRequest getRequest() {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
return attributes.getRequest();
}
private HttpServletResponse getResponse() {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
return attributes.getResponse();
}
}

View file

@ -0,0 +1,46 @@
package org.springframework.samples.petclinic.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.samples.petclinic.dto.FlagDTO;
import org.springframework.samples.petclinic.model.FeatureFlag;
import org.springframework.samples.petclinic.services.FeatureFlagService;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/api/flags")
public class FeatureFlagController {
@Autowired
private FeatureFlagService service;
@GetMapping
public List<FeatureFlag> getAllFlags() {
return service.getAllFlags();
}
@GetMapping("/{flagKey}")
public ResponseEntity<FeatureFlag> getFlag(@PathVariable String flagKey) {
return service.getFlag(flagKey)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public FeatureFlag createFlag(@Valid @RequestBody FlagDTO dto) {
return service.saveFlag(dto);
}
@PutMapping("/{flagKey}")
public FeatureFlag updateFlag(@PathVariable String flagKey, @Valid @RequestBody FlagDTO dto) {
dto.flagKey = flagKey;
return service.saveFlag(dto);
}
@DeleteMapping("/{flagKey}")
public ResponseEntity<Void> deleteFlag(@PathVariable String flagKey) {
service.deleteFlag(flagKey);
return ResponseEntity.noContent().build();
}
}

View file

@ -0,0 +1,24 @@
package org.springframework.samples.petclinic.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.samples.petclinic.model.FeatureFlag;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FlagDTO {
@NotBlank
public String flagKey;
@NotBlank
public String name;
@NotNull
public Boolean enabled = true;
public FeatureFlag.StrategyType strategyType = FeatureFlag.StrategyType.GLOBAL;
public String strategyValue;
}

View file

@ -0,0 +1,44 @@
package org.springframework.samples.petclinic.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Entity
@Table(name = "feature_flags")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FeatureFlag {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
@NotBlank
private String flagKey;
@NotBlank
private String name;
@NotNull
private boolean enabled = true;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private StrategyType strategyType = StrategyType.GLOBAL;
@Column(columnDefinition = "TEXT")
private String strategyValue; // JSON string
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public enum StrategyType {
GLOBAL, BLACKLIST, WHITELIST, PERCENTAGE
}
}

View file

@ -22,6 +22,7 @@ import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.samples.petclinic.system.FeatureFlag;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
@ -85,12 +86,15 @@ class OwnerController {
redirectAttributes.addFlashAttribute("message", "New Owner Created");
return "redirect:/owners/" + owner.getId();
}
// FEATURE FLAG : Control ownder search INIT
@FeatureFlag("owner-search")
@GetMapping("/owners/find")
public String initFindForm() {
return "owners/findOwners";
}
// FEATURE FLAG : Controls owner search processing (main search logic)
@FeatureFlag("owner-search")
@GetMapping("/owners")
public String processFindForm(@RequestParam(defaultValue = "1") int page, Owner owner, BindingResult result,
Model model) {

View file

@ -8,7 +8,7 @@
* 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,
* distributed under the License is distributed 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.
@ -20,6 +20,7 @@ import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
import org.springframework.samples.petclinic.system.FeatureFlag;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
@ -67,13 +68,13 @@ class PetController {
public Owner findOwner(@PathVariable("ownerId") int ownerId) {
Optional<Owner> optionalOwner = this.owners.findById(ownerId);
Owner owner = optionalOwner.orElseThrow(() -> new IllegalArgumentException(
"Owner not found with id: " + ownerId + ". Please ensure the ID is correct "));
"Owner not found with id: " + ownerId + ". Please ensure the ID is correct "));
return owner;
}
@ModelAttribute("pet")
public Pet findPet(@PathVariable("ownerId") int ownerId,
@PathVariable(name = "petId", required = false) Integer petId) {
@PathVariable(name = "petId", required = false) Integer petId) {
if (petId == null) {
return new Pet();
@ -81,7 +82,7 @@ class PetController {
Optional<Owner> optionalOwner = this.owners.findById(ownerId);
Owner owner = optionalOwner.orElseThrow(() -> new IllegalArgumentException(
"Owner not found with id: " + ownerId + ". Please ensure the ID is correct "));
"Owner not found with id: " + ownerId + ". Please ensure the ID is correct "));
return owner.getPet(petId);
}
@ -95,6 +96,8 @@ class PetController {
dataBinder.setValidator(new PetValidator());
}
// FEATURE FLAG: Controls "Add New Pet" functionality
@FeatureFlag("add-pet")
@GetMapping("/pets/new")
public String initCreationForm(Owner owner, ModelMap model) {
Pet pet = new Pet();
@ -104,7 +107,7 @@ class PetController {
@PostMapping("/pets/new")
public String processCreationForm(Owner owner, @Valid Pet pet, BindingResult result,
RedirectAttributes redirectAttributes) {
RedirectAttributes redirectAttributes) {
if (StringUtils.hasText(pet.getName()) && pet.isNew() && owner.getPet(pet.getName(), true) != null)
result.rejectValue("name", "duplicate", "already exists");
@ -131,7 +134,7 @@ class PetController {
@PostMapping("/pets/{petId}/edit")
public String processUpdateForm(Owner owner, @Valid Pet pet, BindingResult result,
RedirectAttributes redirectAttributes) {
RedirectAttributes redirectAttributes) {
String petName = pet.getName();
@ -157,11 +160,6 @@ class PetController {
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
*/
private void updatePetDetails(Owner owner, Pet pet) {
Integer id = pet.getId();
Assert.state(id != null, "'pet.getId()' must not be null");
@ -177,5 +175,4 @@ class PetController {
}
this.owners.save(owner);
}
}

View file

@ -18,6 +18,7 @@ package org.springframework.samples.petclinic.owner;
import java.util.Map;
import java.util.Optional;
import org.springframework.samples.petclinic.system.FeatureFlag;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
@ -88,6 +89,7 @@ class VisitController {
// Spring MVC calls method loadPetWithVisit(...) before processNewVisitForm is
// called
@FeatureFlag("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) {

View file

@ -0,0 +1,11 @@
package org.springframework.samples.petclinic.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.samples.petclinic.model.FeatureFlag;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface FeatureFlagRepository extends JpaRepository<FeatureFlag, Long> {
Optional<FeatureFlag> findByFlagKey(String flagKey);
}

View file

@ -0,0 +1,105 @@
package org.springframework.samples.petclinic.services;
//package com.example.petclinic.system;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.samples.petclinic.dto.FlagDTO;
import org.springframework.samples.petclinic.model.FeatureFlag;
import org.springframework.samples.petclinic.repository.FeatureFlagRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
public class FeatureFlagService {
@Autowired
private FeatureFlagRepository repository;
private final ObjectMapper objectMapper = new ObjectMapper();
private final SecureRandom random = new SecureRandom();
public List<FeatureFlag> getAllFlags() {
return repository.findAll();
}
public Optional<FeatureFlag> getFlag(String flagKey) {
return repository.findByFlagKey(flagKey);
}
public FeatureFlag saveFlag(FlagDTO dto) {
FeatureFlag flag = repository.findByFlagKey(dto.flagKey).orElse(new FeatureFlag());
flag.setFlagKey(dto.flagKey);
flag.setName(dto.name);
flag.setEnabled(dto.enabled != null ? dto.enabled : true);
flag.setStrategyType(dto.strategyType != null ? dto.strategyType : FeatureFlag.StrategyType.GLOBAL);
flag.setStrategyValue(dto.strategyValue);
flag.setUpdatedAt(LocalDateTime.now());
if (flag.getCreatedAt() == null) {
flag.setCreatedAt(LocalDateTime.now());
}
return repository.save(flag);
}
public void deleteFlag(String flagKey) {
repository.findByFlagKey(flagKey).ifPresent(repository::delete);
}
// 🔥 THE HELPER FUNCTION - Call this anywhere!
public boolean isFeatureEnabled(String flagKey, String userEmail) {
return getFlag(flagKey)
.filter(FeatureFlag::isEnabled)
.map(flag -> evaluateStrategy(flag, userEmail))
.orElse(true); // Default ON if flag doesn't exist
}
private boolean evaluateStrategy(FeatureFlag flag, String userEmail) {
try {
switch (flag.getStrategyType()) {
case GLOBAL:
return true;
case BLACKLIST:
return !isUserInList(flag.getStrategyValue(), userEmail);
case WHITELIST:
return isUserInList(flag.getStrategyValue(), userEmail);
case PERCENTAGE:
return isUserInPercentage(flag.getStrategyValue());
default:
return true;
}
} catch (Exception e) {
return true; // Fail open
}
}
private boolean isUserInList(String strategyValue, String userEmail) {
if (strategyValue == null || strategyValue.isEmpty()) return true;
JsonNode users = parseJson(strategyValue).get("users");
return users != null && users.has(userEmail);
}
private boolean isUserInPercentage(String strategyValue) {
if (strategyValue == null || strategyValue.isEmpty()) return true;
JsonNode node = parseJson(strategyValue);
int percentage = node.get("percentage").asInt(100);
return random.nextInt(100) < percentage;
}
private JsonNode parseJson(String json) {
try {
return objectMapper.readTree(json);
} catch (Exception e) {
return objectMapper.createObjectNode();
}
}
}

View file

@ -0,0 +1,14 @@
package org.springframework.samples.petclinic.system;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface FeatureFlag {
String value();
}

View file

@ -1,7 +1,13 @@
# database init, supports postgres too
database=postgres
spring.datasource.url=${POSTGRES_URL:jdbc:postgresql://localhost/petclinic}
spring.datasource.url=${POSTGRES_URL:jdbc:postgresql://localhost:5432/petclinic}
spring.datasource.username=${POSTGRES_USER:petclinic}
spring.datasource.password=${POSTGRES_PASS:petclinic}
# SQL is written to be idempotent so this is safe
spring.sql.init.mode=always
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
# H2 for PetClinic data (keep both)
spring.h2.console.enabled=true

View file

@ -1,26 +1,17 @@
# database init, supports mysql too
database=h2
spring.sql.init.schema-locations=classpath*:db/${database}/schema.sql
spring.sql.init.data-locations=classpath*:db/${database}/data.sql
# database init, supports postgres too
database=postgres
spring.datasource.url=jdbc:postgresql://localhost:5432/petclinic
spring.datasource.username=petclinic
spring.datasource.password=petclinic
spring.datasource.driver-class-name=org.postgresql.Driver
# Web
spring.thymeleaf.mode=HTML
# SQL is written to be idempotent so this is safe
spring.sql.init.mode=always
# JPA
spring.jpa.hibernate.ddl-auto=none
spring.jpa.open-in-view=false
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategySnakeCaseImpl
# JPA - CRITICAL FOR feature_flags TABLE
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
# Internationalization
spring.messages.basename=messages/messages
# Actuator
management.endpoints.web.exposure.include=*
# 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
spring.web.resources.cache.cachecontrol.max-age=12h
# Disable H2 console for postgres profile
spring.h2.console.enabled=false