1 /** @file dedregister.h Register of definitions.
2 *
3 * @authors Copyright (c) 2014-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4 *
5 * @par License
6 * GPL: http://www.gnu.org/licenses/gpl.html
7 *
8 * <small>This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by the
10 * Free Software Foundation; either version 2 of the License, or (at your
11 * option) any later version. This program is distributed in the hope that it
12 * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
13 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
14 * Public License for more details. You should have received a copy of the GNU
15 * General Public License along with this program; if not, see:
16 * http://www.gnu.org/licenses</small>
17 */
18
19 #include "doomsday/defs/dedregister.h"
20 #include "doomsday/defs/definition.h"
21
22 #include <de/ArrayValue>
23 #include <de/DictionaryValue>
24 #include <de/TextValue>
25 #include <de/RecordValue>
26 #include <QSet>
27 #include <QMap>
28
29 using namespace de;
30
31 static String const VAR_ORDER = "order";
32
DENG2_PIMPL(DEDRegister)33 DENG2_PIMPL(DEDRegister)
34 , DENG2_OBSERVES(Record, Deletion)
35 , DENG2_OBSERVES(Record, Addition)
36 , DENG2_OBSERVES(Record, Removal)
37 , DENG2_OBSERVES(Variable, ChangeFrom)
38 {
39 Record *names;
40 ArrayValue *orderArray;
41 struct Key {
42 LookupFlags flags;
43 Key(LookupFlags const &f = DefaultLookup) : flags(f) {}
44 };
45 typedef QMap<String, Key> Keys;
46 Keys keys;
47 QMap<Variable *, Record *> parents;
48
49 Impl(Public *i, Record &rec) : Base(i), names(&rec)
50 {
51 names->audienceForDeletion() += this;
52
53 // The definitions will be stored here in the original order.
54 orderArray = &names->addArray(VAR_ORDER).array();
55 }
56
57 void recordBeingDeleted(Record &DENG2_DEBUG_ONLY(record))
58 {
59 DENG2_ASSERT(names == &record);
60 names = nullptr;
61 orderArray = nullptr;
62 }
63
64 void clear()
65 {
66 // As a side-effect, the lookups will be cleared, too, as the members of
67 // each definition record are deleted.
68 order().clear();
69
70 #ifdef DENG2_DEBUG
71 DENG2_ASSERT(parents.isEmpty());
72 foreach (String const &key, keys.keys())
73 {
74 DENG2_ASSERT(lookup(key).size() == 0);
75 }
76 #endif
77 }
78
79 void addKey(String const &name, LookupFlags const &flags)
80 {
81 keys.insert(name, Key(flags));
82 names->addDictionary(name + "Lookup");
83 }
84
85 ArrayValue &order()
86 {
87 DENG2_ASSERT(orderArray != nullptr);
88 return *orderArray;
89 }
90
91 ArrayValue const &order() const
92 {
93 DENG2_ASSERT(orderArray != nullptr);
94 return *orderArray;
95 }
96
97 DictionaryValue &lookup(String const &keyName)
98 {
99 return (*names)[keyName + "Lookup"].value<DictionaryValue>();
100 }
101
102 DictionaryValue const &lookup(String const &keyName) const
103 {
104 return (*names)[keyName + "Lookup"].value<DictionaryValue>();
105 }
106
107 template <typename Type>
108 Type lookupOperation(String const &key, String value,
109 std::function<Type (DictionaryValue const &, String)> operation) const
110 {
111 auto foundKey = keys.constFind(key);
112 if (foundKey == keys.constEnd()) return 0;
113
114 if (!foundKey.value().flags.testFlag(CaseSensitive))
115 {
116 // Case insensitive lookup is done in lower case.
117 value = value.lower();
118 }
119
120 return operation(lookup(key), value);
121 }
122
123 Record const *tryFind(String const &key, String const &value) const
124 {
125 return lookupOperation<Record const *>(key, value,
126 [] (DictionaryValue const &lut, String v) -> Record const * {
127 TextValue const val(v);
128 auto i = lut.elements().find(DictionaryValue::ValueRef(&val));
129 if (i == lut.elements().end()) return 0; // Value not in dictionary.
130 return i->second->as<RecordValue>().record();
131 });
132 }
133
134 bool has(String const &key, String const &value) const
135 {
136 return lookupOperation<bool>(key, value, [] (DictionaryValue const &lut, String v) {
137 return lut.contains(TextValue(v));
138 });
139 }
140
141 Record &append()
142 {
143 Record *sub = new Record;
144
145 // Let each subrecord know their ordinal.
146 sub->set(defn::Definition::VAR_ORDER, int(order().size())).setReadOnly();
147
148 // Observe what goes into this record.
149 //sub->audienceForDeletion() += this;
150 sub->audienceForAddition() += this;
151 sub->audienceForRemoval() += this;
152
153 order().add(new RecordValue(sub, RecordValue::OwnsRecord));
154 return *sub;
155 }
156
157 bool isEmptyKeyValue(Value const &value) const
158 {
159 return is<TextValue>(value) && value.asText().isEmpty();
160 }
161
162 bool isValidKeyValue(Value const &value) const
163 {
164 // Empty strings are not indexable.
165 if (isEmptyKeyValue(value)) return false;
166 return true;
167 }
168
169 /// Returns @c true if the value was added.
170 bool addToLookup(String const &key, Value const &value, Record &def)
171 {
172 if (!isValidKeyValue(value))
173 return false;
174
175 String valText = value.asText();
176 DENG2_ASSERT(!valText.isEmpty());
177 DENG2_ASSERT(keys.contains(key));
178
179 if (!keys[key].flags.testFlag(CaseSensitive))
180 {
181 valText = valText.lower();
182 }
183
184 DictionaryValue &dict = lookup(key);
185
186 if (keys[key].flags.testFlag(OnlyFirst))
187 {
188 // Only index the first one that is found.
189 if (dict.contains(TextValue(valText))) return false;
190 }
191
192 // Index definition using its current value.
193 dict.add(new TextValue(valText), new RecordValue(&def));
194 return true;
195 }
196
197 bool removeFromLookup(String const &key, Value const &value, Record &def)
198 {
199 //qDebug() << "removeFromLookup" << key << value.asText() << &def;
200
201 if (!isValidKeyValue(value))
202 return false;
203
204 String valText = value.asText();
205 DENG2_ASSERT(!valText.isEmpty());
206 DENG2_ASSERT(keys.contains(key));
207
208 if (!keys[key].flags.testFlag(CaseSensitive))
209 {
210 valText = valText.lower();
211 }
212
213 DictionaryValue &dict = lookup(key);
214
215 // Remove from the index.
216 if (dict.contains(TextValue(valText)))
217 {
218 Value const &indexed = dict.element(TextValue(valText));
219 //qDebug() << " -" << indexed.as<RecordValue>().record() << &def;
220 Record const *indexedDef = indexed.as<RecordValue>().record();
221 if (!indexedDef || indexedDef == &def)
222 {
223 // This is the definition that was indexed using the key value.
224 // Let's remove it.
225 dict.remove(TextValue(valText));
226
227 /// @todo Should now index any other definitions with this key value;
228 /// needs to add a lookup of which other definitions have this value.
229 return true;
230 }
231 }
232
233 // There was some other definition indexed using this key.
234 return false;
235 }
236
237 void recordMemberAdded(Record &def, Variable &key)
238 {
239 // Keys must be observed so that they are indexed in the lookup table.
240 if (keys.contains(key.name()))
241 {
242 // Index definition using its current value.
243 // Observe empty keys so we'll get the key's value when it's set.
244 if (addToLookup(key.name(), key.value(), def) ||
245 isEmptyKeyValue(key.value()))
246 {
247 parents.insert(&key, &def);
248 key.audienceForChangeFrom() += this;
249 }
250 }
251 }
252
253 void recordMemberRemoved(Record &def, Variable &key)
254 {
255 if (keys.contains(key.name()))
256 {
257 key.audienceForChangeFrom() -= this;
258 parents.remove(&key);
259 removeFromLookup(key.name(), key.value(), def);
260 }
261 }
262
263 void variableValueChangedFrom(Variable &key, Value const &oldValue, Value const &newValue)
264 {
265 //qDebug() << "changed" << key.name() << "from" << oldValue.asText() << "to"
266 // << newValue.asText();
267
268 DENG2_ASSERT(parents.contains(&key));
269
270 // The value of a key has changed, so it needs to be reindexed.
271 removeFromLookup(key.name(), oldValue, *parents[&key]);
272 addToLookup(key.name(), newValue, *parents[&key]);
273 }
274 };
275
DEDRegister(Record & names)276 DEDRegister::DEDRegister(Record &names) : d(new Impl(this, names))
277 {}
278
addLookupKey(String const & variableName,LookupFlags const & flags)279 void DEDRegister::addLookupKey(String const &variableName, LookupFlags const &flags)
280 {
281 d->addKey(variableName, flags);
282 }
283
clear()284 void DEDRegister::clear()
285 {
286 d->clear();
287 }
288
append()289 Record &DEDRegister::append()
290 {
291 return d->append();
292 }
293
appendCopy(int index)294 Record &DEDRegister::appendCopy(int index)
295 {
296 return copy(index, append());
297 }
298
copy(int fromIndex,Record & to)299 Record &DEDRegister::copy(int fromIndex, Record &to)
300 {
301 QStringList omitted;
302 omitted << "__.*"; // double-underscore
303
304 // By default lookup keys are not copied, as they are used as identifiers and
305 // therefore duplicates should not occur.
306 DENG2_FOR_EACH_CONST(Impl::Keys, i, d->keys)
307 {
308 if (i.value().flags.testFlag(AllowCopy)) continue;
309 omitted << i.key();
310 }
311
312 return to.assign((*this)[fromIndex], QRegExp(omitted.join("|")));
313 }
314
size() const315 int DEDRegister::size() const
316 {
317 return d->order().size();
318 }
319
has(String const & key,String const & value) const320 bool DEDRegister::has(String const &key, String const &value) const
321 {
322 return d->has(key, value);
323 }
324
operator [](int index)325 Record &DEDRegister::operator [] (int index)
326 {
327 return *d->order().at(index).as<RecordValue>().record();
328 }
329
operator [](int index) const330 Record const &DEDRegister::operator [] (int index) const
331 {
332 return *d->order().at(index).as<RecordValue>().record();
333 }
334
tryFind(String const & key,String const & value)335 Record *DEDRegister::tryFind(String const &key, String const &value)
336 {
337 return const_cast<Record *>(d->tryFind(key, value));
338 }
339
tryFind(String const & key,String value) const340 Record const *DEDRegister::tryFind(String const &key, String value) const
341 {
342 return d->tryFind(key, value);
343 }
344
find(String const & key,String const & value)345 Record &DEDRegister::find(String const &key, String const &value)
346 {
347 return const_cast<Record &>(const_cast<DEDRegister const *>(this)->find(key, value));
348 }
349
find(String const & key,String const & value) const350 Record const &DEDRegister::find(String const &key, String const &value) const
351 {
352 if (!d->keys.contains(key))
353 {
354 throw UndefinedKeyError("DEDRegister::find", "Key '" + key + "' not defined");
355 }
356 Record const *rec = tryFind(key, value);
357 if (!rec)
358 {
359 throw NotFoundError("DEDRegister::find", key + " '" + value + "' not found");
360 }
361 return *rec;
362 }
363
lookup(String const & key) const364 DictionaryValue const &DEDRegister::lookup(String const &key) const
365 {
366 if (!d->keys.contains(key))
367 {
368 throw UndefinedKeyError("DEDRegister::lookup", "Key '" + key + "' not defined");
369 }
370 return d->lookup(key);
371 }
372