diff --git a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/CanvasBuilder.kt b/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/CanvasBuilder.kt index 5fad095dd..7ff0efc29 100644 --- a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/CanvasBuilder.kt +++ b/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/CanvasBuilder.kt @@ -2,6 +2,7 @@ package com.valb3r.bpmn.intellij.plugin import com.intellij.psi.PsiFile import com.intellij.ui.EditorTextField +import com.valb3r.bpmn.intellij.plugin.events.setUpdateEventsRegistry import com.valb3r.bpmn.intellij.plugin.events.updateEventsRegistry import com.valb3r.bpmn.intellij.plugin.flowable.parser.FlowableObjectFactory import com.valb3r.bpmn.intellij.plugin.flowable.parser.FlowableParser @@ -13,13 +14,14 @@ import javax.swing.JTable class CanvasBuilder { - private val updateEvents = updateEventsRegistry() private var newObjectsFactory: NewElementsProvider? = null fun build(properties: JTable, editorFactory: (value: String) -> EditorTextField, canvas: Canvas, bpmnFile: PsiFile) { - updateEvents.reset() + val parser = FlowableParser() + setUpdateEventsRegistry(parser, bpmnFile.virtualFile) + updateEventsRegistry().reset() bpmnFile.virtualFile.inputStream.use { - val process = FlowableParser().parse(it) + val process = parser.parse(it) newObjectsFactory = newElementsFactory(FlowableObjectFactory()) canvas.reset(properties, editorFactory, process.toView(newObjectsFactory!!), BpmnProcessRenderer()) } diff --git a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/events/Events.kt b/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/events/Events.kt index 3183ce4a4..4cad875be 100644 --- a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/events/Events.kt +++ b/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/events/Events.kt @@ -4,18 +4,17 @@ import com.valb3r.bpmn.intellij.plugin.bpmn.api.bpmn.BpmnElementId import com.valb3r.bpmn.intellij.plugin.bpmn.api.bpmn.elements.WithBpmnId import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.DiagramElementId import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.elements.ShapeElement +import com.valb3r.bpmn.intellij.plugin.bpmn.api.events.* import com.valb3r.bpmn.intellij.plugin.bpmn.api.info.Property import com.valb3r.bpmn.intellij.plugin.bpmn.api.info.PropertyType -import com.valb3r.bpmn.intellij.plugin.render.EdgeElementState -import com.valb3r.bpmn.intellij.plugin.render.WaypointElementState -data class StringValueUpdatedEvent(override val bpmnElementId: BpmnElementId, override val property: PropertyType, val newValue: String): PropertyUpdateWithId +data class StringValueUpdatedEvent(override val bpmnElementId: BpmnElementId, override val property: PropertyType, override val newValue: String): PropertyUpdateWithId -data class BooleanValueUpdatedEvent(override val bpmnElementId: BpmnElementId, override val property: PropertyType, val newValue: Boolean): PropertyUpdateWithId +data class BooleanValueUpdatedEvent(override val bpmnElementId: BpmnElementId, override val property: PropertyType, override val newValue: Boolean): PropertyUpdateWithId -data class DraggedToEvent(override val diagramElementId: DiagramElementId, override val dx: Float, override val dy: Float): LocationUpdateWithId +data class DraggedToEvent(override val diagramElementId: DiagramElementId, override val dx: Float, override val dy: Float, override val parentElementId: DiagramElementId?, override val internalPos: Int?): LocationUpdateWithId -data class NewWaypointsEvent(override val edgeElementId: DiagramElementId, override val waypoints: List): NewWaypoints +data class NewWaypointsEvent(override val edgeElementId: DiagramElementId, override val waypoints: List, override val epoch: Int): NewWaypoints data class DiagramElementRemovedEvent(override val elementId: DiagramElementId): DiagramElementRemoved @@ -23,6 +22,6 @@ data class BpmnElementRemovedEvent(override val elementId: BpmnElementId): BpmnE data class BpmnShapeObjectAddedEvent(override val bpmnObject: WithBpmnId, override val shape: ShapeElement, override val props: Map): BpmnShapeObjectAdded -data class BpmnEdgeObjectAddedEvent(override val bpmnObject: WithBpmnId, override val edge: EdgeElementState, override val props: Map): BpmnEdgeObjectAdded +data class BpmnEdgeObjectAddedEvent(override val bpmnObject: WithBpmnId, override val edge: EdgeWithIdentifiableWaypoints, override val props: Map): BpmnEdgeObjectAdded data class CommittedToFile(val eventCount: Int): Event \ No newline at end of file diff --git a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/events/ProcessModelUpdateEvents.kt b/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/events/ProcessModelUpdateEvents.kt index 1994e1ce1..9741b7185 100644 --- a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/events/ProcessModelUpdateEvents.kt +++ b/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/events/ProcessModelUpdateEvents.kt @@ -1,7 +1,16 @@ package com.valb3r.bpmn.intellij.plugin.events +import com.intellij.openapi.application.WriteAction +import com.intellij.openapi.vfs.VirtualFile +import com.valb3r.bpmn.intellij.plugin.bpmn.api.BpmnParser import com.valb3r.bpmn.intellij.plugin.bpmn.api.bpmn.BpmnElementId import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.DiagramElementId +import com.valb3r.bpmn.intellij.plugin.bpmn.api.events.Event +import com.valb3r.bpmn.intellij.plugin.bpmn.api.events.EventOrder +import com.valb3r.bpmn.intellij.plugin.bpmn.api.events.LocationUpdateWithId +import com.valb3r.bpmn.intellij.plugin.bpmn.api.events.PropertyUpdateWithId +import java.io.ByteArrayOutputStream +import java.io.IOException import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicLong @@ -9,18 +18,16 @@ import java.util.concurrent.atomic.AtomicReference private val updateEvents = AtomicReference() -fun updateEventsRegistry(): ProcessModelUpdateEvents { - return updateEvents.updateAndGet { - if (null == it) { - return@updateAndGet ProcessModelUpdateEvents(CopyOnWriteArrayList()) - } +fun setUpdateEventsRegistry(parser: BpmnParser, file: VirtualFile) { + updateEvents.set(ProcessModelUpdateEvents(parser, file, CopyOnWriteArrayList())) +} - return@updateAndGet it - } +fun updateEventsRegistry(): ProcessModelUpdateEvents { + return updateEvents.get()!! } // Global singleton -class ProcessModelUpdateEvents(private val updates: MutableList>) { +class ProcessModelUpdateEvents(private val parser: BpmnParser, private val file: VirtualFile, private val updates: MutableList>) { private val order: AtomicLong = AtomicLong() private val fileCommitListeners: MutableList = ArrayList() @@ -46,48 +53,68 @@ class ProcessModelUpdateEvents(private val updates: MutableList } fun commitToFile() { + val lastCommit = updates.filter { it.event is CommittedToFile }.maxBy { it.order } + val bos = ByteArrayOutputStream() + file.inputStream.use {input -> + parser.update(input, bos, updates.filter { it.order > (lastCommit?.order ?: -1) }.map { it.event }) + } + val toStore = Order(order.getAndIncrement(), CommittedToFile(0)) + updates.add(toStore) + + WriteAction.run { + file.getOutputStream(null).use { + it.write(bos.toByteArray()) + } + } } fun addPropertyUpdateEvent(event: PropertyUpdateWithId) { val toStore = Order(order.getAndIncrement(), event) updates.add(toStore) propertyUpdatesByStaticId.computeIfAbsent(event.bpmnElementId) { CopyOnWriteArrayList() } += toStore + commitToFile() } fun addLocationUpdateEvent(event: LocationUpdateWithId) { val toStore = Order(order.getAndIncrement(), event) updates.add(toStore) locationUpdatesByStaticId.computeIfAbsent(event.diagramElementId) { CopyOnWriteArrayList() } += toStore + commitToFile() } fun addWaypointStructureUpdate(event: NewWaypointsEvent) { val toStore = Order(order.getAndIncrement(), event) updates.add(toStore) parentCreatesByStaticId.computeIfAbsent(event.edgeElementId) { CopyOnWriteArrayList() } += toStore + commitToFile() } fun addElementRemovedEvent(event: DiagramElementRemovedEvent) { val toStore = Order(order.getAndIncrement(), event) updates.add(toStore) deletionsByStaticId.computeIfAbsent(event.elementId) { CopyOnWriteArrayList() } += toStore + commitToFile() } fun addElementRemovedEvent(event: BpmnElementRemovedEvent) { val toStore = Order(order.getAndIncrement(), event) updates.add(toStore) deletionsByStaticBpmnId.computeIfAbsent(event.elementId) { CopyOnWriteArrayList() } += toStore + commitToFile() } fun addObjectEvent(event: BpmnShapeObjectAddedEvent) { val toStore = Order(order.getAndIncrement(), event) updates.add(toStore) newShapeElements.add(toStore) + commitToFile() } fun addObjectEvent(event: BpmnEdgeObjectAddedEvent) { val toStore = Order(order.getAndIncrement(), event) updates.add(toStore) newDiagramElements.add(toStore) + commitToFile() } fun currentPropertyUpdateEventList(elementId: BpmnElementId): List> { diff --git a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/properties/PropertiesVisualizer.kt b/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/properties/PropertiesVisualizer.kt index 9d4dc7f4b..ef635cdf3 100644 --- a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/properties/PropertiesVisualizer.kt +++ b/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/properties/PropertiesVisualizer.kt @@ -11,15 +11,27 @@ import com.valb3r.bpmn.intellij.plugin.events.BooleanValueUpdatedEvent import com.valb3r.bpmn.intellij.plugin.events.StringValueUpdatedEvent import com.valb3r.bpmn.intellij.plugin.events.updateEventsRegistry import com.valb3r.bpmn.intellij.plugin.ui.components.FirstColumnReadOnlyModel -import java.awt.event.FocusEvent -import java.awt.event.FocusListener import javax.swing.JTable class PropertiesVisualizer(val table: JTable, val editorFactory: (value: String) -> EditorTextField) { - private val updateRegistry = updateEventsRegistry() private var listenersForCurrentView: MutableList<(() -> Unit)> = mutableListOf() + @Synchronized + fun clear() { + // fire de-focus to move changes to memory, component listeners doesn't seem to work with EditorTextField + listenersForCurrentView.forEach { it() } + listenersForCurrentView.clear() + + // drop and re-create table model + val model = FirstColumnReadOnlyModel() + model.addColumn("") + model.addColumn("") + table.model = model + table.columnModel.getColumn(1).preferredWidth = 500 + model.fireTableDataChanged() + } + @Synchronized fun visualize(bpmnElementId: BpmnElementId, properties: Map) { // fire de-focus to move changes to memory, component listeners doesn't seem to work with EditorTextField @@ -51,7 +63,7 @@ class PropertiesVisualizer(val table: JTable, val editorFactory: (value: String) listenersForCurrentView.add { if (initialValue != field.text) { - updateRegistry.addPropertyUpdateEvent(StringValueUpdatedEvent(bpmnElementId, type, field.text)) + updateEventsRegistry().addPropertyUpdateEvent(StringValueUpdatedEvent(bpmnElementId, type, field.text)) } } return field @@ -64,7 +76,7 @@ class PropertiesVisualizer(val table: JTable, val editorFactory: (value: String) listenersForCurrentView.add { if (initialValue != field.isSelected) { - updateRegistry.addPropertyUpdateEvent(StringValueUpdatedEvent(bpmnElementId, type, field.text)) + updateEventsRegistry().addPropertyUpdateEvent(BooleanValueUpdatedEvent(bpmnElementId, type, field.isSelected)) } } return field @@ -88,7 +100,7 @@ class PropertiesVisualizer(val table: JTable, val editorFactory: (value: String) val initialValue = field.text listenersForCurrentView.add { if (initialValue != field.text) { - updateRegistry.addPropertyUpdateEvent(StringValueUpdatedEvent(bpmnElementId, type, removeQuotes(field.text))) + updateEventsRegistry().addPropertyUpdateEvent(StringValueUpdatedEvent(bpmnElementId, type, removeQuotes(field.text))) } } } @@ -98,7 +110,7 @@ class PropertiesVisualizer(val table: JTable, val editorFactory: (value: String) } private fun lastStringValueFromRegistry(bpmnElementId: BpmnElementId, type: PropertyType): String? { - return (updateRegistry.currentPropertyUpdateEventList(bpmnElementId) + return (updateEventsRegistry().currentPropertyUpdateEventList(bpmnElementId) .map { it.event } .filter { it.property.id == type.id } .lastOrNull { it is StringValueUpdatedEvent } as StringValueUpdatedEvent?) @@ -106,21 +118,10 @@ class PropertiesVisualizer(val table: JTable, val editorFactory: (value: String) } private fun lastBooleanValueFromRegistry(bpmnElementId: BpmnElementId, type: PropertyType): Boolean? { - return (updateRegistry.currentPropertyUpdateEventList(bpmnElementId) + return (updateEventsRegistry().currentPropertyUpdateEventList(bpmnElementId) .map { it.event } .filter { it.property.id == type.id } .lastOrNull { it is BooleanValueUpdatedEvent } as BooleanValueUpdatedEvent?) ?.newValue } - - private class FocusEventListener(val onFocusLost: ((e: FocusEvent?) -> Unit)): FocusListener { - - override fun focusLost(e: FocusEvent?) { - onFocusLost(e) - } - - override fun focusGained(e: FocusEvent?) { - // NOP - } - } } \ No newline at end of file diff --git a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/render/BpmnProcessRenderer.kt b/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/render/BpmnProcessRenderer.kt index 9d7f271e5..79ac1b0bf 100644 --- a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/render/BpmnProcessRenderer.kt +++ b/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/render/BpmnProcessRenderer.kt @@ -6,6 +6,8 @@ import com.valb3r.bpmn.intellij.plugin.bpmn.api.bpmn.BpmnElementId import com.valb3r.bpmn.intellij.plugin.bpmn.api.bpmn.elements.* import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.DiagramElementId import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.elements.* +import com.valb3r.bpmn.intellij.plugin.bpmn.api.events.EdgeWithIdentifiableWaypoints +import com.valb3r.bpmn.intellij.plugin.bpmn.api.events.IdentifiableWaypoint import com.valb3r.bpmn.intellij.plugin.bpmn.api.info.Property import com.valb3r.bpmn.intellij.plugin.bpmn.api.info.PropertyType import com.valb3r.bpmn.intellij.plugin.bpmn.api.info.PropertyType.NAME @@ -52,7 +54,7 @@ class BpmnProcessRenderer { return areaByElement } - private fun drawBpmnEdges(shapes: List, areaByElement: MutableMap, canvas: CanvasPainter, renderMeta: RenderMetadata) { + private fun drawBpmnEdges(shapes: List, areaByElement: MutableMap, canvas: CanvasPainter, renderMeta: RenderMetadata) { shapes.forEach { mergeArea(it.id, areaByElement, drawEdgeElement(canvas, it, renderMeta)) if (isActive(it.id, renderMeta)) { @@ -63,7 +65,6 @@ class BpmnProcessRenderer { val deleteCallback = { dest: ProcessModelUpdateEvents -> dest.addElementRemovedEvent(DiagramElementRemovedEvent(it.id))} val actionsElem = drawActionsElement(canvas, it, renderMeta.interactionContext, mutableMapOf(Actions.DELETE to deleteCallback)) areaByElement += actionsElem - renderMeta.interactionContext.dragEndCallbacks[it.id] = { dx: Float, dy: Float, dest: ProcessModelUpdateEvents -> dest.addLocationUpdateEvent(DraggedToEvent(it.id, dx, dy))} } } } @@ -87,7 +88,7 @@ class BpmnProcessRenderer { } val actionsElem = drawActionsElement(canvas, it, renderMeta.interactionContext, mutableMapOf(Actions.DELETE to deleteCallback, Actions.NEW_LINK to newSequenceCallback)) areaByElement += actionsElem - renderMeta.interactionContext.dragEndCallbacks[it.id] = { dx: Float, dy: Float, dest: ProcessModelUpdateEvents -> dest.addLocationUpdateEvent(DraggedToEvent(it.id, dx, dy))} + renderMeta.interactionContext.dragEndCallbacks[it.id] = { dx: Float, dy: Float, dest: ProcessModelUpdateEvents, droppedOn: BpmnElementId? -> dest.addLocationUpdateEvent(DraggedToEvent(it.id, dx, dy, null, null))} } } } @@ -106,7 +107,7 @@ class BpmnProcessRenderer { areas[id] = AreaWithZindex(target.area, area.dragCenter, target.areaType, target.anchorsForWaypoints, target.anchorsForShape, min(target.index, area.index), target.parentToSelect ?: area.parentToSelect) } - private fun drawEdgeElement(canvas: CanvasPainter, shape: EdgeElementState, meta: RenderMetadata): AreaWithZindex { + private fun drawEdgeElement(canvas: CanvasPainter, shape: EdgeWithIdentifiableWaypoints, meta: RenderMetadata): AreaWithZindex { val active = isActive(shape.id, meta) val area = Area() @@ -122,7 +123,7 @@ class BpmnProcessRenderer { return AreaWithZindex(area, Point2D.Float(0.0f, 0.0f), AreaType.POINT, if (active) mutableSetOf() else trueWaypoints.map { Point2D.Float(it.x, it.y) }.toMutableSet()) } - private fun drawWaypointElements(canvas: CanvasPainter, shape: EdgeElementState, meta: RenderMetadata): Map { + private fun drawWaypointElements(canvas: CanvasPainter, shape: EdgeWithIdentifiableWaypoints, meta: RenderMetadata): Map { val area = HashMap() val trueWaypoints = calculateTrueWaypoints(shape, meta) // draw all endpoints only if none virtual is dragged and not physical @@ -139,24 +140,33 @@ class BpmnProcessRenderer { return area } - private fun drawWaypointAnchors(canvas: CanvasPainter, begin: WaypointElementState, end: WaypointElementState, parent: EdgeElementState, meta: RenderMetadata, isLast: Boolean, endWaypointIndex: Int): Map { + private fun drawWaypointAnchors(canvas: CanvasPainter, begin: IdentifiableWaypoint, end: IdentifiableWaypoint, parent: EdgeWithIdentifiableWaypoints, meta: RenderMetadata, isLast: Boolean, endWaypointIndex: Int): Map { val result = HashMap() - val dragCallback = {dx: Float, dy: Float, dest: ProcessModelUpdateEvents, elem: WaypointElementState -> + val dragCallback = {dx: Float, dy: Float, dest: ProcessModelUpdateEvents, elem: IdentifiableWaypoint, droppedOn: BpmnElementId? -> + val index = parent.waypoint.indexOf(elem) if (elem.physical) { - dest.addLocationUpdateEvent(DraggedToEvent(elem.id, dx, dy)) + dest.addLocationUpdateEvent(DraggedToEvent(elem.id, dx, dy, parent.id, elem.internalPhysicalPos)) + if (null != droppedOn && null != parent.bpmnElement) { + if (parent.waypoint.size - 1 == index ) { + dest.addPropertyUpdateEvent(StringValueUpdatedEvent(parent.bpmnElement!!, PropertyType.TARGET_REF, droppedOn.id)) + } else if (0 == index) { + dest.addPropertyUpdateEvent(StringValueUpdatedEvent(parent.bpmnElement!!, PropertyType.SOURCE_REF, droppedOn.id)) + } + } } else { dest.addWaypointStructureUpdate(NewWaypointsEvent( parent.id, parent.waypoint .filter { it.physical || it.id == elem.id } - .map { if (it.id == elem.id && !it.physical) it.moveTo(dx, dy).asPhysical() else it.originalLocation() } - .toList() + .map { if (it.id == elem.id && !it.physical) it.moveTo(dx, dy).asPhysical() else it } + .toList(), + parent.epoch + 1 )) } } - val drawNode = { node: WaypointElementState, index: Int -> + val drawNode = { node: IdentifiableWaypoint, index: Int -> val translatedNode = translateElement(meta, node) val active = isActive(node.id, meta) val color = color(active, if (node.physical) Colors.WAYPOINT_COLOR else Colors.MID_WAYPOINT_COLOR) @@ -169,15 +179,15 @@ class BpmnProcessRenderer { ANCHOR_Z_INDEX, parent.id ) - meta.interactionContext.dragEndCallbacks[node.id] = { dx: Float, dy: Float, dest: ProcessModelUpdateEvents -> dragCallback(dx, dy, dest, node)} + meta.interactionContext.dragEndCallbacks[node.id] = { dx: Float, dy: Float, dest: ProcessModelUpdateEvents, droppedOn: BpmnElementId? -> dragCallback(dx, dy, dest, node, droppedOn)} if (active && node.physical && index > 0 && (index < parent.waypoint.size - 1)) { val callback = { dest: ProcessModelUpdateEvents -> dest.addWaypointStructureUpdate(NewWaypointsEvent( parent.id, parent.waypoint .filter { it.physical } .filter { it.id != node.id } - .map { it.originalLocation() } - .toList() + .toList(), + parent.epoch + 1 ))} result += drawActionsElement(canvas, translatedNode, meta.interactionContext, mapOf(Actions.DELETE to callback)) } @@ -190,15 +200,15 @@ class BpmnProcessRenderer { } drawNode(begin, endWaypointIndex - 1) - meta.interactionContext.dragEndCallbacks[begin.id] = { dx: Float, dy: Float, dest: ProcessModelUpdateEvents -> dragCallback(dx, dy, dest, begin)} + meta.interactionContext.dragEndCallbacks[begin.id] = { dx: Float, dy: Float, dest: ProcessModelUpdateEvents, droppedOn: BpmnElementId? -> dragCallback(dx, dy, dest, begin, droppedOn)} return result } - private fun calculateTrueWaypoints(shape: EdgeElementState, meta: RenderMetadata): List { + private fun calculateTrueWaypoints(shape: EdgeWithIdentifiableWaypoints, meta: RenderMetadata): List { return shape.waypoint.filter { it.physical || isActive(it.id, meta) } } - private fun drawEdge(canvas: CanvasPainter, begin: WaypointElementState, end: WaypointElementState, meta: RenderMetadata, color: Color, isLast: Boolean): Area { + private fun drawEdge(canvas: CanvasPainter, begin: IdentifiableWaypoint, end: IdentifiableWaypoint, meta: RenderMetadata, color: Color, isLast: Boolean): Area { val translatedBegin = translateElement(meta, begin) val translatedEnd = translateElement(meta, end) if (isLast) { @@ -326,7 +336,7 @@ class BpmnProcessRenderer { return elemId.let { meta.selectedIds.contains(it) } } - private fun drawActionsElement(canvas: CanvasPainter, edge: EdgeElementState, ctx: ElementInteractionContext, actions: Map Unit>): Map { + private fun drawActionsElement(canvas: CanvasPainter, edge: EdgeWithIdentifiableWaypoints, ctx: ElementInteractionContext, actions: Map Unit>): Map { val minX = edge.waypoint.minBy { it.x }?.x ?: 0.0f val minY = edge.waypoint.minBy { it.y }?.y ?: 0.0f val maxX = edge.waypoint.maxBy { it.x }?.x ?: 0.0f @@ -342,7 +352,7 @@ class BpmnProcessRenderer { ) } - private fun drawActionsElement(canvas: CanvasPainter, waypoint: WaypointElementState, ctx: ElementInteractionContext, actions: Map Unit>): Map { + private fun drawActionsElement(canvas: CanvasPainter, waypoint: IdentifiableWaypoint, ctx: ElementInteractionContext, actions: Map Unit>): Map { return drawActionsElement( canvas, waypoint.id, diff --git a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/render/Canvas.kt b/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/render/Canvas.kt index 5a9536770..5081ac408 100644 --- a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/render/Canvas.kt +++ b/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/render/Canvas.kt @@ -4,6 +4,8 @@ import com.google.common.cache.CacheBuilder import com.intellij.ui.EditorTextField import com.valb3r.bpmn.intellij.plugin.Colors import com.valb3r.bpmn.intellij.plugin.bpmn.api.BpmnProcessObjectView +import com.valb3r.bpmn.intellij.plugin.bpmn.api.bpmn.BpmnElementId +import com.valb3r.bpmn.intellij.plugin.bpmn.api.bpmn.elements.BpmnSequenceFlow import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.DiagramElementId import com.valb3r.bpmn.intellij.plugin.events.updateEventsRegistry import com.valb3r.bpmn.intellij.plugin.properties.PropertiesVisualizer @@ -30,7 +32,6 @@ class Canvas: JPanel() { private val defaultCameraOrigin = Point2D.Float(0f, 0f) private val defaultZoomRatio = 1f private val stateProvider = currentStateProvider() - private val updateEvents = updateEventsRegistry() private var selectedElements: MutableSet = mutableSetOf() private var camera = Camera(defaultCameraOrigin, Point2D.Float(defaultZoomRatio, defaultZoomRatio)) @@ -67,7 +68,7 @@ class Canvas: JPanel() { fun click(location: Point2D.Float) { val clickedElements = elemsUnderCursor(location) - clickedElements.forEach { interactionCtx.clickCallbacks.get(it)?.invoke(updateEvents) } + clickedElements.forEach { interactionCtx.clickCallbacks.get(it)?.invoke(updateEventsRegistry()) } this.selectedElements.clear() interactionCtx = ElementInteractionContext(mutableSetOf(), mutableMapOf(), mutableMapOf(), emptySet(), Point2D.Float(), Point2D.Float()) @@ -80,7 +81,7 @@ class Canvas: JPanel() { .elementByDiagramId[elementIdForPropertiesTable] ?.let { elemId -> stateProvider.currentState().elemPropertiesByStaticElementId[elemId]?.let { propertiesVisualizer?.visualize(elemId, it) } - } + } ?: propertiesVisualizer?.clear() } fun dragCanvas(start: Point2D.Float, current: Point2D.Float) { @@ -171,7 +172,7 @@ class Canvas: JPanel() { interactionCtx = attractToAnchors(interactionCtx) val dx = interactionCtx.current.x - interactionCtx.start.x val dy = interactionCtx.current.y - interactionCtx.start.y - interactionCtx.draggedIds.forEach { interactionCtx.dragEndCallbacks[it]?.invoke(dx, dy, updateEvents) } + interactionCtx.draggedIds.forEach { interactionCtx.dragEndCallbacks[it]?.invoke(dx, dy, updateEventsRegistry(), bpmnElemsUnderDragCurrent()) } } interactionCtx = interactionCtx.copy(draggedIds = emptySet()) @@ -200,6 +201,20 @@ class Canvas: JPanel() { return camera.fromCameraView(point) } + private fun bpmnElemsUnderDragCurrent(): BpmnElementId? { + val onScreen = camera.toCameraView(interactionCtx.current) + val cursor = cursorRect(onScreen) + val elems = areaByElement?.filter { it.value.area.intersects(cursor) } + + return elems?.map { stateProvider.currentState().elementByDiagramId[it.key] } + ?.filterNotNull() + ?.map { stateProvider.currentState().elementByStaticId[it] } + ?.filter { it !is BpmnSequenceFlow } + ?.filterNotNull() + ?.map { it.id } + ?.firstOrNull() + } + private fun elemsUnderCursor(cursorPoint: Point2D.Float): List { val cursor = cursorRect(cursorPoint) val intersection = areaByElement?.filter { it.value.area.intersects(cursor) } diff --git a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/render/EdgeExtension.kt b/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/render/EdgeExtension.kt index c591dad4e..2a59e743f 100644 --- a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/render/EdgeExtension.kt +++ b/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/render/EdgeExtension.kt @@ -1,79 +1,89 @@ package com.valb3r.bpmn.intellij.plugin.render +import com.google.common.hash.Hashing import com.valb3r.bpmn.intellij.plugin.bpmn.api.bpmn.BpmnElementId import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.DiagramElementId import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.elements.EdgeElement -import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.elements.Translatable import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.elements.WaypointElement -import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.elements.WithDiagramId -import java.util.* -import kotlin.collections.ArrayList +import com.valb3r.bpmn.intellij.plugin.bpmn.api.events.EdgeWithIdentifiableWaypoints +import com.valb3r.bpmn.intellij.plugin.bpmn.api.events.IdentifiableWaypoint +import java.nio.charset.StandardCharsets.UTF_8 data class EdgeElementState ( - val id: DiagramElementId, - val bpmnElement: BpmnElementId?, - val waypoint: MutableList = mutableListOf() -) { - constructor(elem: EdgeElement): this(elem.id, elem.bpmnElement, ArrayList()) { + override val id: DiagramElementId, + override val bpmnElement: BpmnElementId?, + override val waypoint: MutableList = mutableListOf(), + override val epoch: Int +): EdgeWithIdentifiableWaypoints { + constructor(elem: EdgeElement): this(elem.id, elem.bpmnElement, ArrayList(), 0) { elem.waypoint?.withIndex()?.forEach { when { - it.index == 0 -> waypoint += WaypointElementState(it.value) + it.index == 0 -> waypoint += WaypointElementState(waypointId(epoch, 0), it.value, 0) it.index > 0 -> { val midpointX = (elem.waypoint!![it.index - 1].x + it.value.x) / 2.0f val midpointY = (elem.waypoint!![it.index - 1].y + it.value.y) / 2.0f - val next = WaypointElementState(it.value) - waypoint += WaypointElementState(waypoint[it.index - 1].id.id + ":" + next.id.id, midpointX, midpointY) + val next = WaypointElementState(waypointId(epoch, it.index), it.value, it.index) + waypoint += WaypointElementState(childWaypointId(waypoint[it.index - 1], next), midpointX, midpointY, it.index) waypoint += next } } } } - constructor(toCopy: EdgeElementState, newPhysicalWaypoints: List): this(toCopy.id, toCopy.bpmnElement, ArrayList()) { + constructor(toCopy: EdgeWithIdentifiableWaypoints, newPhysicalWaypoints: List, newEpoch: Int): this(toCopy.id, toCopy.bpmnElement, ArrayList(), newEpoch) { newPhysicalWaypoints.withIndex().forEach { when { - it.index == 0 -> waypoint += it.value + it.index == 0 -> waypoint += WaypointElementState(waypointId(epoch, 0), it.value.asWaypointElement(), 0) it.index > 0 -> { val midpointX = (newPhysicalWaypoints[it.index - 1].x + it.value.x) / 2.0f val midpointY = (newPhysicalWaypoints[it.index - 1].y + it.value.y) / 2.0f - val next = it.value - waypoint += WaypointElementState(waypoint[it.index - 1].id.id + ":" + next.id.id, midpointX, midpointY) + val next = WaypointElementState(waypointId(epoch, it.index), it.value.asWaypointElement(), it.index) + waypoint += WaypointElementState(childWaypointId(waypoint[it.index - 1], next), midpointX, midpointY, it.index) waypoint += next } } } } + + fun waypointId(currentEpoch: Int, internalOrder: Int): String { + return Hashing.md5().hashString(currentEpoch.toString() + ":" + id.id + ":" + internalOrder, UTF_8).toString() + } + + fun childWaypointId(start: IdentifiableWaypoint, end: IdentifiableWaypoint): String { + return Hashing.md5().hashString(start.id.id + ":" + end.id.id, UTF_8).toString() + } } data class WaypointElementState ( override val id: DiagramElementId, - val x: Float, - val y: Float, - val origX: Float, - val origY: Float, - val physical: Boolean -): Translatable, WithDiagramId { + override val x: Float, + override val y: Float, + override val origX: Float, + override val origY: Float, + override val physical: Boolean, + override val internalPhysicalPos: Int +): IdentifiableWaypoint { - constructor(elem: WaypointElement): this(DiagramElementId(UUID.randomUUID().toString()), elem.x, elem.y, elem.x, elem.y, true) - constructor(id: String, x: Float, y: Float): this(DiagramElementId(id), x, y, x, y, false) + constructor(id: String, elem: WaypointElement, internalPos: Int): this(DiagramElementId(id), elem.x, elem.y, elem.x, elem.y, true, internalPos) + constructor(id: String, x: Float, y: Float, internalPos: Int): this(DiagramElementId(id), x, y, x, y, false, internalPos) override fun copyAndTranslate(dx: Float, dy: Float): WaypointElementState { - return WaypointElementState(id, x + dx, y + dy, origX, origY, physical) + return WaypointElementState(id, x + dx, y + dy, origX, origY, physical, internalPhysicalPos) } - fun moveTo(dx: Float, dy: Float): WaypointElementState { - return WaypointElementState(id, x + dx, y + dy, x + dx, y + dy, physical) + override fun moveTo(dx: Float, dy: Float): WaypointElementState { + return WaypointElementState(id, x + dx, y + dy, x + dx, y + dy, physical, internalPhysicalPos) } - fun asPhysical(): WaypointElementState { - return WaypointElementState(id, x, y, origX, origY, true) + override fun asPhysical(): WaypointElementState { + return WaypointElementState(id, x, y, origX, origY, true, internalPhysicalPos) } - fun originalLocation(): WaypointElementState { - return WaypointElementState(id, origX, origY, origX, origY, true) + override fun originalLocation(): WaypointElementState { + return WaypointElementState(id, origX, origY, origX, origY, true, internalPhysicalPos) } - fun asWaypointElement(): WaypointElement { + override fun asWaypointElement(): WaypointElement { return WaypointElement(x, y) } } \ No newline at end of file diff --git a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/render/ElementInteractionContext.kt b/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/render/ElementInteractionContext.kt index 8c275ab9e..0433bcb8b 100644 --- a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/render/ElementInteractionContext.kt +++ b/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/render/ElementInteractionContext.kt @@ -1,12 +1,13 @@ package com.valb3r.bpmn.intellij.plugin.render +import com.valb3r.bpmn.intellij.plugin.bpmn.api.bpmn.BpmnElementId import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.DiagramElementId import com.valb3r.bpmn.intellij.plugin.events.ProcessModelUpdateEvents import java.awt.geom.Point2D data class ElementInteractionContext( val draggedIds: Set, - val dragEndCallbacks: MutableMap Unit>, + val dragEndCallbacks: MutableMap Unit>, val clickCallbacks: MutableMap Unit>, val anchorsHit: Set>, val start: Point2D.Float, diff --git a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/state/CurrentState.kt b/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/state/CurrentState.kt index f8c8a3a17..9f165e862 100644 --- a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/state/CurrentState.kt +++ b/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/state/CurrentState.kt @@ -5,12 +5,13 @@ import com.valb3r.bpmn.intellij.plugin.bpmn.api.bpmn.BpmnElementId import com.valb3r.bpmn.intellij.plugin.bpmn.api.bpmn.elements.WithBpmnId import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.DiagramElementId import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.elements.ShapeElement +import com.valb3r.bpmn.intellij.plugin.bpmn.api.events.EdgeWithIdentifiableWaypoints +import com.valb3r.bpmn.intellij.plugin.bpmn.api.events.IdentifiableWaypoint import com.valb3r.bpmn.intellij.plugin.bpmn.api.info.Property import com.valb3r.bpmn.intellij.plugin.bpmn.api.info.PropertyType import com.valb3r.bpmn.intellij.plugin.events.StringValueUpdatedEvent import com.valb3r.bpmn.intellij.plugin.events.updateEventsRegistry import com.valb3r.bpmn.intellij.plugin.render.EdgeElementState -import com.valb3r.bpmn.intellij.plugin.render.WaypointElementState import java.util.concurrent.atomic.AtomicReference private val currentStateProvider = AtomicReference() @@ -27,7 +28,7 @@ fun currentStateProvider(): CurrentStateProvider { data class CurrentState( val shapes: List, - val edges: List, + val edges: List, val elementByDiagramId: Map, val elementByStaticId: Map, val elemPropertiesByStaticElementId: Map> @@ -35,8 +36,6 @@ data class CurrentState( // Global singleton class CurrentStateProvider { - - private val updateEvents = updateEventsRegistry() private var fileState = CurrentState(emptyList(), emptyList(), emptyMap(), emptyMap(), emptyMap()) private var currentState = CurrentState(emptyList(), emptyList(), emptyMap(), emptyMap(), emptyMap()) @@ -49,13 +48,13 @@ class CurrentStateProvider { processObject.elemPropertiesByElementId ) currentState = fileState - updateEvents.reset() + updateEventsRegistry().reset() } fun currentState(): CurrentState { - val newShapes = updateEvents.newShapeElements().map { it.event } - val newEdges = updateEvents.newEdgeElements().map { it.event } - val newEdgeElems = updateEvents.newEdgeElements().map { it.event } + val newShapes = updateEventsRegistry().newShapeElements().map { it.event } + val newEdges = updateEventsRegistry().newEdgeElements().map { it.event } + val newEdgeElems = updateEventsRegistry().newEdgeElements().map { it.event } val newElementByDiagramId: MutableMap = HashMap() val newElementByStaticId: MutableMap = HashMap() @@ -75,7 +74,7 @@ class CurrentStateProvider { val updatedProperties: MutableMap> = HashMap() allProperties.forEach { prop -> updatedProperties[prop.key] = prop.value.toMutableMap() - updateEvents.currentPropertyUpdateEventList(prop.key) + updateEventsRegistry().currentPropertyUpdateEventList(prop.key) .map { it.event } .filterIsInstance() .forEach { @@ -85,10 +84,10 @@ class CurrentStateProvider { return CurrentState( shapes = fileState.shapes.toList().union(newShapes.map { it.shape }) - .filter { !updateEvents.isDeleted(it.id) && !updateEvents.isDeleted(it.bpmnElement) } + .filter { !updateEventsRegistry().isDeleted(it.id) && !updateEventsRegistry().isDeleted(it.bpmnElement) } .map { updateLocationAndInnerTopology(it) }, edges = fileState.edges.toList().union(newEdgeElems.map { it.edge }) - .filter { !updateEvents.isDeleted(it.id) && !(it.bpmnElement?.let { updateEvents.isDeleted(it) } ?: false)} + .filter { !updateEventsRegistry().isDeleted(it.id) && !(it.bpmnElement?.let { updateEventsRegistry().isDeleted(it) } ?: false)} .map { updateLocationAndInnerTopology(it) }, elementByDiagramId = fileState.elementByDiagramId.toMutableMap().plus(newElementByDiagramId), elementByStaticId = fileState.elementByStaticId.toMutableMap().plus(newElementByStaticId), @@ -97,28 +96,30 @@ class CurrentStateProvider { } private fun updateLocationAndInnerTopology(elem: ShapeElement): ShapeElement { - val updates = updateEvents.currentLocationUpdateEventList(elem.id) + val updates = updateEventsRegistry().currentLocationUpdateEventList(elem.id) var dx = 0.0f var dy = 0.0f updates.forEach { dx += it.event.dx; dy += it.event.dy } return elem.copyAndTranslate(dx, dy) } - private fun updateLocationAndInnerTopology(elem: EdgeElementState): EdgeElementState { - val hasNoCommittedAnchorUpdates = elem.waypoint.firstOrNull { updateEvents.currentLocationUpdateEventList(it.id).isNotEmpty() } - val hasNoNewAnchors = updateEvents.newWaypointStructure(elem.id).isEmpty() + private fun updateLocationAndInnerTopology(elem: EdgeWithIdentifiableWaypoints): EdgeWithIdentifiableWaypoints { + val hasNoCommittedAnchorUpdates = elem.waypoint.firstOrNull { updateEventsRegistry().currentLocationUpdateEventList(it.id).isNotEmpty() } + val hasNoNewAnchors = updateEventsRegistry().newWaypointStructure(elem.id).isEmpty() if (null == hasNoCommittedAnchorUpdates && hasNoNewAnchors) { return elem } - val waypoints: MutableList = - updateEvents.newWaypointStructure(elem.id).lastOrNull()?.event?.waypoints?.toMutableList() ?: elem.waypoint.filter { it.physical }.toMutableList() - val updatedLocations = waypoints.filter { it.physical }.map { updateWaypointLocation(it) } - return EdgeElementState(elem, updatedLocations) + val event = updateEventsRegistry().newWaypointStructure(elem.id).lastOrNull()?.event + val waypoints = event?.waypoints?.toMutableList() ?: elem.waypoint.filter { it.physical }.toMutableList() + val epoch = event?.epoch ?: 0 + val newState = EdgeElementState(elem, waypoints, epoch) + val updatedWaypoints = newState.waypoint.filter { it.physical }.map { updateWaypointLocation(it) } + return EdgeElementState(newState, updatedWaypoints, epoch) } - private fun updateWaypointLocation(waypoint: WaypointElementState): WaypointElementState { - val updates = updateEvents.currentLocationUpdateEventList(waypoint.id) + private fun updateWaypointLocation(waypoint: IdentifiableWaypoint): IdentifiableWaypoint { + val updates = updateEventsRegistry().currentLocationUpdateEventList(waypoint.id) var dx = 0.0f var dy = 0.0f updates.forEach { dx += it.event.dx; dy += it.event.dy } diff --git a/flowable-xml-parser/build.gradle b/flowable-xml-parser/build.gradle index 5d4228b6c..7dda70735 100644 --- a/flowable-xml-parser/build.gradle +++ b/flowable-xml-parser/build.gradle @@ -29,7 +29,7 @@ apply plugin: 'kotlin-kapt' dependencies { api project(":xml-parser-api") - implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.10.1" + implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.10.1' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.10.1' api 'com.github.pozo:mapstruct-kotlin:1.3.1.2' diff --git a/flowable-xml-parser/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/FlowableObjectFactory.kt b/flowable-xml-parser/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/FlowableObjectFactory.kt index acc2c6c81..c6751093f 100644 --- a/flowable-xml-parser/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/FlowableObjectFactory.kt +++ b/flowable-xml-parser/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/FlowableObjectFactory.kt @@ -97,12 +97,12 @@ class FlowableObjectFactory: BpmnObjectFactory { val propertyTree = mapper.valueToTree(dto) for (type in PropertyType.values()) { - if (type.id.contains(".")) { + if (type.path.contains(".")) { tryParseNestedValue(type, propertyTree, result) continue } - propertyTree[type.id]?.apply { + propertyTree[type.path]?.apply { parseValue(result, type) } } @@ -111,7 +111,7 @@ class FlowableObjectFactory: BpmnObjectFactory { } private fun tryParseNestedValue(type: PropertyType, propertyTree: JsonNode, result: MutableMap) { - val split = type.id.split(".", limit = 2) + val split = type.path.split(".", limit = 2) val targetId = split[0] propertyTree[targetId]?.apply { if (split[1].contains(".")) { @@ -131,7 +131,7 @@ class FlowableObjectFactory: BpmnObjectFactory { private fun bounds(forBpmnObject: WithBpmnId): BoundsElement { return when(forBpmnObject) { - is BpmnStartEvent, is BpmnEndEvent, is BpmnExclusiveGateway -> BoundsElement(0.0f, 0.0f, 30.0f, 30.0f) + is BpmnStartEvent, is BpmnEndEvent, is BpmnExclusiveGateway -> BoundsElement(0.0f, 0.0f, 40.0f, 40.0f) else -> BoundsElement(0.0f, 0.0f, 100.0f, 80.0f) } } diff --git a/flowable-xml-parser/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/FlowableParser.kt b/flowable-xml-parser/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/FlowableParser.kt index a82aac1ad..89919d828 100644 --- a/flowable-xml-parser/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/FlowableParser.kt +++ b/flowable-xml-parser/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/FlowableParser.kt @@ -10,14 +10,68 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.readValue import com.valb3r.bpmn.intellij.plugin.bpmn.api.BpmnParser import com.valb3r.bpmn.intellij.plugin.bpmn.api.BpmnProcessObject +import com.valb3r.bpmn.intellij.plugin.bpmn.api.bpmn.elements.* +import com.valb3r.bpmn.intellij.plugin.bpmn.api.events.* +import com.valb3r.bpmn.intellij.plugin.bpmn.api.info.PropertyType +import com.valb3r.bpmn.intellij.plugin.bpmn.api.info.PropertyValueType +import com.valb3r.bpmn.intellij.plugin.bpmn.api.info.PropertyValueType.* import com.valb3r.bpmn.intellij.plugin.flowable.parser.nodes.BpmnFile -import java.io.InputStream +import org.w3c.dom.Document +import org.w3c.dom.Element +import org.w3c.dom.Node +import java.io.* +import java.nio.charset.StandardCharsets.UTF_8 +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult +import javax.xml.xpath.XPathConstants +import javax.xml.xpath.XPathFactory + const val CDATA_FIELD = "CDATA" +enum class PropertyTypeDetails( + val propertyType: PropertyType, + val xmlPath: String, + val type: XmlType +) { + ID(PropertyType.ID, "id", XmlType.ATTRIBUTE), + NAME(PropertyType.NAME,"name", XmlType.ATTRIBUTE), + DOCUMENTATION(PropertyType.DOCUMENTATION, "documentation.text", XmlType.CDATA), + ASYNC(PropertyType.ASYNC, "flowable:async", XmlType.ATTRIBUTE), + CALLED_ELEM(PropertyType.CALLED_ELEM, "calledElement", XmlType.ATTRIBUTE), + CALLED_ELEM_TYPE(PropertyType.CALLED_ELEM_TYPE, "flowable:calledElementType", XmlType.ATTRIBUTE), + INHERIT_VARS(PropertyType.INHERIT_VARS, "flowable:inheritVariables", XmlType.ATTRIBUTE), + FALLBACK_TO_DEF_TENANT(PropertyType.FALLBACK_TO_DEF_TENANT, "flowable:fallbackToDefaultTenant", XmlType.ATTRIBUTE), + EXCLUSIVE(PropertyType.EXCLUSIVE,"flowable:exclusive", XmlType.ATTRIBUTE), + DELEGATE_EXPRESSION(PropertyType.DELEGATE_EXPRESSION, "flowable:delegateExpression", XmlType.ATTRIBUTE), + IS_TRIGGERABLE(PropertyType.IS_TRIGGERABLE, "flowable:triggerable", XmlType.ATTRIBUTE), + SOURCE_REF(PropertyType.SOURCE_REF,"sourceRef", XmlType.ATTRIBUTE), + TARGET_REF(PropertyType.TARGET_REF, "targetRef", XmlType.ATTRIBUTE), + CONDITION_EXPR_VALUE(PropertyType.CONDITION_EXPR_VALUE, "conditionExpression.text", XmlType.CDATA), + CONDITION_EXPR_TYPE(PropertyType.CONDITION_EXPR_TYPE, "conditionExpression.xsi:type", XmlType.ATTRIBUTE), + DEFAULT_FLOW(PropertyType.DEFAULT_FLOW, "default", XmlType.ATTRIBUTE) +} + +enum class XmlType { + + CDATA, + ATTRIBUTE +} + class FlowableParser : BpmnParser { + val OMGDI_NS = "http://www.omg.org/spec/DD/20100524/DI" + val BPMDI_NS = "http://www.omg.org/spec/BPMN/20100524/DI" + val OMGDC_NS = "http://www.omg.org/spec/DD/20100524/DC" + private val mapper: XmlMapper = mapper() + private val dbFactory = DocumentBuilderFactory.newInstance() + private val transformer = TransformerFactory.newInstance() + private val xpathFactory = XPathFactory.newInstance() + override fun parse(input: String): BpmnProcessObject { val dto = mapper.readValue(input) @@ -29,6 +83,318 @@ class FlowableParser : BpmnParser { return toProcessObject(dto) } + /** + * Impossible to use FasterXML - Multiple objects of same type issue: + * https://github.com/FasterXML/jackson-dataformat-xml/issues/205 + */ + override fun update(input: InputStream, output: OutputStream, events: List){ + val dBuilder = dbFactory.newDocumentBuilder() + val doc = dBuilder.parse(input) + + parseAndWrite(doc, OutputStreamWriter(output), events) + } + + /** + * Impossible to use FasterXML - Multiple objects of same type issue: + * https://github.com/FasterXML/jackson-dataformat-xml/issues/205 + */ + override fun update(input: String, events: List): String { + val dBuilder = dbFactory.newDocumentBuilder() + val doc = dBuilder.parse(ByteArrayInputStream(input.toByteArray(UTF_8))) + + return parseAndWrite(doc, StringWriter(), events).buffer.toString() + } + + private fun parseAndWrite(doc: Document, writer: T, events: List): T { + doc.documentElement.normalize() + + doUpdate(doc, events) + + val transformer = transformer.newTransformer() + transformer.setOutputProperty(OutputKeys.INDENT, "yes") + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4") + transformer.transform(DOMSource(doc), StreamResult(writer)) + return writer + } + + private fun doUpdate(doc: Document, events: List) { + for (event in events) { + when (event) { + is LocationUpdateWithId -> applyLocationUpdate(doc, event) + is NewWaypoints -> applyNewWaypoints(doc, event) + is DiagramElementRemoved -> applyDiagramElementRemoved(doc, event) + is BpmnElementRemoved -> applyBpmnElementRemoved(doc, event) + is BpmnShapeObjectAdded -> applyBpmnShapeObjectAdded(doc, event) + is BpmnEdgeObjectAdded -> applyBpmnEdgeObjectAdded(doc, event) + is PropertyUpdateWithId -> applyPropertyUpdateWithId(doc, event) + } + } + } + + fun trimWhitespace(node: Node, recurse: Boolean = true) { + val children = node.childNodes + val toRemove = mutableListOf() + for (i in 0 until children.length) { + val child = children.item(i) + if (child.nodeType == Node.TEXT_NODE && child.textContent.trim().isBlank()) { + toRemove.add(child) + } else if (recurse) { + trimWhitespace(child) + } + } + toRemove.forEach { it.parentNode.removeChild(it) } + } + + private fun applyLocationUpdate(doc: Document, update: LocationUpdateWithId) { + val xpath = xpathFactory.newXPath() + val node = if (null != update.internalPos) { + // Internal waypoint update + xpath.evaluate( + "//BPMNEdge[@id='${update.parentElementId!!.id}']/*[@x][@y][${update.internalPos!! + 1}]", + doc, + XPathConstants.NODE + ) as Node + } else { + xpath.evaluate( + "//BPMNShape[@id='${update.diagramElementId.id}']/*[@x][@y]", + doc, + XPathConstants.NODE + ) as Node + } + + val nx = node.attributes.getNamedItem("x") + val ny = node.attributes.getNamedItem("y") + + nx.nodeValue = (nx.nodeValue.toFloat() + update.dx).toString() + ny.nodeValue = (ny.nodeValue.toFloat() + update.dy).toString() + } + + private fun applyNewWaypoints(doc: Document, update: NewWaypoints) { + val xpath = xpathFactory.newXPath() + val node = xpath.evaluate( + "//BPMNEdge[@id='${update.edgeElementId.id}'][1]", + doc, + XPathConstants.NODE + ) as Node + + val toRemove = mutableListOf() + for (pos in 0 until node.childNodes.length) { + val target = node.childNodes.item(pos) + if (target.nodeName.contains("waypoint")) { + toRemove.add(target) + continue + } + } + + toRemove.forEach { node.removeChild(it) } + trimWhitespace(node) + + update.waypoints.filter { it.physical }.sortedBy { it.internalPhysicalPos }.forEach { + newWaypoint(doc, it, node) + } + } + + private fun newWaypoint(doc: Document, it: IdentifiableWaypoint, parentEdgeElem: Node) { + val elem = doc.createElementNS(OMGDI_NS, "omgdi:waypoint") + elem.setAttribute("x", it.x.toString()) + elem.setAttribute("y", it.y.toString()) + parentEdgeElem.appendChild(elem) + } + + private fun applyDiagramElementRemoved(doc: Document, update: DiagramElementRemoved) { + val xpath = xpathFactory.newXPath() + val node = xpath.evaluate( + "//BPMNDiagram/BPMNPlane/*[@id='${update.elementId.id}'][1]", + doc, + XPathConstants.NODE + ) as Node + + val parent = node.parentNode + node.parentNode.removeChild(node) + trimWhitespace(parent, false) + } + + private fun applyBpmnElementRemoved(doc: Document, update: BpmnElementRemoved) { + val xpath = xpathFactory.newXPath() + val node = xpath.evaluate( + "//process/*[@id='${update.elementId.id}'][1]", + doc, + XPathConstants.NODE + ) as Node + + val parent = node.parentNode + node.parentNode.removeChild(node) + trimWhitespace(parent, false) + } + + private fun applyBpmnShapeObjectAdded(doc: Document, update: BpmnShapeObjectAdded) { + val xpath = xpathFactory.newXPath() + val diagramParent = xpath.evaluate( + "//process[1]", + doc, + XPathConstants.NODE + ) as Node + + val newNode = when(update.bpmnObject) { + is BpmnStartEvent -> doc.createElement("startEvent") + is BpmnCallActivity -> doc.createElement("callActivity") + is BpmnExclusiveGateway -> doc.createElement("exclusiveGateway") + is BpmnSequenceFlow -> doc.createElement("sequenceFlow") + is BpmnServiceTask -> doc.createElement("serviceTask") + is BpmnEndEvent -> doc.createElement("endEvent") + else -> throw IllegalArgumentException("Can't store: " + update.bpmnObject) + } + + update.props.forEach { setToNode(doc, newNode, it.key, it.value.value) } + trimWhitespace(diagramParent, false) + diagramParent.appendChild(newNode) + + val shapeParent = xpath.evaluate( + "//BPMNDiagram/BPMNPlane[1]", + doc, + XPathConstants.NODE + ) as Node + val newShape = doc.createElementNS(BPMDI_NS, "bpmndi:BPMNShape") + newShape.setAttribute("id", update.shape.id.id) + newShape.setAttribute("bpmnElement", update.bpmnObject.id.id) + shapeParent.appendChild(newShape) + val newBounds = doc.createElementNS(OMGDC_NS, "omgdc:Bounds") + newBounds.setAttribute("x", update.shape.bounds.x.toString()) + newBounds.setAttribute("y", update.shape.bounds.y.toString()) + newBounds.setAttribute("width", update.shape.bounds.width.toString()) + newBounds.setAttribute("height", update.shape.bounds.height.toString()) + newShape.appendChild(newBounds) + trimWhitespace(shapeParent, false) + } + + private fun applyBpmnEdgeObjectAdded(doc: Document, update: BpmnEdgeObjectAdded) { + val xpath = xpathFactory.newXPath() + val diagramParent = xpath.evaluate( + "//process[1]", + doc, + XPathConstants.NODE + ) as Node + + val newNode = when(update.bpmnObject) { + is BpmnSequenceFlow -> doc.createElement("sequenceFlow") + else -> throw IllegalArgumentException("Can't store: " + update.bpmnObject) + } + + update.props.forEach { setToNode(doc, newNode, it.key, it.value.value) } + trimWhitespace(diagramParent, false) + diagramParent.appendChild(newNode) + + val shapeParent = xpath.evaluate( + "//BPMNDiagram/BPMNPlane[1]", + doc, + XPathConstants.NODE + ) as Node + val newShape = doc.createElementNS(BPMDI_NS, "bpmndi:BPMNEdge") + newShape.setAttribute("id", update.edge.id.id) + newShape.setAttribute("bpmnElement", update.bpmnObject.id.id) + shapeParent.appendChild(newShape) + update.edge.waypoint.filter { it.physical }.forEach { newWaypoint(doc, it, newShape) } + trimWhitespace(shapeParent, false) + } + + private fun applyPropertyUpdateWithId(doc: Document, update: PropertyUpdateWithId) { + val xpath = xpathFactory.newXPath() + val node = xpath.evaluate( + "//process/*[@id='${update.bpmnElementId.id}'][1]", + doc, + XPathConstants.NODE + ) as Element + + setToNode(doc, node, update.property, update.newValue) + } + + + private fun setToNode(doc: Document, node: Element, type: PropertyType, value: Any?) { + val details = PropertyTypeDetails.values().filter { it.propertyType == type }.firstOrNull()!! + when { + details.xmlPath.contains(".") -> setNestedToNode(doc, node, type, details, value) + else -> setAttributeOrValueOrCdataOrRemoveIfNull(node, details.xmlPath, details, asString(type.valueType, value)) + } + } + + private fun setNestedToNode(doc: Document, node: Element, type: PropertyType, details: PropertyTypeDetails, value: Any?) { + val segments = details.xmlPath.split(".") + val childOf: ((Element, String) -> Element?) = {target, name -> nodeChildByName(target, name)} + + var currentNode = node + for (segment in 0 until segments.size - 1) { + val name = segments[segment] + if ("" == name) { + continue + } + + val child = childOf(currentNode, name) + if (null == child) { + // do not create elements for null values + if (null == value ) { + return + } + + val newElem = doc.createElement(name) + currentNode.appendChild(newElem) + currentNode = newElem + } else { + currentNode = child + } + } + + setAttributeOrValueOrCdataOrRemoveIfNull(currentNode, segments[segments.size - 1], details, asString(type.valueType, value)) + } + + private fun nodeChildByName(target: Element, name: String): Element? { + for (pos in 0 until target.childNodes.length) { + if (target.childNodes.item(pos).nodeName.contains(name)) { + return target.childNodes.item(pos) as Element + } + } + return null + } + + private fun setAttributeOrValueOrCdataOrRemoveIfNull(node: Element, name: String, details: PropertyTypeDetails, value: String?) { + when (details.type) { + XmlType.ATTRIBUTE -> setAttribute(node, name, value) + XmlType.CDATA -> setCdata(node, name, value) + } + } + + private fun setAttribute(node: Element, name: String, value: String?) { + if (value.isNullOrEmpty()) { + if (node.hasAttribute(name)) { + node.removeAttribute(name) + } + return + } + + node.setAttribute(name, value) + } + + private fun setCdata(node: Element, name: String, value: String?) { + if (value.isNullOrEmpty()) { + if (node.textContent.isNotBlank()) { + node.textContent = null + } + return + } + + node.textContent = value + } + + private fun asString(type: PropertyValueType, value: Any?): String? { + if (null == value || "" == value) { + return null + } + + return when(type) { + STRING, CLASS, EXPRESSION -> value as String + BOOLEAN -> (value as Boolean).toString() + } + } + private fun toProcessObject(dto: BpmnFile): BpmnProcessObject { // TODO - Multi process support val process = dto.processes[0].toElement() diff --git a/flowable-xml-parser/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/nodes/diagram/BPMNPlane.kt b/flowable-xml-parser/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/nodes/diagram/BPMNPlane.kt index 576d781a8..406b32a3a 100644 --- a/flowable-xml-parser/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/nodes/diagram/BPMNPlane.kt +++ b/flowable-xml-parser/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/nodes/diagram/BPMNPlane.kt @@ -1,15 +1,21 @@ package com.valb3r.bpmn.intellij.plugin.flowable.parser.nodes.diagram +import com.fasterxml.jackson.annotation.JsonMerge import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.elements.PlaneElement import com.valb3r.bpmn.intellij.plugin.flowable.parser.nodes.process.BpmnElementIdMapper import org.mapstruct.Mapper +// For mixed lists in XML we need to have JsonSetter/JsonMerge on field +// https://github.com/FasterXML/jackson-dataformat-xml/issues/363 +// unfortunately this has failed with Kotlin 'data' classes data class BPMNPlane( @JacksonXmlProperty(isAttribute = true) val id: String, @JacksonXmlProperty(isAttribute = true) val bpmnElement: String, + @JsonMerge @JacksonXmlProperty(localName = "BPMNShape") @JacksonXmlElementWrapper(useWrapping = false) val bpmnShape: List?, + @JsonMerge @JacksonXmlProperty(localName = "BPMNEdge") @JacksonXmlElementWrapper(useWrapping = false) val bpmnEdge: List? ) { @Mapper(uses = [DiagramElementIdMapper::class, BpmnElementIdMapper::class]) diff --git a/flowable-xml-parser/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/nodes/process/SequenceFlow.kt b/flowable-xml-parser/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/nodes/process/SequenceFlow.kt index 1ffd2e0c9..e2f94e8a3 100644 --- a/flowable-xml-parser/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/nodes/process/SequenceFlow.kt +++ b/flowable-xml-parser/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/nodes/process/SequenceFlow.kt @@ -14,8 +14,8 @@ data class SequenceFlow( @JacksonXmlProperty(isAttribute = true) val id: String, @JacksonXmlProperty(isAttribute = true) val name: String?, val documentation: String?, - @JacksonXmlProperty(isAttribute = true) val sourceRef: String, - @JacksonXmlProperty(isAttribute = true) val targetRef: String, + @JacksonXmlProperty(isAttribute = true) val sourceRef: String?, + @JacksonXmlProperty(isAttribute = true) val targetRef: String?, val conditionExpression: ConditionExpression? ): BpmnMappable { @@ -30,6 +30,6 @@ data class SequenceFlow( } data class ConditionExpression( - val type: String, - @JsonProperty(CDATA_FIELD) @JacksonXmlText @JacksonXmlCData val text: String + val type: String?, + @JsonProperty(CDATA_FIELD) @JacksonXmlText @JacksonXmlCData val text: String? ) \ No newline at end of file diff --git a/flowable-xml-parser/src/test/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/FlowableParserTest.kt b/flowable-xml-parser/src/test/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/FlowableParserTest.kt index 195deded3..3e875bd99 100644 --- a/flowable-xml-parser/src/test/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/FlowableParserTest.kt +++ b/flowable-xml-parser/src/test/kotlin/com/valb3r/bpmn/intellij/plugin/flowable/parser/FlowableParserTest.kt @@ -1,13 +1,28 @@ package com.valb3r.bpmn.intellij.plugin.flowable.parser import com.valb3r.bpmn.intellij.plugin.bpmn.api.BpmnProcessObject +import com.valb3r.bpmn.intellij.plugin.bpmn.api.bpmn.BpmnElementId +import com.valb3r.bpmn.intellij.plugin.bpmn.api.bpmn.elements.BpmnExclusiveGateway +import com.valb3r.bpmn.intellij.plugin.bpmn.api.bpmn.elements.BpmnSequenceFlow +import com.valb3r.bpmn.intellij.plugin.bpmn.api.bpmn.elements.WithBpmnId +import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.DiagramElementId +import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.elements.BoundsElement +import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.elements.ShapeElement +import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.elements.WaypointElement +import com.valb3r.bpmn.intellij.plugin.bpmn.api.events.* +import com.valb3r.bpmn.intellij.plugin.bpmn.api.info.Property +import com.valb3r.bpmn.intellij.plugin.bpmn.api.info.PropertyType import org.amshove.kluent.shouldNotBeNull import org.junit.jupiter.api.Test import java.nio.charset.StandardCharsets.UTF_8 +import java.util.* internal class FlowableParserTest { + private val bmpnElemId = BpmnElementId(UUID.randomUUID().toString()) + private val diagramElementId = DiagramElementId(UUID.randomUUID().toString()) + @Test fun `XML process with all Flowable elements is parseable without error`() { val processObject: BpmnProcessObject? @@ -17,5 +32,174 @@ internal class FlowableParserTest { processObject.shouldNotBeNull() } + @Test + fun `XML process with interlaced elements of same type should be parseable without error`() { + val processObject: BpmnProcessObject? + + processObject = FlowableParser().parse("duplicates.bpmn20.xml".asResource()!!) + + processObject.shouldNotBeNull() + } + + @Test + fun `XML process with interlaced elements of same type should be updatable with location without error`() { + val updated = FlowableParser().update("duplicates.bpmn20.xml".asResource()!!, listOf( + object: LocationUpdateWithId { + override val diagramElementId: DiagramElementId + get() = DiagramElementId("BPMNEdge_sid-F270B5BF-127E-422B-BF8D-6A6B7E411D31") + override val dx: Float + get() = 10.0f + override val dy: Float + get() = -10.0f + override val internalPos: Int? + get() = 0 + } + )) + + updated.shouldNotBeNull() + } + + @Test + fun `XML process with interlaced elements of same type should be updatable with new waypoints without error`() { + val updated = FlowableParser().update("duplicates.bpmn20.xml".asResource()!!, listOf( + object: NewWaypoints { + override val edgeElementId: DiagramElementId + get() = DiagramElementId("BPMNEdge_sid-C3BC8962-12B0-482B-B9B5-DCB6551306BD") + override val waypoints: List + get() = listOf( + WaypointElementState(diagramElementId, 10.0f, 20.0f, 0.0f, 0.0f, true, 1), + WaypointElementState(diagramElementId, 30.0f, 40.0f, 0.0f, 0.0f, true, 0) + ) + } + )) + + updated.shouldNotBeNull() + } + + @Test + fun `XML process with interlaced elements of same type should be updatable with diagram element removal without error`() { + val updated = FlowableParser().update("duplicates.bpmn20.xml".asResource()!!, listOf( + object: DiagramElementRemoved { + override val elementId: DiagramElementId + get() = DiagramElementId("BPMNEdge_sid-C3BC8962-12B0-482B-B9B5-DCB6551306BD") + } + )) + + updated.shouldNotBeNull() + } + + @Test + fun `XML process with interlaced elements of same type should be updatable with BPMN element removal without error`() { + val updated = FlowableParser().update("duplicates.bpmn20.xml".asResource()!!, listOf( + object: BpmnElementRemoved { + override val elementId: BpmnElementId + get() = BpmnElementId("serviceTaskStart") + } + )) + + updated.shouldNotBeNull() + } + + @Test + fun `XML process with interlaced elements of same type should be updatable with BPMN element addition without error`() { + val updated = FlowableParser().update("duplicates.bpmn20.xml".asResource()!!, listOf( + object: BpmnShapeObjectAdded { + override val bpmnObject: WithBpmnId + get() = BpmnExclusiveGateway(bmpnElemId, "Exclusive gateway", null, null) + override val shape: ShapeElement + get() = ShapeElement(diagramElementId, bpmnObject.id, BoundsElement(0.0f, 0.0f, 30.0f, 40.0f)) + override val props: Map + get() = mapOf( + PropertyType.ID to Property(bpmnObject.id.id), + PropertyType.CONDITION_EXPR_VALUE to Property("condition 1"), + PropertyType.CONDITION_EXPR_TYPE to Property("a type") + ) + } + )) + + updated.shouldNotBeNull() + } + + @Test + fun `XML process with interlaced elements of same type should be updatable with BPMN edge element addition without error`() { + val updated = FlowableParser().update("duplicates.bpmn20.xml".asResource()!!, listOf( + object: BpmnEdgeObjectAdded { + override val bpmnObject: WithBpmnId + get() = BpmnSequenceFlow(bmpnElemId, "Exclusive gateway", null, "source", "target", null) + override val edge: EdgeWithIdentifiableWaypoints + get() = EdgeElementState( + diagramElementId, + bpmnObject.id, + mutableListOf( + WaypointElementState(diagramElementId, 10.0f, 20.0f, 0.0f, 0.0f, true, 1), + WaypointElementState(diagramElementId, 30.0f, 40.0f, 0.0f, 0.0f, true, 0) + ) + ) + override val props: Map + get() = mapOf( + PropertyType.ID to Property(bpmnObject.id.id), + PropertyType.CONDITION_EXPR_VALUE to Property("condition 1"), + PropertyType.CONDITION_EXPR_TYPE to Property("a type") + ) + } + )) + + updated.shouldNotBeNull() + } + + + @Test + fun `XML process with interlaced elements of same type should be updatable with property update event without error`() { + val updated = FlowableParser().update("duplicates.bpmn20.xml".asResource()!!, listOf( + object: PropertyUpdateWithId { + override val bpmnElementId: BpmnElementId + get() = BpmnElementId("activityStart") + override val property: PropertyType + get() = PropertyType.NAME + override val newValue: Any + get() = "A new name" + } + )) + + updated.shouldNotBeNull() + } + fun String.asResource(): String? = object {}::class.java.classLoader.getResource(this)?.readText(UTF_8) -} \ No newline at end of file +} + +data class WaypointElementState ( + override val id: DiagramElementId, + override val x: Float, + override val y: Float, + override val origX: Float, + override val origY: Float, + override val physical: Boolean, + override val internalPhysicalPos: Int +): IdentifiableWaypoint { + + override fun moveTo(dx: Float, dy: Float): IdentifiableWaypoint { + TODO("Not yet implemented") + } + + override fun asPhysical(): IdentifiableWaypoint { + TODO("Not yet implemented") + } + + override fun originalLocation(): IdentifiableWaypoint { + TODO("Not yet implemented") + } + + override fun asWaypointElement(): WaypointElement { + TODO("Not yet implemented") + } + + override fun copyAndTranslate(dx: Float, dy: Float): IdentifiableWaypoint { + TODO("Not yet implemented") + } +} + +data class EdgeElementState ( + override val id: DiagramElementId, + override val bpmnElement: BpmnElementId?, + override val waypoint: MutableList = mutableListOf() +): EdgeWithIdentifiableWaypoints \ No newline at end of file diff --git a/flowable-xml-parser/src/test/resources/duplicates.bpmn20.xml b/flowable-xml-parser/src/test/resources/duplicates.bpmn20.xml new file mode 100644 index 000000000..883011e0c --- /dev/null +++ b/flowable-xml-parser/src/test/resources/duplicates.bpmn20.xml @@ -0,0 +1,59 @@ + + + + Testing duplicates + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/xml-parser-api/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/bpmn/api/BpmnParser.kt b/xml-parser-api/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/bpmn/api/BpmnParser.kt index 7c9edee2a..67c7b24fb 100644 --- a/xml-parser-api/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/bpmn/api/BpmnParser.kt +++ b/xml-parser-api/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/bpmn/api/BpmnParser.kt @@ -1,9 +1,14 @@ package com.valb3r.bpmn.intellij.plugin.bpmn.api +import com.valb3r.bpmn.intellij.plugin.bpmn.api.events.Event import java.io.InputStream +import java.io.OutputStream interface BpmnParser { fun parse(input: InputStream): BpmnProcessObject fun parse(input: String): BpmnProcessObject + + fun update(input: String, events: List): String + fun update(input: InputStream, output: OutputStream, events: List) } \ No newline at end of file diff --git a/xml-parser-api/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/bpmn/api/bpmn/elements/BpmnSequenceFlow.kt b/xml-parser-api/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/bpmn/api/bpmn/elements/BpmnSequenceFlow.kt index ad1960303..e2bf23033 100644 --- a/xml-parser-api/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/bpmn/api/bpmn/elements/BpmnSequenceFlow.kt +++ b/xml-parser-api/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/bpmn/api/bpmn/elements/BpmnSequenceFlow.kt @@ -8,13 +8,13 @@ data class BpmnSequenceFlow( override val id: BpmnElementId, val name: String?, val documentation: String?, - val sourceRef: String, - val targetRef: String, + val sourceRef: String?, // can't be null in reality, but malformed XMLs should be editable too + val targetRef: String?, // can't be null in reality, but malformed XMLs should be editable too val conditionExpression: ConditionExpression? ): WithBpmnId @KotlinBuilder data class ConditionExpression( - val type: String, - val text: String + val type: String?, // can't be null in reality, but malformed XMLs should be editable too + val text: String? // can't be null in reality, but malformed XMLs should be editable too ) \ No newline at end of file diff --git a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/events/EventInterfaces.kt b/xml-parser-api/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/bpmn/api/events/EventInterfaces.kt similarity index 53% rename from bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/events/EventInterfaces.kt rename to xml-parser-api/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/bpmn/api/events/EventInterfaces.kt index c222ca8f0..da483c900 100644 --- a/bpmn-intellij-plugin/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/events/EventInterfaces.kt +++ b/xml-parser-api/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/bpmn/api/events/EventInterfaces.kt @@ -1,14 +1,14 @@ -package com.valb3r.bpmn.intellij.plugin.events +package com.valb3r.bpmn.intellij.plugin.bpmn.api.events import com.valb3r.bpmn.intellij.plugin.bpmn.api.bpmn.BpmnElementId import com.valb3r.bpmn.intellij.plugin.bpmn.api.bpmn.elements.WithBpmnId import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.DiagramElementId import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.elements.ShapeElement +import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.elements.Translatable +import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.elements.WaypointElement +import com.valb3r.bpmn.intellij.plugin.bpmn.api.diagram.elements.WithDiagramId import com.valb3r.bpmn.intellij.plugin.bpmn.api.info.Property import com.valb3r.bpmn.intellij.plugin.bpmn.api.info.PropertyType -import com.valb3r.bpmn.intellij.plugin.render.EdgeElementState -import com.valb3r.bpmn.intellij.plugin.render.WaypointElementState - interface Event interface EventOrder { @@ -20,11 +20,14 @@ interface LocationUpdateWithId: Event { val diagramElementId: DiagramElementId val dx: Float val dy: Float + val parentElementId: DiagramElementId? + val internalPos: Int? } interface NewWaypoints: Event { val edgeElementId: DiagramElementId - val waypoints: List + val waypoints: List + val epoch: Int } interface DiagramElementRemoved: Event { @@ -43,11 +46,32 @@ interface BpmnShapeObjectAdded: Event { interface BpmnEdgeObjectAdded: Event { val bpmnObject: WithBpmnId - val edge: EdgeElementState + val edge: EdgeWithIdentifiableWaypoints val props: Map } interface PropertyUpdateWithId: Event { val bpmnElementId: BpmnElementId val property: PropertyType + val newValue: Any +} + +interface IdentifiableWaypoint: Translatable, WithDiagramId { + val x: Float + val y: Float + val origX: Float + val origY: Float + val physical: Boolean + val internalPhysicalPos: Int + + fun moveTo(dx: Float, dy: Float): IdentifiableWaypoint + fun asPhysical(): IdentifiableWaypoint + fun originalLocation(): IdentifiableWaypoint + fun asWaypointElement(): WaypointElement +} + +interface EdgeWithIdentifiableWaypoints: WithDiagramId { + val bpmnElement: BpmnElementId? + val waypoint: MutableList + val epoch: Int } \ No newline at end of file diff --git a/xml-parser-api/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/bpmn/api/info/PropertyType.kt b/xml-parser-api/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/bpmn/api/info/PropertyType.kt index b4067f6df..8a81d06cc 100644 --- a/xml-parser-api/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/bpmn/api/info/PropertyType.kt +++ b/xml-parser-api/src/main/kotlin/com/valb3r/bpmn/intellij/plugin/bpmn/api/info/PropertyType.kt @@ -2,9 +2,13 @@ package com.valb3r.bpmn.intellij.plugin.bpmn.api.info import com.valb3r.bpmn.intellij.plugin.bpmn.api.info.PropertyValueType.* -enum class PropertyType(val id: String, val caption: String, val valueType: PropertyValueType) { - - ID("id.id", "ID", STRING), +enum class PropertyType( + val id: String, + val caption: String, + val valueType: PropertyValueType, + val path: String = id +) { + ID("id", "ID", STRING, "id.id"), NAME("name", "Name", STRING), DOCUMENTATION("documentation", "Documentation", STRING), ASYNC("async", "Asynchronous", BOOLEAN), @@ -17,7 +21,7 @@ enum class PropertyType(val id: String, val caption: String, val valueType: Prop IS_TRIGGERABLE("triggerable", "Is activity triggerable?", BOOLEAN), SOURCE_REF("sourceRef","Source reference", STRING), TARGET_REF("targetRef", "Target reference", STRING), - CONDITION_EXPR_VALUE("conditionExpression.text", "Condition expression", EXPRESSION), - CONDITION_EXPR_TYPE("conditionExpression.type", "Condition expression type", STRING), + CONDITION_EXPR_VALUE("conditionExpression.text", "Condition expression", EXPRESSION, "conditionExpression.text"), + CONDITION_EXPR_TYPE("conditionExpression.type", "Condition expression type", STRING, "conditionExpression.type"), DEFAULT_FLOW("defaultElement", "Default flow element", STRING) } \ No newline at end of file