1 /*
2     SPDX-FileCopyrightText: 2019 Andreas Cord-Landwehr <cordlandwehr@kde.org>
3 
4     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
5 */
6 
7 #include "editablecourseresource.h"
8 #include "artikulate_debug.h"
9 #include "core/phoneme.h"
10 #include "core/phrase.h"
11 #include "core/unit.h"
12 #include "courseparser.h"
13 #include <KLocalizedString>
14 #include <KTar>
15 #include <QDir>
16 #include <QDomDocument>
17 #include <QFile>
18 #include <QFileInfo>
19 #include <QQmlEngine>
20 #include <QUuid>
21 
EditableCourseResource(const QUrl & path,IResourceRepository * repository)22 EditableCourseResource::EditableCourseResource(const QUrl &path, IResourceRepository *repository)
23     : IEditableCourse()
24     , m_course(new CourseResource(path, repository, false))
25 {
26     QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
27 
28     connect(m_course.get(), &ICourse::unitAboutToBeAdded, this, &ICourse::unitAboutToBeAdded);
29     connect(m_course.get(), &ICourse::unitAdded, this, &ICourse::unitAdded);
30     connect(m_course.get(), &CourseResource::idChanged, this, &EditableCourseResource::idChanged);
31     connect(m_course.get(), &CourseResource::foreignIdChanged, this, &EditableCourseResource::foreignIdChanged);
32     connect(m_course.get(), &CourseResource::titleChanged, this, &EditableCourseResource::titleChanged);
33     connect(m_course.get(), &CourseResource::descriptionChanged, this, &EditableCourseResource::descriptionChanged);
34     connect(m_course.get(), &CourseResource::languageChanged, this, &EditableCourseResource::languageChanged);
35 
36     for (auto &unit : m_course->units()) {
37         connect(unit.get(), &Unit::phrasesChanged, this, &IEditableCourse::unitChanged);
38         connect(unit.get(), &Unit::modified, this, &EditableCourseResource::markModified);
39     }
40 }
41 
create(const QUrl & path,IResourceRepository * repository)42 std::shared_ptr<EditableCourseResource> EditableCourseResource::create(const QUrl &path, IResourceRepository *repository)
43 {
44     std::shared_ptr<EditableCourseResource> course(new EditableCourseResource(path, repository));
45     course->setSelf(course);
46     return course;
47 }
48 
setSelf(std::shared_ptr<ICourse> self)49 void EditableCourseResource::setSelf(std::shared_ptr<ICourse> self)
50 {
51     m_course->setSelf(self);
52 }
53 
id() const54 QString EditableCourseResource::id() const
55 {
56     return m_course->id();
57 }
58 
setId(QString id)59 void EditableCourseResource::setId(QString id)
60 {
61     if (m_course->id() != id) {
62         m_course->setId(id);
63         m_modified = true;
64     }
65 }
66 
foreignId() const67 QString EditableCourseResource::foreignId() const
68 {
69     return m_course->foreignId();
70 }
71 
setForeignId(QString foreignId)72 void EditableCourseResource::setForeignId(QString foreignId)
73 {
74     m_course->setForeignId(std::move(foreignId));
75 }
76 
title() const77 QString EditableCourseResource::title() const
78 {
79     return m_course->title();
80 }
81 
setTitle(QString title)82 void EditableCourseResource::setTitle(QString title)
83 {
84     if (m_course->title() != title) {
85         m_course->setTitle(title);
86         m_modified = true;
87     }
88 }
89 
i18nTitle() const90 QString EditableCourseResource::i18nTitle() const
91 {
92     return m_course->i18nTitle();
93 }
94 
setI18nTitle(QString i18nTitle)95 void EditableCourseResource::setI18nTitle(QString i18nTitle)
96 {
97     if (m_course->i18nTitle() != i18nTitle) {
98         m_course->setI18nTitle(i18nTitle);
99         m_modified = true;
100     }
101 }
102 
description() const103 QString EditableCourseResource::description() const
104 {
105     return m_course->description();
106 }
107 
setDescription(QString description)108 void EditableCourseResource::setDescription(QString description)
109 {
110     if (m_course->description() != description) {
111         m_course->setDescription(description);
112         m_modified = true;
113     }
114 }
115 
language() const116 std::shared_ptr<ILanguage> EditableCourseResource::language() const
117 {
118     return m_course->language();
119 }
120 
languageTitle() const121 QString EditableCourseResource::languageTitle() const
122 {
123     return m_course->languageTitle();
124 }
125 
setLanguage(std::shared_ptr<ILanguage> language)126 void EditableCourseResource::setLanguage(std::shared_ptr<ILanguage> language)
127 {
128     if (m_course->language() != language) {
129         m_course->setLanguage(language);
130         m_modified = true;
131     }
132 }
133 
file() const134 QUrl EditableCourseResource::file() const
135 {
136     return m_course->file();
137 }
138 
self() const139 std::shared_ptr<IEditableCourse> EditableCourseResource::self() const
140 {
141     return std::static_pointer_cast<IEditableCourse>(m_course->self());
142 }
143 
sync()144 bool EditableCourseResource::sync()
145 {
146     Q_ASSERT(file().isValid());
147     Q_ASSERT(file().isLocalFile());
148     Q_ASSERT(!file().isEmpty());
149 
150     // not writing back if not modified
151     if (!m_modified) {
152         qCDebug(ARTIKULATE_LOG()) << "Aborting sync, course was not modified.";
153         return false;
154     }
155     bool ok = exportToFile(file());
156     if (ok) {
157         m_modified = false;
158     }
159     return ok;
160 }
161 
exportToFile(const QUrl & filePath) const162 bool EditableCourseResource::exportToFile(const QUrl &filePath) const
163 {
164     // write back to file
165     // create directories if necessary
166     QFileInfo info(filePath.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path());
167     if (!info.exists()) {
168         qCDebug(ARTIKULATE_LOG()) << "create xml output file directory, not existing";
169         QDir dir;
170         dir.mkpath(filePath.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path());
171     }
172 
173     // TODO port to KSaveFile
174     QFile file(filePath.toLocalFile());
175     if (!file.open(QIODevice::WriteOnly)) {
176         qCWarning(ARTIKULATE_LOG()) << "Unable to open file " << file.fileName() << " in write mode, aborting.";
177         return false;
178     }
179 
180     file.write(CourseParser::serializedDocument(self(), false).toByteArray());
181     return true;
182 }
183 
addUnit(std::shared_ptr<Unit> unit)184 std::shared_ptr<Unit> EditableCourseResource::addUnit(std::shared_ptr<Unit> unit)
185 {
186     m_modified = true;
187     m_course->addUnit(unit);
188     unit->setCourse(self());
189     connect(unit.get(), &Unit::phrasesChanged, this, &IEditableCourse::unitChanged);
190     return unit;
191 }
192 
units()193 QVector<std::shared_ptr<Unit>> EditableCourseResource::units()
194 {
195     if (!m_unitsLoaded) {
196         for (auto &unit : m_course->units()) {
197             unit->setCourse(self());
198         }
199         m_unitsLoaded = true;
200     }
201     return m_course->units();
202 }
203 
updateFrom(std::shared_ptr<ICourse> skeleton)204 void EditableCourseResource::updateFrom(std::shared_ptr<ICourse> skeleton)
205 {
206     for (auto skeletonUnit : skeleton->units()) {
207         // find matching unit or create one
208         std::shared_ptr<Unit> matchingUnit;
209         auto it = std::find_if(m_course->units().cbegin(), m_course->units().cend(), [skeletonUnit](std::shared_ptr<Unit> compareUnit) { return compareUnit->foreignId() == skeletonUnit->id(); });
210         if (it == m_course->units().cend()) {
211             // import complete unit
212             auto importUnit = Unit::create();
213             importUnit->setId(skeletonUnit->id());
214             importUnit->setForeignId(skeletonUnit->id());
215             importUnit->setTitle(skeletonUnit->title());
216             matchingUnit = m_course->addUnit(std::move(importUnit));
217         } else {
218             matchingUnit = *it;
219         }
220 
221         // import phrases
222         for (auto skeletonPhrase : skeletonUnit->phrases()) {
223             auto it = std::find_if(matchingUnit->phrases().cbegin(), matchingUnit->phrases().cend(), [skeletonPhrase](std::shared_ptr<IPhrase> comparePhrase) { return comparePhrase->foreignId() == skeletonPhrase->id(); });
224             if (it == matchingUnit->phrases().cend()) {
225                 // import complete Phrase
226                 std::shared_ptr<Phrase> importPhrase = Phrase::create();
227                 importPhrase->setId(skeletonPhrase->id());
228                 importPhrase->setForeignId(skeletonPhrase->id());
229                 importPhrase->setText(skeletonPhrase->text());
230                 importPhrase->seti18nText(skeletonPhrase->i18nText());
231                 importPhrase->setType(skeletonPhrase->type());
232                 importPhrase->setUnit(matchingUnit);
233                 matchingUnit->addPhrase(importPhrase, matchingUnit->phrases().size());
234             }
235         }
236     }
237 
238     qCInfo(ARTIKULATE_LOG()) << "Update performed!";
239 }
240 
isModified() const241 bool EditableCourseResource::isModified() const
242 {
243     return m_modified;
244 }
245 
createUnit()246 Unit *EditableCourseResource::createUnit()
247 {
248     // find first unused id
249     QStringList unitIds;
250     for (auto unit : m_course->units()) {
251         unitIds.append(unit->id());
252     }
253     QString id = QUuid::createUuid().toString();
254     while (unitIds.contains(id)) {
255         id = QUuid::createUuid().toString();
256         qCWarning(ARTIKULATE_LOG) << "Unit id generator has found a collision, recreating id.";
257     }
258 
259     // create unit
260     std::shared_ptr<Unit> unit = Unit::create();
261     unit->setCourse(self());
262     unit->setId(id);
263     unit->setTitle(i18n("New Unit"));
264     auto sharedUnit = addUnit(std::move(unit));
265 
266     return sharedUnit.get();
267 }
268 
createPhraseAfter(IPhrase * previousPhrase)269 bool EditableCourseResource::createPhraseAfter(IPhrase *previousPhrase)
270 {
271     std::shared_ptr<Unit> parentUnit = units().last();
272     if (previousPhrase) {
273         for (const auto &unit : units()) {
274             if (previousPhrase->unit()->id() == unit->id()) {
275                 parentUnit = unit;
276                 break;
277             }
278         }
279     }
280 
281     // find index
282     int index = parentUnit->phrases().size();
283     for (int i = 0; i < parentUnit->phrases().size(); ++i) {
284         if (parentUnit->phrases().at(i)->id() == previousPhrase->id()) {
285             index = i;
286             break;
287         }
288     }
289 
290     // find globally unique phrase id inside course
291     QStringList phraseIds;
292     for (auto unit : m_course->units()) {
293         for (auto &phrase : unit->phrases()) {
294             phraseIds.append(phrase->id());
295         }
296     }
297     QString id = QUuid::createUuid().toString();
298     while (phraseIds.contains(id)) {
299         id = QUuid::createUuid().toString();
300         qCWarning(ARTIKULATE_LOG) << "Phrase id generator has found a collision, recreating id.";
301     }
302 
303     // create unit
304     std::shared_ptr<Phrase> phrase = Phrase::create();
305     phrase->setId(id);
306     phrase->setText(QLatin1String(""));
307     phrase->setType(IPhrase::Type::Word);
308     parentUnit->addPhrase(phrase, index + 1);
309 
310     qCDebug(ARTIKULATE_CORE()) << "Created phrase at index" << index + 1;
311 
312     return true;
313 }
314 
deletePhrase(IPhrase * phrase)315 bool EditableCourseResource::deletePhrase(IPhrase *phrase)
316 {
317     Q_ASSERT(phrase);
318     if (!phrase) {
319         return false;
320     }
321     auto unitId = phrase->unit()->id();
322     for (auto &unit : units()) {
323         if (unit->id() == unitId) {
324             unit->removePhrase(phrase->self());
325             return true;
326         }
327     }
328     return false;
329 }
330 
markModified()331 void EditableCourseResource::markModified()
332 {
333     if (!m_modified) {
334         m_modified = true;
335         emit modifiedChanged(true);
336     }
337 }
338