diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..912c0d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +target/ +.idea/ + +*.iml +*.log diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..bc3fadf --- /dev/null +++ b/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + com.ironhack + lab-java-springboot-rest-api + 0.0.1-SNAPSHOT + lab-java-springboot-rest-api + LAB SpringBoot REST API - Ironhack + + + 17 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/src/main/java/com/ironhack/lab/LabRestApiApplication.java b/src/main/java/com/ironhack/lab/LabRestApiApplication.java new file mode 100644 index 0000000..b1555e3 --- /dev/null +++ b/src/main/java/com/ironhack/lab/LabRestApiApplication.java @@ -0,0 +1,12 @@ +package com.ironhack.lab; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class LabRestApiApplication { + + public static void main(String[] args) { + SpringApplication.run(LabRestApiApplication.class, args); + } +} diff --git a/src/main/java/com/ironhack/lab/controller/CustomerController.java b/src/main/java/com/ironhack/lab/controller/CustomerController.java new file mode 100644 index 0000000..5f5da39 --- /dev/null +++ b/src/main/java/com/ironhack/lab/controller/CustomerController.java @@ -0,0 +1,55 @@ +package com.ironhack.lab.controller; + +import com.ironhack.lab.model.Customer; +import com.ironhack.lab.service.CustomerService; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/customers") +public class CustomerController { + + private final CustomerService customerService; + + public CustomerController(CustomerService customerService) { + this.customerService = customerService; + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public Customer createCustomer(@Valid @RequestBody Customer customer) { + return customerService.addCustomer(customer); + } + + @GetMapping + public List getAllCustomers() { + return customerService.getAllCustomers(); + } + + @GetMapping("/{email}") + public Customer getCustomerByEmail(@PathVariable String email) { + return customerService.getCustomerByEmail(email); + } + + @PutMapping("/{email}") + public Customer updateCustomer(@PathVariable String email, @Valid @RequestBody Customer customer) { + return customerService.updateCustomer(email, customer); + } + + @DeleteMapping("/{email}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteCustomer(@PathVariable String email) { + customerService.deleteCustomer(email); + } +} diff --git a/src/main/java/com/ironhack/lab/controller/ProductController.java b/src/main/java/com/ironhack/lab/controller/ProductController.java new file mode 100644 index 0000000..6db2a7b --- /dev/null +++ b/src/main/java/com/ironhack/lab/controller/ProductController.java @@ -0,0 +1,88 @@ +package com.ironhack.lab.controller; + +import com.ironhack.lab.exception.InvalidApiKeyException; +import com.ironhack.lab.model.Product; +import com.ironhack.lab.service.ProductService; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/products") +public class ProductController { + + private static final String VALID_API_KEY = "123456"; + private final ProductService productService; + + public ProductController(ProductService productService) { + this.productService = productService; + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public Product createProduct(@RequestHeader("API-Key") String apiKey, @Valid @RequestBody Product product) { + validateApiKey(apiKey); + return productService.addProduct(product); + } + + @GetMapping + public List getAllProducts(@RequestHeader("API-Key") String apiKey) { + validateApiKey(apiKey); + return productService.getAllProducts(); + } + + @GetMapping("/{name}") + public Product getProductByName(@RequestHeader("API-Key") String apiKey, @PathVariable String name) { + validateApiKey(apiKey); + return productService.getProductByName(name); + } + + @PutMapping("/{name}") + public Product updateProduct( + @RequestHeader("API-Key") String apiKey, + @PathVariable String name, + @Valid @RequestBody Product product) { + validateApiKey(apiKey); + return productService.updateProduct(name, product); + } + + @DeleteMapping("/{name}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteProduct(@RequestHeader("API-Key") String apiKey, @PathVariable String name) { + validateApiKey(apiKey); + productService.deleteProduct(name); + } + + @GetMapping("/category/{category}") + public List getProductsByCategory(@RequestHeader("API-Key") String apiKey, @PathVariable String category) { + validateApiKey(apiKey); + return productService.getProductsByCategory(category); + } + + @GetMapping("/price") + public List getProductsByPriceRange( + @RequestHeader("API-Key") String apiKey, + @RequestParam double min, + @RequestParam double max) { + validateApiKey(apiKey); + return productService.getProductsByPriceRange(min, max); + } + + private void validateApiKey(String apiKey) { + if (!VALID_API_KEY.equals(apiKey)) { + throw new InvalidApiKeyException(); + } + } +} diff --git a/src/main/java/com/ironhack/lab/exception/CustomerNotFoundException.java b/src/main/java/com/ironhack/lab/exception/CustomerNotFoundException.java new file mode 100644 index 0000000..a6625f3 --- /dev/null +++ b/src/main/java/com/ironhack/lab/exception/CustomerNotFoundException.java @@ -0,0 +1,8 @@ +package com.ironhack.lab.exception; + +public class CustomerNotFoundException extends RuntimeException { + + public CustomerNotFoundException(String email) { + super("Customer not found: " + email); + } +} diff --git a/src/main/java/com/ironhack/lab/exception/InvalidApiKeyException.java b/src/main/java/com/ironhack/lab/exception/InvalidApiKeyException.java new file mode 100644 index 0000000..d053cb7 --- /dev/null +++ b/src/main/java/com/ironhack/lab/exception/InvalidApiKeyException.java @@ -0,0 +1,8 @@ +package com.ironhack.lab.exception; + +public class InvalidApiKeyException extends RuntimeException { + + public InvalidApiKeyException() { + super("Invalid API-Key header"); + } +} diff --git a/src/main/java/com/ironhack/lab/exception/InvalidPriceRangeException.java b/src/main/java/com/ironhack/lab/exception/InvalidPriceRangeException.java new file mode 100644 index 0000000..db68226 --- /dev/null +++ b/src/main/java/com/ironhack/lab/exception/InvalidPriceRangeException.java @@ -0,0 +1,8 @@ +package com.ironhack.lab.exception; + +public class InvalidPriceRangeException extends RuntimeException { + + public InvalidPriceRangeException() { + super("Invalid price range: min must be less than or equal to max"); + } +} diff --git a/src/main/java/com/ironhack/lab/exception/ProductNotFoundException.java b/src/main/java/com/ironhack/lab/exception/ProductNotFoundException.java new file mode 100644 index 0000000..5fd24be --- /dev/null +++ b/src/main/java/com/ironhack/lab/exception/ProductNotFoundException.java @@ -0,0 +1,8 @@ +package com.ironhack.lab.exception; + +public class ProductNotFoundException extends RuntimeException { + + public ProductNotFoundException(String name) { + super("Product not found: " + name); + } +} diff --git a/src/main/java/com/ironhack/lab/handler/GlobalExceptionHandler.java b/src/main/java/com/ironhack/lab/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..2bd71c6 --- /dev/null +++ b/src/main/java/com/ironhack/lab/handler/GlobalExceptionHandler.java @@ -0,0 +1,68 @@ +package com.ironhack.lab.handler; + +import com.ironhack.lab.exception.CustomerNotFoundException; +import com.ironhack.lab.exception.InvalidApiKeyException; +import com.ironhack.lab.exception.InvalidPriceRangeException; +import com.ironhack.lab.exception.ProductNotFoundException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingRequestHeaderException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.HashMap; +import java.util.Map; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationErrors(MethodArgumentNotValidException exception) { + Map validationErrors = new HashMap<>(); + + exception.getBindingResult().getFieldErrors().forEach(error -> + validationErrors.put(error.getField(), error.getDefaultMessage())); + + return buildErrorResponse(HttpStatus.BAD_REQUEST, "Validation failed", validationErrors); + } + + @ExceptionHandler(MissingRequestHeaderException.class) + public ResponseEntity> handleMissingHeader(MissingRequestHeaderException exception) { + return buildErrorResponse( + HttpStatus.UNAUTHORIZED, + "Missing required header: " + exception.getHeaderName(), + null); + } + + @ExceptionHandler(InvalidApiKeyException.class) + public ResponseEntity> handleInvalidApiKey(InvalidApiKeyException exception) { + return buildErrorResponse(HttpStatus.UNAUTHORIZED, exception.getMessage(), null); + } + + @ExceptionHandler({ProductNotFoundException.class, CustomerNotFoundException.class}) + public ResponseEntity> handleNotFound(RuntimeException exception) { + return buildErrorResponse(HttpStatus.NOT_FOUND, exception.getMessage(), null); + } + + @ExceptionHandler(InvalidPriceRangeException.class) + public ResponseEntity> handleInvalidPriceRange(InvalidPriceRangeException exception) { + return buildErrorResponse(HttpStatus.BAD_REQUEST, exception.getMessage(), null); + } + + private ResponseEntity> buildErrorResponse( + HttpStatus status, + String message, + Map validationErrors) { + Map response = new HashMap<>(); + response.put("status", status.value()); + response.put("error", status.getReasonPhrase()); + response.put("message", message); + + if (validationErrors != null) { + response.put("validationErrors", validationErrors); + } + + return ResponseEntity.status(status).body(response); + } +} diff --git a/src/main/java/com/ironhack/lab/model/Customer.java b/src/main/java/com/ironhack/lab/model/Customer.java new file mode 100644 index 0000000..51f16f1 --- /dev/null +++ b/src/main/java/com/ironhack/lab/model/Customer.java @@ -0,0 +1,63 @@ +package com.ironhack.lab.model; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class Customer { + + @NotBlank(message = "Customer name is required") + private String name; + + @Email(message = "Email must be valid") + @NotBlank(message = "Email is required") + private String email; + + @Min(value = 18, message = "Customer must be at least 18 years old") + private int age; + + @NotBlank(message = "Address is required") + private String address; + + public Customer() { + } + + public Customer(String name, String email, int age, String address) { + this.name = name; + this.email = email; + this.age = age; + this.address = address; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } +} diff --git a/src/main/java/com/ironhack/lab/model/Product.java b/src/main/java/com/ironhack/lab/model/Product.java new file mode 100644 index 0000000..4014af6 --- /dev/null +++ b/src/main/java/com/ironhack/lab/model/Product.java @@ -0,0 +1,63 @@ +package com.ironhack.lab.model; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; + +public class Product { + + @NotBlank(message = "Product name is required") + @Size(min = 3, message = "Product name must have at least 3 characters") + private String name; + + @Positive(message = "Price must be a positive number") + private double price; + + @NotBlank(message = "Category is required") + private String category; + + @Positive(message = "Quantity must be a positive number") + private int quantity; + + public Product() { + } + + public Product(String name, double price, String category, int quantity) { + this.name = name; + this.price = price; + this.category = category; + this.quantity = quantity; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public int getQuantity() { + return quantity; + } + + public void setQuantity(int quantity) { + this.quantity = quantity; + } +} diff --git a/src/main/java/com/ironhack/lab/service/CustomerService.java b/src/main/java/com/ironhack/lab/service/CustomerService.java new file mode 100644 index 0000000..e18e6d9 --- /dev/null +++ b/src/main/java/com/ironhack/lab/service/CustomerService.java @@ -0,0 +1,44 @@ +package com.ironhack.lab.service; + +import com.ironhack.lab.exception.CustomerNotFoundException; +import com.ironhack.lab.model.Customer; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class CustomerService { + + private final List customers = new ArrayList<>(); + + public Customer addCustomer(Customer customer) { + customers.add(customer); + return customer; + } + + public List getAllCustomers() { + return customers; + } + + public Customer getCustomerByEmail(String email) { + return customers.stream() + .filter(customer -> customer.getEmail().equalsIgnoreCase(email)) + .findFirst() + .orElseThrow(() -> new CustomerNotFoundException(email)); + } + + public Customer updateCustomer(String email, Customer updatedCustomer) { + Customer existingCustomer = getCustomerByEmail(email); + existingCustomer.setName(updatedCustomer.getName()); + existingCustomer.setEmail(updatedCustomer.getEmail()); + existingCustomer.setAge(updatedCustomer.getAge()); + existingCustomer.setAddress(updatedCustomer.getAddress()); + return existingCustomer; + } + + public void deleteCustomer(String email) { + Customer customer = getCustomerByEmail(email); + customers.remove(customer); + } +} diff --git a/src/main/java/com/ironhack/lab/service/ProductService.java b/src/main/java/com/ironhack/lab/service/ProductService.java new file mode 100644 index 0000000..0e829f3 --- /dev/null +++ b/src/main/java/com/ironhack/lab/service/ProductService.java @@ -0,0 +1,61 @@ +package com.ironhack.lab.service; + +import com.ironhack.lab.exception.InvalidPriceRangeException; +import com.ironhack.lab.exception.ProductNotFoundException; +import com.ironhack.lab.model.Product; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class ProductService { + + private final List products = new ArrayList<>(); + + public Product addProduct(Product product) { + products.add(product); + return product; + } + + public List getAllProducts() { + return products; + } + + public Product getProductByName(String name) { + return products.stream() + .filter(product -> product.getName().equalsIgnoreCase(name)) + .findFirst() + .orElseThrow(() -> new ProductNotFoundException(name)); + } + + public Product updateProduct(String name, Product updatedProduct) { + Product existingProduct = getProductByName(name); + existingProduct.setName(updatedProduct.getName()); + existingProduct.setPrice(updatedProduct.getPrice()); + existingProduct.setCategory(updatedProduct.getCategory()); + existingProduct.setQuantity(updatedProduct.getQuantity()); + return existingProduct; + } + + public void deleteProduct(String name) { + Product product = getProductByName(name); + products.remove(product); + } + + public List getProductsByCategory(String category) { + return products.stream() + .filter(product -> product.getCategory().equalsIgnoreCase(category)) + .toList(); + } + + public List getProductsByPriceRange(double min, double max) { + if (min < 0 || max < 0 || min > max) { + throw new InvalidPriceRangeException(); + } + + return products.stream() + .filter(product -> product.getPrice() >= min && product.getPrice() <= max) + .toList(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..3033234 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,2 @@ +spring.application.name=lab-java-springboot-rest-api +server.port=8080