1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 Jochen Becher
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25
26 #include "modelindexer.h"
27
28 #include "modeleditor_constants.h"
29
30 #include "qmt/infrastructure/exceptions.h"
31 #include "qmt/infrastructure/uid.h"
32
33 #include "qmt/serializer/projectserializer.h"
34
35 #include "qmt/project/project.h"
36 #include "qmt/model_controller/mvoidvisitor.h"
37 #include "qmt/model/mpackage.h"
38 #include "qmt/model/mdiagram.h"
39
40 #include "qmt/tasks/findrootdiagramvisitor.h"
41
42 #include <projectexplorer/project.h>
43 #include <projectexplorer/session.h>
44 #include <projectexplorer/projectnodes.h>
45
46 #include <utils/mimetypes/mimetype.h>
47 #include <utils/mimetypes/mimedatabase.h>
48 #include <utils/qtcassert.h>
49
50 #include <QQueue>
51 #include <QMutex>
52 #include <QMutexLocker>
53 #include <QThread>
54 #include <QDateTime>
55
56 #include <QLoggingCategory>
57 #include <QDebug>
58 #include <QPointer>
59
60 namespace ModelEditor {
61 namespace Internal {
62
63 class ModelIndexer::QueuedFile
64 {
65 friend uint qHash(const ModelIndexer::QueuedFile &queuedFile);
66 friend bool operator==(const ModelIndexer::QueuedFile &lhs,
67 const ModelIndexer::QueuedFile &rhs);
68
69 public:
70 QueuedFile() = default;
71
QueuedFile(const QString & file,ProjectExplorer::Project * project)72 QueuedFile(const QString &file, ProjectExplorer::Project *project)
73 : m_file(file),
74 m_project(project)
75 {
76 }
77
QueuedFile(const QString & file,ProjectExplorer::Project * project,const QDateTime & lastModified)78 QueuedFile(const QString &file, ProjectExplorer::Project *project,
79 const QDateTime &lastModified)
80 : m_file(file),
81 m_project(project),
82 m_lastModified(lastModified)
83 {
84 }
85
isValid() const86 bool isValid() const { return !m_file.isEmpty() && m_project; }
file() const87 QString file() const { return m_file; }
project() const88 ProjectExplorer::Project *project() const { return m_project; }
lastModified() const89 QDateTime lastModified() const { return m_lastModified; }
90
91 private:
92 QString m_file;
93 ProjectExplorer::Project *m_project = nullptr;
94 QDateTime m_lastModified;
95 };
96
operator ==(const ModelIndexer::QueuedFile & lhs,const ModelIndexer::QueuedFile & rhs)97 bool operator==(const ModelIndexer::QueuedFile &lhs, const ModelIndexer::QueuedFile &rhs)
98 {
99 return lhs.m_file == rhs.m_file && lhs.m_project == rhs.m_project;
100 }
101
qHash(const ModelIndexer::QueuedFile & queuedFile)102 uint qHash(const ModelIndexer::QueuedFile &queuedFile)
103 {
104 return qHash(queuedFile.m_project) + qHash(queuedFile.m_project);
105 }
106
107 class ModelIndexer::IndexedModel
108 {
109 public:
IndexedModel(const QString & modelFile,const QDateTime & lastModified)110 IndexedModel(const QString &modelFile, const QDateTime &lastModified)
111 : m_modelFile(modelFile),
112 m_lastModified(lastModified)
113 {
114 }
115
reset(const QDateTime & lastModified)116 void reset(const QDateTime &lastModified)
117 {
118 m_lastModified = lastModified;
119 m_modelUid = qmt::Uid::invalidUid();
120 m_diagrams.clear();
121 }
122
file() const123 QString file() const { return m_modelFile; }
lastModified() const124 QDateTime lastModified() const { return m_lastModified; }
owningProjects() const125 QSet<ProjectExplorer::Project *> owningProjects() const { return m_owningProjects; }
addOwningProject(ProjectExplorer::Project * project)126 void addOwningProject(ProjectExplorer::Project *project) { m_owningProjects.insert(project); }
removeOwningProject(ProjectExplorer::Project * project)127 void removeOwningProject(ProjectExplorer::Project *project)
128 {
129 m_owningProjects.remove(project);
130 }
modelUid() const131 qmt::Uid modelUid() const { return m_modelUid; }
setModelUid(const qmt::Uid & modelUid)132 void setModelUid(const qmt::Uid &modelUid) { m_modelUid = modelUid; }
diagrams() const133 QSet<qmt::Uid> diagrams() const { return m_diagrams; }
addDiagram(const qmt::Uid & diagram)134 void addDiagram(const qmt::Uid &diagram) { m_diagrams.insert(diagram); }
135
136 private:
137 QString m_modelFile;
138 QDateTime m_lastModified;
139 QSet<ProjectExplorer::Project *> m_owningProjects;
140 qmt::Uid m_modelUid;
141 QSet<qmt::Uid> m_diagrams;
142 };
143
144 class ModelIndexer::IndexedDiagramReference
145 {
146 public:
IndexedDiagramReference(const QString & file,const QDateTime & lastModified)147 IndexedDiagramReference(const QString &file, const QDateTime &lastModified)
148 : m_file(file),
149 m_lastModified(lastModified)
150 {
151 }
152
reset(const QDateTime & lastModified)153 void reset(const QDateTime &lastModified)
154 {
155 m_lastModified = lastModified;
156 m_modelUid = qmt::Uid::invalidUid();
157 m_diagramUid = qmt::Uid::invalidUid();
158 }
159
file() const160 QString file() const { return m_file; }
lastModified() const161 QDateTime lastModified() const { return m_lastModified; }
owningProjects() const162 QSet<ProjectExplorer::Project *> owningProjects() const { return m_owningProjects; }
addOwningProject(ProjectExplorer::Project * project)163 void addOwningProject(ProjectExplorer::Project *project) { m_owningProjects.insert(project); }
removeOwningProject(ProjectExplorer::Project * project)164 void removeOwningProject(ProjectExplorer::Project *project)
165 {
166 m_owningProjects.remove(project);
167 }
modelUid() const168 qmt::Uid modelUid() const { return m_modelUid; }
setModelUid(const qmt::Uid & modelUid)169 void setModelUid(const qmt::Uid &modelUid) { m_modelUid = modelUid; }
diagramUid() const170 qmt::Uid diagramUid() const { return m_diagramUid; }
setDiagramUid(const qmt::Uid & diagramUid)171 void setDiagramUid(const qmt::Uid &diagramUid) { m_diagramUid = diagramUid; }
172
173 private:
174 QString m_file;
175 QDateTime m_lastModified;
176 QSet<ProjectExplorer::Project *> m_owningProjects;
177 qmt::Uid m_modelUid;
178 qmt::Uid m_diagramUid;
179 };
180
181 class ModelIndexer::IndexerThread :
182 public QThread
183 {
184 public:
IndexerThread(ModelIndexer * indexer)185 IndexerThread(ModelIndexer *indexer)
186 : QThread(),
187 m_indexer(indexer)
188 {
189 }
190
191 void onQuitIndexerThread();
192 void onFilesQueued();
193
194 private:
195 ModelIndexer *m_indexer;
196 };
197
198 class ModelIndexer::DiagramsCollectorVisitor :
199 public qmt::MVoidConstVisitor
200 {
201 public:
202 DiagramsCollectorVisitor(ModelIndexer::IndexedModel *indexedModel);
203
204 void visitMObject(const qmt::MObject *object) final;
205 void visitMDiagram(const qmt::MDiagram *diagram) final;
206
207 private:
208 ModelIndexer::IndexedModel *m_indexedModel;
209 };
210
DiagramsCollectorVisitor(IndexedModel * indexedModel)211 ModelIndexer::DiagramsCollectorVisitor::DiagramsCollectorVisitor(IndexedModel *indexedModel)
212 : qmt::MVoidConstVisitor(),
213 m_indexedModel(indexedModel)
214 {
215 }
216
visitMObject(const qmt::MObject * object)217 void ModelIndexer::DiagramsCollectorVisitor::visitMObject(const qmt::MObject *object)
218 {
219 for (const qmt::Handle<qmt::MObject> &child : object->children()) {
220 if (child.hasTarget())
221 child.target()->accept(this);
222 }
223 visitMElement(object);
224 }
225
visitMDiagram(const qmt::MDiagram * diagram)226 void ModelIndexer::DiagramsCollectorVisitor::visitMDiagram(const qmt::MDiagram *diagram)
227 {
228 qCDebug(logger) << "add diagram " << diagram->name() << " to index";
229 m_indexedModel->addDiagram(diagram->uid());
230 visitMObject(diagram);
231 }
232
233 class ModelIndexer::ModelIndexerPrivate
234 {
235 public:
~ModelIndexerPrivate()236 ~ModelIndexerPrivate()
237 {
238 QMT_CHECK(filesQueue.isEmpty());
239 QMT_CHECK(queuedFilesSet.isEmpty());
240 QMT_CHECK(indexedModels.isEmpty());
241 QMT_CHECK(indexedModelsByUid.isEmpty());
242 QMT_CHECK(indexedDiagramReferences.isEmpty());
243 QMT_CHECK(indexedDiagramReferencesByDiagramUid.isEmpty());
244 delete indexerThread;
245 }
246
247 QMutex indexerMutex;
248
249 QQueue<ModelIndexer::QueuedFile> filesQueue;
250 QSet<ModelIndexer::QueuedFile> queuedFilesSet;
251 QSet<ModelIndexer::QueuedFile> defaultModelFiles;
252
253 QHash<QString, ModelIndexer::IndexedModel *> indexedModels;
254 QHash<qmt::Uid, QSet<ModelIndexer::IndexedModel *> > indexedModelsByUid;
255
256 QHash<QString, ModelIndexer::IndexedDiagramReference *> indexedDiagramReferences;
257 QHash<qmt::Uid, QSet<ModelIndexer::IndexedDiagramReference *> > indexedDiagramReferencesByDiagramUid;
258
259 ModelIndexer::IndexerThread *indexerThread = nullptr;
260 };
261
onQuitIndexerThread()262 void ModelIndexer::IndexerThread::onQuitIndexerThread()
263 {
264 QThread::exit(0);
265 }
266
onFilesQueued()267 void ModelIndexer::IndexerThread::onFilesQueued()
268 {
269 QMutexLocker locker(&m_indexer->d->indexerMutex);
270
271 while (!m_indexer->d->filesQueue.isEmpty()) {
272 ModelIndexer::QueuedFile queuedFile = m_indexer->d->filesQueue.takeFirst();
273 m_indexer->d->queuedFilesSet.remove(queuedFile);
274 qCDebug(logger) << "handle queued file " << queuedFile.file()
275 << "from project " << queuedFile.project()->displayName();
276
277 bool scanModel = false;
278 IndexedModel *indexedModel = m_indexer->d->indexedModels.value(queuedFile.file());
279 if (!indexedModel) {
280 qCDebug(logger) << "create new indexed model";
281 indexedModel = new IndexedModel(queuedFile.file(),
282 queuedFile.lastModified());
283 indexedModel->addOwningProject(queuedFile.project());
284 m_indexer->d->indexedModels.insert(queuedFile.file(), indexedModel);
285 scanModel = true;
286 } else if (queuedFile.lastModified() > indexedModel->lastModified()) {
287 qCDebug(logger) << "update indexed model";
288 indexedModel->addOwningProject(queuedFile.project());
289 indexedModel->reset(queuedFile.lastModified());
290 scanModel = true;
291 }
292 if (scanModel) {
293 locker.unlock();
294 // load model file
295 qmt::ProjectSerializer projectSerializer;
296 qmt::Project project;
297 try {
298 projectSerializer.load(queuedFile.file(), &project);
299 } catch (const qmt::Exception &e) {
300 qWarning() << e.errorMessage();
301 return;
302 }
303 locker.relock();
304 indexedModel->setModelUid(project.uid());
305 // add indexedModel to set of indexedModelsByUid
306 QSet<IndexedModel *> indexedModels = m_indexer->d->indexedModelsByUid.value(project.uid());
307 indexedModels.insert(indexedModel);
308 m_indexer->d->indexedModelsByUid.insert(project.uid(), indexedModels);
309 // collect all diagrams of model
310 DiagramsCollectorVisitor visitor(indexedModel);
311 project.rootPackage()->accept(&visitor);
312 if (m_indexer->d->defaultModelFiles.contains(queuedFile)) {
313 m_indexer->d->defaultModelFiles.remove(queuedFile);
314 // check if model has a diagram which could be opened
315 qmt::FindRootDiagramVisitor diagramVisitor;
316 project.rootPackage()->accept(&diagramVisitor);
317 if (diagramVisitor.diagram())
318 emit m_indexer->openDefaultModel(project.uid());
319 }
320 }
321 }
322 }
323
ModelIndexer(QObject * parent)324 ModelIndexer::ModelIndexer(QObject *parent)
325 : QObject(parent),
326 d(new ModelIndexerPrivate())
327 {
328 d->indexerThread = new IndexerThread(this);
329 connect(this, &ModelIndexer::quitIndexerThread,
330 d->indexerThread, &ModelIndexer::IndexerThread::onQuitIndexerThread);
331 connect(this, &ModelIndexer::filesQueued,
332 d->indexerThread, &ModelIndexer::IndexerThread::onFilesQueued);
333 d->indexerThread->start();
334 connect(ProjectExplorer::SessionManager::instance(), &ProjectExplorer::SessionManager::projectAdded,
335 this, &ModelIndexer::onProjectAdded);
336 connect(ProjectExplorer::SessionManager::instance(), &ProjectExplorer::SessionManager::aboutToRemoveProject,
337 this, &ModelIndexer::onAboutToRemoveProject);
338 }
339
~ModelIndexer()340 ModelIndexer::~ModelIndexer()
341 {
342 emit quitIndexerThread();
343 d->indexerThread->wait();
344 delete d;
345 }
346
findModel(const qmt::Uid & modelUid)347 QString ModelIndexer::findModel(const qmt::Uid &modelUid)
348 {
349 QMutexLocker locker(&d->indexerMutex);
350 QSet<IndexedModel *> indexedModels = d->indexedModelsByUid.value(modelUid);
351 if (indexedModels.isEmpty())
352 return QString();
353 IndexedModel *indexedModel = *indexedModels.cbegin();
354 QMT_ASSERT(indexedModel, return QString());
355 return indexedModel->file();
356 }
357
findDiagram(const qmt::Uid & modelUid,const qmt::Uid & diagramUid)358 QString ModelIndexer::findDiagram(const qmt::Uid &modelUid, const qmt::Uid &diagramUid)
359 {
360 Q_UNUSED(modelUid) // avoid warning in release mode
361
362 QMutexLocker locker(&d->indexerMutex);
363 QSet<IndexedDiagramReference *> indexedDiagramReferences = d->indexedDiagramReferencesByDiagramUid.value(diagramUid);
364 if (indexedDiagramReferences.isEmpty())
365 return QString();
366 IndexedDiagramReference *indexedDiagramReference = *indexedDiagramReferences.cbegin();
367 QMT_ASSERT(indexedDiagramReference, return QString());
368 QMT_ASSERT(indexedDiagramReference->modelUid() == modelUid, return QString());
369 return indexedDiagramReference->file();
370 }
371
onProjectAdded(ProjectExplorer::Project * project)372 void ModelIndexer::onProjectAdded(ProjectExplorer::Project *project)
373 {
374 connect(project,
375 &ProjectExplorer::Project::fileListChanged,
376 this,
377 [this, p = QPointer(project)] { if (p) onProjectFileListChanged(p.data()); },
378 Qt::QueuedConnection);
379 scanProject(project);
380 }
381
onAboutToRemoveProject(ProjectExplorer::Project * project)382 void ModelIndexer::onAboutToRemoveProject(ProjectExplorer::Project *project)
383 {
384 disconnect(project, &ProjectExplorer::Project::fileListChanged, this, nullptr);
385 forgetProject(project);
386 }
387
onProjectFileListChanged(ProjectExplorer::Project * project)388 void ModelIndexer::onProjectFileListChanged(ProjectExplorer::Project *project)
389 {
390 scanProject(project);
391 }
392
scanProject(ProjectExplorer::Project * project)393 void ModelIndexer::scanProject(ProjectExplorer::Project *project)
394 {
395 if (!project->rootProjectNode())
396 return;
397
398 // TODO harmonize following code with findFirstModel()?
399 const Utils::FilePaths files = project->files(ProjectExplorer::Project::SourceFiles);
400 QQueue<QueuedFile> filesQueue;
401 QSet<QueuedFile> filesSet;
402
403 const Utils::MimeType modelMimeType = Utils::mimeTypeForName(Constants::MIME_TYPE_MODEL);
404 if (modelMimeType.isValid()) {
405 for (const Utils::FilePath &file : files) {
406 const QFileInfo fileInfo = file.toFileInfo();
407 if (modelMimeType.suffixes().contains(fileInfo.completeSuffix())) {
408 QueuedFile queuedFile(file.toString(), project, fileInfo.lastModified());
409 filesQueue.append(queuedFile);
410 filesSet.insert(queuedFile);
411 }
412 }
413 }
414
415 // FIXME: This potentially iterates over all files again.
416 QString defaultModelFile = findFirstModel(project->rootProjectNode(), modelMimeType);
417
418 bool filesAreQueued = false;
419 {
420 QMutexLocker locker(&d->indexerMutex);
421
422 // remove deleted files from queue
423 for (int i = 0; i < d->filesQueue.size();) {
424 if (d->filesQueue.at(i).project() == project) {
425 if (filesSet.contains(d->filesQueue.at(i))) {
426 ++i;
427 } else {
428 d->queuedFilesSet.remove(d->filesQueue.at(i));
429 d->filesQueue.removeAt(i);
430 }
431 }
432 }
433
434 // remove deleted files from indexed models
435 foreach (const QString &file, d->indexedModels.keys()) {
436 if (!filesSet.contains(QueuedFile(file, project)))
437 removeModelFile(file, project);
438 }
439
440 // remove deleted files from indexed diagrams
441 foreach (const QString &file, d->indexedDiagramReferences.keys()) {
442 if (!filesSet.contains(QueuedFile(file, project)))
443 removeDiagramReferenceFile(file, project);
444 }
445
446 // queue files
447 while (!filesQueue.isEmpty()) {
448 QueuedFile queuedFile = filesQueue.takeFirst();
449 if (!d->queuedFilesSet.contains(queuedFile)) {
450 QMT_CHECK(!d->filesQueue.contains(queuedFile));
451 d->filesQueue.append(queuedFile);
452 d->queuedFilesSet.insert(queuedFile);
453 filesAreQueued = true;
454 }
455 }
456
457 // auto-open model file only if project is already configured
458 if (!defaultModelFile.isEmpty() && !project->targets().isEmpty()) {
459 d->defaultModelFiles.insert(QueuedFile(defaultModelFile, project, QDateTime()));
460 }
461 }
462
463 if (filesAreQueued)
464 emit filesQueued();
465 }
466
findFirstModel(ProjectExplorer::FolderNode * folderNode,const Utils::MimeType & mimeType)467 QString ModelIndexer::findFirstModel(ProjectExplorer::FolderNode *folderNode,
468 const Utils::MimeType &mimeType)
469 {
470 if (!mimeType.isValid())
471 return QString();
472 foreach (ProjectExplorer::FileNode *fileNode, folderNode->fileNodes()) {
473 if (mimeType.suffixes().contains(fileNode->filePath().completeSuffix()))
474 return fileNode->filePath().toString();
475 }
476 foreach (ProjectExplorer::FolderNode *subFolderNode, folderNode->folderNodes()) {
477 QString modelFileName = findFirstModel(subFolderNode, mimeType);
478 if (!modelFileName.isEmpty())
479 return modelFileName;
480 }
481 return QString();
482 }
483
forgetProject(ProjectExplorer::Project * project)484 void ModelIndexer::forgetProject(ProjectExplorer::Project *project)
485 {
486 const Utils::FilePaths files = project->files(ProjectExplorer::Project::SourceFiles);
487
488 QMutexLocker locker(&d->indexerMutex);
489 for (const Utils::FilePath &file : files) {
490 const QString fileString = file.toString();
491 // remove file from queue
492 QueuedFile queuedFile(fileString, project);
493 if (d->queuedFilesSet.contains(queuedFile)) {
494 QMT_CHECK(d->filesQueue.contains(queuedFile));
495 d->filesQueue.removeOne(queuedFile);
496 QMT_CHECK(!d->filesQueue.contains(queuedFile));
497 d->queuedFilesSet.remove(queuedFile);
498 }
499 removeModelFile(fileString, project);
500 removeDiagramReferenceFile(fileString, project);
501 }
502 }
503
removeModelFile(const QString & file,ProjectExplorer::Project * project)504 void ModelIndexer::removeModelFile(const QString &file, ProjectExplorer::Project *project)
505 {
506 IndexedModel *indexedModel = d->indexedModels.value(file);
507 if (indexedModel && indexedModel->owningProjects().contains(project)) {
508 qCDebug(logger) << "remove model file " << file
509 << " from project " << project->displayName();
510 indexedModel->removeOwningProject(project);
511 if (indexedModel->owningProjects().isEmpty()) {
512 qCDebug(logger) << "delete indexed model " << project->displayName();
513 d->indexedModels.remove(file);
514
515 // remove indexedModel from set of indexedModelsByUid
516 QMT_CHECK(d->indexedModelsByUid.contains(indexedModel->modelUid()));
517 QSet<IndexedModel *> indexedModels = d->indexedModelsByUid.value(indexedModel->modelUid());
518 QMT_CHECK(indexedModels.contains(indexedModel));
519 indexedModels.remove(indexedModel);
520 if (indexedModels.isEmpty())
521 d->indexedModelsByUid.remove(indexedModel->modelUid());
522 else
523 d->indexedModelsByUid.insert(indexedModel->modelUid(), indexedModels);
524
525 delete indexedModel;
526 }
527 }
528 }
529
removeDiagramReferenceFile(const QString & file,ProjectExplorer::Project * project)530 void ModelIndexer::removeDiagramReferenceFile(const QString &file,
531 ProjectExplorer::Project *project)
532 {
533 IndexedDiagramReference *indexedDiagramReference = d->indexedDiagramReferences.value(file);
534 if (indexedDiagramReference) {
535 QMT_CHECK(indexedDiagramReference->owningProjects().contains(project));
536 qCDebug(logger) << "remove diagram reference file "
537 << file << " from project " << project->displayName();
538 indexedDiagramReference->removeOwningProject(project);
539 if (indexedDiagramReference->owningProjects().isEmpty()) {
540 qCDebug(logger) << "delete indexed diagram reference from " << file;
541 d->indexedDiagramReferences.remove(file);
542
543 // remove indexedDiagramReference from set of indexedDiagramReferecesByDiagramUid
544 QMT_CHECK(d->indexedDiagramReferencesByDiagramUid.contains(indexedDiagramReference->diagramUid()));
545 QSet<IndexedDiagramReference *> indexedDiagramReferences = d->indexedDiagramReferencesByDiagramUid.value(indexedDiagramReference->diagramUid());
546 QMT_CHECK(indexedDiagramReferences.contains(indexedDiagramReference));
547 indexedDiagramReferences.remove(indexedDiagramReference);
548 if (indexedDiagramReferences.isEmpty()) {
549 d->indexedDiagramReferencesByDiagramUid.remove(
550 indexedDiagramReference->diagramUid());
551 } else {
552 d->indexedDiagramReferencesByDiagramUid.insert(
553 indexedDiagramReference->diagramUid(), indexedDiagramReferences);
554 }
555
556 delete indexedDiagramReference;
557 }
558 }
559 }
560
logger()561 const QLoggingCategory &ModelIndexer::logger()
562 {
563 static const QLoggingCategory category("qtc.modeleditor.modelindexer", QtWarningMsg);
564 return category;
565 }
566
567 } // namespace Internal
568 } // namespace ModelEditor
569