Skip to content
Draft
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
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Created by https://www.toptal.com/developers/gitignore/api/osx,vim,node,patch,vuejs,gradle,intellij,certificates,java
# Edit at https://www.toptal.com/developers/gitignore?templates=osx,vim,node,patch,vuejs,gradle,intellij,certificates,java
# Created by https://www.toptal.com/developers/gitignore/api/osx,vim,node,patch,vuejs,gradle,intellij,visualstudiocode,certificates,java
# Edit at https://www.toptal.com/developers/gitignore?templates=osx,vim,node,patch,vuejs,gradle,intellij,visualstudiocode,certificates,java

### Frontebd build artifcats ###
app/src/main/resources/META-INF/resources
Expand Down Expand Up @@ -398,6 +398,9 @@ gradle-app.setting
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
.factorypath
.settings/
**/bin/

# Java heap dump
*.hprof
Expand Down
4 changes: 3 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ val quarkusPlatformVersion: String by project
dependencies {
errorprone(libs.errorprone.core)
errorprone(libs.nullaway)

implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))

implementation(libs.quarkus.arc)
Expand All @@ -33,6 +33,8 @@ dependencies {
implementation(libs.quarkus.rest)
implementation(libs.quarkus.rest.jackson)
implementation(libs.quarkus.smallrye.jwt)
implementation("io.quarkiverse.amazonservices:quarkus-amazon-s3:3.12.1")
implementation("software.amazon.awssdk:url-connection-client:2.40.13")

testImplementation(libs.assertj.core)
testImplementation(libs.mockito.junit)
Expand Down
Binary file added app/data/images/spaghetti.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
package dev.blaauwendraad.recipe_book.config;

import dev.blaauwendraad.recipe_book.service.ImageService;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.io.File;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.flywaydb.core.Flyway;
import org.jboss.logging.Logger;

@ApplicationScoped
public final class DemoDataSyncService {
private static final Logger log = Logger.getLogger(DemoDataSyncService.class);
private ImageService imageService;

private final String jdbcUrl;
private final String username;
private final String password;

@Inject
public DemoDataSyncService(
@ConfigProperty(name = "quarkus.datasource.jdbc.url") String jdbcUrl,
@ConfigProperty(name = "quarkus.datasource.username") String username,
@ConfigProperty(name = "quarkus.datasource.password") String password) {
@ConfigProperty(name = "quarkus.datasource.password") String password,
ImageService imageService) {
this.jdbcUrl = jdbcUrl;
this.username = username;
this.password = password;
this.imageService = imageService;
}

@Transactional
Expand All @@ -46,4 +53,10 @@ void insertDemoData() {
throw new RuntimeException("Demo data migration failed", e);
}
}

void insertDemoImage() {
File imageFile = new File("data/images/spaghetti.jpg");
imageService.putImage(imageFile, "spaghetti.jpg");
log.info("Inserted demo image into Garage/S3");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public void syncReferenceData(@Observes StartupEvent startupEvent) {
validateRoles();
if (profile.isPresent() && "dev".equals(profile.get())) {
demoDataSyncService.insertDemoData();
demoDataSyncService.insertDemoImage();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ public class RecipeEntity extends PanacheEntityBase {
@Nullable
public String description;

@Column(name = "image_name")
@Nullable
public String imageName;

@Column(name = "num_servings")
@SuppressWarnings("NullAway.Init")
public Integer numServings;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public Long persistRecipeEntity(
UserAccountEntity userAccountEntity,
String title,
@Nullable String description,
@Nullable String imageName,
Integer numServings,
PreparationTime preparationTime,
List<Ingredient> ingredients,
Expand All @@ -49,6 +50,7 @@ public Long persistRecipeEntity(
var recipeEntity = existingRecipeEntity != null ? existingRecipeEntity : new RecipeEntity();
recipeEntity.title = title;
recipeEntity.description = description;
recipeEntity.imageName = imageName;
recipeEntity.numServings = numServings;
recipeEntity.preparationTime = preparationTime;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package dev.blaauwendraad.recipe_book.service;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.persistence.EntityNotFoundException;
import java.io.File;
import java.util.UUID;
import software.amazon.awssdk.core.ResponseBytes;
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.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

@ApplicationScoped
public class ImageService {
private static final String BUCKET_NAME = "images";
private S3Client s3Client;

@Inject
public ImageService(S3Client s3Client) {
this.s3Client = s3Client;
}

public String putImage(File image) {
String key = image.getName() + "_" + UUID.randomUUID().toString();
putImage(image, key);
return key;
}

public void putImage(File image, String imageKey) {
PutObjectRequest request = PutObjectRequest.builder()
.bucket(BUCKET_NAME)
.key(imageKey)
.contentType("image/jpeg")
.build();
s3Client.putObject(request, RequestBody.fromFile(image));
}

public byte[] getImage(String imageKey) {
try {
GetObjectRequest getObjectRequest =
GetObjectRequest.builder().bucket(BUCKET_NAME).key(imageKey).build();
ResponseBytes<GetObjectResponse> objectBytes = s3Client.getObjectAsBytes(getObjectRequest);
return objectBytes.asByteArray();
} catch (NoSuchKeyException e) {
throw new EntityNotFoundException("Image not found for imageKey: " + imageKey, e);
}
}

public void deleteImage(String imageKey) {
DeleteObjectRequest deleteObjectRequest =
DeleteObjectRequest.builder().bucket(BUCKET_NAME).key(imageKey).build();
s3Client.deleteObject(deleteObjectRequest);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import dev.blaauwendraad.recipe_book.data.model.UserAccountEntity;
import dev.blaauwendraad.recipe_book.repository.RecipeRepository;
import dev.blaauwendraad.recipe_book.repository.UserRepository;
import dev.blaauwendraad.recipe_book.service.exception.EntityNotFoundException;
import dev.blaauwendraad.recipe_book.service.exception.UserAuthenticationException;
import dev.blaauwendraad.recipe_book.service.exception.UserAuthorizationException;
import dev.blaauwendraad.recipe_book.service.model.Author;
import dev.blaauwendraad.recipe_book.service.model.Ingredient;
import dev.blaauwendraad.recipe_book.service.model.PreparationStep;
Expand All @@ -15,18 +18,21 @@
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.io.File;
import java.util.List;
import java.util.stream.Collectors;

@ApplicationScoped
public class RecipeService {
private final UserRepository userRepository;
private final RecipeRepository recipeRepository;
private final ImageService imageService;

@Inject
public RecipeService(UserRepository userRepository, RecipeRepository recipeRepository) {
public RecipeService(UserRepository userRepository, RecipeRepository recipeRepository, ImageService imageService) {
this.userRepository = userRepository;
this.recipeRepository = recipeRepository;
this.imageService = imageService;
}

public List<RecipeSummary> getAllRecipeSummaries(
Expand Down Expand Up @@ -62,21 +68,22 @@ private RecipeSummary toRecipeSummary(RecipeEntity recipeEntity) {
recipeEntity.id,
recipeEntity.title,
recipeEntity.description,
recipeEntity.imageName,
recipeEntity.numServings,
recipeEntity.preparationTime,
recipeEntity.author == null ? null : new Author(recipeEntity.author.id, recipeEntity.author.username));
}

@Nullable
public Recipe getRecipeById(Long recipeId) {
public Recipe getRecipe(Long recipeId) throws EntityNotFoundException {
RecipeEntity recipeEntity = recipeRepository.findById(recipeId);
if (recipeEntity == null) {
return null;
throw new EntityNotFoundException("Recipe with recipeId " + recipeId + " does not exist");
}
return new Recipe(
recipeEntity.id,
recipeEntity.title,
recipeEntity.description,
recipeEntity.imageName,
recipeEntity.numServings,
recipeEntity.preparationTime,
recipeEntity.author == null ? null : new Author(recipeEntity.author.id, recipeEntity.author.username),
Expand All @@ -88,6 +95,18 @@ public Recipe getRecipeById(Long recipeId) {
.collect(Collectors.toList()));
}

public byte[] getRecipeImage(Long recipeId) throws EntityNotFoundException {
Recipe recipe = getRecipe(recipeId);
if (recipe.imageName() == null) {
throw new EntityNotFoundException("Recipe with recipeId: " + recipeId + " has no image");
}
byte[] imageFile = imageService.getImage(recipe.imageName());
if (imageFile == null) {
throw new EntityNotFoundException("Image not found for recipeId: " + recipeId);
}
return imageFile;
}

@Transactional
public Long createRecipe(
String title,
Expand All @@ -96,36 +115,44 @@ public Long createRecipe(
PreparationTime preparationTime,
Long userId,
List<Ingredient> ingredients,
List<PreparationStep> preparationSteps) {
List<PreparationStep> preparationSteps,
@Nullable File recipeImage)
throws UserAuthenticationException {
UserAccountEntity userAccountEntity = userRepository.findById(userId);
if (userAccountEntity == null) {
throw new IllegalArgumentException("Author with userId " + userId + " does not exist");
throw new UserAuthenticationException("User with userId " + userId + " does not exist");
}
String imageName = null;
if (recipeImage != null) {
imageName = imageService.putImage(recipeImage);
}
return recipeRepository.persistRecipeEntity(
null,
userAccountEntity,
title,
description,
imageName,
numServings,
preparationTime,
ingredients,
preparationSteps);
}

@Transactional
public void deleteRecipe(Long recipeId, Long userId) {
public void deleteRecipe(Long recipeId, Long userId)
throws UserAuthorizationException, UserAuthenticationException, EntityNotFoundException {
UserAccountEntity userAccount = userRepository.findById(userId);
if (userAccount == null) {
throw new IllegalArgumentException("User with userId " + userId + " does not exist");
throw new UserAuthenticationException("User with userId " + userId + " does not exist");
}
RecipeEntity recipeEntity = RecipeEntity.findById(recipeId);
if (recipeEntity == null) {
throw new IllegalArgumentException("Recipe with recipeId " + recipeId + " does not exist");
}
if (recipeEntity.author == null || !recipeEntity.author.id.equals(userId)) {
throw new IllegalArgumentException(
Recipe recipe = getRecipe(recipeId);
if (recipe.author() == null || !recipe.author().id().equals(userId)) {
throw new UserAuthorizationException(
"User with userId " + userId + " is not the author of the recipe with recipeId " + recipeId);
}
if (recipe.imageName() != null) {
imageService.deleteImage(recipe.imageName());
}
recipeRepository.deleteById(recipeId);
}

Expand All @@ -138,24 +165,34 @@ public void updateRecipe(
PreparationTime preparationTime,
Long userId,
List<Ingredient> ingredients,
List<PreparationStep> preparationSteps) {
List<PreparationStep> preparationSteps,
@Nullable File recipeImage)
throws UserAuthorizationException, UserAuthenticationException, EntityNotFoundException {
UserAccountEntity userAccount = userRepository.findById(userId);
if (userAccount == null) {
throw new IllegalArgumentException("User with userId " + userId + " does not exist");
throw new UserAuthenticationException("User with userId " + userId + " does not exist");
}
RecipeEntity existingRecipeEntity = RecipeEntity.findById(recipeId);
if (existingRecipeEntity == null) {
throw new IllegalArgumentException("Recipe with recipeId " + recipeId + " does not exist");
throw new EntityNotFoundException("Recipe with recipeId " + recipeId + " does not exist");
}
if (existingRecipeEntity.author == null || !existingRecipeEntity.author.id.equals(userId)) {
throw new IllegalArgumentException(
throw new UserAuthorizationException(
"User with userId " + userId + " is not the author of the recipe with recipeId " + recipeId);
}
if (existingRecipeEntity.imageName != null) {
imageService.deleteImage(existingRecipeEntity.imageName);
}
String imageName = null;
if (recipeImage != null) {
imageName = imageService.putImage(recipeImage);
}
recipeRepository.persistRecipeEntity(
existingRecipeEntity,
existingRecipeEntity.author,
title,
description,
imageName,
numServings,
preparationTime,
ingredients,
Expand Down
Loading