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..361bdcfc
--- /dev/null
+++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/FilesExport.groovy
@@ -0,0 +1,53 @@
+//! ---
+//! 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", "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,
+ 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.fileType))
+ 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..80ef6293
--- /dev/null
+++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/FilesImport.groovy
@@ -0,0 +1,75 @@
+//! ---
+//! 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.FileType
+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 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)
+
+ 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/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/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/exampleResources/migrationModelExample/logo.pdf b/migration-examples/src/main/resources/exampleResources/migrationModelExample/logo.pdf
new file mode 100644
index 00000000..cc1fe3c7
Binary files /dev/null and b/migration-examples/src/main/resources/exampleResources/migrationModelExample/logo.pdf differ
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-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
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/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..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
@@ -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)
@@ -99,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) =
@@ -154,6 +158,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/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/InspireDocumentObjectBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilder.kt
index ac9677b7..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
@@ -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
@@ -273,6 +284,7 @@ abstract class InspireDocumentObjectBuilder(
}
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))
@@ -285,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)
@@ -321,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
}
@@ -597,6 +611,40 @@ abstract class InspireDocumentObjectBuilder(
protected abstract fun applyImageAlternateText(layout: Layout, image: Image, alternateText: String)
+ private fun buildFileRef(
+ layout: Layout,
+ fileRef: FileModelRef,
+ ): Flow? {
+ val fileModel = fileRepository.findModelOrFail(fileRef.id)
+
+ 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."
+ )
+ }
+
+ val flow = getFlowByName(layout, fileModel.nameOrId()) ?: run {
+ layout.addFlow()
+ .setName(fileModel.nameOrId())
+ .setType(Flow.Type.DIRECT_EXTERNAL)
+ .setLocation(getFilePath(fileModel))
+ }
+
+ return flow
+ }
+
private fun buildCompositeFlow(
layout: Layout,
variableStructure: VariableStructureModel,
@@ -731,6 +779,10 @@ abstract class InspireDocumentObjectBuilder(
currentText.appendFlow(flow)
}
+ is FileModelRef -> buildFileRef(layout, it)?.also { flow ->
+ currentText.appendFlow(flow)
+ }
+
is ImageModelRef -> buildAndAppendImage(layout, currentText, it)
is HyperlinkModel -> currentText = buildAndAppendHyperlink(layout, paragraph, baseTextStyleModel, it)
is FirstMatchModel -> currentText.appendFlow(
@@ -758,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()
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..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
@@ -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,28 @@ 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 baseFileName = name ?: id
+ val fileName = appendExtensionIfMissing(baseFileName, sourcePath)
+
+ if (targetFolder?.isAbsolute() == true) {
+ return targetFolder.join(fileName).toString()
+ }
+
+ val fileConfigPath = when (fileType) {
+ FileType.Document -> projectConfig.paths.documents.orDefault("Documents")
+ FileType.Attachment -> projectConfig.paths.attachments.orDefault("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/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())
+ }
+}
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/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/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..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,12 +6,14 @@ 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
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
@@ -27,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
@@ -51,6 +54,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 +65,7 @@ class DeployClientTest {
private val subject = DesignerDeployClient(
documentObjectRepository,
imageRepository,
+ fileRepository,
statusTrackingRepository,
textStyleRepository,
paragraphStyleRepository,
@@ -76,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
@@ -151,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"))
@@ -159,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()
}
@@ -172,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
@@ -182,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"))
@@ -190,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),
@@ -197,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()
}
@@ -213,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()
}
@@ -401,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(),
) {
@@ -412,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))
@@ -422,6 +464,7 @@ class DeployClientTest {
id: String,
deps: List = listOf(),
imageDeps: List = listOf(),
+ fileDeps: List = listOf(),
textStyles: List = listOf(),
paragraphStyles: List = listOf(),
deployTimestamp: Instant,
@@ -432,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))
@@ -442,6 +486,7 @@ class DeployClientTest {
id: String,
deps: List = listOf(),
imageDeps: List = listOf(),
+ fileDeps: List = listOf(),
textStyles: List = listOf(),
paragraphStyles: List = listOf(),
deployTimestamp: Instant,
@@ -451,6 +496,7 @@ class DeployClientTest {
id,
deps,
imageDeps = imageDeps,
+ fileDeps = fileDeps,
textStyles = textStyles,
paragraphStyles = paragraphStyles, events = listOf(
aActiveStatusEvent(timestamp = deployTimestamp - 1.seconds),
@@ -464,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(),
@@ -473,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(
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..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,10 +8,13 @@ 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
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
@@ -28,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
@@ -52,6 +56,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 +67,7 @@ class DesignerDeployClientTest {
private val subject = DesignerDeployClient(
documentObjectRepository,
imageRepository,
+ fileRepository,
statusTrackingRepository,
textStyleRepository,
paragraphStyleRepository,
@@ -80,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)
)
)
)
@@ -113,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
@@ -346,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 0c95fce5..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
@@ -15,6 +17,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
@@ -35,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
@@ -58,6 +62,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 +75,7 @@ class InteractiveDeployClientTest {
private val subject = InteractiveDeployClient(
documentObjectRepository,
imageRepository,
+ fileRepository,
statusTrackingRepository,
textStyleRepository,
paragraphStyleRepository,
@@ -233,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 ""
@@ -246,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)
@@ -279,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)))
)
)
)
@@ -309,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
)
}
@@ -649,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/DesignerDocumentObjectBuilderTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilderTest.kt
index 9541f25d..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
@@ -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
@@ -27,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
@@ -43,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
@@ -73,6 +76,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 +741,7 @@ class DesignerDocumentObjectBuilderTest {
variableStructureRepository,
displayRuleRepository,
imageRepository,
+ fileRepository,
config,
ipsService,
)
@@ -895,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/service/inspirebuilder/InspireDocumentObjectBuilderTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilderTest.kt
index df1c21d8..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
@@ -33,6 +36,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 +49,7 @@ class InspireDocumentObjectBuilderTest {
variableStructureRepository,
displayRuleRepository,
imageRepository,
+ fileRepository,
aProjectConfig(output = InspireOutput.Designer),
ipsService,
)
@@ -152,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()
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..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
@@ -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
@@ -32,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
@@ -53,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
@@ -87,6 +90,7 @@ class InteractiveDocumentObjectBuilderTest {
val variableStructureRepository = mockk()
val displayRuleRepository = mockk()
val imageRepository = mockk()
+ val fileRepository = mockk()
val config = aProjectConfig()
val ipsService = mockk()
@@ -1481,6 +1485,7 @@ class InteractiveDocumentObjectBuilderTest {
variableStructureRepository,
displayRuleRepository,
imageRepository,
+ fileRepository,
config,
ipsService,
)
@@ -1611,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
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..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
@@ -28,6 +29,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 +37,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
@@ -48,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
@@ -402,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) })
@@ -413,4 +440,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