1 /** @file profiles.cpp  Abstract set of persistent profiles.
2  *
3  * @authors Copyright (c) 2016-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4  *
5  * @par License
6  * LGPL: http://www.gnu.org/licenses/lgpl.html
7  *
8  * <small>This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU Lesser General Public License as published by
10  * the Free Software Foundation; either version 3 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 Lesser
14  * General Public License for more details. You should have received a copy of
15  * the GNU Lesser General Public License along with this program; if not, see:
16  * http://www.gnu.org/licenses</small>
17  */
18 
19 #include "de/Profiles"
20 #include "de/App"
21 #include "de/FileSystem"
22 #include "de/Folder"
23 #include "de/String"
24 
25 #include <QTextStream>
26 
27 namespace de {
28 
nameToKey(String const & name)29 static String nameToKey(String const &name)
30 {
31     return name.toLower();
32 }
33 
DENG2_PIMPL(Profiles)34 DENG2_PIMPL(Profiles)
35 , DENG2_OBSERVES(Deletable, Deletion)
36 {
37     typedef QMap<String, AbstractProfile *> Profiles;
38     Profiles profiles;
39     String persistentName;
40 
41     Impl(Public *i) : Base(i)
42     {}
43 
44     ~Impl()
45     {
46         clear();
47     }
48 
49     void add(AbstractProfile *profile)
50     {
51         String const key = nameToKey(profile->name());
52         if (profiles.contains(nameToKey(key)))
53         {
54             delete profiles[key];
55         }
56         profiles.insert(key, profile);
57         profile->setOwner(thisPublic);
58         profile->audienceForDeletion += this;
59 
60         DENG2_FOR_PUBLIC_AUDIENCE2(Addition, i)
61         {
62             i->profileAdded(*profile);
63         }
64     }
65 
66     void remove(AbstractProfile &profile)
67     {
68         profile.audienceForDeletion -= this;
69         profile.setOwner(nullptr);
70         profiles.remove(nameToKey(profile.name()));
71 
72         DENG2_FOR_PUBLIC_AUDIENCE2(Removal, i)
73         {
74             i->profileRemoved(profile);
75         }
76     }
77 
78     void changeLookupKey(AbstractProfile const &profile, String const &newName)
79     {
80         profiles.remove(nameToKey(profile.name()));
81         profiles.insert(nameToKey(newName), const_cast<AbstractProfile *>(&profile));
82     }
83 
84     void objectWasDeleted(Deletable *obj)
85     {
86         // At this point the AbstractProfile itself is already deleted.
87         QMutableMapIterator<String, AbstractProfile *> iter(profiles);
88         while (iter.hasNext())
89         {
90             iter.next();
91             if (iter.value() == obj)
92             {
93                 iter.remove();
94                 break;
95             }
96         }
97     }
98 
99     void clear()
100     {
101         for (auto *prof : profiles)
102         {
103             prof->audienceForDeletion -= this;
104             prof->setOwner(nullptr);
105         }
106         qDeleteAll(profiles.values());
107         profiles.clear();
108     }
109 
110     /**
111      * For persistent profiles, determines the file name of the Info file
112      * where all the profile contents are written to and read from.
113      */
114     String fileName() const
115     {
116         if (persistentName.isEmpty()) return "";
117         return String("/home/configs/%1.dei").arg(persistentName);
118     }
119 
120     void loadProfilesFromInfo(File const &file, bool markReadOnly)
121     {
122         try
123         {
124             LOG_RES_VERBOSE("Reading profiles from %s") << file.description();
125 
126             Block raw;
127             file >> raw;
128 
129             de::Info info;
130             info.parse(String::fromUtf8(raw));
131 
132             foreach (de::Info::Element const *elem, info.root().contentsInOrder())
133             {
134                 if (!elem->isBlock()) continue;
135 
136                 // There may be multiple profiles in the file.
137                 de::Info::BlockElement const &profBlock = elem->as<de::Info::BlockElement>();
138                 if (profBlock.blockType() == "group" &&
139                     profBlock.name()      == "profile")
140                 {
141                     String profileName = profBlock.keyValue("name").text;
142                     if (profileName.isEmpty()) continue; // Name is required.
143 
144                     LOG_VERBOSE("Reading profile '%s'") << profileName;
145 
146                     auto *prof = self().profileFromInfoBlock(profBlock);
147                     prof->setName(profileName);
148                     prof->setReadOnly(markReadOnly);
149                     add(prof);
150                 }
151             }
152         }
153         catch (Error const &er)
154         {
155             LOG_RES_WARNING("Failed to load profiles from %s:\n%s")
156                     << file.description() << er.asText();
157         }
158     }
159     DENG2_PIMPL_AUDIENCE(Addition)
160     DENG2_PIMPL_AUDIENCE(Removal)
161 };
162 
DENG2_AUDIENCE_METHOD(Profiles,Addition)163 DENG2_AUDIENCE_METHOD(Profiles, Addition)
164 DENG2_AUDIENCE_METHOD(Profiles, Removal)
165 
166 Profiles::Profiles()
167     : d(new Impl(this))
168 {}
169 
~Profiles()170 Profiles::~Profiles()
171 {}
172 
profiles() const173 StringList Profiles::profiles() const
174 {
175     StringList names;
176     for (auto const *p : d->profiles.values()) names << p->name();
177     return names;
178 }
179 
count() const180 int Profiles::count() const
181 {
182     return d->profiles.size();
183 }
184 
tryFind(String const & name) const185 Profiles::AbstractProfile *Profiles::tryFind(String const &name) const
186 {
187     auto found = d->profiles.constFind(nameToKey(name));
188     if (found != d->profiles.constEnd())
189     {
190         return found.value();
191     }
192     return nullptr;
193 }
194 
find(String const & name) const195 Profiles::AbstractProfile &Profiles::find(String const &name) const
196 {
197     if (auto *p = tryFind(name))
198     {
199         return *p;
200     }
201     throw NotFoundError("Profiles::find", "Profile '" + name + "' not found");
202 }
203 
setPersistentName(String const & name)204 void Profiles::setPersistentName(String const &name)
205 {
206     d->persistentName = name;
207 }
208 
persistentName() const209 String Profiles::persistentName() const
210 {
211     return d->persistentName;
212 }
213 
isPersistent() const214 bool Profiles::isPersistent() const
215 {
216     return !d->persistentName.isEmpty();
217 }
218 
forAll(std::function<LoopResult (AbstractProfile &)> func) const219 LoopResult Profiles::forAll(std::function<LoopResult (AbstractProfile &)> func) const
220 {
221     foreach (AbstractProfile *prof, d->profiles.values())
222     {
223         if (auto result = func(*prof))
224         {
225             return result;
226         }
227     }
228     return LoopContinue;
229 }
230 
clear()231 void Profiles::clear()
232 {
233     d->clear();
234 }
235 
add(AbstractProfile * profile)236 void Profiles::add(AbstractProfile *profile)
237 {
238     d->add(profile);
239 }
240 
remove(AbstractProfile & profile)241 void Profiles::remove(AbstractProfile &profile)
242 {
243     DENG2_ASSERT(&profile.owner() == this);
244 
245     d->remove(profile);
246 }
247 
rename(AbstractProfile const & profile,String const & newName)248 bool Profiles::rename(AbstractProfile const &profile, String const &newName)
249 {
250     if (newName.isEmpty() || tryFind(newName)) return false;
251     d->changeLookupKey(profile, newName);
252     return true;
253 }
254 
serialize() const255 void Profiles::serialize() const
256 {
257     if (!isPersistent()) return;
258 
259     LOG_AS("Profiles");
260     LOGDEV_VERBOSE("Serializing %s profiles") << d->persistentName;
261 
262     // We will write one Info file with all the profiles.
263     String info;
264     QTextStream os(&info);
265     os.setCodec("UTF-8");
266 
267     os << "# Autogenerated Info file based on " << d->persistentName
268        << " profiles\n";
269 
270     // Write /home/configs/(persistentName).dei with all non-readonly profiles.
271     int count = 0;
272     for (auto *prof : d->profiles)
273     {
274         if (prof->isReadOnly()) continue;
275 
276         os << "\nprofile {\n"
277               "    name: " << prof->name() << "\n";
278         for (auto line : prof->toInfoSource().split('\n'))
279         {
280             os << "    " << line << "\n";
281         }
282         os << "}\n";
283         ++count;
284     }
285 
286     // Create the pack and update the file system.
287     File &outFile = App::rootFolder().replaceFile(d->fileName());
288     outFile << info.toUtf8();
289     outFile.flush(); // we're done
290 
291     LOG_VERBOSE("Wrote \"%s\" with %i profile%s")
292             << d->fileName() << count << (count != 1? "s" : "");
293 }
294 
deserialize()295 void Profiles::deserialize()
296 {
297     if (!isPersistent()) return;
298 
299     LOG_AS("Profiles");
300     LOGDEV_VERBOSE("Deserializing %s profiles") << d->persistentName;
301 
302     clear();
303 
304     // Read all fixed profiles from */profiles/(persistentName)/
305     FS::FoundFiles folders;
306     App::fileSystem().findAll("profiles" / d->persistentName, folders);
307     DENG2_FOR_EACH(FS::FoundFiles, i, folders)
308     {
309         if (auto const *folder = maybeAs<Folder>(*i))
310         {
311             // Let's see if it contains any .dei files.
312             folder->forContents([this] (String name, File &file)
313             {
314                 if (name.fileNameExtension() == ".dei")
315                 {
316                     // Load this profile.
317                     d->loadProfilesFromInfo(file, true /* read-only */);
318                 }
319                 return LoopContinue;
320             });
321         }
322     }
323 
324     // Read /home/configs/(persistentName).dei
325     if (File const *file = App::rootFolder().tryLocate<File const>(d->fileName()))
326     {
327         d->loadProfilesFromInfo(*file, false /* modifiable */);
328     }
329 }
330 
331 // Profiles::AbstractProfile --------------------------------------------------
332 
DENG2_PIMPL(Profiles::AbstractProfile)333 DENG2_PIMPL(Profiles::AbstractProfile)
334 {
335     Profiles *owner = nullptr;
336     String name;
337     bool readOnly = false;
338 
339     Impl(Public *i) : Base(i) {}
340 
341     ~Impl()
342     {
343         if (owner)
344         {
345             owner->remove(self());
346         }
347     }
348 
349     DENG2_PIMPL_AUDIENCE(Change)
350 };
351 
DENG2_AUDIENCE_METHOD(Profiles::AbstractProfile,Change)352 DENG2_AUDIENCE_METHOD(Profiles::AbstractProfile, Change)
353 
354 Profiles::AbstractProfile::AbstractProfile()
355     : d(new Impl(this))
356 {}
357 
AbstractProfile(AbstractProfile const & profile)358 Profiles::AbstractProfile::AbstractProfile(AbstractProfile const &profile)
359     : d(new Impl(this))
360 {
361     d->name     = profile.name();
362     d->readOnly = profile.isReadOnly();
363 }
364 
~AbstractProfile()365 Profiles::AbstractProfile::~AbstractProfile()
366 {}
367 
operator =(AbstractProfile const & other)368 Profiles::AbstractProfile &Profiles::AbstractProfile::operator = (AbstractProfile const &other)
369 {
370     d->name     = other.d->name;
371     d->readOnly = other.d->readOnly;
372     // owner is not copied
373     return *this;
374 }
375 
setOwner(Profiles * owner)376 void Profiles::AbstractProfile::setOwner(Profiles *owner)
377 {
378     DENG2_ASSERT(d->owner != owner);
379     d->owner = owner;
380 }
381 
owner()382 Profiles &Profiles::AbstractProfile::owner()
383 {
384     DENG2_ASSERT(d->owner);
385     return *d->owner;
386 }
387 
owner() const388 Profiles const &Profiles::AbstractProfile::owner() const
389 {
390     DENG2_ASSERT(d->owner);
391     return *d->owner;
392 }
393 
name() const394 String Profiles::AbstractProfile::name() const
395 {
396     return d->name;
397 }
398 
setName(String const & newName)399 bool Profiles::AbstractProfile::setName(String const &newName)
400 {
401     if (newName.isEmpty()) return false;
402 
403     Profiles *owner = d->owner;
404     if (!owner ||
405         !d->name.compareWithoutCase(newName) || // just a case change
406         owner->rename(*this, newName))
407     {
408         d->name = newName;
409         notifyChange();
410     }
411     return true;
412 }
413 
isReadOnly() const414 bool Profiles::AbstractProfile::isReadOnly() const
415 {
416     return d->readOnly;
417 }
418 
setReadOnly(bool readOnly)419 void Profiles::AbstractProfile::setReadOnly(bool readOnly)
420 {
421     d->readOnly = readOnly;
422 }
423 
notifyChange()424 void Profiles::AbstractProfile::notifyChange()
425 {
426     DENG2_FOR_AUDIENCE2(Change, i)
427     {
428         i->profileChanged(*this);
429     }
430 }
431 
432 } // namespace de
433