Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@
<artifactId>jackson-core</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.25.0</version>
</dependency>
</dependencies>

<build>
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/net/hackyourfuture/hyfshop/product/AppConfig.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
41 changes: 41 additions & 0 deletions src/main/java/net/hackyourfuture/hyfshop/product/B2Config.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
50 changes: 50 additions & 0 deletions src/main/java/net/hackyourfuture/hyfshop/product/FileService.java
Original file line number Diff line number Diff line change
@@ -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()
);
}
}
2 changes: 2 additions & 0 deletions src/main/java/net/hackyourfuture/hyfshop/product/Product.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import lombok.Setter;

import java.math.BigDecimal;
import java.util.Map;

@AllArgsConstructor
@NoArgsConstructor
Expand All @@ -17,4 +18,5 @@ public class Product {
private BigDecimal price;
private String category;
private String imageUrl;
private Map<String, Object> details;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@ public List<ProductResponse> getProducts() {
return productService.getAllProducts();
}

@GetMapping("/search")
public List<ProductResponse> searchProducts(@Nullable @RequestParam("color") String color) {
@GetMapping("/{id}")
public ProductResponse getProductById(@PathVariable int id){
return productService.getProductById(id);
}

@GetMapping(params = "color")
public List<ProductResponse> searchProducts(@Nullable @RequestParam String color) {
if (color == null) {
return productService.getAllProducts();
}
Expand All @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,60 +1,95 @@
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> PRODUCT_ROW_MAPPER = (rs, _) -> {
public final RowMapper<Product> 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<Map<String, Object>>() {
}));
}
}catch(Exception e){
throw new RuntimeException(e);
}

return product;
};

public List<Product> 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();

}

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<Product> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
private final FileService fileService;

public List<ProductResponse> getAllProducts() {
return productRepository.getAllProducts().stream().map(ProductResponse::from).toList();
Expand All @@ -20,20 +21,26 @@ public List<ProductResponse> 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@
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<String, Object> details
){
public static ProductResponse from(Product product) {
return new ProductResponse(
product.getId(),
product.getTitle(),
product.getPrice(),
product.getCategory(),
product.getImageUrl()
product.getImageUrl(),
product.getDetails()
);
}
}
6 changes: 6 additions & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'