diff --git a/pom.xml b/pom.xml index bbdda38..f1c2606 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,15 @@ jackson-core 3.2.0 + + com.fasterxml.jackson.core + jackson-databind + + + software.amazon.awssdk + s3 + 2.25.0 + diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/AppConfig.java b/src/main/java/net/hackyourfuture/hyfshop/product/AppConfig.java new file mode 100644 index 0000000..d2ad17b --- /dev/null +++ b/src/main/java/net/hackyourfuture/hyfshop/product/AppConfig.java @@ -0,0 +1,14 @@ +package net.hackyourfuture.hyfshop.product; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AppConfig { + + @Bean + public ObjectMapper objectMapper(){ + return new ObjectMapper(); + } +} diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/B2Config.java b/src/main/java/net/hackyourfuture/hyfshop/product/B2Config.java new file mode 100644 index 0000000..71e3d48 --- /dev/null +++ b/src/main/java/net/hackyourfuture/hyfshop/product/B2Config.java @@ -0,0 +1,41 @@ +package net.hackyourfuture.hyfshop.product; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; + +import java.net.URI; + +@Configuration +public class B2Config { + + @Value("${b2.endpoint}") private String endpoint; + @Value("${b2.region}") private String region; + @Value("${b2.access-key}") private String accessKey; + @Value("${b2.secret-key}") private String secretKey; + + @Bean + public S3Client s3Client() { + return S3Client.builder() + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKey, secretKey))) + .endpointOverride(URI.create(endpoint)) + .region(Region.of(region)) + .build(); + } + + @Bean + public S3Presigner s3Presigner() { + return S3Presigner.builder() + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKey, secretKey))) + .endpointOverride(URI.create(endpoint)) + .region(Region.of(region)) + .build(); + } +} \ No newline at end of file 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..d82c6e2 --- /dev/null +++ b/src/main/java/net/hackyourfuture/hyfshop/product/FileService.java @@ -0,0 +1,50 @@ +package net.hackyourfuture.hyfshop.product; + +import org.springframework.beans.factory.annotation.Value; +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 software.amazon.awssdk.services.s3.presigner.S3Presigner; + +import java.util.UUID; + +@Service + +public class FileService { + + private final S3Client s3Client; + private final S3Presigner s3Presigner; + + @Value("${b2.bucket}") + private String bucket; + + @Value("${b2.endpoint}") + private String endpoint; + + public FileService(S3Client s3Client, S3Presigner s3Presigner) { + this.s3Client = s3Client; + this.s3Presigner = s3Presigner; + } + + public String upload(MultipartFile file) throws Exception { + String key = "uploads/" + UUID.randomUUID() + "-" + file.getOriginalFilename(); + s3Client.putObject( + PutObjectRequest.builder() + .bucket(bucket).key(key) + .contentType(file.getContentType()).build(), + RequestBody.fromInputStream(file.getInputStream(), file.getSize()) + ); + return endpoint + "/" + bucket + "/" + key; + } + + public void delete(String imageUrl) { + String key = imageUrl.replace(endpoint + "/" + bucket + "/", ""); + s3Client.deleteObject( + DeleteObjectRequest.builder() + .bucket(bucket).key(key).build() + ); + } +} \ 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..a3108b9 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 @@ -17,4 +18,5 @@ public class Product { private BigDecimal price; private String category; private String imageUrl; + private Map details; } diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/ProductController.java b/src/main/java/net/hackyourfuture/hyfshop/product/ProductController.java index 9e6c162..78feccf 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/ProductController.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/ProductController.java @@ -20,8 +20,13 @@ public List getProducts() { return productService.getAllProducts(); } - @GetMapping("/search") - public List searchProducts(@Nullable @RequestParam("color") String color) { + @GetMapping("/{id}") + public ProductResponse getProductById(@PathVariable int id){ + return productService.getProductById(id); + } + + @GetMapping(params = "color") + public List searchProducts(@Nullable @RequestParam String color) { if (color == null) { return productService.getAllProducts(); } @@ -34,12 +39,13 @@ 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) throws Exception { return productService.setProductImage(id, file); } @DeleteMapping("/{id}/image") public ProductResponse deleteProductImage(@PathVariable int id) { + return productService.deleteProductImage(id); } } diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java index 2766734..3e0f0e4 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java @@ -1,29 +1,46 @@ package net.hackyourfuture.hyfshop.product; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.AllArgsConstructor; +import net.hackyourfuture.hyfshop.product.dto.ProductResponse; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.simple.JdbcClient; import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Map; @Repository @AllArgsConstructor public class ProductRepository { private final JdbcClient jdbcClient; + private final ObjectMapper objectMapper = new ObjectMapper(); - public static final RowMapper PRODUCT_ROW_MAPPER = (rs, _) -> { + public final RowMapper PRODUCT_ROW_MAPPER = (rs, _) -> { var product = new Product(); product.setId(rs.getInt("id")); product.setTitle(rs.getString("title")); 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){ + product.setDetails(objectMapper.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 * FROM products ORDER BY id") .query(PRODUCT_ROW_MAPPER) .list(); @@ -31,30 +48,48 @@ 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 * FROM products WHERE id = :id") .param("id", id) .query(PRODUCT_ROW_MAPPER) .single(); } - public void setImageUrl(int id, String imageUrl) { - jdbcClient.sql(""" + public Product setImageUrl(int id, String imageUrl) { + return jdbcClient.sql(""" UPDATE products SET image_url = :imageUrl WHERE id = :id + RETURNING * """) - .param("id", id) .param("imageUrl", imageUrl) - .update(); + .param("id", id) + .query(PRODUCT_ROW_MAPPER) + .single(); } public List findByColor(String color) { - // TODO: Implement - throw new UnsupportedOperationException("Not implemented yet"); + return jdbcClient.sql(""" + SELECT * FROM products + WHERE details->>'color' = :color + OR details ->'colors' @> :colorJson::jsonb + ORDER BY id + """) + .param("color", color) + .param("colorJson", "\"" + color + "\"") + .query(PRODUCT_ROW_MAPPER) + .list(); } public Product setSize(int id, String size) { - // TODO: Implement - throw new UnsupportedOperationException("Not implemented yet"); + return jdbcClient.sql(""" + UPDATE products + SET details = jsonb_set(details, '{size}', :size::jsonb) + WHERE id = :id + RETURNING * + """) + .param("size", "\"" + size + "\"") + .param("id", id) + .query(PRODUCT_ROW_MAPPER) + .single(); } } diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/ProductService.java b/src/main/java/net/hackyourfuture/hyfshop/product/ProductService.java index 779d3b2..c498e1e 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(); @@ -20,20 +21,26 @@ public List searchProducts(String color) { return productRepository.findByColor(color).stream().map(ProductResponse::from).toList(); } - public ProductResponse setProductSize(int id, String size) { - productRepository.setSize(id, size); + public ProductResponse getProductById(int id) { return ProductResponse.from(productRepository.findById(id)); } - public ProductResponse setProductImage(int id, MultipartFile file) { - // TODO: Implement - // call ProductRepository.setImageUrl() afterwards with the new URL - throw new UnsupportedOperationException("Not implemented yet"); + public ProductResponse setProductSize(int id, String size) { + return ProductResponse.from(productRepository.setSize(id, size)); + } + + public ProductResponse setProductImage(int id, MultipartFile file) throws Exception { + String url = fileService.upload(file); + return ProductResponse.from(productRepository.setImageUrl(id, url)); } 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); + + if(product.getImageUrl() != null) { + fileService.delete(product.getImageUrl()); + } + + return ProductResponse.from(productRepository.setImageUrl(id, null)); } } 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() ); } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 05b7788..4df60e7 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -5,5 +5,11 @@ spring: url: '${DB_URL}' username: '${DB_USERNAME}' password: '${DB_PASSWORD}' +b2: + endpoint: https://s3.us-west-004.backblazeb2.com + region: us-west-004 + access-key: '${BACKBLAZE_KEY_ID}' + secret-key: '${BACKBLAZE_APPLICATION_KEY}' + bucket: '${BACKBLAZE_BUCKET_NAME}' server: port: '8080'