diff --git a/library/Icinga/Repository/IniRepository.php b/library/Icinga/Repository/IniRepository.php
new file mode 100644
index 000000000..7953422be
--- /dev/null
+++ b/library/Icinga/Repository/IniRepository.php
@@ -0,0 +1,201 @@
+
+ *
Insert, update and delete capabilities
+ *
+ */
+abstract class IniRepository extends Repository implements Extensible, Updatable, Reducible
+{
+ /**
+ * Insert the given data for the given target
+ *
+ * In case the data source provides a valid key column, $data must provide a proper
+ * value for it which is then being used as the section name instead of $target.
+ *
+ * @param string $target
+ * @param array $data
+ *
+ * @throws StatementException In case the operation has failed
+ */
+ public function insert($target, array $data)
+ {
+ $newData = $this->requireStatementColumns($data);
+ $section = $this->extractSectionName($target, $newData);
+
+ if ($this->ds->hasSection($section)) {
+ throw new StatementException(t('Cannot insert. Section "%s" does already exist'), $section);
+ }
+
+ $this->ds->setSection($section, $newData);
+
+ try {
+ $this->ds->saveIni();
+ } catch (Exception $e) {
+ throw new StatementException(t('Failed to insert. An error occurred: %s'), $e->getMessage());
+ }
+ }
+
+ /**
+ * Update the target with the given data and optionally limit the affected entries by using a filter
+ *
+ * The section(s) to update are either identified by $filter or $target, in order. If neither of both
+ * is given, all sections provided by the data source are going to be updated. Uniqueness of a section's
+ * name will be ensured.
+ *
+ * @param string $target
+ * @param array $data
+ * @param Filter $filter
+ *
+ * @throws StatementException In case the operation has failed
+ */
+ public function update($target, array $data, Filter $filter = null)
+ {
+ $newData = $this->requireStatementColumns($data);
+ $keyColumn = $this->ds->getConfigObject()->getKeyColumn();
+ if ($keyColumn && $filter === null && isset($newData[$keyColumn]) && !$this->ds->hasSection($target)) {
+ throw new StatementException(
+ t('Cannot update. Column "%s" holds a section\'s name which must be unique'),
+ $keyColumn
+ );
+ }
+
+ if ($target && !$filter) {
+ if (! $this->ds->hasSection($target)) {
+ throw new StatementException(t('Cannot update. Section "%s" does not exist'), $target);
+ }
+
+ $results = array($target => $this->ds->getSection($target));
+ } else {
+ $query = $this->ds->select();
+ if ($filter) {
+ $this->requireFilter($filter);
+ $query->applyFilter($filter);
+ }
+
+ $results = $query->fetchAll();
+ }
+
+ $newSection = null;
+ foreach ($results as $section => $config) {
+ if ($newSection !== null) {
+ throw new StatementException(
+ t('Cannot update. Column "%s" holds a section\'s name which must be unique'),
+ $keyColumn
+ );
+ }
+
+ foreach ($newData as $column => $value) {
+ if ($keyColumn && $column === $keyColumn) {
+ $newSection = $value;
+ } else {
+ $config->$column = $value;
+ }
+ }
+
+ if ($keyColumn && isset($config->$keyColumn) && $config->$keyColumn === $section) {
+ unset($config->$keyColumn);
+ }
+
+ if ($newSection) {
+ if ($this->ds->hasSection($newSection)) {
+ throw new StatementException(t('Cannot update. Section "%s" does already exist'), $newSection);
+ }
+
+ $this->ds->removeSection($section)->setSection($newSection, $config);
+ } else {
+ $this->ds->setSection($section, $config);
+ }
+ }
+
+ try {
+ $this->ds->saveIni();
+ } catch (Exception $e) {
+ throw new StatementException(t('Failed to update. An error occurred: %s'), $e->getMessage());
+ }
+ }
+
+ /**
+ * Delete entries in the given target, optionally limiting the affected entries by using a filter
+ *
+ * The section(s) to delete are either identified by $filter or $target, in order. If neither of both
+ * is given, all sections provided by the data source are going to be deleted.
+ *
+ * @param string $target
+ * @param Filter $filter
+ *
+ * @throws StatementException In case the operation has failed
+ */
+ public function delete($target, Filter $filter = null)
+ {
+ if ($target && !$filter) {
+ if (! $this->ds->hasSection($target)) {
+ return; // Nothing to do
+ }
+
+ $results = array($target => $this->ds->getSection($target));
+ } else {
+ $query = $this->ds->select();
+ if ($filter) {
+ $this->requireFilter($filter);
+ $query->applyFilter($filter);
+ }
+
+ $results = $query->fetchAll();
+ }
+
+ foreach ($results as $section => $_) {
+ $this->ds->removeSection($section);
+ }
+
+ try {
+ $this->ds->saveIni();
+ } catch (Exception $e) {
+ throw new StatementException(t('Failed to delete. An error occurred: %s'), $e->getMessage());
+ }
+ }
+
+ /**
+ * Extract and return the section name off of the given $data, if available, or validate $target
+ *
+ * @param string $target
+ * @param array $data
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case no valid section name is available
+ */
+ protected function extractSectionName($target, array & $data)
+ {
+ if (($keyColumn = $this->ds->getConfigObject()->getKeyColumn())) {
+ if (! isset($data[$keyColumn])) {
+ throw new ProgrammingError('$data does not provide a value for key column "%s"', $keyColumn);
+ }
+
+ $target = $data[$keyColumn];
+ unset($data[$keyColumn]);
+ }
+
+ if (! is_string($target)) {
+ throw new ProgrammingError(
+ 'Neither the data source nor the $target parameter provide a valid section name'
+ );
+ }
+
+ return $target;
+ }
+}
diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php
index 3ecdbe61c..1726426ba 100644
--- a/library/Icinga/Repository/Repository.php
+++ b/library/Icinga/Repository/Repository.php
@@ -8,6 +8,7 @@ use Icinga\Data\Filter\Filter;
use Icinga\Data\Selectable;
use Icinga\Exception\ProgrammingError;
use Icinga\Exception\QueryException;
+use Icinga\Exception\StatementException;
/**
* Abstract base class for concrete repository implementations
@@ -573,4 +574,56 @@ abstract class Repository implements Selectable
return $aliasColumnMap[$name];
}
+
+ /**
+ * Return whether the given column name or alias is a valid statement column
+ *
+ * @param string $name The column name or alias to check
+ *
+ * @return bool
+ */
+ public function hasStatementColumn($name)
+ {
+ return $this->hasQueryColumn($name);
+ }
+
+ /**
+ * Validate that the given column is a valid statement column and return it or the actual name if it's an alias
+ *
+ * @param string $name The name or alias of the column to validate
+ *
+ * @return string The given column's name
+ *
+ * @throws StatementException In case the given column is not a statement column
+ */
+ public function requireStatementColumn($name)
+ {
+ if (in_array($name, $this->filterColumns)) {
+ throw new StatementException('Filter column "%s" cannot be referenced in a statement', $name);
+ }
+
+ $aliasColumnMap = $this->getAliasColumnMap();
+ if (! array_key_exists($name, $aliasColumnMap)) {
+ throw new StatementException('Statement column "%s" not found', $name);
+ }
+
+ return $aliasColumnMap[$name];
+ }
+
+ /**
+ * Resolve the given aliases or column names supposed to be persisted and convert their values
+ *
+ * @param array $data
+ *
+ * @return array
+ */
+ public function requireStatementColumns(array $data)
+ {
+ $resolved = array();
+ foreach ($data as $alias => $value) {
+ $resolved[$this->requireStatementColumn($alias)] = $this->persistColumn($alias, $value);
+ }
+
+ return $resolved;
+ }
}