summaryrefslogtreecommitdiffstats
path: root/library/Businessprocess/Modification
diff options
context:
space:
mode:
Diffstat (limited to 'library/Businessprocess/Modification')
-rw-r--r--library/Businessprocess/Modification/NodeAction.php179
-rw-r--r--library/Businessprocess/Modification/NodeAddChildrenAction.php74
-rw-r--r--library/Businessprocess/Modification/NodeApplyManualOrderAction.php35
-rw-r--r--library/Businessprocess/Modification/NodeCopyAction.php48
-rw-r--r--library/Businessprocess/Modification/NodeCreateAction.php129
-rw-r--r--library/Businessprocess/Modification/NodeModifyAction.php121
-rw-r--r--library/Businessprocess/Modification/NodeMoveAction.php227
-rw-r--r--library/Businessprocess/Modification/NodeRemoveAction.php125
-rw-r--r--library/Businessprocess/Modification/ProcessChanges.php294
9 files changed, 1232 insertions, 0 deletions
diff --git a/library/Businessprocess/Modification/NodeAction.php b/library/Businessprocess/Modification/NodeAction.php
new file mode 100644
index 0000000..b5baa5d
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeAction.php
@@ -0,0 +1,179 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Exception\ModificationError;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * Abstract NodeAction class
+ *
+ * Every instance of a NodeAction represents a single applied change. Changes are pushed to
+ * a stack and consumed from there. When persisted, NodeActions are serialized with their name,
+ * node name and optionally additional properties according preserveProperties. For each property
+ * that should be preserved, getter and setter methods have to be defined.
+ *
+ * @package Icinga\Module\Businessprocess
+ */
+abstract class NodeAction
+{
+ /** @var string Name of this action (currently create, modify, remove) */
+ protected $actionName;
+
+ /** @var string Name of the node this action applies to */
+ protected $nodeName;
+
+ /** @var array Properties which should be preserved when serializing this action */
+ protected $preserveProperties = array();
+
+ /**
+ * NodeAction constructor.
+ *
+ * @param Node|string $node
+ */
+ public function __construct($node = null)
+ {
+ if ($node !== null) {
+ $this->nodeName = (string) $node;
+ }
+ }
+
+ /**
+ * Every NodeAction must be able to apply itself to a BusinessProcess
+ *
+ * @param BpConfig $config
+ * @return mixed
+ */
+ abstract public function applyTo(BpConfig $config);
+
+ /**
+ * Every NodeAction must be able to tell whether it can be applied to a BusinessProcess
+ *
+ * @param BpConfig $config
+ *
+ * @throws ModificationError
+ *
+ * @return bool
+ */
+ abstract public function appliesTo(BpConfig $config);
+
+ /**
+ * The name of the node this modification applies to
+ *
+ * @return string
+ */
+ public function getNodeName()
+ {
+ return $this->nodeName;
+ }
+
+ public function hasNode()
+ {
+ return $this->nodeName !== null;
+ }
+
+ /**
+ * Whether this is an instance of a given action name
+ *
+ * @param string $actionName
+ * @return bool
+ */
+ public function is($actionName)
+ {
+ return $this->getActionName() === $actionName;
+ }
+
+ /**
+ * Throw a ModificationError
+ *
+ * @param string $msg
+ * @param mixed ...
+ *
+ * @throws ModificationError
+ */
+ protected function error($msg)
+ {
+ $error = ModificationError::create(func_get_args());
+ /** @var ModificationError $error */
+ throw $error;
+ }
+
+ /**
+ * Create an instance of a given actionName for a specific Node
+ *
+ * @param string $actionName
+ * @param string $nodeName
+ *
+ * @return static
+ */
+ public static function create($actionName, $nodeName)
+ {
+ $className = __NAMESPACE__ . '\\Node' . ucfirst($actionName) . 'Action';
+
+ return new $className($nodeName);
+ }
+
+ /**
+ * Returns a JSON-encoded serialized NodeAction
+ *
+ * @return string
+ */
+ public function serialize()
+ {
+ $object = (object) array(
+ 'actionName' => $this->getActionName(),
+ 'nodeName' => $this->getNodeName(),
+ 'properties' => array()
+ );
+
+ foreach ($this->preserveProperties as $key) {
+ $func = 'get' . ucfirst($key);
+ $object->properties[$key] = $this->$func();
+ }
+
+ return json_encode($object);
+ }
+
+ /**
+ * Decodes a JSON-serialized NodeAction and returns an object instance
+ *
+ * @param $string
+ * @return NodeAction
+ */
+ public static function unSerialize($string)
+ {
+ $object = json_decode($string, true);
+ $action = self::create($object['actionName'], $object['nodeName']);
+
+ foreach ($object['properties'] as $key => $val) {
+ $func = 'set' . ucfirst($key);
+ $action->$func($val);
+ }
+
+ return $action;
+ }
+
+ /**
+ * Returns the defined action name or determines such from the class name
+ *
+ * @return string The action name
+ *
+ * @throws ProgrammingError when no such class exists
+ */
+ public function getActionName()
+ {
+ if ($this->actionName === null) {
+ if (! preg_match('/\\\Node(\w+)Action$/', get_class($this), $m)) {
+ throw new ProgrammingError(
+ '"%s" is not a NodeAction class',
+ get_class($this)
+ );
+ }
+ $this->actionName = lcfirst($m[1]);
+ }
+
+ return $this->actionName;
+ }
+}
diff --git a/library/Businessprocess/Modification/NodeAddChildrenAction.php b/library/Businessprocess/Modification/NodeAddChildrenAction.php
new file mode 100644
index 0000000..162c380
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeAddChildrenAction.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+
+class NodeAddChildrenAction extends NodeAction
+{
+ protected $children = array();
+
+ protected $preserveProperties = array('children');
+
+ /**
+ * @inheritdoc
+ */
+ public function appliesTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+
+ if (! $config->hasBpNode($name)) {
+ $this->error('Process "%s" not found', $name);
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function applyTo(BpConfig $config)
+ {
+ $node = $config->getBpNode($this->getNodeName());
+
+ foreach ($this->children as $name) {
+ if (! $config->hasNode($name) || $config->getNode($name)->getBpConfig()->getName() !== $config->getName()) {
+ [$prefix, $suffix] = BpConfig::splitNodeName($name);
+ if ($suffix !== null) {
+ if ($suffix === 'Hoststatus') {
+ $config->createHost($prefix);
+ } else {
+ $config->createService($prefix, $suffix);
+ }
+ } elseif ($name[0] === '@' && strpos($name, ':') !== false) {
+ list($configName, $nodeName) = preg_split('~:\s*~', substr($name, 1), 2);
+ $config->createImportedNode($configName, $nodeName);
+ }
+ }
+ $node->addChild($config->getNode($name));
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param array|string $children
+ * @return $this
+ */
+ public function setChildren($children)
+ {
+ if (is_string($children)) {
+ $children = array($children);
+ }
+ $this->children = $children;
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getChildren()
+ {
+ return $this->children;
+ }
+}
diff --git a/library/Businessprocess/Modification/NodeApplyManualOrderAction.php b/library/Businessprocess/Modification/NodeApplyManualOrderAction.php
new file mode 100644
index 0000000..4ad53e0
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeApplyManualOrderAction.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Common\Sort;
+
+class NodeApplyManualOrderAction extends NodeAction
+{
+ use Sort;
+
+ public function appliesTo(BpConfig $config)
+ {
+ return $config->getMetadata()->get('ManualOrder') !== 'yes';
+ }
+
+ public function applyTo(BpConfig $config)
+ {
+ $i = 0;
+ foreach ($config->getBpNodes() as $name => $node) {
+ if ($node->getDisplay() > 0) {
+ $node->setDisplay(++$i);
+ }
+
+ if ($node->hasChildren()) {
+ $node->setChildNames(array_keys(
+ $this->setSort('display_name asc')
+ ->sort($node->getChildren())
+ ));
+ }
+ }
+
+ $config->getMetadata()->set('ManualOrder', 'yes');
+ }
+}
diff --git a/library/Businessprocess/Modification/NodeCopyAction.php b/library/Businessprocess/Modification/NodeCopyAction.php
new file mode 100644
index 0000000..80d781b
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeCopyAction.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Common\Sort;
+
+class NodeCopyAction extends NodeAction
+{
+ use Sort;
+
+ /**
+ * @param BpConfig $config
+ * @return bool
+ */
+ public function appliesTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+
+ if (! $config->hasBpNode($name)) {
+ $this->error('Process "%s" not found', $name);
+ }
+
+ if ($config->hasRootNode($name)) {
+ $this->error('A toplevel node with name "%s" already exists', $name);
+ }
+
+ return true;
+ }
+
+ /**
+ * @param BpConfig $config
+ */
+ public function applyTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+
+ $display = 1;
+ if ($config->getMetadata()->isManuallyOrdered()) {
+ $rootNodes = self::applyManualSorting($config->getRootNodes());
+ $display = end($rootNodes)->getDisplay() + 1;
+ }
+
+ $config->addRootNode($name)
+ ->getBpNode($name)
+ ->setDisplay($display);
+ }
+}
diff --git a/library/Businessprocess/Modification/NodeCreateAction.php b/library/Businessprocess/Modification/NodeCreateAction.php
new file mode 100644
index 0000000..167d3bc
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeCreateAction.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Node;
+
+class NodeCreateAction extends NodeAction
+{
+ /** @var string */
+ protected $parentName;
+
+ /** @var array */
+ protected $properties = array();
+
+ /** @var array */
+ protected $preserveProperties = array('parentName', 'properties');
+
+ /**
+ * @param Node $name
+ */
+ public function setParent(Node $name)
+ {
+ $this->parentName = $name->getName();
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasParent()
+ {
+ return $this->parentName !== null;
+ }
+
+ /**
+ * @return string
+ */
+ public function getParentName()
+ {
+ return $this->parentName;
+ }
+
+ /**
+ * @param string $name
+ */
+ public function setParentName($name)
+ {
+ $this->parentName = $name;
+ }
+
+ /**
+ * @return array
+ */
+ public function getProperties()
+ {
+ return $this->properties;
+ }
+
+ /**
+ * @param array $properties
+ * @return $this
+ */
+ public function setProperties($properties)
+ {
+ $this->properties = (array) $properties;
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function appliesTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+ if ($config->hasNode($name)) {
+ $this->error('A node with name "%s" already exists', $name);
+ }
+
+ $parent = $this->getParentName();
+ if ($parent !== null && !$config->hasBpNode($parent)) {
+ $this->error('Parent process "%s" missing', $parent);
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function applyTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+
+ $properties = array(
+ 'name' => $name,
+ 'operator' => $this->properties['operator'],
+ );
+ if (array_key_exists('childNames', $this->properties)) {
+ $properties['child_names'] = $this->properties['childNames'];
+ } else {
+ $properties['child_names'] = array();
+ }
+ $node = new BpNode((object) $properties);
+ $node->setBpConfig($config);
+
+ foreach ($this->getProperties() as $key => $val) {
+ if ($key === 'parentName') {
+ $config->getBpNode($val)->addChild($node);
+ continue;
+ }
+ $func = 'set' . ucfirst($key);
+ $node->$func($val);
+ }
+
+ if ($node->getDisplay() > 1) {
+ $i = $node->getDisplay();
+ foreach ($config->getRootNodes() as $_ => $rootNode) {
+ if ($rootNode->getDisplay() >= $node->getDisplay()) {
+ $rootNode->setDisplay(++$i);
+ }
+ }
+ }
+
+ $config->addNode($name, $node);
+
+ return $node;
+ }
+}
diff --git a/library/Businessprocess/Modification/NodeModifyAction.php b/library/Businessprocess/Modification/NodeModifyAction.php
new file mode 100644
index 0000000..1b33094
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeModifyAction.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Node;
+
+class NodeModifyAction extends NodeAction
+{
+ protected $properties = array();
+
+ protected $formerProperties = array();
+
+ protected $preserveProperties = array('formerProperties', 'properties');
+
+ /**
+ * Set properties for a specific node
+ *
+ * Can be called multiple times
+ *
+ * @param Node $node
+ * @param array $properties
+ *
+ * @return $this
+ */
+ public function setNodeProperties(Node $node, array $properties)
+ {
+ foreach (array_keys($properties) as $key) {
+ $this->properties[$key] = $properties[$key];
+
+ if (array_key_exists($key, $this->formerProperties)) {
+ continue;
+ }
+
+ $func = 'get' . ucfirst($key);
+ $this->formerProperties[$key] = $node->$func();
+ }
+
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function appliesTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+
+ if (! $config->hasNode($name)) {
+ $this->error('Node "%s" not found', $name);
+ }
+
+ $node = $config->getNode($name);
+
+ foreach ($this->properties as $key => $val) {
+ $currentVal = $node->{'get' . ucfirst($key)}();
+ if ($this->formerProperties[$key] !== $currentVal) {
+ $this->error(
+ 'Property %s of node "%s" changed its value from "%s" to "%s"',
+ $key,
+ $name,
+ $this->formerProperties[$key],
+ $currentVal
+ );
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function applyTo(BpConfig $config)
+ {
+ $node = $config->getNode($this->getNodeName());
+
+ foreach ($this->properties as $key => $val) {
+ $func = 'set' . ucfirst($key);
+ $node->$func($val);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param $properties
+ * @return $this
+ */
+ public function setProperties($properties)
+ {
+ $this->properties = $properties;
+ return $this;
+ }
+
+ /**
+ * @param $properties
+ * @return $this
+ */
+ public function setFormerProperties($properties)
+ {
+ $this->formerProperties = $properties;
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getProperties()
+ {
+ return $this->properties;
+ }
+
+ /**
+ * @return array
+ */
+ public function getFormerProperties()
+ {
+ return $this->formerProperties;
+ }
+}
diff --git a/library/Businessprocess/Modification/NodeMoveAction.php b/library/Businessprocess/Modification/NodeMoveAction.php
new file mode 100644
index 0000000..4c4305d
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeMoveAction.php
@@ -0,0 +1,227 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Common\Sort;
+
+class NodeMoveAction extends NodeAction
+{
+ use Sort;
+
+ /**
+ * @var string
+ */
+ protected $parent;
+
+ /**
+ * @var string
+ */
+ protected $newParent;
+
+ /**
+ * @var int
+ */
+ protected $from;
+
+ /**
+ * @var int
+ */
+ protected $to;
+
+ protected $preserveProperties = ['parent', 'newParent', 'from', 'to'];
+
+ public function setParent($name)
+ {
+ $this->parent = $name;
+ }
+
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ public function setNewParent($name)
+ {
+ $this->newParent = $name;
+ }
+
+ public function getNewParent()
+ {
+ return $this->newParent;
+ }
+
+ public function setFrom($from)
+ {
+ $this->from = (int) $from;
+ }
+
+ public function getFrom()
+ {
+ return $this->from;
+ }
+
+ public function setTo($to)
+ {
+ $this->to = (int) $to;
+ }
+
+ public function getTo()
+ {
+ return $this->to;
+ }
+
+ public function appliesTo(BpConfig $config)
+ {
+ if (! $config->getMetadata()->isManuallyOrdered()) {
+ $this->error('Process configuration is not manually ordered yet');
+ }
+
+ $name = $this->getNodeName();
+ if ($this->parent !== null) {
+ if (! $config->hasBpNode($this->parent)) {
+ $this->error('Parent process "%s" missing', $this->parent);
+ }
+ $parent = $config->getBpNode($this->parent);
+ if (! $parent->hasChild($name)) {
+ $this->error('Node "%s" not found in process "%s"', $name, $this->parent);
+ }
+
+ $nodes = $parent->getChildNames();
+ if (! isset($nodes[$this->from]) || $nodes[$this->from] !== $name) {
+ $reversedNodes = array_reverse($nodes); // The user may have reversed the sort direction
+ if (! isset($reversedNodes[$this->from]) || $reversedNodes[$this->from] !== $name) {
+ $this->error('Node "%s" not found at position %d', $name, $this->from);
+ } else {
+ $this->from = array_search($reversedNodes[$this->from], $nodes, true);
+ $this->to = array_search($reversedNodes[$this->to], $nodes, true);
+ }
+ }
+ } else {
+ if (! $config->hasRootNode($name)) {
+ $this->error('Toplevel process "%s" not found', $name);
+ }
+
+ $nodes = array_keys(self::applyManualSorting($config->getRootNodes()));
+ if (! isset($nodes[$this->from]) || $nodes[$this->from] !== $name) {
+ $reversedNodes = array_reverse($nodes); // The user may have reversed the sort direction
+ if (! isset($reversedNodes[$this->from]) || $reversedNodes[$this->from] !== $name) {
+ $this->error('Toplevel process "%s" not found at position %d', $name, $this->from);
+ } else {
+ $this->from = array_search($reversedNodes[$this->from], $nodes, true);
+ $this->to = array_search($reversedNodes[$this->to], $nodes, true);
+ }
+ }
+ }
+
+ if ($this->parent !== $this->newParent) {
+ if ($this->newParent !== null) {
+ if (! $config->hasBpNode($this->newParent)) {
+ $this->error('New parent process "%s" missing', $this->newParent);
+ } elseif ($config->getBpNode($this->newParent)->hasChild($name)) {
+ $this->error(
+ 'New parent process "%s" already has a node with the name "%s"',
+ $this->newParent,
+ $name
+ );
+ }
+
+ $childrenCount = $config->getBpNode($this->newParent)->countChildren();
+ if ($this->to > 0 && $childrenCount < $this->to) {
+ $this->error(
+ 'New parent process "%s" has not enough children. Target position %d out of range',
+ $this->newParent,
+ $this->to
+ );
+ }
+ } else {
+ if ($config->hasRootNode($name)) {
+ $this->error('Process "%s" is already a toplevel process', $name);
+ }
+
+ $childrenCount = $config->countChildren();
+ if ($this->to > 0 && $childrenCount < $this->to) {
+ $this->error(
+ 'Process configuration has not enough toplevel processes. Target position %d out of range',
+ $this->to
+ );
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public function applyTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+ if ($this->parent !== null) {
+ $nodes = $config->getBpNode($this->parent)->getChildren();
+ } else {
+ $nodes = self::applyManualSorting($config->getRootNodes());
+ }
+
+ $node = $nodes[$name];
+ $nodes = array_merge(
+ array_slice($nodes, 0, $this->from, true),
+ array_slice($nodes, $this->from + 1, null, true)
+ );
+ if ($this->parent === $this->newParent) {
+ $nodes = array_merge(
+ array_slice($nodes, 0, $this->to, true),
+ [$name => $node],
+ array_slice($nodes, $this->to, null, true)
+ );
+ } else {
+ if ($this->newParent !== null) {
+ $newNodes = $config->getBpNode($this->newParent)->getChildren();
+ } else {
+ $newNodes = self::applyManualSorting($config->getRootNodes());
+ }
+
+ $newNodes = array_merge(
+ array_slice($newNodes, 0, $this->to, true),
+ [$name => $node],
+ array_slice($newNodes, $this->to, null, true)
+ );
+
+ if ($this->newParent !== null) {
+ $config->getBpNode($this->newParent)->setChildNames(array_keys($newNodes));
+ } else {
+ $config->addRootNode($name);
+
+ $i = 0;
+ foreach ($newNodes as $newName => $newNode) {
+ /** @var BpNode $newNode */
+ if ($newNode->getDisplay() > 0 || $newName === $name) {
+ $i += 1;
+ if ($newNode->getDisplay() !== $i) {
+ $newNode->setDisplay($i);
+ }
+ }
+ }
+ }
+ }
+
+ if ($this->parent !== null) {
+ $config->getBpNode($this->parent)->setChildNames(array_keys($nodes));
+ } else {
+ if ($this->newParent !== null) {
+ $config->removeRootNode($name);
+ $node->setDisplay(0);
+ }
+
+ $i = 0;
+ foreach ($nodes as $_ => $oldNode) {
+ /** @var BpNode $oldNode */
+ if ($oldNode->getDisplay() > 0) {
+ $i += 1;
+ if ($oldNode->getDisplay() !== $i) {
+ $oldNode->setDisplay($i);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/library/Businessprocess/Modification/NodeRemoveAction.php b/library/Businessprocess/Modification/NodeRemoveAction.php
new file mode 100644
index 0000000..6100146
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeRemoveAction.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Node;
+
+/**
+ * NodeRemoveAction
+ *
+ * Tracks removed nodes
+ *
+ * @package Icinga\Module\Businessprocess
+ */
+class NodeRemoveAction extends NodeAction
+{
+ protected $preserveProperties = array('parentName');
+
+ protected $parentName;
+
+ /**
+ * @param $parentName
+ * @return $this
+ */
+ public function setParentName($parentName = null)
+ {
+ $this->parentName = $parentName;
+ return $this;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getParentName()
+ {
+ return $this->parentName;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function appliesTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+ $parent = $this->getParentName();
+ if ($parent === null) {
+ if (!$config->hasNode($name)) {
+ $this->error('Toplevel process "%s" not found', $name);
+ }
+ } else {
+ if (! $config->hasNode($parent)) {
+ $this->error('Parent process "%s" missing', $parent);
+ } elseif (! $config->getBpNode($parent)->hasChild($name)) {
+ $this->error('Node "%s" not found in process "%s"', $name, $parent);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function applyTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+ $parentName = $this->getParentName();
+ $node = $config->getNode($name);
+
+ /** @var ?BpNode $parentBpNode */
+ $parentBpNode = $parentName ? $config->getNode($parentName) : null;
+ $this->updateStateOverrides($node, $parentBpNode);
+
+ if ($parentName === null) {
+ if (! $config->hasBpNode($name)) {
+ $config->removeNode($name);
+ } else {
+ $oldDisplay = $config->getBpNode($name)->getDisplay();
+ $config->removeNode($name);
+ if ($config->getMetadata()->isManuallyOrdered()) {
+ foreach ($config->getRootNodes() as $_ => $node) {
+ $nodeDisplay = $node->getDisplay();
+ if ($nodeDisplay > $oldDisplay) {
+ $node->setDisplay($node->getDisplay() - 1);
+ } elseif ($nodeDisplay === $oldDisplay) {
+ break; // Stop immediately to not make things worse ;)
+ }
+ }
+ }
+ }
+ } else {
+ $parent = $config->getBpNode($parentName);
+ $parent->removeChild($name);
+ $node->removeParent($parentName);
+ if (! $node->hasParents()) {
+ $config->removeNode($name);
+ }
+ }
+ }
+
+ /**
+ * Update state overrides
+ *
+ * @param Node $node
+ * @param BpNode|null $nodeParent
+ *
+ * @return void
+ */
+ private function updateStateOverrides(Node $node, ?BpNode $nodeParent): void
+ {
+ $parents = [];
+ if ($nodeParent !== null) {
+ $parents = [$nodeParent];
+ } else {
+ $parents = $node->getParents();
+ }
+
+ foreach ($parents as $parent) {
+ $parentStateOverrides = $parent->getStateOverrides();
+ unset($parentStateOverrides[$node->getName()]);
+ $parent->setStateOverrides($parentStateOverrides);
+ }
+ }
+}
diff --git a/library/Businessprocess/Modification/ProcessChanges.php b/library/Businessprocess/Modification/ProcessChanges.php
new file mode 100644
index 0000000..9257558
--- /dev/null
+++ b/library/Businessprocess/Modification/ProcessChanges.php
@@ -0,0 +1,294 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Web\Session\SessionNamespace as Session;
+
+class ProcessChanges
+{
+ /** @var NodeAction[] */
+ protected $changes = array();
+
+ /** @var Session */
+ protected $session;
+
+ /** @var BpConfig */
+ protected $config;
+
+ /** @var bool */
+ protected $hasBeenModified = false;
+
+ /** @var string Session storage key for this processes changes */
+ protected $sessionKey;
+
+ /**
+ * ProcessChanges constructor.
+ *
+ * Direct access is not allowed
+ */
+ private function __construct()
+ {
+ }
+
+ /**
+ * @param BpConfig $bp
+ * @param Session $session
+ *
+ * @return ProcessChanges
+ */
+ public static function construct(BpConfig $bp, Session $session)
+ {
+ $key = 'changes.' . $bp->getName();
+ $changes = new ProcessChanges();
+ $changes->sessionKey = $key;
+
+ if ($actions = $session->get($key)) {
+ foreach ($actions as $string) {
+ $changes->push(NodeAction::unSerialize($string));
+ }
+ }
+ $changes->session = $session;
+ $changes->config = $bp;
+
+ return $changes;
+ }
+
+ /**
+ * @param Node $node
+ * @param $properties
+ *
+ * @return $this
+ */
+ public function modifyNode(Node $node, $properties)
+ {
+ $action = new NodeModifyAction($node);
+ $action->setNodeProperties($node, $properties);
+ return $this->push($action, true);
+ }
+
+ /**
+ * @param Node $node
+ * @param $properties
+ *
+ * @return $this
+ */
+ public function addChildrenToNode($children, Node $node = null)
+ {
+ $action = new NodeAddChildrenAction($node);
+ $action->setChildren($children);
+ return $this->push($action, true);
+ }
+
+ /**
+ * @param Node|string $nodeName
+ * @param array $properties
+ * @param Node $parent
+ *
+ * @return $this
+ */
+ public function createNode($nodeName, $properties, Node $parent = null)
+ {
+ $action = new NodeCreateAction($nodeName);
+ $action->setProperties($properties);
+ if ($parent !== null) {
+ $action->setParent($parent);
+ }
+ return $this->push($action, true);
+ }
+
+ /**
+ * @param $nodeName
+ * @return $this
+ */
+ public function copyNode($nodeName)
+ {
+ $action = new NodeCopyAction($nodeName);
+ return $this->push($action, true);
+ }
+
+ /**
+ * @param Node $node
+ * @param string $parentName
+ * @return $this
+ */
+ public function deleteNode(Node $node, $parentName = null)
+ {
+ $action = new NodeRemoveAction($node);
+ if ($parentName !== null) {
+ $action->setParentName($parentName);
+ }
+
+ return $this->push($action, true);
+ }
+
+ /**
+ * Move the given node
+ *
+ * @param Node $node
+ * @param int $from
+ * @param int $to
+ * @param string $newParent
+ * @param string $parent
+ *
+ * @return $this
+ */
+ public function moveNode(Node $node, $from, $to, $newParent, $parent = null)
+ {
+ $action = new NodeMoveAction($node);
+ $action->setParent($parent);
+ $action->setNewParent($newParent);
+ $action->setFrom($from);
+ $action->setTo($to);
+
+ return $this->push($action, true);
+ }
+
+ /**
+ * Apply manual order on the entire bp configuration file
+ *
+ * @return $this
+ */
+ public function applyManualOrder()
+ {
+ return $this->push(new NodeApplyManualOrderAction(), true);
+ }
+
+ /**
+ * Add a new action to the stack
+ *
+ * @param NodeAction $change
+ * @param bool $apply
+ *
+ * @return $this
+ */
+ public function push(NodeAction $change, $apply = false)
+ {
+ if ($apply && $change->appliesTo($this->config)) {
+ $change->applyTo($this->config);
+ }
+
+ $this->changes[] = $change;
+ $this->hasBeenModified = true;
+ return $this;
+ }
+
+ /**
+ * Get all stacked actions
+ *
+ * @return NodeAction[]
+ */
+ public function getChanges()
+ {
+ return $this->changes;
+ }
+
+ /**
+ * Forget all changes and remove them from the Session
+ *
+ * @return $this
+ */
+ public function clear()
+ {
+ $this->hasBeenModified = true;
+ $this->changes = array();
+ $this->session->set($this->getSessionKey(), null);
+ return $this;
+ }
+
+ /**
+ * Whether there are no stacked changes
+ *
+ * @return bool
+ */
+ public function isEmpty()
+ {
+ return $this->count() === 0;
+ }
+
+ /**
+ * Number of stacked changes
+ *
+ * @return int
+ */
+ public function count()
+ {
+ return count($this->changes);
+ }
+
+ /**
+ * Get the first change on the stack, false if empty
+ *
+ * @return NodeAction|boolean
+ */
+ public function shift()
+ {
+ if ($this->isEmpty()) {
+ return false;
+ }
+
+ $this->hasBeenModified = true;
+ return array_shift($this->changes);
+ }
+
+ /**
+ * Get the last change on the stack, false if empty
+ *
+ * @return NodeAction|boolean
+ */
+ public function pop()
+ {
+ if ($this->isEmpty()) {
+ return false;
+ }
+
+ $this->hasBeenModified = true;
+ return array_pop($this->changes);
+ }
+
+ /**
+ * The identifier used for this processes changes in our Session storage
+ *
+ * @return string
+ */
+ protected function getSessionKey()
+ {
+ return $this->sessionKey;
+ }
+
+ protected function hasBeenModified()
+ {
+ return $this->hasBeenModified;
+ }
+
+ /**
+ * @return array
+ */
+ public function serialize()
+ {
+ $serialized = array();
+ foreach ($this->getChanges() as $change) {
+ $serialized[] = $change->serialize();
+ }
+
+ return $serialized;
+ }
+
+ /**
+ * Persist to session on destruction
+ */
+ public function __destruct()
+ {
+ if (! $this->hasBeenModified()) {
+ unset($this->session);
+ return;
+ }
+ $session = $this->session;
+ $key = $this->getSessionKey();
+ if (! $this->isEmpty()) {
+ $session->set($key, $this->serialize());
+ }
+ unset($this->session);
+ }
+}