From c798b58a6579409aa6aeaa6d846b64f18519e9a8 Mon Sep 17 00:00:00 2001 From: pko <214621362+pkoq@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:18:39 +0200 Subject: [PATCH 1/5] feat: add email options, finish and cleanup multichannel impl --- CHANGELOG.md | 5 + .../categorizeAndTranslate.groovy | 2 +- .../example/common/mapping/AreasExport.groovy | 1 - .../common/report/ComplexityReport.groovy | 12 + .../AcknowledgementLetterFromSource.groovy | 2 +- .../migration/example/example/Import.groovy | 83 +++++- .../api/dto/migrationmodel/DocumentObject.kt | 1 - .../migrationmodel/DocumentObjectOptions.kt | 67 +++++ .../builder/DocumentObjectBuilder.kt | 10 +- .../builder/EmailOptionsBuilder.kt | 49 ++++ .../builder/SmsOptionsBuilder.kt | 16 ++ .../migrationmodel/builder/SnippetBuilder.kt | 2 +- .../components/HasDocumentObjectOptions.kt | 36 ++- .../documentcontent/GridLayoutBuilder.kt | 159 +++++++++-- .../repository/DocumentObjectRepository.kt | 6 +- .../DocumentObjectOptionsEntity.kt | 33 +++ .../persistence/table/DocumentObjectTable.kt | 7 +- ..._document_object_options_entity_upgrade.kt | 52 ++++ .../migration/service/IcmDataCache.kt | 27 +- .../DesignerDocumentObjectBuilder.kt | 8 +- .../InspireDocumentObjectBuilder.kt | 89 +++++-- .../InteractiveDocumentObjectBuilder.kt | 18 +- .../migration/shared/DocumentObjectOptions.kt | 18 -- .../DocumentObjectRepositoryTest.kt | 39 +++ .../migration/service/IcmDataCacheTest.kt | 12 +- .../DesignerDocumentObjectBuilderTest.kt | 88 ++++++- .../InteractiveDocumentObjectBuilderTest.kt | 246 ++++++++++++++++++ .../migration/tools/TestObjectBuilders.kt | 2 +- .../tools/model/TestModelObjectBuilders.kt | 2 +- .../wfdxml/api/layoutnodes/SheetNameType.java | 11 +- .../internal/layoutnodes/PagesImplTest.groovy | 52 ++++ 31 files changed, 1041 insertions(+), 114 deletions(-) create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/DocumentObjectOptions.kt create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/EmailOptionsBuilder.kt create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/SmsOptionsBuilder.kt create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/DocumentObjectOptionsEntity.kt create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/persistence/upgrade/V15__document_object_options_entity_upgrade.kt delete mode 100644 migration-library/src/main/kotlin/com/quadient/migration/shared/DocumentObjectOptions.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index ace05abe..d103ba2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) ### Changed +**Breaking** `PageOptions` were moved from `com.quadient.migration.shared` to the +`com.quadient.migration.api.dto.migrationmodel` package, you will need to adjust imports in your code to reflect the new +path. + + ### Fixed ## [17.0.27] - 2026-07-14 diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/azureAI/categorizeAndTranslate/categorizeAndTranslate.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/azureAI/categorizeAndTranslate/categorizeAndTranslate.groovy index bc2733f8..dfbb944d 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/azureAI/categorizeAndTranslate/categorizeAndTranslate.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/azureAI/categorizeAndTranslate/categorizeAndTranslate.groovy @@ -17,7 +17,7 @@ import com.quadient.migration.api.dto.migrationmodel.builder.TextStyleDefinition import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.ImageType import com.quadient.migration.shared.LineSpacing -import com.quadient.migration.shared.PageOptions +import com.quadient.migration.api.dto.migrationmodel.PageOptions import com.quadient.migration.shared.Position import com.quadient.migration.shared.Size import groovy.json.JsonSlurper diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AreasExport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AreasExport.groovy index 8685dc59..00063705 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AreasExport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AreasExport.groovy @@ -13,7 +13,6 @@ import com.quadient.migration.api.repository.DocumentObjectRepository import com.quadient.migration.example.common.util.Csv import com.quadient.migration.example.common.util.Mapping import com.quadient.migration.shared.DocumentObjectType -import com.quadient.migration.shared.PageOptions import groovy.transform.Field import java.nio.file.Path diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/report/ComplexityReport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/report/ComplexityReport.groovy index 852ddbc7..e33c8b21 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/report/ComplexityReport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/report/ComplexityReport.groovy @@ -165,6 +165,18 @@ class Stats { case ColumnLayout -> this.columnLayoutsCount++ case StringValue -> this.collectTextContent([content]) case VariableRef -> this.collectTextContent([content]) + case GridLayout -> { + for (column in content.columns) { + for (columnContent in column.content) { + switch (columnContent) { + case GridContent.Content -> this.collectContent(columnContent.content) + case GridContent.Image -> this.usedImages.add(columnContent.ref.id) + case GridContent.ExternalImage -> {} + + } + } + } + } case Shape -> this.shapesCount++ case Barcode -> this.barcodesCount++ default -> throw new IllegalStateException("Unknown content type: ${content.class.name}") diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/example/AcknowledgementLetterFromSource.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/example/AcknowledgementLetterFromSource.groovy index f156a0c4..33065742 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/example/AcknowledgementLetterFromSource.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/example/AcknowledgementLetterFromSource.groovy @@ -18,7 +18,7 @@ import com.quadient.migration.shared.Alignment import com.quadient.migration.shared.DataType import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.ImageType -import com.quadient.migration.shared.PageOptions +import com.quadient.migration.api.dto.migrationmodel.PageOptions import com.quadient.migration.shared.Size import static com.quadient.migration.example.common.util.InitMigration.initMigration diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/example/Import.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/example/Import.groovy index 2c524998..6fd6da38 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/example/Import.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/example/Import.groovy @@ -11,9 +11,11 @@ import com.quadient.migration.api.dto.migrationmodel.VariableRef import com.quadient.migration.api.dto.migrationmodel.builder.AttachmentBuilder import com.quadient.migration.api.dto.migrationmodel.builder.DisplayRuleBuilder import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder +import com.quadient.migration.api.dto.migrationmodel.builder.EmailObjectBuilder import com.quadient.migration.api.dto.migrationmodel.builder.ImageBuilder import com.quadient.migration.api.dto.migrationmodel.builder.ParagraphBuilder import com.quadient.migration.api.dto.migrationmodel.builder.ParagraphStyleBuilder +import com.quadient.migration.api.dto.migrationmodel.builder.SmsObjectBuilder import com.quadient.migration.api.dto.migrationmodel.builder.SnippetBuilder import com.quadient.migration.api.dto.migrationmodel.builder.TableBuilder import com.quadient.migration.api.dto.migrationmodel.builder.TextStyleBuilder @@ -30,7 +32,7 @@ import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.GroupOp import com.quadient.migration.shared.ImageOptions import com.quadient.migration.shared.ImageType -import com.quadient.migration.shared.PageOptions +import com.quadient.migration.api.dto.migrationmodel.PageOptions import com.quadient.migration.shared.ParagraphPdfTaggingRule import com.quadient.migration.shared.QrCodeErrorCorrectionLevel import com.quadient.migration.shared.QrCodeSize @@ -407,7 +409,7 @@ def address = new DocumentObjectBuilder("address", DocumentObjectType.Block) // Footer of the document containing a signature. def signature = new DocumentObjectBuilder("signature", DocumentObjectType.Block) .paragraph { it.styleRef(compactParagraphStyle).text { it.styleRef(normalStyle).string("Sincerely,") } } - .paragraph { it.styleRef(compactParagraphStyle).text { it.styleRef(normalStyle).string("John Smith") } } + .paragraph { it.styleRef(compactParagraphStyle).text { it.styleRef(normalStyle).string("John Migration") } } .paragraph { it.styleRef(compactParagraphStyle).text { it.styleRef(normalStyle).string("CEO of Lorem ipsum") } } .variableStructureRef(variableStructure) .build() @@ -650,6 +652,81 @@ def page = new DocumentObjectBuilder("page1", DocumentObjectType.Page) .variableStructureRef(variableStructure) .build() +def sms = new SmsObjectBuilder("sms") + .options { it.numberTo("123456789") } + .string("Hello, ").variableRef(nameVariable).string(".") + .string(" Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.") + .build() + +def email = new EmailObjectBuilder("email") + .options { + it.width(600.0) + it.backgroundFill("#ffffff") + it.from("john@migration.com") + it.fromName("John Migration") + it.subject { it.string("Hello, ").variableRef(nameVariable.id) } + it.to("john.doe@example.com") + } + .gridLayout { + it.column { + it.image { + it.imageRef(logo.id) + } + } + } + .gridLayout { + it.column { + it.content { + it.string("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.") + } + } + it.column { + it.content { + it.table { + it.border { + it.allBorders(borderColor, borderWidth) + } + it.addRow { + it.addCell { + it.border { + it.allBorders(borderColor, borderWidth) + } + it.string("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.") + } + it.addCell { + it.border { + it.allBorders(borderColor, borderWidth) + } + it.string("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.") + } + } + it.addRow { + it.addCell { + it.border { + it.allBorders(borderColor, borderWidth) + } + it.string("Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.") + } + it.addCell { + it.border { + it.allBorders(borderColor, borderWidth) + } + it.string("Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") + } + } + } + } + } + } + .build() + +def templateEmailSms = new DocumentObjectBuilder("templateEmailSms", DocumentObjectType.Template) + .documentObjectRef(sms) + .documentObjectRef(email) + .baseTemplate("vcs://Interactive/StandardPackage/BaseTemplates/ResponsiveEmailBaseTemplate.wfd") + .variableStructureRef(variableStructure) + .build() + def template = new DocumentObjectBuilder("template", DocumentObjectType.Template) .documentObjectRef(page) .subject("Document example template") @@ -664,7 +741,7 @@ def template = new DocumentObjectBuilder("template", DocumentObjectType.Template .build() // Insert all content into the database to be used in the deploy task -for (item in [address, signature, paragraph1, paragraph2, conditionalParagraph, page, template, firstMatchBlock, selectByLanguageBlock, jobListBlock, snippet, fmSnippet]) { +for (item in [address, signature, paragraph1, paragraph2, conditionalParagraph, page, template, firstMatchBlock, selectByLanguageBlock, jobListBlock, snippet, fmSnippet, sms, email, templateEmailSms]) { migration.documentObjectRepository.upsert(item) } for (item in [headingStyle, normalStyle]) { diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/DocumentObject.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/DocumentObject.kt index 249df325..5fa97da1 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/DocumentObject.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/DocumentObject.kt @@ -1,6 +1,5 @@ package com.quadient.migration.api.dto.migrationmodel -import com.quadient.migration.shared.DocumentObjectOptions import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.MetadataEntry import com.quadient.migration.shared.SkipOptions diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/DocumentObjectOptions.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/DocumentObjectOptions.kt new file mode 100644 index 00000000..5ad2359d --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/DocumentObjectOptions.kt @@ -0,0 +1,67 @@ +package com.quadient.migration.api.dto.migrationmodel + +import com.quadient.migration.persistence.migrationmodel.DocumentObjectOptionsEntity +import com.quadient.migration.persistence.migrationmodel.EmailOptionsEntity +import com.quadient.migration.persistence.migrationmodel.PageOptionsEntity +import com.quadient.migration.persistence.migrationmodel.SmsOptionsEntity +import com.quadient.migration.shared.Color +import com.quadient.migration.shared.Size + +sealed interface DocumentObjectOptions { + companion object { + fun fromDb(entity: DocumentObjectOptionsEntity): DocumentObjectOptions = when (entity) { + is PageOptionsEntity -> PageOptions( + width = entity.width, + height = entity.height, + ) + is EmailOptionsEntity -> EmailOptions( + width = entity.width, + backgroundFill = entity.backgroundFill, + from = entity.from.map(VariableStringContent::fromDb), + fromName = entity.fromName.map(VariableStringContent::fromDb), + subject = entity.subject.map(VariableStringContent::fromDb), + to = entity.to.map(VariableStringContent::fromDb), + ) + is SmsOptionsEntity -> SmsOptions( + numberTo = entity.numberTo.map(VariableStringContent::fromDb), + ) + } + } + + fun toDb(): DocumentObjectOptionsEntity = when (this) { + is PageOptions -> PageOptionsEntity( + width = width, + height = height, + ) + is EmailOptions -> EmailOptionsEntity( + width = width, + backgroundFill = backgroundFill, + from = from.map(VariableStringContent::toDb), + fromName = fromName.map(VariableStringContent::toDb), + subject = subject.map(VariableStringContent::toDb), + to = to.map(VariableStringContent::toDb), + ) + is SmsOptions -> SmsOptionsEntity( + numberTo = numberTo.map(VariableStringContent::toDb), + ) + } +} + + +data class PageOptions( + val width: Size?, + val height: Size?, +) : DocumentObjectOptions + +data class EmailOptions( + val width: Double?, + val backgroundFill: Color, + val from: List, + val fromName: List, + val subject: List, + val to: List, +) : DocumentObjectOptions + +data class SmsOptions( + val numberTo: List, +) : DocumentObjectOptions diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/DocumentObjectBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/DocumentObjectBuilder.kt index 883f24a2..d0e06c74 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/DocumentObjectBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/DocumentObjectBuilder.kt @@ -10,6 +10,7 @@ import com.quadient.migration.api.dto.migrationmodel.builder.components.HasCateg import com.quadient.migration.api.dto.migrationmodel.builder.components.HasDisplayRuleRef import com.quadient.migration.api.dto.migrationmodel.builder.components.HasDocumentObjectOptions import com.quadient.migration.api.dto.migrationmodel.builder.components.HasEmailOptions +import com.quadient.migration.api.dto.migrationmodel.builder.components.HasSmsOptions import com.quadient.migration.api.dto.migrationmodel.builder.components.HasInternal import com.quadient.migration.api.dto.migrationmodel.builder.components.HasMetadata import com.quadient.migration.api.dto.migrationmodel.builder.components.HasPdfMetadata @@ -17,11 +18,12 @@ import com.quadient.migration.api.dto.migrationmodel.builder.components.HasSkip import com.quadient.migration.api.dto.migrationmodel.builder.components.HasSubject import com.quadient.migration.api.dto.migrationmodel.builder.components.HasTargetFolder import com.quadient.migration.api.dto.migrationmodel.builder.components.HasVariableStructureRef -import com.quadient.migration.shared.DocumentObjectOptions +import com.quadient.migration.api.dto.migrationmodel.DocumentObjectOptions import com.quadient.migration.shared.DocumentObjectType -import com.quadient.migration.shared.EmailOptions +import com.quadient.migration.api.dto.migrationmodel.EmailOptions import com.quadient.migration.shared.MetadataEntry import com.quadient.migration.shared.SkipOptions +import com.quadient.migration.api.dto.migrationmodel.SmsOptions class DocumentObjectBuilder(id: String, private val type: DocumentObjectType) : DtoBuilderBase(id), DocumentContentBuilderBase, @@ -126,6 +128,7 @@ class SmsObjectBuilder(id: String) : DtoBuilderBase, HasParagraphContent, HasSkip, + HasSmsOptions, HasVariableStructureRef { override var variableStructureRef: VariableStructureRef? = null @@ -133,6 +136,7 @@ class SmsObjectBuilder(id: String) : DtoBuilderBase = emptyList() + private var fromName: List = emptyList() + private var subject: List = emptyList() + private var to: List = emptyList() + + fun width(width: Double?) = apply { this.width = width } + + fun backgroundFill(color: Color) = apply { this.backgroundFill = color } + fun backgroundFill(hex: String) = apply { this.backgroundFill = Color.fromHex(hex) } + + fun from(from: String) = apply { this.from = listOf(StringValue(from)) } + fun from(vararg content: VariableStringContent) = apply { this.from = content.toList() } + fun from(builder: VariableStringContentBuilder.() -> Unit) = + apply { this.from = VariableStringContentBuilder().apply(builder).build() } + + fun fromName(fromName: String) = apply { this.fromName = listOf(StringValue(fromName)) } + fun fromName(vararg content: VariableStringContent) = apply { this.fromName = content.toList() } + fun fromName(builder: VariableStringContentBuilder.() -> Unit) = + apply { this.fromName = VariableStringContentBuilder().apply(builder).build() } + + fun subject(subject: String) = apply { this.subject = listOf(StringValue(subject)) } + fun subject(vararg content: VariableStringContent) = apply { this.subject = content.toList() } + fun subject(builder: VariableStringContentBuilder.() -> Unit) = + apply { this.subject = VariableStringContentBuilder().apply(builder).build() } + + fun to(to: String) = apply { this.to = listOf(StringValue(to)) } + fun to(vararg content: VariableStringContent) = apply { this.to = content.toList() } + fun to(builder: VariableStringContentBuilder.() -> Unit) = + apply { this.to = VariableStringContentBuilder().apply(builder).build() } + + fun build(): EmailOptions = EmailOptions( + width = width, + backgroundFill = backgroundFill, + from = from, + fromName = fromName, + subject = subject, + to = to, + ) +} diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/SmsOptionsBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/SmsOptionsBuilder.kt new file mode 100644 index 00000000..6599bb6c --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/SmsOptionsBuilder.kt @@ -0,0 +1,16 @@ +package com.quadient.migration.api.dto.migrationmodel.builder + +import com.quadient.migration.api.dto.migrationmodel.StringValue +import com.quadient.migration.api.dto.migrationmodel.VariableStringContent +import com.quadient.migration.api.dto.migrationmodel.SmsOptions + +class SmsOptionsBuilder { + private var numberTo: List = emptyList() + + fun numberTo(numberTo: String) = apply { this.numberTo = listOf(StringValue(numberTo)) } + fun numberTo(vararg content: VariableStringContent) = apply { this.numberTo = content.toList() } + fun numberTo(builder: VariableStringContentBuilder.() -> Unit) = + apply { this.numberTo = VariableStringContentBuilder().apply(builder).build() } + + fun build(): SmsOptions = SmsOptions(numberTo = numberTo) +} diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/SnippetBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/SnippetBuilder.kt index d8ace06e..088f60a6 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/SnippetBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/SnippetBuilder.kt @@ -16,7 +16,7 @@ import com.quadient.migration.api.dto.migrationmodel.builder.components.HasSkip import com.quadient.migration.api.dto.migrationmodel.builder.components.HasSubject import com.quadient.migration.api.dto.migrationmodel.builder.components.HasTargetFolder import com.quadient.migration.api.dto.migrationmodel.builder.components.HasVariableStructureRef -import com.quadient.migration.shared.DocumentObjectOptions +import com.quadient.migration.api.dto.migrationmodel.DocumentObjectOptions import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.MetadataEntry import com.quadient.migration.shared.SkipOptions diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/components/HasDocumentObjectOptions.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/components/HasDocumentObjectOptions.kt index 59270fa5..9be293e6 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/components/HasDocumentObjectOptions.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/components/HasDocumentObjectOptions.kt @@ -1,7 +1,10 @@ package com.quadient.migration.api.dto.migrationmodel.builder.components -import com.quadient.migration.shared.DocumentObjectOptions -import com.quadient.migration.shared.EmailOptions +import com.quadient.migration.api.dto.migrationmodel.builder.EmailOptionsBuilder +import com.quadient.migration.api.dto.migrationmodel.builder.SmsOptionsBuilder +import com.quadient.migration.api.dto.migrationmodel.DocumentObjectOptions +import com.quadient.migration.api.dto.migrationmodel.EmailOptions +import com.quadient.migration.api.dto.migrationmodel.SmsOptions @Suppress("UNCHECKED_CAST") interface HasDocumentObjectOptions { @@ -25,4 +28,33 @@ interface HasEmailOptions { * @return This builder instance for method chaining. */ fun options(options: EmailOptions?) = apply { this.options = options } as T + /** + * Set options for the email using a builder. + * @param builder Builder function where receiver is an [EmailOptionsBuilder]. + * @return This builder instance for method chaining. + */ + fun options(builder: EmailOptionsBuilder.() -> Unit) = apply { + this.options = EmailOptionsBuilder().apply(builder).build() + } as T +} + +@Suppress("UNCHECKED_CAST") +interface HasSmsOptions { + var options: SmsOptions? + + /** + * Set options for the SMS. + * @param options [SmsOptions] to set for the SMS. + * @return This builder instance for method chaining. + */ + fun options(options: SmsOptions?) = apply { this.options = options } as T + + /** + * Set options for the SMS using a builder. + * @param builder Builder function where receiver is a [SmsOptionsBuilder]. + * @return This builder instance for method chaining. + */ + fun options(builder: SmsOptionsBuilder.() -> Unit) = apply { + this.options = SmsOptionsBuilder().apply(builder).build() + } as T } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/documentcontent/GridLayoutBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/documentcontent/GridLayoutBuilder.kt index 1a47cc1c..a00e9824 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/documentcontent/GridLayoutBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/documentcontent/GridLayoutBuilder.kt @@ -3,6 +3,7 @@ package com.quadient.migration.api.dto.migrationmodel.builder.documentcontent import com.quadient.migration.api.dto.migrationmodel.DisplayRuleRef import com.quadient.migration.api.dto.migrationmodel.DocumentContent import com.quadient.migration.api.dto.migrationmodel.GridContent +import com.quadient.migration.api.dto.migrationmodel.Image import com.quadient.migration.api.dto.migrationmodel.ImageRef import com.quadient.migration.api.dto.migrationmodel.StringValue import com.quadient.migration.api.dto.migrationmodel.VariableStringContent @@ -216,25 +217,46 @@ class GridLayoutBuilder : HasDisplayRuleRef { GridContent.Content(content, paddingTop, paddingBottom, paddingLeft, paddingRight) } - class ImageBuilder : HasDoublePadding { - internal var ref: ImageRef? = null - var horizontalAlignment: GridHorizontalAlignment? = null - var width: Double? = null - internal var linkUrl: List = emptyList() - var openInNewWindow: Boolean = false + class ImageBuilder : + HasDoublePadding, + HasGridHorizontalAlignment, + HasGridImageWidth, + HasGridLinkUrl { + private var ref: ImageRef? = null + override var horizontalAlignment: GridHorizontalAlignment? = null + override var width: Double? = null + override var linkUrl: List = emptyList() + override var openInNewWindow: Boolean = false override var paddingTop: Double = 0.0 override var paddingBottom: Double = 0.0 override var paddingLeft: Double = 0.0 override var paddingRight: Double = 0.0 + /** + * Sets the image reference by image ID. + * @param imageId The ID of the image to reference. + * @return This builder instance for method chaining. + */ fun imageRef(imageId: String) = apply { ref = ImageRef(imageId) } - fun horizontalAlignment(horizontalAlignment: GridHorizontalAlignment) = apply { this.horizontalAlignment = horizontalAlignment } - fun width(width: Double) = apply { this.width = width } - fun linkUrl(linkUrl: String) = apply { this.linkUrl = listOf(StringValue(linkUrl)) } - fun linkUrl(linkUrl: List) = apply { this.linkUrl = linkUrl } - fun linkUrl(builder: VariableStringContentBuilder.() -> Unit) = apply { this.linkUrl = VariableStringContentBuilder().apply(builder).build() } - fun openInNewWindow(openInNewWindow: Boolean) = apply { this.openInNewWindow = openInNewWindow } + /** + * Sets the image reference from an [Image] object. + * @param image The [Image] whose ID is used as the reference. + * @return This builder instance for method chaining. + */ + fun imageRef(image: Image) = apply { ref = ImageRef(image.id) } + + /** + * Sets the image reference directly. + * @param ref The [ImageRef] to use. + * @return This builder instance for method chaining. + */ + fun imageRef(ref: ImageRef) = apply { this.ref = ref } + + /** + * Builds the [GridContent.Image] instance, or null if no image reference was set. + * @return The constructed [GridContent.Image], or null if [imageRef] was not called. + */ fun build(): GridContent.Image? = ref?.let { GridContent.Image( it, @@ -250,31 +272,118 @@ class GridLayoutBuilder : HasDisplayRuleRef { } } - class ExternalImageBuilder : HasDoublePadding { - internal var url: List = emptyList() - var horizontalAlignment: GridHorizontalAlignment? = null - var width: Double? = null - var alternateText: String? = null - internal var linkUrl: List = emptyList() - var openInNewWindow: Boolean = false + class ExternalImageBuilder : + HasDoublePadding, + HasGridHorizontalAlignment, + HasGridImageWidth, + HasGridLinkUrl { + private var url: List = emptyList() + override var horizontalAlignment: GridHorizontalAlignment? = null + override var width: Double? = null + private var alternateText: String? = null + override var linkUrl: List = emptyList() + override var openInNewWindow: Boolean = false override var paddingTop: Double = 0.0 override var paddingBottom: Double = 0.0 override var paddingLeft: Double = 0.0 override var paddingRight: Double = 0.0 + /** + * Sets the image source URL as a plain string. + * @param url The URL of the external image. + * @return This builder instance for method chaining. + */ fun url(url: String) = apply { this.url = listOf(StringValue(url)) } + + /** + * Sets the image source URL as a list of variable string content. + * @param url The list of [VariableStringContent] composing the URL. + * @return This builder instance for method chaining. + */ fun url(url: List) = apply { this.url = url } + + /** + * Sets the image source URL using a builder function. + * @param builder A builder function to configure the [VariableStringContentBuilder]. + * @return This builder instance for method chaining. + */ fun url(builder: VariableStringContentBuilder.() -> Unit) = apply { this.url = VariableStringContentBuilder().apply(builder).build() } - fun linkUrl(linkUrl: String) = apply { this.linkUrl = listOf(StringValue(linkUrl)) } - fun linkUrl(linkUrl: List) = apply { this.linkUrl = linkUrl } - fun linkUrl(builder: VariableStringContentBuilder.() -> Unit) = apply { this.linkUrl = VariableStringContentBuilder().apply(builder).build() } - fun horizontalAlignment(horizontalAlignment: GridHorizontalAlignment) = apply { this.horizontalAlignment = horizontalAlignment } - fun width(width: Double) = apply { this.width = width } + + /** + * Sets the alternate text for the image for accessibility purposes. + * @param alternateText The alternate text string. + * @return This builder instance for method chaining. + */ fun alternateText(alternateText: String) = apply { this.alternateText = alternateText } - fun openInNewWindow(openInNewWindow: Boolean) = apply { this.openInNewWindow = openInNewWindow } + /** + * Builds the [GridContent.ExternalImage] instance. + * @return The constructed [GridContent.ExternalImage] instance. + */ fun build(): GridContent.ExternalImage = GridContent.ExternalImage(url, horizontalAlignment, width, alternateText, linkUrl, openInNewWindow, paddingTop, paddingBottom, paddingLeft, paddingRight) } } } + +@Suppress("UNCHECKED_CAST") +interface HasGridHorizontalAlignment { + var horizontalAlignment: GridHorizontalAlignment? + + /** + * Sets the horizontal alignment of the image within the column. + * @param horizontalAlignment The [GridHorizontalAlignment] to apply. + * @return This builder instance for method chaining. + */ + fun horizontalAlignment(horizontalAlignment: GridHorizontalAlignment) = + apply { this.horizontalAlignment = horizontalAlignment } as T +} + +@Suppress("UNCHECKED_CAST") +interface HasGridImageWidth { + var width: Double? + + /** + * Sets the display width of the image in pixels. + * @param width The width in pixels. + * @return This builder instance for method chaining. + */ + fun width(width: Double) = apply { this.width = width } as T +} + +@Suppress("UNCHECKED_CAST") +interface HasGridLinkUrl { + var linkUrl: List + var openInNewWindow: Boolean + + /** + * Sets the hyperlink URL for the image as a plain string. + * @param linkUrl The URL string to use as the link. + * @return This builder instance for method chaining. + */ + fun linkUrl(linkUrl: String) = apply { this.linkUrl = listOf(StringValue(linkUrl)) } as T + + /** + * Sets the hyperlink URL for the image as a list of variable string content. + * @param linkUrl The list of [VariableStringContent] composing the URL. + * @return This builder instance for method chaining. + */ + fun linkUrl(linkUrl: List) = apply { this.linkUrl = linkUrl } as T + + /** + * Sets the hyperlink URL for the image using a builder function. + * @param builder A builder function to configure the [VariableStringContentBuilder]. + * @return This builder instance for method chaining. + */ + fun linkUrl(builder: VariableStringContentBuilder.() -> Unit) = + apply { this.linkUrl = VariableStringContentBuilder().apply(builder).build() } as T + + /** + * Sets whether the link should open in a new window. + * Defaults to false if not set. + * @param openInNewWindow Whether to open the link in a new browser window. + * @return This builder instance for method chaining. + */ + fun openInNewWindow(openInNewWindow: Boolean) = apply { this.openInNewWindow = openInNewWindow } as T +} + diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/repository/DocumentObjectRepository.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/repository/DocumentObjectRepository.kt index a3960030..bbc3c4ad 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/repository/DocumentObjectRepository.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/repository/DocumentObjectRepository.kt @@ -8,7 +8,7 @@ import com.quadient.migration.api.dto.migrationmodel.toDb import com.quadient.migration.persistence.table.DocumentObjectTable import com.quadient.migration.service.deploy.utility.ResourceType import com.quadient.migration.shared.DocumentObjectType -import com.quadient.migration.shared.PageOptions +import com.quadient.migration.api.dto.migrationmodel.PageOptions import com.quadient.migration.tools.concat import com.quadient.migration.tools.logger import kotlin.time.Clock @@ -86,7 +86,7 @@ class DocumentObjectRepository(projectName: ProjectName, private val statusTrack stmt.setString(index++, dto.displayRuleRef?.id) stmt.setString(index++, dto.variableStructureRef?.id) stmt.setString(index++, dto.baseTemplate) - stmt.setObject(index++, dto.options?.let { Json.encodeToString(it) }, Types.OTHER) + stmt.setObject(index++, dto.options?.let { Json.encodeToString(it.toDb()) }, Types.OTHER) stmt.setObject(index++, dto.pdfMetadata?.let { Json.encodeToString(it.toDb()) }, Types.OTHER) stmt.setObject(index++, dto.metadata.let { Json.encodeToString(it) }, Types.OTHER) stmt.setObject(index++, Json.encodeToString(dto.skip), Types.OTHER) @@ -129,7 +129,7 @@ class DocumentObjectRepository(projectName: ProjectName, private val statusTrack it[DocumentObjectTable.displayRuleRef] = dto.displayRuleRef?.id it[DocumentObjectTable.variableStructureRef] = dto.variableStructureRef?.id it[DocumentObjectTable.baseTemplate] = dto.baseTemplate - it[DocumentObjectTable.documentObjectOptions] = dto.options + it[DocumentObjectTable.documentObjectOptions] = dto.options?.toDb() it[DocumentObjectTable.pdfMetadata] = dto.pdfMetadata?.toDb() it[DocumentObjectTable.metadata] = dto.metadata it[DocumentObjectTable.skip] = dto.skip diff --git a/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/DocumentObjectOptionsEntity.kt b/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/DocumentObjectOptionsEntity.kt new file mode 100644 index 00000000..3cf2611d --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/DocumentObjectOptionsEntity.kt @@ -0,0 +1,33 @@ +package com.quadient.migration.persistence.migrationmodel + +import com.quadient.migration.shared.Color +import com.quadient.migration.shared.Size +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +sealed interface DocumentObjectOptionsEntity + +@Serializable +@SerialName("PageOptionsEntity") +data class PageOptionsEntity( + val width: Size?, + val height: Size?, +) : DocumentObjectOptionsEntity + +@Serializable +@SerialName("EmailOptionsEntity") +data class EmailOptionsEntity( + val width: Double?, + val backgroundFill: Color, + val from: List, + val fromName: List, + val subject: List, + val to: List, +) : DocumentObjectOptionsEntity + +@Serializable +@SerialName("SmsOptionsEntity") +data class SmsOptionsEntity( + val numberTo: List +) : DocumentObjectOptionsEntity diff --git a/migration-library/src/main/kotlin/com/quadient/migration/persistence/table/DocumentObjectTable.kt b/migration-library/src/main/kotlin/com/quadient/migration/persistence/table/DocumentObjectTable.kt index d23286bf..3bd78d19 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/persistence/table/DocumentObjectTable.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/persistence/table/DocumentObjectTable.kt @@ -4,11 +4,12 @@ import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap import com.quadient.migration.api.dto.migrationmodel.DisplayRuleRef import com.quadient.migration.api.dto.migrationmodel.DocumentContent import com.quadient.migration.api.dto.migrationmodel.DocumentObject +import com.quadient.migration.api.dto.migrationmodel.DocumentObjectOptions import com.quadient.migration.api.dto.migrationmodel.PdfMetadata import com.quadient.migration.api.dto.migrationmodel.VariableStructureRef import com.quadient.migration.persistence.migrationmodel.DocumentContentEntity import com.quadient.migration.persistence.migrationmodel.PdfMetadataEntity -import com.quadient.migration.shared.DocumentObjectOptions +import com.quadient.migration.persistence.migrationmodel.DocumentObjectOptionsEntity import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.MetadataEntry import com.quadient.migration.shared.SkipOptions @@ -24,7 +25,7 @@ object DocumentObjectTable : MigrationObjectTable("document_object") { val displayRuleRef = varchar("display_rule_ref", 255).nullable() val variableStructureRef = varchar("variable_structure_ref", 255).nullable() val baseTemplate = varchar("base_template", 255).nullable() - val documentObjectOptions = jsonb("options", Json).nullable() + val documentObjectOptions = jsonb("options", Json).nullable() val pdfMetadata = jsonb("pdf_metadata", Json).nullable() val metadata = jsonb>("metadata", Json) val skip = jsonb("skip", Json) @@ -45,7 +46,7 @@ object DocumentObjectTable : MigrationObjectTable("document_object") { displayRuleRef = result[displayRuleRef]?.let { DisplayRuleRef(it) }, variableStructureRef = result[variableStructureRef]?.let { VariableStructureRef(it) }, baseTemplate = result[baseTemplate], - options = result[documentObjectOptions], + options = result[documentObjectOptions]?.let(DocumentObjectOptions::fromDb), pdfMetadata = result[pdfMetadata]?.let(PdfMetadata::fromDb), metadata = result[metadata], skip = result[skip], diff --git a/migration-library/src/main/kotlin/com/quadient/migration/persistence/upgrade/V15__document_object_options_entity_upgrade.kt b/migration-library/src/main/kotlin/com/quadient/migration/persistence/upgrade/V15__document_object_options_entity_upgrade.kt new file mode 100644 index 00000000..187ed615 --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/persistence/upgrade/V15__document_object_options_entity_upgrade.kt @@ -0,0 +1,52 @@ +package com.quadient.migration.persistence.upgrade + +import org.flywaydb.core.api.migration.BaseJavaMigration +import org.flywaydb.core.api.migration.Context +import tools.jackson.databind.node.ObjectNode +import tools.jackson.module.kotlin.jacksonObjectMapper +import java.sql.Connection + +class V15__document_object_options_entity_upgrade : BaseJavaMigration() { + private val mapper = jacksonObjectMapper() + + override fun migrate(context: Context) { + migrateDocumentObjectOptions(context.connection) + } + + private fun migrateDocumentObjectOptions(connection: Connection) { + val selectStmt = connection.prepareStatement("SELECT id, options FROM document_object WHERE options IS NOT NULL") + val updateStmt = connection.prepareStatement("UPDATE document_object SET options = ?::jsonb WHERE id = ?") + + selectStmt.use { sel -> + val rs = sel.executeQuery() + while (rs.next()) { + val id = rs.getObject("id") + val optionsJson = rs.getString("options") + + try { + val node = mapper.readTree(optionsJson) as? ObjectNode ?: continue + val migratedNode = migrateOptionsNode(node) ?: continue + updateStmt.setString(1, mapper.writeValueAsString(migratedNode)) + updateStmt.setObject(2, id) + updateStmt.addBatch() + } catch (ex: Exception) { + System.err.println("Error migrating document_object options id=$id: ${ex.message}") + throw ex + } + } + rs.close() + updateStmt.executeBatch() + } + updateStmt.close() + } + + private fun migrateOptionsNode(node: ObjectNode): ObjectNode? { + val oldType = node.get("type")?.asString() ?: return null + if (oldType != "com.quadient.migration.shared.PageOptions") { + throw IllegalArgumentException("Unexpected type in options: $oldType") + } + + node.put("type", "PageOptionsEntity") + return node + } +} diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/IcmDataCache.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/IcmDataCache.kt index f1721321..2f9f29ce 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/IcmDataCache.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/IcmDataCache.kt @@ -108,9 +108,9 @@ abstract class IcmDataCache( private fun parseBaseTemplateData(xml: String): BaseTemplateData { val layoutXmlTree = xmlMapper.readTree(xml.trimIndent())["Layout"]["Layout"] val pagesInteractiveFlowNode = layoutXmlTree["Pages"]?.get("InteractiveFlow") - ?: return BaseTemplateData(emptyMap(), emptyMap(), emptyMap()) + ?: return BaseTemplateData(emptyMap(), null, null) val flowNodes = layoutXmlTree["Flow"] - ?: return BaseTemplateData(emptyMap(), emptyMap(), emptyMap()) + ?: return BaseTemplateData(emptyMap(), null, null) val interactiveFlowIds: List = if (pagesInteractiveFlowNode is ArrayNode) { pagesInteractiveFlowNode.toList().map { it["FlowId"].asString() } @@ -123,29 +123,28 @@ abstract class IcmDataCache( val smsRootFlowId = layoutXmlTree["SMSRoot"]?.get("FlowId")?.stringValue() val interactiveFlowNamesToIds = mutableMapOf() - val emailFlowNamesToIds = mutableMapOf() - val smsFlowNamesToIds = mutableMapOf() + var smsFlowId: String? = null + var emailFlowId: String? = null interactiveFlowIds.forEachIndexed { i, id -> - val targetMap = when { - emailBodyRootFlowId != null && id == emailBodyRootFlowId -> emailFlowNamesToIds - smsRootFlowId != null && id == smsRootFlowId -> smsFlowNamesToIds - else -> interactiveFlowNamesToIds + when { + emailBodyRootFlowId != null && id == emailBodyRootFlowId -> emailFlowId = "Def.InteractiveFlow$i" + smsRootFlowId != null && id == smsRootFlowId -> smsFlowId = "Def.InteractiveFlow$i" } val flowData = flowNodes.first { flow -> flow["Id"].asString() == id } - flowData["Name"]?.stringValue()?.let { targetMap[it] = "Def.InteractiveFlow$i" } + flowData["Name"]?.stringValue()?.let { interactiveFlowNamesToIds[it] = "Def.InteractiveFlow$i" } flowData["CustomProperty"]?.stringValue()?.let { raw -> lenientJson.decodeFromString(raw).customName?.let { - targetMap[it] = "Def.InteractiveFlow$i" + interactiveFlowNamesToIds[it] = "Def.InteractiveFlow$i" } } } return BaseTemplateData( interactiveFlowNamesToIds = interactiveFlowNamesToIds, - emailFlowNamesToIds = emailFlowNamesToIds, - smsFlowNamesToIds = smsFlowNamesToIds, + emailFlowId = emailFlowId, + smsFlowId = smsFlowId, ) } @@ -157,8 +156,8 @@ abstract class IcmDataCache( data class BaseTemplateData( val interactiveFlowNamesToIds: Map, - val emailFlowNamesToIds: Map, - val smsFlowNamesToIds: Map, + val emailFlowId: String?, + val smsFlowId: String?, ) @Serializable diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilder.kt index 6189bafb..b500875f 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilder.kt @@ -15,9 +15,9 @@ import com.quadient.migration.service.IcmDataCache import com.quadient.migration.service.ResourcePathProvider import com.quadient.migration.service.resolveAliases import com.quadient.migration.shared.DocumentObjectType -import com.quadient.migration.shared.EmailOptions +import com.quadient.migration.api.dto.migrationmodel.EmailOptions import com.quadient.migration.shared.IcmPath -import com.quadient.migration.shared.PageOptions +import com.quadient.migration.api.dto.migrationmodel.PageOptions import com.quadient.migration.shared.ShapePath import com.quadient.migration.shared.Position import com.quadient.migration.shared.millimeters @@ -112,7 +112,7 @@ class DesignerDocumentObjectBuilder( layout.data.setLanguageVariable(variable) } - addPdfMetadataToPages(layout, documentObject, variableStructure) + layout.addPdfMetadataToPages(documentObject, variableStructure) documentObject.content.paragraphIfEmpty().forEach { val model = (it as? DocumentObjectRef)?.id?.let(documentObjectRepository::findOrFail) @@ -313,6 +313,7 @@ class DesignerDocumentObjectBuilder( } private fun DocumentObject.buildSmsRoot(layout: Layout, varStructure: VariableStructure, languages: List) { + layout.addSmsNumberToPages(this, varStructure) val smsRoot = layout.addSmsRoot() val flow = buildDocumentContentAsSingleFlow( @@ -328,6 +329,7 @@ class DesignerDocumentObjectBuilder( smsRoot.setContent(flow) } private fun DocumentObject.buildEmailRoot(layout: Layout, varStructure: VariableStructure, languages: List) { + layout.addEmailMetadataToPages(this, varStructure) val emailRoot = layout.addEmailComponentRoot() val emailBodyRootFlow = layout.addFlow().setSectionFlow(true) val emailTmText = layout.addEmailTMText() diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilder.kt index ff330971..fcd883c7 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilder.kt @@ -12,6 +12,7 @@ import com.quadient.migration.api.dto.migrationmodel.DocumentObject import com.quadient.migration.api.dto.migrationmodel.DocumentObjectRef import com.quadient.migration.api.dto.migrationmodel.AttachmentRef import com.quadient.migration.api.dto.migrationmodel.Barcode +import com.quadient.migration.api.dto.migrationmodel.Code39Barcode import com.quadient.migration.api.dto.migrationmodel.FirstMatch import com.quadient.migration.api.dto.migrationmodel.GridContent import com.quadient.migration.api.dto.migrationmodel.GridLayout @@ -120,7 +121,10 @@ import com.quadient.migration.shared.VariablePath import com.quadient.migration.shared.LiteralPath import com.quadient.migration.shared.VariableRefPath import com.quadient.migration.service.resolveTarget +import com.quadient.migration.api.dto.migrationmodel.EmailOptions +import com.quadient.migration.api.dto.migrationmodel.QrCode import com.quadient.migration.shared.Size +import com.quadient.migration.api.dto.migrationmodel.SmsOptions import com.quadient.wfdxml.api.layoutnodes.Flow.WebEditingType.SECTION import com.quadient.wfdxml.api.layoutnodes.email.EmailComponentContent @@ -197,12 +201,37 @@ abstract class InspireDocumentObjectBuilder( } } - else -> {} + is Code39Barcode -> {} + is QrCode -> {} + is ColumnLayout -> {} + is Hyperlink -> {} + is AttachmentRef -> {} + is ImageRef -> {} + is StringValue -> {} + is VariableRef -> {} } } } - else -> {} + is GridLayout -> { + item.columns.forEach { column -> + column.content.forEach { content -> + when (content) { + is GridContent.Content -> collectLanguagesFromContent(content.content) + is GridContent.ExternalImage -> {} + is GridContent.Image -> {} + } + } + } + } + + is Code39Barcode -> {} + is QrCode -> {} + is AttachmentRef -> {} + is ImageRef -> {} + is Shape -> {} + is StringValue -> {} + is VariableRef -> {} } } } @@ -1833,31 +1862,41 @@ abstract class InspireDocumentObjectBuilder( } } - protected fun addPdfMetadataToPages( - layout: Layout, documentObject: DocumentObject, variableStructure: VariableStructure - ) { - val pdfMetadata = documentObject.pdfMetadata ?: return + protected fun Layout.addEmailMetadataToPages(documentObject: DocumentObject, variableStructure: VariableStructure) { + val emailOptions = documentObject.options as? EmailOptions ?: return + this.addSheetNameVariable(variableStructure, SheetNameType.EMAIL_FROM, "EmailFrom", emailOptions.from) + this.addSheetNameVariable(variableStructure, SheetNameType.EMAIL_FROM_NAME, "EmailFromName", emailOptions.fromName) + this.addSheetNameVariable(variableStructure, SheetNameType.EMAIL_SUBJECT, "EmailSubject", emailOptions.subject) + this.addSheetNameVariable(variableStructure, SheetNameType.EMAIL_TO, "EmailTo", emailOptions.to) + } - val metadataMap = mapOf( - SheetNameType.PDF_TITLE to Pair("TaggingTitle", pdfMetadata.title), - SheetNameType.PDF_AUTHOR to Pair("TaggingAuthor", pdfMetadata.author), - SheetNameType.PDF_SUBJECT to Pair("TaggingSubject", pdfMetadata.subject), - SheetNameType.PDF_KEYWORDS to Pair("TaggingKeywords", pdfMetadata.keywords), - SheetNameType.PDF_PRODUCER to Pair("TaggingProduce", pdfMetadata.producer) - ) + protected fun Layout.addSmsNumberToPages(documentObject: DocumentObject, variableStructure: VariableStructure) { + val smsOptions = documentObject.options as? SmsOptions ?: return + this.addSheetNameVariable(variableStructure, SheetNameType.SMS_NUMBER_TO, "NumberTo", smsOptions.numberTo) + } - metadataMap.forEach { (type, data) -> - val (variableName, value) = data - if (!value.isNullOrEmpty()) { - val variable = - layout.data.addVariable().setName(variableName).setKind(VariableKind.CALCULATED).setScript( - variableStringContentToScript( - value, layout, variableStructure, variableRepository::findOrFail - ) - ) - layout.pages.addSheetName(type, variable) - } - } + protected fun Layout.addPdfMetadataToPages(documentObject: DocumentObject, variableStructure: VariableStructure) { + val pdfMetadata = documentObject.pdfMetadata ?: return + this.addSheetNameVariable(variableStructure, SheetNameType.PDF_TITLE, "TaggingTitle", pdfMetadata.title) + this.addSheetNameVariable(variableStructure, SheetNameType.PDF_AUTHOR, "TaggingAuthor", pdfMetadata.author) + this.addSheetNameVariable(variableStructure, SheetNameType.PDF_SUBJECT, "TaggingSubject", pdfMetadata.subject) + this.addSheetNameVariable(variableStructure, SheetNameType.PDF_KEYWORDS, "TaggingKeywords", pdfMetadata.keywords) + this.addSheetNameVariable(variableStructure, SheetNameType.PDF_PRODUCER, "TaggingProduce", pdfMetadata.producer) + } + + private fun Layout.addSheetNameVariable( + variableStructure: VariableStructure, + type: SheetNameType, + variableName: String, + value: List?, + ) { + if (value.isNullOrEmpty()) return + val variable = this.data + .addVariable() + .setName(variableName) + .setKind(VariableKind.CALCULATED) + .setScript(variableStringContentToScript(value, this, variableStructure, variableRepository::findOrFail)) + this.pages.addSheetName(type, variable) } sealed interface ScriptResult { diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InteractiveDocumentObjectBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InteractiveDocumentObjectBuilder.kt index a158a30f..4350c620 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InteractiveDocumentObjectBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InteractiveDocumentObjectBuilder.kt @@ -100,9 +100,11 @@ class InteractiveDocumentObjectBuilder( val languages = collectLanguages(documentObject) val variableStructure = initVariableStructure(layout, documentObject.variableStructureRef?.id) - addPdfMetadataToPages(layout, documentObject, variableStructure) + layout.addPdfMetadataToPages(documentObject, variableStructure) val interactiveFlowsWithContent = mutableMapOf>() + var usedSmsModel: DocumentObject? = null + var usedEmailModel: DocumentObject? = null when (documentObject.type) { DocumentObjectType.Snippet -> return snippetBuilder.buildSnippet( documentObject, @@ -129,8 +131,9 @@ class InteractiveDocumentObjectBuilder( } } DocumentObjectType.Email -> { - val bodyFlow = currentBaseTemplateData.emailFlowNamesToIds["Body Content"] + val bodyFlow = currentBaseTemplateData.emailFlowId ?: throw IllegalStateException("Base template '$baseTemplatePath' does not contain email interactive flow.") + usedEmailModel = referencedModel for (pageContentPart in referencedModel.content.paragraphIfEmpty()) { interactiveFlowsWithContent .getOrPut(bodyFlow) { mutableListOf() } @@ -138,8 +141,9 @@ class InteractiveDocumentObjectBuilder( } } DocumentObjectType.Sms -> { - val bodyFlow = currentBaseTemplateData.smsFlowNamesToIds["SMS Content"] + val bodyFlow = currentBaseTemplateData.smsFlowId ?: throw IllegalStateException("Base template '$baseTemplatePath' does not contain SMS interactive flow.") + usedSmsModel = referencedModel for (pageContentPart in referencedModel.content.paragraphIfEmpty()) { interactiveFlowsWithContent .getOrPut(bodyFlow) { mutableListOf() } @@ -159,6 +163,14 @@ class InteractiveDocumentObjectBuilder( val hasMultipleFlows = interactiveFlowsWithContent.size > 1 + if (usedSmsModel != null) { + layout.addSmsNumberToPages(usedSmsModel, variableStructure) + } + + if (usedEmailModel != null) { + layout.addEmailMetadataToPages(usedEmailModel, variableStructure) + } + interactiveFlowsWithContent.forEach { val interactiveFlowText = layout.addFlow().setId(it.key).setType(Flow.Type.SIMPLE).setSectionFlow(true).setWebEditingType(SECTION) diff --git a/migration-library/src/main/kotlin/com/quadient/migration/shared/DocumentObjectOptions.kt b/migration-library/src/main/kotlin/com/quadient/migration/shared/DocumentObjectOptions.kt deleted file mode 100644 index 2e69a5a3..00000000 --- a/migration-library/src/main/kotlin/com/quadient/migration/shared/DocumentObjectOptions.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.quadient.migration.shared - -import kotlinx.serialization.Serializable - -@Serializable -sealed interface DocumentObjectOptions - -@Serializable -data class PageOptions( - val width: Size?, - val height: Size?, -) : DocumentObjectOptions - -@Serializable -data class EmailOptions( - val width: Double?, - val backgroundFill: Color, -) : DocumentObjectOptions diff --git a/migration-library/src/test/kotlin/com/quadient/migration/persistence/DocumentObjectRepositoryTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/persistence/DocumentObjectRepositoryTest.kt index 0b5c6222..8ffe2eb4 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/persistence/DocumentObjectRepositoryTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/persistence/DocumentObjectRepositoryTest.kt @@ -7,7 +7,9 @@ import com.quadient.migration.shared.GridAlignment import com.quadient.migration.api.dto.migrationmodel.Paragraph import com.quadient.migration.api.dto.migrationmodel.StringValue import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder +import com.quadient.migration.api.dto.migrationmodel.builder.EmailObjectBuilder import com.quadient.migration.api.dto.migrationmodel.builder.ParagraphBuilder +import com.quadient.migration.api.dto.migrationmodel.builder.SmsObjectBuilder import com.quadient.migration.api.dto.migrationmodel.builder.TableBuilder import com.quadient.migration.api.dto.migrationmodel.builder.documentcontent.GridLayoutBuilder import com.quadient.migration.api.repository.StatusTrackingRepository @@ -256,6 +258,43 @@ class DocumentObjectRepositoryTest { result.shouldBeEqualTo(updatedDto) } + @Test + fun `emailOptions roundtrip with all fields`() { + val dto = EmailObjectBuilder("email1") + .options { + width(600.0) + backgroundFill("#ffffff") + from("sender@example.com") + fromName("Sender Name") + subject("Test Subject") + to("recipient@example.com") + } + .string("Email body") + .build() + + repo.upsert(dto) + val result = repo.find(dto.id)!! + + val updatedDto = dto.copy(lastUpdated = result.lastUpdated, created = result.created) + result.shouldBeEqualTo(updatedDto) + } + + @Test + fun `smsOptions roundtrip with all fields`() { + val dto = SmsObjectBuilder("sms1") + .options { + numberTo("+1234567890") + } + .string("SMS body") + .build() + + repo.upsert(dto) + val result = repo.find(dto.id)!! + + val updatedDto = dto.copy(lastUpdated = result.lastUpdated, created = result.created) + result.shouldBeEqualTo(updatedDto) + } + @Test fun `gridLayout roundtrip`() { val gridLayout = GridLayoutBuilder() diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/IcmDataCacheTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/IcmDataCacheTest.kt index 574f27fc..0ff75734 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/IcmDataCacheTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/IcmDataCacheTest.kt @@ -2851,9 +2851,15 @@ class IcmDataCacheTest { val result = subject.getOrLoadBaseTemplateData(path)!! // then - result.interactiveFlowNamesToIds.shouldBeEqualTo(mapOf("Letter Content" to "Def.InteractiveFlow0")) - result.emailFlowNamesToIds.shouldBeEqualTo(mapOf("Flow BT 5" to "Def.InteractiveFlow1", "Body Content" to "Def.InteractiveFlow1")) - result.smsFlowNamesToIds.shouldBeEqualTo(mapOf("Flow BT 7" to "Def.InteractiveFlow2", "SMS Content" to "Def.InteractiveFlow2")) + result.interactiveFlowNamesToIds.shouldBeEqualTo(mapOf( + "Letter Content" to "Def.InteractiveFlow0", + "Flow BT 5" to "Def.InteractiveFlow1", + "Body Content" to "Def.InteractiveFlow1", + "Flow BT 7" to "Def.InteractiveFlow2", + "SMS Content" to "Def.InteractiveFlow2", + )) + result.emailFlowId.shouldBeEqualTo("Def.InteractiveFlow1") + result.smsFlowId.shouldBeEqualTo("Def.InteractiveFlow2") } @Test diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilderTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilderTest.kt index 0a81a985..d9564d6e 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilderTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilderTest.kt @@ -42,7 +42,7 @@ import com.quadient.migration.shared.DocumentObjectType.* import com.quadient.migration.shared.IcmPath import com.quadient.migration.shared.Literal import com.quadient.migration.shared.LiteralDataType -import com.quadient.migration.shared.PageOptions +import com.quadient.migration.api.dto.migrationmodel.PageOptions import com.quadient.migration.shared.Position import com.quadient.migration.shared.Size import com.quadient.migration.shared.SkipOptions @@ -1146,6 +1146,92 @@ class DesignerDocumentObjectBuilderTest { .shouldBeEqualTo("SMS content") } + @Test + fun `buildDocumentObject sets email width and bg fill on ECRoot when email options has width`() { + // given + val emailDoc = EmailObjectBuilder("E_1") + .string("Email content") + .options { + width(600.0) + backgroundFill("#ff0000") + } + .build().mock() + val template = DocumentObjectBuilder("T_1", Template).documentObjectRef(emailDoc).build().mock() + + // when + val result = subject.buildDocumentObject(template).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + result["ECRoot"]["EmailWidth"].stringValue().shouldBeEqualTo("600px") + result["ECRoot"]["FillStyleId"].stringValue().shouldNotBeEmpty() + } + + @Test + fun `buildDocumentObject creates email sheet name variables when email options has metadata`() { + // given + val emailDoc = EmailObjectBuilder("E_1") + .string("Email content") + .options { + from("sender@example.com") + fromName("Sender Name") + subject("Test Subject") + to("recipient@example.com") + } + .build().mock() + val template = DocumentObjectBuilder("T_1", Template).documentObjectRef(emailDoc).build().mock() + + // when + val result = subject.buildDocumentObject(template).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + val emailSheetNameVariableIds = result["Pages"]["SheetNameVariableId"] + .toList() + .map { it.stringValue() ?: "" } + .filter { it.isNotBlank() } + emailSheetNameVariableIds.size.shouldBeEqualTo(4) + + val variableNames = emailSheetNameVariableIds.map { varId -> + result["Variable"].first { it["Id"].stringValue() == varId }["Name"].stringValue() + } + variableNames.shouldBeEqualTo(listOf("EmailFromName", "EmailFrom", "EmailTo", "EmailSubject")) + + val variableScripts = emailSheetNameVariableIds.map { varId -> + result["Variable"].last { it["Id"].stringValue() == varId }["Script"].stringValue() + } + variableScripts.shouldBeEqualTo(listOf( + "return 'Sender Name';", + "return 'sender@example.com';", + "return 'recipient@example.com';", + "return 'Test Subject';", + )) + } + + @Test + fun `buildDocumentObject creates sms sheet name variable when sms options has numberTo`() { + // given + val smsDoc = SmsObjectBuilder("S_1") + .string("SMS content") + .options { numberTo("+1234567890") } + .build().mock() + val template = DocumentObjectBuilder("T_1", Template).documentObjectRef(smsDoc).build().mock() + + // when + val result = subject.buildDocumentObject(template).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + val smsSheetNameVariableIds = result["Pages"]["SheetNameVariableId"] + .toList() + .map { it.stringValue() ?: "" } + .filter { it.isNotBlank() } + smsSheetNameVariableIds.size.shouldBeEqualTo(1) + + val variableId = smsSheetNameVariableIds.first() + result["Variable"].first { it["Id"].stringValue() == variableId }["Name"].stringValue() + .shouldBeEqualTo("NumberTo") + result["Variable"].last { it["Id"].stringValue() == variableId }["Script"].stringValue() + .shouldBeEqualTo("return '+1234567890';") + } + private fun DocumentObject.mock(): DocumentObject { val id = this.id every { documentObjectRepository.findOrFail(id) } returns this diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InteractiveDocumentObjectBuilderTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InteractiveDocumentObjectBuilderTest.kt index fbafa2f2..2c50b5d9 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InteractiveDocumentObjectBuilderTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InteractiveDocumentObjectBuilderTest.kt @@ -18,9 +18,11 @@ import com.quadient.migration.api.dto.migrationmodel.VariableRef import com.quadient.migration.api.dto.migrationmodel.VariableStructure import com.quadient.migration.api.dto.migrationmodel.builder.DisplayRuleBuilder import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder +import com.quadient.migration.api.dto.migrationmodel.builder.EmailObjectBuilder import com.quadient.migration.api.dto.migrationmodel.builder.ImageBuilder import com.quadient.migration.api.dto.migrationmodel.builder.ParagraphBuilder import com.quadient.migration.api.dto.migrationmodel.builder.ParagraphStyleBuilder +import com.quadient.migration.api.dto.migrationmodel.builder.SmsObjectBuilder import com.quadient.migration.api.dto.migrationmodel.builder.TextStyleBuilder import com.quadient.migration.api.dto.migrationmodel.builder.VariableBuilder import com.quadient.migration.api.dto.migrationmodel.builder.VariableStructureBuilder @@ -1649,6 +1651,250 @@ class InteractiveDocumentObjectBuilderTest { private fun ecContentForGrid(result: tools.jackson.databind.JsonNode, id: String): tools.jackson.databind.JsonNode = result["ECContent"].toList().first { it["Id"]?.stringValue() == id && it["ContentId"] != null } + @Test + fun `build template with email model reference maps content to email interactive flow`() { + // given + val emailDoc = EmailObjectBuilder("E_1").string("Email content").build().mock() + val template = DocumentObjectBuilder("T_1", Template).documentObjectRef(emailDoc).build() + + every { ipsService.wfd2xml(getBaseTemplateFullPath(config, null)) } returns """ + + + + + 65 + Letter Content + + + 66 + Body Content + {"customName":"Body Content"} + + + + 65 + Normal + + + 66 + HTML + + + + Def.EmailsHeader + Header + + + + Def.EmailsBody + Body + 66 + + + Def.EmailsFooter + Footer + + + + + + """.trimIndent() + + // when + val result = subject.buildDocumentObject(template).let { xmlMapper.readTree(it.trimIndent()) } + + // then + val emailInteractiveFlow = result["Flow"].first { it["Id"].stringValue() == "Def.InteractiveFlow1" } + val emailFlowContentId = emailInteractiveFlow["FlowContent"]["P"]["T"]["O"]["Id"].stringValue() + result["Flow"].last { it["Id"].stringValue() == emailFlowContentId }["FlowContent"]["P"]["T"][""].stringValue() + .shouldBeEqualTo("Email content") + } + + @Test + fun `build template with sms model reference maps content to sms interactive flow`() { + // given + val smsDoc = SmsObjectBuilder("S_1").string("SMS content").build().mock() + val template = DocumentObjectBuilder("T_1", Template).documentObjectRef(smsDoc).build() + + every { ipsService.wfd2xml(getBaseTemplateFullPath(config, null)) } returns """ + + + + + 65 + Letter Content + + + 67 + SMS Content + {"customName":"SMS Content"} + + + + 65 + Normal + + + 67 + Normal + + + + Def.SMSRoot + 67 + + + + + """.trimIndent() + + // when + val result = subject.buildDocumentObject(template).let { xmlMapper.readTree(it.trimIndent()) } + + // then + val smsInteractiveFlow = result["Flow"].first { it["Id"].stringValue() == "Def.InteractiveFlow1" } + val smsFlowContentId = smsInteractiveFlow["FlowContent"]["P"]["T"]["O"]["Id"].stringValue() + result["Flow"].last { it["Id"].stringValue() == smsFlowContentId }["FlowContent"]["P"]["T"][""].stringValue() + .shouldBeEqualTo("SMS content") + } + + @Test + fun `build template with email model and options creates email sheet name variables`() { + // given + val emailDoc = EmailObjectBuilder("E_1") + .string("Email content") + .options { + from("sender@example.com") + fromName("Sender Name") + subject("Test Subject") + to("recipient@example.com") + } + .build().mock() + val template = DocumentObjectBuilder("T_1", Template).documentObjectRef(emailDoc).build() + + every { ipsService.wfd2xml(getBaseTemplateFullPath(config, null)) } returns """ + + + + + 65 + Letter Content + + + 66 + Body Content + {"customName":"Body Content"} + + + + 65 + Normal + + + 66 + HTML + + + + Def.EmailsHeader + Header + + + + Def.EmailsBody + Body + 66 + + + Def.EmailsFooter + Footer + + + + + + """.trimIndent() + + // when + val result = subject.buildDocumentObject(template).let { xmlMapper.readTree(it.trimIndent()) } + + // then + val allSheetNameVariableIds = result["Pages"]["SheetNameVariableId"].toList().map { it.stringValue() ?: "" } + val emailSheetNameVariableIds = allSheetNameVariableIds.filter { it.isNotBlank() } + emailSheetNameVariableIds.size.shouldBeEqualTo(4) + + val variableNames = emailSheetNameVariableIds.map { varId -> + result["Variable"].first { it["Id"].stringValue() == varId }["Name"].stringValue() + } + variableNames.shouldBeEqualTo(listOf("EmailFromName", "EmailFrom", "EmailTo", "EmailSubject")) + + val variableScripts = emailSheetNameVariableIds.map { varId -> + result["Variable"].last { it["Id"].stringValue() == varId }["Script"].stringValue() + } + variableScripts.shouldBeEqualTo(listOf( + "return 'Sender Name';", + "return 'sender@example.com';", + "return 'recipient@example.com';", + "return 'Test Subject';", + )) + } + + @Test + fun `build template with sms model and options creates sms sheet name variable`() { + // given + val smsDoc = SmsObjectBuilder("S_1") + .string("SMS content") + .options { numberTo("+1234567890") } + .build().mock() + val template = DocumentObjectBuilder("T_1", Template).documentObjectRef(smsDoc).build() + + every { ipsService.wfd2xml(getBaseTemplateFullPath(config, null)) } returns """ + + + + + 65 + Letter Content + + + 67 + SMS Content + {"customName":"SMS Content"} + + + + 65 + Normal + + + 67 + Normal + + + + Def.SMSRoot + 67 + + + + + """.trimIndent() + + // when + val result = subject.buildDocumentObject(template).let { xmlMapper.readTree(it.trimIndent()) } + + // then + val allSheetNameVariableIds = result["Pages"]["SheetNameVariableId"].toList().map { it.stringValue() ?: "" } + val smsSheetNameVariableIds = allSheetNameVariableIds.filter { it.isNotBlank() } + smsSheetNameVariableIds.size.shouldBeEqualTo(1) + + val variableId = smsSheetNameVariableIds.first() + result["Variable"].first { it["Id"].stringValue() == variableId }["Name"].stringValue() + .shouldBeEqualTo("NumberTo") + result["Variable"].last { it["Id"].stringValue() == variableId }["Script"].stringValue() + .shouldBeEqualTo("return '+1234567890';") + } + private fun DocumentObject.mock(): DocumentObject { val id = this.id every { documentObjectRepository.findOrFail(id) } returns this diff --git a/migration-library/src/test/kotlin/com/quadient/migration/tools/TestObjectBuilders.kt b/migration-library/src/test/kotlin/com/quadient/migration/tools/TestObjectBuilders.kt index b8fe9b34..360122d7 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/tools/TestObjectBuilders.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/tools/TestObjectBuilders.kt @@ -47,7 +47,7 @@ import com.quadient.migration.shared.Alignment import com.quadient.migration.shared.BorderOptions import com.quadient.migration.shared.Color import com.quadient.migration.shared.DataType -import com.quadient.migration.shared.DocumentObjectOptions +import com.quadient.migration.api.dto.migrationmodel.DocumentObjectOptions import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.IcmPath import com.quadient.migration.shared.ImageType diff --git a/migration-library/src/test/kotlin/com/quadient/migration/tools/model/TestModelObjectBuilders.kt b/migration-library/src/test/kotlin/com/quadient/migration/tools/model/TestModelObjectBuilders.kt index 928ab27b..a5be8190 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/tools/model/TestModelObjectBuilders.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/tools/model/TestModelObjectBuilders.kt @@ -30,7 +30,7 @@ import com.quadient.migration.shared.BorderOptions import com.quadient.migration.shared.Color import com.quadient.migration.shared.DataType import com.quadient.migration.shared.DisplayRuleDefinition -import com.quadient.migration.shared.DocumentObjectOptions +import com.quadient.migration.api.dto.migrationmodel.DocumentObjectOptions import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.Group import com.quadient.migration.shared.GroupOp diff --git a/wfd-xml/api/src/main/java/com/quadient/wfdxml/api/layoutnodes/SheetNameType.java b/wfd-xml/api/src/main/java/com/quadient/wfdxml/api/layoutnodes/SheetNameType.java index 51c95090..2cb33fae 100644 --- a/wfd-xml/api/src/main/java/com/quadient/wfdxml/api/layoutnodes/SheetNameType.java +++ b/wfd-xml/api/src/main/java/com/quadient/wfdxml/api/layoutnodes/SheetNameType.java @@ -5,7 +5,16 @@ public enum SheetNameType { PDF_AUTHOR(38), PDF_SUBJECT(39), PDF_KEYWORDS(40), - PDF_PRODUCER(41); + PDF_PRODUCER(41), + + EMAIL_FROM_NAME(47), + EMAIL_FROM(48), + + EMAIL_TO(50), + + EMAIL_SUBJECT(57), + + SMS_NUMBER_TO(79); private final int index; diff --git a/wfd-xml/impl/src/test/groovy/com/quadient/wfdxml/internal/layoutnodes/PagesImplTest.groovy b/wfd-xml/impl/src/test/groovy/com/quadient/wfdxml/internal/layoutnodes/PagesImplTest.groovy index eb45dc5e..a6d458d3 100644 --- a/wfd-xml/impl/src/test/groovy/com/quadient/wfdxml/internal/layoutnodes/PagesImplTest.groovy +++ b/wfd-xml/impl/src/test/groovy/com/quadient/wfdxml/internal/layoutnodes/PagesImplTest.groovy @@ -3,6 +3,7 @@ package com.quadient.wfdxml.internal.layoutnodes import com.quadient.wfdxml.api.layoutnodes.Flow import com.quadient.wfdxml.api.layoutnodes.Pages import com.quadient.wfdxml.api.layoutnodes.SheetNameType +import groovy.xml.XmlParser import com.quadient.wfdxml.internal.layoutnodes.data.VariableImpl import com.quadient.wfdxml.internal.xml.export.XmlExporter import com.quadient.wfdxml.utils.AssertXml @@ -203,6 +204,57 @@ class PagesImplTest extends Specification { (xml =~ /<\/SheetNameVariableId>/).count == 39 } + def "export Pages with sheet names - SMS"() { + given: + VariableImpl var79 = new VariableImpl() + String id79 = exporter.idRegister.getOrCreateId(var79) + + PagesImpl pages = new PagesImpl() + .addSheetName(SheetNameType.SMS_NUMBER_TO, var79) + + when: + pages.export(exporter) + + then: + def xml = exporter.buildString() + List sheetIds = new XmlParser().parseText("$xml")['SheetNameVariableId'] as List + sheetIds.size() == 80 + sheetIds[79].text() == id79 + (0..78).every { sheetIds[it as int].text() == "" } + } + + def "export Pages with sheet names - all email fields"() { + given: + VariableImpl var47 = new VariableImpl() + VariableImpl var48 = new VariableImpl() + VariableImpl var50 = new VariableImpl() + VariableImpl var57 = new VariableImpl() + + String id47 = exporter.idRegister.getOrCreateId(var47) + String id48 = exporter.idRegister.getOrCreateId(var48) + String id50 = exporter.idRegister.getOrCreateId(var50) + String id57 = exporter.idRegister.getOrCreateId(var57) + + PagesImpl pages = new PagesImpl() + .addSheetName(SheetNameType.EMAIL_FROM_NAME, var47) + .addSheetName(SheetNameType.EMAIL_FROM, var48) + .addSheetName(SheetNameType.EMAIL_TO, var50) + .addSheetName(SheetNameType.EMAIL_SUBJECT, var57) + + when: + pages.export(exporter) + + then: + def xml = exporter.buildString() + List sheetIds = new XmlParser().parseText("$xml")['SheetNameVariableId'] as List + sheetIds.size() == 58 + sheetIds[47].text() == id47 + sheetIds[48].text() == id48 + sheetIds[50].text() == id50 + sheetIds[57].text() == id57 + (0..56).findAll { !(it in [47, 48, 50, 57]) }.every { sheetIds[it as int].text() == "" } + } + def "export Pages without sheet names - should not add SheetNameVariableId"() { given: PagesImpl pages = new PagesImpl() From 2d4c00d341216d8ad171f7add2bf54491dbbfc73 Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Fri, 17 Jul 2026 13:41:16 +0200 Subject: [PATCH 2/5] MIG-562 Base Template definition and deployment - Initial support - Base template boiler plate - tables, mappings, etc. --- migration-examples/layout/index.html | 4 +- .../common/mapping/VariablesImport.groovy | 6 + .../com/quadient/migration/api/Migration.kt | 3 + .../api/dto/migrationmodel/BaseTemplate.kt | 15 ++ .../api/dto/migrationmodel/DisplayRule.kt | 6 +- .../api/dto/migrationmodel/DocumentObject.kt | 6 +- .../api/dto/migrationmodel/Mapping.kt | 20 ++- .../migration/api/dto/migrationmodel/Ref.kt | 33 ++++ .../builder/DisplayRuleBuilder.kt | 3 +- .../builder/DocumentObjectBuilder.kt | 3 +- .../migrationmodel/builder/SnippetBuilder.kt | 5 +- .../builder/components/HasBaseTemplate.kt | 32 +++- .../api/repository/BaseTemplateRepository.kt | 96 ++++++++++ .../api/repository/DisplayRuleRepository.kt | 8 +- .../repository/DocumentObjectRepository.kt | 4 +- .../api/repository/MappingRepository.kt | 41 +++-- .../migrationmodel/MappingEntity.kt | 35 +++- .../persistence/migrationmodel/RefEntity.kt | 12 ++ .../persistence/table/BaseTemplateTable.kt | 10 ++ .../persistence/table/DisplayRuleTable.kt | 3 +- .../persistence/table/DocumentObjectTable.kt | 6 +- .../upgrade/V16__add_base_template.kt | 60 +++++++ .../migration/service/DeployPhaseUtils.kt | 16 +- .../migration/service/RefCollector.kt | 4 + .../migration/service/ReferenceValidator.kt | 13 ++ .../migration/service/StylesValidator.kt | 2 + .../migration/service/deploy/DeployClient.kt | 5 +- .../service/deploy/InteractiveDeployClient.kt | 3 +- .../service/deploy/utility/ProgressReport.kt | 2 + .../migration/shared/BaseTemplatePage.kt | 18 ++ .../migration/shared/JrdDefinition.kt | 3 +- .../persistence/DisplayRuleRepositoryTest.kt | 8 +- .../DocumentObjectRepositoryTest.kt | 6 +- .../persistence/MappingRepositoryTest.kt | 41 ++++- .../migrationmodel/MappingEntityTest.kt | 5 +- .../persistence/upgrade/V16MigrationTest.kt | 167 ++++++++++++++++++ .../migration/service/DeployPhaseUtilsTest.kt | 5 +- .../service/ReferenceValidatorTest.kt | 3 + .../service/deploy/EvolveDeployClientTest.kt | 2 +- .../migration/tools/TestObjectBuilders.kt | 7 +- .../tools/model/TestModelObjectBuilders.kt | 7 +- 41 files changed, 658 insertions(+), 70 deletions(-) create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/BaseTemplate.kt create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/api/repository/BaseTemplateRepository.kt create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/persistence/table/BaseTemplateTable.kt create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/persistence/upgrade/V16__add_base_template.kt create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/shared/BaseTemplatePage.kt create mode 100644 migration-library/src/test/kotlin/com/quadient/migration/persistence/upgrade/V16MigrationTest.kt diff --git a/migration-examples/layout/index.html b/migration-examples/layout/index.html index 3af7fc91..7f9a56a8 100644 --- a/migration-examples/layout/index.html +++ b/migration-examples/layout/index.html @@ -1979,8 +1979,8 @@ */ function areaLabel(preview) { if (!preview) return ""; - const m = preview.match(/\(([^)]+)\)$/); - return m ? m[1] : preview.slice(0, 40); + const firstChunk = preview.split(";")[0]; + return firstChunk.slice(0, 40); } /** diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/VariablesImport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/VariablesImport.groovy index fd9ca30e..c6da7832 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/VariablesImport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/VariablesImport.groovy @@ -10,6 +10,7 @@ package com.quadient.migration.example.common.mapping import com.quadient.migration.api.Migration import com.quadient.migration.api.dto.migrationmodel.MappingItem import com.quadient.migration.api.dto.migrationmodel.VariableRef +import com.quadient.migration.api.dto.migrationmodel.builder.VariableStructureBuilder import com.quadient.migration.example.common.util.Csv import com.quadient.migration.example.common.util.Mapping import com.quadient.migration.shared.DataType @@ -39,6 +40,11 @@ static void run(Migration migration, Path path) { def structureId = Mapping.variableStructureIdFromFileName(path.fileName.toString(), migration.projectConfig.name) def structureMapping = migration.mappingRepository.getVariableStructureMapping(structureId) + def existingStructure = migration.variableStructureRepository.find(structureId) + if (existingStructure == null) { + migration.variableStructureRepository.upsert(new VariableStructureBuilder(structureId).build()) + } + def languageVariableFound = false def mappings = new HashMap() for (line in lines) { diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/Migration.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/Migration.kt index 555a0c9f..95348178 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/Migration.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/Migration.kt @@ -96,6 +96,7 @@ class Migration(val config: MigConfig, val projectConfig: ProjectConfig) { singleOf(::ParagraphStyleRepository) singleOf(::VariableStructureRepository) singleOf(::DisplayRuleRepository) + singleOf(::BaseTemplateRepository) singleOf(::ImageRepository) singleOf(::AttachmentRepository) singleOf(::StatusTrackingRepository) @@ -128,6 +129,7 @@ class Migration(val config: MigConfig, val projectConfig: ProjectConfig) { val paragraphStyleRepository: ParagraphStyleRepository by lazy { koin.get() } val variableStructureRepository: VariableStructureRepository by lazy { koin.get() } val displayRuleRepository: DisplayRuleRepository by lazy { koin.get() } + val baseTemplateRepository: BaseTemplateRepository by lazy { koin.get() } val imageRepository: ImageRepository by lazy { koin.get() } val attachmentRepository: AttachmentRepository by lazy { koin.get() } val statusTrackingRepository: StatusTrackingRepository by lazy { koin.get() } @@ -164,6 +166,7 @@ class Migration(val config: MigConfig, val projectConfig: ProjectConfig) { repositories.add(paragraphStyleRepository) repositories.add(variableStructureRepository) repositories.add(displayRuleRepository) + repositories.add(baseTemplateRepository) repositories.add(imageRepository) repositories.add(attachmentRepository) diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/BaseTemplate.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/BaseTemplate.kt new file mode 100644 index 00000000..e2c9cc29 --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/BaseTemplate.kt @@ -0,0 +1,15 @@ +package com.quadient.migration.api.dto.migrationmodel + +import com.quadient.migration.shared.BaseTemplatePage +import kotlin.time.Instant + +data class BaseTemplate( + override val id: String, + override var name: String? = null, + override var originLocations: List = emptyList(), + override var customFields: CustomFieldMap, + var targetFolder: String? = null, + var pages: List = emptyList(), + override var created: Instant? = null, + override var lastUpdated: Instant? = null, +) : MigrationObject diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/DisplayRule.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/DisplayRule.kt index a135c1c3..d08e199b 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/DisplayRule.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/DisplayRule.kt @@ -17,10 +17,12 @@ data class DisplayRule @JvmOverloads constructor( val metadata: List = emptyList(), val subject: String? = null, val targetFolder: String? = null, - val baseTemplate: String? = null, + val baseTemplate: BaseTemplateLocation? = null, val variableStructureRef: VariableStructureRef? = null, ) : MigrationObject, RefValidatable { override fun collectRefs(): List { - return (definition?.collectRefs() ?: emptyList()) + listOfNotNull(variableStructureRef, targetId) + val baseTemplateRef = baseTemplate as? BaseTemplateRef + + return (definition?.collectRefs() ?: emptyList()) + listOfNotNull(variableStructureRef, targetId, baseTemplateRef) } } \ No newline at end of file diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/DocumentObject.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/DocumentObject.kt index 5fa97da1..17ef94b7 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/DocumentObject.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/DocumentObject.kt @@ -16,7 +16,7 @@ data class DocumentObject( var targetFolder: String? = null, var displayRuleRef: DisplayRuleRef? = null, var variableStructureRef: VariableStructureRef? = null, - var baseTemplate: String? = null, + var baseTemplate: BaseTemplateLocation? = null, var options: DocumentObjectOptions? = null, var pdfMetadata: PdfMetadata? = null, override var created: Instant? = null, @@ -35,6 +35,8 @@ data class DocumentObject( val pdfMetadataRefs = pdfMetadata?.collectRefs().orEmpty() - return contentRefs + pdfMetadataRefs + listOfNotNull(displayRuleRef, variableStructureRef) + val baseTemplateRef = baseTemplate as? BaseTemplateRef + + return contentRefs + pdfMetadataRefs + listOfNotNull(displayRuleRef, variableStructureRef, baseTemplateRef) } } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Mapping.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Mapping.kt index 59d8692b..f172b530 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Mapping.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Mapping.kt @@ -13,7 +13,7 @@ sealed class MappingItem { data class DocumentObject( override var name: String?, var internal: Boolean?, - var baseTemplate: String?, + var baseTemplate: BaseTemplateLocation?, var targetFolder: String?, var type: DocumentObjectType?, var variableStructureRef: String?, @@ -98,11 +98,17 @@ sealed class MappingItem { override var name: String?, var targetFolder: String?, var targetId: String?, - var baseTemplate: String?, + var baseTemplate: BaseTemplateLocation?, var variableStructureRef: String?, var internal: Boolean?, ) : MappingItem() + data class BaseTemplate( + override var name: String?, + var targetFolder: String?, + var pages: List = emptyList(), + ) : MappingItem() + data class Table( override var name: String? = null, val tables: List = emptyList(), @@ -123,7 +129,7 @@ sealed class MappingItem { MappingItemEntity.DocumentObject( name = this.name, internal = this.internal, - baseTemplate = this.baseTemplate, + baseTemplate = this.baseTemplate?.toDb(), targetFolder = this.targetFolder, type = this.type, variableStructureRef = this.variableStructureRef, @@ -135,6 +141,12 @@ sealed class MappingItem { name = this.name, areas = this.areas, flowToNextPage = this.flowToNextPage ) + is MappingItem.BaseTemplate -> MappingItemEntity.BaseTemplate( + name = this.name, + targetFolder = this.targetFolder, + pages = this.pages, + ) + is MappingItem.Image -> { MappingItemEntity.Image( name = this.name, @@ -221,7 +233,7 @@ sealed class MappingItem { internal = this.internal, variableStructureRef = this.variableStructureRef, targetFolder = this.targetFolder, - baseTemplate = this.baseTemplate, + baseTemplate = this.baseTemplate?.toDb(), ) is MappingItem.Table -> MappingItemEntity.Table( diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Ref.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Ref.kt index d1bd3fec..3492b400 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Ref.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Ref.kt @@ -1,6 +1,8 @@ package com.quadient.migration.api.dto.migrationmodel import com.quadient.migration.persistence.migrationmodel.ColumnLayoutEntity +import com.quadient.migration.persistence.migrationmodel.BaseTemplateEntityRef +import com.quadient.migration.persistence.migrationmodel.BaseTemplateLocationEntity import com.quadient.migration.persistence.migrationmodel.DisplayRuleEntityRef import com.quadient.migration.persistence.migrationmodel.DocumentObjectEntityRef import com.quadient.migration.persistence.migrationmodel.FirstMatchEntity @@ -8,6 +10,7 @@ import com.quadient.migration.persistence.migrationmodel.HyperlinkEntity import com.quadient.migration.persistence.migrationmodel.ImageEntityRef import com.quadient.migration.persistence.migrationmodel.AttachmentEntityRef import com.quadient.migration.persistence.migrationmodel.BarcodeEntity +import com.quadient.migration.persistence.migrationmodel.LiteralBaseTemplatePathEntity import com.quadient.migration.persistence.migrationmodel.ParagraphStyleEntityRef import com.quadient.migration.persistence.migrationmodel.ResourceEntityRef import com.quadient.migration.persistence.migrationmodel.StringEntity @@ -138,6 +141,36 @@ data class VariableStructureRef(override val id: String) : Ref(id) { fun toDb() = VariableStructureEntityRef(id) } +sealed interface BaseTemplateLocation { + companion object { + fun fromDb(entity: BaseTemplateLocationEntity): BaseTemplateLocation = when (entity) { + is LiteralBaseTemplatePathEntity -> LiteralBaseTemplatePath.fromDb(entity) + is BaseTemplateEntityRef -> BaseTemplateRef.fromDb(entity) + } + } +} + +fun BaseTemplateLocation.toDb(): BaseTemplateLocationEntity = when (this) { + is LiteralBaseTemplatePath -> this.toDb() + is BaseTemplateRef -> this.toDb() +} + +data class LiteralBaseTemplatePath(val path: String) : BaseTemplateLocation { + companion object { + fun fromDb(entity: LiteralBaseTemplatePathEntity) = LiteralBaseTemplatePath(entity.path) + } + + fun toDb() = LiteralBaseTemplatePathEntity(path) +} + +data class BaseTemplateRef(override val id: String) : Ref(id), BaseTemplateLocation { + companion object { + fun fromDb(entity: BaseTemplateEntityRef) = BaseTemplateRef(entity.id) + } + + fun toDb() = BaseTemplateEntityRef(id) +} + data class StringValue(val value: String) : VariableStringContent { override val pathName = "str" override fun toPreview(nameResolver: (DocumentContent) -> String?): String = diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/DisplayRuleBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/DisplayRuleBuilder.kt index ab35c2b9..e06860ed 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/DisplayRuleBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/DisplayRuleBuilder.kt @@ -11,6 +11,7 @@ import com.quadient.migration.api.dto.migrationmodel.builder.components.HasMetad import com.quadient.migration.api.dto.migrationmodel.builder.components.HasSubject import com.quadient.migration.api.dto.migrationmodel.builder.components.HasTargetFolder import com.quadient.migration.api.dto.migrationmodel.builder.components.HasVariableStructureRef +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateLocation import com.quadient.migration.shared.BinOp import com.quadient.migration.shared.Binary import com.quadient.migration.shared.BinaryOrGroup @@ -35,7 +36,7 @@ class DisplayRuleBuilder(id: String) : DtoBuilderBase = mutableListOf() override var variableStructureRef: VariableStructureRef? = null - override var baseTemplate: String? = null + override var baseTemplate: BaseTemplateLocation? = null override var targetFolder: String? = null var definition: DisplayRuleDefinition? = null var targetId: String? = null diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/DocumentObjectBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/DocumentObjectBuilder.kt index d0e06c74..39fd0c9e 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/DocumentObjectBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/DocumentObjectBuilder.kt @@ -19,6 +19,7 @@ import com.quadient.migration.api.dto.migrationmodel.builder.components.HasSubje import com.quadient.migration.api.dto.migrationmodel.builder.components.HasTargetFolder import com.quadient.migration.api.dto.migrationmodel.builder.components.HasVariableStructureRef import com.quadient.migration.api.dto.migrationmodel.DocumentObjectOptions +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateLocation import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.api.dto.migrationmodel.EmailOptions import com.quadient.migration.shared.MetadataEntry @@ -47,7 +48,7 @@ class DocumentObjectBuilder(id: String, private val type: DocumentObjectType) : override var variableStructureRef: VariableStructureRef? = null override var internal: Boolean = false override var targetFolder: String? = null - override var baseTemplate: String? = null + override var baseTemplate: BaseTemplateLocation? = null override var subject: String? = null override var options: DocumentObjectOptions? = null override var metadata: MutableList = mutableListOf() diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/SnippetBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/SnippetBuilder.kt index 088f60a6..dcc064b9 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/SnippetBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/SnippetBuilder.kt @@ -17,6 +17,7 @@ import com.quadient.migration.api.dto.migrationmodel.builder.components.HasSubje import com.quadient.migration.api.dto.migrationmodel.builder.components.HasTargetFolder import com.quadient.migration.api.dto.migrationmodel.builder.components.HasVariableStructureRef import com.quadient.migration.api.dto.migrationmodel.DocumentObjectOptions +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateLocation import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.MetadataEntry import com.quadient.migration.shared.SkipOptions @@ -54,7 +55,7 @@ class FirstMatchSnippetBuilder(id: String) : DtoBuilderBase = mutableListOf() @@ -118,7 +119,7 @@ class SimpleSnippetBuilder(id: String) : DtoBuilderBase = mutableListOf() diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/components/HasBaseTemplate.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/components/HasBaseTemplate.kt index 3b6306f9..34994825 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/components/HasBaseTemplate.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/components/HasBaseTemplate.kt @@ -1,13 +1,39 @@ package com.quadient.migration.api.dto.migrationmodel.builder.components +import com.quadient.migration.api.dto.migrationmodel.BaseTemplate +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateLocation +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef +import com.quadient.migration.api.dto.migrationmodel.LiteralBaseTemplatePath + @Suppress("UNCHECKED_CAST") interface HasBaseTemplate { - var baseTemplate: String? + var baseTemplate: BaseTemplateLocation? /** * Override the default base template for this object. - * @param baseTemplate Path to the base template to use for this document object. + * @param baseTemplate Location (literal path or base template reference) of the base template to use for this object. + * @return This builder instance for method chaining. + */ + fun baseTemplate(baseTemplate: BaseTemplateLocation?) = apply { this.baseTemplate = baseTemplate } as T + + /** + * Overrides the default base template for this object with a literal ICM path. + * @param path Path to the base template to use for this object. + * @return This builder instance for method chaining. + */ + fun baseTemplatePath(path: String?) = apply { this.baseTemplate = path?.let { LiteralBaseTemplatePath(it) } } as T + + /** + * Overrides the default base template for this object with a reference to a [BaseTemplate] migration object. + * @param baseTemplateId Id of the [BaseTemplate] migration object to use for this object. + * @return This builder instance for method chaining. + */ + fun baseTemplateRef(baseTemplateId: String?) = apply { this.baseTemplate = baseTemplateId?.let { BaseTemplateRef(it) } } as T + + /** + * Overrides the default base template for this object with a reference to a [BaseTemplate] migration object. + * @param baseTemplate The [BaseTemplate] migration object to use for this object. * @return This builder instance for method chaining. */ - fun baseTemplate(baseTemplate: String?) = apply { this.baseTemplate = baseTemplate } as T + fun baseTemplateRef(baseTemplate: BaseTemplate) = apply { this.baseTemplate = BaseTemplateRef(baseTemplate.id) } as T } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/repository/BaseTemplateRepository.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/repository/BaseTemplateRepository.kt new file mode 100644 index 00000000..c57ee9aa --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/repository/BaseTemplateRepository.kt @@ -0,0 +1,96 @@ +package com.quadient.migration.api.repository + +import com.quadient.migration.api.ProjectName +import com.quadient.migration.api.dto.migrationmodel.BaseTemplate +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef +import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap +import com.quadient.migration.api.dto.migrationmodel.MigrationObject +import com.quadient.migration.persistence.table.BaseTemplateTable +import com.quadient.migration.persistence.table.DocumentObjectTable +import com.quadient.migration.tools.concat +import kotlin.time.Clock +import kotlin.time.toJavaInstant +import kotlinx.serialization.json.Json +import java.sql.Types +import org.jetbrains.exposed.v1.core.ResultRow +import org.jetbrains.exposed.v1.core.eq +import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.jdbc.transactions.transaction +import org.jetbrains.exposed.v1.jdbc.upsertReturning + +class BaseTemplateRepository(projectName: ProjectName) : + Repository(BaseTemplateTable, projectName.name) { + + override fun fromDb(row: ResultRow): BaseTemplate { + return BaseTemplate( + id = row[BaseTemplateTable.id].value, + name = row[BaseTemplateTable.name], + customFields = CustomFieldMap(row[BaseTemplateTable.customFields].toMutableMap()), + lastUpdated = row[BaseTemplateTable.lastUpdated], + created = row[BaseTemplateTable.created], + originLocations = row[BaseTemplateTable.originLocations], + targetFolder = row[BaseTemplateTable.targetFolder], + pages = row[BaseTemplateTable.pages], + ) + } + + override fun findUsages(id: String): List { + return transaction { + DocumentObjectTable.selectAll().where { DocumentObjectTable.projectName eq projectName } + .map { DocumentObjectTable.fromResultRow(it) } + .filter { it.collectRefs().any { ref -> ref is BaseTemplateRef && ref.id == id } } + .distinct() + } + } + + override fun upsert(dto: BaseTemplate) { + upsertInternal { + val existingItem = table.selectAll().where(filter(dto.id)).firstOrNull()?.let(::fromDb) + + val now = Clock.System.now() + + table.upsertReturning(table.id, table.projectName) { + it[BaseTemplateTable.id] = dto.id + it[BaseTemplateTable.projectName] = this@BaseTemplateRepository.projectName + it[BaseTemplateTable.name] = dto.name + it[BaseTemplateTable.originLocations] = existingItem?.originLocations.concat(dto.originLocations).distinct() + it[BaseTemplateTable.customFields] = dto.customFields.inner + it[BaseTemplateTable.created] = existingItem?.created ?: now + it[BaseTemplateTable.lastUpdated] = now + it[BaseTemplateTable.targetFolder] = dto.targetFolder + it[BaseTemplateTable.pages] = dto.pages + }.first() + } + } + + override fun upsertBatch(dtos: Collection) { + if (dtos.isEmpty()) return + + val columns = listOf( + "id", "project_name", "name", "origin_locations", "custom_fields", + "created", "last_updated", "target_folder", "pages" + ) + val sql = createSql(columns, dtos.size) + val now = Clock.System.now() + + upsertBatchInternal(dtos) { + val stmt = it.prepareStatement(sql) + var index = 1 + dtos.forEach { dto -> + val existingItem = find(dto.id) + + stmt.setString(index++, dto.id) + stmt.setString(index++, this@BaseTemplateRepository.projectName) + stmt.setString(index++, dto.name) + stmt.setArray(index++, it.createArrayOf("text", existingItem?.originLocations.concat(dto.originLocations).distinct().toTypedArray())) + stmt.setObject(index++, Json.encodeToString(dto.customFields.inner), Types.OTHER) + stmt.setTimestamp(index++, java.sql.Timestamp.from((existingItem?.created ?: now).toJavaInstant())) + stmt.setTimestamp(index++, java.sql.Timestamp.from(now.toJavaInstant())) + stmt.setString(index++, dto.targetFolder) + stmt.setObject(index++, Json.encodeToString(dto.pages), Types.OTHER) + } + + stmt.executeUpdate() + } + } +} diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/repository/DisplayRuleRepository.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/repository/DisplayRuleRepository.kt index 25500358..e3ece305 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/repository/DisplayRuleRepository.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/repository/DisplayRuleRepository.kt @@ -1,11 +1,13 @@ package com.quadient.migration.api.repository import com.quadient.migration.api.ProjectName +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateLocation import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap import com.quadient.migration.api.dto.migrationmodel.DisplayRule import com.quadient.migration.api.dto.migrationmodel.DisplayRuleRef import com.quadient.migration.api.dto.migrationmodel.MigrationObject import com.quadient.migration.api.dto.migrationmodel.VariableStructureRef +import com.quadient.migration.api.dto.migrationmodel.toDb import com.quadient.migration.persistence.table.DisplayRuleTable import com.quadient.migration.persistence.table.DocumentObjectTable import com.quadient.migration.service.deploy.utility.ResourceType @@ -32,7 +34,7 @@ class DisplayRuleRepository(projectName: ProjectName, private val statusTracking lastUpdated = row[DisplayRuleTable.lastUpdated], created = row[DisplayRuleTable.created], definition = row[DisplayRuleTable.definition], - baseTemplate = row[DisplayRuleTable.baseTemplate], + baseTemplate = row[DisplayRuleTable.baseTemplate]?.let { BaseTemplateLocation.fromDb(it) }, metadata = row[DisplayRuleTable.metadata], subject = row[DisplayRuleTable.subject], internal = row[DisplayRuleTable.internal], @@ -85,7 +87,7 @@ class DisplayRuleRepository(projectName: ProjectName, private val statusTracking stmt.setBoolean(index++, dto.internal) stmt.setString(index++, dto.subject) stmt.setString(index++, dto.targetFolder) - stmt.setString(index++, dto.baseTemplate) + stmt.setObject(index++, dto.baseTemplate?.let { Json.encodeToString(it.toDb()) }, Types.OTHER) stmt.setString(index++, dto.variableStructureRef?.id) stmt.setObject(index++, Json.encodeToString(dto.metadata), Types.OTHER) } @@ -117,7 +119,7 @@ class DisplayRuleRepository(projectName: ProjectName, private val statusTracking it[DisplayRuleTable.internal] = dto.internal it[DisplayRuleTable.subject] = dto.subject it[DisplayRuleTable.targetFolder] = dto.targetFolder - it[DisplayRuleTable.baseTemplate] = dto.baseTemplate + it[DisplayRuleTable.baseTemplate] = dto.baseTemplate?.toDb() it[DisplayRuleTable.variableStructureRef] = dto.variableStructureRef?.id it[DisplayRuleTable.metadata] = dto.metadata }.first() diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/repository/DocumentObjectRepository.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/repository/DocumentObjectRepository.kt index bbc3c4ad..7893e441 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/repository/DocumentObjectRepository.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/repository/DocumentObjectRepository.kt @@ -85,7 +85,7 @@ class DocumentObjectRepository(projectName: ProjectName, private val statusTrack stmt.setTimestamp(index++, java.sql.Timestamp.from(now.toJavaInstant())) stmt.setString(index++, dto.displayRuleRef?.id) stmt.setString(index++, dto.variableStructureRef?.id) - stmt.setString(index++, dto.baseTemplate) + stmt.setObject(index++, dto.baseTemplate?.let { Json.encodeToString(it.toDb()) }, Types.OTHER) stmt.setObject(index++, dto.options?.let { Json.encodeToString(it.toDb()) }, Types.OTHER) stmt.setObject(index++, dto.pdfMetadata?.let { Json.encodeToString(it.toDb()) }, Types.OTHER) stmt.setObject(index++, dto.metadata.let { Json.encodeToString(it) }, Types.OTHER) @@ -128,7 +128,7 @@ class DocumentObjectRepository(projectName: ProjectName, private val statusTrack it[DocumentObjectTable.lastUpdated] = now it[DocumentObjectTable.displayRuleRef] = dto.displayRuleRef?.id it[DocumentObjectTable.variableStructureRef] = dto.variableStructureRef?.id - it[DocumentObjectTable.baseTemplate] = dto.baseTemplate + it[DocumentObjectTable.baseTemplate] = dto.baseTemplate?.toDb() it[DocumentObjectTable.documentObjectOptions] = dto.options?.toDb() it[DocumentObjectTable.pdfMetadata] = dto.pdfMetadata?.toDb() it[DocumentObjectTable.metadata] = dto.metadata diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/repository/MappingRepository.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/repository/MappingRepository.kt index 982edcc4..267a24c2 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/repository/MappingRepository.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/repository/MappingRepository.kt @@ -2,13 +2,13 @@ package com.quadient.migration.api.repository import com.quadient.migration.api.ProjectName import com.quadient.migration.api.dto.migrationmodel.Attachment +import com.quadient.migration.api.dto.migrationmodel.BaseTemplate import com.quadient.migration.api.dto.migrationmodel.DisplayRule import com.quadient.migration.api.dto.migrationmodel.DocumentObject import com.quadient.migration.api.dto.migrationmodel.Image import com.quadient.migration.api.dto.migrationmodel.ParagraphStyle import com.quadient.migration.api.dto.migrationmodel.TextStyle import com.quadient.migration.api.dto.migrationmodel.Variable -import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap import com.quadient.migration.api.dto.migrationmodel.Mapping import com.quadient.migration.api.dto.migrationmodel.MappingItem import com.quadient.migration.api.dto.migrationmodel.MigrationObject @@ -16,6 +16,7 @@ import com.quadient.migration.api.dto.migrationmodel.VariableStructure import com.quadient.migration.persistence.migrationmodel.MappingItemEntity import com.quadient.migration.persistence.repository.MappingInternalRepository import com.quadient.migration.persistence.table.AttachmentTable +import com.quadient.migration.persistence.table.BaseTemplateTable import com.quadient.migration.persistence.table.DisplayRuleTable import com.quadient.migration.persistence.table.DocumentObjectTable import com.quadient.migration.persistence.table.ImageTable @@ -38,6 +39,7 @@ class MappingRepository( private val variableRepository: VariableRepository, private val variableStructureRepository: VariableStructureRepository, private val displayRuleRepository: DisplayRuleRepository, + private val baseTemplateRepository: BaseTemplateRepository, ) { private val logger by logger() private val internalRepository = MappingInternalRepository(projectName.name) @@ -59,6 +61,7 @@ class MappingRepository( applyAllVariableStructureMappings() applyAllDisplayRuleMappings() applyAllTableMappings(onError) + applyAllBaseTemplateMappings() } fun upsert(id: String, mapping: MappingItem): Mapping { @@ -276,18 +279,9 @@ class MappingRepository( fun applyVariableStructureMapping(id: String) { val mapping = internalRepository.find(id) - val structure = variableStructureRepository.find(id) ?: VariableStructure( - id = id, - name = null, - originLocations = emptyList(), - customFields = CustomFieldMap(), - created = kotlin.time.Clock.System.now(), - lastUpdated = kotlin.time.Clock.System.now(), - structure = mutableMapOf(), - languageVariable = null, - ) - - if (mapping == null) { + val structure = variableStructureRepository.find(id) + + if (mapping == null || structure == null) { return } @@ -316,6 +310,27 @@ class MappingRepository( displayRuleRepository.upsert(mapping.apply(displayRule)) } + fun getBaseTemplateMapping(id: String): MappingItem.BaseTemplate { + return (internalRepository.find(id) ?: MappingItemEntity.BaseTemplate( + name = null, targetFolder = null, pages = emptyList() + )).toDto() as MappingItem.BaseTemplate + } + + fun applyBaseTemplateMapping(id: String) { + val mapping = internalRepository.find(id) + val baseTemplate = baseTemplateRepository.find(id) + + if (mapping == null || baseTemplate == null) { + return + } + + baseTemplateRepository.upsert(mapping.apply(baseTemplate)) + } + + fun applyAllBaseTemplateMappings() { + applyAllResourceMappings(baseTemplateRepository, BaseTemplateTable) + } + fun deleteAll() { internalRepository.deleteAll() } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/MappingEntity.kt b/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/MappingEntity.kt index 353934da..59b0b36f 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/MappingEntity.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/MappingEntity.kt @@ -22,6 +22,7 @@ import com.quadient.migration.api.dto.migrationmodel.VariableStructure as Variab import com.quadient.migration.api.dto.migrationmodel.Area as AreaModel import com.quadient.migration.api.dto.migrationmodel.DisplayRule as DisplayRuleModel import com.quadient.migration.api.dto.migrationmodel.Table as TableModel +import com.quadient.migration.api.dto.migrationmodel.BaseTemplate as BaseTemplateModel class MappingEntity(id: EntityID) : CompositeEntity(id) { companion object : CompositeEntityClass(MappingTable) @@ -54,6 +55,7 @@ sealed class MappingItemEntity { is VariableStructure -> this.apply(entity as VariableStructureModel) is DisplayRule -> this.apply(entity as DisplayRuleModel) is Table -> this.apply(entity as DocumentObjectModel, onError) + is BaseTemplate -> this.apply(entity as BaseTemplateModel) } } @@ -61,7 +63,7 @@ sealed class MappingItemEntity { data class DocumentObject( override val name: String?, val internal: Boolean?, - val baseTemplate: String?, + val baseTemplate: BaseTemplateLocationEntity?, val targetFolder: String?, val variableStructureRef: String?, @SerialName("documentObjectType") val type: DocumentObjectType?, @@ -72,7 +74,7 @@ sealed class MappingItemEntity { return item.copy( name = name, internal = internal ?: false, - baseTemplate = baseTemplate, + baseTemplate = baseTemplate?.let { BaseTemplateLocation.fromDb(it) }, targetFolder = targetFolder, type = type ?: item.type, variableStructureRef = variableStructureRef?.let { VariableStructureRef(it) }, @@ -108,6 +110,21 @@ sealed class MappingItemEntity { } } + @Serializable + data class BaseTemplate( + override val name: String?, + val targetFolder: String?, + val pages: List = emptyList(), + ) : MappingItemEntity() { + fun apply(item: BaseTemplateModel): BaseTemplateModel { + return item.copy( + name = name, + targetFolder = targetFolder, + pages = pages, + ) + } + } + @Serializable data class Image( override val name: String?, @@ -234,7 +251,7 @@ sealed class MappingItemEntity { override var name: String?, val targetFolder: String?, var targetId: String?, - val baseTemplate: String?, + val baseTemplate: BaseTemplateLocationEntity?, val variableStructureRef: String?, var internal: Boolean?, ) : MappingItemEntity() { @@ -242,7 +259,7 @@ sealed class MappingItemEntity { return item.copy( name = name, targetFolder = targetFolder, - baseTemplate = baseTemplate, + baseTemplate = baseTemplate?.let { BaseTemplateLocation.fromDb(it) }, variableStructureRef = variableStructureRef?.let { VariableStructureRef(it) }, targetId = targetId?.let { DisplayRuleRef(it) }, internal = internal ?: true @@ -358,7 +375,7 @@ sealed class MappingItemEntity { MappingItem.DocumentObject( name = this.name, internal = this.internal, - baseTemplate = this.baseTemplate, + baseTemplate = this.baseTemplate?.let { BaseTemplateLocation.fromDb(it) }, targetFolder = this.targetFolder, type = this.type, variableStructureRef = this.variableStructureRef, @@ -368,6 +385,12 @@ sealed class MappingItemEntity { is Area -> MappingItem.Area(name = this.name, areas = this.areas) + is BaseTemplate -> MappingItem.BaseTemplate( + name = this.name, + targetFolder = this.targetFolder, + pages = this.pages, + ) + is Image -> { MappingItem.Image( name = this.name, @@ -455,7 +478,7 @@ sealed class MappingItemEntity { internal = this.internal, variableStructureRef = this.variableStructureRef, targetFolder = this.targetFolder, - baseTemplate = this.baseTemplate, + baseTemplate = this.baseTemplate?.let { BaseTemplateLocation.fromDb(it) }, ) is Table -> MappingItem.Table( diff --git a/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/RefEntity.kt b/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/RefEntity.kt index 3d5b6fac..72744bac 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/RefEntity.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/RefEntity.kt @@ -4,6 +4,7 @@ import com.quadient.migration.shared.Color import com.quadient.migration.shared.ShapePath import com.quadient.migration.shared.Position import com.quadient.migration.shared.Size +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable @@ -43,6 +44,17 @@ data class AttachmentEntityRef(val id: String) : ResourceEntityRef @Serializable data class VariableStructureEntityRef(val id: String) : RefEntity +@Serializable +sealed interface BaseTemplateLocationEntity + +@Serializable +@SerialName("BaseTemplatePath") +data class LiteralBaseTemplatePathEntity(val path: String) : BaseTemplateLocationEntity + +@Serializable +@SerialName("BaseTemplateRef") +data class BaseTemplateEntityRef(val id: String) : RefEntity, BaseTemplateLocationEntity + @Serializable data class StringEntity(val value: String) : VariableStringContentEntity diff --git a/migration-library/src/main/kotlin/com/quadient/migration/persistence/table/BaseTemplateTable.kt b/migration-library/src/main/kotlin/com/quadient/migration/persistence/table/BaseTemplateTable.kt new file mode 100644 index 00000000..0be62259 --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/persistence/table/BaseTemplateTable.kt @@ -0,0 +1,10 @@ +package com.quadient.migration.persistence.table + +import com.quadient.migration.shared.BaseTemplatePage +import kotlinx.serialization.json.Json +import org.jetbrains.exposed.v1.json.jsonb + +object BaseTemplateTable : MigrationObjectTable("base_template") { + val targetFolder = varchar("target_folder", 255).nullable() + val pages = jsonb>("pages", Json) +} diff --git a/migration-library/src/main/kotlin/com/quadient/migration/persistence/table/DisplayRuleTable.kt b/migration-library/src/main/kotlin/com/quadient/migration/persistence/table/DisplayRuleTable.kt index 8fd8e733..0b01a938 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/persistence/table/DisplayRuleTable.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/persistence/table/DisplayRuleTable.kt @@ -1,5 +1,6 @@ package com.quadient.migration.persistence.table +import com.quadient.migration.persistence.migrationmodel.BaseTemplateLocationEntity import com.quadient.migration.shared.DisplayRuleDefinition import com.quadient.migration.shared.MetadataEntry import kotlinx.serialization.json.Json @@ -11,7 +12,7 @@ object DisplayRuleTable : MigrationObjectTable("display_rule") { val internal = bool("internal") val subject = varchar("subject", 255).nullable() val targetFolder = varchar("target_folder", 255).nullable() - val baseTemplate = varchar("base_template", 255).nullable() + val baseTemplate = jsonb("base_template", Json).nullable() val variableStructureRef = varchar("variable_structure_ref", 255).nullable() val metadata = jsonb>("metadata", Json) } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/persistence/table/DocumentObjectTable.kt b/migration-library/src/main/kotlin/com/quadient/migration/persistence/table/DocumentObjectTable.kt index 3bd78d19..59964a9e 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/persistence/table/DocumentObjectTable.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/persistence/table/DocumentObjectTable.kt @@ -1,5 +1,6 @@ package com.quadient.migration.persistence.table +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateLocation import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap import com.quadient.migration.api.dto.migrationmodel.DisplayRuleRef import com.quadient.migration.api.dto.migrationmodel.DocumentContent @@ -7,6 +8,7 @@ import com.quadient.migration.api.dto.migrationmodel.DocumentObject import com.quadient.migration.api.dto.migrationmodel.DocumentObjectOptions import com.quadient.migration.api.dto.migrationmodel.PdfMetadata import com.quadient.migration.api.dto.migrationmodel.VariableStructureRef +import com.quadient.migration.persistence.migrationmodel.BaseTemplateLocationEntity import com.quadient.migration.persistence.migrationmodel.DocumentContentEntity import com.quadient.migration.persistence.migrationmodel.PdfMetadataEntity import com.quadient.migration.persistence.migrationmodel.DocumentObjectOptionsEntity @@ -24,7 +26,7 @@ object DocumentObjectTable : MigrationObjectTable("document_object") { val targetFolder = varchar("target_folder", 255).nullable() val displayRuleRef = varchar("display_rule_ref", 255).nullable() val variableStructureRef = varchar("variable_structure_ref", 255).nullable() - val baseTemplate = varchar("base_template", 255).nullable() + val baseTemplate = jsonb("base_template", Json).nullable() val documentObjectOptions = jsonb("options", Json).nullable() val pdfMetadata = jsonb("pdf_metadata", Json).nullable() val metadata = jsonb>("metadata", Json) @@ -45,7 +47,7 @@ object DocumentObjectTable : MigrationObjectTable("document_object") { lastUpdated = result[lastUpdated], displayRuleRef = result[displayRuleRef]?.let { DisplayRuleRef(it) }, variableStructureRef = result[variableStructureRef]?.let { VariableStructureRef(it) }, - baseTemplate = result[baseTemplate], + baseTemplate = result[baseTemplate]?.let { BaseTemplateLocation.fromDb(it) }, options = result[documentObjectOptions]?.let(DocumentObjectOptions::fromDb), pdfMetadata = result[pdfMetadata]?.let(PdfMetadata::fromDb), metadata = result[metadata], diff --git a/migration-library/src/main/kotlin/com/quadient/migration/persistence/upgrade/V16__add_base_template.kt b/migration-library/src/main/kotlin/com/quadient/migration/persistence/upgrade/V16__add_base_template.kt new file mode 100644 index 00000000..fab0745e --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/persistence/upgrade/V16__add_base_template.kt @@ -0,0 +1,60 @@ +package com.quadient.migration.persistence.upgrade + +import org.flywaydb.core.api.migration.BaseJavaMigration +import org.flywaydb.core.api.migration.Context + +class V16__add_base_template : BaseJavaMigration() { + override fun migrate(context: Context) { + val connection = context.connection + connection.createStatement().use { stmt -> + stmt.execute( + """ + CREATE TABLE IF NOT EXISTS base_template ( + id VARCHAR(255) NOT NULL, + project_name VARCHAR(50) NOT NULL, + name VARCHAR(255), + origin_locations TEXT[] NOT NULL, + custom_fields JSONB NOT NULL, + last_updated TIMESTAMP NOT NULL, + created TIMESTAMP NOT NULL, + target_folder VARCHAR(255), + pages JSONB NOT NULL, + PRIMARY KEY (id, project_name) + ) + """.trimIndent() + ) + + for (table in listOf("document_object", "display_rule")) { + stmt.execute( + """ + ALTER TABLE $table + ALTER COLUMN base_template TYPE JSONB + USING ( + CASE + WHEN base_template IS NULL THEN NULL + ELSE jsonb_build_object('type', 'BaseTemplatePath', 'path', base_template) + END + ) + """.trimIndent() + ) + } + + for (type in listOf("DocumentObject", "DisplayRule")) { + stmt.execute( + """ + UPDATE mapping + SET mappings = jsonb_set( + mappings, + '{baseTemplate}', + jsonb_build_object('type', 'BaseTemplatePath', 'path', mappings->>'baseTemplate'), + false + ) + WHERE type = '$type' + AND mappings ? 'baseTemplate' + AND jsonb_typeof(mappings->'baseTemplate') = 'string' + """.trimIndent() + ) + } + } + } +} diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/DeployPhaseUtils.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/DeployPhaseUtils.kt index 34a3a876..9e6cc7ad 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/DeployPhaseUtils.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/DeployPhaseUtils.kt @@ -3,10 +3,13 @@ package com.quadient.migration.service import com.quadient.migration.api.ProjectConfig import com.quadient.migration.api.dto.migrationmodel.Attachment import com.quadient.migration.api.dto.migrationmodel.AttachmentRef +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateLocation import com.quadient.migration.api.dto.migrationmodel.DisplayRule import com.quadient.migration.api.dto.migrationmodel.DocumentContent import com.quadient.migration.api.dto.migrationmodel.Image import com.quadient.migration.api.dto.migrationmodel.ImageRef +import com.quadient.migration.api.dto.migrationmodel.LiteralBaseTemplatePath import com.quadient.migration.api.dto.migrationmodel.ResourceRef import com.quadient.migration.api.repository.Repository import com.quadient.migration.shared.IcmPath @@ -59,9 +62,16 @@ fun resolveTargetDir(defaultTargetFolder: IcmPath? = null, specificTargetFolder: } } -fun getBaseTemplateFullPath(config: ProjectConfig, documentObjectBaseTemplatePath: String?): IcmPath { - val baseTemplatePath = documentObjectBaseTemplatePath ?: config.baseTemplatePath - val path = baseTemplatePath.toIcmPath() +fun getBaseTemplateFullPath(config: ProjectConfig, documentObjectBaseTemplate: BaseTemplateLocation?): IcmPath { + val literalPath = when (documentObjectBaseTemplate) { + is LiteralBaseTemplatePath -> documentObjectBaseTemplate.path + is BaseTemplateRef -> error( + "Referencing base templates by id ('${documentObjectBaseTemplate.id}') is not yet supported during deployment." // TODO d.svitak - handle + ) + + null -> config.baseTemplatePath + } + val path = literalPath.toIcmPath() if (path.isAbsolute()) return path diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/RefCollector.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/RefCollector.kt index c52f0ebd..8195c0b1 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/RefCollector.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/RefCollector.kt @@ -1,6 +1,7 @@ package com.quadient.migration.service import com.quadient.migration.api.dto.migrationmodel.AttachmentRef +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef import com.quadient.migration.api.dto.migrationmodel.DisplayRuleRef import com.quadient.migration.api.dto.migrationmodel.DocumentObjectRef import com.quadient.migration.api.dto.migrationmodel.ImageRef @@ -12,6 +13,7 @@ import com.quadient.migration.api.dto.migrationmodel.TextStyleRef import com.quadient.migration.api.dto.migrationmodel.VariableRef import com.quadient.migration.api.dto.migrationmodel.VariableStructureRef import com.quadient.migration.api.repository.AttachmentRepository +import com.quadient.migration.api.repository.BaseTemplateRepository import com.quadient.migration.api.repository.DisplayRuleRepository import com.quadient.migration.api.repository.DocumentObjectRepository import com.quadient.migration.api.repository.ImageRepository @@ -29,6 +31,7 @@ class RefCollector( private val displayRuleRepository: DisplayRuleRepository, private val variableRepository: VariableRepository, private val variableStructureRepository: VariableStructureRepository, + private val baseTemplateRepository: BaseTemplateRepository, ) { fun collectAllRefs(obj: T, breakFn: (MigrationObject) -> Boolean = { true }): Set { return obj.collectAllRefs(breakFn) @@ -61,6 +64,7 @@ class RefCollector( is TextStyleRef -> textStyleRepository.findOrFail(ref.id) is VariableRef -> variableRepository.findOrFail(ref.id) is VariableStructureRef -> variableStructureRepository.findOrFail(ref.id) + is BaseTemplateRef -> baseTemplateRepository.findOrFail(ref.id) } private fun T.traverse( diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/ReferenceValidator.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/ReferenceValidator.kt index 97201b6d..a6bfa11b 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/ReferenceValidator.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/ReferenceValidator.kt @@ -1,6 +1,7 @@ package com.quadient.migration.service import com.quadient.migration.api.dto.migrationmodel.* +import com.quadient.migration.api.repository.BaseTemplateRepository import com.quadient.migration.api.repository.DisplayRuleRepository import com.quadient.migration.api.repository.DocumentObjectRepository import com.quadient.migration.api.repository.AttachmentRepository @@ -19,6 +20,7 @@ class ReferenceValidator( private val displayRuleRepository: DisplayRuleRepository, private val imageRepository: ImageRepository, private val attachmentRepository: AttachmentRepository, + private val baseTemplateRepository: BaseTemplateRepository, ) { /** * Validates all objects in the database. @@ -156,6 +158,17 @@ class ReferenceValidator( missingRefs.add(current) } } + + is BaseTemplateRef -> { + val baseTemplate = baseTemplateRepository.find(current.id) + + if (baseTemplate != null) { + validatedRefs.add(current) + alreadyValidRefs.add(current) + } else { + missingRefs.add(current) + } + } } } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/StylesValidator.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/StylesValidator.kt index 5c7cc097..8e1ad7d3 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/StylesValidator.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/StylesValidator.kt @@ -4,6 +4,7 @@ import com.quadient.migration.api.dto.migrationmodel.DisplayRuleRef import com.quadient.migration.api.dto.migrationmodel.DocumentObject import com.quadient.migration.api.dto.migrationmodel.DocumentObjectRef import com.quadient.migration.api.dto.migrationmodel.AttachmentRef +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef import com.quadient.migration.api.dto.migrationmodel.ImageRef import com.quadient.migration.api.dto.migrationmodel.ParagraphStyle import com.quadient.migration.api.dto.migrationmodel.ParagraphStyleRef @@ -77,6 +78,7 @@ class StylesValidator( is AttachmentRef -> {} is VariableRef -> {} is VariableStructureRef -> {} + is BaseTemplateRef -> {} } } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DeployClient.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DeployClient.kt index c9fb6571..784c68db 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DeployClient.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DeployClient.kt @@ -9,6 +9,7 @@ import com.quadient.migration.api.dto.migrationmodel.DocumentObject import com.quadient.migration.api.dto.migrationmodel.DocumentObjectRef import com.quadient.migration.api.dto.migrationmodel.Attachment import com.quadient.migration.api.dto.migrationmodel.AttachmentRef +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef import com.quadient.migration.api.dto.migrationmodel.DisplayRule import com.quadient.migration.api.dto.migrationmodel.Image import com.quadient.migration.api.dto.migrationmodel.ImageRef @@ -401,7 +402,7 @@ sealed class DeployClient( val dependencies = mutableListOf() this.collectRefs().forEach { ref -> when (ref) { - is DisplayRuleRef, is TextStyleRef, is ParagraphStyleRef, is VariableRef, is VariableStructureRef -> {} + is DisplayRuleRef, is TextStyleRef, is ParagraphStyleRef, is VariableRef, is VariableStructureRef, is BaseTemplateRef -> {} is ImageRef -> {} is AttachmentRef -> {} is DocumentObjectRef -> { @@ -421,7 +422,7 @@ sealed class DeployClient( this.collectRefs().forEach { ref -> when (ref) { - is DisplayRuleRef, is TextStyleRef, is ParagraphStyleRef, is VariableRef, is VariableStructureRef -> {} + is DisplayRuleRef, is TextStyleRef, is ParagraphStyleRef, is VariableRef, is VariableStructureRef, is BaseTemplateRef -> {} is ResourceRef -> resources.add(ref) is DocumentObjectRef -> { val model = documentObjectRepository.find(ref.id) diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt index 37bb072f..15b01c9e 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt @@ -5,6 +5,7 @@ import com.quadient.migration.api.repository.StatusTrackingRepository import com.quadient.migration.api.dto.migrationmodel.DocumentObject import com.quadient.migration.api.dto.migrationmodel.Attachment import com.quadient.migration.api.dto.migrationmodel.AttachmentRef +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap import com.quadient.migration.api.dto.migrationmodel.DisplayRule import com.quadient.migration.api.dto.migrationmodel.DisplayRuleRef @@ -455,7 +456,7 @@ open class InteractiveDeployClient( resources.addAll(model.getAllExternalDisplayRules()) } - is ParagraphStyleRef, is AttachmentRef, is ImageRef, is TextStyleRef, is VariableRef, is VariableStructureRef -> {} + is ParagraphStyleRef, is AttachmentRef, is ImageRef, is TextStyleRef, is VariableRef, is VariableStructureRef, is BaseTemplateRef -> {} } } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/ProgressReport.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/ProgressReport.kt index fccbd810..692a2a79 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/ProgressReport.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/ProgressReport.kt @@ -5,6 +5,7 @@ import com.quadient.migration.api.dto.migrationmodel.DocumentObject import com.quadient.migration.api.dto.migrationmodel.Image import com.quadient.migration.api.dto.migrationmodel.Attachment import com.quadient.migration.api.dto.migrationmodel.AttachmentRef +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef import com.quadient.migration.api.dto.migrationmodel.DisplayRule import com.quadient.migration.api.dto.migrationmodel.DisplayRuleRef import com.quadient.migration.api.dto.migrationmodel.DocumentObjectRef @@ -247,6 +248,7 @@ class ProgressReporterImpl( is ParagraphStyleRef -> null is VariableRef -> null is VariableStructureRef -> null + is BaseTemplateRef -> null } if (resource != null) { diff --git a/migration-library/src/main/kotlin/com/quadient/migration/shared/BaseTemplatePage.kt b/migration-library/src/main/kotlin/com/quadient/migration/shared/BaseTemplatePage.kt new file mode 100644 index 00000000..11f1edad --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/shared/BaseTemplatePage.kt @@ -0,0 +1,18 @@ +package com.quadient.migration.shared + +import kotlinx.serialization.Serializable + +@Serializable +data class BaseTemplateArea( + var interactiveFlowName: String, + var position: Position? = null, + var flowToNextPage: Boolean = false, +) + +@Serializable +data class BaseTemplatePage( + var name: String? = null, + var pageWidth: Size? = null, + var pageHeight: Size? = null, + var areas: List = emptyList(), +) diff --git a/migration-library/src/main/kotlin/com/quadient/migration/shared/JrdDefinition.kt b/migration-library/src/main/kotlin/com/quadient/migration/shared/JrdDefinition.kt index 2b430b44..9dec9916 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/shared/JrdDefinition.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/shared/JrdDefinition.kt @@ -20,6 +20,7 @@ import com.quadient.migration.service.inspirebuilder.removeDataFromVariablePath import com.quadient.migration.service.inspirebuilder.toScript import com.quadient.migration.service.inspirebuilder.resolve import com.quadient.migration.service.inspirebuilder.variableToScript +import com.quadient.migration.service.getBaseTemplateFullPath import com.quadient.migration.shared.BinOp.Equals import com.quadient.migration.shared.BinOp.EqualsCaseInsensitive import com.quadient.migration.shared.BinOp.GreaterOrEqualThan @@ -91,7 +92,7 @@ data class JrdDefinition( variable } - val baseTemplate = IcmPath.from(rule.baseTemplate ?: projectConfig.baseTemplatePath) + val baseTemplate = getBaseTemplateFullPath(projectConfig, rule.baseTemplate) .toMapInteractive(projectConfig.interactiveTenant) val ruleDef = requireNotNull(rule.definition) { "Display rule '${rule.id}' cannot be deployed because it has missing definition" } diff --git a/migration-library/src/test/kotlin/com/quadient/migration/persistence/DisplayRuleRepositoryTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/persistence/DisplayRuleRepositoryTest.kt index 6988d41d..2e1339f1 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/persistence/DisplayRuleRepositoryTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/persistence/DisplayRuleRepositoryTest.kt @@ -21,7 +21,7 @@ class DisplayRuleRepositoryTest { .originLocations(listOf("test1", "test2")) .definition(DisplayRuleDefinition(group = Group(items = emptyList(), operator = GroupOp.Or, false))) .metadata("meta1") { string("val") } - .baseTemplate("icm://Interactive/StandardPackage/BaseTemplates/templ.wfd") + .baseTemplatePath("icm://Interactive/StandardPackage/BaseTemplates/templ.wfd") .internal(false) .targetFolder("targetFolder") .subject("Test Subject") @@ -46,7 +46,7 @@ class DisplayRuleRepositoryTest { .originLocations(listOf("origin1")) .definition(DisplayRuleDefinition(group = Group(items = emptyList(), operator = GroupOp.And, false))) .metadata("meta1") { string("val") } - .baseTemplate("icm://Interactive/StandardPackage/BaseTemplates/templ.wfd") + .baseTemplatePath("icm://Interactive/StandardPackage/BaseTemplates/templ.wfd") .internal(false) .targetFolder("targetFolder") .subject("Test Subject") @@ -60,7 +60,7 @@ class DisplayRuleRepositoryTest { .originLocations(listOf("origin2")) .definition(DisplayRuleDefinition(group = Group(items = emptyList(), operator = GroupOp.Or, true))) .metadata("meta1") { string("val") } - .baseTemplate("icm://Interactive/StandardPackage/BaseTemplates/templ.wfd") + .baseTemplatePath("icm://Interactive/StandardPackage/BaseTemplates/templ.wfd") .internal(false) .targetFolder("targetFolder") .subject("Test Subject") @@ -101,4 +101,4 @@ class DisplayRuleRepositoryTest { updatedBlock1.name.shouldBeEqualTo("Updated Rule 1") updatedBlock2.name.shouldBeEqualTo("Updated Rule 2") } -} \ No newline at end of file +} diff --git a/migration-library/src/test/kotlin/com/quadient/migration/persistence/DocumentObjectRepositoryTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/persistence/DocumentObjectRepositoryTest.kt index 8ffe2eb4..b85e3029 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/persistence/DocumentObjectRepositoryTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/persistence/DocumentObjectRepositoryTest.kt @@ -57,7 +57,7 @@ class DocumentObjectRepositoryTest { .internal(true) .targetFolder("acquired") .displayRuleRef("someruleref") - .baseTemplate("someBaseTemplate") + .baseTemplatePath("someBaseTemplate") .skip("reason", "placeholder") .variableStructureRef("someVarStructRef") .subject("somesubject") @@ -76,7 +76,7 @@ class DocumentObjectRepositoryTest { // given val block1 = DocumentObjectBuilder("block1", DocumentObjectType.Block) .internal(true) - .baseTemplate("baseTemplate1") + .baseTemplatePath("baseTemplate1") .customFields(mutableMapOf("field1" to "value1")) .originLocations(listOf("origin1")) .content(listOf(Paragraph("content1"))) @@ -91,7 +91,7 @@ class DocumentObjectRepositoryTest { val block2 = DocumentObjectBuilder("block2", DocumentObjectType.Block) .internal(false) - .baseTemplate("baseTemplate2") + .baseTemplatePath("baseTemplate2") .customFields(mutableMapOf("field2" to "value2")) .originLocations(listOf("origin2")) .content(listOf(Paragraph("content2"))) diff --git a/migration-library/src/test/kotlin/com/quadient/migration/persistence/MappingRepositoryTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/persistence/MappingRepositoryTest.kt index 82593aab..1acdc6e1 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/persistence/MappingRepositoryTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/persistence/MappingRepositoryTest.kt @@ -7,6 +7,7 @@ import com.quadient.migration.api.dto.migrationmodel.MappingItem import com.quadient.migration.api.dto.migrationmodel.VariableStructure import com.quadient.migration.api.dto.migrationmodel.VariableStructureRef import com.quadient.migration.api.repository.AttachmentRepository +import com.quadient.migration.api.repository.BaseTemplateRepository import com.quadient.migration.api.repository.DisplayRuleRepository import com.quadient.migration.api.repository.DocumentObjectRepository import com.quadient.migration.api.repository.ImageRepository @@ -15,6 +16,11 @@ import com.quadient.migration.api.repository.ParagraphStyleRepository import com.quadient.migration.api.repository.TextStyleRepository import com.quadient.migration.api.repository.VariableRepository import com.quadient.migration.api.repository.VariableStructureRepository +import com.quadient.migration.api.dto.migrationmodel.LiteralBaseTemplatePath +import com.quadient.migration.shared.BaseTemplateArea +import com.quadient.migration.shared.BaseTemplatePage +import com.quadient.migration.shared.Position +import com.quadient.migration.shared.Size import com.quadient.migration.shared.VariablePathData import com.quadient.migration.tools.aProjectConfig import com.quadient.migration.tools.aVariable @@ -38,6 +44,7 @@ class MappingRepositoryTest { val variableRepository = mockk() val variableStructureRepository = mockk() val displayRuleRepository = mockk() + val baseTemplateRepository = mockk() private val repo = MappingRepository( ProjectName(projectConfig.name), @@ -49,6 +56,7 @@ class MappingRepositoryTest { variableRepository, variableStructureRepository, displayRuleRepository, + baseTemplateRepository, ) @Test @@ -118,7 +126,7 @@ class MappingRepositoryTest { name = "new name", targetFolder = "/some/folder", targetId = "target-123", - baseTemplate = "template-base", + baseTemplate = LiteralBaseTemplatePath("template-base"), variableStructureRef = "ref", internal = true, ) @@ -133,11 +141,40 @@ class MappingRepositoryTest { it.name == "new name" && it.targetFolder == "/some/folder" && it.targetId?.id == "target-123" && - it.baseTemplate == "template-base" && + it.baseTemplate == LiteralBaseTemplatePath("template-base") && it.internal && it.variableStructureRef == VariableStructureRef("ref") } ) } } + + @Test + fun `apply base template mapping does nothing when base template does not exist`() { + every { baseTemplateRepository.find("baseTemplateId") } returns null + every { baseTemplateRepository.upsert(any()) } just runs + repo.upsert( + "baseTemplateId", MappingItem.BaseTemplate( + name = "New Base Template", + targetFolder = "icm://Interactive/Tenant/BaseTemplates/some.wfd", + pages = listOf( + BaseTemplatePage( + name = "Page1", + pageWidth = Size.ofMillimeters(210.0), + pageHeight = Size.ofMillimeters(297.0), + areas = listOf( + BaseTemplateArea( + interactiveFlowName = "Flow1", + position = Position(Size.ofMillimeters(1.0), Size.ofMillimeters(2.0), Size.ofMillimeters(3.0), Size.ofMillimeters(4.0)), + ) + ), + ) + ), + ) + ) + + repo.applyBaseTemplateMapping("baseTemplateId") + + verify(exactly = 0) { baseTemplateRepository.upsert(any()) } + } } \ No newline at end of file diff --git a/migration-library/src/test/kotlin/com/quadient/migration/persistence/migrationmodel/MappingEntityTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/persistence/migrationmodel/MappingEntityTest.kt index 6c37f950..5f4dc414 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/persistence/migrationmodel/MappingEntityTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/persistence/migrationmodel/MappingEntityTest.kt @@ -9,6 +9,7 @@ import com.quadient.migration.api.dto.migrationmodel.builder.ParagraphStyleBuild import com.quadient.migration.api.dto.migrationmodel.builder.TableBuilder import com.quadient.migration.api.dto.migrationmodel.builder.TextStyleBuilder import com.quadient.migration.api.dto.migrationmodel.builder.documentcontent.AreaBuilder +import com.quadient.migration.api.dto.migrationmodel.LiteralBaseTemplatePath import com.quadient.migration.shared.Alignment import com.quadient.migration.shared.Color import com.quadient.migration.shared.DataType @@ -69,7 +70,7 @@ class MappingEntityTest { val mapping = MappingItemEntity.DocumentObject( name = "new name", internal = false, - baseTemplate = "new base", + baseTemplate = LiteralBaseTemplatePathEntity("new base"), targetFolder = "new folder", type = Block, variableStructureRef = "new structure", @@ -89,7 +90,7 @@ class MappingEntityTest { assertEquals(result.name, "new name") assertEquals(result.internal, false) - assertEquals(result.baseTemplate, "new base") + assertEquals(result.baseTemplate, LiteralBaseTemplatePath("new base")) assertEquals(result.targetFolder, "new folder") assertEquals(result.type, Block) assertEquals(result.variableStructureRef?.id, "new structure") diff --git a/migration-library/src/test/kotlin/com/quadient/migration/persistence/upgrade/V16MigrationTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/persistence/upgrade/V16MigrationTest.kt new file mode 100644 index 00000000..3cfdc913 --- /dev/null +++ b/migration-library/src/test/kotlin/com/quadient/migration/persistence/upgrade/V16MigrationTest.kt @@ -0,0 +1,167 @@ +package com.quadient.migration.persistence.upgrade + +import com.quadient.migration.Postgres.Companion.POSTGRES_CONTAINER +import kotlinx.serialization.json.Json +import org.flywaydb.core.Flyway +import org.jetbrains.exposed.v1.jdbc.Database +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test +import org.testcontainers.postgresql.PostgreSQLContainer +import java.sql.Connection +import java.sql.DriverManager + +/** + * Integration test for V16 migration that introduces the base_template table and converts the + * document_object/display_rule base_template columns (and their mapping counterparts) from a plain + * path string into a discriminated jsonb reference (path or id). + */ +class V16MigrationTest { + + @Test + fun `V16 migration converts base template references to jsonb`() { + PostgreSQLContainer(POSTGRES_CONTAINER).use { postgres -> + postgres.start() + + Database.connect( + url = postgres.jdbcUrl, + driver = "org.postgresql.Driver", + user = postgres.username, + password = postgres.password + ) + + Flyway.configure() + .dataSource(postgres.jdbcUrl, postgres.username, postgres.password) + .locations("classpath:com/quadient/migration/persistence/upgrade") + .target("15") + .load() + .migrate() + + connection(postgres).use { conn -> + insertDocumentObject(conn, "doc-with-base-template", "templates/base.xml") + insertDocumentObject(conn, "doc-without-base-template", null) + insertDisplayRule(conn, "rule-with-base-template", "templates/base-rule.xml") + insertDisplayRule(conn, "rule-without-base-template", null) + + insertMapping( + conn, "map-doc", "DocumentObject", + """{"name":"mapped","baseTemplate":"templates/mapped.xml"}""" + ) + insertMapping( + conn, "map-doc-null", "DocumentObject", + """{"name":"mapped","baseTemplate":null}""" + ) + insertMapping( + conn, "map-rule", "DisplayRule", + """{"name":"mapped","baseTemplate":"templates/mapped-rule.xml"}""" + ) + insertMapping( + conn, "map-area", "Area", + """{"name":"mapped","areas":{}}""" + ) + } + + Flyway.configure() + .dataSource(postgres.jdbcUrl, postgres.username, postgres.password) + .locations("classpath:com/quadient/migration/persistence/upgrade") + .target("16") + .load() + .migrate() + + connection(postgres).use { conn -> + assertJsonEquals( + """{"type":"BaseTemplatePath","path":"templates/base.xml"}""", + queryColumn(conn, "document_object", "base_template", "doc-with-base-template")!! + ) + assertNull(queryColumn(conn, "document_object", "base_template", "doc-without-base-template")) + + assertJsonEquals( + """{"type":"BaseTemplatePath","path":"templates/base-rule.xml"}""", + queryColumn(conn, "display_rule", "base_template", "rule-with-base-template")!! + ) + assertNull(queryColumn(conn, "display_rule", "base_template", "rule-without-base-template")) + + assertJsonEquals( + """{"name":"mapped","baseTemplate":{"type":"BaseTemplatePath","path":"templates/mapped.xml"}}""", + queryMapping(conn, "map-doc", "DocumentObject") + ) + assertJsonEquals( + """{"name":"mapped","baseTemplate":null}""", + queryMapping(conn, "map-doc-null", "DocumentObject") + ) + assertJsonEquals( + """{"name":"mapped","baseTemplate":{"type":"BaseTemplatePath","path":"templates/mapped-rule.xml"}}""", + queryMapping(conn, "map-rule", "DisplayRule") + ) + assertJsonEquals( + """{"name":"mapped","areas":{}}""", + queryMapping(conn, "map-area", "Area") + ) + } + } + } + + private fun insertDocumentObject(conn: Connection, id: String, baseTemplate: String?) { + conn.prepareStatement( + """ + INSERT INTO document_object (id, project_name, name, origin_locations, custom_fields, type, internal, metadata, skip, base_template, last_updated, created) + VALUES (?, 'test', ?, '{}', '{}', 'Block', true, '[]', '{"skipped":false,"reason":null,"placeholder":null}'::jsonb, ?, NOW(), NOW()) + """.trimIndent() + ).apply { + setString(1, id) + setString(2, id) + setString(3, baseTemplate) + }.executeUpdate() + } + + private fun insertDisplayRule(conn: Connection, id: String, baseTemplate: String?) { + conn.prepareStatement( + """ + INSERT INTO display_rule (id, project_name, name, origin_locations, custom_fields, internal, metadata, base_template, last_updated, created) + VALUES (?, 'test', ?, '{}', '{}', true, '{}', ?, NOW(), NOW()) + """.trimIndent() + ).apply { + setString(1, id) + setString(2, id) + setString(3, baseTemplate) + }.executeUpdate() + } + + private fun insertMapping(conn: Connection, id: String, type: String, mappings: String) { + conn.prepareStatement( + """ + INSERT INTO mapping (id, type, project_name, mappings) + VALUES (?, ?, 'test', ?::jsonb) + """.trimIndent() + ).apply { + setString(1, id) + setString(2, type) + setString(3, mappings) + }.executeUpdate() + } + + private fun queryColumn(conn: Connection, table: String, column: String, id: String): String? { + val rs = conn.prepareStatement("SELECT $column::text AS value FROM $table WHERE id = ?") + .apply { setString(1, id) } + .executeQuery() + assert(rs.next()) { "Expected row '$id' in '$table' to exist" } + return rs.getString("value") + } + + private fun queryMapping(conn: Connection, id: String, type: String): String { + val rs = conn.prepareStatement("SELECT mappings::text FROM mapping WHERE id = ? AND type = ?") + .apply { + setString(1, id) + setString(2, type) + } + .executeQuery() + assert(rs.next()) { "Expected mapping row '$id' of type '$type' to exist" } + return rs.getString("mappings") + } + + private fun connection(postgres: PostgreSQLContainer): Connection = + DriverManager.getConnection(postgres.jdbcUrl, postgres.username, postgres.password) + + private fun assertJsonEquals(expected: String, actual: String) = + assertEquals(Json.parseToJsonElement(expected), Json.parseToJsonElement(actual)) +} diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/DeployPhaseUtilsTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/DeployPhaseUtilsTest.kt index cbd4f99a..f949e8ca 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/DeployPhaseUtilsTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/DeployPhaseUtilsTest.kt @@ -2,6 +2,7 @@ package com.quadient.migration.service import com.quadient.migration.service.inspirebuilder.appendExtensionIfMissing import com.quadient.migration.service.inspirebuilder.extractExtensionFromPath +import com.quadient.migration.api.dto.migrationmodel.LiteralBaseTemplatePath import com.quadient.migration.tools.aProjectConfig import com.quadient.migration.tools.shouldBeEqualTo import org.junit.jupiter.api.Test @@ -21,14 +22,14 @@ class DeployPhaseUtilsTest { @Test fun `specific base template path is preferred over the project config one`() { val baseTemplatePath = "icm://Interactive/Vital/BaseTemplates/MyBaseTemplate.wfd" - val result = getBaseTemplateFullPath(projectConfig, baseTemplatePath).toString() + val result = getBaseTemplateFullPath(projectConfig, LiteralBaseTemplatePath(baseTemplatePath)).toString() result.shouldBeEqualTo(baseTemplatePath) } @Test fun `path not starting with icm is handled as relative`() { - val result = getBaseTemplateFullPath(projectConfig, "/projectA/AddressBT.wfd").toString() + val result = getBaseTemplateFullPath(projectConfig, LiteralBaseTemplatePath("/projectA/AddressBT.wfd")).toString() result.shouldBeEqualTo("icm://Interactive/${projectConfig.interactiveTenant}/BaseTemplates/projectA/AddressBT.wfd") } diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/ReferenceValidatorTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/ReferenceValidatorTest.kt index f05df432..7003c2b3 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/ReferenceValidatorTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/ReferenceValidatorTest.kt @@ -6,6 +6,7 @@ import com.quadient.migration.api.dto.migrationmodel.ParagraphStyleRef import com.quadient.migration.api.dto.migrationmodel.TextStyleRef import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.tools.aBlockDto +import com.quadient.migration.tools.aBaseTemplateRepository import com.quadient.migration.tools.aDisplayRuleRepository import com.quadient.migration.tools.aDocumentObjectRepository import com.quadient.migration.tools.aAttachmentRepository @@ -32,6 +33,7 @@ class ReferenceValidatorTest { val displayRuleRepository = aDisplayRuleRepository() val imageRepository = aImageRepository() val attachmentRepository = aAttachmentRepository() + val baseTemplateRepository = aBaseTemplateRepository() val docRepo = aDocumentObjectRepository() val paraStyleRepo = aParaStyleRepository() @@ -46,6 +48,7 @@ class ReferenceValidatorTest { displayRuleRepository, imageRepository, attachmentRepository, + baseTemplateRepository, ) @Test diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/EvolveDeployClientTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/EvolveDeployClientTest.kt index 194ee278..85809271 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/EvolveDeployClientTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/EvolveDeployClientTest.kt @@ -332,7 +332,7 @@ class EvolveDeployClientTest { @Test fun `uploadDocumentObject for Block uses custom baseTemplate when set on document object`() { val customBaseTemplate = "icm://Interactive/tenant/BaseTemplates/custom.wfd" - val block = DocumentObjectBuilder("B1", DocumentObjectType.Block).baseTemplate(customBaseTemplate).build() + val block = DocumentObjectBuilder("B1", DocumentObjectType.Block).baseTemplatePath(customBaseTemplate).build() val targetPath = "icm://Interactive/tenant/B1.jld".toIcmPath() val draftResult = DraftJsonIpsResult( draft = CreateDraftResult(guid = draftGuid, url = "http://example.com"), diff --git a/migration-library/src/test/kotlin/com/quadient/migration/tools/TestObjectBuilders.kt b/migration-library/src/test/kotlin/com/quadient/migration/tools/TestObjectBuilders.kt index 360122d7..bba9108f 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/tools/TestObjectBuilders.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/tools/TestObjectBuilders.kt @@ -29,6 +29,7 @@ import com.quadient.migration.api.dto.migrationmodel.TextStyleRef import com.quadient.migration.api.dto.migrationmodel.Variable import com.quadient.migration.api.dto.migrationmodel.VariableStructureRef import com.quadient.migration.api.repository.AttachmentRepository +import com.quadient.migration.api.repository.BaseTemplateRepository import com.quadient.migration.api.repository.DisplayRuleRepository import com.quadient.migration.api.repository.DocumentObjectRepository import com.quadient.migration.api.repository.ImageRepository @@ -52,6 +53,7 @@ import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.IcmPath import com.quadient.migration.shared.ImageType import com.quadient.migration.shared.LineSpacing +import com.quadient.migration.api.dto.migrationmodel.LiteralBaseTemplatePath import com.quadient.migration.shared.ParagraphPdfTaggingRule import com.quadient.migration.shared.Size import com.quadient.migration.shared.SkipOptions @@ -92,7 +94,7 @@ fun aBlockDto( metadata = emptyList(), skip = SkipOptions(false, null, null), subject = null, - baseTemplate = baseTemplate, + baseTemplate = baseTemplate?.let { LiteralBaseTemplatePath(it) }, variableStructureRef = variableStructureRef, ) } @@ -147,7 +149,7 @@ fun aBlockModel( created = Clock.System.now(), lastUpdated = Clock.System.now(), displayRuleRef = null, - baseTemplate = baseTemplate, + baseTemplate = baseTemplate?.let { LiteralBaseTemplatePath(it) }, options = options, metadata = emptyList(), skip = SkipOptions(false, null, null), @@ -528,3 +530,4 @@ fun aTextStyleRepository() = TextStyleRepository(ProjectName(aProjectConfig().na fun aDisplayRuleRepository() = DisplayRuleRepository(ProjectName(aProjectConfig().name), statusRepo) fun aImageRepository() = ImageRepository(ProjectName(aProjectConfig().name), statusRepo) fun aAttachmentRepository() = AttachmentRepository(ProjectName(aProjectConfig().name), statusRepo) +fun aBaseTemplateRepository() = BaseTemplateRepository(ProjectName(aProjectConfig().name)) diff --git a/migration-library/src/test/kotlin/com/quadient/migration/tools/model/TestModelObjectBuilders.kt b/migration-library/src/test/kotlin/com/quadient/migration/tools/model/TestModelObjectBuilders.kt index a5be8190..47c5dcbd 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/tools/model/TestModelObjectBuilders.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/tools/model/TestModelObjectBuilders.kt @@ -37,6 +37,7 @@ import com.quadient.migration.shared.GroupOp import com.quadient.migration.shared.IcmPath import com.quadient.migration.shared.ImageOptions import com.quadient.migration.shared.ImageType +import com.quadient.migration.api.dto.migrationmodel.LiteralBaseTemplatePath import com.quadient.migration.shared.LiteralOrFunctionCall import com.quadient.migration.shared.MetadataEntry import com.quadient.migration.shared.Position @@ -71,7 +72,7 @@ fun aDocObj( created = null, lastUpdated = null, displayRuleRef = displayRuleRef?.let { DisplayRuleRef(it) }, - baseTemplate = baseTemplate, + baseTemplate = baseTemplate?.let { LiteralBaseTemplatePath(it) }, variableStructureRef = VariableStructureRef?.let { VariableStructureRef(it) }, options = options, metadata = metadata, @@ -108,7 +109,7 @@ fun aBlock( created = created, lastUpdated = lastUpdated, displayRuleRef = displayRuleRef, - baseTemplate = baseTemplate, + baseTemplate = baseTemplate?.let { LiteralBaseTemplatePath(it) }, options = null, metadata = metadata, skip = skip, @@ -140,7 +141,7 @@ fun aTemplate( customFields = CustomFieldMap(), created = null, lastUpdated = null, - baseTemplate = baseTemplate, + baseTemplate = baseTemplate?.let { LiteralBaseTemplatePath(it) }, options = null, metadata = emptyList(), skip = SkipOptions(false, null, null), From 47e563b4572c9f58b82bc5a09179f43e54b3bcdf Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Fri, 17 Jul 2026 15:55:57 +0200 Subject: [PATCH 3/5] MIG-562 Base Template definition and deployment - Initial support - base template referencing from document object and display rules --- .../common/mapping/DisplayRulesImport.groovy | 3 +- .../mapping/DocumentObjectsImport.groovy | 5 +- .../migration/example/common/util/Csv.groovy | 9 + .../DisplayRulesMappingExportTest.groovy | 8 +- .../DisplayRulesMappingImportTest.groovy | 155 ++------------- .../DocumentObjectsMappingExportTest.groovy | 4 +- .../DocumentObjectsMappingImportTest.groovy | 183 ++---------------- .../builder/components/HasBaseTemplate.kt | 23 ++- 8 files changed, 77 insertions(+), 313 deletions(-) diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DisplayRulesImport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DisplayRulesImport.groovy index 62630cf8..19605b24 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DisplayRulesImport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DisplayRulesImport.groovy @@ -7,6 +7,7 @@ package com.quadient.migration.example.common.mapping import com.quadient.migration.api.Migration +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateLocation import com.quadient.migration.api.dto.migrationmodel.MappingItem import com.quadient.migration.example.common.util.Csv import com.quadient.migration.example.common.util.Mapping @@ -56,7 +57,7 @@ static void run(Migration migration, File file) { def newInternal = Csv.deserialize(values.get("internal"), boolean) existingMapping.internal = newInternal - def newBaseTemplate = Csv.deserialize(values.get("baseTemplate"), String.class) + def newBaseTemplate = Csv.deserialize(values.get("baseTemplate"), BaseTemplateLocation.class) existingMapping.baseTemplate = newBaseTemplate def newTargetFolder = Csv.deserialize(values.get("targetFolder"), String.class) diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DocumentObjectsImport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DocumentObjectsImport.groovy index 37e016ba..0c5fdad0 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DocumentObjectsImport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DocumentObjectsImport.groovy @@ -7,6 +7,7 @@ package com.quadient.migration.example.common.mapping import com.quadient.migration.api.Migration +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateLocation import com.quadient.migration.api.dto.migrationmodel.MappingItem import com.quadient.migration.example.common.util.Csv import com.quadient.migration.example.common.util.Mapping @@ -59,8 +60,8 @@ static void run(Migration migration, Path documentObjFilePath) { def newInternal = Csv.deserialize(values.get("internal"), boolean) Mapping.mapProp(existingMapping, existingDocObject, "internal", newInternal) - def newBaseTemplate = Csv.deserialize(values.get("baseTemplate"), String.class) - Mapping.mapProp(existingMapping, existingDocObject, "baseTemplate", newBaseTemplate) + def newBaseTemplate = Csv.deserialize(values.get("baseTemplate"), BaseTemplateLocation.class) + existingMapping.baseTemplate = newBaseTemplate def newTargetFolder = Csv.deserialize(values.get("targetFolder"), String.class) Mapping.mapProp(existingMapping, existingDocObject, "targetFolder", newTargetFolder) diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/util/Csv.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/util/Csv.groovy index cd665cc3..fb138349 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/util/Csv.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/util/Csv.groovy @@ -1,5 +1,8 @@ package com.quadient.migration.example.common.util +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateLocation +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef +import com.quadient.migration.api.dto.migrationmodel.LiteralBaseTemplatePath import com.quadient.migration.api.dto.migrationmodel.Tab import com.quadient.migration.api.dto.migrationmodel.Tabs import com.quadient.migration.shared.Color @@ -102,6 +105,8 @@ static String serialize(Object obj, Size.Unit unitOverride) { case Long: return obj.toString() case Color: return obj.toHex() case IcmPath: return obj.toString() + case BaseTemplateRef: return "\$${obj.id}" + case LiteralBaseTemplatePath: return obj.path case Size: return unitOverride != null ? obj.toString(unitOverride) : obj.toString() case Tabs: return """{ "tabs": ${serialize(obj.tabs)}; "useOutsideTabs": "${serialize(obj.useOutsideTabs)}" }""" case Tab: return """{ "position": ${serialize(obj.position)}; "type": "${serialize(obj.type)}" }""" @@ -142,6 +147,10 @@ static T deserialize(String value, Class cls) { case IcmPath: return IcmPath.from(value) as T case Color: return Color.fromHex(value) as T case Size: return Size.fromString(value) as T + case BaseTemplateLocation: { + if (value.startsWith("@") || value.startsWith("\$")) return new BaseTemplateRef(value.substring(1)) as T + return new LiteralBaseTemplatePath(value) as T + } case Boolean: case boolean: if (value.toLowerCase() == "true") { diff --git a/migration-examples/src/test/groovy/DisplayRulesMappingExportTest.groovy b/migration-examples/src/test/groovy/DisplayRulesMappingExportTest.groovy index 2489b755..09a82f0b 100644 --- a/migration-examples/src/test/groovy/DisplayRulesMappingExportTest.groovy +++ b/migration-examples/src/test/groovy/DisplayRulesMappingExportTest.groovy @@ -1,6 +1,8 @@ +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap import com.quadient.migration.api.dto.migrationmodel.DisplayRule import com.quadient.migration.api.dto.migrationmodel.DisplayRuleRef +import com.quadient.migration.api.dto.migrationmodel.LiteralBaseTemplatePath import com.quadient.migration.api.dto.migrationmodel.VariableStructureRef import com.quadient.migration.data.Active import com.quadient.migration.example.common.mapping.DisplayRulesExport @@ -25,10 +27,10 @@ class DisplayRulesMappingExportTest { when(migration.displayRuleRepository.listAll()).thenReturn([ new DisplayRule("empty", null, [], new CustomFieldMap([:]), null, null, null, null, true, [], null, null, null, null), - new DisplayRule("full", "full", ["foo", "bar"], new CustomFieldMap([:]), null, null, null, new DisplayRuleRef("targetId1"), false, [], null, "targetFolder1", "baseTemplate1", new VariableStructureRef("varStruct1")), + new DisplayRule("full", "full", ["foo", "bar"], new CustomFieldMap([:]), null, null, null, new DisplayRuleRef("targetId1"), false, [], null, "targetFolder1", new LiteralBaseTemplatePath("baseTemplate1"), new VariableStructureRef("varStruct1")), new DisplayRule("with-variable-structure", "with-var-struct", [], new CustomFieldMap([:]), null, null, null, null, true, [], null, null, null, new VariableStructureRef("varStruct2")), new DisplayRule("overridden empty", null, [], new CustomFieldMap([:]), null, null, null, null, true, [], null, null, null, null), - new DisplayRule("overridden full", "full", ["foo", "bar"], new CustomFieldMap(["originalName": "originalFull"]), null, null, null, new DisplayRuleRef("targetId2"), false, [], null, "targetFolder2", "baseTemplate2", null), + new DisplayRule("overridden full", "full", ["foo", "bar"], new CustomFieldMap(["originalName": "originalFull"]), null, null, null, new DisplayRuleRef("targetId2"), false, [], null, "targetFolder2", new BaseTemplateRef("baseTemplate2"), null), ]) when(migration.statusTrackingRepository.findLastEventRelevantToOutput(any(), any(), any())).thenReturn(new Active()) @@ -41,7 +43,7 @@ class DisplayRulesMappingExportTest { full,full,false,baseTemplate1,targetFolder1,targetId1,varStruct1,Active,,[foo; bar], with-variable-structure,with-var-struct,true,,,,varStruct2,Active,,[], overridden empty,,true,,,,,Active,,[], - overridden full,full,false,baseTemplate2,targetFolder2,targetId2,,Active,originalFull,[foo; bar], + overridden full,full,false,\$baseTemplate2,targetFolder2,targetId2,,Active,originalFull,[foo; bar], """.stripIndent() Assertions.assertEquals(expected, mappingFile.toFile().text.replaceAll("\\r\\n|\\r", "\n")) } diff --git a/migration-examples/src/test/groovy/DisplayRulesMappingImportTest.groovy b/migration-examples/src/test/groovy/DisplayRulesMappingImportTest.groovy index bb763dc4..7f6bc8f6 100644 --- a/migration-examples/src/test/groovy/DisplayRulesMappingImportTest.groovy +++ b/migration-examples/src/test/groovy/DisplayRulesMappingImportTest.groovy @@ -1,5 +1,7 @@ import com.quadient.migration.api.InspireOutput import com.quadient.migration.api.Migration +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef +import com.quadient.migration.api.dto.migrationmodel.LiteralBaseTemplatePath import com.quadient.migration.api.dto.migrationmodel.MappingItem import com.quadient.migration.api.dto.migrationmodel.builder.DisplayRuleBuilder import com.quadient.migration.data.Active @@ -22,169 +24,49 @@ class DisplayRulesMappingImportTest { File dir @Test - void overridesDisplayRuleName() { + void overridesAllMappableFields() { def migration = Utils.mockMigration() Path mappingFile = Paths.get(dir.path, "testProject.csv") def input = """\ id,name,internal,baseTemplate,targetFolder,targetId,variableStructureRef,status,originalName (read-only),originLocations (read-only) unchanged,,true,,,,,Active,,[] - kept,keptName,true,,,,,Active,,[] - overridden,someName,true,,,,,Active,,[] + overridden,someName,false,overriddenTemplate,overriddenFolder,overriddenId,overriddenVarStruct,Active,,[] """.stripIndent() mappingFile.toFile().write(input) givenExistingDisplayRule(migration, "unchanged", null, true, null, null, null, null) givenExistingDisplayRuleMapping(migration, "unchanged", null, null, null, null, null, null) - givenExistingDisplayRule(migration, "kept", "someName", true, null, null, null, null) - givenExistingDisplayRuleMapping(migration, "kept", "keptName", null, null, null, null, null) - givenExistingDisplayRule(migration, "overridden", "previousName", true, null, null, null, null) - givenExistingDisplayRuleMapping(migration, "overridden", "previousName", null, null, null, null, null) + givenExistingDisplayRule(migration, "overridden", "previousName", true, "previousId", "previousTemplate", "previousFolder", "previousVarStruct") + givenExistingDisplayRuleMapping(migration, "overridden", "previousName", true, "previousId", "previousTemplate", "previousFolder", "previousVarStruct") DisplayRulesImport.run(migration, mappingFile.toFile()) verify(migration.mappingRepository, times(1)).upsertBatch([ "unchanged" : new MappingItem.DisplayRule(null, null, null, null, null, true), - "kept" : new MappingItem.DisplayRule("keptName", null, null, null, null, true), - "overridden": new MappingItem.DisplayRule("someName", null, null, null, null, true) + "overridden": new MappingItem.DisplayRule("someName", "overriddenFolder", "overriddenId", new LiteralBaseTemplatePath("overriddenTemplate"), "overriddenVarStruct", false) ]) verify(migration.mappingRepository, times(1)).applyAllDisplayRuleMappings() } @Test - void overridesDisplayRuleInternal() { + void overridesDisplayRuleBaseTemplateRef() { def migration = Utils.mockMigration() Path mappingFile = Paths.get(dir.path, "testProject.csv") def input = """\ id,name,internal,baseTemplate,targetFolder,targetId,variableStructureRef,status,originalName (read-only),originLocations (read-only) - unchanged,,false,,,,,Active,,[] - kept,,true,,,,,Active,,[] - overridden,,true,,,,,Active,,[] + atPrefixed,,true,@someBaseTemplateId,,,,Active,,[] + dollarPrefixed,,true,\$anotherBaseTemplateId,,,,Active,,[] """.stripIndent() mappingFile.toFile().write(input) - givenExistingDisplayRule(migration, "unchanged", null, false, null, null, null, null) - givenExistingDisplayRuleMapping(migration, "unchanged", null, null, null, null, null, null) - givenExistingDisplayRule(migration, "kept", null, true, null, null, null, null) - givenExistingDisplayRuleMapping(migration, "kept", null, true, null, null, null, null) - givenExistingDisplayRule(migration, "overridden", null, false, null, null, null, null) - givenExistingDisplayRuleMapping(migration, "overridden", null, false, null, null, null, null) - - DisplayRulesImport.run(migration, mappingFile.toFile()) - - verify(migration.mappingRepository, times(1)).upsertBatch([ - "unchanged" : new MappingItem.DisplayRule(null, null, null, null, null, false), - "kept" : new MappingItem.DisplayRule(null, null, null, null, null, true), - "overridden": new MappingItem.DisplayRule(null, null, null, null, null, true) - ]) - verify(migration.mappingRepository, times(1)).applyAllDisplayRuleMappings() - } - - @Test - void overridesDisplayRuleBaseTemplate() { - def migration = Utils.mockMigration() - Path mappingFile = Paths.get(dir.path, "testProject.csv") - def input = """\ - id,name,internal,baseTemplate,targetFolder,targetId,variableStructureRef,status,originalName (read-only),originLocations (read-only) - unchanged,,true,,,,,Active,,[] - kept,,true,keptTemplate,,,,Active,,[] - overridden,,true,overriddenTemplate,,,,Active,,[] - """.stripIndent() - mappingFile.toFile().write(input) - givenExistingDisplayRule(migration, "unchanged", null, true, null, null, null, null) - givenExistingDisplayRuleMapping(migration, "unchanged", null, null, null, null, null, null) - givenExistingDisplayRule(migration, "kept", null, true, null, "keptTemplate", null, null) - givenExistingDisplayRuleMapping(migration, "kept", null, null, null, "keptTemplate", null, null) - givenExistingDisplayRule(migration, "overridden", null, true, null, "previousTemplate", null, null) - givenExistingDisplayRuleMapping(migration, "overridden", null, null, null, "previousTemplate", null, null) + givenExistingDisplayRule(migration, "atPrefixed", null, true, null, null, null, null) + givenExistingDisplayRuleMapping(migration, "atPrefixed", null, null, null, null, null, null) + givenExistingDisplayRule(migration, "dollarPrefixed", null, true, null, null, null, null) + givenExistingDisplayRuleMapping(migration, "dollarPrefixed", null, null, null, null, null, null) DisplayRulesImport.run(migration, mappingFile.toFile()) verify(migration.mappingRepository, times(1)).upsertBatch([ - "unchanged" : new MappingItem.DisplayRule(null, null, null, null, null, true), - "kept" : new MappingItem.DisplayRule(null, null, null, "keptTemplate", null, true), - "overridden": new MappingItem.DisplayRule(null, null, null, "overriddenTemplate", null, true) - ]) - verify(migration.mappingRepository, times(1)).applyAllDisplayRuleMappings() - } - - @Test - void overridesDisplayRuleTargetFolder() { - def migration = Utils.mockMigration() - Path mappingFile = Paths.get(dir.path, "testProject.csv") - def input = """\ - id,name,internal,baseTemplate,targetFolder,targetId,variableStructureRef,status,originalName (read-only),originLocations (read-only) - unchanged,,true,,,,,Active,,[] - kept,,true,,keptFolder,,,Active,,[] - overridden,,true,,overriddenFolder,,,Active,,[] - """.stripIndent() - mappingFile.toFile().write(input) - givenExistingDisplayRule(migration, "unchanged", null, true, null, null, null, null) - givenExistingDisplayRuleMapping(migration, "unchanged", null, null, null, null, null, null) - givenExistingDisplayRule(migration, "kept", null, true, null, null, "keptFolder", null) - givenExistingDisplayRuleMapping(migration, "kept", null, null, null, null, "keptFolder", null) - givenExistingDisplayRule(migration, "overridden", null, true, null, null, "previousFolder", null) - givenExistingDisplayRuleMapping(migration, "overridden", null, null, null, null, "previousFolder", null) - - DisplayRulesImport.run(migration, mappingFile.toFile()) - - verify(migration.mappingRepository, times(1)).upsertBatch([ - "unchanged" : new MappingItem.DisplayRule(null, null, null, null, null, true), - "kept" : new MappingItem.DisplayRule(null, "keptFolder", null, null, null, true), - "overridden": new MappingItem.DisplayRule(null, "overriddenFolder", null, null, null, true) - ]) - verify(migration.mappingRepository, times(1)).applyAllDisplayRuleMappings() - } - - @Test - void overridesDisplayRuleTargetId() { - def migration = Utils.mockMigration() - Path mappingFile = Paths.get(dir.path, "testProject.csv") - def input = """\ - id,name,internal,baseTemplate,targetFolder,targetId,variableStructureRef,status,originalName (read-only),originLocations (read-only) - unchanged,,true,,,,,Active,,[] - kept,,true,,,keptId,,Active,,[] - overridden,,true,,,overriddenId,,Active,,[] - """.stripIndent() - mappingFile.toFile().write(input) - givenExistingDisplayRule(migration, "unchanged", null, true, null, null, null, null) - givenExistingDisplayRuleMapping(migration, "unchanged", null, null, null, null, null, null) - givenExistingDisplayRule(migration, "kept", null, true, "keptId", null, null, null) - givenExistingDisplayRuleMapping(migration, "kept", null, null, "keptId", null, null, null) - givenExistingDisplayRule(migration, "overridden", null, true, "previousId", null, null, null) - givenExistingDisplayRuleMapping(migration, "overridden", null, null, "previousId", null, null, null) - - DisplayRulesImport.run(migration, mappingFile.toFile()) - - verify(migration.mappingRepository, times(1)).upsertBatch([ - "unchanged" : new MappingItem.DisplayRule(null, null, null, null, null, true), - "kept" : new MappingItem.DisplayRule(null, null, "keptId", null, null, true), - "overridden": new MappingItem.DisplayRule(null, null, "overriddenId", null, null, true) - ]) - verify(migration.mappingRepository, times(1)).applyAllDisplayRuleMappings() - } - - @Test - void overridesDisplayRuleVariableStructureRef() { - def migration = Utils.mockMigration() - Path mappingFile = Paths.get(dir.path, "testProject.csv") - def input = """\ - id,name,internal,baseTemplate,targetFolder,targetId,variableStructureRef,status,originalName (read-only),originLocations (read-only) - unchanged,,true,,,,,Active,,[] - kept,,true,,,,keptVarStruct,Active,,[] - overridden,,true,,,,overriddenVarStruct,Active,,[] - """.stripIndent() - mappingFile.toFile().write(input) - givenExistingDisplayRule(migration, "unchanged", null, true, null, null, null, null) - givenExistingDisplayRuleMapping(migration, "unchanged", null, null, null, null, null, null) - givenExistingDisplayRule(migration, "kept", null, true, null, null, null, "keptVarStruct") - givenExistingDisplayRuleMapping(migration, "kept", null, null, null, null, null, "keptVarStruct") - givenExistingDisplayRule(migration, "overridden", null, true, null, null, null, "previousVarStruct") - givenExistingDisplayRuleMapping(migration, "overridden", null, null, null, null, null, "previousVarStruct") - - DisplayRulesImport.run(migration, mappingFile.toFile()) - - verify(migration.mappingRepository, times(1)).upsertBatch([ - "unchanged" : new MappingItem.DisplayRule(null, null, null, null, null, true), - "kept" : new MappingItem.DisplayRule(null, null, null, null, "keptVarStruct", true), - "overridden": new MappingItem.DisplayRule(null, null, null, null, "overriddenVarStruct", true) + "atPrefixed" : new MappingItem.DisplayRule(null, null, null, new BaseTemplateRef("someBaseTemplateId"), null, true), + "dollarPrefixed": new MappingItem.DisplayRule(null, null, null, new BaseTemplateRef("anotherBaseTemplateId"), null, true) ]) verify(migration.mappingRepository, times(1)).applyAllDisplayRuleMappings() } @@ -233,7 +115,7 @@ class DisplayRulesMappingImportTest { DisplayRulesImport.run(migration, mappingFile.toFile()) verify(migration.mappingRepository, times(1)).upsertBatch([ - "rule1": new MappingItem.DisplayRule("myName", "myFolder", "myId", "myTemplate", "myVarStruct", false) + "rule1": new MappingItem.DisplayRule("myName", "myFolder", "myId", new LiteralBaseTemplatePath("myTemplate"), "myVarStruct", false) ]) verify(migration.mappingRepository, times(1)).applyAllDisplayRuleMappings() } @@ -264,7 +146,8 @@ class DisplayRulesMappingImportTest { String baseTemplate, String targetFolder, String variableStructureRef) { + def baseTemplateLocation = baseTemplate ? new LiteralBaseTemplatePath(baseTemplate) : null when(mig.mappingRepository.getDisplayRuleMapping(id)) - .thenReturn(new MappingItem.DisplayRule(name, targetFolder, targetId, baseTemplate, variableStructureRef, internal)) + .thenReturn(new MappingItem.DisplayRule(name, targetFolder, targetId, baseTemplateLocation, variableStructureRef, internal)) } } diff --git a/migration-examples/src/test/groovy/DocumentObjectsMappingExportTest.groovy b/migration-examples/src/test/groovy/DocumentObjectsMappingExportTest.groovy index 78586dfa..e15ce6a1 100644 --- a/migration-examples/src/test/groovy/DocumentObjectsMappingExportTest.groovy +++ b/migration-examples/src/test/groovy/DocumentObjectsMappingExportTest.groovy @@ -37,7 +37,7 @@ class DocumentObjectsMappingExportTest { .customFields(["originalName": "originalFull"]) .targetFolder("someDir") .variableStructureRef("struct") - .baseTemplate("tmpl.wfd") + .baseTemplateRef("tmplRef") .build(), ]) @@ -50,7 +50,7 @@ class DocumentObjectsMappingExportTest { empty,,Block,false,,,,Active,false,,,,[] full,full,Page,false,tmpl.wfd,someDir,struct,Active,true,placeholder,reason,,[foo; bar] overridden empty,,Block,false,,,,Active,false,,,,[] - overridden full,full,Page,false,tmpl.wfd,someDir,struct,Active,false,,,originalFull,[foo; bar] + overridden full,full,Page,false,\$tmplRef,someDir,struct,Active,false,,,originalFull,[foo; bar] """.stripIndent() Assertions.assertEquals(expected, mappingFile.toFile().text.replaceAll("\\r\\n|\\r", "\n")) } diff --git a/migration-examples/src/test/groovy/DocumentObjectsMappingImportTest.groovy b/migration-examples/src/test/groovy/DocumentObjectsMappingImportTest.groovy index 402d3fd3..c460150d 100644 --- a/migration-examples/src/test/groovy/DocumentObjectsMappingImportTest.groovy +++ b/migration-examples/src/test/groovy/DocumentObjectsMappingImportTest.groovy @@ -1,4 +1,6 @@ import com.quadient.migration.api.Migration +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef +import com.quadient.migration.api.dto.migrationmodel.LiteralBaseTemplatePath import com.quadient.migration.api.dto.migrationmodel.MappingItem import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder import com.quadient.migration.example.common.mapping.DocumentObjectsImport @@ -16,197 +18,49 @@ class DocumentObjectsMappingImportTest { File dir @Test - void overridesDocumentObjectName() { + void overridesAllMappableFields() { def migration = Utils.mockMigration() Path mappingFile = Paths.get(dir.path, "testProject-variables.csv") def input = """\ - id,name,type,internal,originLocation,baseTemplate,targetFolder,variableStructureId,status - unchanged,,Block,false,[],,,,Active - kept,keptName,Block,false,[],,,,Active - overridden,someName,Block,false,[],,,,Active - """.stripIndent() - mappingFile.toFile().write(input) - givenExistingDocumentObject(migration, "unchanged", null, false, null, null, null, null) - givenExistingDocumentObjectMapping(migration, "unchanged", null, null, null, null, null, null) - givenExistingDocumentObject(migration, "kept", "someName", false, null, null, null, null) - givenExistingDocumentObjectMapping(migration, "kept", "keptName", null, null, null, null, null) - givenExistingDocumentObject(migration, "overridden", "previousName", false, null, null, null, null) - givenExistingDocumentObjectMapping(migration, "overridden", "previousName", null, null, null, null, null) - - DocumentObjectsImport.run(migration, mappingFile) - - verify(migration.mappingRepository, times(1)).upsertBatch([ - "unchanged" : new MappingItem.DocumentObject(null, false, null, null, DocumentObjectType.Block, null, new SkipOptions(false, null, null)), - "kept" : new MappingItem.DocumentObject("keptName", false, null, null, DocumentObjectType.Block, null, new SkipOptions(false, null, null)), - "overridden": new MappingItem.DocumentObject("someName", false, null, null, DocumentObjectType.Block, null, new SkipOptions(false, null, null)) - ]) - verify(migration.mappingRepository, times(1)).applyAllDocumentObjectMappings() - } - - @Test - void overridesDocumentObjectInternal() { - def migration = Utils.mockMigration() - Path mappingFile = Paths.get(dir.path, "testProject-variables.csv") - def input = """\ - id,name,type,internal,originLocation,baseTemplate,targetFolder,variableStructureId,status - unchanged,,Block,false,[],,,,Active - kept,,Block,true,[],,,,Active - overridden,,Block,true,[],,,,Active - """.stripIndent() - mappingFile.toFile().write(input) - givenExistingDocumentObject(migration, "unchanged", null, false, null, null, null, null) - givenExistingDocumentObjectMapping(migration, "unchanged", null, null, null, null, null, null) - givenExistingDocumentObject(migration, "kept", null, true, null, null, null, null) - givenExistingDocumentObjectMapping(migration, "kept", null, true, null, null, null, null) - givenExistingDocumentObject(migration, "overridden", null, false, null, null, null, null) - givenExistingDocumentObjectMapping(migration, "overridden", null, false, null, null, null, null) - - DocumentObjectsImport.run(migration, mappingFile) - - verify(migration.mappingRepository, times(1)).upsertBatch([ - "unchanged" : new MappingItem.DocumentObject(null, false, null, null, DocumentObjectType.Block, null, new SkipOptions(false, null, null)), - "kept" : new MappingItem.DocumentObject(null, true, null, null, DocumentObjectType.Block, null, new SkipOptions(false, null, null)), - "overridden": new MappingItem.DocumentObject(null, true, null, null, DocumentObjectType.Block, null, new SkipOptions(false, null, null)) - ]) - verify(migration.mappingRepository, times(1)).applyAllDocumentObjectMappings() - } - - @Test - void overridesDocumentObjectBaseTemplate() { - def migration = Utils.mockMigration() - Path mappingFile = Paths.get(dir.path, "testProject-variables.csv") - def input = """\ - id,name,type,internal,originLocation,baseTemplate,targetFolder,variableStructureId,status - unchanged,,Block,false,[],,,,Active - kept,,Block,false,[],keptTemplate,,,Active - overridden,,Block,false,[],overriddenTemplate,,,Active - """.stripIndent() - mappingFile.toFile().write(input) - givenExistingDocumentObject(migration, "unchanged", null, false, null, null, null, null) - givenExistingDocumentObjectMapping(migration, "unchanged", null, null, null, null, null, null) - givenExistingDocumentObject(migration, "kept", null, false, null, null, null, null) - givenExistingDocumentObjectMapping(migration, "kept", null, null, "keptTemplate", null, null, null) - givenExistingDocumentObject(migration, "overridden", null, false, "previousTemplate", null, null, null) - givenExistingDocumentObjectMapping(migration, "overridden", null, null, "previousTemplate", null, null, null) - - DocumentObjectsImport.run(migration, mappingFile) - - verify(migration.mappingRepository, times(1)).upsertBatch([ - "unchanged" : new MappingItem.DocumentObject(null, false, null, null, DocumentObjectType.Block, null, new SkipOptions(false, null, null)), - "kept" : new MappingItem.DocumentObject(null, false, "keptTemplate", null, DocumentObjectType.Block, null, new SkipOptions(false, null, null)), - "overridden": new MappingItem.DocumentObject(null, false, "overriddenTemplate", null, DocumentObjectType.Block, null, new SkipOptions(false, null, null)) - ]) - verify(migration.mappingRepository, times(1)).applyAllDocumentObjectMappings() - } - - @Test - void overridesDocumentObjectTargetFolder() { - def migration = Utils.mockMigration() - Path mappingFile = Paths.get(dir.path, "testProject-variables.csv") - def input = """\ - id,name,type,internal,originLocation,baseTemplate,targetFolder,variableStructureId,status - unchanged,,Block,false,[],,,,Active - kept,,Block,false,[],,keptFolder,,Active - overridden,,Block,false,[],,overriddenFolder,,Active - """.stripIndent() - mappingFile.toFile().write(input) - givenExistingDocumentObject(migration, "unchanged", null, false, null, null, null, null) - givenExistingDocumentObjectMapping(migration, "unchanged", null, null, null, null, null, null) - givenExistingDocumentObject(migration, "kept", null, false, null, null, null, null) - givenExistingDocumentObjectMapping(migration, "kept", null, null, null, "keptFolder", null, null) - givenExistingDocumentObject(migration, "overridden", null, false, null, "previousFolder", null, null) - givenExistingDocumentObjectMapping(migration, "overridden", null, null, null, "previousFolder", null, null) - - DocumentObjectsImport.run(migration, mappingFile) - - verify(migration.mappingRepository, times(1)).upsertBatch([ - "unchanged" : new MappingItem.DocumentObject(null, false, null, null, DocumentObjectType.Block, null, new SkipOptions(false, null, null)), - "kept" : new MappingItem.DocumentObject(null, false, null, "keptFolder", DocumentObjectType.Block, null, new SkipOptions(false, null, null)), - "overridden": new MappingItem.DocumentObject(null, false, null, "overriddenFolder", DocumentObjectType.Block, null, new SkipOptions(false, null, null)) - ]) - verify(migration.mappingRepository, times(1)).applyAllDocumentObjectMappings() - } - - @Test - void overridesVariableStructureRef() { - def migration = Utils.mockMigration() - Path mappingFile = Paths.get(dir.path, "testProject-variables.csv") - def input = """\ - id,name,type,internal,originLocation,baseTemplate,targetFolder,variableStructureId,status + id,name,type,internal,originLocation,baseTemplate,targetFolder,variableStructureId,status,skip,skipPlaceholder,skipReason unchanged,,Block,false,[],,,,Active - kept,,Block,false,[],,,keptVarStructure,Active - overridden,,Block,false,[],,,overriddenVarStructure,Active + overridden,someName,Page,true,[],overriddenTemplate,overriddenFolder,overriddenVarStructure,Active,true,placeholder,reason """.stripIndent() mappingFile.toFile().write(input) givenExistingDocumentObject(migration, "unchanged", null, false, null, null, null, null) givenExistingDocumentObjectMapping(migration, "unchanged", null, null, null, null, null, null) - givenExistingDocumentObject(migration, "kept", null, false, null, null, null, "keptVarStructure") - givenExistingDocumentObjectMapping(migration, "kept", null, null, null, null, null, "keptVarStructure") - givenExistingDocumentObject(migration, "overridden", null, false, null, null, null, "previousVarStructure") - givenExistingDocumentObjectMapping(migration, "overridden", null, null, null, null, null, "overriddenVarStructure") + givenExistingDocumentObject(migration, "overridden", "previousName", false, "previousTemplate", "previousFolder", DocumentObjectType.Template, "previousVarStructure") + givenExistingDocumentObjectMapping(migration, "overridden", "previousName", false, "previousTemplate", "previousFolder", DocumentObjectType.Template, "previousVarStructure") DocumentObjectsImport.run(migration, mappingFile) verify(migration.mappingRepository, times(1)).upsertBatch([ "unchanged" : new MappingItem.DocumentObject(null, false, null, null, DocumentObjectType.Block, null, new SkipOptions(false, null, null)), - "kept" : new MappingItem.DocumentObject(null, false, null, null, DocumentObjectType.Block, "keptVarStructure", new SkipOptions(false, null, null)), - "overridden": new MappingItem.DocumentObject(null, false, null, null, DocumentObjectType.Block, "overriddenVarStructure", new SkipOptions(false, null, null)) + "overridden": new MappingItem.DocumentObject("someName", true, new LiteralBaseTemplatePath("overriddenTemplate"), "overriddenFolder", DocumentObjectType.Page, "overriddenVarStructure", new SkipOptions(true, "placeholder", "reason")) ]) verify(migration.mappingRepository, times(1)).applyAllDocumentObjectMappings() } @Test - void overridesDocumentObjectType() { + void overridesDocumentObjectBaseTemplateRef() { def migration = Utils.mockMigration() Path mappingFile = Paths.get(dir.path, "testProject-variables.csv") def input = """\ id,name,type,internal,originLocation,baseTemplate,targetFolder,variableStructureId,status - unchanged,,Block,false,[],,,,Active - kept,,Template,false,[],,,,Active - overridden,,Page,false,[],,,,Active + atPrefixed,,Block,false,[],@someBaseTemplateId,,,Active + dollarPrefixed,,Block,false,[],\$anotherBaseTemplateId,,,Active """.stripIndent() mappingFile.toFile().write(input) - givenExistingDocumentObject(migration, "unchanged", null, false, null, null, null, null) - givenExistingDocumentObjectMapping(migration, "unchanged", null, null, null, null, null, null) - givenExistingDocumentObject(migration, "kept", null, false, null, null, null, null) - givenExistingDocumentObjectMapping(migration, "kept", null, null, null, null, DocumentObjectType.Template, null) - givenExistingDocumentObject(migration, "overridden", null, false, null, null, DocumentObjectType.Template, null) - givenExistingDocumentObjectMapping(migration, "overridden", null, null, null, null, DocumentObjectType.Section, null) + givenExistingDocumentObject(migration, "atPrefixed", null, false, null, null, null, null) + givenExistingDocumentObjectMapping(migration, "atPrefixed", null, null, null, null, null, null) + givenExistingDocumentObject(migration, "dollarPrefixed", null, false, null, null, null, null) + givenExistingDocumentObjectMapping(migration, "dollarPrefixed", null, null, null, null, null, null) DocumentObjectsImport.run(migration, mappingFile) verify(migration.mappingRepository, times(1)).upsertBatch([ - "unchanged" : new MappingItem.DocumentObject(null, false, null, null, DocumentObjectType.Block, null, new SkipOptions(false, null, null)), - "kept" : new MappingItem.DocumentObject(null, false, null, null, DocumentObjectType.Template, null, new SkipOptions(false, null, null)), - "overridden": new MappingItem.DocumentObject(null, false, null, null, DocumentObjectType.Page, null, new SkipOptions(false, null, null)) - ]) - verify(migration.mappingRepository, times(1)).applyAllDocumentObjectMappings() - } - - @Test - void overridesSkipOptions() { - def migration = Utils.mockMigration() - Path mappingFile = Paths.get(dir.path, "testProject-variables.csv") - def input = """\ - id,name,type,internal,originLocation,baseTemplate,targetFolder,variableStructureId,status,skip,skipPlaceholder,skipReason - unchanged,,Block,false,[],,,,Active - kept,,Template,false,[],,,,Active - overridden,,Page,false,[],,,,Active,true,placeholder,reason - """.stripIndent() - mappingFile.toFile().write(input) - givenExistingDocumentObject(migration, "unchanged", null, false, null, null, null, null) - givenExistingDocumentObjectMapping(migration, "unchanged", null, null, null, null, null, null) - givenExistingDocumentObject(migration, "kept", null, false, null, null, null, null) - givenExistingDocumentObjectMapping(migration, "kept", null, null, null, null, DocumentObjectType.Template, null) - givenExistingDocumentObject(migration, "overridden", null, false, null, null, DocumentObjectType.Template, null) - givenExistingDocumentObjectMapping(migration, "overridden", null, null, null, null, DocumentObjectType.Section, null) - - DocumentObjectsImport.run(migration, mappingFile) - - verify(migration.mappingRepository, times(1)).upsertBatch([ - "unchanged" : new MappingItem.DocumentObject(null, false, null, null, DocumentObjectType.Block, null, new SkipOptions(false, null, null)), - "kept" : new MappingItem.DocumentObject(null, false, null, null, DocumentObjectType.Template, null, new SkipOptions(false, null, null)), - "overridden": new MappingItem.DocumentObject(null, false, null, null, DocumentObjectType.Page, null, new SkipOptions(true, "placeholder", "reason")) + "atPrefixed" : new MappingItem.DocumentObject(null, false, new BaseTemplateRef("someBaseTemplateId"), null, DocumentObjectType.Block, null, new SkipOptions(false, null, null)), + "dollarPrefixed": new MappingItem.DocumentObject(null, false, new BaseTemplateRef("anotherBaseTemplateId"), null, DocumentObjectType.Block, null, new SkipOptions(false, null, null)) ]) verify(migration.mappingRepository, times(1)).applyAllDocumentObjectMappings() } @@ -240,7 +94,8 @@ class DocumentObjectsMappingImportTest { String targetFolder, DocumentObjectType type, String varStructureRef) { + def baseTemplateLocation = baseTemplate ? new LiteralBaseTemplatePath(baseTemplate) : null when(mig.mappingRepository.getDocumentObjectMapping(id)) - .thenReturn(new MappingItem.DocumentObject(name, internal, baseTemplate, targetFolder, type, varStructureRef, null)) + .thenReturn(new MappingItem.DocumentObject(name, internal, baseTemplateLocation, targetFolder, type, varStructureRef, null)) } } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/components/HasBaseTemplate.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/components/HasBaseTemplate.kt index 34994825..101ed110 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/components/HasBaseTemplate.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/components/HasBaseTemplate.kt @@ -11,10 +11,22 @@ interface HasBaseTemplate { /** * Override the default base template for this object. - * @param baseTemplate Location (literal path or base template reference) of the base template to use for this object. + * @param baseTemplateLocation Location (literal path or base template reference) of the base template to use for this object. * @return This builder instance for method chaining. */ - fun baseTemplate(baseTemplate: BaseTemplateLocation?) = apply { this.baseTemplate = baseTemplate } as T + fun baseTemplate(baseTemplateLocation: BaseTemplateLocation?) = + apply { this.baseTemplate = baseTemplateLocation } as T + + /** + * Overrides the default base template for this object with a literal ICM path. + * @param path Path to the base template to use for this object. + * @return This builder instance for method chaining. + */ + @Deprecated( + message = "Use baseTemplatePath() for a literal path or baseTemplateRef() for a reference to a BaseTemplate migration object instead.", + replaceWith = ReplaceWith("baseTemplatePath(baseTemplate)"), + ) + fun baseTemplate(path: String?) = apply { this.baseTemplate = path?.let { LiteralBaseTemplatePath(it) } } as T /** * Overrides the default base template for this object with a literal ICM path. @@ -25,15 +37,16 @@ interface HasBaseTemplate { /** * Overrides the default base template for this object with a reference to a [BaseTemplate] migration object. - * @param baseTemplateId Id of the [BaseTemplate] migration object to use for this object. + * @param id ID of the [BaseTemplate] migration object to use for this object. * @return This builder instance for method chaining. */ - fun baseTemplateRef(baseTemplateId: String?) = apply { this.baseTemplate = baseTemplateId?.let { BaseTemplateRef(it) } } as T + fun baseTemplateRef(id: String?) = apply { this.baseTemplate = id?.let { BaseTemplateRef(it) } } as T /** * Overrides the default base template for this object with a reference to a [BaseTemplate] migration object. * @param baseTemplate The [BaseTemplate] migration object to use for this object. * @return This builder instance for method chaining. */ - fun baseTemplateRef(baseTemplate: BaseTemplate) = apply { this.baseTemplate = BaseTemplateRef(baseTemplate.id) } as T + fun baseTemplateRef(baseTemplate: BaseTemplate) = + apply { this.baseTemplate = BaseTemplateRef(baseTemplate.id) } as T } From 5729fea99c1f06e44f4a56e9387aed6a1b7689fa Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Mon, 20 Jul 2026 13:14:52 +0200 Subject: [PATCH 4/5] MIG-562 Base Template definition and deployment - Initial support - first iteration of areas mapping and layout index.html base template creation and document objects to base templates mapping --- migration-examples/layout/index.html | 251 ++++++++++++++++-- .../example/common/mapping/AreasExport.groovy | 7 +- .../example/common/mapping/AreasImport.groovy | 125 ++++++++- .../common/mapping/AttachmentsImport.groovy | 6 +- .../common/mapping/DisplayRulesImport.groovy | 6 +- .../mapping/DocumentObjectsImport.groovy | 6 +- .../common/mapping/ImagesImport.groovy | 6 +- .../mapping/ParagraphStylesImport.groovy | 6 +- .../common/mapping/TablesImport.groovy | 9 +- .../common/mapping/TextStylesImport.groovy | 6 +- .../common/mapping/VariablesImport.groovy | 6 +- .../example/common/util/Mapping.groovy | 12 + .../src/test/groovy/AreasExportTest.groovy | 48 +++- .../src/test/groovy/AreasImportTest.groovy | 91 +++++++ .../src/test/groovy/Utils.groovy | 3 + 15 files changed, 503 insertions(+), 85 deletions(-) diff --git a/migration-examples/layout/index.html b/migration-examples/layout/index.html index 7f9a56a8..00ca1c2f 100644 --- a/migration-examples/layout/index.html +++ b/migration-examples/layout/index.html @@ -421,6 +421,7 @@
+
@@ -449,6 +450,7 @@ * @typedef {Object} LayoutData * @property {PageEntry[]} pages * @property {TemplateEntry[]} templates + * @property {({pageIndex: number, areaIndex: number}|null)[]} [rowMap] */ /** @@ -704,7 +706,7 @@ */ function parseCsvToLayoutData(text) { const lines = text.split(/\r?\n/); - if (!lines.length) return { pages: [], templates: [] }; + if (!lines.length) return { pages: [], templates: [], rowMap: [] }; // Parse headers, stripping the " (read-only)" suffix added by Mapping.displayHeader const headers = parseCsvLine(lines[0]).map(h => { @@ -715,12 +717,13 @@ /** @param {string[]} row @param {string} name @returns {string} */ const col = (row, name) => { const idx = headers.indexOf(name); return idx >= 0 ? (row[idx] ?? '') : ''; }; - // Group rows into pages; each unique page in the CSV keeps its declaration order - /** @type {Map} */ + /** @type {Map} */ const pageMap = new Map(); for (let i = 1; i < lines.length; i++) { if (!lines[i].trim()) continue; const row = parseCsvLine(lines[i]); + if (col(row, 'type').trim().toLowerCase() === 'base') continue; + const pageId = col(row, 'pageId') || null; const templateId = col(row, 'templateId') || null; const pageKey = pageId && templateId @@ -741,12 +744,13 @@ pageHeight: col(row, 'pageHeight'), }, rows: [], + lineIndices: [], }); } pageMap.get(pageKey).rows.push(row); + pageMap.get(pageKey).lineIndices.push(i); } - // Assign templatePageIndex as the ordinal of each real page within its template (CSV order) /** @type {Map} */ const pageIndexMap = new Map(); /** @type {Map} */ @@ -758,29 +762,38 @@ tmplPageCounter.set(meta.templateId, n + 1); } - // Build PageEntry objects + // TODO d.svitak - review start /** @type {PageEntry[]} */ const pages = []; - for (const [pageKey, { meta, rows }] of pageMap) { - const rawAreas = /** @type {{x:number,y:number,w:number,h:number,flowToNextPage:boolean,interactiveFlowName:string,contentPreview:string}[]} */ ( - rows.map(row => { - const x = parseSizeMm(col(row, 'x')); - const y = parseSizeMm(col(row, 'y')); - const w = parseSizeMm(col(row, 'width')); - const h = parseSizeMm(col(row, 'height')); - if (x == null || y == null || w == null || h == null) return null; - return { + /** @type {({pageIndex: number, areaIndex: number}|null)[]} */ + const rowMap = new Array(lines.length).fill(null); + for (const [pageKey, { meta, rows, lineIndices }] of pageMap) { + const rawAreasWithLine = rows.map((row, idx) => { + const x = parseSizeMm(col(row, 'x')); + const y = parseSizeMm(col(row, 'y')); + const w = parseSizeMm(col(row, 'width')); + const h = parseSizeMm(col(row, 'height')); + if (x == null || y == null || w == null || h == null) return null; + return { + area: { x, y, w, h, flowToNextPage: col(row, 'flowToNextPage') === 'true', interactiveFlowName: col(row, 'interactiveFlowName') || '', contentPreview: col(row, 'contentPreview') || '', - }; - }).filter(a => a != null) + }, + lineIndex: lineIndices[idx], + }; + }).filter(a => a != null); + const rawAreas = /** @type {{x:number,y:number,w:number,h:number,flowToNextPage:boolean,interactiveFlowName:string,contentPreview:string}[]} */ ( + rawAreasWithLine.map(a => a.area) ); const containment = computeContainment(rawAreas); const areas = /** @type {AreaEntry[]} */ (rawAreas.map((a, idx) => ({ ...a, containedIn: containment[idx] }))); + const pageIndex = pages.length; + rawAreasWithLine.forEach((a, areaIndex) => { rowMap[a.lineIndex] = { pageIndex, areaIndex }; }); + const pw = parseSizeMm(meta.pageWidth); const ph = parseSizeMm(meta.pageHeight); pages.push(/** @type {PageEntry} */ ({ @@ -794,16 +807,43 @@ })); } - return { pages, templates: deriveTemplates(pages) }; + return { pages, templates: deriveTemplates(pages), rowMap }; } +// Raw CSV state kept alongside the parsed LayoutData so the file can be re-serialized +// losslessly (including columns/rows the viewer doesn't otherwise use) for saving back to disk. +/** @type {string[]|null} */ +let currentCsvHeaders = null; +/** @type {string[][]|null} */ +let currentCsvRows = null; +let currentFileName = "areas.csv"; +/** @type {({pageIndex: number, areaIndex: number}|null)[]} */ +let currentRowMap = []; + +const saveCsvBtn = /** @type {HTMLButtonElement} */ (document.getElementById("save-csv")); + /** * @param {File} file */ async function loadFile(file) { try { statusEl.textContent = ""; - render(parseCsvToLayoutData(await file.text())); + const text = await file.text(); + + const lines = text.split(/\r?\n/); + if (lines.length && lines[0].trim() !== "") { + currentCsvHeaders = parseCsvLine(lines[0]); + currentCsvRows = lines.slice(1).map(parseCsvLine); + } else { + currentCsvHeaders = null; + currentCsvRows = null; + } + currentFileName = file.name || "areas.csv"; + saveCsvBtn.disabled = !currentCsvHeaders; + + const layoutData = parseCsvToLayoutData(text); + currentRowMap = layoutData.rowMap ?? []; + render(layoutData); } catch (err) { statusEl.textContent = "Error: " + /** @type {Error} */ (err).message; } @@ -814,6 +854,181 @@ if (file) loadFile(file); }); +function normalizeHeaderName(header) { + const s = (header ?? "").trim(); + return s.endsWith(" (read-only)") ? s.slice(0, -" (read-only)".length) : s; +} + +function ensureCsvColumn(headers, rows, logicalName) { + const idx = headers.findIndex(h => normalizeHeaderName(h) === logicalName); + if (idx >= 0) return idx; + const insertIdx = headers.findIndex(h => normalizeHeaderName(h) === "contentPreview"); + const at = insertIdx >= 0 ? insertIdx : headers.length; + headers.splice(at, 0, logicalName); + rows.forEach(row => { row.splice(at, 0, ""); }); + return at; +} + +function csvEscapeField(field) { + const s = field ?? ""; + if (/[",\r\n]/.test(s)) { + return '"' + s.replace(/"/g, '""') + '"'; + } + return s; +} + +function serializeCsv(headers, rows) { + const lines = [headers.map(csvEscapeField).join(",")]; + for (const row of rows) { + lines.push(row.map(csvEscapeField).join(",")); + } + return lines.join("\r\n") + "\r\n"; +} + +function computeBaseTemplateData(data, viewModel, analysis) { + /** @type {string[][]} */ + const baseRows = []; + /** @type {Map} */ + const overridesByPageIndex = new Map(); + + viewModel.templateGroups.forEach((templateGroup, templateGroupIndex) => { + const baseTemplateId = `G${templateGroupIndex + 1}`; + const baseTemplateName = `Base template of ${templateGroup.templateNames.join(" / ")}`; + + templateGroup.pageGroups.forEach((pageGroup, pageGroupIndex) => { + const basePageId = `${baseTemplateId}-P${pageGroupIndex + 1}`; + const basePageName = `Page group ${pageGroupIndex + 1}`; + + const representativePageIndex = pageGroup.pageIndices[0]; + const representativePage = data.pages[representativePageIndex]; + const representativeGroups = analysis.pageProximityGroups[representativePageIndex] ?? []; + const pageWidthMm = representativePage.pageSize ? `${representativePage.pageSize.w}mm` : ""; + const pageHeightMm = representativePage.pageSize ? `${representativePage.pageSize.h}mm` : ""; + + representativeGroups.forEach((pg, areaGroupIndex) => { + const flowName = `${basePageId}.Area${areaGroupIndex + 1}`; + const flowToNextPage = pg.areaIndices.some(ai => representativePage.areas[ai]?.flowToNextPage); + const contentPreview = pg.areaIndices + .map(ai => representativePage.areas[ai]?.contentPreview) + .filter(Boolean) + .join(";"); + baseRows.push([ + baseTemplateId, baseTemplateName, basePageId, basePageName, + pageWidthMm, pageHeightMm, + flowName, String(flowToNextPage), + `${pg.position.x}mm`, `${pg.position.y}mm`, `${pg.position.w}mm`, `${pg.position.h}mm`, + "Base", "", contentPreview, + ]); + }); + + pageGroup.pageIndices.forEach(pageIndex => { + const page = data.pages[pageIndex]; + const ownGroups = pageIndex === representativePageIndex + ? representativeGroups + : (analysis.pageProximityGroups[pageIndex] ?? []); + + /** @type {(string|null)[]} */ + const flowNames = new Array(page.areas.length).fill(null); + ownGroups.forEach((pg, ordinal) => { + if (ordinal >= representativeGroups.length) return; + const flowName = `${basePageId}.Area${ordinal + 1}`; + pg.areaIndices.forEach(ai => { flowNames[ai] = flowName; }); + }); + overridesByPageIndex.set(pageIndex, { flowNames, targetId: `$${baseTemplateId}` }); + }); + }); + }); + + return { baseRows, overridesByPageIndex }; +} + +async function saveCsv() { + if (!currentCsvHeaders || !currentCsvRows) { + statusEl.textContent = "No CSV loaded to save."; + return; + } + if (!currentData || !currentViewModel || !currentAnalysis) { + statusEl.textContent = "No layout data loaded to save."; + return; + } + + const headers = [...currentCsvHeaders]; + const rows = currentCsvRows.map(row => [...row]); + + const typeIdx = ensureCsvColumn(headers, rows, "type"); + const targetIdIdx = ensureCsvColumn(headers, rows, "targetId"); + const flowNameIdx = ensureCsvColumn(headers, rows, "interactiveFlowName"); + + const { baseRows, overridesByPageIndex } = computeBaseTemplateData(currentData, currentViewModel, currentAnalysis); + + currentRowMap.forEach((entry, lineIndex) => { + if (!entry) return; + const rowIdx = lineIndex - 1; + const row = rows[rowIdx]; + if (!row) return; + + const override = overridesByPageIndex.get(entry.pageIndex); + if (!override) return; + + row[targetIdIdx] = override.targetId; + const flowName = override.flowNames[entry.areaIndex]; + if (flowName != null) row[flowNameIdx] = flowName; + }); + + for (let i = rows.length - 1; i >= 0; i--) { + if (rows[i].every(cell => cell === "")) { + rows.splice(i, 1); + } else if (!rows[i][typeIdx]) { + rows[i][typeIdx] = "Standard"; + } + } + + baseRows.forEach(baseRow => { + const byLogicalName = { + templateId: baseRow[0], templateName: baseRow[1], pageId: baseRow[2], pageName: baseRow[3], + pageWidth: baseRow[4], pageHeight: baseRow[5], interactiveFlowName: baseRow[6], flowToNextPage: baseRow[7], + x: baseRow[8], y: baseRow[9], width: baseRow[10], height: baseRow[11], + type: baseRow[12], targetId: baseRow[13], contentPreview: baseRow[14], + }; + const row = headers.map(h => byLogicalName[normalizeHeaderName(h)] ?? ""); + rows.push(row); + }); + + const text = serializeCsv(headers, rows); + + if (typeof window.showSaveFilePicker === "function") { + try { + const handle = await window.showSaveFilePicker({ + suggestedName: currentFileName, + types: [{ description: "CSV file", accept: { "text/csv": [".csv"] } }], + }); + const writable = await handle.createWritable(); + await writable.write(text); + await writable.close(); + statusEl.textContent = `Saved ${handle.name}`; + return; + } catch (err) { + if (/** @type {Error} */ (err).name === "AbortError") return; + statusEl.textContent = "Error saving file: " + /** @type {Error} */ (err).message; + return; + } + } + + const blob = new Blob([text], { type: "text/csv" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = currentFileName; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + statusEl.textContent = `Downloaded ${currentFileName}`; +} + +saveCsvBtn.addEventListener("click", () => { saveCsv(); }); +// TODO d.svitak - review end + document.body.addEventListener("dragover", e => e.preventDefault()); document.body.addEventListener("drop", e => { e.preventDefault(); diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AreasExport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AreasExport.groovy index 00063705..722444da 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AreasExport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AreasExport.groovy @@ -50,7 +50,9 @@ static void run(Migration migration, Path path) { Mapping.displayHeader("y", true), Mapping.displayHeader("width", true), Mapping.displayHeader("height", true), - Mapping.displayHeader("contentPreview", true) + Mapping.displayHeader("type", false), + Mapping.displayHeader("targetId", false), + Mapping.displayHeader("contentPreview", true), ] writer.writeLine(headers.join(",")) templates.each { template -> @@ -102,6 +104,9 @@ static String buildArea(Migration migration, Number idx, Area area, DocumentObje builder.append(Csv.serialize(area.position.width) + ",") builder.append(Csv.serialize(area.position.height) + ",") + builder.append("Standard,") + builder.append(Csv.serialize(page?.baseTemplate ?: template?.baseTemplate) + ",") + builder.append(Csv.serialize(migration.previewProvider.buildDocumentContentListPreview(area.content))) return builder.toString() diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AreasImport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AreasImport.groovy index 0da1cbd4..7360cb62 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AreasImport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AreasImport.groovy @@ -7,10 +7,17 @@ package com.quadient.migration.example.common.mapping import com.quadient.migration.api.Migration +import com.quadient.migration.api.dto.migrationmodel.BaseTemplate +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateLocation +import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap import com.quadient.migration.api.dto.migrationmodel.DocumentObject import com.quadient.migration.api.dto.migrationmodel.MappingItem import com.quadient.migration.example.common.util.Csv import com.quadient.migration.example.common.util.Mapping +import com.quadient.migration.shared.BaseTemplateArea +import com.quadient.migration.shared.BaseTemplatePage +import com.quadient.migration.shared.Position +import com.quadient.migration.shared.Size import groovy.transform.Field import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -30,20 +37,29 @@ static void run(Migration migration, Path path) { def fileLines = path.toFile().readLines() def columnNames = Csv.parseColumnNames(fileLines.removeFirst()).collect { Mapping.normalizeHeader(it) } - def mappings = new HashMap() + def areaMappings = new HashMap() + def docObjectsToTargetIds = new LinkedHashMap() + def baseTemplateDrafts = new LinkedHashMap() + DocumentObject currentDocumentObject = null - MappingItem.Area mapping = null + MappingItem.Area areaMapping = null int areaIndex = 0 for (line in fileLines) { def values = Csv.getCells(line, columnNames) + def type = Csv.deserialize(values.get("type"), String.class) ?: "Standard" + if (type.equalsIgnoreCase("Base")) { + assignAreaToBaseTemplateDraft(baseTemplateDrafts, values) + continue + } + def pageId = Csv.deserialize(values.get("pageId"), String.class) def templateId = Csv.deserialize(values.get("templateId"), String.class) def documentObjectId = pageId ?: templateId if (currentDocumentObject?.id != documentObjectId) { if (currentDocumentObject != null) { - mappings[currentDocumentObject.id] = mapping + areaMappings[currentDocumentObject.id] = areaMapping } def documentObjectModel = migration.documentObjectRepository.find(documentObjectId) @@ -51,29 +67,114 @@ static void run(Migration migration, Path path) { throw new IllegalStateException("Document object '${documentObjectId}' not found.") } - mapping = migration.mappingRepository.getAreaMapping(documentObjectId) + areaMapping = migration.mappingRepository.getAreaMapping(documentObjectId) currentDocumentObject = documentObjectModel areaIndex = 0 } def interactiveFlowName = Csv.deserialize(values.get("interactiveFlowName"), String.class) - mapping.areas[areaIndex] = interactiveFlowName + areaMapping.areas[areaIndex] = interactiveFlowName def flowToNextPage = Csv.deserialize(values.get("flowToNextPage"), Boolean.class) - mapping.flowToNextPage[areaIndex] = flowToNextPage ?: false + areaMapping.flowToNextPage[areaIndex] = flowToNextPage ?: false + + def targetId = Csv.deserialize(values.get("targetId"), String.class) + if (targetId && !docObjectsToTargetIds.containsKey(documentObjectId)) { + docObjectsToTargetIds[documentObjectId] = targetId + } areaIndex++ } if (currentDocumentObject != null) { - mappings[currentDocumentObject.id] = mapping + areaMappings[currentDocumentObject.id] = areaMapping } - def batches = mappings.entrySet().collate(1000) - for (int i = 0; i < batches.size(); i++) { - log.info "Upserting mappings batch ${i + 1}/${batches.size()} (${batches[i].size()} items)" - migration.mappingRepository.upsertBatch(batches[i].collectEntries()) - } + Mapping.upsertBatched(migration.mappingRepository, areaMappings, "area mappings", log) migration.mappingRepository.applyAllAreaMappings() + + applyDocumentObjectTargetIdMappings(migration, docObjectsToTargetIds) + applyBaseTemplateDraftMappings(migration, baseTemplateDrafts) +} + +private static void assignAreaToBaseTemplateDraft(Map baseTemplateDrafts, Map values) { + def baseTemplateId = Csv.deserialize(values.get("templateId"), String.class) + if (!baseTemplateId) { + throw new IllegalStateException("Rows of type 'Base' must specify a templateId identifying the base template.") + } + def pageGroupId = Csv.deserialize(values.get("pageId"), String.class) ?: baseTemplateId + + def baseTemplateDraft = baseTemplateDrafts.computeIfAbsent(baseTemplateId) { new BaseTemplateDraft() } + if (baseTemplateDraft.name == null) { + baseTemplateDraft.name = Csv.deserialize(values.get("templateName"), String.class) + } + + def page = baseTemplateDraft.pages.computeIfAbsent(pageGroupId) { + new BaseTemplatePage(Csv.deserialize(values.get("pageName"), String.class), + Csv.deserialize(values.get("pageWidth"), Size.class), + Csv.deserialize(values.get("pageHeight"), Size.class), + new ArrayList()) + } + + def interactiveFlowName = Csv.deserialize(values.get("interactiveFlowName"), String.class) + if (!interactiveFlowName) { + throw new IllegalStateException("Rows of type 'Base' must specify an interactiveFlowName for the consolidated area.") + } + def x = Csv.deserialize(values.get("x"), Size.class) + def y = Csv.deserialize(values.get("y"), Size.class) + def width = Csv.deserialize(values.get("width"), Size.class) + def height = Csv.deserialize(values.get("height"), Size.class) + def position = (x != null && y != null && width != null && height != null) ? new Position(x, y, width, height) : null + def flowToNextPage = Csv.deserialize(values.get("flowToNextPage"), Boolean.class) ?: false + + page.areas.add(new BaseTemplateArea(interactiveFlowName, position, flowToNextPage)) } +private static void applyDocumentObjectTargetIdMappings(Migration migration, Map docObjectsToTargetIds) { + if (docObjectsToTargetIds.isEmpty()) return + + def mappings = new HashMap() + docObjectsToTargetIds.each { documentObjectId, targetId -> + def mapping = migration.mappingRepository.getDocumentObjectMapping(documentObjectId) + def existingObject = migration.documentObjectRepository.find(documentObjectId) + + if (mapping.name == null) mapping.name = existingObject?.name + if (mapping.internal == null) mapping.internal = existingObject?.internal + if (mapping.targetFolder == null) mapping.targetFolder = existingObject?.targetFolder + if (mapping.variableStructureRef == null) mapping.variableStructureRef = existingObject?.variableStructureRef?.id + if (mapping.skip == null) mapping.skip = existingObject?.skip + + mapping.baseTemplate = Csv.deserialize(targetId, BaseTemplateLocation.class) + mappings[documentObjectId] = mapping + } + + Mapping.upsertBatched(migration.mappingRepository, mappings, "base template reference mappings", log) + migration.mappingRepository.applyAllDocumentObjectMappings() +} + +private static void applyBaseTemplateDraftMappings(Migration migration, Map baseTemplateDrafts) { + if (baseTemplateDrafts.isEmpty()) return + + def mappings = new HashMap() + baseTemplateDrafts.each { baseTemplateId, draft -> + def existing = migration.baseTemplateRepository.find(baseTemplateId) + if (existing == null) { + migration.baseTemplateRepository.upsert(new BaseTemplate(baseTemplateId, null, [], new CustomFieldMap(new HashMap()), null, [], null, null)) + } + + def mapping = migration.mappingRepository.getBaseTemplateMapping(baseTemplateId) + mapping.name = draft.name ?: existing?.name + mapping.targetFolder = existing?.targetFolder + mapping.pages = new ArrayList(draft.pages.values()) + + mappings[baseTemplateId] = mapping + } + + Mapping.upsertBatched(migration.mappingRepository, mappings, "base template mappings", log) + migration.mappingRepository.applyAllBaseTemplateMappings() +} + +class BaseTemplateDraft { + String name + Map pages = new LinkedHashMap<>() +} diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AttachmentsImport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AttachmentsImport.groovy index c6f95b2f..6576cfe3 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AttachmentsImport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AttachmentsImport.groovy @@ -89,10 +89,6 @@ static void run(Migration migration, Path attachmentsFilePath) { } } - def batches = mappings.entrySet().collate(1000) - for (int i = 0; i < batches.size(); i++) { - log.info "Upserting mappings batch ${i + 1}/${batches.size()} (${batches[i].size()} items)" - migration.mappingRepository.upsertBatch(batches[i].collectEntries()) - } + Mapping.upsertBatched(migration.mappingRepository, mappings, "attachment mappings", log) migration.mappingRepository.applyAllAttachmentMappings() } diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DisplayRulesImport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DisplayRulesImport.groovy index 19605b24..43f321eb 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DisplayRulesImport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DisplayRulesImport.groovy @@ -84,10 +84,6 @@ static void run(Migration migration, File file) { } } - def batches = mappings.entrySet().collate(1000) - for (int i = 0; i < batches.size(); i++) { - log.info "Upserting mappings batch ${i + 1}/${batches.size()} (${batches[i].size()} items)" - migration.mappingRepository.upsertBatch(batches[i].collectEntries()) - } + Mapping.upsertBatched(migration.mappingRepository, mappings, "display rule mappings", log) migration.mappingRepository.applyAllDisplayRuleMappings() } diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DocumentObjectsImport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DocumentObjectsImport.groovy index 0c5fdad0..3c30cd55 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DocumentObjectsImport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DocumentObjectsImport.groovy @@ -94,10 +94,6 @@ static void run(Migration migration, Path documentObjFilePath) { } } - def batches = mappings.entrySet().collate(1000) - for (int i = 0; i < batches.size(); i++) { - log.info "Upserting mappings batch ${i + 1}/${batches.size()} (${batches[i].size()} items)" - migration.mappingRepository.upsertBatch(batches[i].collectEntries()) - } + Mapping.upsertBatched(migration.mappingRepository, mappings, "document object mappings", log) migration.mappingRepository.applyAllDocumentObjectMappings() } \ No newline at end of file diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/ImagesImport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/ImagesImport.groovy index fb041b6e..5375ac27 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/ImagesImport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/ImagesImport.groovy @@ -92,10 +92,6 @@ static void run(Migration migration, Path imagesFilePath) { } } - def batches = mappings.entrySet().collate(1000) - for (int i = 0; i < batches.size(); i++) { - log.info "Upserting mappings batch ${i + 1}/${batches.size()} (${batches[i].size()} items)" - migration.mappingRepository.upsertBatch(batches[i].collectEntries()) - } + Mapping.upsertBatched(migration.mappingRepository, mappings, "image mappings", log) migration.mappingRepository.applyAllImageMappings() } \ No newline at end of file diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/ParagraphStylesImport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/ParagraphStylesImport.groovy index ab34b00d..55c94395 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/ParagraphStylesImport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/ParagraphStylesImport.groovy @@ -58,11 +58,7 @@ static void run(Migration migration, Path path) { } } - def batches = mappings.entrySet().collate(1000) - for (int i = 0; i < batches.size(); i++) { - log.info "Upserting mappings batch ${i + 1}/${batches.size()} (${batches[i].size()} items)" - migration.mappingRepository.upsertBatch(batches[i].collectEntries()) - } + Mapping.upsertBatched(migration.mappingRepository, mappings, "paragraph style mappings", log) migration.mappingRepository.applyAllParagraphStyleMappings() } diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/TablesImport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/TablesImport.groovy index a8e5a758..9d3fabed 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/TablesImport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/TablesImport.groovy @@ -83,16 +83,11 @@ static void run(Migration migration, Path path) { tablesByDocObjId.computeIfAbsent(docObjId) { [] }.add(entry) } - def mappings = tablesByDocObjId.collectEntries { docObjId, tables -> + Map mappings = tablesByDocObjId.collectEntries { docObjId, tables -> [docObjId, new MappingItem.Table(null, tables)] } - def batches = mappings.entrySet().collate(1000) - for (int i = 0; i < batches.size(); i++) { - log.info("Upserting table mappings batch ${i + 1}/${batches.size()} (${batches[i].size()} items)") - migration.mappingRepository.upsertBatch(batches[i].collectEntries()) - } - + Mapping.upsertBatched(migration.mappingRepository, mappings, "table mappings", log) migration.mappingRepository.applyAllTableMappings { hasErrors = true } if (hasErrors) { diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/TextStylesImport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/TextStylesImport.groovy index 97cc09a6..bedcd828 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/TextStylesImport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/TextStylesImport.groovy @@ -56,11 +56,7 @@ static void run(Migration migration, Path path) { } } - def batches = mappings.entrySet().collate(1000) - for (int i = 0; i < batches.size(); i++) { - log.info "Upserting mappings batch ${i + 1}/${batches.size()} (${batches[i].size()} items)" - migration.mappingRepository.upsertBatch(batches[i].collectEntries()) - } + Mapping.upsertBatched(migration.mappingRepository, mappings, "text style mappings", log) migration.mappingRepository.applyAllTextStyleMappings() } diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/VariablesImport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/VariablesImport.groovy index c6da7832..94a9a0f4 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/VariablesImport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/VariablesImport.groovy @@ -80,11 +80,7 @@ static void run(Migration migration, Path path) { } } - def batches = mappings.entrySet().collate(1000) - for (int i = 0; i < batches.size(); i++) { - log.info "Upserting mappings batch ${i + 1}/${batches.size()} (${batches[i].size()} items)" - migration.mappingRepository.upsertBatch(batches[i].collectEntries()) - } + Mapping.upsertBatched(migration.mappingRepository, mappings, "variable mappings", log) migration.mappingRepository.applyAllVariableMappings() if (!languageVariableFound) { diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/util/Mapping.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/util/Mapping.groovy index 899df365..fe9921cd 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/util/Mapping.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/util/Mapping.groovy @@ -1,5 +1,9 @@ package com.quadient.migration.example.common.util +import com.quadient.migration.api.dto.migrationmodel.MappingItem +import com.quadient.migration.api.repository.MappingRepository +import org.slf4j.Logger + import java.nio.file.Path import java.nio.file.Paths @@ -91,3 +95,11 @@ static String normalizeHeader(String displayName) { } return s.trim() } + +static void upsertBatched(MappingRepository mappingRepository, Map mappings, String label, Logger log) { + def batches = mappings.entrySet().collate(1000) + for (int i = 0; i < batches.size(); i++) { + log.info "Upserting ${label} batch ${i + 1}/${batches.size()} (${batches[i].size()} items)" + mappingRepository.upsertBatch(batches[i].collectEntries()) + } +} diff --git a/migration-examples/src/test/groovy/AreasExportTest.groovy b/migration-examples/src/test/groovy/AreasExportTest.groovy index 69f2d736..f589766c 100644 --- a/migration-examples/src/test/groovy/AreasExportTest.groovy +++ b/migration-examples/src/test/groovy/AreasExportTest.groovy @@ -46,12 +46,12 @@ class AreasExportTest { AreasExport.run(migration, mappingFile) def expected = """\ - templateId,templateName (read-only),pageId,pageName (read-only),pageWidth (read-only),pageHeight (read-only),interactiveFlowName,flowToNextPage,x (read-only),y (read-only),width (read-only),height (read-only),contentPreview (read-only) - full tmpl,,full page,,,,test flow2,false,0mm,0mm,0mm,0mm, - full tmpl,,full page,,,,test flow3,true,0mm,0mm,0mm,0mm, - full tmpl,,full page,,,,,false,0mm,0mm,0mm,0mm, - full tmpl,,full page,,,,test flow5,false,0mm,0mm,0mm,0mm, - ,,unreferenced page,,,,test flow,true,0mm,0mm,0mm,0mm, + templateId,templateName (read-only),pageId,pageName (read-only),pageWidth (read-only),pageHeight (read-only),interactiveFlowName,flowToNextPage,x (read-only),y (read-only),width (read-only),height (read-only),type,targetId,contentPreview (read-only) + full tmpl,,full page,,,,test flow2,false,0mm,0mm,0mm,0mm,Standard,, + full tmpl,,full page,,,,test flow3,true,0mm,0mm,0mm,0mm,Standard,, + full tmpl,,full page,,,,,false,0mm,0mm,0mm,0mm,Standard,, + full tmpl,,full page,,,,test flow5,false,0mm,0mm,0mm,0mm,Standard,, + ,,unreferenced page,,,,test flow,true,0mm,0mm,0mm,0mm,Standard,, """.stripIndent() Assertions.assertEquals(expected, mappingFile.toFile().text.replaceAll("\\r\\n|\\r", "\n")) } @@ -69,10 +69,34 @@ class AreasExportTest { AreasExport.run(migration, mappingFile) def expected = """\ - templateId,templateName (read-only),pageId,pageName (read-only),pageWidth (read-only),pageHeight (read-only),interactiveFlowName,flowToNextPage,x (read-only),y (read-only),width (read-only),height (read-only),contentPreview (read-only) - tmpl with areas,,,,,,Address Content,false,0mm,0mm,0mm,0mm, - tmpl with areas,,,,,,,true,0mm,0mm,0mm,0mm, - tmpl with areas,,,,,,Footer,false,0mm,0mm,0mm,0mm, + templateId,templateName (read-only),pageId,pageName (read-only),pageWidth (read-only),pageHeight (read-only),interactiveFlowName,flowToNextPage,x (read-only),y (read-only),width (read-only),height (read-only),type,targetId,contentPreview (read-only) + tmpl with areas,,,,,,Address Content,false,0mm,0mm,0mm,0mm,Standard,, + tmpl with areas,,,,,,,true,0mm,0mm,0mm,0mm,Standard,, + tmpl with areas,,,,,,Footer,false,0mm,0mm,0mm,0mm,Standard,, + """.stripIndent() + Assertions.assertEquals(expected, mappingFile.toFile().text.replaceAll("\\r\\n|\\r", "\n")) + } + + @Test + void exportUsesPageOrTemplateBaseTemplateAsTargetId() { + Path mappingFile = Paths.get(dir.path, "testProject.csv") + when(migration.mappingRepository.getAreaMapping(any())).thenReturn(new MappingItem.Area(null, [:], [:])) + when((migration.documentObjectRepository as DocumentObjectRepository).list(any())).thenReturn([ + new DocumentObjectBuilder("tmpl with base", DocumentObjectType.Template) + .baseTemplateRef("G2") + .documentObjectRef("page with own base") + .build(), + new DocumentObjectBuilder("page with own base", DocumentObjectType.Page) + .baseTemplateRef("G1") + .content([createArea("test flow")]) + .build(), + ]) + + AreasExport.run(migration, mappingFile) + + def expected = """\ + templateId,templateName (read-only),pageId,pageName (read-only),pageWidth (read-only),pageHeight (read-only),interactiveFlowName,flowToNextPage,x (read-only),y (read-only),width (read-only),height (read-only),type,targetId,contentPreview (read-only) + tmpl with base,,page with own base,,,,test flow,false,0mm,0mm,0mm,0mm,Standard,\$G1, """.stripIndent() Assertions.assertEquals(expected, mappingFile.toFile().text.replaceAll("\\r\\n|\\r", "\n")) } @@ -100,8 +124,8 @@ class AreasExportTest { AreasExport.run(migration, mappingFile) def expected = """\ - templateId,templateName (read-only),pageId,pageName (read-only),pageWidth (read-only),pageHeight (read-only),interactiveFlowName,flowToNextPage,x (read-only),y (read-only),width (read-only),height (read-only),contentPreview (read-only) - ,,page with preview,,,,test flow,false,0mm,0mm,0mm,0mm,docRef: Block One;imageRef: Image One;docRef: Block Two;(+2 more) + templateId,templateName (read-only),pageId,pageName (read-only),pageWidth (read-only),pageHeight (read-only),interactiveFlowName,flowToNextPage,x (read-only),y (read-only),width (read-only),height (read-only),type,targetId,contentPreview (read-only) + ,,page with preview,,,,test flow,false,0mm,0mm,0mm,0mm,Standard,,docRef: Block One;imageRef: Image One;docRef: Block Two;(+2 more) """.stripIndent() Assertions.assertEquals(expected, mappingFile.toFile().text.replaceAll("\\r\\n|\\r", "\n")) } diff --git a/migration-examples/src/test/groovy/AreasImportTest.groovy b/migration-examples/src/test/groovy/AreasImportTest.groovy index 2a494468..1e49761b 100644 --- a/migration-examples/src/test/groovy/AreasImportTest.groovy +++ b/migration-examples/src/test/groovy/AreasImportTest.groovy @@ -3,12 +3,17 @@ import com.quadient.migration.api.dto.migrationmodel.* import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder import com.quadient.migration.api.dto.migrationmodel.builder.documentcontent.AreaBuilder import com.quadient.migration.example.common.mapping.AreasImport +import com.quadient.migration.shared.BaseTemplateArea +import com.quadient.migration.shared.BaseTemplatePage import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.Position import com.quadient.migration.shared.Size +import com.quadient.migration.shared.SkipOptions +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir +import org.mockito.ArgumentCaptor import java.nio.file.Path import java.nio.file.Paths @@ -91,6 +96,92 @@ class AreasImportTest { verify(migration.mappingRepository).applyAllAreaMappings() } + @Test + void importSetsPageBaseTemplateFromTargetIdOnStandardRows() { + Path mappingFile = Paths.get(dir.path, "testProject.csv") + + when(migration.mappingRepository.getAreaMapping("page1")).thenReturn(new MappingItem.Area(null, [:], [:])) + when(migration.mappingRepository.getDocumentObjectMapping("page1")).thenReturn( + new MappingItem.DocumentObject(null, null, null, null, null, null, null) + ) + givenPageExists("page1", ["flow1", "flow2"], [false, false]) + + def input = """\ + templateId,templateName,pageId,pageName,interactiveFlowName,flowToNextPage,x,y,width,height,type,targetId,contentPreview + tmpl1,,page1,,flow1,false,0.0mm,0.0mm,0.0mm,0.0mm,Standard,\$G1, + tmpl1,,page1,,flow2,false,0.0mm,0.0mm,0.0mm,0.0mm,Standard,\$G1, + """.stripIndent() + mappingFile.toFile().write(input) + + AreasImport.run(migration, mappingFile) + + verify(migration.mappingRepository).upsertBatch([ + "page1": new MappingItem.DocumentObject(null, false, new BaseTemplateRef("G1"), null, null, null, new SkipOptions(false, null, null)) + ]) + verify(migration.mappingRepository).applyAllDocumentObjectMappings() + } + + @Test + void importCreatesNewBaseTemplateFromBaseRows() { + Path mappingFile = Paths.get(dir.path, "testProject.csv") + + when(migration.mappingRepository.getAreaMapping("page1")).thenReturn(new MappingItem.Area(null, [:], [:])) + when(migration.mappingRepository.getDocumentObjectMapping("page1")).thenReturn( + new MappingItem.DocumentObject(null, null, null, null, null, null, null) + ) + givenPageExists("page1", ["flow1"], [false]) + when(migration.baseTemplateRepository.find("G1")).thenReturn(null) + when(migration.mappingRepository.getBaseTemplateMapping("G1")).thenReturn(new MappingItem.BaseTemplate(null, null, [])) + + def input = """\ + templateId,templateName,pageId,pageName,pageWidth,pageHeight,interactiveFlowName,flowToNextPage,x,y,width,height,type,targetId,contentPreview + tmpl1,,page1,,,,flow1,false,0.0mm,0.0mm,0.0mm,0.0mm,Standard,\$G1, + G1,Base template 1,G1-P1,Page group 1,210mm,297mm,G1-P1.Area1,false,1cm,1cm,190mm,20mm,Base,, + G1,Base template 1,G1-P1,Page group 1,210mm,297mm,G1-P1.Area2,true,1cm,30mm,190mm,50mm,Base,, + """.stripIndent() + mappingFile.toFile().write(input) + + AreasImport.run(migration, mappingFile) + + def baseTemplateCaptor = ArgumentCaptor.forClass(BaseTemplate.class) + verify(migration.baseTemplateRepository).upsert(baseTemplateCaptor.capture()) + Assertions.assertEquals("G1", baseTemplateCaptor.value.id) + + def created = new BaseTemplatePage("Page group 1", Size.ofMillimeters(210), Size.ofMillimeters(297), [ + new BaseTemplateArea("G1-P1.Area1", new Position(Size.ofCentimeters(1), Size.ofCentimeters(1), Size.ofMillimeters(190), Size.ofMillimeters(20)), false), + new BaseTemplateArea("G1-P1.Area2", new Position(Size.ofCentimeters(1), Size.ofMillimeters(30), Size.ofMillimeters(190), Size.ofMillimeters(50)), true), + ]) + verify(migration.mappingRepository).upsertBatch([ + "G1": new MappingItem.BaseTemplate("Base template 1", null, [created]) + ]) + verify(migration.mappingRepository).applyAllBaseTemplateMappings() + } + + @Test + void importUpdatesExistingBaseTemplateMappingButKeepsItsOtherFields() { + Path mappingFile = Paths.get(dir.path, "testProject.csv") + + def existing = new BaseTemplate("G1", "Old name", ["origin.wfd"], new CustomFieldMap(new HashMap()), "target/folder", [], null, null) + when(migration.baseTemplateRepository.find("G1")).thenReturn(existing) + when(migration.mappingRepository.getBaseTemplateMapping("G1")).thenReturn(new MappingItem.BaseTemplate(null, null, [])) + + def input = """\ + templateId,templateName,pageId,pageName,pageWidth,pageHeight,interactiveFlowName,flowToNextPage,x,y,width,height,type,targetId,contentPreview + G1,,G1-P1,Page group 1,210mm,297mm,G1-P1.Area1,false,1cm,1cm,190mm,20mm,Base,, + """.stripIndent() + mappingFile.toFile().write(input) + + AreasImport.run(migration, mappingFile) + + def created = new BaseTemplatePage("Page group 1", Size.ofMillimeters(210), Size.ofMillimeters(297), [ + new BaseTemplateArea("G1-P1.Area1", new Position(Size.ofCentimeters(1), Size.ofCentimeters(1), Size.ofMillimeters(190), Size.ofMillimeters(20)), false), + ]) + verify(migration.mappingRepository).upsertBatch([ + "G1": new MappingItem.BaseTemplate("Old name", "target/folder", [created]) + ]) + verify(migration.mappingRepository).applyAllBaseTemplateMappings() + } + static Area createArea(String flowName, boolean flowToNextPage) { def areaBuilder = new AreaBuilder() .position(new Position(Size.ofMillimeters(0), Size.ofMillimeters(0), Size.ofMillimeters(0), Size.ofMillimeters(0))) diff --git a/migration-examples/src/test/groovy/Utils.groovy b/migration-examples/src/test/groovy/Utils.groovy index aada7c8d..7a0bf2f7 100644 --- a/migration-examples/src/test/groovy/Utils.groovy +++ b/migration-examples/src/test/groovy/Utils.groovy @@ -1,6 +1,7 @@ import com.quadient.migration.api.InspireOutput import com.quadient.migration.api.Migration import com.quadient.migration.api.ProjectConfig +import com.quadient.migration.api.repository.BaseTemplateRepository import com.quadient.migration.api.repository.DisplayRuleRepository import com.quadient.migration.api.repository.DocumentObjectRepository import com.quadient.migration.api.repository.AttachmentRepository @@ -34,6 +35,7 @@ static Migration mockMigration() { def textStyleRepo = mock(TextStyleRepository.class) def paraStyleRepo = mock(ParagraphStyleRepository.class) def displayRuleRepo = mock(DisplayRuleRepository.class) + def baseTemplateRepo = mock(BaseTemplateRepository.class) when(migration.getParagraphStyleRepository()).thenReturn(paraStyleRepo) when(migration.getTextStyleRepository()).thenReturn(textStyleRepo) @@ -45,6 +47,7 @@ static Migration mockMigration() { when(migration.getVariableStructureRepository()).thenReturn(structureRepo) when(migration.getMappingRepository()).thenReturn(mappingRepo) when(migration.getDisplayRuleRepository()).thenReturn(displayRuleRepo) + when(migration.getBaseTemplateRepository()).thenReturn(baseTemplateRepo) def previewProvider = new PreviewProvider(docObjectRepo, imageRepo, attachmentRepo, varRepo) when(migration.getPreviewProvider()).thenReturn(previewProvider) From 836969f060c8e021ead1c0aa97f9b6b59d553690 Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Mon, 20 Jul 2026 15:02:10 +0200 Subject: [PATCH 5/5] MIG-562 Base Template definition and deployment - Initial support - template also references base template based on area mapping --- .../example/common/mapping/AreasImport.groovy | 11 ++++++++--- .../src/test/groovy/AreasImportTest.groovy | 9 ++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AreasImport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AreasImport.groovy index 7360cb62..ce43785b 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AreasImport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/AreasImport.groovy @@ -79,8 +79,13 @@ static void run(Migration migration, Path path) { areaMapping.flowToNextPage[areaIndex] = flowToNextPage ?: false def targetId = Csv.deserialize(values.get("targetId"), String.class) - if (targetId && !docObjectsToTargetIds.containsKey(documentObjectId)) { - docObjectsToTargetIds[documentObjectId] = targetId + if (targetId) { + if (pageId && !docObjectsToTargetIds.containsKey(pageId)) { + docObjectsToTargetIds[pageId] = targetId + } + if (templateId && !docObjectsToTargetIds.containsKey(templateId)) { + docObjectsToTargetIds[templateId] = targetId + } } areaIndex++ @@ -148,7 +153,7 @@ private static void applyDocumentObjectTargetIdMappings(Migration migration, Map mappings[documentObjectId] = mapping } - Mapping.upsertBatched(migration.mappingRepository, mappings, "base template reference mappings", log) + Mapping.upsertBatched(migration.mappingRepository, mappings, "document object base template ref mappings", log) migration.mappingRepository.applyAllDocumentObjectMappings() } diff --git a/migration-examples/src/test/groovy/AreasImportTest.groovy b/migration-examples/src/test/groovy/AreasImportTest.groovy index 1e49761b..fc721eb2 100644 --- a/migration-examples/src/test/groovy/AreasImportTest.groovy +++ b/migration-examples/src/test/groovy/AreasImportTest.groovy @@ -104,6 +104,9 @@ class AreasImportTest { when(migration.mappingRepository.getDocumentObjectMapping("page1")).thenReturn( new MappingItem.DocumentObject(null, null, null, null, null, null, null) ) + when(migration.mappingRepository.getDocumentObjectMapping("tmpl1")).thenReturn( + new MappingItem.DocumentObject(null, null, null, null, null, null, null) + ) givenPageExists("page1", ["flow1", "flow2"], [false, false]) def input = """\ @@ -116,7 +119,8 @@ class AreasImportTest { AreasImport.run(migration, mappingFile) verify(migration.mappingRepository).upsertBatch([ - "page1": new MappingItem.DocumentObject(null, false, new BaseTemplateRef("G1"), null, null, null, new SkipOptions(false, null, null)) + "page1": new MappingItem.DocumentObject(null, false, new BaseTemplateRef("G1"), null, null, null, new SkipOptions(false, null, null)), + "tmpl1": new MappingItem.DocumentObject(null, null, new BaseTemplateRef("G1"), null, null, null, null) ]) verify(migration.mappingRepository).applyAllDocumentObjectMappings() } @@ -129,6 +133,9 @@ class AreasImportTest { when(migration.mappingRepository.getDocumentObjectMapping("page1")).thenReturn( new MappingItem.DocumentObject(null, null, null, null, null, null, null) ) + when(migration.mappingRepository.getDocumentObjectMapping("tmpl1")).thenReturn( + new MappingItem.DocumentObject(null, null, null, null, null, null, null) + ) givenPageExists("page1", ["flow1"], [false]) when(migration.baseTemplateRepository.find("G1")).thenReturn(null) when(migration.mappingRepository.getBaseTemplateMapping("G1")).thenReturn(new MappingItem.BaseTemplate(null, null, []))