Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.ritense.valtimo.autoconfiguration

import com.ritense.valtimo.operaton.service.OperatonRepositoryService
import com.ritense.valtimo.exporter.DecisionDefinitionExporter
import com.ritense.valtimo.exporter.GlobalProcessDefinitionExporter
import com.ritense.valtimo.exporter.ProcessDefinitionExporter
import org.operaton.bpm.engine.RepositoryService
import org.springframework.boot.autoconfigure.AutoConfiguration
Expand All @@ -35,6 +36,13 @@ class ExportAutoConfiguration {
repositoryService: RepositoryService
) = ProcessDefinitionExporter(operatonRepositoryService, repositoryService)

@Bean
@ConditionalOnMissingBean(GlobalProcessDefinitionExporter::class)
fun globalProcessDefinitionExporter(
operatonRepositoryService: OperatonRepositoryService,
repositoryService: RepositoryService
) = GlobalProcessDefinitionExporter(operatonRepositoryService, repositoryService)

@Bean
@ConditionalOnMissingBean(DecisionDefinitionExporter::class)
fun decisionDefinitionExporter(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright 2015-2026 Ritense BV, the Netherlands.
*
* Licensed under EUPL, Version 1.2 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.ritense.valtimo.exporter

import com.ritense.exporter.ExportFile
import com.ritense.exporter.ExportResult
import com.ritense.exporter.Exporter
import com.ritense.exporter.manifest.ArtifactManifestEntry
import com.ritense.exporter.manifest.ArtifactType
import com.ritense.exporter.manifest.ResolvableValue
import com.ritense.exporter.request.GlobalProcessDefinitionExportRequest
import com.ritense.valtimo.operaton.domain.OperatonProcessDefinition
import com.ritense.valtimo.operaton.service.OperatonRepositoryService
import org.operaton.bpm.engine.RepositoryService
import org.operaton.bpm.model.bpmn.Bpmn
import java.io.ByteArrayOutputStream

/**
* Exports the BPMN of a process definition that is not part of a case definition.
*
* Unlike [ProcessDefinitionExporter] this exporter does not create related export requests for

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we not want to export called subprocesses/decision definitions?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed it a bit more internally, we would like to see this included before we accept it into the product. People will get wrong expectations if they export a process definition that references other process or decision definitions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is/was to mimic the existing export of a 'system' process with its process links included. As the existing system process export only exports the bpmn definition and not also its called elements I made the decision for this new export to exclude these as well.

Including these will extend the scope significantly and introduces more complexity if you ask me. It will do a lot more than the existing 'simple' system process export. What should the scope be then, include all called bpmn elements including and referenced form definitions too? What if called elements are used by other bpmn's as well, same question for forms.

Maybe we should discuss this a little more or it could be an iteration of this in the (near) future if there is demand for this.

* called sub-processes or referenced decision definitions. Those can be shared with other
* processes and are exported and imported separately.
*/
class GlobalProcessDefinitionExporter(
private val operatonRepositoryService: OperatonRepositoryService,
private val repositoryService: RepositoryService,
) : Exporter<GlobalProcessDefinitionExportRequest> {

override fun supports(): Class<GlobalProcessDefinitionExportRequest> =
GlobalProcessDefinitionExportRequest::class.java

override fun export(request: GlobalProcessDefinitionExportRequest): ExportResult {
val processDefinition = requireNotNull(
operatonRepositoryService.findProcessDefinitionById(request.processDefinitionId)
) {
"Process definition with id '${request.processDefinitionId}' could not be found!"
}

val bpmnModelInstance = repositoryService.getProcessModel(processDefinition.id).use { inputStream ->
Bpmn.readModelFromStream(inputStream)
}

val exportFile = ByteArrayOutputStream().use {
Bpmn.writeModelToStream(it, bpmnModelInstance)
ExportFile(
PATH.format(processDefinition.key),
it.toByteArray()
)
}

return ExportResult(
exportFiles = setOf(exportFile),
relatedRequests = emptySet(),
manifestArtifact = ArtifactManifestEntry(
artifactVersionTag = ResolvableValue.of(getArtifactVersionTag(processDefinition)),
title = ResolvableValue.of(processDefinition.name ?: processDefinition.key),
type = ArtifactType.PROCESS_DEFINITION,
// Filled in by the export service
valtimoVersion = "",
dependencies = emptyList(),
),
manifestDependencies = emptySet(),
)
}

/**
* A BPMN file has no field the manifest can reference, so the version is written as a literal
* value. The version tag of the model is preferred over the version of the deployment: the latter
* differs per environment. A version tag that encodes a case or building block definition is not
* a version of the process itself and is therefore ignored.
*/
private fun getArtifactVersionTag(processDefinition: OperatonProcessDefinition): String =
processDefinition.takeIf { it.getBlueprintId() == null }?.versionTag
?: processDefinition.version.toString()

companion object {
private const val PATH = "config/global/bpmn/%s.bpmn"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,14 @@ class ValtimoExportService(
}.mapNotNull { exporter ->
try {
val result = exporter.export(request)
if (isRoot) {
result.manifestArtifact?.let { artifacts.add(it) }
// The manifestDependencies of the exporter that contributes the artifact describe that
// artifact itself, for when it is pulled in as a dependency of another export. It is not
// a dependency of itself, so those are ignored here. Any other exporter of the root
// request does contribute dependencies: a root request can be answered by more than one
// exporter (a process definition and its process links, for example).
val artifact = result.manifestArtifact
if (isRoot && artifact != null) {
artifacts.add(artifact)
} else {
dependencies.addAll(result.manifestDependencies)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ package com.ritense.exporter.manifest

enum class ArtifactType {
CASE_DEFINITION,
BUILDING_BLOCK
BUILDING_BLOCK,
PROCESS_DEFINITION
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2015-2026 Ritense BV, the Netherlands.
*
* Licensed under EUPL, Version 1.2 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.ritense.exporter.request

/**
* Export request for a process definition that is not linked to a case definition or building block.
* Exports into the `config/global` folder structure, so it can be imported on another environment
* without a case definition being involved.
*/
data class GlobalProcessDefinitionExportRequest(
val processDefinitionId: String,
override val required: Boolean = true,
) : ExportRequest(required)
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,23 @@ class ValtimoExportServiceTest {
assertThat(dependencyKeys).doesNotContain("ignored-root-dependency")
}

/**
* A root request can be answered by more than one exporter: a process definition and its process
* links are exported by two exporters that support the same request. Only one of them contributes
* the artifact, the dependencies of the other belong to that artifact.
*/
@Test
fun `should collect manifest dependencies of a root exporter that does not contribute the artifact`() {
val service = service(ArtifactExporter(), CoRootDependencyExporter())

val manifest = service.export(RootArtifactRequest()).manifest()

val dependencyKeys = manifest.path("artifacts").single().path("dependencies")
.map { it.path("key").asText() }
assertThat(dependencyKeys).contains("co-root-plugin")
assertThat(dependencyKeys).doesNotContain("ignored-root-dependency")
}

@Test
fun `should deduplicate dependencies contributed by multiple exporters`() {
val service = service(ArtifactExporter(), PluginDependencyExporter(), DuplicatePluginDependencyExporter())
Expand Down Expand Up @@ -192,6 +209,21 @@ class ValtimoExportServiceTest {
)
}

private inner class CoRootDependencyExporter : Exporter<RootArtifactRequest> {
override fun supports() = RootArtifactRequest::class.java
override fun export(request: RootArtifactRequest) = ExportResult(
exportFiles = setOf(ExportFile("config/case/bezwaar/1-0-0/process-link/bezwaar.process-link.json", "[]".toByteArray())),
manifestArtifact = null,
manifestDependencies = setOf(
ArtifactDependency(
DependencyType.PLUGIN,
ResolvableValue.of("co-root-plugin"),
ResolvableValue.of("Co Root Plugin"),
)
),
)
}

private inner class BuildingBlockDependencyExporter : Exporter<BuildingBlockDependencyRequest> {
override fun supports() = BuildingBlockDependencyRequest::class.java
override fun export(request: BuildingBlockDependencyRequest) = ExportResult(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import com.ritense.logging.withLoggingContext
import com.ritense.processlink.autodeployment.ProcessLinkDeployDto
import com.ritense.processlink.domain.ProcessLink
import com.ritense.processlink.mapper.ProcessLinkMapper
import com.ritense.processlink.web.rest.dto.MissingReferenceDto
import com.ritense.processlink.web.rest.dto.MissingReferenceType
import com.ritense.processlink.web.rest.dto.ProcessLinkCreateRequestDto
import com.ritense.processlink.web.rest.dto.ProcessLinkExportResponseDto
import com.ritense.processlink.web.rest.dto.ProcessLinkResponseDto
Expand Down Expand Up @@ -193,6 +195,26 @@ class FormFlowProcessLinkMapper(

override fun getImporterType() = "formflow"

override fun getMissingReference(deployDto: ProcessLinkDeployDto, blueprintId: BlueprintId?): MissingReferenceDto? {
deployDto as FormFlowProcessLinkDeployDto
val definition = when (blueprintId) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're calling the same method twice, why are you using a when for this?

is CaseDefinitionId -> formFlowService.findDefinitionOrNull(deployDto.formFlowDefinitionKey, blueprintId)
is BuildingBlockDefinitionId -> formFlowService.findDefinitionOrNull(deployDto.formFlowDefinitionKey, blueprintId)
// A form flow definition only exists for a case or building block, so a form flow process
// link can never be created without one. See assertFormFlowDefinitionExists.
else -> null
}
return if (definition != null) {
null
} else {
MissingReferenceDto(
type = MissingReferenceType.FORM_FLOW,
reference = deployDto.formFlowDefinitionKey,
activityId = deployDto.activityId,
)
}
}

companion object {
const val PROCESS_LINK_TYPE_FORM_FLOW = "form-flow"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import com.ritense.form.web.rest.dto.FormProcessLinkUpdateRequestDto
import com.ritense.processlink.autodeployment.ProcessLinkDeployDto
import com.ritense.processlink.domain.ProcessLink
import com.ritense.processlink.mapper.ProcessLinkMapper
import com.ritense.processlink.web.rest.dto.MissingReferenceDto
import com.ritense.processlink.web.rest.dto.MissingReferenceType
import com.ritense.processlink.web.rest.dto.ProcessLinkCreateRequestDto
import com.ritense.processlink.web.rest.dto.ProcessLinkExportResponseDto
import com.ritense.processlink.web.rest.dto.ProcessLinkResponseDto
Expand Down Expand Up @@ -171,6 +173,24 @@ class FormProcessLinkMapper(

override fun getImporterType() = "form"

override fun getMissingReference(deployDto: ProcessLinkDeployDto, blueprintId: BlueprintId?): MissingReferenceDto? {
deployDto as FormProcessLinkDeployDto
val formDefinition = if (blueprintId != null) {
formDefinitionService.getFormDefinitionByName(deployDto.formDefinitionName, blueprintId)
} else {
formDefinitionService.getFormDefinitionByName(deployDto.formDefinitionName)
}
return if (formDefinition.isPresent) {
null
} else {
MissingReferenceDto(
type = MissingReferenceType.FORM,
reference = deployDto.formDefinitionName,
activityId = deployDto.activityId,
)
}
}

private fun resolveFormDefinition(formName: String, blueprintId: BlueprintId?): FormIoFormDefinition {
val result = if (blueprintId != null) {
formDefinitionService.getFormDefinitionByName(formName, blueprintId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.ritense.form.web.rest.dto.FormProcessLinkCreateRequestDto
import com.ritense.form.web.rest.dto.FormProcessLinkResponseDto
import com.ritense.form.web.rest.dto.FormProcessLinkUpdateRequestDto
import com.ritense.processlink.domain.ActivityTypeWithEventName.USER_TASK_CREATE
import com.ritense.processlink.web.rest.dto.MissingReferenceType
import com.ritense.valtimo.contract.BlueprintId
import com.ritense.valtimo.contract.buildingblock.BuildingBlockDefinitionId
import com.ritense.valtimo.contract.case_.CaseDefinitionId
Expand Down Expand Up @@ -355,6 +356,67 @@ internal class FormProcessLinkMapperTest {
assertEquals(formDefinitionId, result.formDefinitionId)
}

@Test
fun `should report a missing reference when the form does not exist globally`() {
val deployDto = FormProcessLinkDeployDto(
processDefinitionId = "process-def:1:123",
activityId = "userTask1",
activityType = USER_TASK_CREATE,
formDefinitionName = "my-form",
subtitles = null
)
whenever(formDefinitionService.getFormDefinitionByName("my-form")).thenReturn(Optional.empty())

val missingReference = formProcessLinkMapper.getMissingReference(deployDto, null)

assertThat(missingReference).isNotNull
assertThat(missingReference!!.type).isEqualTo(MissingReferenceType.FORM)
assertThat(missingReference.reference).isEqualTo("my-form")
assertThat(missingReference.activityId).isEqualTo("userTask1")
assertThat(missingReference.blocksImport).isTrue()
}

@Test
fun `should not report a missing reference when the form exists globally`() {
val deployDto = FormProcessLinkDeployDto(
processDefinitionId = "process-def:1:123",
activityId = "userTask1",
activityType = USER_TASK_CREATE,
formDefinitionName = "my-form",
subtitles = null
)
whenever(formDefinitionService.getFormDefinitionByName("my-form")).thenReturn(
Optional.of(
FormIoFormDefinition(
UUID.randomUUID(),
"my-form",
"{}",
FormDefinitionBlueprintId.forCase(caseDefinitionId),
false
)
)
)

assertThat(formProcessLinkMapper.getMissingReference(deployDto, null)).isNull()
}

@Test
fun `should look up the form of a case definition when importing for a case`() {
val deployDto = FormProcessLinkDeployDto(
processDefinitionId = "process-def:1:123",
activityId = "userTask1",
activityType = USER_TASK_CREATE,
formDefinitionName = "my-case-form",
subtitles = null
)
whenever(formDefinitionService.getFormDefinitionByName("my-case-form", caseDefinitionId as BlueprintId))
.thenReturn(Optional.empty())

val missingReference = formProcessLinkMapper.getMissingReference(deployDto, caseDefinitionId)

assertThat(missingReference!!.reference).isEqualTo("my-case-form")
}

companion object {
val SUBTITLES = listOf("test", "test2")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import java.util.UUID

interface ImportService {
fun importGlobal(inputStream: InputStream)
fun importGlobal(
inputStream: InputStream,
pluginConfigurationMappings: Map<UUID, UUID?>?,
) = importGlobal(inputStream)
fun import(inputStream: InputStream, caseDefinitionIdList: List<CaseDefinitionId>): CaseDefinitionId?
fun import(
inputStream: InputStream,
Expand All @@ -37,6 +41,12 @@ interface ImportService {
nameOverride: String?,
pluginConfigurationMappings: Map<UUID, UUID?>?,
): CaseDefinitionId? = import(inputStream, caseDefinitionIdList, keyOverride, nameOverride)
fun importBuildingBlockDefinitions(inputStream: InputStream, buildingBlockDefinitionIdList: List<BuildingBlockDefinitionId>)
fun importBuildingBlockDefinition(entries: List<ZipFileEntry>, buildingBlockDefinitionIdList: List<BuildingBlockDefinitionId>)
fun importBuildingBlockDefinitions(
inputStream: InputStream,
buildingBlockDefinitionIdList: List<BuildingBlockDefinitionId>
)
fun importBuildingBlockDefinition(
entries: List<ZipFileEntry>,
buildingBlockDefinitionIdList: List<BuildingBlockDefinitionId>
)
}
Loading
Loading