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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
HELP.md
.env
target/
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
Expand Down
12 changes: 11 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,17 @@
<artifactId>jackson-core</artifactId>
<version>3.2.0</version>
</dependency>
</dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.21.2</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.25.15</version>
</dependency>
</dependencies>

<build>
<plugins>
Expand Down
66 changes: 66 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,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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracting that URL into a constant (e.g. BASE_STORAGE_URL) helps because:

Single source of truth — if the bucket or region changes, you update one place, not hunt through 15 files
Readability — BASE_STORAGE_URL + path is clearer than a raw URL blob
Typo prevention — one misspelled character in a pasted URL is hard to spot; a misspelled constant name throws an error immediately

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh ! good to know that, thank you so much 🙏

}

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());
}
}
}
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 @@ -14,6 +15,7 @@
public class Product {
private int id;
private String title;
private Map<String, Object> details;
private BigDecimal price;
private String category;
private String imageUrl;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,20 +21,30 @@ 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<Map<String, Object>>() {}));
}
} catch (Exception e) {
throw new RuntimeException(e);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logging the error would be helpful here

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is logging better than throwing exception error ?

}
return product;
};

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

}

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();
Expand All @@ -49,12 +62,18 @@ public void setImageUrl(int id, String imageUrl) {
}

public List<Product> 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);
}
}
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 @@ -26,14 +27,18 @@ public ProductResponse setProductSize(int id, String size) {
}

public ProductResponse setProductImage(int id, MultipartFile file) {

@dilero dilero Jul 7, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens if the file is null? No exception handling exist in this method.

// 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));
}
}
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()
);
}
}