xref: /dokuwiki/inc/Search/Index/MemoryIndex.php (revision 1148921de6af6909f19cb5b30b698d0f27d7751e)
1<?php
2
3namespace dokuwiki\Search\Index;
4
5use dokuwiki\Search\Exception\IndexLockException;
6use dokuwiki\Search\Exception\IndexUsageException;
7use dokuwiki\Search\Exception\IndexWriteException;
8
9/**
10 * Access to a single index file
11 *
12 * Access using this class always happens by loading the full index into memory.
13 * All modifications need to be explicitly made permanent using the save() method.
14 * Should be used for small indexes that receive many changes at once.
15 */
16class MemoryIndex extends AbstractIndex
17{
18    /** @var string the raw data lines of the index, no newlines */
19    protected $data;
20
21    /** @var bool has the index been modified? */
22    protected $dirty = false;
23
24    /**
25     * Loads the full contents of the index into memory
26     *
27     * @inheritdoc
28     */
29    public function __construct($idx, $suffix = '', $isWritable = false)
30    {
31        parent::__construct($idx, $suffix, $isWritable);
32
33        $this->data = [];
34        if (!file_exists($this->filename)) {
35            return;
36        }
37        $this->data = file($this->filename, FILE_IGNORE_NEW_LINES);
38    }
39
40    /**
41     * Warn about dirty unlocks
42     *
43     * @inheritdoc
44     */
45    public function unlock()
46    {
47        if ($this->isDirty()) {
48            throw new IndexUsageException('MemoryIndex unlocked in dirty state - forgot to call save()?');
49        }
50        parent::unlock();
51    }
52
53    /**
54     * @inheritdoc
55     * @throws IndexLockException
56     */
57    public function changeRow($rid, $value)
58    {
59        if (!$this->isWritable) throw new IndexLockException();
60
61        if ($rid > count($this->data)) {
62            $this->data = array_pad($this->data, $rid, '');
63        }
64        $this->data[$rid] = $value;
65        $this->dirty = true;
66    }
67
68    /**
69     * @inheritdoc
70     * @throws IndexLockException
71     */
72    public function retrieveRow($rid)
73    {
74        if (isset($this->data[$rid])) {
75            return $this->data[$rid];
76        }
77        if ($this->isWritable) {
78            $this->changeRow($rid, ''); // add to index
79        }
80        return '';
81    }
82
83    /** @inheritdoc */
84    public function retrieveRows($rids)
85    {
86        $result = [];
87        foreach ($rids as $rid) {
88            if (isset($this->data[$rid])) $result[$rid] = $this->data[$rid];
89        }
90
91        return $result;
92    }
93
94    /** @inheritdoc */
95    public function getRowIDs($values)
96    {
97        $values = array_map('trim', $values);
98        $values = array_fill_keys($values, 1); // easier access as associative array
99
100        $result = [];
101        $count = count($this->data);
102        for ($ln = 0; $ln < $count; $ln++) {
103            $line = $this->data[$ln];
104            if (isset($values[$line])) {
105                $result[$line] = $ln;
106                unset($values[$line]);
107            }
108        }
109
110        if (!$this->isWritable) return $result;
111
112        // if there are still values, they have not been found and will be appended
113        foreach (array_keys($values) as $value) {
114            $this->data[] = $value;
115            $result[$value] = $ln++;
116            $this->dirty = true;
117        }
118
119        return $result;
120    }
121
122    /** @inheritdoc */
123    public function search($re)
124    {
125        return preg_grep($re, $this->data);
126    }
127
128    /**
129     * Save the changed index back to its file
130     *
131     * The method will check the internal dirty state and will only write when the index has actually been changed
132     *
133     * @throws IndexWriteException
134     * @throws IndexLockException
135     */
136    public function save()
137    {
138        global $conf;
139
140        if (!$this->isDirty()) {
141            return;
142        }
143
144        if (!$this->isWritable) throw new IndexLockException();
145
146        $tempname = $this->filename . '.tmp';
147
148        $fh = @fopen($tempname, 'w');
149        if (!$fh) {
150            throw new IndexWriteException("Failed to write $tempname");
151        }
152        fwrite($fh, implode("\n", $this->data));
153        if (count($this->data)) {
154            fwrite($fh, "\n");
155        }
156        fclose($fh);
157
158        if ($conf['fperm']) {
159            chmod($tempname, $conf['fperm']);
160        }
161
162        if (!io_rename($tempname, $this->filename)) {
163            throw new IndexWriteException("Failed to write {$this->filename}");
164        }
165
166        $this->dirty = false;
167    }
168
169    /**
170     * Check if the index has been modified and needs to be saved
171     * @return bool
172     */
173    public function isDirty()
174    {
175        return $this->dirty;
176    }
177
178    /** @inheritdoc */
179    public function count(): int
180    {
181        return count($this->data);
182    }
183
184    /** @inheritdoc */
185    public function getIterator(): \ArrayIterator
186    {
187        return new \ArrayIterator($this->data);
188    }
189}
190