xref: /dokuwiki/inc/Search/Index/MemoryIndex.php (revision 7fcedc39d164da3013a88e9ad6660009940e851b)
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    /**
42     * Warn developer when they forgot to save their changes
43     */
44    public function __destruct()
45    {
46        if ($this->isDirty()) {
47            throw new IndexUsageException('MemoryIndex destroyed in dirty state - forgot to call save()?');
48        }
49    }
50
51    /**
52     * @inheritdoc
53     * @throws IndexLockException
54     */
55    public function changeRow($rid, $value)
56    {
57        if (!$this->isWritable) throw new IndexLockException();
58
59        if ($rid > count($this->data)) {
60            $this->data = array_pad($this->data, $rid, '');
61        }
62        $this->data[$rid] = $value;
63        $this->dirty = true;
64    }
65
66    /**
67     * @inheritdoc
68     * @throws IndexLockException
69     */
70    public function retrieveRow($rid)
71    {
72        if (isset($this->data[$rid])) {
73            return $this->data[$rid];
74        }
75        if ($this->isWritable) {
76            $this->changeRow($rid, ''); // add to index
77        }
78        return '';
79    }
80
81    /** @inheritdoc */
82    public function retrieveRows($rids)
83    {
84        $result = [];
85        foreach ($rids as $rid) {
86            if (isset($this->data[$rid])) $result[$rid] = $this->data[$rid];
87        }
88
89        return $result;
90    }
91
92    /** @inheritdoc */
93    public function getRowIDs($values)
94    {
95        $values = array_map('trim', $values);
96        $values = array_fill_keys($values, 1); // easier access as associative array
97
98        $result = [];
99        $count = count($this->data);
100        for ($ln = 0; $ln < $count; $ln++) {
101            $line = $this->data[$ln];
102            if (isset($values[$line])) {
103                $result[$line] = $ln;
104                unset($values[$line]);
105            }
106        }
107
108        if (!$this->isWritable) return $result;
109
110        // if there are still values, they have not been found and will be appended
111        foreach (array_keys($values) as $value) {
112            $this->data[] = $value;
113            $result[$value] = $ln++;
114            $this->dirty = true;
115        }
116
117        return $result;
118    }
119
120    /** @inheritdoc */
121    public function search($re)
122    {
123        return preg_grep($re, $this->data);
124    }
125
126    /**
127     * Save the changed index back to its file
128     *
129     * The method will check the internal dirty state and will only write when the index has actually been changed
130     *
131     * @throws IndexWriteException
132     * @throws IndexLockException
133     */
134    public function save()
135    {
136        global $conf;
137
138        if (!$this->isDirty()) {
139            return;
140        }
141
142        if (!$this->isWritable) throw new IndexLockException();
143
144        $tempname = $this->filename . '.tmp';
145
146        $fh = @fopen($tempname, 'w');
147        if (!$fh) {
148            throw new IndexWriteException("Failed to write $tempname");
149        }
150        fwrite($fh, implode("\n", $this->data));
151        if (count($this->data)) {
152            fwrite($fh, "\n");
153        }
154        fclose($fh);
155
156        if ($conf['fperm']) {
157            chmod($tempname, $conf['fperm']);
158        }
159
160        if (!io_rename($tempname, $this->filename)) {
161            throw new IndexWriteException("Failed to write {$this->filename}");
162        }
163
164        $this->dirty = false;
165    }
166
167    /**
168     * Check if the index has been modified and needs to be saved
169     * @return bool
170     */
171    public function isDirty()
172    {
173        return $this->dirty;
174    }
175}
176