diff --git a/CHANGELOG.md b/CHANGELOG.md index ed5d3a6..ea88842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) - Conflict validation as a check to prevent any overwrites by the following deployment - Allow building of first match snippets +- Possibility to nest repeated row into repeated row ### Changed 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 f6e5dec..3a362fe 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 @@ -247,10 +247,7 @@ class Stats { } this.repeatedContentCount++ for (innerRow in row.rows) { - this.collectDisplayRule(innerRow.displayRuleRef) - for (cell in innerRow.cells) { - this.collectContent(cell.content) - } + this.collectTableRow(innerRow) } } } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/documentcontent/TableBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/documentcontent/TableBuilder.kt index 68193a2..93202d9 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/documentcontent/TableBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/documentcontent/TableBuilder.kt @@ -45,57 +45,6 @@ class TableBuilder : RowBuilderBase, HasBorder { fun percentWidth(percent: Double) = apply { this.percentWidth = percent } fun alignment(alignment: TableAlignment) = apply { this.alignment = alignment } - /** - * Add a repeated row group to the table. The rows added to the builder will be repeated - * for each element of the given variable. - * @param variable The literal path or variable reference driving repetition. - */ - fun addRepeatedRow(variable: VariablePath) = RepeatedRowBuilder(variable).apply { rows.add(this) } - - /** - * Add a repeated row group to the table and configure it via [init]. - * @param variable The literal path or variable reference driving repetition. - * @return This builder instance for method chaining. - */ - fun addRepeatedRow(variable: VariablePath, init: RepeatedRowBuilder.() -> Unit): TableBuilder = - apply { rows.add(RepeatedRowBuilder(variable).apply(init)) } - - /** - * Add a repeated row group driven by a literal path (e.g. "Data.Clients"). - */ - fun addRepeatedRow(literalPath: String) = addRepeatedRow(LiteralPath(literalPath)) - - /** - * Add a repeated row group driven by a literal path and configure it via [init]. - * @return This builder instance for method chaining. - */ - fun addRepeatedRow(literalPath: String, init: RepeatedRowBuilder.() -> Unit) = - addRepeatedRow(LiteralPath(literalPath), init) - - /** - * Add a repeated row group driven by a registered variable reference. - */ - fun addRepeatedRow(variableRef: VariableRef) = addRepeatedRow(VariableRefPath(variableRef.id)) - - /** - * Add a repeated row group driven by a registered variable reference and configure it via [init]. - * @return This builder instance for method chaining. - */ - fun addRepeatedRow(variableRef: VariableRef, init: RepeatedRowBuilder.() -> Unit) = - addRepeatedRow(VariableRefPath(variableRef.id), init) - - /** - * Add a repeated row group driven by a [Variable] object. - */ - fun addRepeatedRow(variable: Variable) = addRepeatedRow(VariableRefPath(variable.id)) - - /** - * Add a repeated row group driven by a [Variable] object and configure it via [init]. - * @return This builder instance for method chaining. - */ - fun addRepeatedRow(variable: Variable, init: RepeatedRowBuilder.() -> Unit) = - addRepeatedRow(VariableRefPath(variable.id), init) - /** * Add a column width to the table. Column widths are added in the order they are defined. */ @@ -225,7 +174,7 @@ class TableBuilder : RowBuilderBase, HasBorder { override fun build(): Table.RepeatedRow { return Table.RepeatedRow( - rows = rows.map { (it as Row).build() }, + rows = rows.map { it.build() }, variable = variable, displayRuleRef = displayRuleRef, ) @@ -263,7 +212,7 @@ class TableBuilder : RowBuilderBase, HasBorder { } /** - * Common interface for builders that manage a collection of [TableBuilder.Row] entries. + * Common interface for builders that manage a collection of [TableBuilder.TableRow] entries. * Implemented by both [TableBuilder] and [TableBuilder.RepeatedRowBuilder]. */ @Suppress("UNCHECKED_CAST") @@ -291,4 +240,59 @@ interface RowBuilderBase { * @return This builder instance for method chaining. */ fun addRows(rows: List): T = apply { this.rows.addAll(rows) } as T + + /** + * Add a repeated row group. The rows added to the builder will be repeated + * for each element of the given variable. + * @param variable The literal path or variable reference driving repetition. + */ + fun addRepeatedRow(variable: VariablePath): TableBuilder.RepeatedRowBuilder = + TableBuilder.RepeatedRowBuilder(variable).also { rows.add(it) } + + /** + * Add a repeated row group and configure it via [init]. + * @param variable The literal path or variable reference driving repetition. + * @return This builder instance for method chaining. + */ + fun addRepeatedRow(variable: VariablePath, init: TableBuilder.RepeatedRowBuilder.() -> Unit): T = + apply { rows.add(TableBuilder.RepeatedRowBuilder(variable).apply(init)) } as T + + /** + * Add a repeated row group driven by a literal path (e.g. "Data.Clients"). + */ + fun addRepeatedRow(literalPath: String): TableBuilder.RepeatedRowBuilder = + addRepeatedRow(LiteralPath(literalPath)) + + /** + * Add a repeated row group driven by a literal path and configure it via [init]. + * @return This builder instance for method chaining. + */ + fun addRepeatedRow(literalPath: String, init: TableBuilder.RepeatedRowBuilder.() -> Unit): T = + addRepeatedRow(LiteralPath(literalPath), init) + + /** + * Add a repeated row group driven by a registered variable reference. + */ + fun addRepeatedRow(variableRef: VariableRef): TableBuilder.RepeatedRowBuilder = + addRepeatedRow(VariableRefPath(variableRef.id)) + + /** + * Add a repeated row group driven by a registered variable reference and configure it via [init]. + * @return This builder instance for method chaining. + */ + fun addRepeatedRow(variableRef: VariableRef, init: TableBuilder.RepeatedRowBuilder.() -> Unit): T = + addRepeatedRow(VariableRefPath(variableRef.id), init) + + /** + * Add a repeated row group driven by a [Variable] object. + */ + fun addRepeatedRow(variable: Variable): TableBuilder.RepeatedRowBuilder = + addRepeatedRow(VariableRefPath(variable.id)) + + /** + * Add a repeated row group driven by a [Variable] object and configure it via [init]. + * @return This builder instance for method chaining. + */ + fun addRepeatedRow(variable: Variable, init: TableBuilder.RepeatedRowBuilder.() -> Unit): T = + addRepeatedRow(VariableRefPath(variable.id), init) } \ No newline at end of file diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/documentcontent/Table.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/documentcontent/Table.kt index f7344f2..ef630d6 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/documentcontent/Table.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/documentcontent/Table.kt @@ -90,7 +90,7 @@ data class Table( } data class RepeatedRow( - val rows: List, + val rows: List, val variable: VariablePath, val displayRuleRef: DisplayRuleRef? = null, ) : TableRow { @@ -102,7 +102,7 @@ data class Table( fun toDb(): TableEntity.RepeatedRow { return TableEntity.RepeatedRow( - rows = rows.map(Row::toDb), + rows = rows.map { it.toDb() }, variable = variable, displayRuleRef = displayRuleRef?.toDb(), ) @@ -111,7 +111,7 @@ data class Table( companion object { fun fromDb(row: TableEntity.RepeatedRow): RepeatedRow { return RepeatedRow( - rows = row.rows.map(Row::fromDb), + rows = row.rows.map(::tableRowFromDb), variable = row.variable, displayRuleRef = row.displayRuleRef?.let { DisplayRuleRef.fromDb(it) }, ) diff --git a/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/DocumentContentEntity.kt b/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/DocumentContentEntity.kt index ca8e602..590b22b 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/DocumentContentEntity.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/persistence/migrationmodel/DocumentContentEntity.kt @@ -48,7 +48,7 @@ data class TableEntity( @Serializable data class RepeatedRow( - val rows: List, + val rows: List, val variable: VariablePath, val displayRuleRef: DisplayRuleEntityRef? = null, ) : TableRow 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 79324d3..b411de8 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 @@ -158,14 +158,16 @@ abstract class InspireDocumentObjectBuilder( protected fun collectLanguages(documentObject: DocumentObject): List { val languages = mutableSetOf() - fun Table.allRows(): List = - (rows + header + firstHeader + footer + lastFooter).flatMap { row -> + fun List.flattenRows(): List = + flatMap { row -> when (row) { is Table.Row -> listOf(row) - is Table.RepeatedRow -> row.rows + is Table.RepeatedRow -> row.rows.flattenRows() } } + fun Table.allRows(): List = (rows + header + firstHeader + footer + lastFooter).flattenRows() + fun collectLanguagesFromContent(content: List) { for (item in content) { when (item) { @@ -1162,12 +1164,23 @@ abstract class InspireDocumentObjectBuilder( } val repeatedRowSet = layout.addRowSet().setType(RowSet.Type.REPEATED) + + fun addChildRowSets(target: GeneralRowSet) { + repeatedRow.rows.forEach { childRow -> + when (childRow) { + is Table.Row -> target.addRowSet(buildSingleRowSet(childRow, layout, variableStructure, languages)) + is Table.RepeatedRow -> buildRepeatedRowSet(childRow, layout, variableStructure, languages) + ?.let { target.addRowSet(it) } + } + } + } + if (repeatedRow.rows.size > 1) { val multipleRowSet = layout.addRowSet().setType(RowSet.Type.MULTIPLE_ROWS) repeatedRowSet.addRowSet(multipleRowSet) - repeatedRow.rows.forEach { multipleRowSet.addRowSet(buildSingleRowSet(it, layout, variableStructure, languages)) } + addChildRowSets(multipleRowSet) } else { - repeatedRow.rows.forEach { repeatedRowSet.addRowSet(buildSingleRowSet(it, layout, variableStructure, languages)) } + addChildRowSets(repeatedRowSet) } repeatedRowSet.setVariable(arrayVariable) @@ -1184,10 +1197,11 @@ abstract class InspireDocumentObjectBuilder( val warning = Paragraph("") val rows = repeatedRow.rows - val rowsWithWarning = rows.firstOrNull()?.let { firstRow -> + val rowsWithWarning = (rows.firstOrNull { it is Table.Row } as? Table.Row)?.let { firstRow -> firstRow.cells.firstOrNull()?.let { firstCell -> val cellWithWarning = firstCell.copy(content = listOf(warning) + firstCell.content) - listOf(firstRow.copy(cells = listOf(cellWithWarning) + firstRow.cells.drop(1))) + rows.drop(1) + val modifiedRow = firstRow.copy(cells = listOf(cellWithWarning) + firstRow.cells.drop(1)) + listOf(modifiedRow) + rows.filter { it !== firstRow } } } ?: rows @@ -1313,7 +1327,7 @@ abstract class InspireDocumentObjectBuilder( } else { val numberOfColumns = when (val firstRow = model.rows.firstOrNull()) { is Table.Row -> firstRow.cells.size - is Table.RepeatedRow -> firstRow.rows.firstOrNull()?.cells?.size ?: 0 + is Table.RepeatedRow -> firstRow.rows.firstNotNullOfOrNull { (it as? Table.Row)?.cells?.size } ?: 0 null -> 0 } repeat(numberOfColumns) { table.addColumn() } 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 c1fb8ba..527bacf 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 @@ -612,6 +612,65 @@ class InspireDocumentObjectBuilderTest { secondFlow["FlowContent"]["P"]["T"][""].textValue().shouldBeEqualTo("Second") } + @Test + fun `buildDocumentObject creates nested repeated rowsets`() { + // given + val itemNameVar = mockVar(VariableBuilder("itemNameVar").name("Item Name").dataType(DataType.String).build()) + val ordersArrayVar = mockVar(VariableBuilder("orders").name("Orders").dataType(DataType.Array).build()) + + val variableStructure = mockVarStructure( + VariableStructureBuilder("VS_1").addVariable(ordersArrayVar.id, "Data.Clients.Value") + .addVariable(itemNameVar.id, ordersArrayVar).build() + ) + + val block = mockObj( + DocumentObjectBuilder("B_1", Block).table { + addRepeatedRow("Data.Clients.Value") { + addRepeatedRow(ordersArrayVar) { + addRow { + addCell { string("Order: ") } + addCell { paragraph { text { variableRef(itemNameVar.id) } } } + } + } + } + }.variableStructureRef(variableStructure.id).build() + ) + + // when + val result = subject.buildDocumentObject(block).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then — outer repeated rowset driven by Data.Clients.Value + val outerRepeatedRowSetId = result["Table"].last()["RowSetId"].textValue() + val outerRepeatedRowSet = result["RowSet"].last { it["Id"].textValue() == outerRepeatedRowSetId } + outerRepeatedRowSet["RowSetType"].textValue().shouldBeEqualTo("Repeated") + val outerArrayVar = result["Variable"].last { it["Id"].textValue() == outerRepeatedRowSet["VariableId"].textValue() } + outerArrayVar["VarType"].textValue().shouldBeEqualTo("Array") + + // then — inner repeated rowset is nested directly inside the outer + val innerRepeatedRowSetId = outerRepeatedRowSet["SubRowId"].textValue() + val innerRepeatedRowSet = result["RowSet"].last { it["Id"].textValue() == innerRepeatedRowSetId } + innerRepeatedRowSet["RowSetType"].textValue().shouldBeEqualTo("Repeated") + val innerArrayVar = result["Variable"].last { it["Id"].textValue() == innerRepeatedRowSet["VariableId"].textValue() } + innerArrayVar["VarType"].textValue().shouldBeEqualTo("Array") + + // then — single row is nested inside the inner repeated rowset + val singleRowId = innerRepeatedRowSet["SubRowId"].textValue() + val singleRow = result["RowSet"].last { it["Id"].textValue() == singleRowId } + singleRow["RowSetType"].textValue().shouldBeEqualTo("Row") + + val secondCellId = singleRow["SubRowId"][1].textValue() + val secondCell = result["Cell"].last { it["Id"].textValue() == secondCellId } + val secondCellFlow = result["Flow"].last { it["Id"].textValue() == secondCell["FlowId"].textValue() } + val variableId = secondCellFlow["FlowContent"]["P"]["T"]["O"]["Id"].textValue() + val variable = result["Variable"].first { it["Id"].textValue() == variableId } + variable["Name"].textValue().shouldBeEqualTo("Item Name") + + val lockedWebNodes = result["Root"]["LockedWebNodes"]["LockedWebNode"] + val lockedIds = lockedWebNodes.map { it.textValue() } + lockedIds.contains(outerRepeatedRowSetId).shouldBeEqualTo(true) + lockedIds.contains(innerRepeatedRowSetId).shouldBeEqualTo(true) + } + @Test fun `buildDocumentObject creates repeated flow with literal array path`() { // given