diff --git a/codiki-application/src/main/java/org/codiki/application/picture/PictureUseCases.java b/codiki-application/src/main/java/org/codiki/application/picture/PictureUseCases.java index 71c2d0f..7ff59be 100644 --- a/codiki-application/src/main/java/org/codiki/application/picture/PictureUseCases.java +++ b/codiki-application/src/main/java/org/codiki/application/picture/PictureUseCases.java @@ -4,6 +4,7 @@ import java.io.File; import java.util.UUID; import static org.codiki.domain.picture.model.builder.PictureBuilder.aPicture; +import org.codiki.domain.picture.exception.PictureNotFoundException; import org.codiki.domain.picture.model.Picture; import org.codiki.domain.picture.port.PicturePort; import org.springframework.stereotype.Service; @@ -30,4 +31,9 @@ public class PictureUseCases { public void deletePicture(UUID pictureId) { picturePort.deleteById(pictureId); } + + public Picture findById(UUID pictureId) { + return picturePort.findById(pictureId) + .orElseThrow(() -> new PictureNotFoundException(pictureId)); + } } diff --git a/codiki-domain/src/main/java/org/codiki/domain/picture/exception/PictureNotFoundException.java b/codiki-domain/src/main/java/org/codiki/domain/picture/exception/PictureNotFoundException.java new file mode 100644 index 0000000..cca099f --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/picture/exception/PictureNotFoundException.java @@ -0,0 +1,11 @@ +package org.codiki.domain.picture.exception; + +import java.util.UUID; + +import org.codiki.domain.exception.FunctionnalException; + +public class PictureNotFoundException extends FunctionnalException { + public PictureNotFoundException(UUID pictureId) { + super(String.format("Picture with id %s is not found.", pictureId)); + } +} diff --git a/codiki-domain/src/main/java/org/codiki/domain/picture/exception/PictureUploadException.java b/codiki-domain/src/main/java/org/codiki/domain/picture/exception/PictureUploadException.java new file mode 100644 index 0000000..bdeab72 --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/picture/exception/PictureUploadException.java @@ -0,0 +1,9 @@ +package org.codiki.domain.picture.exception; + +import org.codiki.domain.exception.FunctionnalException; + +public class PictureUploadException extends FunctionnalException { + public PictureUploadException(String message) { + super(message); + } +} diff --git a/codiki-exposition/pom.xml b/codiki-exposition/pom.xml index 95b3ca0..392fa00 100644 --- a/codiki-exposition/pom.xml +++ b/codiki-exposition/pom.xml @@ -29,6 +29,10 @@ org.projectlombok lombok + + org.apache.tika + tika-core + diff --git a/codiki-exposition/src/main/java/org/codiki/exposition/configuration/GlobalControllerExceptionHandler.java b/codiki-exposition/src/main/java/org/codiki/exposition/configuration/GlobalControllerExceptionHandler.java index 2023382..fd04a1c 100644 --- a/codiki-exposition/src/main/java/org/codiki/exposition/configuration/GlobalControllerExceptionHandler.java +++ b/codiki-exposition/src/main/java/org/codiki/exposition/configuration/GlobalControllerExceptionHandler.java @@ -2,6 +2,7 @@ package org.codiki.exposition.configuration; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.FORBIDDEN; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; import static org.springframework.http.HttpStatus.NOT_FOUND; import static org.springframework.http.HttpStatus.UNAUTHORIZED; import org.codiki.domain.category.exception.CategoryDeletionException; @@ -11,6 +12,8 @@ import org.codiki.domain.exception.LoginFailureException; import org.codiki.domain.exception.RefreshTokenDoesNotExistException; import org.codiki.domain.exception.RefreshTokenExpiredException; import org.codiki.domain.exception.UserDoesNotExistException; +import org.codiki.domain.picture.exception.PictureNotFoundException; +import org.codiki.domain.picture.exception.PictureUploadException; import org.codiki.domain.publication.exception.PublicationEditionException; import org.codiki.domain.publication.exception.PublicationNotFoundException; import org.codiki.domain.publication.exception.PublicationUpdateForbiddenException; @@ -80,4 +83,16 @@ public class GlobalControllerExceptionHandler { public void handleCategoryDeletionException() { // Do nothing. } + + @ResponseStatus(BAD_REQUEST) + @ExceptionHandler(PictureUploadException.class) + public void handlePictureUploadException() { + // Do nothing. + } + + @ResponseStatus(NOT_FOUND) + @ExceptionHandler(PictureNotFoundException.class) + public void handlePictureNotFoundException() { + // Do nothing. + } } diff --git a/codiki-exposition/src/main/java/org/codiki/exposition/configuration/security/SecurityConfiguration.java b/codiki-exposition/src/main/java/org/codiki/exposition/configuration/security/SecurityConfiguration.java index f2bcbff..72f9322 100644 --- a/codiki-exposition/src/main/java/org/codiki/exposition/configuration/security/SecurityConfiguration.java +++ b/codiki-exposition/src/main/java/org/codiki/exposition/configuration/security/SecurityConfiguration.java @@ -44,6 +44,7 @@ public class SecurityConfiguration { GET, "/api/health/check", "/api/categories", + "/api/pictures/{pictureId}", "/error" ).permitAll() .requestMatchers( diff --git a/codiki-exposition/src/main/java/org/codiki/exposition/picture/MultipartFileConverter.java b/codiki-exposition/src/main/java/org/codiki/exposition/picture/MultipartFileConverter.java index 900bf46..a454654 100644 --- a/codiki-exposition/src/main/java/org/codiki/exposition/picture/MultipartFileConverter.java +++ b/codiki-exposition/src/main/java/org/codiki/exposition/picture/MultipartFileConverter.java @@ -1,23 +1,44 @@ package org.codiki.exposition.picture; +import static java.util.Objects.isNull; import java.io.File; import java.io.IOException; +import java.util.List; import java.util.UUID; +import org.apache.tika.mime.MimeType; +import org.apache.tika.mime.MimeTypeException; +import org.apache.tika.mime.MimeTypes; +import org.codiki.domain.picture.exception.PictureUploadException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; @Component public class MultipartFileConverter { - private final String tempPicturesForlderPath; + private static final List ALLOWED_MIME_TYPES; - public MultipartFileConverter(@Value("${application.pictures.temp-path}") String tempPicturesForlderPath) { - this.tempPicturesForlderPath = tempPicturesForlderPath; + static { + MimeTypes mimeTypes = MimeTypes.getDefaultMimeTypes(); + + try { + ALLOWED_MIME_TYPES = List.of( + mimeTypes.forName("image/png"), + mimeTypes.forName("image/jpeg") + ); + } catch (MimeTypeException exception) { + throw new RuntimeException("An error occurred while loading allowed mime types.", exception); + } + } + + private final String tempPicturesFolderPath; + + public MultipartFileConverter(@Value("${application.pictures.temp-path}") String tempPicturesFolderPath) { + this.tempPicturesFolderPath = tempPicturesFolderPath; } public File transformToFile(MultipartFile fileContent) { - File pictureFile = new File(String.format("%s/%s", tempPicturesForlderPath, UUID.randomUUID())); + File pictureFile = new File(buildPicturePath(fileContent)); try { fileContent.transferTo(pictureFile); } catch (IOException e) { @@ -25,4 +46,34 @@ public class MultipartFileConverter { } return pictureFile; } + + private String buildPicturePath(MultipartFile fileContent) { + MimeType fileContentType = extractMimeType(fileContent); + return String.format( + "%s/%s%s", + tempPicturesFolderPath, + UUID.randomUUID(), + fileContentType.getExtension() + ); + } + + private MimeType extractMimeType(MultipartFile fileContent) { + MimeType result = null; + try { + result = MimeTypes.getDefaultMimeTypes() + .forName(fileContent.getContentType()); + } catch (MimeTypeException exception) { + // Do nothing + } + + if (isNull(result) || !isAllowedMimeType(result)) { + throw new PictureUploadException("Unable to upload the picture because its format is incorrect."); + } + + return result; + } + + private boolean isAllowedMimeType(MimeType mimeType) { + return ALLOWED_MIME_TYPES.contains(mimeType); + } } diff --git a/codiki-exposition/src/main/java/org/codiki/exposition/picture/PictureController.java b/codiki-exposition/src/main/java/org/codiki/exposition/picture/PictureController.java index 3cb07e0..9e71271 100644 --- a/codiki-exposition/src/main/java/org/codiki/exposition/picture/PictureController.java +++ b/codiki-exposition/src/main/java/org/codiki/exposition/picture/PictureController.java @@ -3,9 +3,13 @@ package org.codiki.exposition.picture; import java.io.File; import java.util.UUID; +import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE; import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; import org.codiki.application.picture.PictureUseCases; import org.codiki.domain.picture.model.Picture; +import org.springframework.core.io.FileSystemResource; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -32,4 +36,10 @@ public class PictureController { Picture newPicture = pictureUseCases.createPicture(pictureFile); return newPicture.id(); } + + @GetMapping(value = "/{pictureId}", produces = APPLICATION_OCTET_STREAM_VALUE) + public FileSystemResource loadPicture(@PathVariable("pictureId") UUID pictureId) { + Picture picture = pictureUseCases.findById(pictureId); + return new FileSystemResource(picture.contentFile()); + } } diff --git a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/picture/PictureJpaAdapter.java b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/picture/PictureJpaAdapter.java index 0407dc8..213e391 100644 --- a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/picture/PictureJpaAdapter.java +++ b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/picture/PictureJpaAdapter.java @@ -4,6 +4,8 @@ import java.io.File; import java.util.Optional; import java.util.UUID; +import static org.codiki.domain.picture.model.builder.PictureBuilder.aPicture; +import org.codiki.domain.picture.exception.PictureNotFoundException; import org.codiki.domain.picture.exception.PictureStorageErrorException; import org.codiki.domain.picture.model.Picture; import org.codiki.domain.picture.port.PicturePort; @@ -35,7 +37,17 @@ public class PictureJpaAdapter implements PicturePort { @Override public Optional findById(UUID pictureId) { return repository.findById(pictureId) - .map(PictureEntity::toDomain); + .map(PictureEntity::toDomain) + .map(picture -> { + File pictureFile = new File(String.format("%s/%s", pictureFolderPath, pictureId)); + if (!pictureFile.exists()) { + throw new PictureNotFoundException(pictureId); + } + return aPicture() + .basedOn(picture) + .withContentFile(pictureFile) + .build(); + }); } @Override diff --git a/pom.xml b/pom.xml index 3447375..2c4aeda 100644 --- a/pom.xml +++ b/pom.xml @@ -18,6 +18,7 @@ 6.0.0 4.4.0 42.7.0 + 2.9.0 @@ -72,6 +73,11 @@ postgresql ${postgresql.version} + + org.apache.tika + tika-core + ${tika-core.version} + diff --git a/rest-client-collection/Codiki/environments/localhost.bru b/rest-client-collection/Codiki/environments/localhost.bru index 9e658b6..da6c63f 100644 --- a/rest-client-collection/Codiki/environments/localhost.bru +++ b/rest-client-collection/Codiki/environments/localhost.bru @@ -1,6 +1,7 @@ vars { url: http://localhost:8080 publicationId: fce1de27-11c6-4deb-a248-b63288c00037 - bearerToken: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YWQ0NjJiOC04ZjllLTRhMjYtYmI4Ni1jNzRmZWY1ZDExYjYiLCJleHAiOjE3MTAzMzc2Njd9.ExV8xDeqqKk5WjIVb16NBqF1gPoRqx7uL4jQIhWjjY0QVhB5EAGdHMIbLr4s9Ck2f6z8U4sRlpPAQquDOr_9NA + bearerToken: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YWQ0NjJiOC04ZjllLTRhMjYtYmI4Ni1jNzRmZWY1ZDExYjYiLCJleHAiOjE3MTA0MDYxMzF9.FhAT0my_DfKKTcgpWA3cesv8WYNw36dV6O1ZYrNtW0NR3E9AQ_XP0hAw_GH1K4maMxIzToqzNrZVJ-ug-cIaCQ categoryId: 172fa901-3f4b-4540-92f3-1c15820e8ec9 + pictureId: 65b660b7-66bb-4e4a-a62c-fd0ca101f972 }