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