1 /*
2  *  SPDX-FileCopyrightText: 2012-2015 Andreas Cord-Landwehr <cordlandwehr@kde.org>
3  *
4  *  SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5  */
6 
7 #include "project.h"
8 #include "libgraphtheory/graphdocument.h"
9 #include <KConfig>
10 #include <KConfigGroup>
11 #include <KLocalizedString>
12 #include <KMessageBox>
13 #include <KTextEditor/Document>
14 #include <KTextEditor/Editor>
15 #include <KTar>
16 #include <QUrl>
17 #include <QDir>
18 #include <QHash>
19 #include <QMap>
20 #include <QSaveFile>
21 #include <QTemporaryFile>
22 #include <QTemporaryDir>
23 #include <QJsonDocument>
24 #include <QJsonObject>
25 #include <QJsonArray>
26 #include <QDebug>
27 
28 using namespace GraphTheory;
29 
30 class ProjectPrivate
31 {
32 public:
ProjectPrivate()33     ProjectPrivate()
34         : m_journal(nullptr)
35         , m_graphEditor(nullptr)
36         , m_modified(false)
37         , m_activeGraphDocumentIndex(-1)
38         , m_activeCodeDocumentIndex(-1)
39     {
40 
41     }
42 
43     QUrl m_projectUrl; //!< the project's archive file
44     QTemporaryDir m_workingDirectory; //!< temporary directory where all project files are organized
45     QList<KTextEditor::Document*> m_codeDocuments;
46     QList<GraphDocumentPtr> m_graphDocuments;
47     QHash<KTextEditor::Document*,QString> m_documentNames;
48     KTextEditor::Document *m_journal;
49     GraphTheory::Editor *m_graphEditor;
50     bool m_modified;
51 
52     int m_activeGraphDocumentIndex;
53     int m_activeCodeDocumentIndex;
54 
55     /**
56      * Set project from project archive file.
57      */
58     bool loadProject(const QUrl &path);
59 
60     /**
61      * Write project meta info file
62      */
63     bool writeProjectMetaInfo();
64 };
65 
loadProject(const QUrl & url)66 bool ProjectPrivate::loadProject(const QUrl &url)
67 {
68     // extract archive file to temporary working directory
69     KTar tar = KTar(url.toLocalFile(), QStringLiteral("application/gzip"));
70     if (!tar.open(QIODevice::ReadOnly)) {
71         qCritical() << "Could not open project archive file for reading, aborting.";
72         return false;
73     }
74     tar.directory()->copyTo(m_workingDirectory.path(), true);
75     QFile metaInfoFile(m_workingDirectory.path() + QChar('/') + "project.json");
76     if (!metaInfoFile.open(QIODevice::ReadOnly)) {
77         qWarning("Could not open project.json file for reading, aborting.");
78         return false;
79     }
80     QJsonDocument metaInfoDoc = QJsonDocument::fromJson(metaInfoFile.readAll());
81 
82     // set project
83     QJsonObject metaInfo = metaInfoDoc.object();
84 
85     QJsonArray codeDocs = metaInfo["scripts"].toArray();
86     QJsonArray codeDocNames = metaInfo["scriptNames"].toArray();
87     for (int index = 0; index < codeDocs.count(); ++index) {
88         QJsonObject docInfo = codeDocs.at(index).toObject();
89         QString filename = docInfo["file"].toString();
90         KTextEditor::Document *document = KTextEditor::Editor::instance()->createDocument(nullptr);
91         document->openUrl(QUrl::fromLocalFile(m_workingDirectory.path() + QChar('/') + filename));
92         m_codeDocuments.append(document);
93         m_documentNames.insert(document, codeDocNames.at(index).toString());
94     }
95 
96     QJsonArray graphDocs = metaInfo["graphs"].toArray();
97     for (int index = 0; index < graphDocs.count(); ++index) {
98         QJsonObject docInfo = graphDocs.at(index).toObject();
99         QString fileName = docInfo["file"].toString();
100         QUrl fileUrl = QUrl::fromLocalFile(m_workingDirectory.path() + QChar('/') + fileName);
101         GraphDocumentPtr document = m_graphEditor->openDocument(fileUrl);
102         // add only to project, if document was loaded correctly
103         if (document) {
104             document->setDocumentName(docInfo["name"].toString());
105             m_graphDocuments.append(document);
106         }
107     }
108     m_journal = KTextEditor::Editor::instance()->createDocument(nullptr);
109     m_journal->openUrl(QUrl::fromLocalFile(m_workingDirectory.path() + QChar('/') + metaInfo["journal.txt"].toString()));
110     Q_ASSERT(m_journal != nullptr);
111 
112     //TODO save & load open document index
113 
114     return true;
115 }
116 
writeProjectMetaInfo()117 bool ProjectPrivate::writeProjectMetaInfo()
118 {
119     QJsonObject metaInfo;
120 
121     QJsonArray codeDocs, codeDocNames, graphDocs;
122     for (KTextEditor::Document *document : std::as_const(m_codeDocuments)) {
123         QJsonObject docInfo;
124         docInfo.insert("file", document->url().fileName());
125         codeDocs.append(docInfo);
126         codeDocNames.append(m_documentNames.value(document));
127     }
128     for (const GraphTheory::GraphDocumentPtr &document : std::as_const(m_graphDocuments)) {
129         QJsonObject docInfo;
130         docInfo.insert("file", document->documentUrl().fileName());
131         docInfo.insert("name", document->documentName());
132         graphDocs.append(docInfo);
133     }
134     metaInfo.insert("scripts", codeDocs);
135     metaInfo.insert("scriptNames", codeDocNames);
136     metaInfo.insert("graphs", graphDocs);
137     metaInfo.insert("journal", m_journal->url().fileName());
138 
139     // write to file
140     QFile metaInfoFile(m_workingDirectory.path() + QChar('/') + "project.json");
141     if (!metaInfoFile.open(QIODevice::WriteOnly)) {
142         qWarning("Couldn't open project.json file for writing, abort.");
143         return false;
144     }
145     QJsonDocument metaInfoDoc(metaInfo);
146     metaInfoFile.write(metaInfoDoc.toJson());
147 
148     return true;
149 }
150 
151 
152 //TODO make graphEditor singleton
Project(GraphTheory::Editor * graphEditor)153 Project::Project(GraphTheory::Editor *graphEditor)
154     : d(new ProjectPrivate)
155 {
156     d->m_graphEditor = graphEditor;
157     d->m_journal = KTextEditor::Editor::instance()->createDocument(nullptr);
158     d->m_journal->saveAs(QUrl::fromLocalFile(workingDir() + QChar('/') + QString("journal.txt")));
159 }
160 
161 //TODO make graphEditor singleton
Project(const QUrl & projectFile,GraphTheory::Editor * graphEditor)162 Project::Project(const QUrl &projectFile, GraphTheory::Editor *graphEditor)
163     : d(new ProjectPrivate)
164 {
165     d->m_graphEditor = graphEditor;
166     d->m_projectUrl = projectFile;
167     if (!d->loadProject(projectFile)) {
168         addGraphDocument(graphEditor->createDocument());
169         setModified(false);
170         d->m_journal = KTextEditor::Editor::instance()->createDocument(nullptr);
171 
172         KMessageBox::error(nullptr,
173                            i18nc("@info",
174                                  "The Rocs project could not be imported because the project file could not be parsed."));
175     }
176 
177     for (const auto &document : d->m_codeDocuments) {
178         connect(document, &KTextEditor::Document::modifiedChanged,
179             this, &Project::modifiedChanged);
180     }
181     for (const auto &document : d->m_graphDocuments) {
182         connect(document.data(), &GraphDocument::modifiedChanged,
183             this, &Project::modifiedChanged);
184     }
185 }
186 
~Project()187 Project::~Project()
188 {
189 }
190 
projectUrl() const191 QUrl Project::projectUrl() const
192 {
193     return d->m_projectUrl;
194 }
195 
setProjectUrl(const QUrl & url)196 void Project::setProjectUrl(const QUrl &url)
197 {
198     d->m_projectUrl = url;
199 }
200 
workingDir() const201 QString Project::workingDir() const
202 {
203     return d->m_workingDirectory.path();
204 }
205 
createCodeDocument(const QString & filePath)206 KTextEditor::Document* Project::createCodeDocument(const QString& filePath)
207 {
208     auto path = d->m_workingDirectory.path() + QLatin1Char('/') + filePath + QStringLiteral(".js");
209 
210     auto doc = KTextEditor::Editor::instance()->createDocument(nullptr);
211     if (!doc->saveAs(QUrl::fromLocalFile(path))) {
212         qCritical() << "Error when saving code file to working directory, aborting.";
213         return nullptr;
214     }
215 
216     if (!addCodeDocument(doc)) {
217         return nullptr;
218     }
219 
220     return doc;
221 }
222 
openCodeDocument(const QUrl & url)223 KTextEditor::Document* Project::openCodeDocument(const QUrl &url)
224 {
225     auto doc = KTextEditor::Editor::instance()->createDocument(nullptr);
226     if (!doc->openUrl(url)) {
227         qCritical() << "Error when opening code file, aborting.";
228         return nullptr;
229     }
230 
231     if (!addCodeDocument(doc)) {
232         return nullptr;
233     }
234 
235     return doc;
236 }
237 
addCodeDocument(KTextEditor::Document * document)238 bool Project::addCodeDocument(KTextEditor::Document *document)
239 {
240     if (document == nullptr) {
241         return false;
242     }
243 
244     Q_EMIT codeDocumentAboutToBeAdded(document, d->m_codeDocuments.count());
245     connect(document, &KTextEditor::Document::modifiedChanged,
246         this, &Project::modifiedChanged);
247     d->m_codeDocuments.append(document);
248     Q_EMIT codeDocumentAdded();
249     setModified(true);
250     return true;
251 }
252 
importCodeDocument(const QUrl & url)253 KTextEditor::Document * Project::importCodeDocument(const QUrl &url)
254 {
255     return openCodeDocument(url);
256 }
257 
tryToRemoveCodeDocument(KTextEditor::Document * document)258 void Project::tryToRemoveCodeDocument(KTextEditor::Document *document)
259 {
260     QString path = document->url().toString();
261     if(!document->closeUrl())
262         return;
263     int index = d->m_codeDocuments.indexOf(document);
264     Q_EMIT codeDocumentAboutToBeRemoved(index, index);
265     disconnect(document, &KTextEditor::Document::modifiedChanged,
266         this, &Project::modifiedChanged);
267     d->m_codeDocuments.removeAt(index);
268     Q_EMIT codeDocumentRemoved();
269     if (!path.startsWith(d->m_workingDirectory.path())) {
270         qCritical() << "Aborting removal of document: not in temporary working directory";
271         return;
272     }
273     if (!QFile::remove(path)) {
274         qCritical() << "Could not remove code file" << path;
275     }
276     d->m_documentNames.remove(document);
277     setModified(true);
278 }
279 
documentName(KTextEditor::Document * document) const280 QString Project::documentName(KTextEditor::Document *document) const
281 {
282     const QString docName = d->m_documentNames.value(document);
283     return !docName.isEmpty()
284         ? docName
285         : document->documentName();
286 }
287 
setDocumentName(KTextEditor::Document * document,const QString & name)288 void Project::setDocumentName(KTextEditor::Document *document, const QString &name)
289 {
290     d->m_documentNames.insert(document, name);
291     setModified(true);
292 }
293 
codeDocuments() const294 QList<KTextEditor::Document*> Project::codeDocuments() const
295 {
296     return d->m_codeDocuments;
297 }
298 
setActiveCodeDocument(int index)299 void Project::setActiveCodeDocument(int index)
300 {
301     if (index < 0 || index >= d->m_codeDocuments.count()) {
302         qCritical() << "Code document index invalid, aborting change of current document.";
303         return;
304     }
305     d->m_activeCodeDocumentIndex = index;
306     Q_EMIT activeCodeDocumentChanged(index);
307 }
308 
graphDocuments() const309 QList<GraphDocumentPtr> Project::graphDocuments() const
310 {
311     return d->m_graphDocuments;
312 }
313 
addGraphDocument(GraphDocumentPtr document)314 bool Project::addGraphDocument(GraphDocumentPtr document)
315 {
316     // compute first unused document path
317     QStringList usedFileNames;
318     for (const GraphTheory::GraphDocumentPtr &document : std::as_const(d->m_graphDocuments)) {
319         usedFileNames.append(document->documentUrl().fileName());
320     }
321     QString fileName;
322     for (int i = 0; i <= d->m_graphDocuments.count(); ++i) {
323         fileName = "graphfile" + QString::number(i) + QString(".graph2");
324         if (!usedFileNames.contains(fileName)) {
325             break;
326         }
327     }
328     QString path = d->m_workingDirectory.path()
329             + QChar('/')
330             + fileName;
331 
332     // put document into working directory
333     document->documentSaveAs(QUrl::fromLocalFile(path));
334     int index = d->m_graphDocuments.length();
335     Q_EMIT graphDocumentAboutToBeAdded(document, index);
336     connect(document.data(), &GraphDocument::modifiedChanged,
337         this, &Project::modifiedChanged);
338     d->m_graphDocuments.append(document);
339     Q_EMIT graphDocumentAdded();
340     setModified(true);
341 
342     if (d->m_activeGraphDocumentIndex < 0) {
343         setActiveGraphDocument(index);
344     }
345     return true;
346 }
347 
importGraphDocument(const QUrl & documentUrl)348 GraphTheory::GraphDocumentPtr Project::importGraphDocument(const QUrl &documentUrl)
349 {
350     Q_ASSERT(d->m_graphEditor);
351     GraphTheory::GraphDocumentPtr document = d->m_graphEditor->openDocument(documentUrl);
352     Q_ASSERT(document);
353     addGraphDocument(document);
354     return document;
355 }
356 
removeGraphDocument(GraphDocumentPtr document)357 void Project::removeGraphDocument(GraphDocumentPtr document)
358 {
359     QString path = document->documentUrl().toLocalFile();
360     d->m_graphDocuments.removeAll(document);
361     if (!path.startsWith(d->m_workingDirectory.path())) {
362         qCritical() << "Aborting removal of graph document with path "
363             << path
364             << ", not in temporary working directory" << d->m_workingDirectory.path();
365         return;
366     }
367     if (!QFile::remove(path)) {
368         qCritical() << "Could not remove graph file" << path;
369     }
370     int index = d->m_graphDocuments.indexOf(document);
371     Q_EMIT graphDocumentAboutToBeRemoved(index, index);
372     d->m_graphDocuments.removeAt(index);
373     Q_EMIT graphDocumentRemoved();
374     setModified(true);
375 }
376 
setActiveGraphDocument(int index)377 void Project::setActiveGraphDocument(int index)
378 {
379     if (index < 0 || index >= d->m_graphDocuments.count()) {
380         qCritical() << "Graph document index invalid, aborting change of current document.";
381         return;
382     }
383     d->m_activeGraphDocumentIndex = index;
384     Q_EMIT activeGraphDocumentChanged(index);
385     Q_EMIT activeGraphDocumentChanged(d->m_graphDocuments.at(index));
386 }
387 
activeGraphDocument() const388 GraphDocumentPtr Project::activeGraphDocument() const
389 {
390     if (d->m_activeGraphDocumentIndex < 0 || d->m_graphDocuments.count() <= d->m_activeGraphDocumentIndex) {
391         return GraphDocumentPtr();
392     }
393     return d->m_graphDocuments.at(d->m_activeGraphDocumentIndex);
394 }
395 
journalDocument() const396 KTextEditor::Document * Project::journalDocument() const
397 {
398     return d->m_journal;
399 }
400 
projectSave()401 bool Project::projectSave()
402 {
403     if (d->m_projectUrl.isEmpty()) {
404         qCritical() << "No project file specified, abort saving.";
405         return false;
406     }
407     KTar tar = KTar(d->m_projectUrl.toLocalFile(), QStringLiteral("application/gzip"));
408     tar.open(QIODevice::WriteOnly);
409 
410     for (KTextEditor::Document *document : std::as_const(d->m_codeDocuments)) {
411         document->save();
412         tar.addLocalFile(document->url().toLocalFile(), document->url().fileName());
413     }
414     for (const GraphTheory::GraphDocumentPtr &document : std::as_const(d->m_graphDocuments)) {
415         document->documentSave();
416         tar.addLocalFile(document->documentUrl().toLocalFile(), document->documentUrl().fileName());
417     }
418     tar.addLocalFile(d->m_journal->url().toLocalFile(), "journal.txt");
419     d->writeProjectMetaInfo();
420     tar.addLocalFile(d->m_workingDirectory.path() + QChar('/') + "project.json", "project.json");
421     tar.close();
422 
423     // update modified state
424     setModified(false);
425 
426     return true;
427 }
428 
projectSaveAs(const QUrl & url)429 bool Project::projectSaveAs(const QUrl &url)
430 {
431     d->m_projectUrl = url;
432     return projectSave();
433 }
434 
setModified(bool modified)435 void Project::setModified(bool modified)
436 {
437     if (modified == d->m_modified) {
438         return;
439     }
440     d->m_modified = modified;
441     Q_EMIT modifiedChanged();
442 }
443 
isModified() const444 bool Project::isModified() const
445 {
446     for (const GraphDocumentPtr &document : std::as_const(d->m_graphDocuments)) {
447         if (document->isModified()) {
448             return true;
449         }
450     }
451     for (KTextEditor::Document *document : std::as_const(d->m_codeDocuments)) {
452         if (document->isModified()) {
453             return true;
454         }
455     }
456     if (d->m_journal && d->m_journal->isModified()) {
457         return true;
458     }
459 
460     return d->m_modified;
461 }
462