Skip to content

Commit

Permalink
create xes log for each process definition present in the exported data
Browse files Browse the repository at this point in the history
  • Loading branch information
lwluc committed Nov 10, 2023
1 parent 4294992 commit a3a5e1c
Show file tree
Hide file tree
Showing 7 changed files with 45 additions and 48 deletions.
14 changes: 4 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ and convert it to [XES](https://xes-standard.org/) for Predictive Process Monito
* [✨Features](#features)
* [📤 Camunda Optimize to XES](#-camunda-optimize-to-xes)
* [📊 XES - eXtensible Event Stream](#-xes---extensible-event-stream)
* [🚫 Limitations](#-limitations)
* [🚀🔜 Coming Soon... 🌟🎉👀](#-coming-soon-)
* [🚀 Getting Started](#-getting-started)
* [Configuring `application.yaml`](#configuring-applicationyaml)
Expand All @@ -18,7 +17,9 @@ and convert it to [XES](https://xes-standard.org/) for Predictive Process Monito
## 📤 Camunda Optimize to XES

Here's the scoop on this project: It's your ticket to export your raw process data from Camunda Optimize using the
mighty [Data Export API](https://docs.camunda.io/optimize/apis-tools/optimize-api/report/get-data-export/). 🚀
mighty [Data Export API](https://docs.camunda.io/optimize/apis-tools/optimize-api/report/get-data-export/). 🚀
🔍 For the smoothest experience, consider applying a filter to show only completed instances. 🌟 This helps streamline your view and focus on what's done! ✅🚀

Afterward, we work our magic to transform it into the fantastic world of [XES](#-xes---extensible-event-stream). 🪄✨

## 📊 XES - eXtensible Event Stream
Expand All @@ -30,13 +31,6 @@ treasure map for uncovering insights in the world of processes and workflows!

For our Java-powered adventures, we especially use this [OpenXES](http://code.deckfour.org/xes/) implementation. 🧑‍💻

## 🚫 Limitations

🛑 Currently, you can only transform one process definition at a time. Therefore, when setting up your raw data report
in Camunda Optimize, make sure it contains only a single process definition.

🚫 You can use the application only with Java 21.

## 🚀🔜 Coming Soon... 🌟🎉👀

![Optimize CCR PPM Cycle](./assets/ppm-cycle.png)
Expand All @@ -50,7 +44,7 @@ To set up your configuration in `application.yaml`, follow these steps:
1. Include the Optimize base URL.
2. Specify the Raw Data Report ID.
3. Add the Client ID and Secret.
4. Specify the filename for the resulting XML (optionally, along with its path).
4. *Optionally:* Specify the base bath for the resulting XML(s).

Once you've completed these configurations, you'll be prepared to retrieve the data and convert it to XES format 🎉

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package de.envite.greenbpm.optimzetoxes

import de.envite.greenbpm.optimzetoxes.optimizeexport.usecase.`in`.OptimizeDataQuery
import de.envite.greenbpm.optimzetoxes.xesmapping.XesMappingConfigurationProperties
import de.envite.greenbpm.optimzetoxes.xesmapping.toXMLFile
import de.envite.greenbpm.optimzetoxes.xesmapping.toXes
import de.envite.greenbpm.optimzetoxes.xesmapping.writeXESLogToXML
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties
Expand All @@ -15,13 +15,12 @@ class OptimizeToXesApplication(
private val optimizeDataQuery: OptimizeDataQuery,
private val xesMappingConfigurationProperties: XesMappingConfigurationProperties
) : CommandLineRunner, Logging {
init {
require(xesMappingConfigurationProperties.filename.endsWith(".xml")) {
"You must provide a filename for the resulting XML which ends with '.xml'"
}
}
override fun run(vararg args: String?) =
writeXESLogToXML(optimizeDataQuery.fetchData().toXes(), xesMappingConfigurationProperties.filename)
optimizeDataQuery.fetchData()
.toXes(log())
.forEach{processDefinition ->
processDefinition.toXMLFile(xesMappingConfigurationProperties.basePath, log())
}
}

fun main(args: Array<String>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,22 @@ import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream

data class XesDefinition(
val processDefinitionId: String,
val log: XLog,
)

@Throws(Exception::class)
fun writeXESLogToXML(log: XLog, filename: String, logger: Logger? = null) {
fun XesDefinition.toXMLFile(basePath: String?, logger: Logger? = null) {
val filename = "${basePath?.plus("/") ?: ""}$processDefinitionId.xml"

logger?.debug("Writing XES to file {}", filename)

val fileOutput = FileOutputStream(File(filename))
val bufferedOutput = BufferedOutputStream(fileOutput)
val logSerializer: XSerializer = XesXmlSerializer()

logSerializer.serialize(log, bufferedOutput)
bufferedOutput.close()
fileOutput.close()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,37 @@ private const val ORG_GROUP = "org:group"

private val factory = XFactoryRegistry.instance().currentDefault()

// TODO: Restriction: Single Process-Definition
fun OptimizeData.toXes(logger: Logger? = null): XLog {
logger?.debug("Converting JSON Data to XES")

private fun createBaseLog(): XLog {
val log = factory.createLog()
val logAttributes: XAttributeMap = XAttributeMapImpl()
log.attributes = logAttributes

log.classifiers.add(XEventAttributeClassifier(CONCEPT_NAME, CONCEPT_NAME))
log.classifiers.add(XEventAttributeClassifier(ORG_GROUP, ORG_GROUP))
return log
}

data[0].let {
val processDefinitionKeyAttribute = XAttributeLiteralImpl("processDefinitionKey", data[0].processDefinitionKey)
val processDefinitionIdAttribute = XAttributeLiteralImpl("processDefinitionId", data[0].processDefinitionId)
fun OptimizeData.toXes(logger: Logger? = null): List<XesDefinition> {
logger?.debug("Converting JSON Data to XES")

log.globalTraceAttributes.add(processDefinitionKeyAttribute)
log.globalTraceAttributes.add(processDefinitionIdAttribute)
}
return data
.groupBy { it.processDefinitionId }
.filter { (_, processInstances) -> processInstances.isNotEmpty() }
.mapValues { (processDefinitionId, processInstances) ->
logger?.trace("Converting process definition $processDefinitionId to XES")
val log = createBaseLog()

val traces = data.map { it.toXes() }
val processDefinitionIdAttribute = XAttributeLiteralImpl("processDefinitionId", processDefinitionId)
val processDefinitionKeyAttribute = XAttributeLiteralImpl("processDefinitionKey", data[0].processDefinitionKey)

log.addAll(traces)
return log
log.globalTraceAttributes.add(processDefinitionKeyAttribute)
log.globalTraceAttributes.add(processDefinitionIdAttribute)

val traces = processInstances.map { it.toXes() }

log.addAll(traces)
XesDefinition(processDefinitionId, log)
}.values.toList()
}

fun ProcessInstance.toXes(): XTrace {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties("xes-mapping")
class XesMappingConfigurationProperties(
val filename: String,
val basePath: String?,
)
2 changes: 1 addition & 1 deletion src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ optimize:
token-base-url: https://login.cloud.camunda.io/oauth/token

xes-mapping:
filename: <xes-result-filename-or-path>.xml
base-path: <xes-result-filename-or-path>.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,24 @@ import de.envite.greenbpm.optimzetoxes.optimizeexport.domain.model.OptimizeData
import de.envite.greenbpm.optimzetoxes.optimizeexport.domain.model.ProcessInstance
import de.envite.greenbpm.optimzetoxes.optimizeexport.usecase.`in`.OptimizeDataQuery
import de.envite.greenbpm.optimzetoxes.xesmapping.XesMappingConfigurationProperties
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows

class OptimizeToXesApplicationTest {

private val optimizeDataQueryMock: OptimizeDataQuery = mockk()

private lateinit var classUnderTest: OptimizeToXesApplication

@Nested
inner class ExceptionHandling {

@Test
fun should_throw_if_no_filename_provided() {
assertThrows<IllegalArgumentException> {
OptimizeToXesApplication(optimizeDataQueryMock, XesMappingConfigurationProperties(""))
}.message shouldBe "You must provide a filename for the resulting XML which ends with '.xml'"
}
}

@Nested
inner class ETL {

@BeforeEach
fun setUp() {
classUnderTest = OptimizeToXesApplication(optimizeDataQueryMock, XesMappingConfigurationProperties("xes-output.xml"))
classUnderTest = OptimizeToXesApplication(optimizeDataQueryMock, XesMappingConfigurationProperties("target"))
}

@Test
Expand Down

0 comments on commit a3a5e1c

Please sign in to comment.