From 584f63896fcf8f072b1283c8c14d5bec27fb3226 Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Mon, 2 Feb 2026 15:14:16 +0100 Subject: [PATCH 1/9] Basic implementation of "File", for handling attachments and documents in Inspire --- .../com/quadient/migration/api/Config.kt | 2 +- .../com/quadient/migration/api/Migration.kt | 11 + .../migration/api/dto/migrationmodel/File.kt | 31 +++ .../api/dto/migrationmodel/Mapping.kt | 18 ++ .../migration/api/dto/migrationmodel/Ref.kt | 14 +- .../builder/DocumentContentBuilderBase.kt | 9 + .../dto/migrationmodel/builder/FileBuilder.kt | 65 +++++ .../documentcontent/ParagraphBuilder.kt | 19 ++ .../documentcontent/DocumentContent.kt | 3 + .../documentcontent/Paragraph.kt | 3 + .../api/repository/FileRepository.kt | 97 +++++++ .../api/repository/MappingRepository.kt | 23 ++ .../com/quadient/migration/data/FileModel.kt | 23 ++ .../com/quadient/migration/data/RefModel.kt | 12 + .../documentcontent/DocumentContentModel.kt | 2 + .../data/documentcontent/ParagraphModel.kt | 1 + .../migrationmodel/MappingEntity.kt | 30 +++ .../persistence/migrationmodel/RefEntity.kt | 3 + .../repository/FileInternalRepository.kt | 26 ++ .../migration/persistence/table/FileTable.kt | 13 + .../upgrade/V10__add_file_table.kt | 15 ++ .../migration/service/ReferenceValidator.kt | 18 +- .../migration/service/StylesValidator.kt | 2 + .../migration/service/deploy/DeployClient.kt | 249 +++++++++++++----- .../service/deploy/DeploymentResult.kt | 40 ++- .../service/deploy/DesignerDeployClient.kt | 5 +- .../service/deploy/InteractiveDeployClient.kt | 5 +- .../service/deploy/ProgressReport.kt | 49 ++++ .../DesignerDocumentObjectBuilder.kt | 26 ++ .../InspireDocumentObjectBuilder.kt | 65 ++++- .../InteractiveDocumentObjectBuilder.kt | 26 ++ .../com/quadient/migration/shared/FileType.kt | 6 + .../persistence/MappingRepositoryTest.kt | 2 + .../service/ReferenceValidatorTest.kt | 3 + .../service/deploy/DeployClientTest.kt | 3 + .../deploy/DesignerDeployClientTest.kt | 3 + .../deploy/InteractiveDeployClientTest.kt | 3 + .../DesignerDocumentObjectBuilderTest.kt | 3 + .../InspireDocumentObjectBuilderTest.kt | 2 + .../InteractiveDocumentObjectBuilderTest.kt | 3 + .../tools/model/TestModelObjectBuilders.kt | 5 +- 41 files changed, 858 insertions(+), 80 deletions(-) create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/File.kt create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/FileBuilder.kt create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/api/repository/FileRepository.kt create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/data/FileModel.kt create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/persistence/repository/FileInternalRepository.kt create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/persistence/table/FileTable.kt create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/persistence/upgrade/V10__add_file_table.kt create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/shared/FileType.kt diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/Config.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/Config.kt index 801e38d0..5ad5fb99 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/Config.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/Config.kt @@ -72,4 +72,4 @@ data class InspireConfig(val ipsConfig: IpsConfig = IpsConfig()) data class IpsConfig(val host: String = "localhost", val port: Int = 30354, val timeoutSeconds: Int = 120) -data class PathsConfig(val images: IcmPath? = null, val fonts: IcmPath? = null) \ No newline at end of file +data class PathsConfig(val images: IcmPath? = null, val fonts: IcmPath? = null, val documents: IcmPath? = null, val attachments: IcmPath? = null) \ No newline at end of file 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 99cc02eb..79b6943a 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 @@ -37,6 +37,7 @@ class Migration(public val config: MigConfig, public val projectConfig: ProjectC val variableStructureRepository: Repository val displayRuleRepository: Repository val imageRepository: Repository + val fileRepository: Repository val statusTrackingRepository = StatusTrackingRepository(projectName) val mappingRepository: MappingRepository @@ -67,6 +68,7 @@ class Migration(public val config: MigConfig, public val projectConfig: ProjectC val documentObjectInternalRepository = DocumentObjectInternalRepository(DocumentObjectTable, projectName) val imageInternalRepository = ImageInternalRepository(ImageTable, projectName) + val fileInternalRepository = FileInternalRepository(FileTable, projectName) val displayRuleInternalRepository = DisplayRuleInternalRepository(DisplayRuleTable, projectName) val variableInternalRepository = VariableInternalRepository(VariableTable, projectName) val variableStructureInternalRepository = @@ -81,6 +83,7 @@ class Migration(public val config: MigConfig, public val projectConfig: ProjectC val variableStructureRepository = VariableStructureRepository(variableStructureInternalRepository) val displayRuleRepository = DisplayRuleRepository(displayRuleInternalRepository) val imageRepository = ImageRepository(imageInternalRepository) + val fileRepository = FileRepository(fileInternalRepository) this.variableRepository = variableRepository this.documentObjectRepository = documentObjectRepository @@ -89,11 +92,13 @@ class Migration(public val config: MigConfig, public val projectConfig: ProjectC this.variableStructureRepository = variableStructureRepository this.displayRuleRepository = displayRuleRepository this.imageRepository = imageRepository + this.fileRepository = fileRepository this.mappingRepository = MappingRepository( projectName, documentObjectRepository, imageRepository, + fileRepository, textStyleRepository, paragraphStyleRepository, variableRepository, @@ -107,6 +112,7 @@ class Migration(public val config: MigConfig, public val projectConfig: ProjectC repositories.add(variableStructureRepository) repositories.add(displayRuleRepository) repositories.add(imageRepository) + repositories.add(fileRepository) this.referenceValidator = ReferenceValidator( documentObjectInternalRepository, @@ -116,6 +122,7 @@ class Migration(public val config: MigConfig, public val projectConfig: ProjectC variableStructureInternalRepository, displayRuleInternalRepository, imageInternalRepository, + fileInternalRepository, ) val inspireDocumentObjectBuilder: InspireDocumentObjectBuilder @@ -129,6 +136,7 @@ class Migration(public val config: MigConfig, public val projectConfig: ProjectC variableStructureInternalRepository, displayRuleInternalRepository, imageInternalRepository, + fileInternalRepository, projectConfig, ipsService, ) @@ -136,6 +144,7 @@ class Migration(public val config: MigConfig, public val projectConfig: ProjectC InteractiveDeployClient( documentObjectInternalRepository, imageInternalRepository, + fileInternalRepository, statusTrackingRepository, textStyleInternalRepository, paragraphStyleInternalRepository, @@ -153,6 +162,7 @@ class Migration(public val config: MigConfig, public val projectConfig: ProjectC variableStructureInternalRepository, displayRuleInternalRepository, imageInternalRepository, + fileInternalRepository, projectConfig, ipsService, ) @@ -160,6 +170,7 @@ class Migration(public val config: MigConfig, public val projectConfig: ProjectC DesignerDeployClient( documentObjectInternalRepository, imageInternalRepository, + fileInternalRepository, statusTrackingRepository, textStyleInternalRepository, paragraphStyleInternalRepository, diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/File.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/File.kt new file mode 100644 index 00000000..2e7165ef --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/File.kt @@ -0,0 +1,31 @@ +package com.quadient.migration.api.dto.migrationmodel + +import com.quadient.migration.data.FileModel +import com.quadient.migration.shared.FileType +import com.quadient.migration.shared.SkipOptions + +data class File( + override val id: String, + override var name: String?, + override var originLocations: List, + override var customFields: CustomFieldMap, + var sourcePath: String?, + var targetFolder: String?, + var fileType: FileType, + val skip: SkipOptions, +) : MigrationObject { + companion object { + fun fromModel(model: FileModel): File { + return File( + id = model.id, + name = model.name, + originLocations = model.originLocations, + customFields = CustomFieldMap(model.customFields.toMutableMap()), + sourcePath = model.sourcePath, + targetFolder = model.targetFolder?.toString(), + fileType = model.fileType, + skip = model.skip, + ) + } + } +} diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Mapping.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Mapping.kt index b7ac0e90..1aa633bb 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Mapping.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Mapping.kt @@ -33,6 +33,14 @@ sealed class MappingItem { var alternateText: String? = null, ) : MappingItem() + data class File( + override var name: String?, + var targetFolder: String?, + var sourcePath: String?, + var fileType: FileType?, + var skip: SkipOptions? = null, + ) : MappingItem() + data class ParagraphStyle(override var name: String?, var definition: Definition?) : MappingItem() { sealed interface Definition data class Ref(val targetId: String) : Definition @@ -105,6 +113,16 @@ sealed class MappingItem { ) } + is MappingItem.File -> { + MappingItemEntity.File( + name = this.name, + targetFolder = this.targetFolder, + sourcePath = this.sourcePath, + fileType = this.fileType, + skip = this.skip, + ) + } + is MappingItem.ParagraphStyle -> { val def = definition MappingItemEntity.ParagraphStyle( diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Ref.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Ref.kt index ead9b48b..093410a8 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Ref.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Ref.kt @@ -5,6 +5,7 @@ import com.quadient.migration.data.DocumentObjectModelRef import com.quadient.migration.data.FirstMatchModel import com.quadient.migration.data.HyperlinkModel import com.quadient.migration.data.ImageModelRef +import com.quadient.migration.data.FileModelRef import com.quadient.migration.data.ParagraphStyleDefOrRefModel import com.quadient.migration.data.ParagraphStyleDefinitionModel import com.quadient.migration.data.ParagraphStyleModelRef @@ -19,8 +20,8 @@ import com.quadient.migration.data.VariableModelRef import com.quadient.migration.data.VariableStructureModelRef import com.quadient.migration.persistence.migrationmodel.DisplayRuleEntityRef import com.quadient.migration.persistence.migrationmodel.DocumentObjectEntityRef -import com.quadient.migration.persistence.migrationmodel.HyperlinkEntity import com.quadient.migration.persistence.migrationmodel.ImageEntityRef +import com.quadient.migration.persistence.migrationmodel.FileEntityRef import com.quadient.migration.persistence.migrationmodel.ParagraphStyleEntityRef import com.quadient.migration.persistence.migrationmodel.StringEntity import com.quadient.migration.persistence.migrationmodel.TextStyleEntityRef @@ -38,6 +39,7 @@ sealed interface Ref { is ParagraphStyleModelRef -> ParagraphStyleRef.fromModel(model) is DisplayRuleModelRef -> DisplayRuleRef.fromModel(model) is ImageModelRef -> ImageRef.fromModel(model) + is FileModelRef -> FileRef.fromModel(model) is VariableStructureModelRef -> VariableStructureRef.fromModel(model) } } @@ -48,6 +50,7 @@ sealed interface TextContent { fun fromModel(model: TextContentModel) = when (model) { is DocumentObjectModelRef -> DocumentObjectRef.fromModel(model) is ImageModelRef -> ImageRef.fromModel(model) + is FileModelRef -> FileRef.fromModel(model) is StringModel -> StringValue.fromModel(model) is TableModel -> Table.fromModel(model) is VariableModelRef -> VariableRef.fromModel(model) @@ -154,6 +157,15 @@ data class ImageRef(override val id: String) : Ref, DocumentContent, TextContent fun toDb() = ImageEntityRef(id) } +data class FileRef(override val id: String) : Ref, DocumentContent, TextContent { + companion object { + fun fromModel(model: FileModelRef) = FileRef(model.id) + } + + fun toModel() = FileModelRef(id) + fun toDb() = FileEntityRef(id) +} + data class VariableStructureRef(override val id: String) : Ref { companion object { fun fromModel(model: VariableStructureModelRef) = VariableStructureRef(model.id) diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/DocumentContentBuilderBase.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/DocumentContentBuilderBase.kt index fce261c7..ee8aebf6 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/DocumentContentBuilderBase.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/DocumentContentBuilderBase.kt @@ -4,6 +4,7 @@ import com.quadient.migration.api.dto.migrationmodel.DisplayRuleRef import com.quadient.migration.api.dto.migrationmodel.DocumentContent import com.quadient.migration.api.dto.migrationmodel.DocumentObjectRef import com.quadient.migration.api.dto.migrationmodel.ImageRef +import com.quadient.migration.api.dto.migrationmodel.FileRef import com.quadient.migration.api.dto.migrationmodel.builder.documentcontent.SelectByLanguageBuilder /** @@ -74,6 +75,14 @@ interface DocumentContentBuilderBase { this.content.add(ImageRef(imageId)) } as T + /** + * Adds a file reference to the content. + * @param fileId The ID of the file to reference. + * @return This builder instance for method chaining. + */ + fun fileRef(fileId: String): T = apply { + this.content.add(FileRef(fileId)) + } as T /** * Adds a document object reference to the content. diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/FileBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/FileBuilder.kt new file mode 100644 index 00000000..b6c02da2 --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/FileBuilder.kt @@ -0,0 +1,65 @@ +package com.quadient.migration.api.dto.migrationmodel.builder + +import com.quadient.migration.api.dto.migrationmodel.File +import com.quadient.migration.shared.FileType +import com.quadient.migration.shared.SkipOptions + +class FileBuilder(id: String) : DtoBuilderBase(id) { + var sourcePath: String? = null + var targetFolder: String? = null + var fileType: FileType = FileType.Document + var skip = false + var placeholder: String? = null + var reason: String? = null + + /** + * Sets source path of the file. This path is relative to the storage root folder. + * @param sourcePath the source path of the file + * @return the builder instance for chaining + */ + fun sourcePath(sourcePath: String) = apply { this.sourcePath = sourcePath } + + /** + * Sets target folder for the file. This is additional folder where the file will be deployed. + * Supports nesting, e.g. "folder1/folder2". + * @param targetFolder the target folder for the file + * @return the builder instance for chaining + */ + fun targetFolder(targetFolder: String) = apply { this.targetFolder = targetFolder } + + /** + * Sets the file type (Document or Attachment). Defaults to Document if not specified. + * @param fileType the type of the file + * @return the builder instance for chaining + */ + fun fileType(fileType: FileType) = apply { this.fileType = fileType } + + /** + * Marks the file to be skipped during deployment. + * @param placeholder optional placeholder value to use instead + * @param reason optional reason for skipping + * @return the builder instance for chaining + */ + fun skip(placeholder: String? = null, reason: String? = null) = apply { + this.skip = true + this.placeholder = placeholder + this.reason = reason + } + + /** + * Builds the File instance with the provided properties. + * @return the built File instance + */ + override fun build(): File { + return File( + id = id, + name = name, + originLocations = originLocations, + customFields = customFields, + sourcePath = sourcePath, + targetFolder = targetFolder, + fileType = fileType, + skip = SkipOptions(skipped = skip, reason = reason, placeholder = placeholder), + ) + } +} diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/documentcontent/ParagraphBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/documentcontent/ParagraphBuilder.kt index eb4eb93a..5e37a70a 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/documentcontent/ParagraphBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/documentcontent/ParagraphBuilder.kt @@ -4,6 +4,7 @@ import com.quadient.migration.api.dto.migrationmodel.DisplayRuleRef import com.quadient.migration.api.dto.migrationmodel.DocumentObjectRef import com.quadient.migration.api.dto.migrationmodel.Hyperlink import com.quadient.migration.api.dto.migrationmodel.ImageRef +import com.quadient.migration.api.dto.migrationmodel.FileRef import com.quadient.migration.api.dto.migrationmodel.Paragraph import com.quadient.migration.api.dto.migrationmodel.ParagraphStyleRef import com.quadient.migration.api.dto.migrationmodel.StringValue @@ -274,6 +275,24 @@ class ParagraphBuilder { content.add(ref) } + /** + * Adds a file reference to the text content. + * @param fileId The ID of the file to reference. + * @return The current instance of [TextBuilder] for method chaining. + */ + fun fileRef(fileId: String) = apply { + content.add(FileRef(fileId)) + } + + /** + * Adds a file reference to the text content. + * @param ref The file reference to add. + * @return The current instance of [TextBuilder] for method chaining. + */ + fun fileRef(ref: FileRef) = apply { + content.add(ref) + } + /** * Adds an inline hyperlink to the text content. * @param url The URL to link to (mandatory). diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/documentcontent/DocumentContent.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/documentcontent/DocumentContent.kt index bdd33a44..9a20b4e2 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/documentcontent/DocumentContent.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/documentcontent/DocumentContent.kt @@ -5,6 +5,7 @@ import com.quadient.migration.data.DocumentObjectModelRef import com.quadient.migration.data.FirstMatchModel import com.quadient.migration.data.AreaModel import com.quadient.migration.data.ImageModelRef +import com.quadient.migration.data.FileModelRef import com.quadient.migration.data.ParagraphModel import com.quadient.migration.data.SelectByLanguageModel import com.quadient.migration.data.TableModel @@ -17,6 +18,7 @@ sealed interface DocumentContent { is ParagraphModel -> Paragraph.fromModel(model) is DocumentObjectModelRef -> DocumentObjectRef.fromModel(model) is ImageModelRef -> ImageRef.fromModel(model) + is FileModelRef -> FileRef.fromModel(model) is AreaModel -> Area.fromModel(model) is FirstMatchModel -> FirstMatch.fromModel(model) is SelectByLanguageModel -> SelectByLanguage.fromModel(model) @@ -31,6 +33,7 @@ fun List.toDb(): List { is Paragraph -> it.toDb() is DocumentObjectRef -> it.toDb() is ImageRef -> it.toDb() + is FileRef -> it.toDb() is Area -> it.toDb() is FirstMatch -> it.toDb() is SelectByLanguage -> it.toDb() diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/documentcontent/Paragraph.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/documentcontent/Paragraph.kt index 7a1b9c51..71ac61c5 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/documentcontent/Paragraph.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/documentcontent/Paragraph.kt @@ -1,6 +1,7 @@ package com.quadient.migration.api.dto.migrationmodel import com.quadient.migration.data.DocumentObjectModelRef +import com.quadient.migration.data.FileModelRef import com.quadient.migration.data.FirstMatchModel import com.quadient.migration.data.HyperlinkModel import com.quadient.migration.data.ImageModelRef @@ -31,6 +32,7 @@ data class Paragraph( is DocumentObjectModelRef -> DocumentObjectRef.fromModel(textContent) is TableModel -> Table.fromModel(textContent) is ImageModelRef -> ImageRef.fromModel(textContent) + is FileModelRef -> FileRef.fromModel(textContent) is FirstMatchModel -> FirstMatch.fromModel(textContent) is HyperlinkModel -> Hyperlink.fromModel(textContent) } @@ -74,6 +76,7 @@ data class Paragraph( is Table -> textContent.toDb() is DocumentObjectRef -> textContent.toDb() is ImageRef -> textContent.toDb() + is FileRef -> textContent.toDb() is StringValue -> textContent.toDb() is VariableRef -> textContent.toDb() is FirstMatch -> textContent.toDb() diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/repository/FileRepository.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/repository/FileRepository.kt new file mode 100644 index 00000000..f39b2c31 --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/repository/FileRepository.kt @@ -0,0 +1,97 @@ +package com.quadient.migration.api.repository + +import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap +import com.quadient.migration.api.dto.migrationmodel.DocumentObject +import com.quadient.migration.api.dto.migrationmodel.File +import com.quadient.migration.api.dto.migrationmodel.MigrationObject +import com.quadient.migration.data.FileModel +import com.quadient.migration.persistence.repository.FileInternalRepository +import com.quadient.migration.persistence.table.DocumentObjectTable +import com.quadient.migration.persistence.table.FileTable +import com.quadient.migration.service.deploy.ResourceType +import com.quadient.migration.tools.concat +import kotlinx.datetime.Clock +import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.jdbc.transactions.transaction +import org.jetbrains.exposed.v1.jdbc.upsertReturning + +class FileRepository(internalRepository: FileInternalRepository) : Repository(internalRepository) { + val statusTrackingRepository = StatusTrackingRepository(internalRepository.projectName) + + override fun toDto(model: FileModel): File { + return File( + id = model.id, + name = model.name, + originLocations = model.originLocations, + customFields = CustomFieldMap(model.customFields.toMutableMap()), + sourcePath = model.sourcePath, + targetFolder = model.targetFolder?.toString(), + fileType = model.fileType, + skip = model.skip, + ) + } + + override fun findUsages(id: String): List { + return transaction { + DocumentObjectTable.selectAll().where { DocumentObjectTable.projectName eq internalRepository.projectName } + .map { DocumentObjectTable.fromResultRow(it) }.filter { it.collectRefs().any { it.id == id } } + .map { DocumentObject.fromModel(it) }.distinct() + } + } + + override fun upsert(dto: File) { + internalRepository.upsert { + val existingItem = + internalRepository.table.selectAll().where(internalRepository.filter(dto.id)).firstOrNull() + ?.let { internalRepository.toModel(it) } + + val now = Clock.System.now() + + if (existingItem == null) { + statusTrackingRepository.active(dto.id, ResourceType.File) + } + + internalRepository.table.upsertReturning( + internalRepository.table.id, internalRepository.table.projectName + ) { + it[FileTable.id] = dto.id + it[FileTable.projectName] = internalRepository.projectName + it[FileTable.name] = dto.name + it[FileTable.originLocations] = existingItem?.originLocations.concat(dto.originLocations).distinct() + it[FileTable.customFields] = dto.customFields.inner + it[FileTable.created] = existingItem?.created ?: now + it[FileTable.lastUpdated] = now + it[FileTable.sourcePath] = dto.sourcePath + it[FileTable.targetFolder] = dto.targetFolder + it[FileTable.fileType] = dto.fileType.name + it[FileTable.skip] = dto.skip + }.first() + } + } + + override fun upsertBatch(dtos: Collection) { + internalRepository.upsertBatch(dtos) { dto -> + val existingItem = + internalRepository.table.selectAll().where(internalRepository.filter(dto.id)).firstOrNull() + ?.let { internalRepository.toModel(it) } + + val now = Clock.System.now() + + if (existingItem == null) { + statusTrackingRepository.active(dto.id, ResourceType.File) + } + + this[FileTable.id] = dto.id + this[FileTable.projectName] = internalRepository.projectName + this[FileTable.name] = dto.name + this[FileTable.originLocations] = existingItem?.originLocations.concat(dto.originLocations).distinct() + this[FileTable.customFields] = dto.customFields.inner + this[FileTable.created] = existingItem?.created ?: now + this[FileTable.lastUpdated] = now + this[FileTable.sourcePath] = dto.sourcePath + this[FileTable.targetFolder] = dto.targetFolder + this[FileTable.fileType] = dto.fileType.name + this[FileTable.skip] = dto.skip + } + } +} diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/repository/MappingRepository.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/repository/MappingRepository.kt index a7ab3ded..6387abf0 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/repository/MappingRepository.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/repository/MappingRepository.kt @@ -11,6 +11,7 @@ class MappingRepository( private val projectName: String, private val documentObjectRepository: DocumentObjectRepository, private val imageRepository: ImageRepository, + private val fileRepository: FileRepository, private val textStyleRepository: TextStyleRepository, private val paragraphStyleRepository: ParagraphStyleRepository, private val variableRepository: VariableRepository, @@ -27,6 +28,7 @@ class MappingRepository( when (mapping.mapping) { is MappingItem.DocumentObject -> applyDocumentObjectMapping(mapping.id) is MappingItem.Image -> applyImageMapping(mapping.id) + is MappingItem.File -> applyFileMapping(mapping.id) is MappingItem.TextStyle -> applyTextStyleMapping(mapping.id) is MappingItem.ParagraphStyle -> applyParagraphStyleMapping(mapping.id) is MappingItem.Variable -> applyVariableMapping(mapping.id) @@ -103,6 +105,27 @@ class MappingRepository( imageRepository.upsert(mapping.apply(img)) } + fun getFileMapping(id: String): MappingItem.File { + return (internalRepository.find(id) ?: MappingItemEntity.File( + name = null, + targetFolder = null, + sourcePath = null, + fileType = null, + skip = null, + )).toDto() as MappingItem.File + } + + fun applyFileMapping(id: String) { + val mapping = internalRepository.find(id) + val file = fileRepository.find(id) + + if (mapping == null || file == null) { + return + } + + fileRepository.upsert(mapping.apply(file)) + } + fun getTextStyleMapping(id: String): MappingItem.TextStyle { return (internalRepository.find(id) ?: MappingItemEntity.TextStyle( name = null, diff --git a/migration-library/src/main/kotlin/com/quadient/migration/data/FileModel.kt b/migration-library/src/main/kotlin/com/quadient/migration/data/FileModel.kt new file mode 100644 index 00000000..15c7846c --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/data/FileModel.kt @@ -0,0 +1,23 @@ +package com.quadient.migration.data + +import com.quadient.migration.service.RefValidatable +import com.quadient.migration.shared.FileType +import com.quadient.migration.shared.IcmPath +import com.quadient.migration.shared.SkipOptions +import kotlinx.datetime.Instant + +data class FileModel( + override val id: String, + override val name: String?, + override val originLocations: List, + override val customFields: Map, + override val created: Instant, + val sourcePath: String?, + val targetFolder: IcmPath?, + val fileType: FileType, + val skip: SkipOptions, +) : RefValidatable, MigrationObjectModel { + override fun collectRefs(): List { + return emptyList() + } +} diff --git a/migration-library/src/main/kotlin/com/quadient/migration/data/RefModel.kt b/migration-library/src/main/kotlin/com/quadient/migration/data/RefModel.kt index dd285f51..7f5cd0d6 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/data/RefModel.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/data/RefModel.kt @@ -5,6 +5,7 @@ import com.quadient.migration.persistence.migrationmodel.DocumentObjectEntityRef import com.quadient.migration.persistence.migrationmodel.FirstMatchEntity import com.quadient.migration.persistence.migrationmodel.HyperlinkEntity import com.quadient.migration.persistence.migrationmodel.ImageEntityRef +import com.quadient.migration.persistence.migrationmodel.FileEntityRef import com.quadient.migration.persistence.migrationmodel.ParagraphStyleDefOrRefEntity import com.quadient.migration.persistence.migrationmodel.ParagraphStyleDefinitionEntity import com.quadient.migration.persistence.migrationmodel.ParagraphStyleEntityRef @@ -29,6 +30,7 @@ sealed interface TextContentModel { is TableEntity -> TableModel.fromDb(entity) is DocumentObjectEntityRef -> DocumentObjectModelRef.fromDb(entity) is ImageEntityRef -> ImageModelRef.fromDb(entity) + is FileEntityRef -> FileModelRef.fromDb(entity) is FirstMatchEntity -> FirstMatchModel.fromDb(entity) is HyperlinkEntity -> HyperlinkModel.fromDb(entity) } @@ -98,6 +100,16 @@ data class ImageModelRef(override val id: String) : RefModel, DocumentContentMod } } +data class FileModelRef(override val id: String) : RefModel, DocumentContentModel, TextContentModel { + override fun collectRefs(): List { + return listOf(this) + } + + companion object { + fun fromDb(entity: FileEntityRef) = FileModelRef(entity.id) + } +} + data class VariableStructureModelRef(override val id: String) : RefModel { companion object { fun fromDb(entity: VariableStructureEntityRef) = VariableStructureModelRef(entity.id) diff --git a/migration-library/src/main/kotlin/com/quadient/migration/data/documentcontent/DocumentContentModel.kt b/migration-library/src/main/kotlin/com/quadient/migration/data/documentcontent/DocumentContentModel.kt index 8771de23..540da3d7 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/data/documentcontent/DocumentContentModel.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/data/documentcontent/DocumentContentModel.kt @@ -2,6 +2,7 @@ package com.quadient.migration.data import com.quadient.migration.persistence.migrationmodel.DocumentContentEntity import com.quadient.migration.persistence.migrationmodel.DocumentObjectEntityRef +import com.quadient.migration.persistence.migrationmodel.FileEntityRef import com.quadient.migration.persistence.migrationmodel.FirstMatchEntity import com.quadient.migration.persistence.migrationmodel.AreaEntity import com.quadient.migration.persistence.migrationmodel.ImageEntityRef @@ -17,6 +18,7 @@ sealed interface DocumentContentModel : RefValidatable { is ParagraphEntity -> ParagraphModel.fromDb(entity) is DocumentObjectEntityRef -> DocumentObjectModelRef.fromDb(entity) is ImageEntityRef -> ImageModelRef.fromDb(entity) + is FileEntityRef -> FileModelRef.fromDb(entity) is AreaEntity -> AreaModel.fromDb(entity) is FirstMatchEntity -> FirstMatchModel.fromDb(entity) is SelectByLanguageEntity -> SelectByLanguageModel.fromDb(entity) diff --git a/migration-library/src/main/kotlin/com/quadient/migration/data/documentcontent/ParagraphModel.kt b/migration-library/src/main/kotlin/com/quadient/migration/data/documentcontent/ParagraphModel.kt index 2304d782..d6232ca0 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/data/documentcontent/ParagraphModel.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/data/documentcontent/ParagraphModel.kt @@ -46,6 +46,7 @@ data class ParagraphModel( is TableModel -> it.collectRefs() is DocumentObjectModelRef -> listOf(it) is ImageModelRef -> listOf(it) + is FileModelRef -> listOf(it) is FirstMatchModel -> it.collectRefs() } }.flatten().toMutableList() diff --git a/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/MappingEntity.kt b/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/MappingEntity.kt index 483a3604..1daf4ad1 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/MappingEntity.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/MappingEntity.kt @@ -13,6 +13,7 @@ import org.jetbrains.exposed.v1.dao.CompositeEntity import org.jetbrains.exposed.v1.dao.CompositeEntityClass import com.quadient.migration.api.dto.migrationmodel.DocumentObject as DocumentObjectDto import com.quadient.migration.api.dto.migrationmodel.Image as ImageDto +import com.quadient.migration.api.dto.migrationmodel.File as FileDto import com.quadient.migration.api.dto.migrationmodel.ParagraphStyle as ParagraphStyleDto import com.quadient.migration.api.dto.migrationmodel.TextStyle as TextStyleDto import com.quadient.migration.api.dto.migrationmodel.Variable as VariableDto @@ -105,6 +106,25 @@ sealed class MappingItemEntity { } } + @Serializable + data class File( + override val name: String?, + val targetFolder: String?, + val sourcePath: String?, + val fileType: FileType?, + var skip: SkipOptions? = null, + ) : MappingItemEntity() { + fun apply(item: FileDto): FileDto { + return item.copy( + name = name, + targetFolder = targetFolder, + sourcePath = sourcePath, + fileType = fileType ?: item.fileType, + skip = skip ?: SkipOptions(false, null, null), + ) + } + } + @Serializable data class ParagraphStyle(override val name: String?, val definition: Definition?) : MappingItemEntity() { @Serializable @@ -308,6 +328,16 @@ sealed class MappingItemEntity { ) } + is File -> { + MappingItem.File( + name = this.name, + targetFolder = this.targetFolder, + sourcePath = this.sourcePath, + fileType = this.fileType, + skip = this.skip, + ) + } + is ParagraphStyle -> { MappingItem.ParagraphStyle( name = this.name, definition = when (definition) { diff --git a/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/RefEntity.kt b/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/RefEntity.kt index 7e66ae95..176f23b7 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/RefEntity.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/RefEntity.kt @@ -33,6 +33,9 @@ data class DisplayRuleEntityRef(val id: String) @Serializable data class ImageEntityRef(val id: String) : RefEntity, DocumentContentEntity, TextContentEntity +@Serializable +data class FileEntityRef(val id: String) : RefEntity, DocumentContentEntity, TextContentEntity + @Serializable data class VariableStructureEntityRef(val id: String) : RefEntity diff --git a/migration-library/src/main/kotlin/com/quadient/migration/persistence/repository/FileInternalRepository.kt b/migration-library/src/main/kotlin/com/quadient/migration/persistence/repository/FileInternalRepository.kt new file mode 100644 index 00000000..01e8bf4e --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/persistence/repository/FileInternalRepository.kt @@ -0,0 +1,26 @@ +package com.quadient.migration.persistence.repository + +import com.quadient.migration.data.FileModel +import com.quadient.migration.persistence.table.FileTable +import com.quadient.migration.persistence.table.MigrationObjectTable +import com.quadient.migration.shared.FileType +import com.quadient.migration.shared.IcmPath +import org.jetbrains.exposed.v1.core.ResultRow + +class FileInternalRepository( + table: MigrationObjectTable, projectName: String +) : InternalRepository(table, projectName) { + override fun toModel(row: ResultRow): FileModel { + return FileModel( + id = row[table.id].value, + name = row[table.name], + originLocations = row[table.originLocations], + customFields = row[table.customFields], + created = row[table.created], + sourcePath = row[FileTable.sourcePath], + targetFolder = row[FileTable.targetFolder]?.let(IcmPath::from), + fileType = FileType.valueOf(row[FileTable.fileType]), + skip = row[FileTable.skip], + ) + } +} diff --git a/migration-library/src/main/kotlin/com/quadient/migration/persistence/table/FileTable.kt b/migration-library/src/main/kotlin/com/quadient/migration/persistence/table/FileTable.kt new file mode 100644 index 00000000..d59d9150 --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/persistence/table/FileTable.kt @@ -0,0 +1,13 @@ +package com.quadient.migration.persistence.table + +import com.quadient.migration.shared.FileType +import com.quadient.migration.shared.SkipOptions +import kotlinx.serialization.json.Json +import org.jetbrains.exposed.v1.json.jsonb + +object FileTable : MigrationObjectTable("file") { + val sourcePath = varchar("source_path", 255).nullable() + val targetFolder = varchar("target_folder", 255).nullable() + val fileType = varchar("file_type", 50).default(FileType.Document.name) + val skip = jsonb("skip", Json) +} diff --git a/migration-library/src/main/kotlin/com/quadient/migration/persistence/upgrade/V10__add_file_table.kt b/migration-library/src/main/kotlin/com/quadient/migration/persistence/upgrade/V10__add_file_table.kt new file mode 100644 index 00000000..b08c5af6 --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/persistence/upgrade/V10__add_file_table.kt @@ -0,0 +1,15 @@ +package com.quadient.migration.persistence.upgrade + +import com.quadient.migration.persistence.table.FileTable +import org.flywaydb.core.api.migration.BaseJavaMigration +import org.flywaydb.core.api.migration.Context +import org.jetbrains.exposed.v1.jdbc.SchemaUtils +import org.jetbrains.exposed.v1.jdbc.transactions.transaction + +class V10__add_file_table : BaseJavaMigration() { + override fun migrate(context: Context) { + transaction { + SchemaUtils.create(FileTable) + } + } +} 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 3a8da5f2..fdd3fe9b 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 @@ -3,6 +3,7 @@ package com.quadient.migration.service import com.quadient.migration.data.DisplayRuleModelRef import com.quadient.migration.data.DocumentObjectModelRef import com.quadient.migration.data.ImageModelRef +import com.quadient.migration.data.FileModelRef import com.quadient.migration.data.ParagraphStyleModelRef import com.quadient.migration.data.RefModel import com.quadient.migration.data.TextStyleModelRef @@ -11,6 +12,7 @@ import com.quadient.migration.data.VariableStructureModelRef import com.quadient.migration.persistence.repository.DisplayRuleInternalRepository import com.quadient.migration.persistence.repository.DocumentObjectInternalRepository import com.quadient.migration.persistence.repository.ImageInternalRepository +import com.quadient.migration.persistence.repository.FileInternalRepository import com.quadient.migration.persistence.repository.ParagraphStyleInternalRepository import com.quadient.migration.persistence.repository.TextStyleInternalRepository import com.quadient.migration.persistence.repository.VariableInternalRepository @@ -28,6 +30,7 @@ class ReferenceValidator( private val variableStructureRepository: VariableStructureInternalRepository, private val displayRuleRepository: DisplayRuleInternalRepository, private val imageRepository: ImageInternalRepository, + private val fileRepository: FileInternalRepository, ) { /** * Validates all objects in the database. @@ -43,10 +46,11 @@ class ReferenceValidator( val dataStructures = variableStructureRepository.listAllModel() val displayRules = displayRuleRepository.listAllModel() val images = imageRepository.listAllModel() + val files = fileRepository.listAllModel() val alreadyValidatedRefs = mutableSetOf() val missingRefs = - (documentObjects + variables + paragraphStyles + textStyles + dataStructures + displayRules + images).mapNotNull { + (documentObjects + variables + paragraphStyles + textStyles + dataStructures + displayRules + images + files).mapNotNull { validate(it, alreadyValidatedRefs).missingRefs.ifEmpty { null } }.flatten() @@ -141,6 +145,18 @@ class ReferenceValidator( } } + is FileModelRef -> { + val file = fileRepository.findModel(current.id) + + if (file != null) { + validatedRefs.add(current) + alreadyValidRefs.add(current) + queue.addAll(file.collectRefs()) + } else { + missingRefs.add(current) + } + } + is VariableStructureModelRef -> { val variableStructure = variableStructureRepository.findModel(current.id) diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/StylesValidator.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/StylesValidator.kt index 4ea4b689..74f5693a 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/StylesValidator.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/StylesValidator.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.dataformat.xml.XmlMapper import com.quadient.migration.data.DisplayRuleModelRef import com.quadient.migration.data.DocumentObjectModel import com.quadient.migration.data.DocumentObjectModelRef +import com.quadient.migration.data.FileModelRef import com.quadient.migration.data.ImageModelRef import com.quadient.migration.data.ParagraphStyleDefinitionModel import com.quadient.migration.data.ParagraphStyleModel @@ -73,6 +74,7 @@ class StylesValidator( is DisplayRuleModelRef -> {} is DocumentObjectModelRef -> {} is ImageModelRef -> {} + is FileModelRef -> {} is VariableModelRef -> {} is VariableStructureModelRef -> {} } 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 88ef75e9..45f00d8e 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DeployClient.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DeployClient.kt @@ -9,6 +9,8 @@ import com.quadient.migration.data.Deployed import com.quadient.migration.data.DisplayRuleModelRef import com.quadient.migration.data.DocumentObjectModel import com.quadient.migration.data.DocumentObjectModelRef +import com.quadient.migration.data.FileModel +import com.quadient.migration.data.FileModelRef import com.quadient.migration.data.ImageModel import com.quadient.migration.data.ImageModelRef import com.quadient.migration.data.ParagraphStyleModelRef @@ -18,6 +20,7 @@ import com.quadient.migration.data.VariableModelRef import com.quadient.migration.data.VariableStructureModelRef import com.quadient.migration.persistence.repository.DocumentObjectInternalRepository import com.quadient.migration.persistence.repository.ImageInternalRepository +import com.quadient.migration.persistence.repository.FileInternalRepository import com.quadient.migration.persistence.repository.ParagraphStyleInternalRepository import com.quadient.migration.persistence.repository.TextStyleInternalRepository import com.quadient.migration.service.Storage @@ -45,6 +48,7 @@ data class DocObjectWithRef(val obj: DocumentObjectModel, val documentObjectRefs sealed class DeployClient( protected val documentObjectRepository: DocumentObjectInternalRepository, protected val imageRepository: ImageInternalRepository, + protected val fileRepository: FileInternalRepository, protected val statusTrackingRepository: StatusTrackingRepository, protected val textStyleRepository: TextStyleInternalRepository, protected val paragraphStyleRepository: ParagraphStyleInternalRepository, @@ -228,92 +232,153 @@ sealed class DeployClient( return deployOrder } - protected fun deployImages(documentObjects: List, deploymentId: Uuid, deploymentTimestamp: Instant): DeploymentResult { + protected fun deployImagesAndFiles(documentObjects: List, deploymentId: Uuid, deploymentTimestamp: Instant): DeploymentResult { val deploymentResult = DeploymentResult(deploymentId) val tracker = ResultTracker(statusTrackingRepository, deploymentResult, deploymentId, deploymentTimestamp, output) - val uniqueImageRefs = documentObjects.flatMap { + val allRefs = documentObjects.map { try { - it.getAllDocumentObjectImageRefs() + it.getAllDocumentObjectImageAndFileRefs() } catch (e: IllegalStateException) { deploymentResult.errors.add(DeploymentError(it.id, e.message ?: "")) - emptyList() + Pair(emptyList(), emptyList()) } - }.distinct() + } + + val imageRefs = allRefs.flatMap { pair -> pair.first }.distinct() + val fileRefs = allRefs.flatMap { pair -> pair.second }.distinct() - for (imageRef in uniqueImageRefs) { - if (!shouldDeployObject(imageRef.id, ResourceType.Image, imageRef.id, deploymentResult)) { - logger.info("Skipping deployment of '${imageRef.id}' as it is not marked for deployment.") - continue - } + for (imageRef in imageRefs) { + deployImage(imageRef, deploymentResult, tracker) + } - val imageModel = imageRepository.findModel(imageRef.id) - if (imageModel == null) { - val message = "Image '${imageRef.id}' not found." - logger.error(message) - tracker.errorImage(imageRef.id, null, message) - continue - } + for (fileRef in fileRefs) { + deployFile(fileRef, deploymentResult, tracker) + } - val icmImagePath = documentObjectBuilder.getImagePath(imageModel) + return deploymentResult + } - val invalidMetadata = imageModel.getInvalidMetadataKeys() - if (invalidMetadata.isNotEmpty()) { - logger.error("Failed to deploy '$icmImagePath' due to invalid metadata.") - val keys = invalidMetadata.joinToString(", ", prefix = "[", postfix = "]") - val message = "Metadata of image '${imageModel.id}' contains invalid keys: $keys" - tracker.errorImage(imageModel.id, icmImagePath, message) - continue - } + private fun deployImage(imageRef: ImageModelRef, deploymentResult: DeploymentResult, tracker: ResultTracker) { + if (!shouldDeployObject(imageRef.id, ResourceType.Image, imageRef.id, deploymentResult)) { + logger.info("Skipping deployment of '${imageRef.id}' as it is not marked for deployment.") + return + } + val imageModel = imageRepository.findModel(imageRef.id) + if (imageModel == null) { + val message = "Image '${imageRef.id}' not found." + logger.error(message) + tracker.errorImage(imageRef.id, null, message) + return + } - if (imageModel.imageType == ImageType.Unknown) { - val message = "Skipping deployment of image '${imageModel.nameOrId()}' due to unknown image type." - logger.warn(message) - tracker.warningImage(imageModel.id, icmImagePath, message) - continue - } + val icmImagePath = documentObjectBuilder.getImagePath(imageModel) - if (imageModel.skip.skipped) { - val reason = imageModel.skip.reason?.let { " Reason: $it" } ?: "" - val message = "Image '${imageModel.nameOrId()}' is skipped.$reason" - logger.warn(message) - tracker.warningImage(imageModel.id, icmImagePath, message) - continue - } + val invalidMetadata = imageModel.getInvalidMetadataKeys() + if (invalidMetadata.isNotEmpty()) { + logger.error("Failed to deploy '$icmImagePath' due to invalid metadata.") + val keys = invalidMetadata.joinToString(", ", prefix = "[", postfix = "]") + val message = "Metadata of image '${imageModel.id}' contains invalid keys: $keys" + tracker.errorImage(imageModel.id, icmImagePath, message) + return + } + if (imageModel.imageType == ImageType.Unknown) { + val message = "Skipping deployment of image '${imageModel.nameOrId()}' due to unknown image type." + logger.warn(message) + tracker.warningImage(imageModel.id, icmImagePath, message) + return + } - if (imageModel.sourcePath.isNullOrBlank()) { - val message = "Skipping deployment of image '${imageModel.nameOrId()}' due to missing source path." - logger.warn(message) - tracker.warningImage(imageModel.id, icmImagePath, message) - continue - } + if (imageModel.skip.skipped) { + val reason = imageModel.skip.reason?.let { " Reason: $it" } ?: "" + val message = "Image '${imageModel.nameOrId()}' is skipped.$reason" + logger.warn(message) + tracker.warningImage(imageModel.id, icmImagePath, message) + return + } - logger.debug("Starting deployment of image '${imageModel.nameOrId()}'.") - val readResult = readStorageSafely(imageModel.sourcePath) - if (readResult is ReadResult.Error) { - val message = "Error while reading image source data: ${readResult.errorMessage}." - logger.error(message) - tracker.errorImage(imageModel.id, icmImagePath, message) - continue - } + if (imageModel.sourcePath.isNullOrBlank()) { + val message = "Skipping deployment of image '${imageModel.nameOrId()}' due to missing source path." + logger.warn(message) + tracker.warningImage(imageModel.id, icmImagePath, message) + return + } - val imageData = (readResult as ReadResult.Success).result + logger.debug("Starting deployment of image '${imageModel.nameOrId()}'.") + val readResult = readStorageSafely(imageModel.sourcePath) + if (readResult is ReadResult.Error) { + val message = "Error while reading image source data: ${readResult.errorMessage}." + logger.error(message) + tracker.errorImage(imageModel.id, icmImagePath, message) + return + } - logger.trace("Loaded image data of size ${imageData.size} from storage.") + val imageData = (readResult as ReadResult.Success).result + logger.trace("Loaded image data of size ${imageData.size} from storage.") - val uploadResult = ipsService.tryUpload(icmImagePath, imageData) - if (uploadResult is OperationResult.Failure) { - tracker.errorImage(imageModel.id, icmImagePath, uploadResult.message) - continue - } + val uploadResult = ipsService.tryUpload(icmImagePath, imageData) + if (uploadResult is OperationResult.Failure) { + tracker.errorImage(imageModel.id, icmImagePath, uploadResult.message) + return + } - logger.debug("Deployment of image '${imageModel.nameOrId()}' to '${icmImagePath}' is successful.") - tracker.deployedImage(imageModel.id, icmImagePath) + logger.debug("Deployment of image '${imageModel.nameOrId()}' to '${icmImagePath}' is successful.") + tracker.deployedImage(imageModel.id, icmImagePath) + } + + private fun deployFile(fileRef: FileModelRef, deploymentResult: DeploymentResult, tracker: ResultTracker) { + if (!shouldDeployObject(fileRef.id, ResourceType.File, fileRef.id, deploymentResult)) { + logger.info("Skipping deployment of file '${fileRef.id}' as it is not marked for deployment.") + return } - return deploymentResult + val fileModel = fileRepository.findModel(fileRef.id) + if (fileModel == null) { + val message = "File '${fileRef.id}' not found." + logger.error(message) + tracker.errorFile(fileRef.id, null, message) + return + } + + val icmFilePath = documentObjectBuilder.getFilePath(fileModel) + + if (fileModel.skip.skipped) { + val reason = fileModel.skip.reason?.let { " Reason: $it" } ?: "" + val message = "File '${fileModel.nameOrId()}' is skipped.$reason" + logger.warn(message) + tracker.warningFile(fileModel.id, icmFilePath, message) + return + } + + if (fileModel.sourcePath.isNullOrBlank()) { + val message = "Skipping deployment of file '${fileModel.nameOrId()}' due to missing source path." + logger.warn(message) + tracker.warningFile(fileModel.id, icmFilePath, message) + return + } + + logger.debug("Starting deployment of file '${fileModel.nameOrId()}'.") + val readResult = readStorageSafely(fileModel.sourcePath) + if (readResult is ReadResult.Error) { + val message = "Error while reading file source data: ${readResult.errorMessage}." + logger.error(message) + tracker.errorFile(fileModel.id, icmFilePath, message) + return + } + + val fileData = (readResult as ReadResult.Success).result + logger.trace("Loaded file data of size ${fileData.size} from storage.") + + val uploadResult = ipsService.tryUpload(icmFilePath, fileData) + if (uploadResult is OperationResult.Failure) { + tracker.errorFile(fileModel.id, icmFilePath, uploadResult.message) + return + } + + logger.debug("Deployment of file '${fileModel.nameOrId()}' to '${icmFilePath}' is successful.") + tracker.deployedFile(fileModel.id, icmFilePath) } fun progressReport(deployId: Uuid? = null): ProgressReport { @@ -410,6 +475,26 @@ sealed class DeployClient( img } + is FileModelRef -> { + val file = fileRepository.findModelOrFail(ref.id) + val nextIcmPath = documentObjectBuilder.getFilePath(file) + val deployKind = file.getDeployKind(nextIcmPath) + val lastStatus = file.getLastStatus(lastDeployment) + + report.addFile( + id = file.id, + file = file, + deploymentId = lastStatus.deployId, + deployTimestamp = lastStatus.deployTimestamp, + previousIcmPath = lastStatus.icmPath, + nextIcmPath = nextIcmPath, + lastStatus = lastStatus, + deployKind = deployKind, + errorMessage = lastStatus.errorMessage, + ) + file + } + is TextStyleModelRef -> null is ParagraphStyleModelRef -> null is DisplayRuleModelRef -> null @@ -469,6 +554,7 @@ sealed class DeployClient( when (ref) { is DisplayRuleModelRef, is TextStyleModelRef, is ParagraphStyleModelRef, is VariableModelRef, is VariableStructureModelRef -> {} is ImageModelRef -> {} + is FileModelRef -> {} is DocumentObjectModelRef -> { val model = documentObjectRepository.findModelOrFail(ref.id) if (shouldIncludeDependency(model)) { @@ -481,23 +567,29 @@ sealed class DeployClient( return dependencies } - private fun DocumentObjectModel.getAllDocumentObjectImageRefs(): List { - return this.collectRefs().flatMap { ref -> + private fun DocumentObjectModel.getAllDocumentObjectImageAndFileRefs(): Pair, List> { + val images = mutableListOf() + val files = mutableListOf() + + this.collectRefs().forEach { ref -> when (ref) { - is DisplayRuleModelRef, is TextStyleModelRef, is ParagraphStyleModelRef, is VariableModelRef, is VariableStructureModelRef -> emptyList() - is ImageModelRef -> listOf(ref) + is DisplayRuleModelRef, is TextStyleModelRef, is ParagraphStyleModelRef, is VariableModelRef, is VariableStructureModelRef -> {} + is ImageModelRef -> images.add(ref) + is FileModelRef -> files.add(ref) is DocumentObjectModelRef -> { val model = documentObjectRepository.findModel(ref.id) - ?: error("Unable to collect image references because inner document object '${ref.id}' was not found.") + ?: error("Unable to collect image or file references because inner document object '${ref.id}' was not found.") if (documentObjectBuilder.shouldIncludeInternalDependency(model)) { - model.getAllDocumentObjectImageRefs() - } else { - emptyList() + val (nestedImages, nestedFiles) = model.getAllDocumentObjectImageAndFileRefs() + images.addAll(nestedImages) + files.addAll(nestedFiles) } } } } + + return Pair(images, files) } private fun getLastDeployEvent(): LastDeployment? { @@ -577,6 +669,17 @@ sealed class DeployClient( ) } + private fun FileModel.getLastStatus(lastDeployment: LastDeployment?): LastStatus { + return getLastStatus( + id = this.id, + lastDeployment = lastDeployment, + resourceType = ResourceType.File, + output = output, + internal = false, + isPage = false + ) + } + private fun DocumentObjectModel.getDeployKind(nextIcmPath: String?): DeployKind { return getDeployKind( this.id, @@ -592,6 +695,10 @@ sealed class DeployClient( return getDeployKind(this.id, ResourceType.Image, output, false, nextIcmPath) } + private fun FileModel.getDeployKind(nextIcmPath: String?): DeployKind { + return getDeployKind(this.id, ResourceType.File, output, false, nextIcmPath) + } + private fun getDeployKind( id: String, resourceType: ResourceType, diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DeploymentResult.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DeploymentResult.kt index 2942f404..407d3153 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DeploymentResult.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DeploymentResult.kt @@ -31,7 +31,7 @@ data class DeploymentInfo( ) enum class ResourceType { - DocumentObject, Image, TextStyle, ParagraphStyle + DocumentObject, Image, File, TextStyle, ParagraphStyle } data class DeploymentError(val id: String, val message: String) @@ -108,4 +108,42 @@ class ResultTracker( ) deploymentResult.warnings.add(DeploymentWarning(id, message)) } + + fun deployedFile(id: String, icmPath: String) { + statusTrackingRepository.deployed( + id = id, + deploymentId = deploymentId, + timestamp = timestamp, + resourceType = ResourceType.File, + output = inspireOutput, + icmPath = icmPath, + ) + deploymentResult.deployed.add(DeploymentInfo(id, ResourceType.File, icmPath)) + } + + fun errorFile(id: String, icmPath: String?, message: String) { + statusTrackingRepository.error( + id = id, + deploymentId = deploymentId, + timestamp = timestamp, + resourceType = ResourceType.File, + output = inspireOutput, + icmPath = icmPath, + message = message, + ) + deploymentResult.errors.add(DeploymentError(id, message)) + } + + fun warningFile(id: String, icmPath: String?, message: String) { + statusTrackingRepository.error( + id = id, + deploymentId = deploymentId, + timestamp = timestamp, + resourceType = ResourceType.File, + output = inspireOutput, + icmPath = icmPath, + message = message, + ) + deploymentResult.warnings.add(DeploymentWarning(id, message)) + } } \ No newline at end of file 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 122e6514..c415e2d5 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 @@ -9,6 +9,7 @@ import com.quadient.migration.data.ParagraphStyleDefinitionModel import com.quadient.migration.data.TextStyleDefinitionModel import com.quadient.migration.persistence.repository.DocumentObjectInternalRepository import com.quadient.migration.persistence.repository.ImageInternalRepository +import com.quadient.migration.persistence.repository.FileInternalRepository import com.quadient.migration.persistence.repository.ParagraphStyleInternalRepository import com.quadient.migration.persistence.repository.TextStyleInternalRepository import com.quadient.migration.persistence.table.DocumentObjectTable @@ -28,6 +29,7 @@ import kotlin.uuid.Uuid class DesignerDeployClient( documentObjectRepository: DocumentObjectInternalRepository, imageRepository: ImageInternalRepository, + fileRepository: FileInternalRepository, statusTrackingRepository: StatusTrackingRepository, textStyleRepository: TextStyleInternalRepository, paragraphStyleRepository: ParagraphStyleInternalRepository, @@ -37,6 +39,7 @@ class DesignerDeployClient( ) : DeployClient( documentObjectRepository, imageRepository, + fileRepository, statusTrackingRepository, textStyleRepository, paragraphStyleRepository, @@ -56,7 +59,7 @@ class DesignerDeployClient( val deploymentResult = DeploymentResult(deploymentId) val orderedDocumentObject = deployOrder(documentObjects) - deploymentResult += deployImages(orderedDocumentObject, deploymentId, deploymentTimestamp) + deploymentResult += deployImagesAndFiles(orderedDocumentObject, deploymentId, deploymentTimestamp) val tracker = ResultTracker(statusTrackingRepository, deploymentResult, deploymentId, deploymentTimestamp, output) for (it in orderedDocumentObject) { 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 6d8cae3f..ead0d146 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 @@ -10,6 +10,7 @@ import com.quadient.migration.data.ParagraphStyleDefinitionModel import com.quadient.migration.data.TextStyleDefinitionModel import com.quadient.migration.persistence.repository.DocumentObjectInternalRepository import com.quadient.migration.persistence.repository.ImageInternalRepository +import com.quadient.migration.persistence.repository.FileInternalRepository import com.quadient.migration.persistence.repository.ParagraphStyleInternalRepository import com.quadient.migration.persistence.repository.TextStyleInternalRepository import com.quadient.migration.persistence.table.DocumentObjectTable @@ -29,6 +30,7 @@ import kotlin.uuid.Uuid class InteractiveDeployClient( documentObjectRepository: DocumentObjectInternalRepository, imageRepository: ImageInternalRepository, + fileRepository: FileInternalRepository, statusTrackingRepository: StatusTrackingRepository, textStyleRepository: TextStyleInternalRepository, paragraphStyleRepository: ParagraphStyleInternalRepository, @@ -39,6 +41,7 @@ class InteractiveDeployClient( ) : DeployClient( documentObjectRepository, imageRepository, + fileRepository, statusTrackingRepository, textStyleRepository, paragraphStyleRepository, @@ -108,7 +111,7 @@ class InteractiveDeployClient( val orderedDocumentObject = deployOrder(documentObjects) - deploymentResult += deployImages(orderedDocumentObject, deploymentId, deploymentTimestamp) + deploymentResult += deployImagesAndFiles(orderedDocumentObject, deploymentId, deploymentTimestamp) val tracker = ResultTracker(statusTrackingRepository, deploymentResult, deploymentId, deploymentTimestamp, output) for (it in orderedDocumentObject) { diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/ProgressReport.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/ProgressReport.kt index 5d5d44e1..211abc0d 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/ProgressReport.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/ProgressReport.kt @@ -4,8 +4,10 @@ package com.quadient.migration.service.deploy import com.quadient.migration.api.dto.migrationmodel.DocumentObject import com.quadient.migration.api.dto.migrationmodel.Image +import com.quadient.migration.api.dto.migrationmodel.File import com.quadient.migration.data.DocumentObjectModel import com.quadient.migration.data.ImageModel +import com.quadient.migration.data.FileModel import kotlinx.datetime.Instant import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -133,6 +135,27 @@ data class ReportedImage( errorMessage ) +data class ReportedFile( + override val id: String, + override val previousIcmPath: String? = null, + override val nextIcmPath: String? = null, + override val deployKind: DeployKind, + override val lastStatus: LastStatus, + override val deploymentId: Uuid?, + override val deployTimestamp: Instant?, + override val errorMessage: String?, + val file: File, +) : ProgressReportItem( + id, + previousIcmPath, + nextIcmPath, + deployKind, + lastStatus, + deploymentId, + deployTimestamp, + errorMessage +) + data class ProgressReport(val id: Uuid?, val items: MutableMap, ProgressReportItem>) { fun addDocumentObject( id: String, @@ -185,5 +208,31 @@ data class ProgressReport(val id: Uuid?, val items: MutableMap projectConfig.paths.documents + FileType.Attachment -> projectConfig.paths.attachments + } + + return IcmPath.root().join(fileConfigPath) + .join(resolveTargetDir(projectConfig.defaultTargetFolder, targetFolder)).join(fileName).toString() + } + + override fun getFilePath(file: FileModel): String = + getFilePath(file.id, file.name, file.targetFolder, file.sourcePath, file.fileType) + override fun getStyleDefinitionPath(extension: String): String { val styleDefinitionPath = projectConfig.styleDefinitionPath 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 ac9677b7..294e5a3f 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 @@ -11,6 +11,8 @@ import com.quadient.migration.data.AreaModel import com.quadient.migration.data.HyperlinkModel import com.quadient.migration.data.ImageModel import com.quadient.migration.data.ImageModelRef +import com.quadient.migration.data.FileModel +import com.quadient.migration.data.FileModelRef import com.quadient.migration.data.ParagraphModel import com.quadient.migration.data.ParagraphModel.TextModel import com.quadient.migration.data.ParagraphStyleDefinitionModel @@ -28,6 +30,7 @@ import com.quadient.migration.data.VariableStructureModel import com.quadient.migration.persistence.repository.DisplayRuleInternalRepository import com.quadient.migration.persistence.repository.DocumentObjectInternalRepository import com.quadient.migration.persistence.repository.ImageInternalRepository +import com.quadient.migration.persistence.repository.FileInternalRepository import com.quadient.migration.persistence.repository.ParagraphStyleInternalRepository import com.quadient.migration.persistence.repository.TextStyleInternalRepository import com.quadient.migration.persistence.repository.VariableInternalRepository @@ -41,6 +44,7 @@ import com.quadient.migration.shared.BinOp import com.quadient.migration.shared.Binary import com.quadient.migration.shared.DisplayRuleDefinition import com.quadient.migration.shared.DocumentObjectType +import com.quadient.migration.shared.FileType import com.quadient.migration.shared.Function import com.quadient.migration.shared.Group import com.quadient.migration.shared.IcmPath @@ -96,6 +100,7 @@ abstract class InspireDocumentObjectBuilder( protected val variableStructureRepository: VariableStructureInternalRepository, protected val displayRuleRepository: DisplayRuleInternalRepository, protected val imageRepository: ImageInternalRepository, + protected val fileRepository: FileInternalRepository, protected val projectConfig: ProjectConfig, protected val ipsService: IpsService, ) { @@ -113,6 +118,12 @@ abstract class InspireDocumentObjectBuilder( abstract fun getImagePath(image: ImageModel): String + abstract fun getFilePath( + id: String, name: String?, targetFolder: IcmPath?, sourcePath: String?, fileType: FileType + ): String + + abstract fun getFilePath(file: FileModel): String + abstract fun getStyleDefinitionPath(extension: String = "wfd"): String abstract fun getFontRootFolder(): String @@ -266,7 +277,7 @@ abstract class InspireDocumentObjectBuilder( val flowModels = mutableListOf() while (idx < mutableContent.size) { when (val contentPart = mutableContent[idx]) { - is TableModel, is ParagraphModel, is ImageModelRef -> { + is TableModel, is ParagraphModel, is ImageModelRef, is FileModelRef -> { val flowParts = gatherFlowParts(mutableContent, idx) idx += flowParts.size - 1 flowModels.add(Composite(flowParts)) @@ -597,6 +608,54 @@ abstract class InspireDocumentObjectBuilder( protected abstract fun applyImageAlternateText(layout: Layout, image: Image, alternateText: String) + sealed interface FilePlaceholderResult { + object RenderAsNormal : FilePlaceholderResult + object Skip : FilePlaceholderResult + data class Placeholder(val value: String) : FilePlaceholderResult + } + + // TODO: Implement file placeholder logic similar to images + protected fun getFilePlaceholder(fileModel: FileModel): FilePlaceholderResult { + if (fileModel.sourcePath.isNullOrBlank() && !fileModel.skip.skipped) { + throw IllegalStateException( + "File '${fileModel.nameOrId()}' has missing source path and is not set to be skipped." + ) + } + + if (fileModel.skip.skipped && fileModel.skip.placeholder != null) { + return FilePlaceholderResult.Placeholder(fileModel.skip.placeholder) + } else if (fileModel.skip.skipped) { + return FilePlaceholderResult.Skip + } + + return FilePlaceholderResult.RenderAsNormal + } + + // TODO: Implement file handling logic + protected fun getOrBuildFile(layout: Layout, fileModel: FileModel): Any { + // TODO: Implement actual file building logic + // This is a placeholder for future implementation + throw NotImplementedError("File building not yet implemented. File ID: ${fileModel.id}") + } + + // TODO: Implement file appending logic + private fun buildAndAppendFile(layout: Layout, text: Text, ref: FileModelRef) { + val fileModel = fileRepository.findModelOrFail(ref.id) + + when (val filePlaceholder = getFilePlaceholder(fileModel)) { + is FilePlaceholderResult.Placeholder -> { + text.appendText(filePlaceholder.value) + return + } + is FilePlaceholderResult.RenderAsNormal -> {} + is FilePlaceholderResult.Skip -> return + } + + // TODO: Implement actual file appending logic + // text.appendFile(getOrBuildFile(layout, fileModel)) + throw NotImplementedError("File appending not yet implemented. File ID: ${fileModel.id}") + } + private fun buildCompositeFlow( layout: Layout, variableStructure: VariableStructureModel, @@ -614,6 +673,7 @@ abstract class InspireDocumentObjectBuilder( .appendTable(buildTable(layout, variableStructure, it, languages)) is ImageModelRef -> buildAndAppendImage(layout, flow.addParagraph().addText(), it) + is FileModelRef -> buildAndAppendFile(layout, flow.addParagraph().addText(), it) else -> error("Content part type ${it::class.simpleName} is not allowed in composite flow.") } } @@ -674,7 +734,7 @@ abstract class InspireDocumentObjectBuilder( do { val contentPart = content[index] - if (contentPart is TableModel || contentPart is ParagraphModel || contentPart is ImageModelRef) { + if (contentPart is TableModel || contentPart is ParagraphModel || contentPart is ImageModelRef || contentPart is FileModelRef) { flowParts.add(contentPart) index++ } else { @@ -732,6 +792,7 @@ abstract class InspireDocumentObjectBuilder( } is ImageModelRef -> buildAndAppendImage(layout, currentText, it) + is FileModelRef -> buildAndAppendFile(layout, currentText, it) is HyperlinkModel -> currentText = buildAndAppendHyperlink(layout, paragraph, baseTextStyleModel, it) is FirstMatchModel -> currentText.appendFlow( buildFirstMatch(layout, variableStructure, it, true, null, languages) 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 7ad11b3d..b17d8fc7 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 @@ -7,10 +7,12 @@ import com.quadient.migration.api.ProjectConfig import com.quadient.migration.data.AreaModel import com.quadient.migration.data.DocumentContentModel import com.quadient.migration.data.DocumentObjectModel +import com.quadient.migration.data.FileModel import com.quadient.migration.data.ImageModel import com.quadient.migration.persistence.repository.DisplayRuleInternalRepository import com.quadient.migration.persistence.repository.DocumentObjectInternalRepository import com.quadient.migration.persistence.repository.ImageInternalRepository +import com.quadient.migration.persistence.repository.FileInternalRepository import com.quadient.migration.persistence.repository.ParagraphStyleInternalRepository import com.quadient.migration.persistence.repository.TextStyleInternalRepository import com.quadient.migration.persistence.repository.VariableInternalRepository @@ -20,6 +22,7 @@ import com.quadient.migration.service.imageExtension import com.quadient.migration.service.ipsclient.IpsService import com.quadient.migration.service.resolveTargetDir import com.quadient.migration.shared.DocumentObjectType +import com.quadient.migration.shared.FileType import com.quadient.migration.shared.IcmPath import com.quadient.migration.shared.ImageType import com.quadient.migration.shared.orDefault @@ -40,6 +43,7 @@ class InteractiveDocumentObjectBuilder( variableStructureRepository: VariableStructureInternalRepository, displayRuleRepository: DisplayRuleInternalRepository, imageRepository: ImageInternalRepository, + fileRepository: FileInternalRepository, projectConfig: ProjectConfig, ipsService: IpsService, ) : InspireDocumentObjectBuilder( @@ -50,6 +54,7 @@ class InteractiveDocumentObjectBuilder( variableStructureRepository, displayRuleRepository, imageRepository, + fileRepository, projectConfig, ipsService, ) { @@ -104,6 +109,27 @@ class InteractiveDocumentObjectBuilder( override fun getImagePath(image: ImageModel) = getImagePath(image.id, image.imageType, image.name, image.targetFolder, image.sourcePath) + override fun getFilePath( + id: String, name: String?, targetFolder: IcmPath?, sourcePath: String?, fileType: FileType + ): String { + val fileName = name ?: id + + if (targetFolder?.isAbsolute() == true) { + return targetFolder.join(fileName).toString() + } + + val fileConfigPath = when (fileType) { + FileType.Document -> projectConfig.paths.documents ?: IcmPath.from("Resources/Documents") + FileType.Attachment -> projectConfig.paths.attachments ?: IcmPath.from("Resources/Attachments") + } + + return IcmPath.root().join("Interactive").join(projectConfig.interactiveTenant).join(fileConfigPath) + .join(resolveTargetDir(projectConfig.defaultTargetFolder, targetFolder)).join(fileName).toString() + } + + override fun getFilePath(file: FileModel): String = + getFilePath(file.id, file.name, file.targetFolder, file.sourcePath, file.fileType) + override fun getStyleDefinitionPath(extension: String): String { val styleDefConfigPath = projectConfig.styleDefinitionPath diff --git a/migration-library/src/main/kotlin/com/quadient/migration/shared/FileType.kt b/migration-library/src/main/kotlin/com/quadient/migration/shared/FileType.kt new file mode 100644 index 00000000..c2944955 --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/shared/FileType.kt @@ -0,0 +1,6 @@ +package com.quadient.migration.shared + +enum class FileType { + Document, + Attachment +} diff --git a/migration-library/src/test/kotlin/com/quadient/migration/persistence/MappingRepositoryTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/persistence/MappingRepositoryTest.kt index b521973a..6da2fe6c 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/persistence/MappingRepositoryTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/persistence/MappingRepositoryTest.kt @@ -21,6 +21,7 @@ class MappingRepositoryTest { val projectConfig = aProjectConfig() val documentObjectRepository = mockk() val imageRepository = mockk() + val fileRepository = mockk() val textStyleRepository = mockk() val paraStyleRepository = mockk() val variableRepository = mockk() @@ -30,6 +31,7 @@ class MappingRepositoryTest { projectConfig.name, documentObjectRepository, imageRepository, + fileRepository, textStyleRepository, paraStyleRepository, variableRepository, diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/ReferenceValidatorTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/ReferenceValidatorTest.kt index 654a6da5..3400807c 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/ReferenceValidatorTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/ReferenceValidatorTest.kt @@ -15,6 +15,7 @@ import com.quadient.migration.tools.model.aBlock import com.quadient.migration.tools.model.aDisplayRuleInternalRepository import com.quadient.migration.tools.model.aDocumentObjectInternalRepository import com.quadient.migration.tools.model.aDocumentObjectRef +import com.quadient.migration.tools.model.aFileInternalRepository import com.quadient.migration.tools.model.aImageInternalRepository import com.quadient.migration.tools.model.aParaStyleInternalRepository import com.quadient.migration.tools.model.aTextStyleInternalRepository @@ -34,6 +35,7 @@ class ReferenceValidatorTest { val dataStructureRepository = aVariableStructureInternalRepository() val displayRuleRepository = aDisplayRuleInternalRepository() val imageRuleRepository = aImageInternalRepository() + val fileRuleRepository = aFileInternalRepository() val docRepo = aDocumentObjectRepository() val paraStyleRepo = aParaStyleRepository() @@ -47,6 +49,7 @@ class ReferenceValidatorTest { dataStructureRepository, displayRuleRepository, imageRuleRepository, + fileRuleRepository, ) @Test diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/DeployClientTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/DeployClientTest.kt index 389d278d..21a2f7f9 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/DeployClientTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/DeployClientTest.kt @@ -12,6 +12,7 @@ import com.quadient.migration.data.ParagraphStyleModelRef import com.quadient.migration.data.StatusEvent import com.quadient.migration.data.TextStyleModelRef import com.quadient.migration.persistence.repository.DocumentObjectInternalRepository +import com.quadient.migration.persistence.repository.FileInternalRepository import com.quadient.migration.persistence.repository.ImageInternalRepository import com.quadient.migration.persistence.repository.ParagraphStyleInternalRepository import com.quadient.migration.persistence.repository.TextStyleInternalRepository @@ -51,6 +52,7 @@ import kotlin.uuid.Uuid class DeployClientTest { val documentObjectRepository = mockk() val imageRepository = mockk() + val fileRepository = mockk() val textStyleRepository = mockk() val paragraphStyleRepository = mockk() val statusTrackingRepository = mockk() @@ -61,6 +63,7 @@ class DeployClientTest { private val subject = DesignerDeployClient( documentObjectRepository, imageRepository, + fileRepository, statusTrackingRepository, textStyleRepository, paragraphStyleRepository, 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 e4acc9ea..253306ae 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 @@ -12,6 +12,7 @@ import com.quadient.migration.data.ImageModel import com.quadient.migration.data.ImageModelRef import com.quadient.migration.data.StringModel import com.quadient.migration.persistence.repository.DocumentObjectInternalRepository +import com.quadient.migration.persistence.repository.FileInternalRepository import com.quadient.migration.persistence.repository.ImageInternalRepository import com.quadient.migration.persistence.repository.ParagraphStyleInternalRepository import com.quadient.migration.persistence.repository.TextStyleInternalRepository @@ -52,6 +53,7 @@ import kotlin.uuid.Uuid class DesignerDeployClientTest { val documentObjectRepository = mockk() val imageRepository = mockk() + val fileRepository = mockk() val textStyleRepository = mockk() val paragraphStyleRepository = mockk() val statusTrackingRepository = mockk() @@ -62,6 +64,7 @@ class DesignerDeployClientTest { private val subject = DesignerDeployClient( documentObjectRepository, imageRepository, + fileRepository, statusTrackingRepository, textStyleRepository, paragraphStyleRepository, 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 0c95fce5..420a43df 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 @@ -15,6 +15,7 @@ import com.quadient.migration.data.ParagraphModel.TextModel import com.quadient.migration.data.StringModel import com.quadient.migration.data.VariableModelRef import com.quadient.migration.persistence.repository.DocumentObjectInternalRepository +import com.quadient.migration.persistence.repository.FileInternalRepository import com.quadient.migration.persistence.repository.ImageInternalRepository import com.quadient.migration.persistence.repository.ParagraphStyleInternalRepository import com.quadient.migration.persistence.repository.TextStyleInternalRepository @@ -58,6 +59,7 @@ import kotlin.uuid.Uuid class InteractiveDeployClientTest { val documentObjectRepository = mockk() val imageRepository = mockk() + val fileRepository = mockk() val textStyleRepository = mockk() val paragraphStyleRepository = mockk() val statusTrackingRepository = mockk() @@ -70,6 +72,7 @@ class InteractiveDeployClientTest { private val subject = InteractiveDeployClient( documentObjectRepository, imageRepository, + fileRepository, statusTrackingRepository, textStyleRepository, paragraphStyleRepository, 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 9541f25d..3e668ea6 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 @@ -17,6 +17,7 @@ import com.quadient.migration.data.VariableModelRef import com.quadient.migration.data.VariableStructureModel import com.quadient.migration.persistence.repository.DisplayRuleInternalRepository import com.quadient.migration.persistence.repository.DocumentObjectInternalRepository +import com.quadient.migration.persistence.repository.FileInternalRepository import com.quadient.migration.persistence.repository.ImageInternalRepository import com.quadient.migration.persistence.repository.ParagraphStyleInternalRepository import com.quadient.migration.persistence.repository.TextStyleInternalRepository @@ -73,6 +74,7 @@ class DesignerDocumentObjectBuilderTest { val variableStructureRepository = mockk() val displayRuleRepository = mockk() val imageRepository = mockk() + val fileRepository = mockk() val ipsService = mockk() val config = aProjectConfig(targetDefaultFolder = "defaultFolder") @@ -737,6 +739,7 @@ class DesignerDocumentObjectBuilderTest { variableStructureRepository, displayRuleRepository, imageRepository, + fileRepository, config, ipsService, ) 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 df1c21d8..9e2d06e4 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 @@ -33,6 +33,7 @@ class InspireDocumentObjectBuilderTest { private val variableStructureRepository = mockk() private val displayRuleRepository = mockk() private val imageRepository = mockk() + private val fileRepository = mockk() private val ipsService = mockk() private val xmlMapper = XmlMapper().also { it.findAndRegisterModules() } @@ -45,6 +46,7 @@ class InspireDocumentObjectBuilderTest { variableStructureRepository, displayRuleRepository, imageRepository, + fileRepository, aProjectConfig(output = InspireOutput.Designer), ipsService, ) 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 adf23738..671b5d96 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 @@ -21,6 +21,7 @@ import com.quadient.migration.data.VariableModelRef import com.quadient.migration.data.VariableStructureModel import com.quadient.migration.persistence.repository.DisplayRuleInternalRepository import com.quadient.migration.persistence.repository.DocumentObjectInternalRepository +import com.quadient.migration.persistence.repository.FileInternalRepository import com.quadient.migration.persistence.repository.ImageInternalRepository import com.quadient.migration.persistence.repository.ParagraphStyleInternalRepository import com.quadient.migration.persistence.repository.TextStyleInternalRepository @@ -87,6 +88,7 @@ class InteractiveDocumentObjectBuilderTest { val variableStructureRepository = mockk() val displayRuleRepository = mockk() val imageRepository = mockk() + val fileRepository = mockk() val config = aProjectConfig() val ipsService = mockk() @@ -1481,6 +1483,7 @@ class InteractiveDocumentObjectBuilderTest { variableStructureRepository, displayRuleRepository, imageRepository, + fileRepository, config, ipsService, ) diff --git a/migration-library/src/test/kotlin/com/quadient/migration/tools/model/TestModelObjectBuilders.kt b/migration-library/src/test/kotlin/com/quadient/migration/tools/model/TestModelObjectBuilders.kt index 94b7c535..2fb25d99 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/tools/model/TestModelObjectBuilders.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/tools/model/TestModelObjectBuilders.kt @@ -28,6 +28,7 @@ import com.quadient.migration.data.VariableStructureModel import com.quadient.migration.data.VariableStructureModelRef import com.quadient.migration.persistence.repository.DisplayRuleInternalRepository import com.quadient.migration.persistence.repository.DocumentObjectInternalRepository +import com.quadient.migration.persistence.repository.FileInternalRepository import com.quadient.migration.persistence.repository.ImageInternalRepository import com.quadient.migration.persistence.repository.ParagraphStyleInternalRepository import com.quadient.migration.persistence.repository.TextStyleInternalRepository @@ -35,6 +36,7 @@ import com.quadient.migration.persistence.repository.VariableInternalRepository import com.quadient.migration.persistence.repository.VariableStructureInternalRepository import com.quadient.migration.persistence.table.DisplayRuleTable import com.quadient.migration.persistence.table.DocumentObjectTable +import com.quadient.migration.persistence.table.FileTable import com.quadient.migration.persistence.table.ImageTable import com.quadient.migration.persistence.table.ParagraphStyleTable import com.quadient.migration.persistence.table.TextStyleTable @@ -413,4 +415,5 @@ fun aVariableStructureInternalRepository() = fun aParaStyleInternalRepository() = ParagraphStyleInternalRepository(ParagraphStyleTable, aProjectConfig().name) fun aTextStyleInternalRepository() = TextStyleInternalRepository(TextStyleTable, aProjectConfig().name) fun aDisplayRuleInternalRepository() = DisplayRuleInternalRepository(DisplayRuleTable, aProjectConfig().name) -fun aImageInternalRepository() = ImageInternalRepository(ImageTable, aProjectConfig().name) \ No newline at end of file +fun aImageInternalRepository() = ImageInternalRepository(ImageTable, aProjectConfig().name) +fun aFileInternalRepository() = FileInternalRepository(FileTable, aProjectConfig().name) \ No newline at end of file From 7b135fa7147f47a01bcd280a8d7f6d3f6e5ff76e Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Tue, 3 Feb 2026 07:51:17 +0100 Subject: [PATCH 2/9] adding other support for new files entity in various scripts, config templates, app, etc. --- .../dialogs/settings/AdvancedSettingsForm.tsx | 38 +++++++++ .../web/src/dialogs/settings/settingsTypes.ts | 2 + .../example/common/mapping/FilesExport.groovy | 52 +++++++++++++ .../example/common/mapping/FilesImport.groovy | 71 +++++++++++++++++ .../common/report/ComplexityReport.groovy | 9 +++ .../example/common/util/InitMigration.groovy | 6 +- .../azureai-project-config-template.toml | 2 + .../resources/project-config-template.toml | 2 + .../InspireDocumentObjectBuilder.kt | 77 ++++++++----------- 9 files changed, 215 insertions(+), 44 deletions(-) create mode 100644 migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/FilesExport.groovy create mode 100644 migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/FilesImport.groovy diff --git a/migration-app/web/src/dialogs/settings/AdvancedSettingsForm.tsx b/migration-app/web/src/dialogs/settings/AdvancedSettingsForm.tsx index 9ea9cadb..bc166b82 100644 --- a/migration-app/web/src/dialogs/settings/AdvancedSettingsForm.tsx +++ b/migration-app/web/src/dialogs/settings/AdvancedSettingsForm.tsx @@ -70,6 +70,7 @@ export function AdvancedSettingsForm({ settings, setSettings }: SettingsFormProp projectConfig: { ...prev.projectConfig, paths: { + ...prev.projectConfig.paths, images: e.target.value || undefined, }, }, @@ -87,6 +88,7 @@ export function AdvancedSettingsForm({ settings, setSettings }: SettingsFormProp projectConfig: { ...prev.projectConfig, paths: { + ...prev.projectConfig.paths, fonts: e.target.value || undefined, }, }, @@ -94,6 +96,42 @@ export function AdvancedSettingsForm({ settings, setSettings }: SettingsFormProp } /> +
+ + + setSettings((prev) => ({ + ...prev, + projectConfig: { + ...prev.projectConfig, + paths: { + ...prev.projectConfig.paths, + documents: e.target.value || undefined, + }, + }, + })) + } + /> +
+
+ + + setSettings((prev) => ({ + ...prev, + projectConfig: { + ...prev.projectConfig, + paths: { + ...prev.projectConfig.paths, + attachments: e.target.value || undefined, + }, + }, + })) + } + /> +
diff --git a/migration-app/web/src/dialogs/settings/settingsTypes.ts b/migration-app/web/src/dialogs/settings/settingsTypes.ts index c6c04726..b67f74d8 100644 --- a/migration-app/web/src/dialogs/settings/settingsTypes.ts +++ b/migration-app/web/src/dialogs/settings/settingsTypes.ts @@ -51,6 +51,8 @@ export type ProjectConfig = { export type PathsConfig = { images?: string | undefined; fonts?: string | undefined; + documents?: string | undefined; + attachments?: string | undefined; }; export const inspireOutputOptions = ["Designer", "Interactive", "Evolve"] as const; diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/FilesExport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/FilesExport.groovy new file mode 100644 index 00000000..0d328978 --- /dev/null +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/FilesExport.groovy @@ -0,0 +1,52 @@ +//! --- +//! displayName: Export Files +//! category: Mapping +//! description: Creates CSV files with file details from the migration project. The generated CSV columns can be updated and later imported back into the database using a dedicated import task. +//! target: gradle +//! --- +package com.quadient.migration.example.common.mapping + +import com.quadient.migration.api.Migration +import com.quadient.migration.example.common.util.Csv +import com.quadient.migration.example.common.util.Mapping +import com.quadient.migration.service.deploy.ResourceType + +import java.nio.file.Path + +import static com.quadient.migration.example.common.util.InitMigration.initMigration + +def migration = initMigration(this.binding) + +def filesPath = Mapping.csvPath(binding, migration.projectConfig.name, "files") + +run(migration, filesPath) + +static void run(Migration migration, Path filesDstPath) { + def files = migration.fileRepository.listAll() + + filesDstPath.toFile().createParentDirectories() + + filesDstPath.toFile().withWriter { writer -> + def headers = ["id", "name", "sourcePath", "targetFolder", "status", "skip", "skipPlaceholder", "skipReason", Mapping.displayHeader("originalName", true), Mapping.displayHeader("originLocations", true)] + writer.writeLine(headers.join(",")) + files.each { obj -> + def status = migration.statusTrackingRepository.findLastEventRelevantToOutput(obj.id, + ResourceType.File, + migration.projectConfig.inspireOutput) + + def builder = new StringBuilder() + builder.append(Csv.serialize(obj.id)) + builder.append("," + Csv.serialize(obj.name)) + builder.append("," + Csv.serialize(obj.sourcePath)) + builder.append("," + Csv.serialize(obj.targetFolder)) + builder.append("," + Csv.serialize(status.class.simpleName)) + builder.append("," + Csv.serialize(obj.skip.skipped)) + builder.append("," + Csv.serialize(obj.skip.placeholder)) + builder.append("," + Csv.serialize(obj.skip.reason)) + builder.append("," + Csv.serialize(obj.customFields["originalName"])) + builder.append("," + Csv.serialize(obj.originLocations)) + + writer.writeLine(builder.toString()) + } + } +} diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/FilesImport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/FilesImport.groovy new file mode 100644 index 00000000..630d1d30 --- /dev/null +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/FilesImport.groovy @@ -0,0 +1,71 @@ +//! --- +//! displayName: Import Files +//! category: Mapping +//! description: Imports file details from CSV files into the migration project, applying any updates made to the columns during editing. +//! target: gradle +//! --- +package com.quadient.migration.example.common.mapping + +import com.quadient.migration.api.Migration +import com.quadient.migration.example.common.util.Csv +import com.quadient.migration.example.common.util.Mapping +import com.quadient.migration.service.deploy.ResourceType +import com.quadient.migration.shared.SkipOptions + +import java.nio.file.Path + +import static com.quadient.migration.example.common.util.InitMigration.initMigration + +def migration = initMigration(this.binding) + +def path = Mapping.csvPath(binding, migration.projectConfig.name, "files") + +run(migration, path) + +static void run(Migration migration, Path filesFilePath) { + def deploymentId = UUID.randomUUID().toString() + def now = new Date().getTime() + def output = migration.projectConfig.inspireOutput + def fileLines = filesFilePath.toFile().readLines() + def fileColumnNames = Csv.parseColumnNames(fileLines.removeFirst()).collect { Mapping.normalizeHeader(it) } + + for (line in fileLines) { + def values = Csv.getCells(line, fileColumnNames) + def id = values.get("id") + def existingFile = migration.fileRepository.find(id) + def existingMapping = migration.mappingRepository.getFileMapping(id) + + if (existingFile == null) { + throw new Exception("File with id ${id} not found") + } + def status = migration.statusTrackingRepository.findLastEventRelevantToOutput(existingFile.id, + ResourceType.File, + migration.projectConfig.inspireOutput) + + def newName = Csv.deserialize(values.get("name"), String.class) + Mapping.mapProp(existingMapping, existingFile, "name", newName) + + def newSourcePath = Csv.deserialize(values.get("sourcePath"), String.class) + Mapping.mapProp(existingMapping, existingFile, "sourcePath", newSourcePath) + + def newTargetFolder = Csv.deserialize(values.get("targetFolder"), String.class) + Mapping.mapProp(existingMapping, existingFile, "targetFolder", newTargetFolder) + + def csvStatus = values.get("status") + if (status != null && csvStatus == "Active" && status.class.simpleName != "Active") { + migration.statusTrackingRepository.active(existingFile.id, ResourceType.File, [reason: "Manual"]) + } + if (status != null && csvStatus == "Deployed" && status.class.simpleName != "Deployed") { + migration.statusTrackingRepository.deployed(existingFile.id, deploymentId, now, ResourceType.File, null, output, [reason: "Manual"]) + } + + boolean newSkip = Csv.deserialize(values.get("skip"), boolean) + def newSkipReason = Csv.deserialize(values.get("skipReason"), String.class) + def newSkipPlaceholder = Csv.deserialize(values.get("skipPlaceholder"), String.class) + def skipObj = new SkipOptions(newSkip, newSkipPlaceholder, newSkipReason) + Mapping.mapProp(existingMapping, existingFile, "skip", skipObj) + + migration.mappingRepository.upsert(id, existingMapping) + migration.mappingRepository.applyFileMapping(id) + } +} diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/report/ComplexityReport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/report/ComplexityReport.groovy index 4a6f88e1..ed2b698a 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/report/ComplexityReport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/report/ComplexityReport.groovy @@ -35,6 +35,7 @@ def header = ["Id", "Display rules", "Translated display rules", "Images", + "Files", "Hyperlinks", "Paragraph styles", "Text styles", @@ -87,6 +88,7 @@ file.withWriter { writer -> writer.write("$stats.displayRulesCount,") // Display rules count writer.write("$stats.translatedDisplayRulesCount,") // Translated display rules writer.write("$stats.imagesCount,") // Images count + writer.write("$stats.filesCount,") // Files count writer.write("$stats.usedHyperlinksCount,") // Used Hyperlinks Count writer.write("$stats.usedParagraphStylesCount,") // Used Paragraph Styles Count writer.write("$stats.usedTextStylesCount,") // Used Text Styles Count @@ -107,6 +109,7 @@ class Stats { Set usedDisplayRules = new HashSet() Set usedTranslatedDisplayRules = new HashSet() Set usedImages = new HashSet() + Set usedFiles = new HashSet() Set usedHyperlinks = new HashSet() Set usedParagraphStyles = new HashSet() Set usedTextStyles = new HashSet() @@ -134,6 +137,7 @@ class Stats { switch (content) { case DocumentObjectRef -> this.collectDocumentObjectRef(content) case ImageRef -> this.usedImages.add(content.id) + case FileRef -> this.usedFiles.add(content.id) case Table -> this.collectTable(content) case Paragraph -> this.collectParagraph(content) case Area -> this.collectContent(content.content) @@ -163,6 +167,7 @@ class Stats { switch (content) { case DocumentObjectRef -> this.collectDocumentObjectRef(content) case ImageRef -> this.usedImages.add(content.id) + case FileRef -> this.usedFiles.add(content.id) case StringValue -> { this.characterCount += content.value.chars.length this.wordCount += content.value.split("\\s+").length @@ -251,6 +256,10 @@ class Stats { return this.usedImages.size() } + Number getFilesCount() { + return this.usedFiles.size() + } + Number getUsedHyperlinksCount() { return this.usedHyperlinks.size() } diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/util/InitMigration.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/util/InitMigration.groovy index c34a6210..9170cdb3 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/util/InitMigration.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/util/InitMigration.groovy @@ -48,11 +48,15 @@ static Migration initMigration(Binding binding) { def imagesPathArg = getValueOfArg("--images-path", argsList).orElse(fileProjectConfig.paths.images?.toString()) def fontsPathArg = getValueOfArg("--fonts-path", argsList).orElse(fileProjectConfig.paths.fonts?.toString()) + def documentsPathArg = getValueOfArg("--documents-path", argsList).orElse(fileProjectConfig.paths.documents?.toString()) + def attachmentsPathArg = getValueOfArg("--attachments-path", argsList).orElse(fileProjectConfig.paths.attachments?.toString()) def styleDefinitionPath = (styleDefinitionPathArg == null || styleDefinitionPathArg.isEmpty()) ? null : IcmPath.from(styleDefinitionPathArg) def defFolder = (defaultTargetFolder == null || defaultTargetFolder.isEmpty()) ? null : IcmPath.from(defaultTargetFolder) def imagesPath = (imagesPathArg == null || imagesPathArg.isEmpty()) ? null : IcmPath.from(imagesPathArg) def fontsPath = (fontsPathArg == null || fontsPathArg.isEmpty()) ? null : IcmPath.from(fontsPathArg) + def documentsPath = (documentsPathArg == null || documentsPathArg.isEmpty()) ? null : IcmPath.from(documentsPathArg) + def attachmentsPath = (attachmentsPathArg == null || attachmentsPathArg.isEmpty()) ? null : IcmPath.from(attachmentsPathArg) def projectConfig = new ProjectConfig(projectName, baseTemplatePath, @@ -60,7 +64,7 @@ static Migration initMigration(Binding binding) { inputDataPath, interactiveTenant, defFolder, - new PathsConfig(imagesPath, fontsPath), + new PathsConfig(imagesPath, fontsPath, documentsPath, attachmentsPath), InspireOutput.valueOf(inspireOutput), sourceBaseTemplate, defaultVariableStructure, diff --git a/migration-examples/src/main/resources/azureai-project-config-template.toml b/migration-examples/src/main/resources/azureai-project-config-template.toml index 65407803..00e38a6f 100644 --- a/migration-examples/src/main/resources/azureai-project-config-template.toml +++ b/migration-examples/src/main/resources/azureai-project-config-template.toml @@ -12,6 +12,8 @@ inspireOutput = "Designer" # "Evolve", "Interactive", "Designer" [paths] #images = "" #fonts = "" +#documents = "" +#attachments = "" [context] endpoint = "https://<>/documentintelligence/documentModels/prebuilt-layout:analyze?_overload=analyzeDocument&api-version=2024-11-30&features=styleFont,barcodes" diff --git a/migration-examples/src/main/resources/project-config-template.toml b/migration-examples/src/main/resources/project-config-template.toml index 63551849..f0231b3b 100644 --- a/migration-examples/src/main/resources/project-config-template.toml +++ b/migration-examples/src/main/resources/project-config-template.toml @@ -12,5 +12,7 @@ inspireOutput = "Designer" # "Evolve", "Interactive", "Designer" [paths] #images = "" #fonts = "" +#documents = "" +#attachments = "" [context] \ 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 294e5a3f..b0404e5c 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 @@ -277,13 +277,14 @@ abstract class InspireDocumentObjectBuilder( val flowModels = mutableListOf() while (idx < mutableContent.size) { when (val contentPart = mutableContent[idx]) { - is TableModel, is ParagraphModel, is ImageModelRef, is FileModelRef -> { + is TableModel, is ParagraphModel, is ImageModelRef -> { val flowParts = gatherFlowParts(mutableContent, idx) idx += flowParts.size - 1 flowModels.add(Composite(flowParts)) } is DocumentObjectModelRef -> flowModels.add(DocumentObject(contentPart)) + is FileModelRef -> flowModels.add(File(contentPart)) is AreaModel -> mutableContent.addAll(idx + 1, contentPart.content) is FirstMatchModel -> flowModels.add(FirstMatch(contentPart)) is SelectByLanguageModel -> flowModels.add(SelectByLanguage(contentPart)) @@ -296,6 +297,7 @@ abstract class InspireDocumentObjectBuilder( return flowModels.mapNotNull { when (it) { is DocumentObject -> buildDocumentObjectRef(layout, variableStructure, it.ref, languages) + is File -> buildFileRef(layout, it.ref) is Composite -> { if (flowName == null) { buildCompositeFlow(layout, variableStructure, it.parts, null, languages) @@ -332,6 +334,7 @@ abstract class InspireDocumentObjectBuilder( sealed interface FlowModel { data class Composite(val parts: List) : FlowModel data class DocumentObject(val ref: DocumentObjectModelRef) : FlowModel + data class File(val ref: FileModelRef) : FlowModel data class FirstMatch(val model: FirstMatchModel) : FlowModel data class SelectByLanguage(val model: SelectByLanguageModel) : FlowModel } @@ -608,52 +611,38 @@ abstract class InspireDocumentObjectBuilder( protected abstract fun applyImageAlternateText(layout: Layout, image: Image, alternateText: String) - sealed interface FilePlaceholderResult { - object RenderAsNormal : FilePlaceholderResult - object Skip : FilePlaceholderResult - data class Placeholder(val value: String) : FilePlaceholderResult - } + private fun buildFileRef( + layout: Layout, + fileRef: FileModelRef, + ): Flow? { + val fileModel = fileRepository.findModelOrFail(fileRef.id) - // TODO: Implement file placeholder logic similar to images - protected fun getFilePlaceholder(fileModel: FileModel): FilePlaceholderResult { - if (fileModel.sourcePath.isNullOrBlank() && !fileModel.skip.skipped) { + if (fileModel.skip.skipped && fileModel.skip.placeholder == null) { + val reason = fileModel.skip.reason?.let { "with reason: $it" } ?: "without reason" + logger.debug("File ${fileRef.id} is set to be skipped without placeholder $reason.") + return null + } else if (fileModel.skip.skipped && fileModel.skip.placeholder != null) { + val reason = fileModel.skip.reason?.let { "and reason: $it" } ?: "without reason" + logger.debug("File ${fileRef.id} is set to be skipped with placeholder $reason.") + val flow = layout.addFlow().setType(Flow.Type.SIMPLE) + flow.addParagraph().addText().appendText(fileModel.skip.placeholder) + return flow + } + + if (fileModel.sourcePath.isNullOrBlank()) { throw IllegalStateException( "File '${fileModel.nameOrId()}' has missing source path and is not set to be skipped." ) } - if (fileModel.skip.skipped && fileModel.skip.placeholder != null) { - return FilePlaceholderResult.Placeholder(fileModel.skip.placeholder) - } else if (fileModel.skip.skipped) { - return FilePlaceholderResult.Skip + val flow = getFlowByName(layout, fileModel.nameOrId()) ?: run { + layout.addFlow() + .setName(fileModel.nameOrId()) + .setType(Flow.Type.DIRECT_EXTERNAL) + .setLocation(getFilePath(fileModel)) } - return FilePlaceholderResult.RenderAsNormal - } - - // TODO: Implement file handling logic - protected fun getOrBuildFile(layout: Layout, fileModel: FileModel): Any { - // TODO: Implement actual file building logic - // This is a placeholder for future implementation - throw NotImplementedError("File building not yet implemented. File ID: ${fileModel.id}") - } - - // TODO: Implement file appending logic - private fun buildAndAppendFile(layout: Layout, text: Text, ref: FileModelRef) { - val fileModel = fileRepository.findModelOrFail(ref.id) - - when (val filePlaceholder = getFilePlaceholder(fileModel)) { - is FilePlaceholderResult.Placeholder -> { - text.appendText(filePlaceholder.value) - return - } - is FilePlaceholderResult.RenderAsNormal -> {} - is FilePlaceholderResult.Skip -> return - } - - // TODO: Implement actual file appending logic - // text.appendFile(getOrBuildFile(layout, fileModel)) - throw NotImplementedError("File appending not yet implemented. File ID: ${fileModel.id}") + return flow } private fun buildCompositeFlow( @@ -673,7 +662,6 @@ abstract class InspireDocumentObjectBuilder( .appendTable(buildTable(layout, variableStructure, it, languages)) is ImageModelRef -> buildAndAppendImage(layout, flow.addParagraph().addText(), it) - is FileModelRef -> buildAndAppendFile(layout, flow.addParagraph().addText(), it) else -> error("Content part type ${it::class.simpleName} is not allowed in composite flow.") } } @@ -734,7 +722,7 @@ abstract class InspireDocumentObjectBuilder( do { val contentPart = content[index] - if (contentPart is TableModel || contentPart is ParagraphModel || contentPart is ImageModelRef || contentPart is FileModelRef) { + if (contentPart is TableModel || contentPart is ParagraphModel || contentPart is ImageModelRef) { flowParts.add(contentPart) index++ } else { @@ -791,8 +779,11 @@ abstract class InspireDocumentObjectBuilder( currentText.appendFlow(flow) } + is FileModelRef -> buildFileRef(layout, it)?.also { flow -> + currentText.appendFlow(flow) + } + is ImageModelRef -> buildAndAppendImage(layout, currentText, it) - is FileModelRef -> buildAndAppendFile(layout, currentText, it) is HyperlinkModel -> currentText = buildAndAppendHyperlink(layout, paragraph, baseTextStyleModel, it) is FirstMatchModel -> currentText.appendFlow( buildFirstMatch(layout, variableStructure, it, true, null, languages) @@ -819,7 +810,7 @@ abstract class InspireDocumentObjectBuilder( *TextStyleInheritFlag.entries .filter { it != TextStyleInheritFlag.UNDERLINE && it != TextStyleInheritFlag.FILL_STYLE } .toTypedArray()) - (hyperlinkStyle as TextStyleImpl).setAncestorId("Def.TextStyleHyperlink") + (hyperlinkStyle as TextStyleImpl).ancestorId = "Def.TextStyleHyperlink" if (baseTextStyleModel != null) { val definition = baseTextStyleModel.resolve() From e29f8e41d66a5bccebc082f4b0ca05a91c9c226a Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Tue, 3 Feb 2026 11:40:51 +0100 Subject: [PATCH 3/9] enrich file path with extension from source path if not available in file name right away. Also add file example to Import.groovy. --- .../migration/example/example/Import.groovy | 23 ++++++++++---- .../migrationModelExample/logo.pdf | Bin 0 -> 3741 bytes .../DesignerDocumentObjectBuilder.kt | 3 +- .../inspirebuilder/InspireBuilderUtils.kt | 28 ++++++++++++++++++ .../InteractiveDocumentObjectBuilder.kt | 3 +- 5 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 migration-examples/src/main/resources/exampleResources/migrationModelExample/logo.pdf 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 1e31b6d1..02e431ae 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/example/Import.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/example/Import.groovy @@ -11,6 +11,7 @@ import com.quadient.migration.api.dto.migrationmodel.DisplayRuleRef import com.quadient.migration.api.dto.migrationmodel.ParagraphStyleRef 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.FileBuilder import com.quadient.migration.api.dto.migrationmodel.builder.ImageBuilder import com.quadient.migration.api.dto.migrationmodel.builder.ParagraphBuilder import com.quadient.migration.api.dto.migrationmodel.builder.ParagraphStyleBuilder @@ -19,6 +20,7 @@ import com.quadient.migration.api.dto.migrationmodel.builder.VariableBuilder import com.quadient.migration.api.dto.migrationmodel.builder.VariableStructureBuilder import com.quadient.migration.shared.DataType import com.quadient.migration.shared.DocumentObjectType +import com.quadient.migration.shared.FileType import com.quadient.migration.shared.GroupOp import com.quadient.migration.shared.ImageOptions import com.quadient.migration.shared.ImageType @@ -149,6 +151,14 @@ def logo = new ImageBuilder("logo") .alternateText("Example logo image") .build() +def logoPdfFile = this.class.getClassLoader().getResource('exampleResources/migrationModelExample/logo.pdf') +migration.storage.write("logo.pdf", logoPdfFile.bytes) +def logoDocument = new FileBuilder("logoDocument").fileType(FileType.Document) + .sourcePath("logo.pdf") + .build() + +migration.fileRepository.upsert(logoDocument) + // Table containing some data with the first address row being optionally hidden // by using displayRuleRef to the display displayHeaderRule defined above. // The table also contains some merged cells and custom column widths. @@ -310,13 +320,13 @@ def firstMatchBlock = new DocumentObjectBuilder("firstMatch", DocumentObjectType .firstMatch { fb -> fb.case { cb -> cb.name("Czech Variant").appendContent(new ParagraphBuilder().styleRef(paragraphStyle.id).text { - it.appendContent("Nashledanou.") + it.string("Nashledanou.") }.build()).displayRule(displayRuleStateCzechia.id) }.case { cb -> cb.name("French Variant").appendContent(new ParagraphBuilder().styleRef(paragraphStyle.id).text { - it.appendContent("Au revoir.") + it.string("Au revoir.") }.build()).displayRule(displayRuleStateFrance.id) - }.default(new ParagraphBuilder().styleRef(paragraphStyle.id).text { it.appendContent("Goodbye.") }.build()) + }.default(new ParagraphBuilder().styleRef(paragraphStyle.id).text { it.string("Goodbye.") }.build()) }.build() // SelectByLanguage demonstrates language-based content selection. @@ -326,17 +336,17 @@ def selectByLanguageBlock = new DocumentObjectBuilder("selectByLanguage", Docume sb.case { cb -> cb.language("en_us") cb.appendContent(new ParagraphBuilder().styleRef(paragraphStyle.id).text { - it.appendContent("This document was created in English.") + it.string("This document was created in English.") }.build()) }.case { cb -> cb.language("de") cb.appendContent(new ParagraphBuilder().styleRef(paragraphStyle.id).text { - it.appendContent("Dieses Dokument wurde auf Deutsch erstellt.") + it.string("Dieses Dokument wurde auf Deutsch erstellt.") }.build()) }.case { cb -> cb.language("es") cb.appendContent(new ParagraphBuilder().styleRef(paragraphStyle.id).text { - it.appendContent("Este documento fue creado en español.") + it.string("Este documento fue creado en español.") }.build()) } }.build() @@ -395,6 +405,7 @@ def page = new DocumentObjectBuilder("page1", DocumentObjectType.Page) } .documentObjectRef(signature.id) } + .fileRef(logoDocument.id) .variableStructureRef(variableStructure.id) .build() diff --git a/migration-examples/src/main/resources/exampleResources/migrationModelExample/logo.pdf b/migration-examples/src/main/resources/exampleResources/migrationModelExample/logo.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cc1fe3c7f0b9676a0ea381c3d878ed699b77c364 GIT binary patch literal 3741 zcmbuCcU%+M7RRx{f)pF8L4+WpAPbWQA&4NoNQsmnkf4|hp(Z*BO{!9M6-9&qMrlg1 zP{g1VX^N|o06`WI6+}=#Py`i`1rgswaNYOW-S@}a%qO2a$;>_Hch9-^p6^v7nptR} zwed1)&tJ54$Vj1)C?xZMpNxS4U`Go9Sav7+n+wg+=M==;3tSH2^y% z?VlbIAto(|3PS3zb!UjlK*Fs($o>gQLZh|OC=`Lr&@eQV0U0!rFU)tJL@zqXLSo@R z90BLBgCJn)#r6td(g6|(v0yC#S{wJhBkp@g6JIuq2tuaJz=KQ%$Y3M&08?fF6LLD} zMFo*MUkzIL2CzX0un2$$WCl{1G!U?I6uCp804va!?!!i+bx?pU$e^=*kQf|+05}C5 zV2fl}2g1S@6zz35;0IFK0O?y-S6>?ZI#ve-3uCe2pan{WX1;m)$;png;npEFNJE}t z8L9#0pczh31{%VvS~pqk@e?ojO&UUU_eLQTwxr;3?IYvN@SMvbXFbESsL$-_S)Zef zjN;34l-i0vXWr<3@+os)gV?|RnDG1i`N zL$uMO%bzWyqoeKY?1F=Xot-fGmnjX7tl*qU0{z(%PATBl9UvT>9lM)gMKk^=Vt~uHPW8<=+WMnZjxMs4-vRu<6+wL(6c7bglWHc;zcida!{Fh;*T?kN?v1nMn?C82T1;xonvo$ zDk~XJ1#N8(c6L*&{^xfSCB((YK(&tfYSznHNo_mo^!QWWA&r_NP(?7C-DK!ZrBdD8 z9>3};y3^P=r|4hv1dhK_t{44MY3WBD@{8xsU%h%YH`dtPGI#IfMoCG@`pr!PA9>pW zU|OCf;Ap%QX1yLAeaEV5!Si^$2=nC0?!Nk%>>EcVMjj$)RkMfvJIM)qrI%!JFZYUP^9 zPaj(&GD}Of_w~$TGAhoxn48CAIOJ2!@v#NO`1p7M@z}9rBnQ)-{wtVoyAY-O8k9H@ z70hR)EiXU5=^>f&12YB09UIjIsvlq#=&4J4VKJb)EHyL%)42>nR#KStA z`@AzxpY~$9*RV$&sn_%Kx9m2aot@pRs(SL!;PT3hv|eIKaW&chjNAo!Nk76pNW41H zf!h+la{br}&P6U~i56w4LDbw{gqiLpQNYEA4D(uSTO``cNpGb~O1GAJ)?KkJtge3S zxr;zVl-1YwMXcJFC|yy$<{X}>E6#~@m|Xp#Sl+<2h!AVg7|>#-gf6SQrX3y(RiO(D zD#OE4+_`C~mS+-=ALoGhFJ%>QjOjjIh(Q&Qx|QcMv87OvaU$&buac9iWjE;$9wxcC zxCnUXa&EqchnPUh@_JSudCzvxn!XL4CTF9pckot3TwqT{b#_`rdb6VZqyv58X^rb0 z_uJU(ucSRH0?^)@l~rGSwW{$ca#%Is$#wozac0lWprV8Y&r1)u7sfj|G!=OVT$4QtZ$g(ciYCtkqrUzeq#?nbyM3LM#Pxtiun(MV$nrbHfO z59b>@pObZ@PEt*OnMFI|Xi6H3Exw!sOMY?hq(V}#e(YDnU5F2gZ`k6Mki`$@v47YQ z_8%twET^2_MskTd^;Vf1eZfX#uqx;FR59l&e)MTg2)w4HVasUx!JV4=%M4;fUD)EG zG4rl#WsOe83HK{yEy+8Bt;&|qTB5Y;kGEEFit}sP;v*-7@ZE^?14Ypo+7Y<~eOUGd z>@F5NA(v)Oj~O13Hs&7}inWTQ!zRvlT*0b0sOYcCqtqu0BbM*n5xbZwh{Za|UH-gA zVS3^j)9~o~@aKx&L$%nEg#@V>89h5=4MDurGa4Ei_s-lurHofLp4nL|TYeK7I?fv# zHp(#;jSbEZy)9eEdkq3Xz~N{>uA=(S8>!hw(R~rgjg2PMIDYE39-+;;_3Jg8=Rbc6 zNnM;*LL!AyVta3H$0+m3lP8=d{{H^05*h&DDn9deAhxd9%-%i;-QRiHsWqn?-UPDG zyU^dJoyc7AajD=ENj$*U7p*)f%-SJ9t_M7?XQXA$!z=Gz47}yzmCY7)K>{;N%Z;gS zHNhoyF|WI?;w=&r6U%W!HCe_`}+b?C=735BHL$>(=h z7M=HOh}!hykKWR1pCU=7S{@a)To)=euZwu#>CGU4k3){(jdhl=JJBAI0qi3Ur>3Vr z?CJ40w1>-tg+&5}Gq!SX>y=BFtPp-5#~vF+&6TaYQeW?h*uH(c4OFpCG(ALpBO@cr zy;~g%Ib81Jjt=V9U!;R(qV&P!xVYs-+j2+7#yCU9;H@-GSHVyMUk`JiSo$Tgo)@zu3vZNrtGacI8fwzbxygjw|C6{Jpfm-G0=rb?AIL5B(UJ5jDI;uxNBo$1D6=i zYTDgf5#fm4u!Ln1LE>QzWz&@H^#PB{Zqsa_B=wAOOTIg|1@iFlkdTnDls&CXt1K-o zwb!mo>uy#}NlA&CpYy#6XORNegeYE0fstF^#K_2o@XlMeV#tj-1eQhE+IIDxX=%}v zin%K6*WX?Fp;_TczIE#S1}E|0PLz>q1gH00XJ_YNKFJ~5;N4WA(Zdg;9pGo2YHrj+6EY!Vy!zaL7>G=qlxr|<$5C|7Zuao3{BaJW6s@}k{2@4R)qO$R6X z>UPmY)%N&@329Z@gW;i}g2KYHGZYvG-szoSh2b2>O_O&dw*2(kdgEysh3W#82h$s2 z2guwf@a*vO8`#z$WnJ-doP>p4k?5`**rp_*oelnfF}Dn&3BZ>Qd-=zjbuD zmbk0oSM#^!G*KjYv8bC&JqEmA{cA!Ji+M05$Jea=T?&TN1)BSYsk+~3)0D|z!;r~B z>U{-EJCNqgK*2wC(HJB~7efJvUXTbx7ho2KMP?901z9jy!*on_0@)q_OvOmR z84P737XZ^_u{B+6ttr4FZ6fg^M}~$A2r8oF?-VT3u!!t`L%54Hi~BD+by6SH8q~q! z98pC0!o)t{SJ%#FSBpz&&(t2Nl{z9Nc2Yy)^j||j_@AyUd?6MaiTei7I6PpFL@y|Y znc0aQ_-8=Il2F95YwaOIpjC4Zs&iUstC6JZgd+ZMyi4x#PgY%WR<;gW73Issy56n~ z|Em(TDAfNv>IEr;Kct{xDVTqh;tB`B1%=xK>zQuRA)@xcGI-@3A=X;Ww)Qem$WpN& zlEkOK5%H(f(!zlP&Uv6H#qil9D3HN|gF%Gg$w6f^AtV|D=kISaWNqf5PR3wxWUMZV zj78z$dmSL{|K*P__lt1TCUZ5f`hLkpMuVm6M?LwyPzZ|0? zmw?OPbqX7DS3c&%(f#>brGl?;8~vO5%m*aAc0RMQGh7Kt(f%4r7TXJAheDvY3>K?L MkWo`Jcd(H958>-cjsO4v literal 0 HcmV?d00001 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 e7189ee2..0f0b9283 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 @@ -116,7 +116,8 @@ class DesignerDocumentObjectBuilder( override fun getFilePath( id: String, name: String?, targetFolder: IcmPath?, sourcePath: String?, fileType: FileType ): String { - val fileName = name ?: id + val baseFileName = name ?: id + val fileName = appendExtensionIfMissing(baseFileName, sourcePath) if (targetFolder?.isAbsolute() == true) { return targetFolder.join(fileName).toString() 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 e365d90a..ee49a226 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 @@ -156,4 +156,32 @@ private val disallowedCharsRegex = Regex("[\\s\\-()?!.:;]") fun sanitizeVariablePart(part: String): String { return part.replace(disallowedCharsRegex, "_") +} + +fun extractExtensionFromPath(path: String?): String? { + if (path.isNullOrBlank()) return null + val lastDot = path.lastIndexOf('.') + val lastSlash = maxOf(path.lastIndexOf('/'), path.lastIndexOf('\\')) + + return if (lastDot > lastSlash && lastDot > 0 && lastDot < path.length - 1) { + ".${path.substring(lastDot + 1)}" + } else { + null + } +} + +fun appendExtensionIfMissing(fileName: String, sourcePath: String?): String { + val lastDot = fileName.lastIndexOf('.') + val hasExtension = lastDot > 0 && lastDot < fileName.length - 1 + + if (hasExtension) { + return fileName + } + + val extension = extractExtensionFromPath(sourcePath) + return if (extension != null) { + fileName + extension + } else { + fileName + } } \ No newline at end of file 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 b17d8fc7..d889b14e 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 @@ -112,7 +112,8 @@ class InteractiveDocumentObjectBuilder( override fun getFilePath( id: String, name: String?, targetFolder: IcmPath?, sourcePath: String?, fileType: FileType ): String { - val fileName = name ?: id + val baseFileName = name ?: id + val fileName = appendExtensionIfMissing(baseFileName, sourcePath) if (targetFolder?.isAbsolute() == true) { return targetFolder.join(fileName).toString() From b874b906190b57cfdc6addf5af03c66f860d53a6 Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Wed, 4 Feb 2026 10:57:03 +0100 Subject: [PATCH 4/9] first batch of unit tests for attachments and documents --- .../src/test/groovy/Utils.groovy | 3 + .../migration/service/DeployPhaseUtilsTest.kt | 39 ++++++++++++ .../DesignerDocumentObjectBuilderTest.kt | 60 +++++++++++++++++++ .../tools/model/TestModelObjectBuilders.kt | 25 ++++++++ 4 files changed, 127 insertions(+) diff --git a/migration-examples/src/test/groovy/Utils.groovy b/migration-examples/src/test/groovy/Utils.groovy index fbb20097..5207dcb8 100644 --- a/migration-examples/src/test/groovy/Utils.groovy +++ b/migration-examples/src/test/groovy/Utils.groovy @@ -1,6 +1,7 @@ import com.quadient.migration.api.Migration import com.quadient.migration.api.ProjectConfig import com.quadient.migration.api.repository.DocumentObjectRepository +import com.quadient.migration.api.repository.FileRepository import com.quadient.migration.api.repository.ImageRepository import com.quadient.migration.api.repository.MappingRepository import com.quadient.migration.api.repository.ParagraphStyleRepository @@ -24,6 +25,7 @@ static Migration mockMigration() { def mappingRepo = mock(MappingRepository.class) def docObjectRepo = mock(DocumentObjectRepository.class) def imageRepo = mock(ImageRepository.class) + def fileRepo = mock(FileRepository.class) def statusTrackingRepo = mock(StatusTrackingRepository.class) def textStyleRepo = mock(TextStyleRepository.class) def paraStyleRepo = mock(ParagraphStyleRepository.class) @@ -32,6 +34,7 @@ static Migration mockMigration() { when(migration.getTextStyleRepository()).thenReturn(textStyleRepo) when(migration.getStatusTrackingRepository()).thenReturn(statusTrackingRepo) when(migration.getImageRepository()).thenReturn(imageRepo) + when(migration.getFileRepository()).thenReturn(fileRepo) when(migration.getDocumentObjectRepository()).thenReturn(docObjectRepo) when(migration.getVariableRepository()).thenReturn(varRepo) when(migration.getVariableStructureRepository()).thenReturn(structureRepo) 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 e5991168..cbd4f99a 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 @@ -1,5 +1,7 @@ package com.quadient.migration.service +import com.quadient.migration.service.inspirebuilder.appendExtensionIfMissing +import com.quadient.migration.service.inspirebuilder.extractExtensionFromPath import com.quadient.migration.tools.aProjectConfig import com.quadient.migration.tools.shouldBeEqualTo import org.junit.jupiter.api.Test @@ -37,4 +39,41 @@ class DeployPhaseUtilsTest { result.shouldBeEqualTo("icm://Interactive/StandardPackage/BaseTemplates/myBT.wfd") } + + @Test + fun `extractExtensionFromPath handles various path formats correctly`() { + // Valid extensions + extractExtensionFromPath("file.pdf").shouldBeEqualTo(".pdf") + extractExtensionFromPath("C:/folder/file.txt").shouldBeEqualTo(".txt") + extractExtensionFromPath("folder/subfolder/file.bat").shouldBeEqualTo(".bat") + extractExtensionFromPath("archive.tar.gz").shouldBeEqualTo(".gz") + extractExtensionFromPath("C:\\Windows\\Path\\file.docx").shouldBeEqualTo(".docx") + + // Invalid cases + extractExtensionFromPath(null).shouldBeEqualTo(null) + extractExtensionFromPath("").shouldBeEqualTo(null) + extractExtensionFromPath(" ").shouldBeEqualTo(null) + extractExtensionFromPath("C:/folder/filename").shouldBeEqualTo(null) + extractExtensionFromPath("folder.ext/filename").shouldBeEqualTo(null) + extractExtensionFromPath(".gitignore").shouldBeEqualTo(null) + extractExtensionFromPath("file.").shouldBeEqualTo(null) + } + + @Test + fun `appendExtensionIfMissing handles various scenarios correctly`() { + // Appends extension when missing + appendExtensionIfMissing("document", "C:/file.pdf").shouldBeEqualTo("document.pdf") + appendExtensionIfMissing("file", "folder/doc.txt").shouldBeEqualTo("file.txt") + appendExtensionIfMissing("file", "C:\\folder\\doc.bat").shouldBeEqualTo("file.bat") + + // Preserves existing extension + appendExtensionIfMissing("report.docx", "C:/file.pdf").shouldBeEqualTo("report.docx") + appendExtensionIfMissing("archive.tar.gz", "file.txt").shouldBeEqualTo("archive.tar.gz") + + // Handles null/blank/invalid sourcePath gracefully + appendExtensionIfMissing("file", null).shouldBeEqualTo("file") + appendExtensionIfMissing("file", "").shouldBeEqualTo("file") + appendExtensionIfMissing("file", "noext").shouldBeEqualTo("file") + appendExtensionIfMissing("file", "folder.ext/noext").shouldBeEqualTo("file") + } } \ No newline at end of file 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 3e668ea6..7d2598c0 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 @@ -28,6 +28,7 @@ import com.quadient.migration.shared.BinOp import com.quadient.migration.shared.DataType import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.DocumentObjectType.* +import com.quadient.migration.shared.FileType import com.quadient.migration.shared.IcmPath import com.quadient.migration.shared.Literal import com.quadient.migration.shared.LiteralDataType @@ -44,6 +45,7 @@ import com.quadient.migration.tools.model.aVariable import com.quadient.migration.tools.model.aDisplayRule import com.quadient.migration.tools.model.aDocObj import com.quadient.migration.tools.model.aDocumentObjectRef +import com.quadient.migration.tools.model.aFile import com.quadient.migration.tools.model.aImage import com.quadient.migration.tools.model.aParagraph import com.quadient.migration.tools.model.aRow @@ -898,5 +900,63 @@ class DesignerDocumentObjectBuilderTest { null -> "" else -> this.trim() } + + @ParameterizedTest + @CsvSource( + // fileType,paths.documents,paths.attachments,targetFolder,defaultTargetFolder,expected + "Document,,,, ,icm://File_F1.pdf", + "Document,,,relative, ,icm://relative/File_F1.pdf", + "Document,Docs,,relative, ,icm://Docs/relative/File_F1.pdf", + "Document,,,icm://absolute/, ,icm://absolute/File_F1.pdf", + "Document,,, ,def,icm://def/File_F1.pdf", + "Attachment,,,, ,icm://File_F1.pdf", + "Attachment,,Attach,relative, ,icm://Attach/relative/File_F1.pdf", + "Attachment,,,icm://absolute/, ,icm://absolute/File_F1.pdf", + ) + fun testFilePath( + fileType: String, + documentsPath: String?, + attachmentsPath: String?, + targetFolder: String?, + defaultTargetFolder: String?, + expected: String + ) { + val config = aProjectConfig( + output = InspireOutput.Designer, + paths = PathsConfig( + documents = documentsPath.nullToNull()?.let(IcmPath::from), + attachments = attachmentsPath.nullToNull()?.let(IcmPath::from) + ), + targetDefaultFolder = defaultTargetFolder.nullToNull(), + ) + val pathTestSubject = aSubject(config) + val file = aFile("F1", targetFolder = targetFolder.nullToNull(), fileType = FileType.valueOf(fileType)) + + val path = pathTestSubject.getFilePath(file) + + path.shouldBeEqualTo(expected) + } + + @Test + fun `file path appends extension from sourcePath when fileName lacks one`() { + val config = aProjectConfig(output = InspireOutput.Designer) + val pathTestSubject = aSubject(config) + val file = aFile("F1", name = "document", sourcePath = "C:/files/doc.pdf") + + val path = pathTestSubject.getFilePath(file) + + path.shouldBeEqualTo("icm://document.pdf") + } + + @Test + fun `file path preserves fileName extension when present`() { + val config = aProjectConfig(output = InspireOutput.Designer) + val pathTestSubject = aSubject(config) + val file = aFile("F1", name = "report.docx", sourcePath = "file.pdf") + + val path = pathTestSubject.getFilePath(file) + + path.shouldBeEqualTo("icm://report.docx") + } } } \ No newline at end of file diff --git a/migration-library/src/test/kotlin/com/quadient/migration/tools/model/TestModelObjectBuilders.kt b/migration-library/src/test/kotlin/com/quadient/migration/tools/model/TestModelObjectBuilders.kt index 2fb25d99..f31dd524 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/tools/model/TestModelObjectBuilders.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/tools/model/TestModelObjectBuilders.kt @@ -6,6 +6,7 @@ import com.quadient.migration.data.DisplayRuleModelRef import com.quadient.migration.data.DocumentContentModel import com.quadient.migration.data.DocumentObjectModel import com.quadient.migration.data.DocumentObjectModelRef +import com.quadient.migration.data.FileModel import com.quadient.migration.data.ImageModel import com.quadient.migration.data.ParagraphModel import com.quadient.migration.data.ParagraphModel.TextModel @@ -50,6 +51,7 @@ import com.quadient.migration.shared.DataType import com.quadient.migration.shared.DisplayRuleDefinition import com.quadient.migration.shared.DocumentObjectOptions import com.quadient.migration.shared.DocumentObjectType +import com.quadient.migration.shared.FileType import com.quadient.migration.shared.Group import com.quadient.migration.shared.GroupOp import com.quadient.migration.shared.IcmPath @@ -404,6 +406,29 @@ fun aImage( ) } +fun aFile( + id: String, + name: String = "File_$id", + sourcePath: String? = "$name.pdf", + fileType: FileType = FileType.Document, + originLocations: List = emptyList(), + customFields: MutableMap = mutableMapOf(), + targetFolder: String? = null, + skip: SkipOptions = SkipOptions(false, null, null), +): FileModel { + return FileModel( + id = id, + name = name, + originLocations = originLocations, + customFields = customFields, + created = Clock.System.now(), + sourcePath = sourcePath, + fileType = fileType, + targetFolder = targetFolder?.let(IcmPath::from), + skip = skip, + ) +} + fun aDocumentObjectRef(id: String, displayRuleId: String? = null) = DocumentObjectModelRef(id, displayRuleId?.let { DisplayRuleModelRef(it) }) From b601ef6a4ae915179a11cc9915a21d25356f9bf3 Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Wed, 4 Feb 2026 11:24:23 +0100 Subject: [PATCH 5/9] fix files mapping and related unit tests --- .../example/common/mapping/FilesExport.groovy | 3 +- .../example/common/mapping/FilesImport.groovy | 4 ++ .../src/test/groovy/AreasExportTest.groovy | 2 +- .../src/test/groovy/AreasImportTest.groovy | 2 +- .../test/groovy/FilesMappingExportTest.groovy | 50 +++++++++++++++++ .../test/groovy/FilesMappingImportTest.groovy | 55 +++++++++++++++++++ .../groovy/ImagesMappingImportTest.groovy | 2 +- .../ParagraphStylesMappingExportTest.groovy | 2 +- .../ParagraphStylesMappingImportTest.groovy | 2 +- .../groovy/TextStylesMappingExportTest.groovy | 2 +- .../groovy/TextStylesMappingImportTest.groovy | 2 +- 11 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 migration-examples/src/test/groovy/FilesMappingExportTest.groovy create mode 100644 migration-examples/src/test/groovy/FilesMappingImportTest.groovy diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/FilesExport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/FilesExport.groovy index 0d328978..361bdcfc 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/FilesExport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/FilesExport.groovy @@ -27,7 +27,7 @@ static void run(Migration migration, Path filesDstPath) { filesDstPath.toFile().createParentDirectories() filesDstPath.toFile().withWriter { writer -> - def headers = ["id", "name", "sourcePath", "targetFolder", "status", "skip", "skipPlaceholder", "skipReason", Mapping.displayHeader("originalName", true), Mapping.displayHeader("originLocations", true)] + def headers = ["id", "name", "sourcePath", "fileType", "targetFolder", "status", "skip", "skipPlaceholder", "skipReason", Mapping.displayHeader("originalName", true), Mapping.displayHeader("originLocations", true)] writer.writeLine(headers.join(",")) files.each { obj -> def status = migration.statusTrackingRepository.findLastEventRelevantToOutput(obj.id, @@ -38,6 +38,7 @@ static void run(Migration migration, Path filesDstPath) { builder.append(Csv.serialize(obj.id)) builder.append("," + Csv.serialize(obj.name)) builder.append("," + Csv.serialize(obj.sourcePath)) + builder.append("," + Csv.serialize(obj.fileType)) builder.append("," + Csv.serialize(obj.targetFolder)) builder.append("," + Csv.serialize(status.class.simpleName)) builder.append("," + Csv.serialize(obj.skip.skipped)) diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/FilesImport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/FilesImport.groovy index 630d1d30..80ef6293 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/FilesImport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/FilesImport.groovy @@ -10,6 +10,7 @@ import com.quadient.migration.api.Migration import com.quadient.migration.example.common.util.Csv import com.quadient.migration.example.common.util.Mapping import com.quadient.migration.service.deploy.ResourceType +import com.quadient.migration.shared.FileType import com.quadient.migration.shared.SkipOptions import java.nio.file.Path @@ -48,6 +49,9 @@ static void run(Migration migration, Path filesFilePath) { def newSourcePath = Csv.deserialize(values.get("sourcePath"), String.class) Mapping.mapProp(existingMapping, existingFile, "sourcePath", newSourcePath) + def newFileType = Csv.deserialize(values.get("fileType"), FileType.class) + Mapping.mapProp(existingMapping, existingFile, "fileType", newFileType) + def newTargetFolder = Csv.deserialize(values.get("targetFolder"), String.class) Mapping.mapProp(existingMapping, existingFile, "targetFolder", newTargetFolder) diff --git a/migration-examples/src/test/groovy/AreasExportTest.groovy b/migration-examples/src/test/groovy/AreasExportTest.groovy index 5c71b2e6..769a8b8c 100644 --- a/migration-examples/src/test/groovy/AreasExportTest.groovy +++ b/migration-examples/src/test/groovy/AreasExportTest.groovy @@ -19,7 +19,7 @@ import static org.mockito.Mockito.when class AreasExportTest { @TempDir - File dir + java.io.File dir Migration migration diff --git a/migration-examples/src/test/groovy/AreasImportTest.groovy b/migration-examples/src/test/groovy/AreasImportTest.groovy index a33f13d6..7579b46e 100644 --- a/migration-examples/src/test/groovy/AreasImportTest.groovy +++ b/migration-examples/src/test/groovy/AreasImportTest.groovy @@ -19,7 +19,7 @@ import static org.mockito.Mockito.times class AreasImportTest { @TempDir - File dir + java.io.File dir Migration migration diff --git a/migration-examples/src/test/groovy/FilesMappingExportTest.groovy b/migration-examples/src/test/groovy/FilesMappingExportTest.groovy new file mode 100644 index 00000000..fc5baea8 --- /dev/null +++ b/migration-examples/src/test/groovy/FilesMappingExportTest.groovy @@ -0,0 +1,50 @@ +import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap +import com.quadient.migration.api.dto.migrationmodel.File +import com.quadient.migration.data.Active +import com.quadient.migration.example.common.mapping.FilesExport +import com.quadient.migration.shared.FileType +import com.quadient.migration.shared.SkipOptions +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +import java.nio.file.Path +import java.nio.file.Paths + +import static org.mockito.ArgumentMatchers.any +import static org.mockito.Mockito.when + +class FilesMappingExportTest { + @TempDir + java.io.File dir + + @Test + void allPossibleFiles() { + Path mappingFile = Paths.get(dir.path, "testProject.csv") + def migration = Utils.mockMigration() + + when(migration.fileRepository.listAll()).thenReturn([ + new File("empty", null, [], new CustomFieldMap([:]), null, null, FileType.Document, emptySkipOptions()), + new File("full", "full", ["foo", "bar"], new CustomFieldMap([:]), "sourcePath", "targetDir", FileType.Document, new SkipOptions(true, "placeholder", "reason")), + new File("overridden empty", null, [], new CustomFieldMap([:]), null, null, FileType.Document, emptySkipOptions()), + new File("overridden full", "full", ["foo", "bar"], new CustomFieldMap(["originalName": "originalFull"]), "sourcePath", "targetDir", FileType.Attachment, emptySkipOptions()), + ]) + + when(migration.statusTrackingRepository.findLastEventRelevantToOutput(any(), any(), any())).thenReturn(new Active()) + + FilesExport.run(migration, mappingFile) + + def expected = """\ + id,name,sourcePath,fileType,targetFolder,status,skip,skipPlaceholder,skipReason,originalName (read-only),originLocations (read-only) + empty,,,Document,,Active,false,,,,[] + full,full,sourcePath,Document,targetDir,Active,true,placeholder,reason,,[foo; bar] + overridden empty,,,Document,,Active,false,,,,[] + overridden full,full,sourcePath,Attachment,targetDir,Active,false,,,originalFull,[foo; bar] + """.stripIndent() + Assertions.assertEquals(expected, mappingFile.toFile().text.replaceAll("\\r\\n|\\r", "\n")) + } + + static SkipOptions emptySkipOptions() { + return new SkipOptions(false, null, null) + } +} diff --git a/migration-examples/src/test/groovy/FilesMappingImportTest.groovy b/migration-examples/src/test/groovy/FilesMappingImportTest.groovy new file mode 100644 index 00000000..3c3c03e1 --- /dev/null +++ b/migration-examples/src/test/groovy/FilesMappingImportTest.groovy @@ -0,0 +1,55 @@ +import com.quadient.migration.api.Migration +import com.quadient.migration.api.dto.migrationmodel.* +import com.quadient.migration.example.common.mapping.FilesImport +import com.quadient.migration.shared.FileType +import com.quadient.migration.shared.SkipOptions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +import java.nio.file.Path +import java.nio.file.Paths + +import static org.mockito.Mockito.* + +class FilesMappingImportTest { + @TempDir + java.io.File dir + + @Test + void importsFileMapping() { + def migration = Utils.mockMigration() + Path mappingFile = Paths.get(dir.path, "testProject.csv") + def input = """\ + id,name,sourcePath,fileType,targetFolder,status,originLocations,skip,skipPlaceholder,skipReason + file1,newName,newPath,Document,newFolder,Active,[],false,, + file2,,,Attachment,,Active,[],true,placeholder,reason + """.stripIndent() + mappingFile.toFile().write(input) + + givenExistingFile(migration, "file1", "oldName", "oldFolder", "oldPath", FileType.Attachment) + givenExistingFileMapping(migration, "file1", "oldName", "oldFolder", "oldPath", FileType.Attachment) + givenExistingFile(migration, "file2", "someName", "someFolder", "somePath", FileType.Document) + givenExistingFileMapping(migration, "file2", null, null, null, null) + + FilesImport.run(migration, mappingFile) + + verify(migration.mappingRepository, times(1)).upsert("file1", new MappingItem.File("newName", "newFolder", "newPath", FileType.Document, new SkipOptions(false, null, null))) + verify(migration.mappingRepository, times(1)).applyFileMapping("file1") + verify(migration.mappingRepository, times(1)).upsert("file2", new MappingItem.File(null, null, null, FileType.Attachment, new SkipOptions(true, "placeholder", "reason"))) + verify(migration.mappingRepository, times(1)).applyFileMapping("file2") + } + + static void givenExistingFile(Migration mig, String id, String name, String targetFolder, String sourcePath, FileType fileType) { + when(mig.fileRepository.find(id)).thenReturn(new File(id, name, [], new CustomFieldMap([:]), sourcePath, targetFolder, fileType, new SkipOptions(false, null, null))) + } + + static void givenExistingFileMapping(Migration mig, + String id, + String name, + String targetFolder, + String sourcePath, + FileType fileType) { + when(mig.mappingRepository.getFileMapping(id)) + .thenReturn(new MappingItem.File(name, targetFolder, sourcePath, fileType, null)) + } +} diff --git a/migration-examples/src/test/groovy/ImagesMappingImportTest.groovy b/migration-examples/src/test/groovy/ImagesMappingImportTest.groovy index 8e96f583..3d6749a0 100644 --- a/migration-examples/src/test/groovy/ImagesMappingImportTest.groovy +++ b/migration-examples/src/test/groovy/ImagesMappingImportTest.groovy @@ -13,7 +13,7 @@ import static org.mockito.Mockito.* class ImagesMappingImportTest { @TempDir - File dir + java.io.File dir @Test void overridesImageName() { diff --git a/migration-examples/src/test/groovy/ParagraphStylesMappingExportTest.groovy b/migration-examples/src/test/groovy/ParagraphStylesMappingExportTest.groovy index 6f78ef08..97ea1db7 100644 --- a/migration-examples/src/test/groovy/ParagraphStylesMappingExportTest.groovy +++ b/migration-examples/src/test/groovy/ParagraphStylesMappingExportTest.groovy @@ -16,7 +16,7 @@ import static org.mockito.Mockito.when class ParagraphStylesMappingExportTest { @TempDir - File dir + java.io.File dir @Test void exportWorksCorrectlyForAllVariants() { diff --git a/migration-examples/src/test/groovy/ParagraphStylesMappingImportTest.groovy b/migration-examples/src/test/groovy/ParagraphStylesMappingImportTest.groovy index 106f7500..278ed971 100644 --- a/migration-examples/src/test/groovy/ParagraphStylesMappingImportTest.groovy +++ b/migration-examples/src/test/groovy/ParagraphStylesMappingImportTest.groovy @@ -14,7 +14,7 @@ import static org.mockito.Mockito.when class ParagraphStylesMappingImportTest { @TempDir - File dir + java.io.File dir Migration migration diff --git a/migration-examples/src/test/groovy/TextStylesMappingExportTest.groovy b/migration-examples/src/test/groovy/TextStylesMappingExportTest.groovy index 3bb9e14f..26480294 100644 --- a/migration-examples/src/test/groovy/TextStylesMappingExportTest.groovy +++ b/migration-examples/src/test/groovy/TextStylesMappingExportTest.groovy @@ -15,7 +15,7 @@ import static org.mockito.Mockito.when class TextStylesMappingExportTest { @TempDir - File dir + java.io.File dir @Test void exportWorksCorrectlyForAllVariants() { diff --git a/migration-examples/src/test/groovy/TextStylesMappingImportTest.groovy b/migration-examples/src/test/groovy/TextStylesMappingImportTest.groovy index 827c8700..a1e51b32 100644 --- a/migration-examples/src/test/groovy/TextStylesMappingImportTest.groovy +++ b/migration-examples/src/test/groovy/TextStylesMappingImportTest.groovy @@ -16,7 +16,7 @@ import static org.mockito.Mockito.when class TextStylesMappingImportTest { @TempDir - File dir + java.io.File dir Migration migration From 55436f032685eb1bf4897100724fa767ddfebc07 Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Wed, 4 Feb 2026 11:52:06 +0100 Subject: [PATCH 6/9] another batch of unit tests and related updates --- .../InteractiveDocumentObjectBuilder.kt | 4 +- .../InteractiveDocumentObjectBuilderTest.kt | 52 ++++++++++++++++++- 2 files changed, 53 insertions(+), 3 deletions(-) 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 d889b14e..9c32d240 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 @@ -120,8 +120,8 @@ class InteractiveDocumentObjectBuilder( } val fileConfigPath = when (fileType) { - FileType.Document -> projectConfig.paths.documents ?: IcmPath.from("Resources/Documents") - FileType.Attachment -> projectConfig.paths.attachments ?: IcmPath.from("Resources/Attachments") + FileType.Document -> projectConfig.paths.documents.orDefault("Documents") + FileType.Attachment -> projectConfig.paths.attachments.orDefault("Attachments") } return IcmPath.root().join("Interactive").join(projectConfig.interactiveTenant).join(fileConfigPath) 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 671b5d96..ab5e3b43 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 @@ -33,6 +33,7 @@ import com.quadient.migration.shared.BinOp.* import com.quadient.migration.shared.DataType import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.DocumentObjectType.* +import com.quadient.migration.shared.FileType import com.quadient.migration.shared.IcmPath import com.quadient.migration.shared.ImageOptions import com.quadient.migration.shared.ImageType @@ -54,6 +55,7 @@ import com.quadient.migration.tools.aProjectConfig import com.quadient.migration.tools.model.aCell import com.quadient.migration.tools.model.aDocObj import com.quadient.migration.tools.model.aDocumentObjectRef +import com.quadient.migration.tools.model.aFile import com.quadient.migration.tools.model.aImage import com.quadient.migration.tools.model.aRow import com.quadient.migration.tools.model.aSelectByLanguage @@ -1614,8 +1616,56 @@ class InteractiveDocumentObjectBuilderTest { private fun String?.nullToNull() = when (this?.trim()) { "null" -> null - null -> "" + null -> null else -> this.trim() } + + @ParameterizedTest + @CsvSource( + // fileType,paths.documents,paths.attachments,targetFolder,defaultTargetFolder,expected + "Document,,,, ,icm://Interactive/tenant/Documents/File_F1.pdf", + "Document,,,relative, ,icm://Interactive/tenant/Documents/relative/File_F1.pdf", + "Document,Docs,,relative, ,icm://Interactive/tenant/Docs/relative/File_F1.pdf", + "Document,,,icm://absolute/, ,icm://absolute/File_F1.pdf", + "Document,,, ,def,icm://Interactive/tenant/Documents/def/File_F1.pdf", + "Attachment,,,, ,icm://Interactive/tenant/Attachments/File_F1.pdf", + "Attachment,,Attach,relative, ,icm://Interactive/tenant/Attach/relative/File_F1.pdf", + "Attachment,,,icm://absolute/, ,icm://absolute/File_F1.pdf", + ) + fun testFilePath( + fileType: String, + documentsPath: String?, + attachmentsPath: String?, + targetFolder: String?, + defaultTargetFolder: String?, + expected: String + ) { + val config = aProjectConfig( + output = InspireOutput.Interactive, + interactiveTenant = "tenant", + paths = PathsConfig( + documents = documentsPath.nullToNull()?.let(IcmPath::from), + attachments = attachmentsPath.nullToNull()?.let(IcmPath::from) + ), + targetDefaultFolder = defaultTargetFolder.nullToNull(), + ) + val pathTestSubject = aSubject(config) + val file = aFile("F1", targetFolder = targetFolder.nullToNull(), fileType = FileType.valueOf(fileType)) + + val path = pathTestSubject.getFilePath(file) + + path.shouldBeEqualTo(expected) + } + + @Test + fun `file path appends extension from sourcePath when fileName lacks one`() { + val config = aProjectConfig(output = InspireOutput.Interactive, interactiveTenant = "tenant") + val pathTestSubject = aSubject(config) + val file = aFile("F1", name = "document", sourcePath = "C:/files/doc.pdf") + + val path = pathTestSubject.getFilePath(file) + + path.shouldBeEqualTo("icm://Interactive/tenant/Documents/document.pdf") + } } } \ No newline at end of file From 403650ba6da5a5972723e2de60ad5c3639e8ad65 Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Wed, 4 Feb 2026 16:12:52 +0100 Subject: [PATCH 7/9] unit tests for deploy clients --- .../deploy/DesignerDeployClientTest.kt | 37 ++++++++- .../deploy/InteractiveDeployClientTest.kt | 58 +++++++++++--- .../InspireDocumentObjectBuilderTest.kt | 76 +++++++++++++++++++ 3 files changed, 156 insertions(+), 15 deletions(-) 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 253306ae..a1f0b0c9 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 @@ -8,6 +8,8 @@ import com.quadient.migration.data.Active import com.quadient.migration.data.Deployed import com.quadient.migration.data.DocumentObjectModel import com.quadient.migration.data.Error +import com.quadient.migration.data.FileModel +import com.quadient.migration.data.FileModelRef import com.quadient.migration.data.ImageModel import com.quadient.migration.data.ImageModelRef import com.quadient.migration.data.StringModel @@ -29,6 +31,7 @@ import com.quadient.migration.tools.aErrorStatus import com.quadient.migration.tools.model.aBlock import com.quadient.migration.tools.model.aDocObj import com.quadient.migration.tools.model.aDocumentObjectRef +import com.quadient.migration.tools.model.aFile import com.quadient.migration.tools.model.aImage import com.quadient.migration.tools.model.aParagraph import com.quadient.migration.tools.model.aTemplate @@ -83,14 +86,16 @@ class DesignerDeployClientTest { } @Test - fun `deployDocumentObjects deploys complex structure template`() { + fun `deployDocumentObjects deploys complex structure template with images and files`() { // given val image1 = mockImg(aImage("I_1")) val image2 = mockImg(aImage("I_2")) + val file1 = mockFile(aFile("F_1")) val externalBlock = mockObj( aDocObj( - "Txt_Img_1", DocumentObjectType.Block, listOf( - aParagraph(aText(StringModel("Image: "))), ImageModelRef(image1.id) + "Txt_Img_File_1", DocumentObjectType.Block, listOf( + aParagraph(aText(StringModel("Image: "))), ImageModelRef(image1.id), + aParagraph(aText(StringModel("File: "))), FileModelRef(file1.id) ) ) ) @@ -116,13 +121,17 @@ class DesignerDeployClientTest { every { ipsService.fileExists(any()) } returns false // when - subject.deployDocumentObjects() + val deploymentResult = subject.deployDocumentObjects() // then + deploymentResult.deployed.size.shouldBeEqualTo(5) + deploymentResult.errors.shouldBeEqualTo(emptyList()) + verify { ipsService.xml2wfd(any(), "icm://${template.nameOrId()}") } verify { ipsService.xml2wfd(any(), "icm://${externalBlock.nameOrId()}") } verify { ipsService.tryUpload("icm://${image1.nameOrId()}", any()) } verify { ipsService.tryUpload("icm://${image2.nameOrId()}", any()) } + verify { ipsService.tryUpload("icm://${file1.nameOrId()}", any()) } } @Test @@ -349,6 +358,26 @@ class DesignerDeployClientTest { return image } + private fun mockFile(file: FileModel, success: Boolean = true): FileModel { + val filePath = "icm://${file.nameOrId()}" + + every { documentObjectBuilder.getFilePath(file) } returns filePath + every { fileRepository.findModel(file.id) } returns file + if (!file.sourcePath.isNullOrBlank()) { + val byteArray = ByteArray(10) + every { storage.read(file.sourcePath) } answers { + if (success) { + byteArray + } else { + throw Exception() + } + } + every { ipsService.tryUpload(filePath, byteArray) } returns OperationResult.Success + } + + return file + } + private fun mockObj(documentObject: DocumentObjectModel): DocumentObjectModel { every { documentObjectRepository.findModel(documentObject.id) } returns documentObject 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 420a43df..e0444830 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 @@ -8,6 +8,8 @@ import com.quadient.migration.data.Active import com.quadient.migration.data.Deployed import com.quadient.migration.data.DocumentObjectModel import com.quadient.migration.data.Error +import com.quadient.migration.data.FileModel +import com.quadient.migration.data.FileModelRef import com.quadient.migration.data.ImageModel import com.quadient.migration.data.ImageModelRef import com.quadient.migration.data.ParagraphModel @@ -36,6 +38,7 @@ import com.quadient.migration.tools.aProjectConfig import com.quadient.migration.tools.model.aBlock import com.quadient.migration.tools.model.aDocObj import com.quadient.migration.tools.model.aDocumentObjectRef +import com.quadient.migration.tools.model.aFile import com.quadient.migration.tools.model.aImage import com.quadient.migration.tools.model.aParagraph import com.quadient.migration.tools.model.aTemplate @@ -236,10 +239,11 @@ class InteractiveDeployClientTest { } @Test - fun `deployDocumentObjects deploys images when used in document objects`() { + fun `deployDocumentObjects deploys images and files when used in document objects`() { // given val image = mockImage(aImage("Bunny")) - val block = mockDocumentObject(aBlock(id = "1", listOf(ImageModelRef(image.id)))) + val file = mockFile(aFile("Report")) + val block = mockDocumentObject(aBlock(id = "1", listOf(ImageModelRef(image.id), FileModelRef(file.id)))) every { documentObjectRepository.list(any()) } returns listOf(block) every { documentObjectBuilder.buildDocumentObject(any(), any()) } returns "" @@ -249,27 +253,39 @@ class InteractiveDeployClientTest { mockBasicSuccessfulIpsOperations() val expectedImageIcmPath = "icm://Interactive/$tenant/Resources/Images/defaultFolder/${image.sourcePath}" + val expectedFileIcmPath = "icm://Interactive/$tenant/Resources/Files/defaultFolder/${file.sourcePath}" // when - subject.deployDocumentObjects() + val deploymentResult = subject.deployDocumentObjects() // then + deploymentResult.deployed.size.shouldBeEqualTo(3) + deploymentResult.errors.shouldBeEqualTo(emptyList()) + verify { ipsService.tryUpload(expectedImageIcmPath, any()) } + verify { ipsService.tryUpload(expectedFileIcmPath, any()) } verifyBasicIpsOperations( listOf( - expectedImageIcmPath, "icm://Interactive/$tenant/Blocks/defaultFolder/${block.id}.jld" + expectedImageIcmPath, expectedFileIcmPath, "icm://Interactive/$tenant/Blocks/defaultFolder/${block.id}.jld" ), 1 ) } @Test - fun `Images with unknown type or missing source path are omitted from deployment`() { + fun `Images with unknown type or missing source path and files with missing source path or skip flag are omitted from deployment`() { // given val catImage = mockImage(aImage("Cat", imageType = ImageType.Unknown)) val dogImage = mockImage(aImage("Dog", sourcePath = null)) + val missingFile = mockFile(aFile("MissingDoc", sourcePath = null)) + val skippedFile = mockFile(aFile("SkippedDoc", skip = SkipOptions(true, null, "Not needed"))) val block = mockDocumentObject( - aBlock("1", listOf(ImageModelRef(catImage.id), ImageModelRef(dogImage.id))) + aBlock("1", listOf( + ImageModelRef(catImage.id), + ImageModelRef(dogImage.id), + FileModelRef(missingFile.id), + FileModelRef(skippedFile.id) + )) ) every { documentObjectRepository.list(any()) } returns listOf(block) @@ -282,23 +298,29 @@ class InteractiveDeployClientTest { mockBasicSuccessfulIpsOperations() // when - subject.deployDocumentObjects() + val deploymentResult = subject.deployDocumentObjects() // then + deploymentResult.deployed.size.shouldBeEqualTo(1) + deploymentResult.warnings.size.shouldBeEqualTo(4) + deploymentResult.errors.shouldBeEqualTo(emptyList()) + verify(exactly = 0) { ipsService.upload(any(), any()) } verifyBasicIpsOperations(listOf("icm://Interactive/$tenant/Blocks/defaultFolder/${block.id}.jld")) } @Test - fun `Multiple times used image is deployed only once`() { + fun `Multiple times used image or file is deployed only once`() { // given val image = mockImage(aImage("Bunny")) - val innerBlock = aBlock("10", listOf(ImageModelRef(image.id)), internal = true) + val file = mockFile(aFile("Report")) + val innerBlock = aBlock("10", listOf(ImageModelRef(image.id), FileModelRef(file.id)), internal = true) val block = mockDocumentObject( aBlock( "1", listOf( aDocumentObjectRef(innerBlock.id), - aParagraph(aText(ImageModelRef(image.id))) + aParagraph(aText(ImageModelRef(image.id))), + aParagraph(aText(FileModelRef(file.id))) ) ) ) @@ -312,15 +334,17 @@ class InteractiveDeployClientTest { mockBasicSuccessfulIpsOperations() val expectedImageIcmPath = "icm://Interactive/$tenant/Resources/Images/defaultFolder/${image.sourcePath}" + val expectedFileIcmPath = "icm://Interactive/$tenant/Resources/Files/defaultFolder/${file.sourcePath}" // when subject.deployDocumentObjects() // then verify(exactly = 1) { ipsService.tryUpload(expectedImageIcmPath, any()) } + verify(exactly = 1) { ipsService.tryUpload(expectedFileIcmPath, any()) } verifyBasicIpsOperations( listOf( - expectedImageIcmPath, "icm://Interactive/$tenant/Blocks/defaultFolder/${block.id}.jld" + expectedImageIcmPath, expectedFileIcmPath, "icm://Interactive/$tenant/Blocks/defaultFolder/${block.id}.jld" ), 1 ) } @@ -652,6 +676,18 @@ class InteractiveDeployClientTest { return image } + private fun mockFile(file: FileModel, success: Boolean = true): FileModel { + val dir = resolveTargetDir(config.defaultTargetFolder) + every { documentObjectBuilder.getFilePath(file) } returns "icm://Interactive/$tenant/Resources/Files/$dir/${file.sourcePath}" + + every { fileRepository.findModel(file.id) } returns if (success) { file } else { null } + if (!file.sourcePath.isNullOrBlank()) { + every { storage.read(file.sourcePath) } returns ByteArray(10) + } + + return file + } + private fun mockBasicSuccessfulIpsOperations() { every { ipsService.deployJld(any(), any(), any(), any(), any()) 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 9e2d06e4..b5527b94 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 @@ -4,6 +4,8 @@ import com.fasterxml.jackson.dataformat.xml.XmlMapper import com.quadient.migration.api.InspireOutput import com.quadient.migration.data.DisplayRuleModel import com.quadient.migration.data.DocumentObjectModel +import com.quadient.migration.data.FileModel +import com.quadient.migration.data.FileModelRef import com.quadient.migration.data.HyperlinkModel import com.quadient.migration.data.StringModel import com.quadient.migration.data.TextStyleModel @@ -15,6 +17,7 @@ import com.quadient.migration.shared.Function import com.quadient.migration.shared.Literal import com.quadient.migration.shared.LiteralDataType import com.quadient.migration.shared.Size +import com.quadient.migration.shared.SkipOptions import com.quadient.migration.tools.aProjectConfig import com.quadient.migration.tools.model.* import com.quadient.migration.tools.shouldBeEqualTo @@ -154,11 +157,84 @@ class InspireDocumentObjectBuilderTest { assert(inheritFlags.none { it.textValue() == "FillStyle" }) } + @Test + fun `file reference creates DirectExternal flow with correct structure`() { + // given + val file = aFile("File_1", name = "document", sourcePath = "C:/files/document.pdf") + every { fileRepository.findModelOrFail(file.id) } returns file + val block = mockObj( + aBlock("B_1", listOf(aParagraph(aText(listOf(StringModel("See attached: "), FileModelRef(file.id)))))) + ) + + // when + val result = + subject.buildDocumentObject(block, null).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + val flowAreaFlowId = result["FlowArea"].last()["FlowId"].textValue() + val flowAreaFlow = result["Flow"].last { it["Id"].textValue() == flowAreaFlowId } + + flowAreaFlow["FlowContent"]["P"]["T"][""].textValue().shouldBeEqualTo("See attached: ") + val fileFlowId = flowAreaFlow["FlowContent"]["P"]["T"]["O"]["Id"].textValue() + + val fileFlow = result["Flow"].last { it["Id"].textValue() == fileFlowId } + fileFlow["Type"].textValue().shouldBeEqualTo("DirectExternal") + fileFlow["ExternalLocation"].textValue().shouldBeEqualTo("icm://document.pdf") + } + + @Test + fun `file reference with skip and placeholder creates simple flow with placeholder text`() { + // given + val file = aFile("File_1", skip = SkipOptions(true, "File not available", "Missing source")) + every { fileRepository.findModelOrFail(file.id) } returns file + val block = mockObj( + aBlock("B_1", listOf(aParagraph(aText(listOf(FileModelRef(file.id)))))) + ) + + // when + val result = subject.buildDocumentObject(block, null).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + val placeholderFlow = result["Flow"].last() + placeholderFlow["FlowContent"]["P"]["T"][""].textValue() == "File not available" + } + + @Test + fun `file reference with skip but no placeholder does not create flow`() { + // given + val file = mockFile(aFile("File_1", skip = SkipOptions(true, null, "Not needed"))) + val block = mockObj( + aBlock( + "B_1", listOf( + aParagraph( + aText( + listOf( + StringModel("Text "), FileModelRef(file.id), StringModel(" more text") + ) + ) + ) + ) + ) + ) + + // when + val result = subject.buildDocumentObject(block, null).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + val flow = result["Flow"].last() + flow["FlowContent"]["P"]["T"][""].textValue().shouldBeEqualTo("Text more text") + } + private fun mockObj(documentObject: DocumentObjectModel): DocumentObjectModel { every { documentObjectRepository.findModelOrFail(documentObject.id) } returns documentObject return documentObject } + private fun mockFile(file: FileModel): FileModel { + every { fileRepository.findModelOrFail(file.id) } returns file + return file + } + private fun mockTextStyle(textStyle: TextStyleModel): TextStyleModel { every { textStyleRepository.firstWithDefinitionModel(textStyle.id) } returns textStyle val currentAllStyles = textStyleRepository.listAllModel() From e43210f7325871b16055b5fc4ebf442ed89c85ce Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Wed, 4 Feb 2026 16:31:48 +0100 Subject: [PATCH 8/9] progress report unit tests --- .../service/deploy/DeployClientTest.kt | 54 +++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/DeployClientTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/DeployClientTest.kt index 21a2f7f9..2ad85e11 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/DeployClientTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/DeployClientTest.kt @@ -6,6 +6,7 @@ import com.quadient.migration.api.dto.migrationmodel.StatusTracking import com.quadient.migration.api.repository.StatusTrackingRepository import com.quadient.migration.data.DocumentObjectModel import com.quadient.migration.data.DocumentObjectModelRef +import com.quadient.migration.data.FileModelRef import com.quadient.migration.data.ImageModelRef import com.quadient.migration.data.ParagraphModel import com.quadient.migration.data.ParagraphStyleModelRef @@ -28,6 +29,7 @@ import com.quadient.migration.tools.aDeployedStatusEvent import com.quadient.migration.tools.aErrorStatusEvent import com.quadient.migration.tools.aProjectConfig import com.quadient.migration.tools.model.aBlock +import com.quadient.migration.tools.model.aFile import com.quadient.migration.tools.model.aImage import com.quadient.migration.tools.model.aParaStyle import com.quadient.migration.tools.model.aTextStyle @@ -79,6 +81,8 @@ class DeployClientTest { every { documentObjectBuilder.getDocumentObjectPath(any(), any(), any()) } answers { callOriginal() } every { documentObjectBuilder.getImagePath(any()) } answers { callOriginal() } every { documentObjectBuilder.getImagePath(any(), any(), any(), any(), any()) } answers { callOriginal() } + every { documentObjectBuilder.getFilePath(any()) } answers { callOriginal() } + every { documentObjectBuilder.getFilePath(any(), any(), any(), any(), any()) } answers { callOriginal() } } @Test @@ -154,7 +158,7 @@ class DeployClientTest { givenNewExternalDocumentObject("1", deps = listOf("2")) givenNewExternalDocumentObject("2", deps = listOf("3", "4")) - givenNewExternalDocumentObject("3", imageDeps = listOf("1", "2", "3")) + givenNewExternalDocumentObject("3", imageDeps = listOf("1", "2", "3"), fileDeps = listOf("1", "2")) givenInternalDocumentObject("4", deps = listOf("1", "5")) givenInternalDocumentObject("5", deps = listOf("6")) givenNewExternalDocumentObject("6", deps = listOf("7")) @@ -162,10 +166,12 @@ class DeployClientTest { givenNewImage("1") givenNewImage("2") givenNewImage("3") + givenNewFile("1") + givenNewFile("2") val result = subject.progressReport() - result.items.size.shouldBeEqualTo(10) + result.items.size.shouldBeEqualTo(12) for (i in arrayOf(1, 2, 3, 6, 7)) { result.items[Pair(i.toString(), ResourceType.DocumentObject)].shouldBeNew() } @@ -175,6 +181,9 @@ class DeployClientTest { for (i in arrayOf(1, 2, 3)) { result.items[Pair(i.toString(), ResourceType.Image)].shouldBeNew() } + for (i in arrayOf(1, 2)) { + result.items[Pair(i.toString(), ResourceType.File)].shouldBeNew() + } } @Test @@ -185,7 +194,7 @@ class DeployClientTest { givenNewExternalDocumentObject("1", deps = listOf("2")) givenChangedExternalDocumentObject("2", deps = listOf("3", "4"), deployTimestamp = currentDeployTimestamp, deploymentId = deploymentId) givenChangedExternalDocumentObject("8", deps = listOf("3", "4"), deployTimestamp = currentDeployTimestamp, icmPath = "icm://other.wfd", deploymentId = deploymentId) - givenExistingExternalDocumentObject("3", imageDeps = listOf("1", "2", "3"), deployTimestamp = currentDeployTimestamp, deploymentId = deploymentId) + givenExistingExternalDocumentObject("3", imageDeps = listOf("1", "2", "3"), fileDeps = listOf("1", "2"), deployTimestamp = currentDeployTimestamp, deploymentId = deploymentId) givenInternalDocumentObject("4", deps = listOf("1", "5")) givenInternalDocumentObject("5", deps = listOf("6")) givenNewExternalDocumentObject("6", deps = listOf("7")) @@ -193,6 +202,8 @@ class DeployClientTest { givenNewImage("1") givenChangedImage("2", deployTimestamp = currentDeployTimestamp, deploymentId = deploymentId) givenNewImage("3") + givenNewFile("1") + givenChangedFile("2", deployTimestamp = currentDeployTimestamp, deploymentId = deploymentId) every { statusTrackingRepository.listAll() } returns listOf( aDeployedStatus("random", deploymentId = deploymentId, timestamp = currentDeployTimestamp), @@ -200,7 +211,7 @@ class DeployClientTest { val result = subject.progressReport() - result.items.size.shouldBeEqualTo(11) + result.items.size.shouldBeEqualTo(13) for (i in arrayOf(1, 6, 7)) { result.items[Pair(i.toString(), ResourceType.DocumentObject)].shouldBeNew() } @@ -216,6 +227,9 @@ class DeployClientTest { for (i in arrayOf(1, 3)) { result.items[Pair(i.toString(), ResourceType.Image)].shouldBeNew() } + for (i in arrayOf(1)) { + result.items[Pair(i.toString(), ResourceType.File)].shouldBeNew() + } for (i in arrayOf(8)) { result.items[Pair(i.toString(), ResourceType.DocumentObject)].shouldBeChangedPath() } @@ -404,10 +418,34 @@ class DeployClientTest { } returns events } + private fun givenNewFile(id: String) { + givenFile(id = id, events = listOf(aActiveStatusEvent(Clock.System.now() - 1.hours))) + } + + private fun givenChangedFile(id: String, deployTimestamp: Instant, deploymentId: Uuid) { + givenFile( + id = id, events = listOf( + aActiveStatusEvent(timestamp = deployTimestamp - 1.seconds), + aDeployedStatusEvent(deploymentId, timestamp = deployTimestamp), + aActiveStatusEvent(timestamp = deployTimestamp + 1.seconds) + ) + ) + } + + private fun givenFile(id: String, events: List = listOf()) { + every { fileRepository.findModelOrFail(id) } returns aFile(id = id) + every { + statusTrackingRepository.findEventsRelevantToOutput( + id, ResourceType.File, any() + ) + } returns events + } + private fun givenNewExternalDocumentObject( id: String, deps: List = listOf(), imageDeps: List = listOf(), + fileDeps: List = listOf(), textStyles: List = listOf(), paragraphStyles: List = listOf(), ) { @@ -415,6 +453,7 @@ class DeployClientTest { id = id, deps = deps, imageDeps = imageDeps, + fileDeps = fileDeps, textStyles = textStyles, paragraphStyles = paragraphStyles, events = listOf(aActiveStatusEvent(Clock.System.now() - 1.hours)) @@ -425,6 +464,7 @@ class DeployClientTest { id: String, deps: List = listOf(), imageDeps: List = listOf(), + fileDeps: List = listOf(), textStyles: List = listOf(), paragraphStyles: List = listOf(), deployTimestamp: Instant, @@ -435,6 +475,7 @@ class DeployClientTest { id = id, deps = deps, imageDeps = imageDeps, + fileDeps = fileDeps, textStyles = textStyles, paragraphStyles = paragraphStyles, events = listOf(aActiveStatusEvent(timestamp = deployTimestamp - 1.hours), aDeployedStatusEvent(deploymentId, icmPath, deployTimestamp), aActiveStatusEvent(timestamp = deployTimestamp + 1.seconds)) @@ -445,6 +486,7 @@ class DeployClientTest { id: String, deps: List = listOf(), imageDeps: List = listOf(), + fileDeps: List = listOf(), textStyles: List = listOf(), paragraphStyles: List = listOf(), deployTimestamp: Instant, @@ -454,6 +496,7 @@ class DeployClientTest { id, deps, imageDeps = imageDeps, + fileDeps = fileDeps, textStyles = textStyles, paragraphStyles = paragraphStyles, events = listOf( aActiveStatusEvent(timestamp = deployTimestamp - 1.seconds), @@ -467,6 +510,7 @@ class DeployClientTest { id: String, deps: List = listOf(), imageDeps: List = listOf(), + fileDeps: List = listOf(), textStyles: List = listOf(), paragraphStyles: List = listOf(), events: List = listOf(), @@ -476,7 +520,7 @@ class DeployClientTest { DocumentObjectModelRef( it, null ) - } + imageDeps.map { ImageModelRef(it) } + textStyles.map { + } + imageDeps.map { ImageModelRef(it) } + fileDeps.map { FileModelRef(it) } + textStyles.map { ParagraphModel( listOf( ParagraphModel.TextModel( From 6548bf8fb939f753b8d0e02164c7f67aa1fd9ffa Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Thu, 5 Feb 2026 09:31:49 +0100 Subject: [PATCH 9/9] add FileRepositoryTest.kt --- .../migration/api/dto/migrationmodel/Ref.kt | 1 + .../persistence/FileRepositoryTest.kt | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 migration-library/src/test/kotlin/com/quadient/migration/persistence/FileRepositoryTest.kt diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Ref.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Ref.kt index 093410a8..b7ca9c26 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Ref.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/Ref.kt @@ -102,6 +102,7 @@ data class DocumentObjectRef(override val id: String, val displayRuleRef: Displa DocumentContent, TextContent { constructor(id: String) : this(id, null) + constructor(id: String, displayRuleId: String) : this(id, DisplayRuleRef(displayRuleId)) companion object { fun fromModel(model: DocumentObjectModelRef) = diff --git a/migration-library/src/test/kotlin/com/quadient/migration/persistence/FileRepositoryTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/persistence/FileRepositoryTest.kt new file mode 100644 index 00000000..151c3a57 --- /dev/null +++ b/migration-library/src/test/kotlin/com/quadient/migration/persistence/FileRepositoryTest.kt @@ -0,0 +1,56 @@ +package com.quadient.migration.persistence + +import com.quadient.migration.Postgres +import com.quadient.migration.api.dto.migrationmodel.builder.FileBuilder +import com.quadient.migration.api.repository.FileRepository +import com.quadient.migration.api.repository.StatusTrackingRepository +import com.quadient.migration.data.Active +import com.quadient.migration.service.deploy.ResourceType +import com.quadient.migration.shared.FileType +import com.quadient.migration.tools.model.aFileInternalRepository +import com.quadient.migration.tools.shouldBeEqualTo +import com.quadient.migration.tools.shouldBeOfSize +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.Test + +@Postgres +class FileRepositoryTest { + private val internalRepo = aFileInternalRepository() + private val repo = FileRepository(internalRepo) + private val statusRepo = StatusTrackingRepository(internalRepo.projectName) + + @Test + fun roundtrip() { + val dto = FileBuilder("id") + .customFields(mutableMapOf("f1" to "val1")) + .originLocations(listOf("test1", "test2")) + .targetFolder("someFolder") + .sourcePath("path/to/file.pdf") + .fileType(FileType.Attachment) + .skip("reason", "placeholder") + .build() + + repo.upsert(dto) + val result = repo.listAll() + + result.first().shouldBeEqualTo(dto) + } + + @Test + fun `upsert tracks active status for new objects and does not insert it again for existing objects`() { + val dto = FileBuilder("id") + .customFields(mutableMapOf("f1" to "val1")) + .originLocations(listOf("test1", "test2")) + .targetFolder("someFolder") + .fileType(FileType.Document) + .build() + + repo.upsert(dto) + repo.upsert(dto) + + val result = statusRepo.find(dto.id, ResourceType.File) + + result?.statusEvents?.shouldBeOfSize(1) + assertInstanceOf(Active::class.java, result?.statusEvents?.last()) + } +}