diff --git a/README.md b/README.md index 3e441c9..b9019cb 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,36 @@ -![logo_ironhack_blue 7](https://user-images.githubusercontent.com/23629340/40541063-a07a0a8a-601a-11e8-91b5-2f13e4e6b441.png) +I am currently learning Backend course through IronHack and this was one of my projects: -# LAB | SpringBoot REST API +Built a fully functional Spring Boot REST API from scratch! -### Instructions +I focused on applying real-world backend development practices rather than just making it “work”. Aiming for clean architecture, validation, and proper API design. -1. Fork this repo. -2. Clone your fork to your local machine. -3. Solve the challenges. +🔧 Tech Stack: - -## Deliverables +Java + Spring Boot, +Spring Web, +Spring Validation, +and Dev-Tools. -- Upon completion, add your solution to git. -- Then commit to git and push to your repo on GitHub. -- Make a pull request and paste the pull request link in the submission field in the Student Portal. +💡 Key Features Implemented: -## Tasks +Designed a Product & Customer management system +Applied input validation using annotations (e.g., constraints on name, price, email, age) +Built a service layer architecture with clean separation of concerns +Used constructor injection (no field injection) for better design +Implemented custom filtering logic (by category & price range) +Secured endpoints with a required API-Key header +Created a global exception handler to manage: +Validation errors +Missing API key +Resource not found +Invalid input scenarios +Returned proper HTTP status codes for all operations +Tested all endpoints using Postman -1. Create a Spring Boot application using Spring Initializr with the following dependencies: - - Spring Web - - Spring Boot DevTools - - Spring Boot Starter Validation +📌 Endpoints include: -2. Create a `Product` class with the following validated properties: - - name (not blank, min length 3) - - price (positive number) - - category (not blank) - - quantity (positive number) +Full CRUD operations for Products & Customers +Search by name/email +Filter by category and price range -3. Create a `ProductService` class that manages a List of Products and has methods to: - - Add a new product - - Get all products - - Get product by name - - Update product - - Delete product - - Get products by category - - Get products by price range - -4. Create a `ProductController` class that: - - Uses constructor injection for the ProductService - - Requires an "API-Key" header for all requests (value: "123456") - - Has the following endpoints: - * POST `/products` - Create new product - * GET `/products` - Get all products - * GET `/products/{name}` - Get product by name - * PUT `/products/{name}` - Update product - * DELETE `/products/{name}` - Delete product - * GET `/products/category/{category}` - Get products by category - * GET `/products/price?min={min}&max={max}` - Get products by price range - -5. Create a global exception handler that handles: - - Validation errors (return proper error messages) - - Missing API-Key header - - Product not found - - Invalid price range - -6. Create a `Customer` class with the following validated properties: - - name (not blank) - - email (valid email format) - - age (minimum 18) - - address (not blank) - -7. Create a `CustomerController` with endpoints to: - - Create new customer (with validation) - - Get all customers - - Get customer by email - - Update customer - - Delete customer - -**Remember**: -- Use proper package structure -- Use constructor injection instead of @Autowired -- Test all endpoints using Postman -- Include appropriate error handling -- Use meaningful variable and method names -- Return appropriate HTTP status codes -- Include validation messages in responses +This project helped reinforce how important structured code, validation, and error handling are in building production-ready APIs. diff --git a/src/main/java/com/SpringRestAPI/Controllers/CustomerController.java b/src/main/java/com/SpringRestAPI/Controllers/CustomerController.java new file mode 100644 index 0000000..447c6c1 --- /dev/null +++ b/src/main/java/com/SpringRestAPI/Controllers/CustomerController.java @@ -0,0 +1,104 @@ +package com.SpringRestAPI.Controllers; + +import com.SpringRestAPI.Exceptions.MissingApiKeyException; +import com.SpringRestAPI.Models.Customer; +import com.SpringRestAPI.Services.CustomerService; +import jakarta.validation.Valid; +// @Valid Annotations provide a consistent approach to validation, which improves readability and maintainability. +// It gives you automatic error feedback to the client, indicating exactly which fields are incorrect and why. +// @Valid is used for incoming data validation. Like 'Post' and 'Put' requests. It's not needed for others. +import org.springframework.web.bind.annotation.*; +import java.util.List; +import java.util.logging.Logger; + +@RestController +@RequestMapping("/api") +public class CustomerController { + + CustomerService customerService; + + public CustomerController(CustomerService customerService) { + this.customerService = customerService; + } + + // create new customer + @PostMapping("/customer") + public Customer addNewCustomer( + @RequestHeader("API-Key") String apiKey, + @Valid @RequestBody Customer customer ) { + if (!"123456".equals(apiKey)) { + throw new MissingApiKeyException("API-Key header is missing or invalid"); + } + Logger myLogger = Logger.getLogger("CustomerController new customer"); + myLogger.info("Adding new customer "); + + return customerService.addNewCustomer( + customer.getCustomerName(), + customer.getCustomerEmail(), + customer.getCustomerAge(), + customer.getCustomerAddress() + ); + } + + // get all customer + @GetMapping("/customer") + public List getCustomers( + @RequestHeader("API-Key") String apiKey) { + if (!"123456".equals(apiKey)) { + throw new RuntimeException("Invalid API Key"); + } + + Logger myLogger = Logger.getLogger("CustomerService get all customers"); + myLogger.info("Getting all customers"); + + return customerService.getCustomers(); + } + + // get customer by email + @GetMapping("/customer/{customerEmail}") + public List getCustomers( + @RequestHeader("API-Key") String apiKey, + @PathVariable String customerEmail) { + if (!"123456".equals(apiKey)) { + throw new RuntimeException("Invalid API Key"); + } + + Logger myLogger = Logger.getLogger("CustomerService getting customer by email"); + myLogger.info("Getting customer by email"); + + return customerService.getCustomerByEmail(customerEmail); + } + + // update customer name + @PutMapping("/customer/{currentName}/{newName}") + public Customer updateCustomerName( + @RequestHeader("API-Key") String apiKey, + @Valid @PathVariable String currentName, + @PathVariable String newName) { + if (!"123456".equals(apiKey)) { + throw new RuntimeException("Invalid API Key"); + } + + Logger myLogger = Logger.getLogger("CustomerController update customer name"); + myLogger.info("Updating customer name"); + + return customerService.updateCustomerName(currentName, newName); + } + + // delete customer + @DeleteMapping("/customer/{customerName}") + public Customer deleteCustomer( + @RequestHeader("API-Key") String apiKey, + @PathVariable String customerName) { + if (!"123456".equals(apiKey)) { + throw new RuntimeException("Invalid API Key"); + } + + Logger myLogger = Logger.getLogger("CustomerController delete customer name"); + myLogger.info("Deleting customer name"); + + return customerService.deleteCustomer(customerName) + .orElseThrow(() -> new RuntimeException("Customer not found")); + } + +} \ No newline at end of file diff --git a/src/main/java/com/SpringRestAPI/Controllers/ProductController.java b/src/main/java/com/SpringRestAPI/Controllers/ProductController.java new file mode 100644 index 0000000..9e43ed8 --- /dev/null +++ b/src/main/java/com/SpringRestAPI/Controllers/ProductController.java @@ -0,0 +1,172 @@ +package com.SpringRestAPI.Controllers; + +import com.SpringRestAPI.Exceptions.InvalidPriceRangeException; +import com.SpringRestAPI.Exceptions.MissingApiKeyException; +import com.SpringRestAPI.Exceptions.ProductNotFoundException; +import com.SpringRestAPI.Models.Product; +import com.SpringRestAPI.Models.ProductCategories; +import com.SpringRestAPI.Services.ProductService; +import jakarta.validation.Valid; +// @Valid Annotations provide a consistent approach to validation, which improves readability and maintainability. +// It gives you automatic error feedback to the client, indicating exactly which fields are incorrect and why. +// @Valid is used for incoming data validation. Like 'Post' and 'Put' requests. It's not needed for others. +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.logging.Logger; + +@RestController +@RequestMapping("/api") +public class ProductController { + + + ProductService productService; + + public ProductController(ProductService productService) { + this.productService = productService; + } + + // Create new product + @PostMapping("/products") // http://localhost:8080/api/products/ + public Product addNewProduct( + @RequestHeader("API-Key") String apiKey, + @Valid @RequestBody Product product ) { + if (!"123456".equals(apiKey)) { + throw new MissingApiKeyException("API-Key header is missing or invalid"); + } + Logger myLogger = Logger.getLogger("ProductController"); + myLogger.info("Create a new Product"); + + return productService.addNewProduct( + product.getProductName(), + product.getProductPrice(), + product.getProductCategory(), + product.getProductQuantity() + ); + } + + // get all products + @GetMapping("/products") // http://localhost:8080/api/products/ + public List getAllProducts(@RequestHeader("API-Key") String apiKey) { + if (!"123456".equals(apiKey)) { + throw new MissingApiKeyException("API-Key header is missing or invalid"); + } + Logger myLogger = Logger.getLogger("ProductList"); + myLogger.info("Get all the Product list"); + + return productService.getProductList(); + } + + // Get product by name + @GetMapping("/products/{productName}") + public Product getProductByName( + @RequestHeader(value="API-Key") String apiKey, + @PathVariable String productName) { + if (!"123456".equals(apiKey)) { + throw new MissingApiKeyException("API-Key header is missing or invalid"); + } + return productService.getProductByName(productName) + .orElseThrow(() -> new ProductNotFoundException("Product not found")); + } + + // change product name + @PutMapping("/products/{productName}/{newName}") // http://localhost:8080/api/products/{name}/{newName} + public Product updateProductName( + @Valid @PathVariable String productName, + @PathVariable String newName, + @RequestHeader("API-Key") String apiKey) { + if (!"123456".equals(apiKey)) { + throw new MissingApiKeyException("API-Key header is missing or invalid"); + } + Logger myLogger = Logger.getLogger(" by Name"); + myLogger.info("change Product Name"); + + return productService.updateProductName(productName, newName); + } + + // delete product by name + @DeleteMapping("/products/{productName}") // http://localhost:8080/api/products/{productName} + public Product deleteProduct( + @PathVariable String productName, + @RequestHeader("API-Key") String apiKey) { + if (!"123456".equals(apiKey)) { + throw new MissingApiKeyException("API-Key header is missing or invalid"); + } + Logger myLogger = Logger.getLogger("ProductList by Name"); + myLogger.info("delete Product by Name" + productName); + + return productService.deleteProduct(productName) + .orElseThrow(() -> new ProductNotFoundException("Product not found")); + } + + // get item by category + @GetMapping("/products/category/{category}") // http://localhost:8080/api/products/category/{category} + public List getProductByCategory( + @PathVariable ProductCategories category, + @RequestHeader("API-Key") String apiKey) { + if (!"123456".equals(apiKey)) { + throw new RuntimeException("Invalid API Key"); + } + Logger myLogger = Logger.getLogger("ProductList by Category"); + myLogger.info("get Product by Category"); + + return productService.getProductByCategory(category); + } + + // get products by price range + @GetMapping("/products/price") // http://localhost:8080/api/products/price?min=10&max=30 + public ResponseEntity> getProductsByPriceRange( + @RequestParam double min, // @RequestParam is /price > ?min=VALUE + @RequestParam double max, // &max=VALUE + @RequestHeader(value="API-Key") String apiKey) { + // Check API Key presence and validity + if (!"123456".equals(apiKey)) { + throw new MissingApiKeyException("API-Key header is missing or invalid"); + } + // Check for valid price range + if (min > max) { + throw new InvalidPriceRangeException("Min price cannot be greater than max price"); + } + // Business logic to find products by price range + List products = productService.getProductsByPriceRange(min, max); + + if (products == null || products.isEmpty()) { + throw new ProductNotFoundException("No product found in this price range"); + } + return ResponseEntity.ok(products); + } + +/// IF YOU ARE READING THIS +/// MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW +/// MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW +/// MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW +/// MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW +/// MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW +/// MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW +/// MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW + + /* + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⡷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⡿⠋⠈⠻⣮⣳⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣴⣾⡿⠋⠀⠀⠀⠀⠙⣿⣿⣤⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣶⣿⡿⠟⠛⠉⠀⠀⠀⠀⠀⠀⠀⠈⠛⠛⠿⠿⣿⣷⣶⣤⣄⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣴⣾⡿⠟⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠛⠻⠿⣿⣶⣦⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ + ⠀⠀⠀⣀⣠⣤⣤⣀⡀⠀⠀⣀⣴⣿⡿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠿⣿⣷⣦⣄⡀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣤⣄⠀⠀ + ⢀⣤⣾⡿⠟⠛⠛⢿⣿⣶⣾⣿⠟⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠿⣿⣷⣦⣀⣀⣤⣶⣿⡿⠿⢿⣿⡀⠀ + ⣿⣿⠏⠀⢰⡆⠀⠀⠉⢿⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠻⢿⡿⠟⠋⠁⠀⠀⢸⣿⠇⠀ + ⣿⡟⠀⣀⠈⣀⡀⠒⠃⠀⠙⣿⡆⠀⠀⠀⠀⠀⠀⠀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⠇⠀ + ⣿⡇⠀⠛⢠⡋⢙⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⠀⠀ + ⣿⣧⠀⠀⠀⠓⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠛⠋⠀⠀⢸⣧⣤⣤⣶⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⡿⠀⠀ + ⣿⣿⣤⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠻⣷⣶⣶⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⠁⠀⠀ + ⠈⠛⠻⠿⢿⣿⣷⣶⣦⣤⣄⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⡏⠀⠀⠀ + ⠀⠀⠀⠀⠀⠀⠀⠉⠙⠛⠻⠿⢿⣿⣷⣶⣦⣤⣄⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠿⠛⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢿⣿⡄⠀⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠙⠛⠻⠿⢿⣿⣷⣶⣦⣤⣄⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⣿⡄⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠛⠛⠿⠿⣿⣷⣶⣶⣤⣤⣀⡀⠀⠀⠀⢀⣴⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⡿⣄ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠛⠛⠿⠿⣿⣷⣶⡿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⣹ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⠀⠀⠀⠀⠀⠀⢸⣧ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣿⣆⠀⠀⠀⠀⠀⠀⢀⣀⣠⣤⣶⣾⣿⣿⣿⣿⣤⣄⣀⡀⠀⠀⠀⣿ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⢿⣻⣷⣶⣾⣿⣿⡿⢯⣛⣛⡋⠁⠀⠀⠉⠙⠛⠛⠿⣿⣿⡷⣶⣿ + */ + +} diff --git a/src/main/java/com/SpringRestAPI/Exceptions/GlobalExceptionHandler.java b/src/main/java/com/SpringRestAPI/Exceptions/GlobalExceptionHandler.java new file mode 100644 index 0000000..182a100 --- /dev/null +++ b/src/main/java/com/SpringRestAPI/Exceptions/GlobalExceptionHandler.java @@ -0,0 +1,62 @@ +package com.SpringRestAPI.Exceptions; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; + +import java.util.HashMap; +import java.util.Map; + +/// @ControllerAdvice provides a centralized mechanism to handle exceptions across all your REST controllers. +/// It allows you to define ways to handle specific exception types and craft consistent, meaningful responses. + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(ProductNotFoundException.class) + public ResponseEntity handleProductNotFound(ProductNotFoundException ex, WebRequest request) { + return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(MissingApiKeyException.class) + public ResponseEntity handleMissingApiKey(MissingApiKeyException ex) { + return new ResponseEntity<>(ex.getMessage(), HttpStatus.FORBIDDEN); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) // For product and customer model validations + public ResponseEntity> handleValidationExceptions(MethodArgumentNotValidException ex) { + /// Happens when Spring validates your request body or parameters + /// and finds problems (like a @NotNull or @Min annotation failing) + + Map errors = new HashMap<>(); /// Instead of returning just one error, + /// you might have multiple fields failing, so we use a HashMap to return each field and its error message. + + ex.getBindingResult().getAllErrors().forEach(error -> { /// gives all validation errors from the request. + String fieldName = ((FieldError) error).getField(); /// Cast each error to FieldError so we can get the field name. + String errorMessage = error.getDefaultMessage(); /// Get the default message + errors.put(fieldName, errorMessage); /// Put it in the Map with fieldName as key and errorMessage as value. + }); + return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); + /// Return all errors as a JSON object with 400 Bad Request + } + + /// If a field fails validation, Spring creates a FieldError object. + /// FieldError contains info about: + + /// field: the name of the field that failed (name or price) + /// rejectedValue: the value the user sent + /// defaultMessage: the message you wrote in the annotation + + /// getDefaultMessage() returns the validation message you wrote in the annotation + + @ExceptionHandler(InvalidPriceRangeException.class) + public ResponseEntity handleInvalidPriceRange(InvalidPriceRangeException ex) { + return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/SpringRestAPI/Exceptions/InvalidPriceRangeException.java b/src/main/java/com/SpringRestAPI/Exceptions/InvalidPriceRangeException.java new file mode 100644 index 0000000..aff03a8 --- /dev/null +++ b/src/main/java/com/SpringRestAPI/Exceptions/InvalidPriceRangeException.java @@ -0,0 +1,7 @@ +package com.SpringRestAPI.Exceptions; + +public class InvalidPriceRangeException extends RuntimeException { + public InvalidPriceRangeException(String message) { + super(message); + } +} diff --git a/src/main/java/com/SpringRestAPI/Exceptions/MissingApiKeyException.java b/src/main/java/com/SpringRestAPI/Exceptions/MissingApiKeyException.java new file mode 100644 index 0000000..b4c69fc --- /dev/null +++ b/src/main/java/com/SpringRestAPI/Exceptions/MissingApiKeyException.java @@ -0,0 +1,7 @@ +package com.SpringRestAPI.Exceptions; + +public class MissingApiKeyException extends RuntimeException { + public MissingApiKeyException(String message) { + super(message); + } +} diff --git a/src/main/java/com/SpringRestAPI/Exceptions/ProductNotFoundException.java b/src/main/java/com/SpringRestAPI/Exceptions/ProductNotFoundException.java new file mode 100644 index 0000000..9951708 --- /dev/null +++ b/src/main/java/com/SpringRestAPI/Exceptions/ProductNotFoundException.java @@ -0,0 +1,7 @@ +package com.SpringRestAPI.Exceptions; + +public class ProductNotFoundException extends RuntimeException { + public ProductNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/SpringRestAPI/Models/Customer.java b/src/main/java/com/SpringRestAPI/Models/Customer.java new file mode 100644 index 0000000..d30b855 --- /dev/null +++ b/src/main/java/com/SpringRestAPI/Models/Customer.java @@ -0,0 +1,79 @@ +package com.SpringRestAPI.Models; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class Customer { + + @NotBlank(message = "The Name cannot be blank") + @Size(min = 2, max = 16, message = "Name must be between 2 and 16 characters") + private String customerName; + + @Email(message = "Please enter a valid e-mail address") + private String customerEmail; + + @Min(value = 18, message = "Age must be at least 18") + private int customerAge; + + @NotBlank(message = "The Address cannot be blank") + private String customerAddress; + + + /// @NotBlank: Ensures that the annotated string is not null or consists of only whitespace characters. + /// This is useful for fields like name and address to ensure that they are filled with meaningful data. + + /// @Size: Validates that the annotated string's length is within the specified range (min and max). + /// This helps enforce constraints on text fields, such as ensuring a name is neither too short nor too long. + + /// @Email: Validates that the annotated string is a well-formed email address. + /// This is crucial for preventing invalid email formats in your system. + + /// @Min: Checks that the annotated integer is at least a specified value. + /// In this case it is 18. Which is often used for age restrictions. + + + public Customer(String customerName, String customerEmail, int customerAge, String customerAddress) { + this.customerName = customerName; + this.customerEmail = customerEmail; + this.customerAge = customerAge; + this.customerAddress = customerAddress; + } + + public String getCustomerName() { + return customerName; + } + + public void setCustomerName(String customerName) { + this.customerName = customerName; + } + + public String getCustomerEmail() { + return customerEmail; + } + + public void setCustomerEmail(String customerEmail) { + this.customerEmail = customerEmail; + } + + public int getCustomerAge() { + return customerAge; + } + + public void setCustomerAge(int customerAge) { + this.customerAge = customerAge; + } + + public String getCustomerAddress() { + return customerAddress; + } + + public void setCustomerAddress(String customerAddress) { + this.customerAddress = customerAddress; + } +} + + + + diff --git a/src/main/java/com/SpringRestAPI/Models/Product.java b/src/main/java/com/SpringRestAPI/Models/Product.java new file mode 100644 index 0000000..1a86e63 --- /dev/null +++ b/src/main/java/com/SpringRestAPI/Models/Product.java @@ -0,0 +1,73 @@ +package com.SpringRestAPI.Models; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; + +public class Product { + + + @NotBlank(message = "The name cannot be blank") + @Size(min = 3, max = 12, message = "Name must be between 3 and 12 characters") + private String productName; + + @Positive(message = "Price cannot be negative") + private double productPrice; + + @NotNull(message = "The Category cannot be blank") + private ProductCategories productCategory; + + @Positive(message = "Quantity cannot be negative") + private int productQuantity; + + /// @NotBlank: Ensures that the annotated string is not null or consists of only whitespace characters. + /// This is useful for fields like name and address to ensure that they are filled with meaningful data. + + + /// @Size: Validates that the annotated string's length is within the specified range (min and max). + /// This helps enforce constraints on text fields, such as ensuring a name is neither too short nor too long. + + /// @Positive: Ensures that the annotated number is strictly greater than 0. + /// This is useful for values that must always be positive, such as quantities, prices, or IDs. + + public Product(String productName, double productPrice, ProductCategories productCategory, int productQuantity) { + this.productName = productName; + this.productPrice = productPrice; + this.productCategory = productCategory; + this.productQuantity = productQuantity; + } + + public String getProductName() { + return productName; + } + + public void setProductName(String productName) { + this.productName = productName; + } + + public double getProductPrice() { + return productPrice; + } + + public void setProductPrice(double productPrice) { + this.productPrice = productPrice; + } + + public ProductCategories getProductCategory() { + return productCategory; + } + + public void setProductCategory(ProductCategories productCategory) { + this.productCategory = productCategory; + } + + public int getProductQuantity() { + return productQuantity; + } + + public void setProductQuantity(int productQuantity) { + this.productQuantity = productQuantity; + } + +} diff --git a/src/main/java/com/SpringRestAPI/Models/ProductCategories.java b/src/main/java/com/SpringRestAPI/Models/ProductCategories.java new file mode 100644 index 0000000..9ab208d --- /dev/null +++ b/src/main/java/com/SpringRestAPI/Models/ProductCategories.java @@ -0,0 +1,7 @@ +package com.SpringRestAPI.Models; + +public enum ProductCategories { + + ELECTRONIC, TOYS, MUSIC, MOVIE, BOOKS + +} diff --git a/src/main/java/com/SpringRestAPI/Services/CustomerService.java b/src/main/java/com/SpringRestAPI/Services/CustomerService.java new file mode 100644 index 0000000..94e66f1 --- /dev/null +++ b/src/main/java/com/SpringRestAPI/Services/CustomerService.java @@ -0,0 +1,89 @@ +package com.SpringRestAPI.Services; + +import com.SpringRestAPI.Models.Customer; +import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +public class CustomerService { + + private List customers; + + public CustomerService() { + setCustomerList(); + } + + // creating customers + public void setCustomerList() { + customers = new ArrayList<>(); + + customers.add(new Customer("Eren", "eren@test.com", 20, "Hamburg, Germany")); + customers.add(new Customer("Mikasa", "mikasa@test.com", 20, "Berlin, Germany")); + customers.add(new Customer("Armin", "armin@test.com", 20, "Leipzig, Germany")); + } + + // get all customers + public List getCustomers() { + return customers; + } + + // add new customer + public Customer addNewCustomer(String customerName, String customerEmail, int customerAge, String customerAddress) { + Customer newCustomer = new Customer(customerName, customerEmail, customerAge, customerAddress); + customers.add(newCustomer); + return newCustomer; + } + + // get customer by email + public List getCustomerByEmail(String email) { + List customerList = new ArrayList<>(); + for (Customer customer : customers) { + if (customer.getCustomerEmail().equalsIgnoreCase(email)) { + customerList.add(customer); + } + } + return customerList; + } + + // update customer + public Customer updateCustomerName(String currentName, String newName) { + for (Customer customer : customers) { + if (customer.getCustomerName().equalsIgnoreCase(currentName)) { + customer.setCustomerName(newName); + return customer; + } + } + throw new RuntimeException("Customer not found"); + } + + // delete customer + public Optional deleteCustomer(String customerName) { /// Optional because we're deleting one + Optional found = customers.stream() /// This takes your list and turns into Stream. + /// Stream lets you process elements one by one instead of writing loops manually + + .filter(p -> p.getCustomerName().equals(customerName)) + /// This keeps only the customers that match the name + /// p -> ... lambda function (short version of a method) + + .findFirst(); + /// Gives the first element from the filtered results. + + found.ifPresent(customers::remove); + /// if a product was found, then execute this code + /// Long version: + /* + if (found.isPresent()) { + productList.remove(found.get()); + } + */ + return found; + /// Returns results or Exception if not found + } + + + + + +} diff --git a/src/main/java/com/SpringRestAPI/Services/ProductService.java b/src/main/java/com/SpringRestAPI/Services/ProductService.java new file mode 100644 index 0000000..3280026 --- /dev/null +++ b/src/main/java/com/SpringRestAPI/Services/ProductService.java @@ -0,0 +1,99 @@ +package com.SpringRestAPI.Services; + +import com.SpringRestAPI.Models.Product; +import com.SpringRestAPI.Models.ProductCategories; +import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +public class ProductService { + + + private List productList; + + public ProductService() { + setProductList(); + } + + public void setProductList (){ + productList = new ArrayList<>(); + + productList.add(new Product("iPhone 17",879.00, ProductCategories.ELECTRONIC,5)); + productList.add(new Product("Hot Wheels Porsche",4.99, ProductCategories.TOYS,20)); + productList.add(new Product("Ado's Best Adobum",39.00, ProductCategories.MUSIC,3)); + productList.add(new Product("Howl's Moving Castle",27.50, ProductCategories.MOVIE,50)); + productList.add(new Product("The Hunger Games",19.90, ProductCategories.BOOKS,110)); + } + + // add a new product + public Product addNewProduct(String productName, double productPrice, ProductCategories productCategory, int productQuantity) { + Product newProduct = new Product(productName, productPrice, productCategory, productQuantity); + productList.add(newProduct); + return newProduct; + } + + // get all products + public List getProductList() { + return productList; + } + + // get products by name + public Optional getProductByName(String productName) { + for (Product product : productList) { + if (product.getProductName().equalsIgnoreCase(productName)) { + return Optional.of(product); // return if product found with the name in that list + } + } + return Optional.empty(); // if no product found by the name, it will return empty + } + + // update product + public Product updateProductName(String currentName, String newName) { + for (Product product : productList) { + if (product.getProductName().equalsIgnoreCase(currentName)) { + product.setProductName(newName); + return product; + } + } + throw new RuntimeException("Product not found"); + } + + // delete product + public Optional deleteProduct(String productName) { + Optional found = productList.stream() + .filter(p -> p.getProductName().equals(productName)) + .findFirst(); + + found.ifPresent(productList::remove); + return found; + } + + // get products by category + public List getProductByCategory (ProductCategories productCategory){ + List products = new ArrayList<>(); + for (Product product : productList) { + if (product.getProductCategory().equals(productCategory)) { + products.add(product); + } + } + return products; + } + + // get products by price range + public List getProductsByPriceRange (double min, double max){ + List products = new ArrayList<>(); + for (Product product : productList) { + if (product.getProductPrice()>=min&&product.getProductPrice()<=max) { + products.add(product); + } + } + return products; + } + + /// Product → “There is definitely a product” + /// Optional → “There might be a product” + /// List → “There could be many products” + +} diff --git a/src/main/java/com/SpringRestAPI/SpringBootRestApiApplication.java b/src/main/java/com/SpringRestAPI/SpringBootRestApiApplication.java new file mode 100644 index 0000000..4d16643 --- /dev/null +++ b/src/main/java/com/SpringRestAPI/SpringBootRestApiApplication.java @@ -0,0 +1,13 @@ +package com.SpringRestAPI; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringBootRestApiApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringBootRestApiApplication.class, args); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..403d95f --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=SpringBoot-REST-API diff --git a/src/test/java/com/SpringRestAPI/SpringBootRestApiApplicationTests.java b/src/test/java/com/SpringRestAPI/SpringBootRestApiApplicationTests.java new file mode 100644 index 0000000..abd5fdd --- /dev/null +++ b/src/test/java/com/SpringRestAPI/SpringBootRestApiApplicationTests.java @@ -0,0 +1,13 @@ +package com.SpringRestAPI; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SpringBootRestApiApplicationTests { + + @Test + void contextLoads() { + } + +}