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
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.putObject(imageFile);
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,38 @@
package dev.blaauwendraad.recipe_book.service;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.io.File;
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.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
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 void putObject(File file) {
PutObjectRequest request = PutObjectRequest.builder()
.bucket(BUCKET_NAME)
.key(file.getName())
.contentType("image/jpeg")
.build();
s3Client.putObject(request, RequestBody.fromFile(file));
}

public byte[] getObject(String objectKey) {
GetObjectRequest getObjectRequest =
GetObjectRequest.builder().bucket(BUCKET_NAME).key(objectKey).build();
ResponseBytes<GetObjectResponse> objectBytes = s3Client.getObjectAsBytes(getObjectRequest);
return objectBytes.asByteArray();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ 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));
Expand All @@ -77,6 +78,7 @@ public Recipe getRecipeById(Long recipeId) {
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 @@ -92,6 +94,7 @@ public Recipe getRecipeById(Long recipeId) {
public Long createRecipe(
String title,
@Nullable String description,
@Nullable String imageName,
Integer numServings,
PreparationTime preparationTime,
Long userId,
Expand All @@ -106,6 +109,7 @@ public Long createRecipe(
userAccountEntity,
title,
description,
imageName,
numServings,
preparationTime,
ingredients,
Expand Down Expand Up @@ -134,6 +138,7 @@ public void updateRecipe(
Long recipeId,
String title,
@Nullable String description,
@Nullable String imageName,
Integer numServings,
PreparationTime preparationTime,
Long userId,
Expand All @@ -156,6 +161,7 @@ public void updateRecipe(
existingRecipeEntity.author,
title,
description,
imageName,
numServings,
preparationTime,
ingredients,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public record Recipe(
Long id,
String title,
@Nullable String description,
@Nullable String imageName,
Integer numServings,
PreparationTime preparationTime,
@Nullable Author author,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public record RecipeSummary(
Long id,
String title,
@Nullable String description,
@Nullable String imageName,
Integer numServings,
PreparationTime preparationTime,
@Nullable Author author) {}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.blaauwendraad.recipe_book.web;

import dev.blaauwendraad.recipe_book.service.ImageService;
import dev.blaauwendraad.recipe_book.service.RecipeService;
import dev.blaauwendraad.recipe_book.service.model.Ingredient;
import dev.blaauwendraad.recipe_book.service.model.PreparationStep;
Expand Down Expand Up @@ -38,13 +39,15 @@
@Path("/api/recipes")
public class RecipeResource {
private final RecipeService recipeService;
private final ImageService imageService;

@Inject
JsonWebToken jwt;

@Inject
public RecipeResource(RecipeService recipeService) {
public RecipeResource(RecipeService recipeService, ImageService imageService) {
this.recipeService = recipeService;
this.imageService = imageService;
}

@GET
Expand All @@ -69,6 +72,7 @@ public RecipeSummariesResponse getRecipeSummaries(@PathParam("filter") RecipeSum
recipeSummary.id(),
recipeSummary.title(),
recipeSummary.description(),
recipeSummary.imageName(),
recipeSummary.numServings(),
recipeSummary.preparationTime(),
recipeSummary.author() == null
Expand All @@ -93,6 +97,7 @@ public RecipeResponse getRecipe(@PathParam("recipeId") Long id) {
recipe.id(),
recipe.title(),
recipe.description(),
recipe.imageName(),
recipe.numServings(),
recipe.preparationTime(),
recipe.author() == null
Expand All @@ -107,6 +112,25 @@ public RecipeResponse getRecipe(@PathParam("recipeId") Long id) {
.toList()));
}

@GET
@Path("/{recipeId}/image")
@PermitAll
@Produces("image/jpeg")
public Response getRecipeImage(@PathParam("recipeId") Long id) {
Recipe recipe = recipeService.getRecipeById(id);
if (recipe == null) {
throw new NotFoundException("Recipe not found with recipeId: " + id);
}
if (recipe.imageName() == null) {
throw new NotFoundException("Recipe with recipeId: " + id + " has no image");
}
byte[] imageFile = imageService.getObject(recipe.imageName());
if (imageFile == null) {
throw new NotFoundException("Image not found for recipeId: " + id);
}
return Response.ok(imageFile).build();
}

@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
Expand All @@ -115,6 +139,7 @@ public SaveRecipeResponseDto createRecipe(@NotNull @Valid SaveRecipeRequestDto n
Long recipeId = recipeService.createRecipe(
newRecipe.title(),
newRecipe.description(),
newRecipe.imageName(),
newRecipe.numServings(),
newRecipe.preparationTime(),
Long.valueOf(jwt.getName()),
Expand All @@ -138,6 +163,7 @@ public SaveRecipeResponseDto updateRecipe(
recipeId,
updatedRecipe.title(),
updatedRecipe.description(),
updatedRecipe.imageName(),
updatedRecipe.numServings(),
updatedRecipe.preparationTime(),
Long.valueOf(jwt.getName()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public record RecipeDto(
Long id,
String title,
@Nullable String description,
@Nullable String imageName,
Integer numServings,
PreparationTime preparationTime,
@Nullable RecipeAuthorDto author,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public record RecipeSummaryDto(
Long id,
String title,
@Nullable String description,
@Nullable String imageName,
Integer numServings,
PreparationTime preparationTime,
@Nullable RecipeAuthorDto author) {}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
public record SaveRecipeRequestDto(
@NotBlank @Size(min = 5, max = 100) String title,
@Nullable @Size(max = 2000) String description,
@Nullable @Size(max = 255) String imageName,
@NotNull @Positive @Max(100) Integer numServings,
@NotNull PreparationTime preparationTime,
@Size(min = 1, max = 50) List<@Valid IngredientDto> ingredients,
Expand Down
11 changes: 10 additions & 1 deletion app/src/main/resources/application-dev.properties
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,13 @@ quarkus.flyway.locations=db/migration
quarkus.http.cors.origins=*

mp.jwt.verify.publickey.location=classpath:local-dev-keys/publicKey.pem
smallrye.jwt.sign.key.location=classpath:local-dev-keys/privateKey.pem
smallrye.jwt.sign.key.location=classpath:local-dev-keys/privateKey.pem

# S3 configuration for Garage for local development
quarkus.s3.endpoint-override=http://localhost:3900
quarkus.s3.path-style-access=true
quarkus.s3.chunked-encoding=false
quarkus.s3.aws.region=garage
quarkus.s3.aws.credentials.type=static
quarkus.s3.aws.credentials.static-provider.access-key-id=${S3_ACCESS_KEY_ID}
quarkus.s3.aws.credentials.static-provider.secret-access-key=${S3_ACCESS_KEY_SECRET}
26 changes: 13 additions & 13 deletions app/src/main/resources/db/dev-data/V2.2__insert_recipes.sql
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
-- Insert demo recipes
INSERT INTO recipe (title, description, num_servings, preparation_time, author_id)
SELECT title, description, num_servings, preparation_time, ua.id
FROM (VALUES ('Spaghetti Bolognese', 'Classic Italian pasta dish.', 4, 'MIN_0_15', 'Robert'),
('Chicken Curry', 'Spicy Indian-style curry with tender chicken.', 4, 'MIN_0_15', 'Breus'),
('Caesar Salad', 'Classic romaine lettuce salad with creamy dressing.', 4, 'MIN_15_30', 'ChefMaster'),
('Chocolate Chip Cookies', 'Soft and chewy homemade cookies.', 4, 'MIN_15_30', 'FoodLover'),
('Mushroom Risotto', 'Creamy Italian rice dish with mushrooms.', 4, 'MIN_30_45', 'Breus'),
('Fish Tacos', 'Mexican-style fish tacos with fresh salsa.', 4, 'MIN_30_45', 'ChefMaster'),
('Greek Salad', 'Traditional Mediterranean salad with feta cheese.', 4, 'HOUR_PLUS', 'FoodLover'),
('Beef Stir Fry', 'Quick and easy Asian-style beef dish.', 4, 'HOUR_PLUS', 'Robert'),
('Banana Bread', 'Moist and delicious homemade bread.', 4, 'MIN_15_30', NULL),
('Vegetable Soup', 'Healthy and warming soup with mixed vegetables.', 4, 'MIN_15_30',
NULL)) AS recipes(title, description, num_servings, preparation_time, username)
INSERT INTO recipe (title, description, image_name, num_servings, preparation_time, author_id)
SELECT title, description, image_name, num_servings, preparation_time, ua.id
FROM (VALUES ('Spaghetti Bolognese', 'Classic Italian pasta dish.', 'spaghetti.jpg', 4, 'MIN_0_15', 'Robert'),
('Chicken Curry', 'Spicy Indian-style curry with tender chicken.', null, 4, 'MIN_0_15', 'Breus'),
('Caesar Salad', 'Classic romaine lettuce salad with creamy dressing.', null, 4, 'MIN_15_30', 'ChefMaster'),
('Chocolate Chip Cookies', 'Soft and chewy homemade cookies.', null, 4, 'MIN_15_30', 'FoodLover'),
('Mushroom Risotto', 'Creamy Italian rice dish with mushrooms.', null, 4, 'MIN_30_45', 'Breus'),
('Fish Tacos', 'Mexican-style fish tacos with fresh salsa.', null, 4, 'MIN_30_45', 'ChefMaster'),
('Greek Salad', 'Traditional Mediterranean salad with feta cheese.', null, 4, 'HOUR_PLUS', 'FoodLover'),
('Beef Stir Fry', 'Quick and easy Asian-style beef dish.', null, 4, 'HOUR_PLUS', 'Robert'),
('Banana Bread', 'Moist and delicious homemade bread.', null, 4, 'MIN_15_30', NULL),
('Vegetable Soup', 'Healthy and warming soup with mixed vegetables.', null, 4, 'MIN_15_30',
NULL)) AS recipes(title, description, image_name, num_servings, preparation_time, username)
LEFT JOIN user_account ua ON ua.username = recipes.username;
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ CREATE TABLE recipe
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
image_name VARCHAR(255),
num_servings INTEGER NOT NULL,
preparation_time VARCHAR CHECK (preparation_time IN
('MIN_0_15', 'MIN_15_30', 'MIN_30_45', 'MIN_45_60', 'HOUR_PLUS')),
Expand Down
Loading