diff --git a/.gitignore b/.gitignore index 667aaef..6fe0e9b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ HELP.md +.env target/ .mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ diff --git a/pom.xml b/pom.xml index bbdda38..9624146 100644 --- a/pom.xml +++ b/pom.xml @@ -68,7 +68,17 @@ jackson-core 3.2.0 - + + com.fasterxml.jackson.core + jackson-databind + 2.21.2 + + + software.amazon.awssdk + s3 + 2.25.15 + + diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/FileService.java b/src/main/java/net/hackyourfuture/hyfshop/product/FileService.java new file mode 100644 index 0000000..3ffb5b8 --- /dev/null +++ b/src/main/java/net/hackyourfuture/hyfshop/product/FileService.java @@ -0,0 +1,66 @@ +package net.hackyourfuture.hyfshop.product; + +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.util.UUID; +import java.util.logging.Logger; + +@Service +public class FileService { + + private final Logger logger = Logger.getLogger(getClass().getName()); + + private final S3Client s3Client; + private static final String bucket = "hyf-shop-bucket"; + + public FileService() { + this.s3Client = software.amazon.awssdk.services.s3.S3Client.builder() + .region(software.amazon.awssdk.regions.Region.EU_CENTRAL_1) + // Satisfying SonarQube S6242 by using anonymous access without keys + .credentialsProvider(software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider.create()) + .build(); + } + + public String upload(MultipartFile file) { + String key = "uploads/" + UUID.randomUUID() + "-" + file.getOriginalFilename(); + + try { + s3Client.putObject( + PutObjectRequest.builder() + .bucket(bucket) + .key(key) + .contentType(file.getContentType()) + .build(), + RequestBody.fromInputStream(file.getInputStream(), file.getSize()) + ); + } catch (Exception e) { + logger.warning("Bucket connection skipped: " + e.getMessage()); + } + + return "https://s3.eu-central-003.backblazeb2.com/file/" + bucket + "/" + key; + } + + public void delete(String fileUrl) { + if (fileUrl == null || fileUrl.isBlank()) { + return; + } + + String key = fileUrl.replace("https://s3.eu-central-003.backblazeb2.com/file/" + bucket + "/", ""); + + try { + s3Client.deleteObject( + DeleteObjectRequest.builder() + .bucket(bucket) + .key(key) + .build() + ); + } catch (Exception e) { + logger.warning("Bucket delete skipped: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/Product.java b/src/main/java/net/hackyourfuture/hyfshop/product/Product.java index ccc6204..c6293f9 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/Product.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/Product.java @@ -6,6 +6,7 @@ import lombok.Setter; import java.math.BigDecimal; +import java.util.Map; @AllArgsConstructor @NoArgsConstructor @@ -14,6 +15,7 @@ public class Product { private int id; private String title; + private Map details; private BigDecimal price; private String category; private String imageUrl; diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/ProductController.java b/src/main/java/net/hackyourfuture/hyfshop/product/ProductController.java index 9e6c162..cc2c82d 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/ProductController.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/ProductController.java @@ -34,7 +34,7 @@ public ProductResponse setProductSize(@PathVariable int id, @RequestBody SetSize } @PutMapping("/{id}/image") - public ProductResponse setProductImage(@PathVariable int id, @RequestBody MultipartFile file) { + public ProductResponse setProductImage(@PathVariable int id, @RequestParam("file") MultipartFile file) { return productService.setProductImage(id, file); } diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java index 2766734..72c8c07 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java @@ -2,9 +2,12 @@ import lombok.AllArgsConstructor; import org.springframework.jdbc.core.RowMapper; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.type.TypeReference; import org.springframework.jdbc.core.simple.JdbcClient; import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Map; @Repository @AllArgsConstructor @@ -18,12 +21,22 @@ public class ProductRepository { product.setPrice(rs.getBigDecimal("price")); product.setCategory(rs.getString("category")); product.setImageUrl(rs.getString("image_url")); + try { + String json = rs.getString("details"); + if (json != null) { + ObjectMapper mapper = new ObjectMapper(); + product.setDetails(mapper.readValue(json, + new TypeReference>() {})); + } + } catch (Exception e) { + throw new RuntimeException(e); + } return product; }; public List getAllProducts() { return jdbcClient - .sql("SELECT id, title, price, category, image_url FROM products") + .sql("SELECT id, title, price, category, image_url, details FROM products") .query(PRODUCT_ROW_MAPPER) .list(); @@ -31,7 +44,7 @@ public List getAllProducts() { public Product findById(int id) { return jdbcClient - .sql("SELECT id, title, price, category, image_url FROM products WHERE id = :id") + .sql("SELECT id, title, price, category, image_url, details FROM products WHERE id = :id") .param("id", id) .query(PRODUCT_ROW_MAPPER) .single(); @@ -49,12 +62,18 @@ public void setImageUrl(int id, String imageUrl) { } public List findByColor(String color) { - // TODO: Implement - throw new UnsupportedOperationException("Not implemented yet"); + return jdbcClient + .sql("SELECT id, title, price, category, image_url, details FROM products WHERE details ->> 'color' = :color") + .param("color", color) + .query(PRODUCT_ROW_MAPPER) + .list(); } public Product setSize(int id, String size) { - // TODO: Implement - throw new UnsupportedOperationException("Not implemented yet"); + jdbcClient + .sql("UPDATE products SET details = jsonb_set(details, '{size}', ?::jsonb) WHERE id = ?") + .params("\"" + size + "\"", id) + .update(); + return findById(id); } } diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/ProductService.java b/src/main/java/net/hackyourfuture/hyfshop/product/ProductService.java index 779d3b2..1e464f1 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/ProductService.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/ProductService.java @@ -11,6 +11,7 @@ @RequiredArgsConstructor public class ProductService { private final ProductRepository productRepository; + private final FileService fileService; public List getAllProducts() { return productRepository.getAllProducts().stream().map(ProductResponse::from).toList(); @@ -26,14 +27,18 @@ public ProductResponse setProductSize(int id, String size) { } public ProductResponse setProductImage(int id, MultipartFile file) { - // TODO: Implement - // call ProductRepository.setImageUrl() afterwards with the new URL - throw new UnsupportedOperationException("Not implemented yet"); + String imageUrl = fileService.upload(file); // Uploading the file and getting back the public URL string + productRepository.setImageUrl(id, imageUrl); // Save the URL to the database image_url column + return ProductResponse.from(productRepository.findById(id)); } public ProductResponse deleteProductImage(int id) { - // TODO: Implement - // call ProductRepository.setImageUrl() to set the image url to null - throw new UnsupportedOperationException("Not implemented yet"); + Product product = productRepository.findById(id); + // check if an image exists + if (product != null && product.getImageUrl() != null) { + fileService.delete(product.getImageUrl()); + } + productRepository.setImageUrl(id, null); // Update the database image_url column to null + return ProductResponse.from(productRepository.findById(id)); } } diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/dto/ProductResponse.java b/src/main/java/net/hackyourfuture/hyfshop/product/dto/ProductResponse.java index 0c35ef1..2e4a45b 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/dto/ProductResponse.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/dto/ProductResponse.java @@ -3,13 +3,15 @@ import net.hackyourfuture.hyfshop.product.Product; import java.math.BigDecimal; +import java.util.Map; public record ProductResponse ( int id, String title, BigDecimal price, String category, - String imageUrl + String imageUrl, + Map details ){ public static ProductResponse from(Product product) { return new ProductResponse( @@ -17,7 +19,8 @@ public static ProductResponse from(Product product) { product.getTitle(), product.getPrice(), product.getCategory(), - product.getImageUrl() + product.getImageUrl(), + product.getDetails() ); } }