summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Repository/IniRepository.php
blob: 2519d03c64fda0844d5072a04b060ad66cde7f64 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
<?php
/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */

namespace Icinga\Repository;

use Exception;
use Icinga\Application\Config;
use Icinga\Data\ConfigObject;
use Icinga\Data\Extensible;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Updatable;
use Icinga\Data\Reducible;
use Icinga\Exception\ProgrammingError;
use Icinga\Exception\StatementException;

/**
 * Abstract base class for concrete INI repository implementations
 *
 * Additionally provided features:
 * <ul>
 *  <li>Insert, update and delete capabilities</li>
 *  <li>Triggers for inserts, updates and deletions</li>
 *  <li>Lazy initialization of table specific configs</li>
 * </ul>
 */
abstract class IniRepository extends Repository implements Extensible, Updatable, Reducible
{
    /**
     * The configuration files used as table specific datasources
     *
     * This must be initialized by concrete repository implementations, in the following format
     * <code>
     * array(
     *   'table_name' => array(
     *     'name'      => 'name_of_the_ini_file_without_extension',
     *     'keyColumn' => 'the_name_of_the_column_to_use_as_key_column',
     *    ['module'    => 'the_name_of_the_module_if_any']
     *   )
     * )
     * </code>
     *
     * @var array
     */
    protected $configs;

    /**
     * The tables for which triggers are available when inserting, updating or deleting rows
     *
     * This may be initialized by concrete repository implementations and describes for which table names triggers
     * are available. The repository attempts to find a method depending on the type of event and table for which
     * to run the trigger. The name of such a method is expected to be declared using lowerCamelCase.
     * (e.g. group_membership will be translated to onUpdateGroupMembership and groupmembership will be translated
     * to onUpdateGroupmembership) The available events are onInsert, onUpdate and onDelete.
     *
     * @var array
     */
    protected $triggers;

    /**
     * Create a new INI repository object
     *
     * @param   Config|null $ds     The data source to use
     *
     * @throws  ProgrammingError    In case the given data source does not provide a valid key column
     */
    public function __construct(Config $ds = null)
    {
        parent::__construct($ds); // First! Due to init().

        if ($ds !== null && !$ds->getConfigObject()->getKeyColumn()) {
            throw new ProgrammingError('INI repositories require their data source to provide a valid key column');
        }
    }

    /**
     * {@inheritDoc}
     *
     * @return  Config
     */
    public function getDataSource($table = null)
    {
        if ($this->ds !== null) {
            return parent::getDataSource($table);
        }

        $table = $table ?: $this->getBaseTable();
        $configs = $this->getConfigs();
        if (! isset($configs[$table])) {
            throw new ProgrammingError('Config for table "%s" missing', $table);
        } elseif (! $configs[$table] instanceof Config) {
            $configs[$table] = $this->createConfig($configs[$table], $table);
        }

        if (! $configs[$table]->getConfigObject()->getKeyColumn()) {
            throw new ProgrammingError(
                'INI repositories require their data source to provide a valid key column'
            );
        }

        return $configs[$table];
    }

    /**
     * Return the configuration files used as table specific datasources
     *
     * Calls $this->initializeConfigs() in case $this->configs is null.
     *
     * @return  array
     */
    public function getConfigs()
    {
        if ($this->configs === null) {
            $this->configs = $this->initializeConfigs();
        }

        return $this->configs;
    }

    /**
     * Overwrite this in your repository implementation in case you need to initialize the configs lazily
     *
     * @return  array
     */
    protected function initializeConfigs()
    {
        return array();
    }

    /**
     * Return the tables for which triggers are available when inserting, updating or deleting rows
     *
     * Calls $this->initializeTriggers() in case $this->triggers is null.
     *
     * @return  array
     */
    public function getTriggers()
    {
        if ($this->triggers === null) {
            $this->triggers = $this->initializeTriggers();
        }

        return $this->triggers;
    }

    /**
     * Overwrite this in your repository implementation in case you need to initialize the triggers lazily
     *
     * @return  array
     */
    protected function initializeTriggers()
    {
        return array();
    }

    /**
     * Run a trigger for the given table and row which is about to be inserted
     *
     * @param   string          $table
     * @param   ConfigObject    $new
     *
     * @return  ConfigObject
     */
    public function onInsert($table, ConfigObject $new)
    {
        $trigger = $this->getTrigger($table, 'onInsert');
        if ($trigger !== null) {
            $row = $this->$trigger($new);
            if ($row !== null) {
                $new = $row;
            }
        }

        return $new;
    }

    /**
     * Run a trigger for the given table and row which is about to be updated
     *
     * @param   string          $table
     * @param   ConfigObject    $old
     * @param   ConfigObject    $new
     *
     * @return  ConfigObject
     */
    public function onUpdate($table, ConfigObject $old, ConfigObject $new)
    {
        $trigger = $this->getTrigger($table, 'onUpdate');
        if ($trigger !== null) {
            $row = $this->$trigger($old, $new);
            if ($row !== null) {
                $new = $row;
            }
        }

        return $new;
    }

    /**
     * Run a trigger for the given table and row which has been deleted
     *
     * @param   string          $table
     * @param   ConfigObject    $old
     */
    public function onDelete($table, ConfigObject $old)
    {
        $trigger = $this->getTrigger($table, 'onDelete');
        if ($trigger !== null) {
            $this->$trigger($old);
        }
    }

    /**
     * Return the name of the trigger method for the given table and event-type
     *
     * @param   string  $table  The table name for which to return a trigger method
     * @param   string  $event  The name of the event type
     *
     * @return  ?string
     */
    protected function getTrigger($table, $event)
    {
        if (! in_array($table, $this->getTriggers())) {
            return;
        }

        $identifier = join('', array_map('ucfirst', explode('_', $table)));
        if (method_exists($this, $event . $identifier)) {
            return $event . $identifier;
        }
    }

    /**
     * Insert the given data for the given target
     *
     * $data must provide a proper value for the data source's key column.
     *
     * @param   string  $target
     * @param   array   $data
     *
     * @throws  StatementException  In case the operation has failed
     */
    public function insert($target, array $data)
    {
        $ds = $this->getDataSource($target);
        $newData = $this->requireStatementColumns($target, $data);

        $config = $this->onInsert($target, new ConfigObject($newData));
        $section = $this->extractSectionName($config, $ds->getConfigObject()->getKeyColumn());

        if ($ds->hasSection($section)) {
            throw new StatementException(t('Cannot insert. Section "%s" does already exist'), $section);
        }

        $ds->setSection($section, $config);

        try {
            $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
     *
     * @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)
    {
        $ds = $this->getDataSource($target);
        $newData = $this->requireStatementColumns($target, $data);

        $keyColumn = $ds->getConfigObject()->getKeyColumn();
        if ($filter === null && isset($newData[$keyColumn])) {
            throw new StatementException(
                t('Cannot update. Column "%s" holds a section\'s name which must be unique'),
                $keyColumn
            );
        }

        $query = $ds->select();
        if ($filter !== null) {
            $query->addFilter($this->requireFilter($target, $filter));
        }

        /** @var ConfigObject $config */
        $newSection = null;
        foreach ($query as $section => $config) {
            if ($newSection !== null) {
                throw new StatementException(
                    t('Cannot update. Column "%s" holds a section\'s name which must be unique'),
                    $keyColumn
                );
            }

            $newConfig = clone $config;
            foreach ($newData as $column => $value) {
                if ($column === $keyColumn) {
                    if ($value !== $config->get($keyColumn)) {
                        $newSection = $value;
                    }
                } else {
                    $newConfig->$column = $value;
                }
            }

            // This is necessary as the query result set contains the key column.
            unset($newConfig->$keyColumn);

            if ($newSection) {
                if ($ds->hasSection($newSection)) {
                    throw new StatementException(t('Cannot update. Section "%s" does already exist'), $newSection);
                }

                $ds->removeSection($section)->setSection(
                    $newSection,
                    $this->onUpdate($target, $config, $newConfig)
                );
            } else {
                $ds->setSection(
                    $section,
                    $this->onUpdate($target, $config, $newConfig)
                );
            }
        }

        try {
            $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
     *
     * @param   string  $target
     * @param   Filter  $filter
     *
     * @throws  StatementException  In case the operation has failed
     */
    public function delete($target, Filter $filter = null)
    {
        $ds = $this->getDataSource($target);

        $query = $ds->select();
        if ($filter !== null) {
            $query->addFilter($this->requireFilter($target, $filter));
        }

        /** @var ConfigObject $config */
        foreach ($query as $section => $config) {
            $ds->removeSection($section);
            $this->onDelete($target, $config);
        }

        try {
            $ds->saveIni();
        } catch (Exception $e) {
            throw new StatementException(t('Failed to delete. An error occurred: %s'), $e->getMessage());
        }
    }

    /**
     * Create and return a Config for the given meta and table
     *
     * @param   array   $meta
     * @param   string  $table
     *
     * @return  Config
     *
     * @throws  ProgrammingError    In case the given meta is invalid
     */
    protected function createConfig(array $meta, $table)
    {
        if (! isset($meta['name'])) {
            throw new ProgrammingError('Config file name missing for table "%s"', $table);
        } elseif (! isset($meta['keyColumn'])) {
            throw new ProgrammingError('Config key column name missing for table "%s"', $table);
        }

        if (isset($meta['module'])) {
            $config = Config::module($meta['module'], $meta['name']);
        } else {
            $config = Config::app($meta['name']);
        }

        $config->getConfigObject()->setKeyColumn($meta['keyColumn']);
        return $config;
    }

    /**
     * Extract and return the section name off of the given $config
     *
     * @param   array|ConfigObject  $config
     * @param   string              $keyColumn
     *
     * @return  string
     *
     * @throws  ProgrammingError    In case no valid section name is available
     */
    protected function extractSectionName(&$config, $keyColumn)
    {
        if (! is_array($config) && !$config instanceof ConfigObject) {
            throw new ProgrammingError('$config is neither an array nor a ConfigObject');
        } elseif (! isset($config[$keyColumn])) {
            throw new ProgrammingError('$config does not provide a value for key column "%s"', $keyColumn);
        }

        $section = $config[$keyColumn];
        unset($config[$keyColumn]);
        return $section;
    }
}