diff --git a/CHANGELOG.md b/CHANGELOG.md index 91bc4adb..31a2f09f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) ### Added +- DeployBaseTemplates task that deploys base templates with pages, areas, interactive flows and other options. + ### Changed ### Fixed diff --git a/migration-examples/hierarchy/index.html b/migration-examples/hierarchy/index.html index be962983..0abee186 100644 --- a/migration-examples/hierarchy/index.html +++ b/migration-examples/hierarchy/index.html @@ -183,6 +183,10 @@ color: #40a02b; } + .label.base_template { + color: #8839ef; + } + .label mark { background: var(--hl); padding: 0 2px; @@ -247,6 +251,8 @@ + +
@@ -289,6 +295,7 @@ const variableStructuresChkbox = /** @type {HTMLInputElement} */ (document.getElementById("variableStructures")); const attachmentsChkbox = /** @type {HTMLInputElement} */ (document.getElementById("attachments")); const imagesChkbox = /** @type {HTMLInputElement} */ (document.getElementById("images")); + const baseTemplatesChkbox = /** @type {HTMLInputElement} */ (document.getElementById("baseTemplates")); function refresh() { indexes = buildIndexes(data); @@ -307,6 +314,7 @@ variableStructuresChkbox.addEventListener("change", refresh); attachmentsChkbox.addEventListener("change", refresh); imagesChkbox.addEventListener("change", refresh); + baseTemplatesChkbox.addEventListener("change", refresh); treeEl.addEventListener("click", onTreeClick); @@ -489,6 +497,9 @@ case 'VARIABLE_STRUCTURE': { return variableStructuresChkbox.checked; }; + case 'BASE_TEMPLATE': { + return baseTemplatesChkbox.checked; + }; } } @@ -916,6 +927,7 @@ addBucket(root.images, "IMAGE"); addBucket(root.variables, "VARIABLE"); addBucket(root.variableStructures, "VARIABLE_STRUCTURE"); + addBucket(root.baseTemplates, "BASE_TEMPLATE"); return { nodeByKey, childrenByKey, parentsByKey }; } @@ -967,6 +979,9 @@ case 'VARIABLE_STRUCTURE': { return root.variableStructures[ref.id]; }; + case 'BASE_TEMPLATE': { + return root.baseTemplates[ref.id]; + }; } } @@ -1012,6 +1027,10 @@ label = "VS" break; }; + case 'BASE_TEMPLATE': { + label = "BT" + break; + }; } let eid = id; @@ -1040,6 +1059,7 @@ * @property {Object.} images * @property {Object.} variables * @property {Object.} variableStructures + * @property {Object.} baseTemplates */ /** @@ -1073,7 +1093,8 @@ * | 'ATTACHMENT' * | 'IMAGE' * | 'VARIABLE' - * | 'VARIABLE_STRUCTURE'} ChildType + * | 'VARIABLE_STRUCTURE' + * | 'BASE_TEMPLATE'} ChildType */ diff --git a/migration-examples/layout/index.html b/migration-examples/layout/index.html index 85e2602d..d1292f6e 100644 --- a/migration-examples/layout/index.html +++ b/migration-examples/layout/index.html @@ -929,6 +929,10 @@ const baseTemplateId = `bt-${templateGroupIndex + 1}`; const baseTemplateName = `${templateGroup.templateNames[0]}BaseTemplate`; + const usedFlowNames = new Set(); + /** @type {Map} */ + const firstPageGroupIndexForName = new Map(); + templateGroup.pageGroups.forEach((pageGroup, pageGroupIndex) => { const basePageId = `page-${pageGroupIndex + 1}`; const basePageName = `Page ${pageGroupIndex + 1}`; @@ -941,14 +945,25 @@ /** @type {string[]} */ const flowNamesByAreaGroupIndex = []; - const usedFlowNames = new Set(); representativeDrafts.forEach((draft, areaGroupIndex) => { const flowToNextPage = draft.areaIndices.some(ai => representativePage.areas[ai]?.flowToNextPage); - let flowName = draft.name; - if (usedFlowNames.has(flowName)) { - flowName = `${flowName} (Area ${areaGroupIndex + 1})`; + const baseFlowName = `${draft.name}Flow`; + let flowName = baseFlowName; + if (usedFlowNames.has(baseFlowName)) { + flowName = firstPageGroupIndexForName.get(baseFlowName) === pageGroupIndex + ? `${baseFlowName} (Area ${areaGroupIndex + 1})` + : `${baseFlowName} (${basePageName})`; + } else { + firstPageGroupIndexForName.set(baseFlowName, pageGroupIndex); + } + let uniqueFlowName = flowName; + let dedupCounter = 2; + while (usedFlowNames.has(uniqueFlowName)) { + uniqueFlowName = `${flowName} (${dedupCounter})`; + dedupCounter++; } + flowName = uniqueFlowName; usedFlowNames.add(flowName); flowNamesByAreaGroupIndex.push(flowName); diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/DeployBaseTemplates.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/DeployBaseTemplates.groovy new file mode 100644 index 00000000..0f859577 --- /dev/null +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/DeployBaseTemplates.groovy @@ -0,0 +1,38 @@ +//! --- +//! displayName: Deploy Base Templates +//! category: Deployment +//! description: Deploys all base templates +//! --- +package com.quadient.migration.example.common + +import groovy.transform.Field +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import static com.quadient.migration.example.common.util.InitMigration.initMigration + +def migration = initMigration(this.binding) +def start = System.currentTimeMillis() + +deploymentResult = migration.deployClient.deployBaseTemplates() +@Field static Logger log = LoggerFactory.getLogger(this.class.name) + +if (!deploymentResult.deployed.empty) { + for (def item : deploymentResult.deployed) { + log.info "Deployed ${item.type.toString()} '${item.id}' to '${item.targetPath}'" + } +} + +if (!deploymentResult.warnings.empty) { + for (def item : deploymentResult.warnings) { + log.warn "Item '${item.id}' deployed with warning: ${item.message}" + } +} + +if (!deploymentResult.errors.empty) { + for (def item : deploymentResult.errors) { + log.error "Item '${item.id}' failed to deploy with error: ${item.message}" + } +} +log.info "Deployment finished. Deployed ${deploymentResult.deployed.size()} items with ${deploymentResult.warnings.size()} warnings and ${deploymentResult.errors.size()} errors" +log.info "Deployment took ${System.currentTimeMillis() - start} ms" 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 3c30cd55..410fd515 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 @@ -70,9 +70,7 @@ static void run(Migration migration, Path documentObjFilePath) { Mapping.mapProp(existingMapping, existingDocObject, "type", newType) def varStructureRef = Csv.deserialize(values.get("variableStructureId"), String.class) - if (varStructureRef != existingDocObject.variableStructureRef?.id && varStructureRef != existingMapping.variableStructureRef) { - existingMapping.variableStructureRef = varStructureRef - } + existingMapping.variableStructureRef = varStructureRef def csvStatus = values.get("status") if (status != null && csvStatus == "Active" && status.class.simpleName != "Active") { diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/LayoutExport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/LayoutExport.groovy index 12289238..1a139998 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/LayoutExport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/LayoutExport.groovy @@ -41,20 +41,20 @@ static void run(Migration migration, Path path) { areasFile.withWriter { writer -> def headers = [ Mapping.displayHeader("templateId", false), - Mapping.displayHeader("templateName", true), + Mapping.displayHeader("templateName", false), Mapping.displayHeader("pageId", false), - Mapping.displayHeader("pageName", true), + Mapping.displayHeader("pageName", false), Mapping.displayHeader("type", false), Mapping.displayHeader("baseTemplateTargetId", false), Mapping.displayHeader("interactiveFlowName", false), Mapping.displayHeader("flowToNextPage", false), - Mapping.displayHeader("areaIndex", true), - Mapping.displayHeader("x", true), - Mapping.displayHeader("y", true), - Mapping.displayHeader("width", true), - Mapping.displayHeader("height", true), - Mapping.displayHeader("pageWidth", true), - Mapping.displayHeader("pageHeight", true), + Mapping.displayHeader("areaIndex", false), + Mapping.displayHeader("x", false), + Mapping.displayHeader("y", false), + Mapping.displayHeader("width", false), + Mapping.displayHeader("height", false), + Mapping.displayHeader("pageWidth", false), + Mapping.displayHeader("pageHeight", false), Mapping.displayHeader("contentPreview", true), ] writer.writeLine(headers.join(",")) diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/report/Hierarchy.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/report/Hierarchy.groovy index 46798a0c..f42b055b 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/report/Hierarchy.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/report/Hierarchy.groovy @@ -27,6 +27,7 @@ def attachments = migration.attachmentRepository.listAll() def images = migration.imageRepository.listAll() def variables = migration.variableRepository.listAll() def variableStructures = migration.variableStructureRepository.listAll() +def baseTemplates = migration.baseTemplateRepository.listAll() def root = new Root() @@ -87,6 +88,12 @@ for (variableStructure in variableStructures) { root.variableStructures[node.id] = node } +for (baseTemplate in baseTemplates) { + def node = new Leaf(id: baseTemplate.id, name: baseTemplate.name, type: ChildType.BASE_TEMPLATE) + + root.baseTemplates[node.id] = node +} + // Collect parents for (node in root.documentObjects.values()) { def parentNode = new Reference(id: node.id, type: ChildType.DOCUMENT_OBJECT) @@ -121,6 +128,7 @@ static void collectParents(Root root, Node node, Reference parentNode) { case ChildType.IMAGE -> root.images case ChildType.VARIABLE -> root.variables case ChildType.VARIABLE_STRUCTURE -> root.variableStructures + case ChildType.BASE_TEMPLATE -> root.baseTemplates default -> throw new IllegalStateException("Unknown parent type: ${child.type}") } @@ -139,6 +147,7 @@ static void collectChildren(Node node, Set refs) { case ImageRef -> ChildType.IMAGE case VariableRef -> ChildType.VARIABLE case VariableStructureRef -> ChildType.VARIABLE_STRUCTURE + case BaseTemplateRef -> ChildType.BASE_TEMPLATE default -> throw new IllegalStateException("Unknown reference type: ${child.class}") } @@ -155,6 +164,7 @@ class Root { Map images = [:] Map variables = [:] Map variableStructures = [:] + Map baseTemplates = [:] } class Node { @@ -186,4 +196,5 @@ enum ChildType { IMAGE, VARIABLE, VARIABLE_STRUCTURE, + BASE_TEMPLATE, } \ No newline at end of file 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 33065742..d943db9d 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 @@ -335,7 +335,8 @@ def mainFlow = new DocumentObjectBuilder("mainFlow", DocumentObjectType.Block) .build() migration.documentObjectRepository.upsert(mainFlow) -def page = new DocumentObjectBuilder("page1", DocumentObjectType.Page) +def page = new DocumentObjectBuilder("page", DocumentObjectType.Page) + .internal(true) .options(new PageOptions(Size.ofMillimeters(210), Size.ofMillimeters(297))) .area { it.position { 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 a96dbbc4..d12d764a 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 @@ -119,7 +119,6 @@ def displayAddressRule = new DisplayRuleBuilder("displayAddressRule") .internal(false) .subject("External display rule") .metadata("key") { it.string("value") } - .variableStructureRef(variableStructure) .group { it.operator(GroupOp.Or) it.comparison { it.variable(nameVariable).notEquals().value("") } @@ -403,7 +402,6 @@ def address = new DocumentObjectBuilder("address", DocumentObjectType.Block) .paragraph { it.styleRef(compactParagraphStyle).text { it.styleRef(normalStyle).variableRef(addressVariable) } } .paragraph { it.styleRef(compactParagraphStyle).text { it.styleRef(normalStyle).variableRef(cityVariable) } } .paragraph { it.styleRef(compactParagraphStyle).text { it.styleRef(normalStyle).variableRef(stateVariable) } } - .variableStructureRef(variableStructure) .build() // Footer of the document containing a signature. @@ -411,7 +409,6 @@ 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 Migration") } } .paragraph { it.styleRef(compactParagraphStyle).text { it.styleRef(normalStyle).string("CEO of Lorem ipsum") } } - .variableStructureRef(variableStructure) .build() // Sample paragraph containing a heading using headingStyle style, @@ -490,7 +487,6 @@ def conditionalParagraph = new DocumentObjectBuilder("conditionalParagraph", Doc it.styleRef(normalStyle).string("Integer quis quam semper, accumsan neque at, pellentesque diam. Etiam in blandit dolor. Maecenas sit amet interdum augue, vel pellentesque erat. Suspendisse ut sem in justo rhoncus placerat vitae ut lacus. Etiam consequat bibendum justo ut posuere. Donec aliquam posuere nibh, vehicula pulvinar lectus dictum et. Nullam rhoncus ultrices ipsum et consectetur. Nam tincidunt id purus ac viverra. ") } } - .variableStructureRef(variableStructure) .build() def firstMatchBlock = new DocumentObjectBuilder("firstMatch", DocumentObjectType.Block) @@ -534,7 +530,6 @@ def snippet = new SnippetBuilder("snippet") .simple() .string("Lorem ipsum: ") .variableRef(nameVariable) - .variableStructureRef(variableStructure) .build() // A simple first match snippet example @@ -551,7 +546,6 @@ def fmSnippet = new SnippetBuilder("firstMatchSnippet") } .defaultString("For more information visit ") } - .variableStructureRef(variableStructure) .build() @@ -574,6 +568,7 @@ def separator = new ShapeBuilder() def paragraph1TopMargin = topMargin + Size.ofMillimeters(25) def signatureTopMargin = pageHeight - Size.ofCentimeters(3) def page = new DocumentObjectBuilder("page1", DocumentObjectType.Page) + .internal(true) .options(new PageOptions(pageWidth, pageHeight)) .shape(separator) .area { @@ -649,7 +644,6 @@ def page = new DocumentObjectBuilder("page1", DocumentObjectType.Page) .attachmentRef(exampleAttachment) .flowToNextPage(true) } - .variableStructureRef(variableStructure) .build() def sms = new SmsObjectBuilder("sms") @@ -720,15 +714,10 @@ def email = new EmailObjectBuilder("email") } .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) + .documentObjectRef(sms) + .documentObjectRef(email) .subject("Document example template") .pdfMetadata { it.author(new VariableRef(nameVariable.id)) @@ -741,7 +730,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, sms, email, templateEmailSms]) { +for (item in [address, signature, paragraph1, paragraph2, conditionalParagraph, page, template, firstMatchBlock, selectByLanguageBlock, jobListBlock, snippet, fmSnippet, sms, email]) { migration.documentObjectRepository.upsert(item) } for (item in [headingStyle, normalStyle]) { diff --git a/migration-examples/src/test/groovy/DocumentObjectsMappingImportTest.groovy b/migration-examples/src/test/groovy/DocumentObjectsMappingImportTest.groovy index c460150d..b3b192fc 100644 --- a/migration-examples/src/test/groovy/DocumentObjectsMappingImportTest.groovy +++ b/migration-examples/src/test/groovy/DocumentObjectsMappingImportTest.groovy @@ -65,6 +65,26 @@ class DocumentObjectsMappingImportTest { verify(migration.mappingRepository, times(1)).applyAllDocumentObjectMappings() } + @Test + void clearsVariableStructureRefWhenCsvValueIsBlank() { + def migration = Utils.mockMigration() + Path mappingFile = Paths.get(dir.path, "testProject-variables.csv") + def input = """\ + id,name,type,internal,originLocation,baseTemplate,targetFolder,variableStructureId,status + cleared,,Block,false,[],,,,Active + """.stripIndent() + mappingFile.toFile().write(input) + givenExistingDocumentObject(migration, "cleared", null, false, null, null, null, "previousVarStructure") + givenExistingDocumentObjectMapping(migration, "cleared", null, null, null, null, null, "previousVarStructure") + + DocumentObjectsImport.run(migration, mappingFile) + + verify(migration.mappingRepository, times(1)).upsertBatch([ + "cleared": new MappingItem.DocumentObject(null, false, null, null, DocumentObjectType.Block, null, new SkipOptions(false, null, null)) + ]) + verify(migration.mappingRepository, times(1)).applyAllDocumentObjectMappings() + } + static void givenExistingDocumentObject(Migration mig, String id, String name, Boolean internal, String baseTemplate, String targetFolder, DocumentObjectType type, String varStructureRef) { def builder = new DocumentObjectBuilder(id, type ?: DocumentObjectType.Block) if (name != null) { diff --git a/migration-examples/src/test/groovy/LayoutExportTest.groovy b/migration-examples/src/test/groovy/LayoutExportTest.groovy index 04a86599..8967af04 100644 --- a/migration-examples/src/test/groovy/LayoutExportTest.groovy +++ b/migration-examples/src/test/groovy/LayoutExportTest.groovy @@ -48,7 +48,7 @@ class LayoutExportTest { LayoutExport.run(migration, mappingFile) def expected = """\ - templateId,templateName (read-only),pageId,pageName (read-only),type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex (read-only),x (read-only),y (read-only),width (read-only),height (read-only),pageWidth (read-only),pageHeight (read-only),contentPreview (read-only) + templateId,templateName,pageId,pageName,type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex,x,y,width,height,pageWidth,pageHeight,contentPreview (read-only) full tmpl,,full page,,Standard,,test flow2,false,0,0mm,0mm,0mm,0mm,,, full tmpl,,full page,,Standard,,test flow3,true,1,0mm,0mm,0mm,0mm,,, full tmpl,,full page,,Standard,,,false,2,0mm,0mm,0mm,0mm,,, @@ -71,7 +71,7 @@ class LayoutExportTest { LayoutExport.run(migration, mappingFile) def expected = """\ - templateId,templateName (read-only),pageId,pageName (read-only),type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex (read-only),x (read-only),y (read-only),width (read-only),height (read-only),pageWidth (read-only),pageHeight (read-only),contentPreview (read-only) + templateId,templateName,pageId,pageName,type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex,x,y,width,height,pageWidth,pageHeight,contentPreview (read-only) tmpl with areas,,,,Standard,,Address Content,false,0,0mm,0mm,0mm,0mm,,, tmpl with areas,,,,Standard,,,true,1,0mm,0mm,0mm,0mm,,, tmpl with areas,,,,Standard,,Footer,false,2,0mm,0mm,0mm,0mm,,, @@ -97,7 +97,7 @@ class LayoutExportTest { LayoutExport.run(migration, mappingFile) def expected = """\ - templateId,templateName (read-only),pageId,pageName (read-only),type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex (read-only),x (read-only),y (read-only),width (read-only),height (read-only),pageWidth (read-only),pageHeight (read-only),contentPreview (read-only) + templateId,templateName,pageId,pageName,type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex,x,y,width,height,pageWidth,pageHeight,contentPreview (read-only) tmpl with base,,page with own base,,Standard,\$G1,test flow,false,0,0mm,0mm,0mm,0mm,,, """.stripIndent() Assertions.assertEquals(expected, mappingFile.toFile().text.replaceAll("\\r\\n|\\r", "\n")) @@ -117,13 +117,13 @@ class LayoutExportTest { new BaseTemplatePage("Page 2", Size.ofMillimeters(210), Size.ofMillimeters(99), [ new BaseTemplateArea("Area 1", new Position(Size.ofMillimeters(0), Size.ofMillimeters(0), Size.ofMillimeters(210), Size.ofMillimeters(99)), false), ]), - ], null, null) + ], null, null, null) when(migration.baseTemplateRepository.listAll()).thenReturn([baseTemplate]) LayoutExport.run(migration, mappingFile) def expected = """\ - templateId,templateName (read-only),pageId,pageName (read-only),type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex (read-only),x (read-only),y (read-only),width (read-only),height (read-only),pageWidth (read-only),pageHeight (read-only),contentPreview (read-only) + templateId,templateName,pageId,pageName,type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex,x,y,width,height,pageWidth,pageHeight,contentPreview (read-only) bt-1,Base template 1,page-1,Page 1,Base,,address,false,0,1cm,1cm,190mm,20mm,210mm,297mm, bt-1,Base template 1,page-1,Page 1,Base,,Area 2,true,1,1cm,30mm,190mm,50mm,210mm,297mm, bt-1,Base template 1,page-2,Page 2,Base,,Area 1,false,0,0mm,0mm,210mm,99mm,210mm,99mm, @@ -154,7 +154,7 @@ class LayoutExportTest { LayoutExport.run(migration, mappingFile) def expected = """\ - templateId,templateName (read-only),pageId,pageName (read-only),type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex (read-only),x (read-only),y (read-only),width (read-only),height (read-only),pageWidth (read-only),pageHeight (read-only),contentPreview (read-only) + templateId,templateName,pageId,pageName,type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex,x,y,width,height,pageWidth,pageHeight,contentPreview (read-only) ,,page with preview,,Standard,,test flow,false,0,0mm,0mm,0mm,0mm,,,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/LayoutImportTest.groovy b/migration-examples/src/test/groovy/LayoutImportTest.groovy index bf8f38b7..51b326d2 100644 --- a/migration-examples/src/test/groovy/LayoutImportTest.groovy +++ b/migration-examples/src/test/groovy/LayoutImportTest.groovy @@ -229,7 +229,7 @@ class LayoutImportTest { 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) + def existing = new BaseTemplate("G1", "Old name", ["origin.wfd"], new CustomFieldMap(new HashMap()), "target/folder", [], null, null, null) when(migration.baseTemplateRepository.find("G1")).thenReturn(existing) when(migration.mappingRepository.getBaseTemplateMapping("G1")).thenReturn(new MappingItem.BaseTemplate(null, null, [])) 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 40841f58..bb1f760f 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 @@ -26,8 +26,11 @@ import com.quadient.migration.service.PreviewProvider import com.quadient.migration.service.ResourcePathProvider import com.quadient.migration.service.deploy.utility.ConflictDetectorImpl import com.quadient.migration.service.deploy.utility.DeployOrderImpl +import com.quadient.migration.service.deploy.utility.RefInheritanceServiceImpl import com.quadient.migration.service.deploy.utility.ProgressReporterImpl import com.quadient.migration.service.inspirebuilder.InspireDocumentObjectBuilder +import com.quadient.migration.service.inspirebuilder.InspireBaseTemplateBuilder +import com.quadient.migration.service.inspirebuilder.InspireVariableStructureBuilder import com.quadient.migration.service.ipsclient.IpsService import com.quadient.migration.service.ipsclient.Version import com.quadient.migration.service.ipsclient.display @@ -110,11 +113,14 @@ class Migration(val config: MigConfig, val projectConfig: ProjectConfig) { single() single() single() + single() single() single() single() single() + single() + single() } private val koinApp: KoinApplication = koinApplication { 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 index e2c9cc29..432e11f3 100644 --- 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 @@ -10,6 +10,9 @@ data class BaseTemplate( override var customFields: CustomFieldMap, var targetFolder: String? = null, var pages: List = emptyList(), + var variableStructureRef: VariableStructureRef? = null, override var created: Instant? = null, override var lastUpdated: Instant? = null, -) : MigrationObject +) : MigrationObject, RefValidatable { + override fun collectRefs(): Set = setOfNotNull(variableStructureRef) +} diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/BaseTemplateBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/BaseTemplateBuilder.kt index 42501644..e6659971 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/BaseTemplateBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/BaseTemplateBuilder.kt @@ -1,8 +1,10 @@ package com.quadient.migration.api.dto.migrationmodel.builder import com.quadient.migration.api.dto.migrationmodel.BaseTemplate +import com.quadient.migration.api.dto.migrationmodel.VariableStructureRef import com.quadient.migration.api.dto.migrationmodel.builder.components.HasPosition 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.BaseTemplateArea import com.quadient.migration.shared.BaseTemplatePage import com.quadient.migration.shared.Position @@ -13,8 +15,10 @@ annotation class BaseTemplateBuilderDsl @BaseTemplateBuilderDsl class BaseTemplateBuilder(id: String) : DtoBuilderBase(id), - HasTargetFolder { + HasTargetFolder, + HasVariableStructureRef { override var targetFolder: String? = null + override var variableStructureRef: VariableStructureRef? = null val pages = mutableListOf() /** Creates a new [Page], appends it, and returns it for further configuration. */ @@ -46,6 +50,7 @@ class BaseTemplateBuilder(id: String) : DtoBuilderBase(BaseTemplateTable, projectName.name) { +class BaseTemplateRepository( + projectName: ProjectName, + private val statusTrackingRepository: StatusTrackingRepository, +) : Repository(BaseTemplateTable, projectName.name) { override fun fromDb(row: ResultRow): BaseTemplate { return BaseTemplate( @@ -31,15 +36,17 @@ class BaseTemplateRepository(projectName: ProjectName) : originLocations = row[BaseTemplateTable.originLocations], targetFolder = row[BaseTemplateTable.targetFolder], pages = row[BaseTemplateTable.pages], + variableStructureRef = row[BaseTemplateTable.variableStructureRef]?.let { VariableStructureRef(it) }, ) } 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() + DocumentObjectTable.selectAll().where { + (DocumentObjectTable.projectName eq projectName) and + (DocumentObjectTable.baseTemplate.extract("type") eq "BaseTemplateRef") and + (DocumentObjectTable.baseTemplate.extract("id") eq id) + }.map { DocumentObjectTable.fromResultRow(it) } } } @@ -49,6 +56,10 @@ class BaseTemplateRepository(projectName: ProjectName) : val now = Clock.System.now() + if (existingItem == null) { + statusTrackingRepository.active(dto.id, ResourceType.BaseTemplate) + } + table.upsertReturning(table.id, table.projectName) { it[BaseTemplateTable.id] = dto.id it[BaseTemplateTable.projectName] = this@BaseTemplateRepository.projectName @@ -59,6 +70,7 @@ class BaseTemplateRepository(projectName: ProjectName) : it[BaseTemplateTable.lastUpdated] = now it[BaseTemplateTable.targetFolder] = dto.targetFolder it[BaseTemplateTable.pages] = dto.pages + it[BaseTemplateTable.variableStructureRef] = dto.variableStructureRef?.id }.first() } } @@ -68,7 +80,7 @@ class BaseTemplateRepository(projectName: ProjectName) : val columns = listOf( "id", "project_name", "name", "origin_locations", "custom_fields", - "created", "last_updated", "target_folder", "pages" + "created", "last_updated", "target_folder", "pages", "variable_structure_ref" ) val sql = createSql(columns, dtos.size) val now = Clock.System.now() @@ -79,6 +91,10 @@ class BaseTemplateRepository(projectName: ProjectName) : dtos.forEach { dto -> val existingItem = find(dto.id) + if (existingItem == null) { + statusTrackingRepository.active(dto.id, ResourceType.BaseTemplate) + } + stmt.setString(index++, dto.id) stmt.setString(index++, this@BaseTemplateRepository.projectName) stmt.setString(index++, dto.name) @@ -88,6 +104,7 @@ class BaseTemplateRepository(projectName: ProjectName) : stmt.setTimestamp(index++, java.sql.Timestamp.from(now.toJavaInstant())) stmt.setString(index++, dto.targetFolder) stmt.setObject(index++, Json.encodeToString(dto.pages), Types.OTHER) + stmt.setString(index++, dto.variableStructureRef?.id) } stmt.executeUpdate() 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 index 0be62259..8e49f5cb 100644 --- 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 @@ -7,4 +7,5 @@ import org.jetbrains.exposed.v1.json.jsonb object BaseTemplateTable : MigrationObjectTable("base_template") { val targetFolder = varchar("target_folder", 255).nullable() val pages = jsonb>("pages", Json) + val variableStructureRef = varchar("variable_structure_ref", 255).nullable() } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/persistence/upgrade/V17__base_template_variable_structure.kt b/migration-library/src/main/kotlin/com/quadient/migration/persistence/upgrade/V17__base_template_variable_structure.kt new file mode 100644 index 00000000..2d92f60c --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/persistence/upgrade/V17__base_template_variable_structure.kt @@ -0,0 +1,18 @@ +package com.quadient.migration.persistence.upgrade + +import org.flywaydb.core.api.migration.BaseJavaMigration +import org.flywaydb.core.api.migration.Context + +class V17__base_template_variable_structure : BaseJavaMigration() { + override fun migrate(context: Context) { + val connection = context.connection + connection.createStatement().use { stmt -> + stmt.execute( + """ + ALTER TABLE base_template + ADD COLUMN IF NOT EXISTS variable_structure_ref VARCHAR(255) + """.trimIndent() + ) + } + } +} 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 a6bfa11b..0c002bfe 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 @@ -37,10 +37,11 @@ class ReferenceValidator( val displayRules = displayRuleRepository.listAll() val images = imageRepository.listAll() val attachments = attachmentRepository.listAll() + val baseTemplates = baseTemplateRepository.listAll() val alreadyValidatedRefs = mutableSetOf() val missingRefs = - (documentObjects + variables + paragraphStyles + textStyles + dataStructures + displayRules + images + attachments).mapNotNull { + (documentObjects + variables + paragraphStyles + textStyles + dataStructures + displayRules + images + attachments + baseTemplates).mapNotNull { validate(it, alreadyValidatedRefs).missingRefs.ifEmpty { null } }.flatten() diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/ResourcePathProvider.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/ResourcePathProvider.kt index a3c8cbdd..a95c3c50 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/ResourcePathProvider.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/ResourcePathProvider.kt @@ -16,10 +16,6 @@ import com.quadient.migration.shared.IcmPath import com.quadient.migration.shared.ImageType import com.quadient.migration.shared.orDefault import com.quadient.migration.shared.toIcmPath -import org.slf4j.LoggerFactory -import java.lang.invoke.MethodHandles - -private val logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()) interface ResourcePathProvider { fun getDocumentObjectPath(nameOrId: String, type: DocumentObjectType, targetFolder: IcmPath?): IcmPath @@ -72,11 +68,7 @@ fun ResourcePathProvider.getBaseTemplateFullPath( is BaseTemplateRef -> { val baseTemplate = findBaseTemplate(documentObjectBaseTemplate.id) - val baseTemplatePath = getBaseTemplatePath(baseTemplate) - val message = - "Base template '$baseTemplatePath' cannot be used because referencing base templates by id is not yet supported during deployment." - logger.error(message) - error(message) + return getBaseTemplatePath(baseTemplate) } null -> config.baseTemplatePath 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 72a55127..80e13ac7 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 @@ -48,6 +48,7 @@ import com.quadient.migration.service.inspirebuilder.InspireDocumentObjectBuilde import com.quadient.migration.service.ResourcePathProvider import com.quadient.migration.service.deploy.utility.DeployOrder import com.quadient.migration.service.deploy.utility.DeployOrderImpl +import com.quadient.migration.service.deploy.utility.RefInheritanceService import com.quadient.migration.service.ipsclient.IpsService import com.quadient.migration.service.ipsclient.OperationResult import com.quadient.migration.service.readSafely @@ -67,6 +68,7 @@ sealed class DeployClient( private val conflictDetector: ConflictDetectorImpl, private val progressReporter: ProgressReporterImpl, private val deployOrder: DeployOrderImpl, + private val refInheritanceService: RefInheritanceService, private val resourcePathProvider: ResourcePathProvider, protected val documentObjectRepository: DocumentObjectRepository, protected val imageRepository: ImageRepository, @@ -108,9 +110,11 @@ sealed class DeployClient( abstract fun shouldIncludeDependency(documentObject: DocumentObject): Boolean + abstract fun deployBaseTemplates(): DeploymentResult + fun deployDocumentObjects(): DeploymentResult { val tracker = ResultTrackerImpl(statusTrackingRepository, projectConfig.inspireOutput) - val ordered = deployOrder(getAllDocumentObjectsToDeploy()) + val ordered = refInheritanceService.apply(deployOrder(getAllDocumentObjectsToDeploy())) val result = deployDocumentObjectsInternal(ordered, tracker, ::uploadDocumentObject, ::uploadImage, ::uploadAttachment, ::uploadDisplayRule) runPostProcessors(result) @@ -122,11 +126,11 @@ sealed class DeployClient( val documentObjects = getDocumentObjectsToDeploy(documentObjectIds) val tracker = ResultTrackerImpl(statusTrackingRepository, projectConfig.inspireOutput) val result = if (skipDependencies) { - val ordered = deployOrder(documentObjects) + val ordered = refInheritanceService.apply(deployOrder(documentObjects)) deployDocumentObjectsInternal(ordered, tracker, ::uploadDocumentObject, ::uploadImage, ::uploadAttachment, ::uploadDisplayRule) } else { val dependencies = documentObjects.flatMap { it.findDependencies() }.filter { it.internal != true } - val ordered = deployOrder((documentObjects + dependencies).toSet().toList()) + val ordered = refInheritanceService.apply(deployOrder((documentObjects + dependencies).toSet().toList())) deployDocumentObjectsInternal(ordered, tracker, ::uploadDocumentObject, ::uploadImage, ::uploadAttachment, ::uploadDisplayRule) } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DesignerDeployClient.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DesignerDeployClient.kt index 7d2f9268..d3da53e4 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DesignerDeployClient.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DesignerDeployClient.kt @@ -25,6 +25,7 @@ import com.quadient.migration.service.deploy.utility.ResultTracker import com.quadient.migration.service.ResourcePathProvider import com.quadient.migration.service.deploy.utility.ConflictDetectorImpl import com.quadient.migration.service.deploy.utility.DeployOrderImpl +import com.quadient.migration.service.deploy.utility.NoopRefInheritanceService import com.quadient.migration.service.deploy.utility.ProgressReporterImpl import com.quadient.migration.service.inspirebuilder.InspireDocumentObjectBuilder import com.quadient.migration.service.ipsclient.IpsService @@ -69,6 +70,7 @@ class DesignerDeployClient( conflictDetector, progressReporter, deployOrder, + NoopRefInheritanceService(), resourcePathProvider, documentObjectRepository, imageRepository, @@ -121,6 +123,10 @@ class DesignerDeployClient( return documentObject.type != DocumentObjectType.Page && documentObject.internal != true } + override fun deployBaseTemplates(): DeploymentResult { + error("Base template deployment is not supported for Designer output.") + } + override fun uploadDocumentObject(obj: DocumentObject, targetPath: IcmPath, wfdXml: String): OperationResult { return ipsService.xml2wfd(wfdXml, targetPath) } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/EvolveDeployClient.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/EvolveDeployClient.kt index 35326d44..d9acee4b 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/EvolveDeployClient.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/EvolveDeployClient.kt @@ -23,8 +23,11 @@ import com.quadient.migration.service.ResourcePathProvider import com.quadient.migration.service.getBaseTemplateFullPath import com.quadient.migration.service.deploy.utility.ConflictDetectorImpl import com.quadient.migration.service.deploy.utility.DeployOrderImpl +import com.quadient.migration.service.deploy.utility.DeploymentResult +import com.quadient.migration.service.deploy.utility.RefInheritanceServiceImpl import com.quadient.migration.service.deploy.utility.ProgressReporterImpl import com.quadient.migration.service.inspirebuilder.InspireDocumentObjectBuilder +import com.quadient.migration.service.inspirebuilder.InspireBaseTemplateBuilder import com.quadient.migration.service.ipsclient.IpsService import com.quadient.migration.service.ipsclient.OperationResult import com.quadient.migration.service.ipsclient.Version @@ -45,6 +48,7 @@ class EvolveDeployClient( conflictDetector: ConflictDetectorImpl, progressReporter: ProgressReporterImpl, deployOrder: DeployOrderImpl, + refInheritanceService: RefInheritanceServiceImpl, documentObjectRepository: DocumentObjectRepository, imageRepository: ImageRepository, attachmentRepository: AttachmentRepository, @@ -56,6 +60,7 @@ class EvolveDeployClient( variableStructureRepository: VariableStructureRepository, baseTemplateRepository: BaseTemplateRepository, documentObjectBuilder: InspireDocumentObjectBuilder, + baseTemplateBuilder: InspireBaseTemplateBuilder, ipsService: IpsService, storage: Storage, ) : InteractiveDeployClient( @@ -66,6 +71,7 @@ class EvolveDeployClient( conflictDetector, progressReporter, deployOrder, + refInheritanceService, documentObjectRepository, imageRepository, attachmentRepository, @@ -77,6 +83,7 @@ class EvolveDeployClient( variableStructureRepository, baseTemplateRepository, documentObjectBuilder, + baseTemplateBuilder, ipsService, storage, ) { @@ -250,6 +257,10 @@ class EvolveDeployClient( error("Styles deployment is not currently supported in Evolve output") } + override fun deployBaseTemplates(): DeploymentResult { + error("Base template deployment is not currently supported in Evolve output") + } + private fun HttpResult<*, ApiBadRequestException>.toOperationResult(): OperationResult = when (this) { is HttpResult.Success -> OperationResult.Success is HttpResult.Failure -> OperationResult.Failure("CA API error ${error.status}: ${error.title} - ${error.detail}") 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 da803cb3..d07826b5 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.BaseTemplateLocation import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap import com.quadient.migration.api.dto.migrationmodel.DisplayRule @@ -34,12 +35,15 @@ import com.quadient.migration.service.deploy.utility.MetadataValidatorImpl import com.quadient.migration.service.deploy.utility.PostProcessImpl import com.quadient.migration.service.deploy.utility.ResourceType import com.quadient.migration.service.deploy.utility.ResultTracker +import com.quadient.migration.service.deploy.utility.ResultTrackerImpl import com.quadient.migration.service.ResourcePathProvider import com.quadient.migration.service.getBaseTemplateFullPath import com.quadient.migration.service.deploy.utility.ConflictDetectorImpl import com.quadient.migration.service.deploy.utility.DeployOrderImpl +import com.quadient.migration.service.deploy.utility.RefInheritanceServiceImpl import com.quadient.migration.service.deploy.utility.ProgressReporterImpl import com.quadient.migration.service.inspirebuilder.InspireDocumentObjectBuilder +import com.quadient.migration.service.inspirebuilder.InspireBaseTemplateBuilder import com.quadient.migration.service.ipsclient.IpsService import com.quadient.migration.service.ipsclient.OperationResult import com.quadient.migration.service.resolveTarget @@ -68,6 +72,7 @@ open class InteractiveDeployClient( conflictDetector: ConflictDetectorImpl, progressReporter: ProgressReporterImpl, deployOrder: DeployOrderImpl, + refInheritanceService: RefInheritanceServiceImpl, documentObjectRepository: DocumentObjectRepository, imageRepository: ImageRepository, attachmentRepository: AttachmentRepository, @@ -79,6 +84,7 @@ open class InteractiveDeployClient( variableStructureRepository: VariableStructureRepository, baseTemplateRepository: BaseTemplateRepository, documentObjectBuilder: InspireDocumentObjectBuilder, + private val baseTemplateBuilder: InspireBaseTemplateBuilder, ipsService: IpsService, storage: Storage, ) : DeployClient( @@ -88,6 +94,7 @@ open class InteractiveDeployClient( conflictDetector, progressReporter, deployOrder, + refInheritanceService, resourcePathProvider, documentObjectRepository, imageRepository, @@ -185,6 +192,41 @@ open class InteractiveDeployClient( return documentObject.internal != true } + override fun deployBaseTemplates(): DeploymentResult { + val tracker = ResultTrackerImpl(statusTrackingRepository, projectConfig.inspireOutput) + + val baseTemplates = baseTemplateRepository.listAll() + logger.info("Found ${baseTemplates.size} base template(s) in the repository.") + + for (baseTemplate in baseTemplates) { + val targetPath = resourcePathProvider.getBaseTemplatePath(baseTemplate) + + if (!shouldDeployObject(baseTemplate.id, ResourceType.BaseTemplate, targetPath, tracker.deploymentResult)) { + logger.info("Skipping deployment of '${baseTemplate.id}' as it is not marked for deployment.") + continue + } + + val wfdXml = baseTemplateBuilder.buildBaseTemplate(baseTemplate) + + when (val result = ipsService.xml2wfd(wfdXml, targetPath)) { + is OperationResult.Success -> { + logger.debug("Deployment of base template '${baseTemplate.nameOrId()}' to $targetPath is successful.") + tracker.deployedBaseTemplate(baseTemplate.id, targetPath) + } + + is OperationResult.Failure -> { + val message = "Failed to deploy base template '${baseTemplate.nameOrId()}' to $targetPath." + logger.error(message) + tracker.errorBaseTemplate(baseTemplate.id, targetPath, message) + } + } + } + + runPostProcessors(tracker.deploymentResult) + + return tracker.deploymentResult + } + override fun getAllDocumentObjectsToDeploy(): List { return documentObjectRepository.list( (DocumentObjectTable.type inList listOf( @@ -234,7 +276,7 @@ open class InteractiveDeployClient( tracker: ResultTracker, deployDisplayRule: (DisplayRule, IcmPath, ByteArray) -> OperationResult, ) { - val rules = documentObjects + val enrichedRules = documentObjects .flatMap { try { it.getAllExternalDisplayRules() @@ -243,10 +285,14 @@ open class InteractiveDeployClient( emptyList() } } - .distinctBy { it.id } - - for (r in rules) { - val rule = r.resolveTarget(displayRuleRepository::findOrFail) + .distinctBy { it.rule.id } + + for (enrichedRule in enrichedRules) { + val resolvedRule = enrichedRule.rule.resolveTarget(displayRuleRepository::findOrFail) + val rule = resolvedRule.copy( + baseTemplate = resolvedRule.baseTemplate ?: enrichedRule.inheritedBaseTemplate, + variableStructureRef = resolvedRule.variableStructureRef ?: enrichedRule.inheritedVariableStructureRef, + ) val targetPath = resourcePathProvider.getDisplayRulePath(rule) if (!shouldDeployObject(rule.id, ResourceType.DisplayRule, targetPath, tracker.deploymentResult)) { @@ -453,8 +499,13 @@ open class InteractiveDeployClient( } } - private fun DocumentObject.getAllExternalDisplayRules(): List { - val resources = mutableListOf() + private fun DocumentObject.getAllExternalDisplayRules( + inheritedBaseTemplate: BaseTemplateLocation? = null, + inheritedVariableStructureRef: VariableStructureRef? = null, + ): List { + val resources = mutableListOf() + val effectiveBaseTemplate = this.baseTemplate ?: inheritedBaseTemplate + val effectiveVariableStructureRef = this.variableStructureRef ?: inheritedVariableStructureRef this.collectRefs().forEach { when (it) { @@ -464,14 +515,14 @@ open class InteractiveDeployClient( val resolvedModel = model.resolveTarget(displayRuleRepository::findOrFail) if (!resolvedModel.internal) { - resources.add(model) + resources.add(EnrichedDisplayRule(model, effectiveBaseTemplate, effectiveVariableStructureRef)) } } is DocumentObjectRef -> { val model = documentObjectRepository.find(it.id) ?: error("Unable to collect resource references because inner document object '${it.id}' was not found.") - resources.addAll(model.getAllExternalDisplayRules()) + resources.addAll(model.getAllExternalDisplayRules(effectiveBaseTemplate, effectiveVariableStructureRef)) } is ParagraphStyleRef, is AttachmentRef, is ImageRef, is TextStyleRef, is VariableRef, is VariableStructureRef, is BaseTemplateRef -> {} } @@ -479,4 +530,10 @@ open class InteractiveDeployClient( return resources } + + protected data class EnrichedDisplayRule( + val rule: DisplayRule, + val inheritedBaseTemplate: BaseTemplateLocation?, + val inheritedVariableStructureRef: VariableStructureRef?, + ) } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/ConflictDetector.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/ConflictDetector.kt index 5635caed..f129823e 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/ConflictDetector.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/ConflictDetector.kt @@ -105,6 +105,7 @@ class ConflictDetectorImpl( ResourceType.TextStyle -> resourcePathProvider.getStyleDefinitionPath() ResourceType.ParagraphStyle -> resourcePathProvider.getStyleDefinitionPath() + ResourceType.BaseTemplate -> null } }.getOrNull() } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/DeploymentResult.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/DeploymentResult.kt index 6d179238..62d39217 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/DeploymentResult.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/DeploymentResult.kt @@ -30,7 +30,7 @@ data class DeploymentInfo( ) enum class ResourceType { - DocumentObject, Image, Attachment, TextStyle, ParagraphStyle, DisplayRule + DocumentObject, Image, Attachment, TextStyle, ParagraphStyle, DisplayRule, BaseTemplate } data class DeploymentError(val id: String, val message: String) @@ -52,6 +52,8 @@ interface ResultTracker { fun deployedDisplayRule(id: String, targetPath: IcmPath) fun warningDisplayRule(id: String, path: IcmPath, message: String) fun errorDisplayRule(id: String, path: IcmPath, message: String) + fun deployedBaseTemplate(id: String, targetPath: IcmPath) + fun errorBaseTemplate(id: String, targetPath: IcmPath?, message: String) } class ResultTrackerImpl( @@ -209,4 +211,29 @@ class ResultTrackerImpl( ) deploymentResult.errors.add(DeploymentError(id, message)) } + + override fun deployedBaseTemplate(id: String, targetPath: IcmPath) { + statusTrackingRepository?.deployed( + id = id, + deploymentId = deploymentId, + timestamp = timestamp, + resourceType = ResourceType.BaseTemplate, + output = inspireOutput, + icmPath = targetPath, + ) + deploymentResult.deployed.add(DeploymentInfo(id, ResourceType.BaseTemplate, targetPath)) + } + + override fun errorBaseTemplate(id: String, targetPath: IcmPath?, message: String) { + statusTrackingRepository?.error( + id = id, + deploymentId = deploymentId, + timestamp = timestamp, + resourceType = ResourceType.BaseTemplate, + output = inspireOutput, + icmPath = targetPath, + message = message, + ) + deploymentResult.errors.add(DeploymentError(id, message)) + } } \ No newline at end of file diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/PostProcess.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/PostProcess.kt index 6c425db1..9f9e7ee3 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/PostProcess.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/PostProcess.kt @@ -77,6 +77,7 @@ class PostProcessImpl( ResourceType.Attachment -> attachmentRepository.find(info.id) ResourceType.TextStyle -> textStyleRepository.find(info.id) ResourceType.ParagraphStyle -> paragraphStyleRepository.find(info.id) + ResourceType.BaseTemplate -> null } if (obj == null) { val msg = "Failed to run '$name' post processor, '${info.type}' with id '${info.id}' at path '${info.targetPath}'." diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/RefInheritanceService.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/RefInheritanceService.kt new file mode 100644 index 00000000..90637780 --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/RefInheritanceService.kt @@ -0,0 +1,72 @@ +package com.quadient.migration.service.deploy.utility + +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateLocation +import com.quadient.migration.api.dto.migrationmodel.DocumentObject +import com.quadient.migration.api.dto.migrationmodel.DocumentObjectRef +import com.quadient.migration.api.dto.migrationmodel.VariableStructureRef +import com.quadient.migration.api.repository.DocumentObjectRepository + +interface RefInheritanceService { + fun apply(documentObjects: List): List +} + +class RefInheritanceServiceImpl( + private val documentObjectRepository: DocumentObjectRepository, +) : RefInheritanceService { + override fun apply(documentObjects: List): List { + val parentsById = mutableMapOf>() + for (obj in documentObjectRepository.listAll()) { + for (ref in obj.collectRefs()) { + if (ref is DocumentObjectRef) { + parentsById.getOrPut(ref.id) { mutableListOf() }.add(obj.id) + } + } + } + + val baseTemplateCache = mutableMapOf() + val variableStructureCache = mutableMapOf() + + fun resolveBaseTemplate(id: String, visiting: MutableSet): BaseTemplateLocation? { + baseTemplateCache[id]?.let { return it } + if (!visiting.add(id)) return null + + val obj = documentObjectRepository.find(id) + val resolved = obj?.baseTemplate ?: parentsById[id]?.firstNotNullOfOrNull { parentId -> + resolveBaseTemplate(parentId, visiting) + } + + visiting.remove(id) + if (resolved != null) baseTemplateCache[id] = resolved + return resolved + } + + fun resolveVariableStructureRef(id: String, visiting: MutableSet): VariableStructureRef? { + variableStructureCache[id]?.let { return it } + if (!visiting.add(id)) return null + + val obj = documentObjectRepository.find(id) + val resolved = obj?.variableStructureRef ?: parentsById[id]?.firstNotNullOfOrNull { parentId -> + resolveVariableStructureRef(parentId, visiting) + } + + visiting.remove(id) + if (resolved != null) variableStructureCache[id] = resolved + return resolved + } + + return documentObjects.map { obj -> + val effectiveBaseTemplate = obj.baseTemplate ?: resolveBaseTemplate(obj.id, mutableSetOf()) + val effectiveVariableStructureRef = obj.variableStructureRef + ?: resolveVariableStructureRef(obj.id, mutableSetOf()) + + obj.copy( + baseTemplate = effectiveBaseTemplate, + variableStructureRef = effectiveVariableStructureRef, + ) + } + } +} + +class NoopRefInheritanceService : RefInheritanceService { + override fun apply(documentObjects: List): List = documentObjects +} 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 cb85e8c1..78b0d952 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 @@ -11,7 +11,6 @@ import com.quadient.migration.api.repository.ImageRepository 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.service.IcmDataCache import com.quadient.migration.service.ResourcePathProvider import com.quadient.migration.service.resolveAliases @@ -36,23 +35,13 @@ import com.quadient.wfdxml.api.module.Layout import com.quadient.wfdxml.internal.layoutnodes.FlowAreaImpl import com.quadient.wfdxml.internal.layoutnodes.PageImpl import com.quadient.wfdxml.internal.layoutnodes.PagesImpl -import org.w3c.dom.Document -import org.w3c.dom.Element -import org.w3c.dom.Node -import org.xml.sax.InputSource -import java.io.StringReader -import java.io.StringWriter -import javax.xml.parsers.DocumentBuilderFactory -import javax.xml.transform.TransformerFactory.newInstance -import javax.xml.transform.dom.DOMSource -import javax.xml.transform.stream.StreamResult class DesignerDocumentObjectBuilder( documentObjectRepository: DocumentObjectRepository, textStyleRepository: TextStyleRepository, paragraphStyleRepository: ParagraphStyleRepository, variableRepository: VariableRepository, - variableStructureRepository: VariableStructureRepository, + variableStructureBuilder: InspireVariableStructureBuilder, displayRuleRepository: DisplayRuleRepository, imageRepository: ImageRepository, attachmentRepository: AttachmentRepository, @@ -65,7 +54,7 @@ class DesignerDocumentObjectBuilder( textStyleRepository, paragraphStyleRepository, variableRepository, - variableStructureRepository, + variableStructureBuilder, displayRuleRepository, imageRepository, attachmentRepository, @@ -100,7 +89,7 @@ class DesignerDocumentObjectBuilder( var smsModel: DocumentObject? = null val virtualPageContent = mutableListOf() - val variableStructure = initVariableStructure(layout, documentObject.variableStructureRef?.id) + val variableStructure = variableStructureBuilder.initVariableStructure(layout, documentObject.variableStructureRef?.id) val languages = collectLanguages(documentObject) val languageVariable = variableStructure.languageVariable @@ -115,7 +104,7 @@ class DesignerDocumentObjectBuilder( layout.data.setLanguageVariable(variable) } - layout.addPdfMetadataToPages(documentObject, variableStructure) + variableStructureBuilder.addPdfMetadataToPages(layout, documentObject, variableStructure) documentObject.content.paragraphIfEmpty().forEach { val model = (it as? DocumentObjectRef)?.id?.let(documentObjectRepository::findOrFail) @@ -176,7 +165,7 @@ class DesignerDocumentObjectBuilder( return if (projectConfig.sourceBaseTemplatePath.isNullOrBlank()) { documentObjectXml } else { - enrichLayoutWithSourceBaseTemplate(documentObjectXml, projectConfig.sourceBaseTemplatePath.toIcmPath()) + enrichLayoutWithSourceBaseTemplate(icmDataCache, documentObjectXml, projectConfig.sourceBaseTemplatePath.toIcmPath()) } } @@ -319,7 +308,7 @@ class DesignerDocumentObjectBuilder( } private fun DocumentObject.buildSmsRoot(layout: Layout, varStructure: VariableStructure, languages: List) { - layout.addSmsNumberToPages(this, varStructure) + variableStructureBuilder.addSmsNumberToPages(layout, this, varStructure) val smsRoot = layout.addSmsRoot() val flow = buildDocumentContentAsSingleFlow( @@ -335,7 +324,7 @@ class DesignerDocumentObjectBuilder( smsRoot.setContent(flow) } private fun DocumentObject.buildEmailRoot(layout: Layout, varStructure: VariableStructure, languages: List) { - layout.addEmailMetadataToPages(this, varStructure) + variableStructureBuilder.addEmailMetadataToPages(layout, this, varStructure) val emailRoot = layout.addEmailComponentRoot() val emailBodyRootFlow = layout.addFlow().setSectionFlow(true) val emailTmText = layout.addEmailTMText() @@ -454,44 +443,6 @@ class DesignerDocumentObjectBuilder( } } - private fun enrichLayoutWithSourceBaseTemplate(documentObjectXml: String, sourceBaseTemplatePath: IcmPath): String { - val sourceBaseTemplateXml = icmDataCache.wfd2Xml(sourceBaseTemplatePath) - - val sourceBaseTemplateDoc = sourceBaseTemplateXml.toXmlDocument() - val documentObjectDoc = documentObjectXml.toXmlDocument() - - val sourceBaseLayoutNode = sourceBaseTemplateDoc.getElementsByTagName("Layout").item(0) as? Element - ?: error("Source base template '$sourceBaseTemplatePath' does not contain a Layout element.") - val sourceBaseInnerLayoutNode = sourceBaseLayoutNode.firstElementChildByTag("Layout") - ?: error("Source base template '$sourceBaseTemplatePath' does not contain an inner Layout element.") - - val documentObjectInnerLayoutNode = - documentObjectDoc.getElementsByTagName("Layout").item(0)?.firstElementChildByTag("Layout") - ?.let { sourceBaseTemplateDoc.importNode(it, true) } - ?: error("Document object does not contain an inner Layout element.") - - sourceBaseLayoutNode.replaceChild(documentObjectInnerLayoutNode, sourceBaseInnerLayoutNode) - return sourceBaseTemplateDoc.toXmlString() - } - - private fun String.toXmlDocument(): Document = - DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(InputSource(StringReader(this))) - - private fun Document.toXmlString(): String { - val result = StringWriter() - val transformer = newInstance().newTransformer() - transformer.transform(DOMSource(this), StreamResult(result)) - return result.toString().replace(Regex("([\\s\\S]*?)")) { matchResult -> - val value = matchResult.groupValues[1] - val encoded = value.replace("\n", " ").replace("\r", "") - "$encoded" - } - } - - private fun Node.firstElementChildByTag(tag: String): Element? = - (0 until childNodes.length).asSequence().map { childNodes.item(it) }.filterIsInstance() - .firstOrNull { it.tagName == tag } - private sealed interface PageContent { data class AreaContent(val content: Area): PageContent data class PathObjectContent(val content: Shape): PageContent diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt new file mode 100644 index 00000000..a6ad6f42 --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt @@ -0,0 +1,152 @@ +package com.quadient.migration.service.inspirebuilder + +import com.quadient.migration.api.ProjectConfig +import com.quadient.migration.api.dto.migrationmodel.BaseTemplate +import com.quadient.migration.api.dto.migrationmodel.DocumentObject +import com.quadient.migration.api.dto.migrationmodel.DocumentObjectRef +import com.quadient.migration.api.dto.migrationmodel.EmailOptions +import com.quadient.migration.api.dto.migrationmodel.SmsOptions +import com.quadient.migration.api.repository.BaseTemplateRepository +import com.quadient.migration.api.repository.DocumentObjectRepository +import com.quadient.migration.service.IcmDataCache +import com.quadient.migration.service.ResourcePathProvider +import com.quadient.migration.service.deploy.utility.RefInheritanceServiceImpl +import com.quadient.migration.shared.DocumentObjectType +import com.quadient.migration.shared.IcmPath +import com.quadient.migration.shared.toIcmPath +import com.quadient.migration.tools.logger +import com.quadient.wfdxml.WfdXmlBuilder +import com.quadient.wfdxml.api.layoutnodes.Flow +import com.quadient.wfdxml.api.layoutnodes.Flow.WebEditingType.SECTION +import com.quadient.wfdxml.api.layoutnodes.Pages +import com.quadient.wfdxml.api.layoutnodes.email.EmailComponentPlaceHolder +import com.quadient.wfdxml.api.module.Layout + +class InspireBaseTemplateBuilder( + private val projectConfig: ProjectConfig, + private val icmDataCache: IcmDataCache, + private val resourcePathProvider: ResourcePathProvider, + private val baseTemplateRepository: BaseTemplateRepository, + private val documentObjectRepository: DocumentObjectRepository, + private val variableStructureBuilder: InspireVariableStructureBuilder, + private val refInheritanceService: RefInheritanceServiceImpl, +) { + private val logger by logger() + + private val resolvedStyleDefinitionPath: IcmPath? by lazy { + val path = resourcePathProvider.getStyleDefinitionPath() + try { + if (icmDataCache.fileExists(path)) path else null + } catch (e: Exception) { + throw RuntimeException("Failed to check for style definition existence", e) + } + } + + fun buildBaseTemplate(baseTemplate: BaseTemplate): String { + logger.debug("Starting to build base template '${baseTemplate.nameOrId()}'.") + + val builder = WfdXmlBuilder() + val layout = builder.addLayout() + val root = layout.setName("DocumentLayout").addRoot().setAllowRuntimeModifications(true) + if (resolvedStyleDefinitionPath != null) { + root.setExternalStylesLayout(resolvedStyleDefinitionPath.toString()) + } + resolveArialFont(layout, icmDataCache) + + val usages = baseTemplateRepository.findUsages(baseTemplate.id).filterIsInstance() + + var mainFlow: Flow? = null + var mainFlowSize = -1.0 + + baseTemplate.pages.forEachIndexed { pageIndex, page -> + val wfdPage = layout.addPage().setType(Pages.PageConditionType.SIMPLE) + page.name?.let { wfdPage.setName(it) } + page.pageWidth?.let { wfdPage.setWidth(it.toMeters()) } + page.pageHeight?.let { wfdPage.setHeight(it.toMeters()) } + + page.areas.forEach { area -> + val flow = layout.addFlow() + .setName(area.interactiveFlowName) + .setType(Flow.Type.SIMPLE) + .setSectionFlow(true) + .setWebEditingType(SECTION) + layout.pages.addInteractiveFlow(flow, Pages.InteractiveFlowType.NORMAL) + + val flowArea = wfdPage.addFlowArea().setName("${area.interactiveFlowName}Area").setFlow(flow) + .setFlowToNextPage(area.flowToNextPage) + + var areaSize = -1.0 + area.position?.let { + flowArea.setPosX(it.x.toMeters()).setPosY(it.y.toMeters()).setWidth(it.width.toMeters()) + .setHeight(it.height.toMeters()) + areaSize = it.width.toMeters() * it.height.toMeters() + } + + if (pageIndex == 0 && areaSize > mainFlowSize) { + mainFlow = flow + mainFlowSize = areaSize + } + } + } + + mainFlow?.let { layout.pages.setMainFlow(it) } + + enrichFromDocumentObjects(baseTemplate, usages, layout) + + val baseTemplateXml = builder.build() + val sourceBaseTemplatePath = if (projectConfig.sourceBaseTemplatePath.isNullOrBlank()) { + IcmPath.root().join("Interactive").join("StandardPackage").join("Sources").join("SourceTemplate.wfd") + } else { + projectConfig.sourceBaseTemplatePath.toIcmPath() + } + + return enrichLayoutWithSourceBaseTemplate(icmDataCache, baseTemplateXml, sourceBaseTemplatePath) + } + + private fun enrichFromDocumentObjects(baseTemplate: BaseTemplate, usages: List, layout: Layout) { + var emailModel: DocumentObject? = null + var smsModel: DocumentObject? = null + + for (usage in usages) { + for (content in usage.content) { + val referenced = (content as? DocumentObjectRef)?.id?.let(documentObjectRepository::find) ?: continue + when (referenced.type) { + DocumentObjectType.Email -> if (emailModel == null) emailModel = referenced + DocumentObjectType.Sms -> if (smsModel == null) smsModel = referenced + else -> Unit + } + } + } + + if (emailModel == null && smsModel == null) return + + val resolvedVariableStructureId = baseTemplate.variableStructureRef?.id + ?: listOfNotNull(emailModel, smsModel).firstNotNullOfOrNull { + refInheritanceService.apply(listOf(it)).first().variableStructureRef?.id + } + + val variableStructure = variableStructureBuilder.initVariableStructure(layout, resolvedVariableStructureId) + + if (smsModel != null) { + val smsFlow = layout.addFlow().setSectionFlow(true).setWebEditingType(SECTION) + .addCustomProperty("customName", "SMS Content") + layout.pages.addInteractiveFlow(smsFlow, Pages.InteractiveFlowType.NORMAL) + layout.addSmsRoot().setContent(smsFlow) + variableStructureBuilder.addSmsNumberToPages( + layout, smsModel.options as? SmsOptions, variableStructure + ) + } + + if (emailModel != null) { + val emailBodyRootFlow = layout.addFlow().setSectionFlow(true).setWebEditingType(SECTION) + .addCustomProperty("customName", "Body Content") + layout.pages.addInteractiveFlow(emailBodyRootFlow, Pages.InteractiveFlowType.HTML) + layout.addEmailComponentRoot().setEmailComponentsText(layout.addEmailTMText()) + layout.addEmailComponentPlaceHolder().setId("Def.EmailsBody").setType(EmailComponentPlaceHolder.Type.BODY) + .setContent(emailBodyRootFlow) + variableStructureBuilder.addEmailMetadataToPages( + layout, emailModel.options as? EmailOptions, variableStructure + ) + } + } +} diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBuilderUtils.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBuilderUtils.kt index e099a20e..f6b97721 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBuilderUtils.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBuilderUtils.kt @@ -1,12 +1,15 @@ package com.quadient.migration.service.inspirebuilder +import com.quadient.migration.service.IcmDataCache import com.quadient.migration.shared.Color +import com.quadient.migration.shared.IcmPath import com.quadient.wfdxml.api.layoutnodes.FillStyle import com.quadient.wfdxml.api.layoutnodes.Flow import com.quadient.wfdxml.api.layoutnodes.Font import com.quadient.wfdxml.api.layoutnodes.Image +import com.quadient.wfdxml.api.layoutnodes.LocationType import com.quadient.wfdxml.api.layoutnodes.data.DataType -import com.quadient.wfdxml.api.layoutnodes.data.Variable +import com.quadient.wfdxml.api.layoutnodes.font.SubFont import com.quadient.wfdxml.api.module.Layout import com.quadient.wfdxml.internal.Group import com.quadient.wfdxml.internal.layoutnodes.FlowImpl @@ -15,6 +18,16 @@ import com.quadient.wfdxml.internal.layoutnodes.ImageImpl import com.quadient.wfdxml.internal.layoutnodes.data.DataImpl import com.quadient.wfdxml.internal.layoutnodes.data.VariableImpl import com.quadient.wfdxml.internal.module.layout.LayoutImpl +import org.w3c.dom.Document +import org.w3c.dom.Element +import org.w3c.dom.Node +import org.xml.sax.InputSource +import java.io.StringReader +import java.io.StringWriter +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.TransformerFactory.newInstance +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult import com.quadient.migration.shared.DataType as DataTypeModel fun getDataType(dataType: DataTypeModel): DataType { @@ -221,4 +234,63 @@ fun appendExtensionIfMissing(fileName: String, sourcePath: String?): String { fun toScriptStringLiteral(value: String): String = "'${ value.replace("\\", "\\\\").replace("\"", "\\\"").replace("'", "\\'") -}'" \ No newline at end of file +}'" + +fun resolveArialFont(layout: Layout, icmDataCache: IcmDataCache) { + val arialFont = getFontByName(layout, "Arial") + require(arialFont != null) { "Layout must contain Arial font." } + arialFont.setName("Arial").setFontName("Arial") + upsertSubFont(icmDataCache, arialFont, isBold = false, isItalic = false) +} + +fun upsertSubFont(icmDataCache: IcmDataCache, font: Font, isBold: Boolean, isItalic: Boolean): SubFont? { + val subFontName = buildFontName(isBold, isItalic) + + val fontLocation = icmDataCache.font[FontKey(font.name, subFontName)] + ?: icmDataCache.font[FontKey(font.name, buildFontName(bold = false, italic = false))] + ?: return null + + font.subFonts.removeAll { it.name == subFontName } + return font.addSubfont().setName(subFontName).setBold(isBold).setItalic(isItalic) + .setLocation(fontLocation, LocationType.ICM) +} + +fun enrichLayoutWithSourceBaseTemplate( + icmDataCache: IcmDataCache, documentObjectXml: String, sourceBaseTemplatePath: IcmPath, +): String { + val sourceBaseTemplateXml = icmDataCache.wfd2Xml(sourceBaseTemplatePath) + + val sourceBaseTemplateDoc = sourceBaseTemplateXml.toXmlDocument() + val documentObjectDoc = documentObjectXml.toXmlDocument() + + val sourceBaseLayoutNode = sourceBaseTemplateDoc.getElementsByTagName("Layout").item(0) as? Element + ?: error("Source base template '$sourceBaseTemplatePath' does not contain a Layout element.") + val sourceBaseInnerLayoutNode = sourceBaseLayoutNode.firstElementChildByTag("Layout") + ?: error("Source base template '$sourceBaseTemplatePath' does not contain an inner Layout element.") + + val documentObjectInnerLayoutNode = + documentObjectDoc.getElementsByTagName("Layout").item(0)?.firstElementChildByTag("Layout") + ?.let { sourceBaseTemplateDoc.importNode(it, true) } + ?: error("Document object does not contain an inner Layout element.") + + sourceBaseLayoutNode.replaceChild(documentObjectInnerLayoutNode, sourceBaseInnerLayoutNode) + return sourceBaseTemplateDoc.toXmlString() +} + +private fun String.toXmlDocument(): Document = + DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(InputSource(StringReader(this))) + +private fun Document.toXmlString(): String { + val result = StringWriter() + val transformer = newInstance().newTransformer() + transformer.transform(DOMSource(this), StreamResult(result)) + return result.toString().replace(Regex("([\\s\\S]*?)")) { matchResult -> + val value = matchResult.groupValues[1] + val encoded = value.replace("\n", " ").replace("\r", "") + "$encoded" + } +} + +private fun Node.firstElementChildByTag(tag: String): Element? = + (0 until childNodes.length).asSequence().map { childNodes.item(it) }.filterIsInstance() + .firstOrNull { it.tagName == tag } \ No newline at end of file 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 6a37bd84..b19fd7b1 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 @@ -4,7 +4,6 @@ import com.quadient.migration.api.InspireOutput import com.quadient.migration.api.ProjectConfig import com.quadient.migration.api.dto.migrationmodel.Area import com.quadient.migration.api.dto.migrationmodel.ColumnLayout -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.DocumentContent @@ -68,7 +67,6 @@ import com.quadient.migration.shared.TableAction import com.quadient.migration.shared.TableAlignment import com.quadient.wfdxml.WfdXmlBuilder import com.quadient.wfdxml.api.layoutnodes.Flow -import com.quadient.wfdxml.api.layoutnodes.Font import com.quadient.wfdxml.api.layoutnodes.email.EmailComponentGrid import com.quadient.migration.shared.ColumnDistribution import com.quadient.wfdxml.api.layoutnodes.Image as WfdXmlImage @@ -77,14 +75,12 @@ import com.quadient.wfdxml.api.layoutnodes.Pages import com.quadient.wfdxml.api.layoutnodes.ParagraphStyle as WfdXmlParagraphStyle import com.quadient.wfdxml.api.layoutnodes.ParagraphStyle.LineSpacingType.* import com.quadient.wfdxml.api.layoutnodes.Section as WfdXmlSection -import com.quadient.wfdxml.api.layoutnodes.SheetNameType import com.quadient.wfdxml.api.layoutnodes.TabulatorType import com.quadient.wfdxml.api.layoutnodes.data.Data import com.quadient.wfdxml.api.layoutnodes.data.DataType import com.quadient.wfdxml.api.layoutnodes.data.Variable as WfdXmlVariable import com.quadient.wfdxml.api.layoutnodes.data.VariableKind import com.quadient.wfdxml.api.layoutnodes.flow.Text as WfdXmlText -import com.quadient.wfdxml.api.layoutnodes.font.SubFont import com.quadient.wfdxml.api.layoutnodes.tables.GeneralRowSet import com.quadient.wfdxml.api.layoutnodes.tables.RowSet import com.quadient.wfdxml.api.layoutnodes.tables.Table as WfdXmlTable @@ -97,12 +93,9 @@ import com.quadient.wfdxml.api.layoutnodes.tables.BorderStyle import com.quadient.wfdxml.api.layoutnodes.tables.Cell import com.quadient.wfdxml.api.layoutnodes.flow.Paragraph as WfdXmlParagraph import com.quadient.wfdxml.api.module.Layout -import com.quadient.wfdxml.internal.data.WorkFlowTreeDefinition import com.quadient.wfdxml.internal.layoutnodes.TextStyleImpl import com.quadient.wfdxml.internal.layoutnodes.data.DataImpl import com.quadient.wfdxml.internal.layoutnodes.data.WorkFlowTreeEnums.NodeOptionality -import com.quadient.wfdxml.internal.layoutnodes.data.WorkFlowTreeEnums.NodeType.SUB_TREE -import kotlin.time.Clock import com.quadient.migration.tools.logger import kotlin.collections.ifEmpty import com.quadient.migration.shared.DataType as DataTypeModel @@ -115,17 +108,14 @@ import com.quadient.migration.api.repository.BaseTemplateRepository import com.quadient.migration.api.repository.DisplayRuleRepository import com.quadient.migration.api.repository.ImageRepository import com.quadient.migration.api.repository.VariableRepository -import com.quadient.migration.api.repository.VariableStructureRepository import com.quadient.migration.service.IcmDataCache import com.quadient.migration.service.ResourcePathProvider 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 @@ -134,7 +124,7 @@ abstract class InspireDocumentObjectBuilder( protected val textStyleRepository: TextStyleRepository, protected val paragraphStyleRepository: ParagraphStyleRepository, protected val variableRepository: VariableRepository, - protected val variableStructureRepository: VariableStructureRepository, + protected val variableStructureBuilder: InspireVariableStructureBuilder, protected val displayRuleRepository: DisplayRuleRepository, protected val imageRepository: ImageRepository, protected val attachmentRepository: AttachmentRepository, @@ -474,74 +464,8 @@ abstract class InspireDocumentObjectBuilder( ) } - protected fun initVariableStructure(layout: Layout, variableStructureId: String?): VariableStructure { - val variableStructureId = variableStructureId ?: projectConfig.defaultVariableStructure - - val variableStructureModel = - variableStructureId?.let { variableStructureRepository.findOrFail(it) } ?: VariableStructure( - id = "defaultVariableStructure", - lastUpdated = Clock.System.now(), - created = Clock.System.now(), - structure = mutableMapOf(), - customFields = CustomFieldMap(), - languageVariable = null, - ) - - val normalizedVariablePaths = variableStructureModel.structure.map { (variableId, variablePathData) -> - val literalPath = variablePathData.path.resolve(variableStructureModel, variableRepository::findOrFail) - ?: error("Variable '$variableId' referenced as array path has no resolvable literal path in structure") - removeDataFromVariablePath(literalPath) - }.filter { it.isNotBlank() }.filter { it != "SystemVariable" && !it.startsWith("SystemVariable.") } - - val variableTree = buildVariableTree(normalizedVariablePaths) - - val workflowTreeDefinition = WorkFlowTreeDefinition("Root", SUB_TREE, NodeOptionality.ARRAY).also { - buildVariablePathPart(it, variableTree) - } - - val layoutData = layout.data - layoutData.importDataDefinition(workflowTreeDefinition) - if (variableTree.isNotEmpty() && variableTree.values.first() is ArrayVariable) { - layoutData.setRepeatedBy("Data.${variableTree.keys.first()}") - } - - return variableStructureModel - } - - private fun buildVariablePathPart( - parentNode: WorkFlowTreeDefinition, currentMap: Map - ) { - currentMap.forEach { - val variablePathPart = it.value - val optionality = - if (variablePathPart is ArrayVariable) NodeOptionality.ARRAY else NodeOptionality.MUST_EXIST - - val node = WorkFlowTreeDefinition(variablePathPart.name, SUB_TREE, optionality) - parentNode.addSubNode(node) - - if (variablePathPart.children.isNotEmpty()) { - buildVariablePathPart(node, variablePathPart.children) - } - } - } - - private fun upsertSubFont(font: Font, isBold: Boolean, isItalic: Boolean): SubFont? { - val subFontName = buildFontName(isBold, isItalic) - - val fontLocation = icmDataCache.font[FontKey(font.name, subFontName)] - ?: icmDataCache.font[FontKey(font.name, buildFontName(bold = false, italic = false))] - ?: return null - - font.subFonts.removeAll { it.name == subFontName } - return font.addSubfont().setName(subFontName).setBold(isBold).setItalic(isItalic) - .setLocation(fontLocation, LocationType.ICM) - } - fun buildTextStyles(layout: Layout, textStyleModels: List) { - val arialFont = getFontByName(layout, "Arial") - require(arialFont != null) { "Layout must contain Arial font." } - arialFont.setName("Arial").setFontName("Arial") - upsertSubFont(arialFont, isBold = false, isItalic = false) + resolveArialFont(layout, icmDataCache) textStyleModels.forEach { styleModel -> val definition = styleModel.resolve().definition @@ -556,7 +480,7 @@ abstract class InspireDocumentObjectBuilder( val font = getFontByName(layout, fontFamily) ?: layout.addFont().setName(fontFamily).setFontName(fontFamily) textStyle.setFont(font) - val subFont = upsertSubFont(font, definition.bold, definition.italic) + val subFont = upsertSubFont(icmDataCache, font, definition.bold, definition.italic) if (subFont != null) { textStyle.setSubFont(subFont) } @@ -1861,43 +1785,6 @@ abstract class InspireDocumentObjectBuilder( } } - 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) - } - - 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) - } - - 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 { data class Success(val variableScript: String) : ScriptResult { override fun toString() = variableScript @@ -2086,7 +1973,7 @@ internal fun VariablePath.resolve(variableStructure: VariableStructure, findVari } } -private fun variableStringContentToScript( +internal fun variableStringContentToScript( variableStringContent: List, layout: Layout, variableStructure: VariableStructure, diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireVariableStructureBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireVariableStructureBuilder.kt new file mode 100644 index 00000000..4c990efe --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireVariableStructureBuilder.kt @@ -0,0 +1,156 @@ +package com.quadient.migration.service.inspirebuilder + +import com.quadient.migration.api.ProjectConfig +import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap +import com.quadient.migration.api.dto.migrationmodel.DocumentObject +import com.quadient.migration.api.dto.migrationmodel.EmailOptions +import com.quadient.migration.api.dto.migrationmodel.SmsOptions +import com.quadient.migration.api.dto.migrationmodel.VariableStringContent +import com.quadient.migration.api.dto.migrationmodel.VariableStructure +import com.quadient.migration.api.repository.VariableRepository +import com.quadient.migration.api.repository.VariableStructureRepository +import com.quadient.wfdxml.api.layoutnodes.SheetNameType +import com.quadient.wfdxml.api.layoutnodes.data.VariableKind +import com.quadient.wfdxml.api.module.Layout +import com.quadient.wfdxml.internal.data.WorkFlowTreeDefinition +import com.quadient.wfdxml.internal.layoutnodes.data.WorkFlowTreeEnums.NodeOptionality +import com.quadient.wfdxml.internal.layoutnodes.data.WorkFlowTreeEnums.NodeType.SUB_TREE +import kotlin.time.Clock + +class InspireVariableStructureBuilder( + private val variableRepository: VariableRepository, + private val variableStructureRepository: VariableStructureRepository, + private val projectConfig: ProjectConfig, +) { + fun initVariableStructure(layout: Layout, variableStructureId: String?): VariableStructure { + val variableStructureId = variableStructureId ?: projectConfig.defaultVariableStructure + + val variableStructureModel = + variableStructureId?.let { variableStructureRepository.findOrFail(it) } ?: VariableStructure( + id = "defaultVariableStructure", + lastUpdated = Clock.System.now(), + created = Clock.System.now(), + structure = mutableMapOf(), + customFields = CustomFieldMap(), + languageVariable = null, + ) + + val normalizedVariablePaths = variableStructureModel.structure.map { (variableId, variablePathData) -> + val literalPath = variablePathData.path.resolve(variableStructureModel, variableRepository::findOrFail) + ?: error("Variable '$variableId' referenced as array path has no resolvable literal path in structure") + removeDataFromVariablePath(literalPath) + }.filter { it.isNotBlank() }.filter { it != "SystemVariable" && !it.startsWith("SystemVariable.") } + + val variableTree = buildVariableTree(normalizedVariablePaths) + + val workflowTreeDefinition = WorkFlowTreeDefinition("Root", SUB_TREE, NodeOptionality.ARRAY).also { + buildVariablePathPart(it, variableTree) + } + + val layoutData = layout.data + layoutData.importDataDefinition(workflowTreeDefinition) + if (variableTree.isNotEmpty() && variableTree.values.first() is ArrayVariable) { + layoutData.setRepeatedBy("Data.${variableTree.keys.first()}") + } + + return variableStructureModel + } + + private fun buildVariablePathPart( + parentNode: WorkFlowTreeDefinition, currentMap: Map + ) { + currentMap.forEach { + val variablePathPart = it.value + val optionality = + if (variablePathPart is ArrayVariable) NodeOptionality.ARRAY else NodeOptionality.MUST_EXIST + + val node = WorkFlowTreeDefinition(variablePathPart.name, SUB_TREE, optionality) + parentNode.addSubNode(node) + + if (variablePathPart.children.isNotEmpty()) { + buildVariablePathPart(node, variablePathPart.children) + } + } + } + + fun addEmailMetadataToPages(layout: Layout, documentObject: DocumentObject, variableStructure: VariableStructure) = + addEmailMetadataToPages(layout, documentObject.options as? EmailOptions, variableStructure) + + fun addEmailMetadataToPages(layout: Layout, emailOptions: EmailOptions?, variableStructure: VariableStructure) { + addSheetNameVariable( + layout, variableStructure, SheetNameType.EMAIL_FROM, "EmailFrom", emailOptions?.from, emitEmpty = true + ) + addSheetNameVariable( + layout, + variableStructure, + SheetNameType.EMAIL_FROM_NAME, + "EmailFromName", + emailOptions?.fromName, + emitEmpty = true, + ) + addSheetNameVariable( + layout, + variableStructure, + SheetNameType.EMAIL_SUBJECT, + "EmailSubject", + emailOptions?.subject, + emitEmpty = true, + ) + addSheetNameVariable( + layout, variableStructure, SheetNameType.EMAIL_TO, "EmailTo", emailOptions?.to, emitEmpty = true + ) + } + + fun addSmsNumberToPages(layout: Layout, documentObject: DocumentObject, variableStructure: VariableStructure) = + addSmsNumberToPages(layout, documentObject.options as? SmsOptions, variableStructure) + + fun addSmsNumberToPages(layout: Layout, smsOptions: SmsOptions?, variableStructure: VariableStructure) { + addSheetNameVariable( + layout, + variableStructure, + SheetNameType.SMS_NUMBER_TO, + "NumberTo", + smsOptions?.numberTo, + emitEmpty = true, + ) + } + + fun addPdfMetadataToPages(layout: Layout, documentObject: DocumentObject, variableStructure: VariableStructure) { + val pdfMetadata = documentObject.pdfMetadata ?: return + addSheetNameVariable(layout, variableStructure, SheetNameType.PDF_TITLE, "TaggingTitle", pdfMetadata.title) + addSheetNameVariable(layout, variableStructure, SheetNameType.PDF_AUTHOR, "TaggingAuthor", pdfMetadata.author) + addSheetNameVariable( + layout, variableStructure, SheetNameType.PDF_SUBJECT, "TaggingSubject", pdfMetadata.subject + ) + addSheetNameVariable( + layout, variableStructure, SheetNameType.PDF_KEYWORDS, "TaggingKeywords", pdfMetadata.keywords + ) + addSheetNameVariable( + layout, variableStructure, SheetNameType.PDF_PRODUCER, "TaggingProduce", pdfMetadata.producer + ) + } + + private fun addSheetNameVariable( + layout: Layout, + variableStructure: VariableStructure, + type: SheetNameType, + variableName: String, + value: List?, + emitEmpty: Boolean = false, + ) { + if (value.isNullOrEmpty() && !emitEmpty) return + + val script = if (value.isNullOrEmpty()) { + "return \"\";" + } else { + variableStringContentToScript(value, layout, variableStructure, variableRepository::findOrFail) + } + + val variable = layout.data + .addVariable() + .setName(variableName) + .setKind(VariableKind.CALCULATED) + .setScript(script) + layout.pages.addSheetName(type, variable) + } +} 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 01da4100..18bce636 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 @@ -15,7 +15,6 @@ import com.quadient.migration.api.repository.ImageRepository 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.service.IcmDataCache import com.quadient.migration.service.ResourcePathProvider import com.quadient.migration.service.getBaseTemplateFullPath @@ -34,7 +33,7 @@ class InteractiveDocumentObjectBuilder( textStyleRepository: TextStyleRepository, paragraphStyleRepository: ParagraphStyleRepository, variableRepository: VariableRepository, - variableStructureRepository: VariableStructureRepository, + variableStructureBuilder: InspireVariableStructureBuilder, displayRuleRepository: DisplayRuleRepository, imageRepository: ImageRepository, attachmentRepository: AttachmentRepository, @@ -47,7 +46,7 @@ class InteractiveDocumentObjectBuilder( textStyleRepository, paragraphStyleRepository, variableRepository, - variableStructureRepository, + variableStructureBuilder, displayRuleRepository, imageRepository, attachmentRepository, @@ -103,9 +102,9 @@ class InteractiveDocumentObjectBuilder( ?: error("Unable to deploy document object ${documentObject.id}. Base template '$baseTemplatePath' does not exist.") val languages = collectLanguages(documentObject) - val variableStructure = initVariableStructure(layout, documentObject.variableStructureRef?.id) + val variableStructure = variableStructureBuilder.initVariableStructure(layout, documentObject.variableStructureRef?.id) - layout.addPdfMetadataToPages(documentObject, variableStructure) + variableStructureBuilder.addPdfMetadataToPages(layout, documentObject, variableStructure) val interactiveFlowsWithContent = mutableMapOf>() var usedSmsModel: DocumentObject? = null @@ -169,11 +168,11 @@ class InteractiveDocumentObjectBuilder( val hasMultipleFlows = interactiveFlowsWithContent.size > 1 if (usedSmsModel != null) { - layout.addSmsNumberToPages(usedSmsModel, variableStructure) + variableStructureBuilder.addSmsNumberToPages(layout, usedSmsModel, variableStructure) } if (usedEmailModel != null) { - layout.addEmailMetadataToPages(usedEmailModel, variableStructure) + variableStructureBuilder.addEmailMetadataToPages(layout, usedEmailModel, variableStructure) } interactiveFlowsWithContent.forEach { 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 3ddd9f52..529cb609 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 @@ -54,23 +54,18 @@ class DeployPhaseUtilsTest { } @Test - fun `base template referenced by id fails because it is not yet supported`() { + fun `base template referenced by id resolves to its deployed path`() { val baseTemplate = BaseTemplate( id = "bt-1", name = "AddressBaseTemplate", customFields = CustomFieldMap(), ) - try { - resourcePathProvider.getBaseTemplateFullPath( - projectConfig, BaseTemplateRef(baseTemplate.id) - ) { id -> if (id == baseTemplate.id) baseTemplate else error("Unexpected id '$id'") } - error("Expected an exception to be thrown") - } catch (e: IllegalStateException) { - e.message.shouldBeEqualTo( - "Base template 'icm://Interactive/StandardPackage/BaseTemplates/AddressBaseTemplate.wfd' cannot be used because referencing base templates by id is not yet supported during deployment." - ) - } + val result = resourcePathProvider.getBaseTemplateFullPath( + projectConfig, BaseTemplateRef(baseTemplate.id) + ) { id -> if (id == baseTemplate.id) baseTemplate else error("Unexpected id '$id'") }.toString() + + result.shouldBeEqualTo("icm://Interactive/StandardPackage/BaseTemplates/AddressBaseTemplate.wfd") } @Test diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/DesignerDeployClientTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/DesignerDeployClientTest.kt index bde382fc..6aa6b90f 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/DesignerDeployClientTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/DesignerDeployClientTest.kt @@ -138,6 +138,7 @@ class DesignerDeployClientTest { } every { ipsService.writeMetadata(any>()) } just runs every { documentObjectRepository.find(any()) } returns null + every { documentObjectRepository.listAll() } returns emptyList() } @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 e956821a..819d9024 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 @@ -25,6 +25,8 @@ import com.quadient.migration.service.deploy.utility.ProgressReporterImpl import com.quadient.migration.service.inspirebuilder.InteractiveDocumentObjectBuilder import com.quadient.migration.service.InteractiveResourcePathProvider import com.quadient.migration.service.deploy.utility.DeployOrderImpl +import com.quadient.migration.service.deploy.utility.RefInheritanceServiceImpl +import com.quadient.migration.service.inspirebuilder.InspireBaseTemplateBuilder import com.quadient.migration.service.ipsclient.IpsService import com.quadient.migration.service.ipsclient.OperationResult import com.quadient.migration.service.ipsclient.Version @@ -56,12 +58,12 @@ class EvolveDeployClientTest { val baseTemplateRepository = mockk() val statusTrackingRepository = mockk() val documentObjectBuilder = mockk() + val baseTemplateBuilder = mockk() val ipsService = mockk() val storage = mockk() val caClient = mockk() val resourcePathProvider = mockk() val postProcess = mockk(relaxed = true) - val deployOrder = DeployOrderImpl(documentObjectRepository) val evolveConfig = EvolveConfig( apiRetryDelayMs = 0L, @@ -82,6 +84,9 @@ class EvolveDeployClientTest { targetDefaultFolder = "defaultFolder" ) + val deployOrder = DeployOrderImpl(documentObjectRepository) + val refInheritanceService = RefInheritanceServiceImpl(documentObjectRepository) + val conflictDetector = ConflictDetectorImpl(documentObjectRepository, imageRepository, attachmentRepository, displayRuleRepository, statusTrackingRepository, resourcePathProvider, projectConfig.inspireOutput) val progressReporter = ProgressReporterImpl(documentObjectRepository, imageRepository, attachmentRepository, displayRuleRepository, documentObjectBuilder, statusTrackingRepository, resourcePathProvider, projectConfig.inspireOutput) @@ -95,6 +100,7 @@ class EvolveDeployClientTest { conflictDetector, progressReporter, deployOrder, + refInheritanceService, documentObjectRepository, imageRepository, attachmentRepository, @@ -106,6 +112,7 @@ class EvolveDeployClientTest { variableStructureRepository, baseTemplateRepository, documentObjectBuilder, + baseTemplateBuilder, ipsService, storage, ) diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt index 75a1488d..aa4f844c 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt @@ -5,6 +5,7 @@ import com.quadient.migration.api.dto.migrationmodel.Attachment import com.quadient.migration.api.dto.migrationmodel.AttachmentRef 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.DocumentObject import com.quadient.migration.api.dto.migrationmodel.Image import com.quadient.migration.api.dto.migrationmodel.ImageRef @@ -12,6 +13,7 @@ import com.quadient.migration.api.dto.migrationmodel.Paragraph import com.quadient.migration.api.dto.migrationmodel.StringValue 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.BaseTemplateBuilder 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.ImageBuilder @@ -44,6 +46,8 @@ import com.quadient.migration.service.deploy.utility.ResultTrackerImpl import com.quadient.migration.service.inspirebuilder.InteractiveDocumentObjectBuilder import com.quadient.migration.service.InteractiveResourcePathProvider import com.quadient.migration.service.deploy.utility.DeployOrderImpl +import com.quadient.migration.service.deploy.utility.RefInheritanceServiceImpl +import com.quadient.migration.service.inspirebuilder.InspireBaseTemplateBuilder import com.quadient.migration.service.ipsclient.IpsService import com.quadient.migration.service.ipsclient.OperationResult import com.quadient.migration.service.resolveTargetDir @@ -60,7 +64,6 @@ import com.quadient.migration.shared.MetadataValue import com.quadient.migration.shared.SkipOptions import com.quadient.migration.shared.toIcmPath import com.quadient.migration.tools.aActiveStatus -import com.quadient.migration.tools.aBlockModel import com.quadient.migration.tools.aDeployedStatus import com.quadient.migration.tools.aErrorStatus import com.quadient.migration.tools.aProjectConfig @@ -103,6 +106,7 @@ class InteractiveDeployClientTest { val baseTemplateRepository = mockk() val statusTrackingRepository = mockk() val documentObjectBuilder = mockk() + val baseTemplateBuilder = mockk() val ipsService = mockk() val storage = mockk() val config = aProjectConfig( @@ -115,6 +119,7 @@ class InteractiveDeployClientTest { val conflictDetector = ConflictDetectorImpl(documentObjectRepository, imageRepository, attachmentRepository, displayRuleRepository, statusTrackingRepository, resourcePathProvider, config.inspireOutput) val progressReporter = ProgressReporterImpl(documentObjectRepository, imageRepository, attachmentRepository, displayRuleRepository, documentObjectBuilder, statusTrackingRepository, resourcePathProvider, config.inspireOutput) val deployOrder = DeployOrderImpl(documentObjectRepository) + val refInheritanceService = RefInheritanceServiceImpl(documentObjectRepository) private val subject = InteractiveDeployClient( config, @@ -124,6 +129,7 @@ class InteractiveDeployClientTest { conflictDetector, progressReporter, deployOrder, + refInheritanceService, documentObjectRepository, imageRepository, attachmentRepository, @@ -135,6 +141,7 @@ class InteractiveDeployClientTest { variableStructureRepository, baseTemplateRepository, documentObjectBuilder, + baseTemplateBuilder, ipsService, storage, ) @@ -148,6 +155,7 @@ class InteractiveDeployClientTest { every { ipsService.writeMetadata(any()) } just runs every { ipsService.setProductionApprovalState(any>()) } returns OperationResult.Success every { documentObjectRepository.find(any()) } returns null + every { documentObjectRepository.listAll() } returns emptyList() } @Test @@ -520,6 +528,53 @@ class InteractiveDeployClientTest { verify(exactly = 0) { ipsService.setProductionApprovalState(any>()) } } + @Test + fun `deployBaseTemplates uploads base templates and sets production approval state`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").build() + every { baseTemplateRepository.listAll() } returns listOf(baseTemplate) + every { baseTemplateBuilder.buildBaseTemplate(baseTemplate) } returns "" + every { ipsService.xml2wfd(any(), any()) } returns OperationResult.Success + every { ipsService.setProductionApprovalState(any>()) } returns OperationResult.Success + every { statusTrackingRepository.findLastEventRelevantToOutput(any(), any(), any()) } returns Active() + every { + statusTrackingRepository.deployed(any(), any(), any(), any(), any(), any()) + } returns aDeployedStatus("id") + + val targetPath = "icm://Interactive/BaseTemplates/BT_1.wfd".toIcmPath() + every { resourcePathProvider.getBaseTemplatePath(baseTemplate) } returns targetPath + + // when + val result = subject.deployBaseTemplates() + + // then + result.deployed.shouldBeOfSize(1) + verify { ipsService.xml2wfd(eq(""), eq(targetPath)) } + verify { ipsService.setProductionApprovalState(eq(listOf(targetPath))) } + } + + @Test + fun `deployBaseTemplates does not set production approval state when upload fails`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").build() + every { baseTemplateRepository.listAll() } returns listOf(baseTemplate) + every { baseTemplateBuilder.buildBaseTemplate(baseTemplate) } returns "" + every { ipsService.xml2wfd(any(), any()) } returns OperationResult.Failure("Problem") + every { ipsService.setProductionApprovalState(any>()) } returns OperationResult.Success + every { resourcePathProvider.getBaseTemplatePath(baseTemplate) } returns "icm://Interactive/BaseTemplates/BT_1.wfd".toIcmPath() + every { statusTrackingRepository.findLastEventRelevantToOutput(any(), any(), any()) } returns Active() + every { + statusTrackingRepository.error(any(), any(), any(), any(), any(), any(), any()) + } returns aDeployedStatus("id") + + // when + val result = subject.deployBaseTemplates() + + // then + result.errors.shouldBeOfSize(1) + verify { ipsService.setProductionApprovalState(eq(emptyList())) } + } + @Test fun `deploy list of document objects validates that no document objects are unsupported`() { val spy = spyk(subject) @@ -925,6 +980,81 @@ class InteractiveDeployClientTest { )) } + @Test + fun `deployDocumentObjects deploys a standalone block inheriting baseTemplate from its ancestor template`() { + // given + val block = mockObj(aDocObj("B_1", DocumentObjectType.Block, internal = false)) + val template = mockObj( + aDocObj( + "T_1", DocumentObjectType.Template, + content = listOf(aDocumentObjectRef(block.id)), + internal = false, + baseTemplate = "icm://Interactive/tenant/BaseTemplates/inherited.wfd", + ) + ) + every { documentObjectRepository.list(any>()) } returns listOf(block) + every { documentObjectRepository.listAll() } returns listOf(block, template) + every { documentObjectBuilder.buildDocumentObject(any()) } answers { firstArg().id } + every { resourcePathProvider.getDocumentObjectPath(any()) } answers { "icm://${firstArg().id}".toIcmPath() } + mockBasicSuccessfulIpsOperations() + every { statusTrackingRepository.findLastEventRelevantToOutput(any(), any(), any()) } returns Active() + every { + statusTrackingRepository.deployed(any(), any(), any(), any(), any(), any(), any()) + } returns aDeployedStatus("id") + + // when + subject.deployDocumentObjects(listOf(block.id), true) + + // then + verify { + documentObjectBuilder.buildDocumentObject( + withArg { it.baseTemplate.shouldBeEqualTo(LiteralBaseTemplatePath("icm://Interactive/tenant/BaseTemplates/inherited.wfd")) } + ) + } + } + + @Test + fun `deployDocumentObjects deploys an external display rule inheriting baseTemplate from the document object that introduced it`() { + // given + val rule = DisplayRuleBuilder("R_1") + .comparison { value("a").equals().value("b") } + .internal(false) + .build() + .mock() + val block = mockObj( + aDocObj( + "B_1", DocumentObjectType.Block, + content = listOf(aParagraph(displayRuleRef = DisplayRuleRef(rule.id))), + internal = false, + baseTemplate = "icm://Interactive/tenant/BaseTemplates/inherited.wfd", + ) + ) + every { documentObjectRepository.list(any>()) } returns listOf(block) + every { documentObjectRepository.listAll() } returns listOf(block) + every { documentObjectBuilder.buildDocumentObject(any()) } returns "" + every { resourcePathProvider.getDocumentObjectPath(any()) } answers { "icm://${firstArg().id}".toIcmPath() } + every { resourcePathProvider.getDisplayRulePath(any()) } returns "icm://Interactive/$tenant/Rules/defaultFolder/${rule.id}.jrd".toIcmPath() + every { ipsService.tryUpload(any(), any()) } returns OperationResult.Success + mockBasicSuccessfulIpsOperations() + every { statusTrackingRepository.findLastEventRelevantToOutput(any(), any(), any()) } returns Active() + every { + statusTrackingRepository.deployed(any(), any(), any(), any(), any(), any(), any()) + } returns aDeployedStatus("id") + + // when + subject.deployDocumentObjects() + + // then + verify { + ipsService.tryUpload( + "icm://Interactive/$tenant/Rules/defaultFolder/${rule.id}.jrd".toIcmPath(), + withArg { + String(it).contains("map://interactive/BaseTemplates/inherited.wfd").shouldBeEqualTo(true) + } + ) + } + } + private fun mockBasicDocumentObjects() { val blocks = List(2) { mockDocumentObject( diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/utility/RefInheritanceServiceTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/utility/RefInheritanceServiceTest.kt new file mode 100644 index 00000000..ce135760 --- /dev/null +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/utility/RefInheritanceServiceTest.kt @@ -0,0 +1,71 @@ +package com.quadient.migration.service.deploy.utility + +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef +import com.quadient.migration.api.dto.migrationmodel.DocumentObject +import com.quadient.migration.api.dto.migrationmodel.VariableStructureRef +import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder +import com.quadient.migration.api.repository.DocumentObjectRepository +import com.quadient.migration.shared.DocumentObjectType +import com.quadient.migration.tools.shouldBeEqualTo +import com.quadient.migration.tools.shouldBeNull +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Test + +class RefInheritanceServiceTest { + val documentObjectRepository = mockk() + val subject = RefInheritanceServiceImpl(documentObjectRepository) + val allObjects = mutableListOf() + + init { + every { documentObjectRepository.listAll() } answers { allObjects.toList() } + } + + @Test + fun `block inherits baseTemplate and variableStructureRef from its page and template ancestors`() { + val block = DocumentObjectBuilder("block", DocumentObjectType.Block).mock() + val page = DocumentObjectBuilder("page", DocumentObjectType.Page) + .documentObjectRef(block) + .variableStructureRef("vs1") + .mock() + DocumentObjectBuilder("template", DocumentObjectType.Template) + .documentObjectRef(page) + .baseTemplateRef("bt1") + .mock() + + val result = subject.apply(listOf(block)) + + result.single().baseTemplate.shouldBeEqualTo(BaseTemplateRef("bt1")) + result.single().variableStructureRef.shouldBeEqualTo(VariableStructureRef("vs1")) + } + + @Test + fun `explicit baseTemplate on the object itself takes priority over inherited value`() { + val block = DocumentObjectBuilder("block", DocumentObjectType.Block).baseTemplateRef("explicit").mock() + DocumentObjectBuilder("template", DocumentObjectType.Template) + .documentObjectRef(block) + .baseTemplateRef("inherited") + .mock() + + val result = subject.apply(listOf(block)) + + result.single().baseTemplate.shouldBeEqualTo(BaseTemplateRef("explicit")) + } + + @Test + fun `standalone block with no ancestors has no resolved baseTemplate`() { + val block = DocumentObjectBuilder("block", DocumentObjectType.Block).mock() + + val result = subject.apply(listOf(block)) + + result.single().baseTemplate.shouldBeNull() + result.single().variableStructureRef.shouldBeNull() + } + + private fun DocumentObjectBuilder.mock(): DocumentObject { + val obj = this.build() + every { documentObjectRepository.find(obj.id) } returns obj + allObjects.add(obj) + return obj + } +} 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 90d820b3..0aae2767 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 @@ -1278,7 +1278,7 @@ class DesignerDocumentObjectBuilderTest { textStyleRepository, paragraphStyleRepository, variableRepository, - variableStructureRepository, + InspireVariableStructureBuilder(variableRepository, variableStructureRepository, config), displayRuleRepository, imageRepository, attachmentRepository, diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt new file mode 100644 index 00000000..b9c4529b --- /dev/null +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt @@ -0,0 +1,370 @@ +package com.quadient.migration.service.inspirebuilder + +import com.quadient.migration.api.dto.migrationmodel.EmailOptions +import com.quadient.migration.api.dto.migrationmodel.SmsOptions +import com.quadient.migration.api.dto.migrationmodel.StringValue +import com.quadient.migration.api.dto.migrationmodel.builder.BaseTemplateBuilder +import com.quadient.migration.api.repository.BaseTemplateRepository +import com.quadient.migration.api.repository.DocumentObjectRepository +import com.quadient.migration.api.repository.VariableRepository +import com.quadient.migration.api.repository.VariableStructureRepository +import com.quadient.migration.service.InteractiveIcmDataCache +import com.quadient.migration.service.InteractiveResourcePathProvider +import com.quadient.migration.service.deploy.utility.RefInheritanceServiceImpl +import com.quadient.migration.service.ipsclient.IpsService +import com.quadient.migration.shared.DocumentObjectType +import com.quadient.migration.shared.Color +import com.quadient.migration.shared.IcmPath +import com.quadient.migration.shared.millimeters +import com.quadient.migration.tools.aProjectConfig +import com.quadient.migration.tools.model.aDocObj +import com.quadient.migration.tools.model.aDocumentObjectRef +import com.quadient.migration.tools.shouldBeEqualTo +import com.quadient.migration.tools.shouldBeNull +import com.quadient.migration.tools.shouldNotBeNull +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import tools.jackson.databind.JsonNode +import tools.jackson.dataformat.xml.XmlMapper +import tools.jackson.module.kotlin.KotlinModule + +class InspireBaseTemplateBuilderTest { + private val ipsService = mockk() + private val config = aProjectConfig() + private val resourcePathProvider = InteractiveResourcePathProvider(config) + private val icmDataCache = InteractiveIcmDataCache(ipsService, resourcePathProvider) + private val baseTemplateRepository = mockk() + private val documentObjectRepository = mockk() + private val variableRepository = mockk() + private val variableStructureRepository = mockk() + private val refInheritanceService = RefInheritanceServiceImpl(documentObjectRepository) + private val subject = InspireBaseTemplateBuilder( + config, + icmDataCache, + resourcePathProvider, + baseTemplateRepository, + documentObjectRepository, + InspireVariableStructureBuilder(variableRepository, variableStructureRepository, config), + refInheritanceService, + ) + private val xmlMapper = XmlMapper.builder().addModule(KotlinModule.Builder().build()).build() + + @BeforeEach + fun setUp() { + every { ipsService.wfd2xml(any()) } returns """ + + + Layout1 + Layout1 + + + + + """.trimIndent() + every { ipsService.fileExists(any()) } returns false + every { ipsService.gatherFontData(any()) } returns "Arial,Regular,icm://Fonts/arial.ttf;" + every { baseTemplateRepository.findUsages(any()) } returns emptyList() + every { documentObjectRepository.listAll() } returns emptyList() + } + + @Test + fun `buildBaseTemplate sets external styles layout when style definition exists`() { + // given + every { ipsService.fileExists(any()) } returns true + val baseTemplate = BaseTemplateBuilder("BT_1").build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + val styleDefinitionPath = resourcePathProvider.getStyleDefinitionPath() + result["Root"]["ExternalStylesLayout"].stringValue().shouldBeEqualTo("VCSLocation,$styleDefinitionPath") + } + + @Test + fun `buildBaseTemplate does not set external styles layout when style definition does not exist`() { + // given + every { ipsService.fileExists(any()) } returns false + val baseTemplate = BaseTemplateBuilder("BT_1").build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + result["Root"]["ExternalStylesLayout"].shouldBeNull() + } + + @Test + fun `buildBaseTemplate redirects Arial font to ICM location`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + result["Font"]["SubFont"]["FontLocation"].stringValue().shouldBeEqualTo("VCSLocation,icm://Fonts/arial.ttf") + } + + @Test + fun `buildBaseTemplate creates page with name and size`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").addPage { + name("Page 1") + pageSize(210.millimeters(), 297.millimeters()) + }.build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + val pageId = result["Page"].first { it["Name"].stringValue() == "Page 1" }["Id"].stringValue() + val pageData = result["Page"].last { it["Id"].stringValue() == pageId } + pageData["Width"].stringValue().shouldBeEqualTo(210.millimeters().toMeters().toString()) + pageData["Height"].stringValue().shouldBeEqualTo(297.millimeters().toMeters().toString()) + } + + @Test + fun `buildBaseTemplate creates interactive flow and flow area per area`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").addPage { + name("Page 1") + addArea("Body") { + flowToNextPage(true) + position { + left(10.millimeters()) + top(20.millimeters()) + width(180.millimeters()) + height(250.millimeters()) + } + } + }.build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + val flowId = result["Flow"].first { it["Name"]?.stringValue() == "Body" }["Id"].stringValue() + + val flowAreaId = result["FlowArea"].first { it["FlowId"]?.stringValue() == flowId }["Id"].stringValue() + val flowAreaStub = result["FlowArea"].first { it["Id"].stringValue() == flowAreaId && it["Name"] != null } + flowAreaStub["Name"].stringValue().shouldBeEqualTo("BodyArea") + val flowArea = result["FlowArea"].last { it["Id"].stringValue() == flowAreaId } + flowArea["FlowingToNextPage"].stringValue().shouldBeEqualTo("True") + flowArea["Pos"]["X"].stringValue().shouldBeEqualTo(10.millimeters().toMeters().toString()) + flowArea["Pos"]["Y"].stringValue().shouldBeEqualTo(20.millimeters().toMeters().toString()) + flowArea["Size"]["X"].stringValue().shouldBeEqualTo(180.millimeters().toMeters().toString()) + flowArea["Size"]["Y"].stringValue().shouldBeEqualTo(250.millimeters().toMeters().toString()) + + result["Pages"]["MainFlow"].stringValue().shouldBeEqualTo(flowId) + result["Pages"]["InteractiveFlow"]["FlowId"].stringValue().shouldBeEqualTo(flowId) + } + + @Test + fun `buildBaseTemplate with multiple pages and areas creates one interactive flow per area`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1") + .addPage { name("Page 1"); addArea("Header"); addArea("Body") } + .addPage { name("Page 2"); addArea("Footer") } + .build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + result["Page"].filter { it["Name"] != null }.size.shouldBeEqualTo(2) + result["Flow"].filter { it["Name"] != null }.size.shouldBeEqualTo(3) + result["FlowArea"].filter { it["FlowId"] != null }.size.shouldBeEqualTo(3) + result["Pages"]["InteractiveFlow"].size().shouldBeEqualTo(3) + } + + @Test + fun `buildBaseTemplate picks the largest area as the main flow`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1") + .addPage { + name("Page 1") + addArea("Header") { + position { left(0.millimeters()); top(0.millimeters()); width(210.millimeters()); height(20.millimeters()) } + } + addArea("Body") { + position { left(0.millimeters()); top(20.millimeters()); width(210.millimeters()); height(250.millimeters()) } + } + } + .build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + val bodyFlowId = result["Flow"].first { it["Name"]?.stringValue() == "Body" }["Id"].stringValue() + result["Pages"]["MainFlow"].stringValue().shouldBeEqualTo(bodyFlowId) + } + + @Test + fun `buildBaseTemplate picks the largest area on the first page only as the main flow`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1") + .addPage { + name("Title page") + addArea("Title") { + position { left(0.millimeters()); top(0.millimeters()); width(210.millimeters()); height(297.millimeters()) } + } + } + .addPage { + name("Page 2") + addArea("Header") { + position { left(0.millimeters()); top(0.millimeters()); width(210.millimeters()); height(20.millimeters()) } + } + addArea("Body") { + position { left(0.millimeters()); top(20.millimeters()); width(210.millimeters()); height(250.millimeters()) } + } + } + .build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + val titleFlowId = result["Flow"].first { it["Name"]?.stringValue() == "Title" }["Id"].stringValue() + result["Pages"]["MainFlow"].stringValue().shouldBeEqualTo(titleFlowId) + } + + @Test + fun `buildBaseTemplate with page without areas creates no flow`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").addPage { name("Page 1") }.build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + result["Flow"].shouldBeNull() + result["FlowArea"].shouldBeNull() + result["Page"].first()["Name"].stringValue().shouldBeEqualTo("Page 1") + } + + @Test + fun `buildBaseTemplate registers SMS root when a usage references an Sms document object`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").build() + val smsModel = aDocObj("Sms_1", DocumentObjectType.Sms) + val template = aDocObj("T_1", DocumentObjectType.Template, content = listOf(aDocumentObjectRef(smsModel.id))) + every { baseTemplateRepository.findUsages(baseTemplate.id) } returns listOf(template) + every { documentObjectRepository.find(smsModel.id) } returns smsModel + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + result["SMSRoot"]["FlowId"].shouldNotBeNull() + result["Pages"]["InteractiveFlow"]["FlowType"].stringValue().shouldBeEqualTo("Normal") + result["ECPlaceHolder"].shouldBeNull() + sheetNameScript(result, 0).shouldBeEqualTo("return \"\";") + } + + @Test + fun `buildBaseTemplate uses sms options to build NumberTo sheet name script when present`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").build() + val smsModel = aDocObj( + "Sms_1", DocumentObjectType.Sms, options = SmsOptions(numberTo = listOf(StringValue("+1234567890"))) + ) + val template = aDocObj("T_1", DocumentObjectType.Template, content = listOf(aDocumentObjectRef(smsModel.id))) + every { baseTemplateRepository.findUsages(baseTemplate.id) } returns listOf(template) + every { documentObjectRepository.find(smsModel.id) } returns smsModel + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + sheetNameScript(result, 0).shouldBeEqualTo("return '+1234567890';") + } + + @Test + fun `buildBaseTemplate registers email component root and HTML interactive flow when a usage references an Email document object`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").build() + val emailModel = aDocObj("Email_1", DocumentObjectType.Email) + val template = aDocObj("T_1", DocumentObjectType.Template, content = listOf(aDocumentObjectRef(emailModel.id))) + every { baseTemplateRepository.findUsages(baseTemplate.id) } returns listOf(template) + every { documentObjectRepository.find(emailModel.id) } returns emailModel + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + result["ECRoot"].shouldNotBeNull() + result["ECPlaceHolder"]["Id"].stringValue().shouldBeEqualTo("Def.EmailsBody") + result["Pages"]["InteractiveFlow"]["FlowType"].stringValue().shouldBeEqualTo("HTML") + result["SMSRoot"].shouldBeNull() + sheetNameScript(result, 0).shouldBeEqualTo("return \"\";") + sheetNameScript(result, 2).shouldBeEqualTo("return \"\";") + } + + @Test + fun `buildBaseTemplate uses email options to build sheet name scripts when present`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").build() + val emailModel = aDocObj( + "Email_1", + DocumentObjectType.Email, + options = EmailOptions( + width = null, + backgroundFill = Color(255, 255, 255), + from = listOf(StringValue("from@quadient.com")), + fromName = emptyList(), + subject = listOf(StringValue("Hello")), + to = emptyList(), + ), + ) + val template = aDocObj("T_1", DocumentObjectType.Template, content = listOf(aDocumentObjectRef(emailModel.id))) + every { baseTemplateRepository.findUsages(baseTemplate.id) } returns listOf(template) + every { documentObjectRepository.find(emailModel.id) } returns emailModel + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + sheetNameScript(result, 0).shouldBeEqualTo("return 'from@quadient.com';") + sheetNameScript(result, 1).shouldBeEqualTo("return \"\";") + sheetNameScript(result, 2).shouldBeEqualTo("return 'Hello';") + } + + @Test + fun `buildBaseTemplate registers neither email nor sms modules when no usage references them`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").build() + val block = aDocObj("B_1", DocumentObjectType.Block) + val template = aDocObj("T_1", DocumentObjectType.Template, content = listOf(aDocumentObjectRef(block.id))) + every { baseTemplateRepository.findUsages(baseTemplate.id) } returns listOf(template) + every { documentObjectRepository.find(block.id) } returns block + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + result["SMSRoot"].shouldBeNull() + result["ECRoot"].shouldBeNull() + } + + @Test + fun `buildBaseTemplate without pages creates empty layout`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + result["Page"].shouldBeNull() + result["Flow"].shouldBeNull() + } + + private fun sheetNameScript(result: JsonNode, index: Int): String { + val calculatedScripts = result["Variable"].filter { it["Script"] != null }.map { it["Script"].stringValue() } + return calculatedScripts[index] + } +} diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilderTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilderTest.kt index 06c81179..69b87670 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilderTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilderTest.kt @@ -95,7 +95,7 @@ class InspireDocumentObjectBuilderTest { textStyleRepository, paragraphStyleRepository, variableRepository, - variableStructureRepository, + InspireVariableStructureBuilder(variableRepository, variableStructureRepository, config), displayRuleRepository, imageRepository, attachmentRepository, @@ -1518,7 +1518,7 @@ class InspireDocumentObjectBuilderTest { textStyleRepository, paragraphStyleRepository, variableRepository, - variableStructureRepository, + InspireVariableStructureBuilder(variableRepository, variableStructureRepository, config), displayRuleRepository, imageRepository, attachmentRepository, 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 aed6c918..7a443d35 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 @@ -1945,7 +1945,7 @@ class InteractiveDocumentObjectBuilderTest { textStyleRepository, paragraphStyleRepository, variableRepository, - variableStructureRepository, + InspireVariableStructureBuilder(variableRepository, variableStructureRepository, config), displayRuleRepository, imageRepository, attachmentRepository, 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 bba9108f..e4f5144b 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 @@ -530,4 +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)) +fun aBaseTemplateRepository() = BaseTemplateRepository(ProjectName(aProjectConfig().name), statusRepo) diff --git a/wfd-xml/api/src/main/java/com/quadient/wfdxml/api/layoutnodes/Pages.java b/wfd-xml/api/src/main/java/com/quadient/wfdxml/api/layoutnodes/Pages.java index bf6679ca..41c65064 100644 --- a/wfd-xml/api/src/main/java/com/quadient/wfdxml/api/layoutnodes/Pages.java +++ b/wfd-xml/api/src/main/java/com/quadient/wfdxml/api/layoutnodes/Pages.java @@ -17,6 +17,8 @@ public interface Pages extends Node { Pages setInteractiveFlows(List interactiveFlows); + Pages addInteractiveFlow(Flow flow, InteractiveFlowType type); + Pages addSheetName(SheetNameType type, Variable variable); enum PageOrder { @@ -25,6 +27,11 @@ enum PageOrder { DATA_VARIABLE_SELECTION, } + enum InteractiveFlowType { + NORMAL, + HTML, + } + enum PageConditionType { SIMPLE, SELECT_BY_INTEGER, diff --git a/wfd-xml/impl/src/main/java/com/quadient/wfdxml/internal/layoutnodes/PagesImpl.java b/wfd-xml/impl/src/main/java/com/quadient/wfdxml/internal/layoutnodes/PagesImpl.java index 84ff4f3f..2f1feaae 100644 --- a/wfd-xml/impl/src/main/java/com/quadient/wfdxml/internal/layoutnodes/PagesImpl.java +++ b/wfd-xml/impl/src/main/java/com/quadient/wfdxml/internal/layoutnodes/PagesImpl.java @@ -8,6 +8,7 @@ import com.quadient.wfdxml.internal.Tree; import com.quadient.wfdxml.internal.xml.export.XmlExporter; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -23,9 +24,11 @@ public class PagesImpl extends Tree implements Pages { private PageConditionType type = SIMPLE; private PageImpl page = null; private Flow mainFlow = null; - private List interactiveFlows = null; + private final List interactiveFlows = new ArrayList<>(); private final Map sheetNames = new HashMap<>(); + private record InteractiveFlowEntry(Flow flow, InteractiveFlowType type) {} + public static String pageConditionTypeToXml(PageConditionType type) { switch (type) { case SIMPLE: @@ -110,7 +113,16 @@ public Pages setMainFlow(Flow mainFlow) { @Override public Pages setInteractiveFlows(List interactiveFlows) { - this.interactiveFlows = interactiveFlows; + this.interactiveFlows.clear(); + for (Flow flow : interactiveFlows) { + this.interactiveFlows.add(new InteractiveFlowEntry(flow, Pages.InteractiveFlowType.NORMAL)); + } + return this; + } + + @Override + public Pages addInteractiveFlow(Flow flow, Pages.InteractiveFlowType type) { + this.interactiveFlows.add(new InteractiveFlowEntry(flow, type)); return this; } @@ -136,13 +148,11 @@ public void export(XmlExporter exporter) { exporter.addElementWithIface("MainFlow", mainFlow); exporter.addElementWithBoolData("UseAnotherFlowAsInteractiveMainFlow", false); } - if (interactiveFlows != null){ - for (Flow flow:interactiveFlows) { - exporter.beginElement("InteractiveFlow"); - exporter.addElementWithIface("FlowId", flow); - exporter.addElementWithStringData("FlowType", "Normal"); - exporter.endElement(); - } + for (InteractiveFlowEntry entry : interactiveFlows) { + exporter.beginElement("InteractiveFlow"); + exporter.addElementWithIface("FlowId", entry.flow()); + exporter.addElementWithStringData("FlowType", interactiveFlowTypeToXml(entry.type())); + exporter.endElement(); } if (pageSelectionType == VARIABLE) { exporter.addElementWithStringData("ConditionType", pageConditionTypeToXml(type)); @@ -187,6 +197,13 @@ private void exportSheetNames(XmlExporter exporter) { } } + public static String interactiveFlowTypeToXml(Pages.InteractiveFlowType type) { + return switch (type) { + case NORMAL -> "Normal"; + case HTML -> "HTML"; + }; + } + public String pageSelectionTypeToXml() { switch (pageSelectionType) { case SIMPLE: 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 a6d458d3..0677b7f7 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 @@ -88,9 +88,12 @@ class PagesImplTest extends Specification { Flow mainFlow = new FlowImpl() Flow interactiveFlow1 = new FlowImpl() Flow interactiveFlow2 = new FlowImpl() + Flow htmlFlow = new FlowImpl() PagesImpl pages = new PagesImpl() .setMainFlow(mainFlow) - .setInteractiveFlows([interactiveFlow1, interactiveFlow2]) + .addInteractiveFlow(interactiveFlow1, Pages.InteractiveFlowType.NORMAL) + .addInteractiveFlow(interactiveFlow2, Pages.InteractiveFlowType.NORMAL) + .addInteractiveFlow(htmlFlow, Pages.InteractiveFlowType.HTML) when: pages.export(exporter) @@ -108,6 +111,10 @@ class PagesImplTest extends Specification { SR_3 Normal + + SR_4 + HTML + """) }