From ced46dd83d2754746b0142c6a4ddc97aa89313a6 Mon Sep 17 00:00:00 2001 From: FyloZ Date: Fri, 30 Apr 2021 18:37:24 -0400 Subject: [PATCH 1/2] =?UTF-8?q?Ajout=20du=20support=20pour=20la=20g=C3=A9n?= =?UTF-8?q?=C3=A9ration=20de=20PDF=20de=20kit=20de=20retouche.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/files/TouchUpKitService.java | 42 ------ .../utils/PdfBuilder.java | 130 ------------------ .../config/properties/CreProperties.kt | 1 + .../rest/files/TouchUpKitController.kt | 24 ++++ .../service/InventoryService.kt | 2 +- .../service/MixMaterialService.kt | 4 +- .../service/MixService.kt | 2 +- .../service/RecipeService.kt | 6 +- .../service/RecipeStepService.kt | 4 +- .../service/files/FileService.kt | 43 ++++-- .../service/files/TouchUpKitService.kt | 78 +++++++++++ .../{service => }/utils/Collections.kt | 2 +- .../fyloz/colorrecipesexplorer/utils/Pdf.kt | 125 +++++++++++++++++ src/main/resources/application.properties | 1 + .../service/RecipeServiceTest.kt | 3 +- .../service/files/TouchUpKitServiceTest.kt | 120 ++++++++++++++++ 16 files changed, 389 insertions(+), 198 deletions(-) delete mode 100644 src/main/java/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitService.java delete mode 100644 src/main/java/dev/fyloz/colorrecipesexplorer/utils/PdfBuilder.java create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/TouchUpKitController.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitService.kt rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service => }/utils/Collections.kt (96%) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Pdf.kt create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt diff --git a/src/main/java/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitService.java b/src/main/java/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitService.java deleted file mode 100644 index 93df0aa..0000000 --- a/src/main/java/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitService.java +++ /dev/null @@ -1,42 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.service.files; - -import dev.fyloz.colorrecipesexplorer.utils.PdfBuilder; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.ResourceLoader; -import org.springframework.stereotype.Service; - -import java.io.IOException; - -@Service -public class TouchUpKitService { - - private static final String TOUCH_UP_FR = "KIT DE RETOUCHE"; - private static final String TOUCH_UP_EN = "TOUCH UP KIT"; - public static final int FONT_SIZE = 42; - - private final ResourceLoader resourceLoader; - - @Autowired - public TouchUpKitService(ResourceLoader resourceLoader) { - this.resourceLoader = resourceLoader; - } - - /** - * Génère un PDF de kit de retouche pour une job. - * - * @param jobNumber La job - * @return Le PDF de kit de retouche pour la job - */ - public byte[] generatePdfForJobNumber(String jobNumber) { - try { - return new PdfBuilder(resourceLoader, true, FONT_SIZE) - .addLine(TOUCH_UP_FR, true, 0) - .addLine(TOUCH_UP_EN, true, 0) - .addLine(jobNumber, false, 10) - .build(); - } catch (IOException ex) { - throw new RuntimeException(String.format("Impossible de générer un PDF de kit de retouche pour la job '%s': %s", jobNumber, ex.getMessage())); - } - } - -} diff --git a/src/main/java/dev/fyloz/colorrecipesexplorer/utils/PdfBuilder.java b/src/main/java/dev/fyloz/colorrecipesexplorer/utils/PdfBuilder.java deleted file mode 100644 index 39ea4ec..0000000 --- a/src/main/java/dev/fyloz/colorrecipesexplorer/utils/PdfBuilder.java +++ /dev/null @@ -1,130 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.utils; - -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.PDPageContentStream; -import org.apache.pdfbox.pdmodel.font.PDFont; -import org.apache.pdfbox.pdmodel.font.PDType0Font; -import org.springframework.core.io.ResourceLoader; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; - -public class PdfBuilder { - - private static final String PATH_FONT_ARIAL_BOLD = "classpath:fonts/arialbd.ttf"; - - private final PDFont font; - private final PDDocument document = new PDDocument(); - private final PDPage page = new PDPage(); - private final Collection lines = new ArrayList<>(); - private final boolean duplicated; - private final int fontSize; - private final int fontSizeBold; - private final int lineSpacing; - - public PdfBuilder(ResourceLoader resourceLoader, boolean duplicated, int fontSize) throws IOException { - this.duplicated = duplicated; - this.fontSize = fontSize; - this.fontSizeBold = this.fontSize + 12; - this.lineSpacing = (int) (this.fontSize * 1.5f); - - document.addPage(page); - font = PDType0Font.load(document, resourceLoader.getResource(PATH_FONT_ARIAL_BOLD).getInputStream()); - } - - public PdfBuilder addLine(String text, boolean bold, int marginTop) { - lines.add(new PdfLine(text, bold, marginTop)); - - return this; - } - - public byte[] build() throws IOException { - writeContent(); - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - document.save(outputStream); - document.close(); - - return outputStream.toByteArray(); - } - - private void writeContent() throws IOException { - PDPageContentStream contentStream = new PDPageContentStream(document, page); - contentStream.beginText(); - - int marginTop = 30; - for (PdfLine line : lines) { - writeCenteredText(contentStream, line, marginTop); - marginTop += lineSpacing; - } - - if (duplicated) { - marginTop = (int) page.getMediaBox().getHeight() / 2; - for (PdfLine line : lines) { - writeCenteredText(contentStream, line, marginTop); - marginTop += lineSpacing; - } - } - - contentStream.endText(); - contentStream.close(); - } - - private void writeCenteredText(PDPageContentStream contentStream, PdfLine line, int marginTop) throws IOException { - float textWidth = font.getStringWidth(line.getText()) / 1000 * (line.isBold() ? fontSizeBold : fontSize); - float textHeight = font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * (line.isBold() ? fontSizeBold : fontSize); - float textX = (page.getMediaBox().getWidth() - textWidth) / 2f; - float textY = (page.getMediaBox().getHeight() - (marginTop + line.getMarginTop()) - textHeight); - - if (line.isBold()) contentStream.setFont(font, fontSizeBold); - else contentStream.setFont(font, fontSize); - - contentStream.newLineAtOffset(textX, textY); - contentStream.showText(line.getText()); - contentStream.newLineAtOffset(-textX, -textY); // Réinitialise la position pour la prochaine ligne - } - - public static class PdfLine { - - private String text; - private boolean bold; - private int marginTop; - - public PdfLine() { - } - - public PdfLine(String text, boolean bold, int marginTop) { - this.text = text; - this.bold = bold; - this.marginTop = marginTop; - } - - public String getText() { - return text; - } - - public void setText(String text) { - this.text = text; - } - - public boolean isBold() { - return bold; - } - - public void setBold(boolean bold) { - this.bold = bold; - } - - public int getMarginTop() { - return marginTop; - } - - public void setMarginTop(int marginTop) { - this.marginTop = marginTop; - } - } - -} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/CreProperties.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/CreProperties.kt index ee2d3c7..9ebf91f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/CreProperties.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/CreProperties.kt @@ -6,4 +6,5 @@ import org.springframework.boot.context.properties.ConfigurationProperties class CreProperties { var workingDirectory: String = "data" var deploymentUrl: String = "http://localhost" + var cacheGeneratedFiles: Boolean = false } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/TouchUpKitController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/TouchUpKitController.kt new file mode 100644 index 0000000..997dbfa --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/TouchUpKitController.kt @@ -0,0 +1,24 @@ +package dev.fyloz.colorrecipesexplorer.rest.files + +import dev.fyloz.colorrecipesexplorer.service.files.TouchUpKitService +import org.springframework.core.io.ByteArrayResource +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/api/touchup") +class TouchUpKitController( + private val touchUpKitService: TouchUpKitService +) { + @GetMapping + fun getJobPdf(@RequestParam job: String): ResponseEntity { + with(touchUpKitService.generateJobPdfResource(job)) { + return ResponseEntity.ok() + .header("Content-Disposition", "filename=TouchUpKit_$job.pdf") + .contentLength(this.contentLength()) + .contentType(MediaType.APPLICATION_PDF) + .body(this) + } + } +} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryService.kt index cefb0b0..4fb4ee3 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryService.kt @@ -2,7 +2,7 @@ package dev.fyloz.colorrecipesexplorer.service import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.service.utils.mapMayThrow +import dev.fyloz.colorrecipesexplorer.utils.mapMayThrow import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import javax.transaction.Transactional diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt index b64cc12..92339e8 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt @@ -3,8 +3,8 @@ package dev.fyloz.colorrecipesexplorer.service import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.MixMaterialRepository -import dev.fyloz.colorrecipesexplorer.service.utils.findDuplicated -import dev.fyloz.colorrecipesexplorer.service.utils.hasGaps +import dev.fyloz.colorrecipesexplorer.utils.findDuplicated +import dev.fyloz.colorrecipesexplorer.utils.hasGaps import org.springframework.context.annotation.Lazy import org.springframework.http.HttpStatus import org.springframework.stereotype.Service diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt index 85411d7..af2217c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt @@ -2,7 +2,7 @@ package dev.fyloz.colorrecipesexplorer.service import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.MixRepository -import dev.fyloz.colorrecipesexplorer.service.utils.setAll +import dev.fyloz.colorrecipesexplorer.utils.setAll import org.springframework.context.annotation.Lazy import org.springframework.stereotype.Service import javax.transaction.Transactional diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt index 709ac4d..4ace03b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt @@ -4,7 +4,7 @@ import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.validation.or import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository import dev.fyloz.colorrecipesexplorer.service.files.FileService -import dev.fyloz.colorrecipesexplorer.service.utils.setAll +import dev.fyloz.colorrecipesexplorer.utils.setAll import org.springframework.context.annotation.Lazy import org.springframework.stereotype.Service import org.springframework.web.multipart.MultipartFile @@ -222,9 +222,9 @@ class RecipeImageServiceImpl( this@getDirectory.imagesDirectoryPath.fullPath().path }) - fun getImageFileName(recipe: Recipe, id: Long) = + private fun getImageFileName(recipe: Recipe, id: Long) = "${recipe.name}$RECIPE_IMAGE_ID_DELIMITER$id" - fun getImagePath(recipe: Recipe, name: String) = + private fun getImagePath(recipe: Recipe, name: String) = "${recipe.imagesDirectoryPath}/$name$RECIPE_IMAGE_EXTENSION" } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt index 44ba176..a72fc76 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt @@ -3,8 +3,8 @@ package dev.fyloz.colorrecipesexplorer.service import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.RecipeStepRepository -import dev.fyloz.colorrecipesexplorer.service.utils.findDuplicated -import dev.fyloz.colorrecipesexplorer.service.utils.hasGaps +import dev.fyloz.colorrecipesexplorer.utils.findDuplicated +import dev.fyloz.colorrecipesexplorer.utils.hasGaps import org.springframework.http.HttpStatus import org.springframework.stereotype.Service diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileService.kt index 4600a8a..b514a93 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileService.kt @@ -28,9 +28,12 @@ interface FileService { /** Creates a file at the given [path]. */ fun create(path: String) - /** Writes the given [file] at the given [path]. If the file already exists, it will be overwritten if [overwrite] is true. */ + /** Writes the given [file] to the given [path]. If the file already exists, it will be overwritten if [overwrite] is enabled. */ fun write(file: MultipartFile, path: String, overwrite: Boolean) + /** Writes the given [data] to the given [path]. If the file at the path already exists, it will be overwritten if [overwrite] is enabled. */ + fun write(data: ByteArrayResource, path: String, overwrite: Boolean) + /** Deletes the file at the given [path]. */ fun delete(path: String) @@ -71,23 +74,15 @@ class FileServiceImpl( } } - override fun write(file: MultipartFile, path: String, overwrite: Boolean) { - val fullPath = path.fullPath() - - if (exists(path)) { - if (!overwrite) throw FileExistsException(path) - } else { - create(path) + override fun write(file: MultipartFile, path: String, overwrite: Boolean) = + prepareWrite(path, overwrite) { + file.transferTo(this.toPath()) } - try { - withFileAt(fullPath) { - file.transferTo(this.toPath()) - } - } catch (ex: IOException) { - FileWriteException(path).logAndThrow(ex, logger) + override fun write(data: ByteArrayResource, path: String, overwrite: Boolean) = + prepareWrite(path, overwrite) { + this.writeBytes(data.byteArray) } - } override fun delete(path: String) { try { @@ -108,6 +103,24 @@ class FileServiceImpl( return FilePath("${creProperties.workingDirectory}/$this") } + private fun prepareWrite(path: String, overwrite: Boolean, op: File.() -> Unit) { + val fullPath = path.fullPath() + + if (exists(path)) { + if (!overwrite) throw FileExistsException(path) + } else { + create(path) + } + + try { + withFileAt(fullPath) { + this.op() + } + } catch (ex: IOException) { + FileWriteException(path).logAndThrow(ex, logger) + } + } + /** Runs the given [block] in the context of a file with the given [fullPath]. */ private fun withFileAt(fullPath: FilePath, block: File.() -> T) = fullPath.file.block() diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitService.kt new file mode 100644 index 0000000..68b043e --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitService.kt @@ -0,0 +1,78 @@ +package dev.fyloz.colorrecipesexplorer.service.files + +import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties +import dev.fyloz.colorrecipesexplorer.utils.* +import org.springframework.core.io.ByteArrayResource +import org.springframework.stereotype.Service + +private const val TOUCH_UP_KIT_FILES_PATH = "pdf/touchupkits" + +const val TOUCH_UP_TEXT_FR = "KIT DE RETOUCHE" +const val TOUCH_UP_TEXT_EN = "TOUCH UP KIT" + +interface TouchUpKitService { + /** Generates and returns a [PdfDocument] for the given [job]. */ + fun generateJobPdf(job: String): PdfDocument + + /** + * Generates and returns a [PdfDocument] for the given [job] as a [ByteArrayResource]. + * + * If [CreProperties.cacheGeneratedFiles] is enabled and a file exists for the job, its content will be returned. + * If caching is enabled but no file exists for the job, the generated ByteArrayResource will be cached on the disk. + */ + fun generateJobPdfResource(job: String): ByteArrayResource + + /** Writes the given [document] to the [FileService] if [CreProperties.cacheGeneratedFiles] is enabled. */ + fun String.cachePdfDocument(document: PdfDocument) +} + +@Service +class TouchUpKitServiceImpl( + private val fileService: FileService, + private val creProperties: CreProperties +) : TouchUpKitService { + override fun generateJobPdf(job: String) = pdf { + container { + centeredVertically = true + drawContainerBottom = true + text(TOUCH_UP_TEXT_FR) { + bold = true + fontSize = PDF_DEFAULT_FONT_SIZE + 12 + } + text(TOUCH_UP_TEXT_EN) { + bold = true + fontSize = PDF_DEFAULT_FONT_SIZE + 12 + } + text(job) { + marginTop = 10f + } + } + + container(containers[0]) { + drawContainerBottom = false + } + } + + override fun generateJobPdfResource(job: String): ByteArrayResource { + if (creProperties.cacheGeneratedFiles) { + with(job.pdfDocumentPath()) { + if (fileService.exists(this)) { + return fileService.read(this) + } + } + } + + return generateJobPdf(job).apply { + job.cachePdfDocument(this) + }.toByteArrayResource() + } + + override fun String.cachePdfDocument(document: PdfDocument) { + if (!creProperties.cacheGeneratedFiles) return + + fileService.write(document.toByteArrayResource(), this.pdfDocumentPath(), true) + } + + private fun String.pdfDocumentPath() = + "$TOUCH_UP_KIT_FILES_PATH/$this.pdf" +} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/utils/Collections.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt similarity index 96% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/utils/Collections.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt index f461138..c6d4b9c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/utils/Collections.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt @@ -1,4 +1,4 @@ -package dev.fyloz.colorrecipesexplorer.service.utils +package dev.fyloz.colorrecipesexplorer.utils /** Returns a list containing the result of the given [transform] applied to each item of the [Iterable]. If the given [transform] throws, the [Throwable] will be passed to the given [throwableConsumer]. */ inline fun Iterable.mapMayThrow( diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Pdf.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Pdf.kt new file mode 100644 index 0000000..a9f1387 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Pdf.kt @@ -0,0 +1,125 @@ +package dev.fyloz.colorrecipesexplorer.utils + +import org.apache.pdfbox.pdmodel.PDDocument +import org.apache.pdfbox.pdmodel.PDPage +import org.apache.pdfbox.pdmodel.PDPageContentStream +import org.apache.pdfbox.pdmodel.font.PDFont +import org.apache.pdfbox.pdmodel.font.PDType1Font +import org.springframework.core.io.ByteArrayResource +import java.io.ByteArrayOutputStream + +val PDF_DEFAULT_FONT: PDType1Font = PDType1Font.HELVETICA +val PDF_DEFAULT_FONT_BOLD: PDType1Font = PDType1Font.HELVETICA_BOLD +const val PDF_DEFAULT_FONT_SIZE = 42f +val PDF_DASH_LINE_PATTERN = floatArrayOf(4f) + +/** Creates a [PdfContainer] and apply the given [block]. */ +fun pdf(block: PdfDocument.() -> Unit = {}) = + PdfDocument().apply { block() } + +/** Creates a [PdfContainer] in the given [PdfDocument] and apply the given [block]. If a [container] is given, the receiver of the block will be a clone of it. */ +fun PdfDocument.container(container: PdfContainer = PdfContainer(), block: PdfContainer.() -> Unit) { + this.containers += PdfContainer(container).apply(block) +} + +/** Creates a [PdfText] with the given [text] in the given [PdfContainer] and apply the given [block]. */ +fun PdfContainer.text(text: String, block: PdfText.() -> Unit) { + this.texts += PdfText(text = text).apply(block) +} + +fun PdfDocument.toByteArrayResource(): ByteArrayResource = PDDocument().use { document -> + val page = PDPage() + + document.addPage(page) + + fun PDPageContentStream.drawText(text: PdfText, y: Float) { + val font = if (text.bold) fontBold else font + val textWidth = font.getStringWidth(text.text) / 1000 * text.fontSize + val textX = (page.mediaBox.width - textWidth) / 2f + + beginText() + newLineAtOffset(textX, y) + setFont(font, text.fontSize) + showText(text.text) + endText() + } + + fun PDPageContentStream.drawDashLine(y: Float) { + moveTo(0f, y) + lineTo(page.mediaBox.width, y) + setLineDashPattern(PDF_DASH_LINE_PATTERN, 0f) + stroke() + } + + fun PDPageContentStream.drawContainer(container: PdfContainer, y: Float, height: Float) { + var textY = y + + if (container.centeredVertically) { + val textsHeight = container.texts + .map { it.fontSize + it.marginTop } + .reduce { acc, textHeight -> acc + textHeight } + textY -= (height - textsHeight) / 2f + } + + if (container.drawContainerBottom) { + this.drawDashLine(y - height) + } + + container.texts.forEach { text -> + textY -= text.fontSize + text.marginTop + this.drawText(text, textY) + } + } + + PDPageContentStream(document, page).use { + var containerY = page.mediaBox.height + + val computedSizeContainerCount = containers + .filter { it.height < 0 } + .count() + val computedSizeContainersHeight = containerY / computedSizeContainerCount + + containers.forEach { container -> + val height = if (container.height < 0) + computedSizeContainersHeight + else + container.height + + it.drawContainer(container, containerY, height) + + containerY -= height + } + } + + ByteArrayOutputStream().use { + document.save(it) + ByteArrayResource(it.toByteArray()) + } +} + +data class PdfDocument( + var font: PDFont = PDF_DEFAULT_FONT, + var fontBold: PDFont = PDF_DEFAULT_FONT_BOLD, + val containers: MutableList = mutableListOf() +) + +data class PdfContainer( + var height: Float = -1f, + var centeredVertically: Boolean = false, + var drawContainerBottom: Boolean = false, + val texts: MutableList = mutableListOf() +) { + constructor(container: PdfContainer) : this( + container.height, + container.centeredVertically, + container.drawContainerBottom, + container.texts + ) +} + +data class PdfText( + var text: String = "Text", + var bold: Boolean = false, + var fontSize: Float = PDF_DEFAULT_FONT_SIZE, + var marginTop: Float = 0f +) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3250a34..74dd17a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,6 +3,7 @@ server.port=9090 # CRE cre.server.working-directory=data cre.server.deployment-url=http://localhost:9090 +cre.server.cache-generated-files=true cre.security.jwt-secret=CtnvGQjgZ44A1fh295gE cre.security.jwt-duration=18000000 # Root user diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt index 509d490..98dd0f4 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt @@ -8,6 +8,7 @@ import io.mockk.* import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.springframework.mock.web.MockMultipartFile +import org.springframework.web.multipart.MultipartFile import java.io.File import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -165,7 +166,7 @@ class RecipeServiceTest : private class RecipeImageServiceTestContext { val fileService = mockk { - every { write(any(), any(), any()) } just Runs + every { write(any(), any(), any()) } just Runs every { delete(any()) } just Runs } val recipeImageService = spyk(RecipeImageServiceImpl(fileService)) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt new file mode 100644 index 0000000..3e6ad10 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt @@ -0,0 +1,120 @@ +package dev.fyloz.colorrecipesexplorer.service.files + +import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties +import dev.fyloz.colorrecipesexplorer.utils.PdfDocument +import dev.fyloz.colorrecipesexplorer.utils.toByteArrayResource +import io.mockk.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.springframework.core.io.ByteArrayResource +import kotlin.test.assertEquals + +private class TouchUpKitServiceTestContext { + val fileService = mockk { + every { write(any(), any(), any()) } just Runs + } + val creProperties = mockk { + every { cacheGeneratedFiles } returns false + } + val touchUpKitService = spyk(TouchUpKitServiceImpl(fileService, creProperties)) + val pdfDocumentData = mockk() + val pdfDocument = mockk { + mockkStatic(PdfDocument::toByteArrayResource) + every { toByteArrayResource() } returns pdfDocumentData + } +} + +class TouchUpKitServiceTest { + private val job = "job" + + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + // generateJobPdf() + + @Test + fun `generateJobPdf() generates a valid PdfDocument for the given job`() { + test { + val generatedPdfDocument = touchUpKitService.generateJobPdf(job) + + setOf(0, 1).forEach { + assertEquals(TOUCH_UP_TEXT_FR, generatedPdfDocument.containers[it].texts[0].text) + assertEquals(TOUCH_UP_TEXT_EN, generatedPdfDocument.containers[it].texts[1].text) + assertEquals(job, generatedPdfDocument.containers[it].texts[2].text) + } + } + } + + // generateJobPdfResource() + + @Test + fun `generateJobPdfResource() generates and returns a ByteArrayResource for the given job then cache it`() { + test { + every { touchUpKitService.generateJobPdf(any()) } returns pdfDocument + with(touchUpKitService) { + every { job.cachePdfDocument(pdfDocument) } just Runs + } + + val generatedResource = touchUpKitService.generateJobPdfResource(job) + + assertEquals(pdfDocumentData, generatedResource) + + verify { + with(touchUpKitService) { + job.cachePdfDocument(pdfDocument) + } + } + } + } + + @Test + fun `generateJobPdfResource() returns a cached ByteArrayResource from the FileService when caching is enabled and a cached file eixsts for the given job`() { + test { + every { creProperties.cacheGeneratedFiles } returns true + every { fileService.exists(any()) } returns true + every { fileService.read(any()) } returns pdfDocumentData + + val redResource = touchUpKitService.generateJobPdfResource(job) + + assertEquals(pdfDocumentData, redResource) + } + } + + // String.cachePdfDocument() + + @Test + fun `cachePdfDocument() does nothing when caching is disabled`() { + test { + every { creProperties.cacheGeneratedFiles } returns false + + with(touchUpKitService) { + job.cachePdfDocument(pdfDocument) + } + + verify(exactly = 0) { + fileService.write(any(), any(), any()) + } + } + } + + @Test + fun `cachePdfDocument() writes the given document to the FileService when cache is enabled`() { + test { + every { creProperties.cacheGeneratedFiles } returns true + + with(touchUpKitService) { + job.cachePdfDocument(pdfDocument) + } + + verify { + fileService.write(pdfDocumentData, any(), true) + } + } + } + + private fun test(test: TouchUpKitServiceTestContext.() -> Unit) { + TouchUpKitServiceTestContext().test() + } +} From d7da1654e2f32deb6b2d2ceaa57099d519fd100a Mon Sep 17 00:00:00 2001 From: FyloZ Date: Sat, 1 May 2021 20:45:35 -0400 Subject: [PATCH 2/2] Ajout d'une permission pour l'API des kits de retouche --- .../model/EmployeePermission.kt | 25 ++++++++++--------- .../rest/files/TouchUpKitController.kt | 2 ++ .../service/files/TouchUpKitServiceTest.kt | 1 + 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeePermission.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeePermission.kt index 7f1aecc..5124243 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeePermission.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeePermission.kt @@ -2,11 +2,10 @@ package dev.fyloz.colorrecipesexplorer.model import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority -import java.util.* enum class EmployeePermission( - val impliedPermissions: List = listOf(), - val deprecated: Boolean = false + val impliedPermissions: List = listOf(), + val deprecated: Boolean = false ) { READ_FILE, WRITE_FILE(listOf(READ_FILE)), @@ -35,19 +34,21 @@ enum class EmployeePermission( ADD_TO_INVENTORY(listOf(VIEW_CATALOG)), DEDUCT_FROM_INVENTORY(listOf(VIEW_RECIPES)), + GENERATE_TOUCH_UP_KIT, ADMIN( - listOf( - EDIT_CATALOG, + listOf( + EDIT_CATALOG, - REMOVE_RECIPES, - REMOVE_USERS, - REMOVE_CATALOG, + REMOVE_RECIPES, + REMOVE_USERS, + REMOVE_CATALOG, - PRINT_MIXES, - ADD_TO_INVENTORY, - DEDUCT_FROM_INVENTORY - ) + PRINT_MIXES, + ADD_TO_INVENTORY, + DEDUCT_FROM_INVENTORY, + GENERATE_TOUCH_UP_KIT + ) ), // deprecated permissions diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/TouchUpKitController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/TouchUpKitController.kt index 997dbfa..6993025 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/TouchUpKitController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/TouchUpKitController.kt @@ -4,10 +4,12 @@ import dev.fyloz.colorrecipesexplorer.service.files.TouchUpKitService import org.springframework.core.io.ByteArrayResource import org.springframework.http.MediaType import org.springframework.http.ResponseEntity +import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/api/touchup") +@PreAuthorize("hasAuthority('GENERATE_TOUCH_UP_KIT')") class TouchUpKitController( private val touchUpKitService: TouchUpKitService ) { diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt index 3e6ad10..4affc53 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt @@ -19,6 +19,7 @@ private class TouchUpKitServiceTestContext { val touchUpKitService = spyk(TouchUpKitServiceImpl(fileService, creProperties)) val pdfDocumentData = mockk() val pdfDocument = mockk { + mockkStatic(PdfDocument::toByteArrayResource) mockkStatic(PdfDocument::toByteArrayResource) every { toByteArrayResource() } returns pdfDocumentData }