summaryrefslogtreecommitdiffstats
path: root/common
diff options
context:
space:
mode:
Diffstat (limited to 'common')
-rw-r--r--common/commands/notification.fbs3
-rw-r--r--common/domain/applicationdomaintype.h48
-rw-r--r--common/listener.cpp3
-rw-r--r--common/modelresult.cpp90
-rw-r--r--common/modelresult.h12
-rw-r--r--common/notification.cpp2
-rw-r--r--common/notification.h1
-rw-r--r--common/query.h9
-rw-r--r--common/resourceaccess.cpp7
-rw-r--r--common/store.h8
-rw-r--r--common/synchronizer.cpp6
-rw-r--r--common/synchronizer.h2
12 files changed, 161 insertions, 30 deletions
diff --git a/common/commands/notification.fbs b/common/commands/notification.fbs
index c82fad3..517111c 100644
--- a/common/commands/notification.fbs
+++ b/common/commands/notification.fbs
@@ -2,9 +2,10 @@ namespace Sink.Commands;
2 2
3table Notification { 3table Notification {
4 type: int = 0; //See notification.h 4 type: int = 0; //See notification.h
5 identifier: string; //An identifier that links back to the something related to the notification (e.g. an entity id or a command id) 5 identifier: string; //An identifier that links back to the something related to the notification (e.g. a command id)
6 message: string; 6 message: string;
7 code: int = 0; //See notification.h 7 code: int = 0; //See notification.h
8 entities: [string]; //A list of entities this applies to
8} 9}
9 10
10root_type Notification; 11root_type Notification;
diff --git a/common/domain/applicationdomaintype.h b/common/domain/applicationdomaintype.h
index ef38d58..6fd2b90 100644
--- a/common/domain/applicationdomaintype.h
+++ b/common/domain/applicationdomaintype.h
@@ -93,7 +93,7 @@
93namespace Sink { 93namespace Sink {
94namespace ApplicationDomain { 94namespace ApplicationDomain {
95 95
96enum ErrorCode { 96enum SINK_EXPORT ErrorCode {
97 NoError = 0, 97 NoError = 0,
98 UnknownError, 98 UnknownError,
99 NoServerError, 99 NoServerError,
@@ -101,10 +101,33 @@ enum ErrorCode {
101 TransmissionError, 101 TransmissionError,
102}; 102};
103 103
104enum SuccessCode { 104enum SINK_EXPORT SuccessCode {
105 TransmissionSuccess 105 TransmissionSuccess
106}; 106};
107 107
108enum SINK_EXPORT SyncStatus {
109 NoSyncStatus,
110 SyncInProgress,
111 SyncError,
112 SyncSuccess
113};
114
115/**
116 * The status of an account or resource.
117 *
118 * It is set as follows:
119 * * By default the status is offline.
120 * * If a connection to the server could be established the status is Connected.
121 * * If an error occurred that keeps the resource from operating (so non transient), the resource enters the error state.
122 * * If a long running operation is started the resource goes to the busy state (and return to the previous state after that).
123 */
124enum SINK_EXPORT Status {
125 OfflineStatus,
126 ConnectedStatus,
127 BusyStatus,
128 ErrorStatus
129};
130
108struct SINK_EXPORT Error { 131struct SINK_EXPORT Error {
109 132
110}; 133};
@@ -113,6 +136,11 @@ struct SINK_EXPORT Progress {
113 136
114}; 137};
115 138
139/**
140 * Internal type.
141 *
142 * Represents a BLOB property.
143 */
116struct BLOB { 144struct BLOB {
117 BLOB() = default; 145 BLOB() = default;
118 BLOB(const BLOB &) = default; 146 BLOB(const BLOB &) = default;
@@ -410,22 +438,6 @@ struct SINK_EXPORT Mail : public Entity {
410 438
411SINK_EXPORT QDebug operator<< (QDebug d, const Mail::Contact &c); 439SINK_EXPORT QDebug operator<< (QDebug d, const Mail::Contact &c);
412 440
413/**
414 * The status of an account or resource.
415 *
416 * It is set as follows:
417 * * By default the status is offline.
418 * * If a connection to the server could be established the status is Connected.
419 * * If an error occurred that keeps the resource from operating (so non transient), the resource enters the error state.
420 * * If a long running operation is started the resource goes to the busy state (and return to the previous state after that).
421 */
422enum SINK_EXPORT Status {
423 OfflineStatus,
424 ConnectedStatus,
425 BusyStatus,
426 ErrorStatus
427};
428
429struct SINK_EXPORT Identity : public ApplicationDomainType { 441struct SINK_EXPORT Identity : public ApplicationDomainType {
430 static constexpr const char *name = "identity"; 442 static constexpr const char *name = "identity";
431 typedef QSharedPointer<Identity> Ptr; 443 typedef QSharedPointer<Identity> Ptr;
diff --git a/common/listener.cpp b/common/listener.cpp
index f18fe1d..96806ad 100644
--- a/common/listener.cpp
+++ b/common/listener.cpp
@@ -25,6 +25,7 @@
25#include "common/definitions.h" 25#include "common/definitions.h"
26#include "common/resourcecontext.h" 26#include "common/resourcecontext.h"
27#include "common/adaptorfactoryregistry.h" 27#include "common/adaptorfactoryregistry.h"
28#include "common/bufferutils.h"
28 29
29// commands 30// commands
30#include "common/commandcompletion_generated.h" 31#include "common/commandcompletion_generated.h"
@@ -406,11 +407,13 @@ void Listener::notify(const Sink::Notification &notification)
406{ 407{
407 auto messageString = m_fbb.CreateString(notification.message.toUtf8().constData(), notification.message.toUtf8().size()); 408 auto messageString = m_fbb.CreateString(notification.message.toUtf8().constData(), notification.message.toUtf8().size());
408 auto idString = m_fbb.CreateString(notification.id.constData(), notification.id.size()); 409 auto idString = m_fbb.CreateString(notification.id.constData(), notification.id.size());
410 auto entities = Sink::BufferUtils::toVector(m_fbb, notification.entities);
409 Sink::Commands::NotificationBuilder builder(m_fbb); 411 Sink::Commands::NotificationBuilder builder(m_fbb);
410 builder.add_type(notification.type); 412 builder.add_type(notification.type);
411 builder.add_code(notification.code); 413 builder.add_code(notification.code);
412 builder.add_identifier(idString); 414 builder.add_identifier(idString);
413 builder.add_message(messageString); 415 builder.add_message(messageString);
416 builder.add_entities(entities);
414 auto command = builder.Finish(); 417 auto command = builder.Finish();
415 Sink::Commands::FinishNotificationBuffer(m_fbb, command); 418 Sink::Commands::FinishNotificationBuffer(m_fbb, command);
416 for (Client &client : m_connections) { 419 for (Client &client : m_connections) {
diff --git a/common/modelresult.cpp b/common/modelresult.cpp
index 904766d..3edbec7 100644
--- a/common/modelresult.cpp
+++ b/common/modelresult.cpp
@@ -24,12 +24,20 @@
24#include <QPointer> 24#include <QPointer>
25 25
26#include "log.h" 26#include "log.h"
27#include "notifier.h"
28#include "notification.h"
29
30using namespace Sink;
31
32static uint getInternalIdentifer(const QByteArray &resourceId, const QByteArray &entityId)
33{
34 return qHash(resourceId + entityId);
35}
27 36
28static uint qHash(const Sink::ApplicationDomain::ApplicationDomainType &type) 37static uint qHash(const Sink::ApplicationDomain::ApplicationDomainType &type)
29{ 38{
30 // Q_ASSERT(!type.resourceInstanceIdentifier().isEmpty());
31 Q_ASSERT(!type.identifier().isEmpty()); 39 Q_ASSERT(!type.identifier().isEmpty());
32 return qHash(type.resourceInstanceIdentifier() + type.identifier()); 40 return getInternalIdentifer(type.resourceInstanceIdentifier(), type.identifier());
33} 41}
34 42
35static qint64 getIdentifier(const QModelIndex &idx) 43static qint64 getIdentifier(const QModelIndex &idx)
@@ -44,6 +52,79 @@ template <class T, class Ptr>
44ModelResult<T, Ptr>::ModelResult(const Sink::Query &query, const QList<QByteArray> &propertyColumns, const Sink::Log::Context &ctx) 52ModelResult<T, Ptr>::ModelResult(const Sink::Query &query, const QList<QByteArray> &propertyColumns, const Sink::Log::Context &ctx)
45 : QAbstractItemModel(), mLogCtx(ctx.subContext("modelresult")), mPropertyColumns(propertyColumns), mQuery(query) 53 : QAbstractItemModel(), mLogCtx(ctx.subContext("modelresult")), mPropertyColumns(propertyColumns), mQuery(query)
46{ 54{
55 if (query.flags().testFlag(Sink::Query::UpdateStatus)) {
56 mNotifier.reset(new Sink::Notifier{query});
57 mNotifier->registerHandler([this](const Notification &notification) {
58 switch (notification.type) {
59 case Notification::Status:
60 case Notification::Warning:
61 case Notification::Error:
62 case Notification::Info:
63 case Notification::Progress:
64 //These are the notifications we care about
65 break;
66 default:
67 //We're not interested
68 return;
69 };
70 if (notification.resource.isEmpty()|| notification.entities.isEmpty()) {
71 return;
72 }
73
74 QVector<qint64> idList;
75 for (const auto &entity : notification.entities) {
76 auto id = getInternalIdentifer(notification.resource, entity);
77 if (mEntities.contains(id)) {
78 idList << id;
79 }
80 }
81
82 if (idList.isEmpty()) {
83 //We don't have this entity in our model
84 return;
85 }
86 const int newStatus = [&] {
87 if (notification.type == Notification::Warning || notification.type == Notification::Error) {
88 return ApplicationDomain::SyncStatus::SyncError;
89 }
90 if (notification.type == Notification::Info) {
91 switch (notification.code) {
92 case ApplicationDomain::SyncInProgress:
93 return ApplicationDomain::SyncInProgress;
94 case ApplicationDomain::SyncSuccess:
95 return ApplicationDomain::SyncSuccess;
96 case ApplicationDomain::SyncError:
97 return ApplicationDomain::SyncError;
98 case ApplicationDomain::NoSyncStatus:
99 break;
100 }
101 return ApplicationDomain::NoSyncStatus;
102 }
103 if (notification.type == Notification::Progress) {
104 return ApplicationDomain::SyncStatus::SyncInProgress;
105 }
106 return ApplicationDomain::NoSyncStatus;
107 }();
108
109 for (const auto id : idList) {
110 const auto oldStatus = mEntityStatus.value(id);
111 QVector<int> changedRoles;
112 if (oldStatus != newStatus) {
113 mEntityStatus.insert(id, newStatus);
114 changedRoles << StatusRole;
115 }
116
117 if (notification.type == Notification::Progress) {
118 changedRoles << ProgressRole;
119 } else if (notification.type == Notification::Warning || notification.type == Notification::Error) {
120 changedRoles << WarningRole;
121 }
122
123 const auto idx = createIndexFromId(id);
124 emit dataChanged(idx, idx, changedRoles);
125 }
126 });
127 }
47} 128}
48 129
49template <class T, class Ptr> 130template <class T, class Ptr>
@@ -60,7 +141,7 @@ qint64 ModelResult<T, Ptr>::parentId(const Ptr &value)
60 if (!mQuery.parentProperty().isEmpty()) { 141 if (!mQuery.parentProperty().isEmpty()) {
61 const auto identifier = value->getProperty(mQuery.parentProperty()).toByteArray(); 142 const auto identifier = value->getProperty(mQuery.parentProperty()).toByteArray();
62 if (!identifier.isEmpty()) { 143 if (!identifier.isEmpty()) {
63 return qHash(T(value->resourceInstanceIdentifier(), identifier, 0, QSharedPointer<Sink::ApplicationDomain::BufferAdaptor>())); 144 return getInternalIdentifer(value->resourceInstanceIdentifier(), identifier);
64 } 145 }
65 } 146 }
66 return 0; 147 return 0;
@@ -106,6 +187,9 @@ QVariant ModelResult<T, Ptr>::data(const QModelIndex &index, int role) const
106 if (role == ChildrenFetchedRole) { 187 if (role == ChildrenFetchedRole) {
107 return childrenFetched(index); 188 return childrenFetched(index);
108 } 189 }
190 if (role == StatusRole) {
191 return mEntityStatus.value(index.internalId());
192 }
109 if (role == Qt::DisplayRole && index.isValid()) { 193 if (role == Qt::DisplayRole && index.isValid()) {
110 if (index.column() < mPropertyColumns.size()) { 194 if (index.column() < mPropertyColumns.size()) {
111 Q_ASSERT(mEntities.contains(index.internalId())); 195 Q_ASSERT(mEntities.contains(index.internalId()));
diff --git a/common/modelresult.h b/common/modelresult.h
index f30a8e1..cc263cf 100644
--- a/common/modelresult.h
+++ b/common/modelresult.h
@@ -30,15 +30,23 @@
30#include "resultprovider.h" 30#include "resultprovider.h"
31#include "threadboundary.h" 31#include "threadboundary.h"
32 32
33namespace Sink {
34class Notifier;
35}
36
33template <class T, class Ptr> 37template <class T, class Ptr>
34class ModelResult : public QAbstractItemModel 38class ModelResult : public QAbstractItemModel
35{ 39{
36public: 40public:
41 //Update the copy in store.h as well if you modify this
37 enum Roles 42 enum Roles
38 { 43 {
39 DomainObjectRole = Qt::UserRole + 1, 44 DomainObjectRole = Qt::UserRole + 1,
40 ChildrenFetchedRole, 45 ChildrenFetchedRole,
41 DomainObjectBaseRole 46 DomainObjectBaseRole,
47 StatusRole, //ApplicationDomain::SyncStatus
48 WarningRole, //ApplicationDomain::Warning, only if status == warning || status == error
49 ProgressRole //ApplicationDomain::Progress
42 }; 50 };
43 51
44 ModelResult(const Sink::Query &query, const QList<QByteArray> &propertyColumns, const Sink::Log::Context &); 52 ModelResult(const Sink::Query &query, const QList<QByteArray> &propertyColumns, const Sink::Log::Context &);
@@ -77,9 +85,11 @@ private:
77 QSet<qint64 /* entity id */> mEntityChildrenFetched; 85 QSet<qint64 /* entity id */> mEntityChildrenFetched;
78 QSet<qint64 /* entity id */> mEntityChildrenFetchComplete; 86 QSet<qint64 /* entity id */> mEntityChildrenFetchComplete;
79 QSet<qint64 /* entity id */> mEntityAllChildrenFetched; 87 QSet<qint64 /* entity id */> mEntityAllChildrenFetched;
88 QMap<qint64 /* entity id */, int /* Status */> mEntityStatus;
80 QList<QByteArray> mPropertyColumns; 89 QList<QByteArray> mPropertyColumns;
81 Sink::Query mQuery; 90 Sink::Query mQuery;
82 std::function<void(const Ptr &)> loadEntities; 91 std::function<void(const Ptr &)> loadEntities;
83 typename Sink::ResultEmitter<Ptr>::Ptr mEmitter; 92 typename Sink::ResultEmitter<Ptr>::Ptr mEmitter;
84 async::ThreadBoundary threadBoundary; 93 async::ThreadBoundary threadBoundary;
94 QScopedPointer<Sink::Notifier> mNotifier;
85}; 95};
diff --git a/common/notification.cpp b/common/notification.cpp
index b399d50..806d04a 100644
--- a/common/notification.cpp
+++ b/common/notification.cpp
@@ -21,6 +21,6 @@
21 21
22QDebug operator<<(QDebug dbg, const Sink::Notification &n) 22QDebug operator<<(QDebug dbg, const Sink::Notification &n)
23{ 23{
24 dbg << "Notification(Id: " << n.id << ", Type: " << n.type << ", Code: " << n.code << ", Message: " << n.message << ")"; 24 dbg << "Notification(Type: " << n.type << "Id, : " << n.id << ", Code: " << n.code << ", Message: " << n.message << ", Entities: " << n.entities << ")";
25 return dbg.space(); 25 return dbg.space();
26} 26}
diff --git a/common/notification.h b/common/notification.h
index 4b52274..f5379fd 100644
--- a/common/notification.h
+++ b/common/notification.h
@@ -51,6 +51,7 @@ public:
51 }; 51 };
52 52
53 QByteArray id; 53 QByteArray id;
54 QByteArrayList entities;
54 int type = 0; 55 int type = 0;
55 QString message; 56 QString message;
56 //A return code. Zero typically indicates success. 57 //A return code. Zero typically indicates success.
diff --git a/common/query.h b/common/query.h
index 49c8d5e..5b37cdd 100644
--- a/common/query.h
+++ b/common/query.h
@@ -300,7 +300,9 @@ public:
300 /** Leave the query running and continuously update the result set. */ 300 /** Leave the query running and continuously update the result set. */
301 LiveQuery = 1, 301 LiveQuery = 1,
302 /** Run the query synchronously. */ 302 /** Run the query synchronously. */
303 SynchronousQuery = 2 303 SynchronousQuery = 2,
304 /** Include status updates via notifications */
305 UpdateStatus = 4
304 }; 306 };
305 Q_DECLARE_FLAGS(Flags, Flag) 307 Q_DECLARE_FLAGS(Flags, Flag)
306 308
@@ -410,6 +412,11 @@ public:
410 mFlags = flags; 412 mFlags = flags;
411 } 413 }
412 414
415 Flags flags() const
416 {
417 return mFlags;
418 }
419
413 bool liveQuery() const 420 bool liveQuery() const
414 { 421 {
415 return mFlags.testFlag(LiveQuery); 422 return mFlags.testFlag(LiveQuery);
diff --git a/common/resourceaccess.cpp b/common/resourceaccess.cpp
index e48b624..9f4f14c 100644
--- a/common/resourceaccess.cpp
+++ b/common/resourceaccess.cpp
@@ -547,6 +547,7 @@ static Sink::Notification getNotification(const Sink::Commands::Notification *bu
547 } 547 }
548 n.type = buffer->type(); 548 n.type = buffer->type();
549 n.code = buffer->code(); 549 n.code = buffer->code();
550 n.entities = BufferUtils::fromVector(*buffer->entities());
550 return n; 551 return n;
551} 552}
552 553
@@ -608,13 +609,17 @@ bool ResourceAccess::processMessageBuffer()
608 mResourceStatus = buffer->code(); 609 mResourceStatus = buffer->code();
609 SinkTrace() << "Updated status: " << mResourceStatus; 610 SinkTrace() << "Updated status: " << mResourceStatus;
610 [[clang::fallthrough]]; 611 [[clang::fallthrough]];
612 case Sink::Notification::Info:
613 [[clang::fallthrough]];
611 case Sink::Notification::Warning: 614 case Sink::Notification::Warning:
612 [[clang::fallthrough]]; 615 [[clang::fallthrough]];
616 case Sink::Notification::Error:
617 [[clang::fallthrough]];
613 case Sink::Notification::FlushCompletion: 618 case Sink::Notification::FlushCompletion:
614 [[clang::fallthrough]]; 619 [[clang::fallthrough]];
615 case Sink::Notification::Progress: { 620 case Sink::Notification::Progress: {
616 auto n = getNotification(buffer); 621 auto n = getNotification(buffer);
617 SinkTrace() << "Received notification: Type:" << n.type << "Message: " << n.message << "Code: " << n.code; 622 SinkTrace() << "Received notification: " << n;
618 n.resource = d->resourceInstanceIdentifier; 623 n.resource = d->resourceInstanceIdentifier;
619 emit notification(n); 624 emit notification(n);
620 } break; 625 } break;
diff --git a/common/store.h b/common/store.h
index 86e4d20..fae76e5 100644
--- a/common/store.h
+++ b/common/store.h
@@ -48,11 +48,15 @@ QString SINK_EXPORT storageLocation();
48 */ 48 */
49QString SINK_EXPORT getTemporaryFilePath(); 49QString SINK_EXPORT getTemporaryFilePath();
50 50
51// Must be the same as in ModelResult
51enum Roles 52enum Roles
52{ 53{
53 DomainObjectRole = Qt::UserRole + 1, // Must be the same as in ModelResult 54 DomainObjectRole = Qt::UserRole + 1,
54 ChildrenFetchedRole, 55 ChildrenFetchedRole,
55 DomainObjectBaseRole 56 DomainObjectBaseRole,
57 StatusRole, //ApplicationDomain::SyncStatus
58 WarningRole, //ApplicationDomain::Warning, only if status == warning || status == error
59 ProgressRole //ApplicationDomain::Progress
56}; 60};
57 61
58/** 62/**
diff --git a/common/synchronizer.cpp b/common/synchronizer.cpp
index 4ed6e3a..ec896ed 100644
--- a/common/synchronizer.cpp
+++ b/common/synchronizer.cpp
@@ -292,13 +292,14 @@ void Synchronizer::flushComplete(const QByteArray &flushId)
292 } 292 }
293} 293}
294 294
295void Synchronizer::emitNotification(Notification::NoticationType type, int code, const QString &message, const QByteArray &id) 295void Synchronizer::emitNotification(Notification::NoticationType type, int code, const QString &message, const QByteArray &id, const QByteArrayList &entities)
296{ 296{
297 Sink::Notification n; 297 Sink::Notification n;
298 n.id = id; 298 n.id = id;
299 n.type = type; 299 n.type = type;
300 n.message = message; 300 n.message = message;
301 n.code = code; 301 n.code = code;
302 n.entities = entities;
302 emit notify(n); 303 emit notify(n);
303} 304}
304 305
@@ -328,6 +329,7 @@ KAsync::Job<void> Synchronizer::processRequest(const SyncRequest &request)
328 return KAsync::start([this, request] { 329 return KAsync::start([this, request] {
329 SinkLogCtx(mLogCtx) << "Synchronizing: " << request.query; 330 SinkLogCtx(mLogCtx) << "Synchronizing: " << request.query;
330 emitNotification(Notification::Status, ApplicationDomain::BusyStatus, "Synchronization has started.", request.requestId); 331 emitNotification(Notification::Status, ApplicationDomain::BusyStatus, "Synchronization has started.", request.requestId);
332 emitNotification(Notification::Info, ApplicationDomain::SyncInProgress, {}, {}, request.query.ids());
331 }).then(synchronizeWithSource(request.query)).then([this] { 333 }).then(synchronizeWithSource(request.query)).then([this] {
332 //Commit after every request, so implementations only have to commit more if they add a lot of data. 334 //Commit after every request, so implementations only have to commit more if they add a lot of data.
333 commit(); 335 commit();
@@ -335,10 +337,12 @@ KAsync::Job<void> Synchronizer::processRequest(const SyncRequest &request)
335 if (error) { 337 if (error) {
336 //Emit notification with error 338 //Emit notification with error
337 SinkWarningCtx(mLogCtx) << "Synchronization failed: " << error.errorMessage; 339 SinkWarningCtx(mLogCtx) << "Synchronization failed: " << error.errorMessage;
340 emitNotification(Notification::Warning, ApplicationDomain::SyncError, {}, {}, request.query.ids());
338 emitNotification(Notification::Status, ApplicationDomain::ErrorStatus, "Synchronization has ended.", request.requestId); 341 emitNotification(Notification::Status, ApplicationDomain::ErrorStatus, "Synchronization has ended.", request.requestId);
339 return KAsync::error(error); 342 return KAsync::error(error);
340 } else { 343 } else {
341 SinkLogCtx(mLogCtx) << "Done Synchronizing"; 344 SinkLogCtx(mLogCtx) << "Done Synchronizing";
345 emitNotification(Notification::Info, ApplicationDomain::SyncSuccess, {}, {}, request.query.ids());
342 emitNotification(Notification::Status, ApplicationDomain::ConnectedStatus, "Synchronization has ended.", request.requestId); 346 emitNotification(Notification::Status, ApplicationDomain::ConnectedStatus, "Synchronization has ended.", request.requestId);
343 return KAsync::null(); 347 return KAsync::null();
344 } 348 }
diff --git a/common/synchronizer.h b/common/synchronizer.h
index 28fe645..e3dbddc 100644
--- a/common/synchronizer.h
+++ b/common/synchronizer.h
@@ -180,7 +180,7 @@ protected:
180 */ 180 */
181 virtual void mergeIntoQueue(const Synchronizer::SyncRequest &request, QList<Synchronizer::SyncRequest> &queue); 181 virtual void mergeIntoQueue(const Synchronizer::SyncRequest &request, QList<Synchronizer::SyncRequest> &queue);
182 182
183 void emitNotification(Notification::NoticationType type, int code, const QString &message, const QByteArray &id = QByteArray{}); 183 void emitNotification(Notification::NoticationType type, int code, const QString &message, const QByteArray &id = QByteArray{}, const QByteArrayList &entiteis = QByteArrayList{});
184 184
185protected: 185protected:
186 Sink::Log::Context mLogCtx; 186 Sink::Log::Context mLogCtx;