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