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