Akonadi

itemcreatehandler.cpp
1/***************************************************************************
2 * SPDX-FileCopyrightText: 2007 Robert Zwerus <arzie@dds.nl> *
3 * *
4 * SPDX-License-Identifier: LGPL-2.0-or-later *
5 ***************************************************************************/
6
7#include "itemcreatehandler.h"
8
9#include "akonadi.h"
10#include "connection.h"
11#include "handlerhelper.h"
12#include "itemfetchhelper.h"
13#include "preprocessormanager.h"
14#include "private/externalpartstorage_p.h"
15#include "storage/datastore.h"
16#include "storage/dbconfig.h"
17#include "storage/itemretrievalmanager.h"
18#include "storage/parthelper.h"
19#include "storage/partstreamer.h"
20#include "storage/parttypehelper.h"
21#include "storage/selectquerybuilder.h"
22#include "storage/transaction.h"
23
24#include "shared/akranges.h"
25
26#include <QScopeGuard>
27
28#include <numeric> //std::accumulate
29
30using namespace Akonadi;
31using namespace Akonadi::Server;
32using namespace AkRanges;
33
34ItemCreateHandler::ItemCreateHandler(AkonadiServer &akonadi)
35 : Handler(akonadi)
36{
37}
38
39bool ItemCreateHandler::buildPimItem(const Protocol::CreateItemCommand &cmd, PimItem &item, Collection &parentCol)
40{
41 parentCol = HandlerHelper::collectionFromScope(cmd.collection(), connection()->context());
42 if (!parentCol.isValid()) {
43 return failureResponse(QStringLiteral("Invalid parent collection"));
44 }
45 if (parentCol.isVirtual()) {
46 return failureResponse(QStringLiteral("Cannot append item into virtual collection"));
47 }
48
49 MimeType mimeType = MimeType::retrieveByNameOrCreate(cmd.mimeType());
50 if (!mimeType.isValid()) {
51 return failureResponse(QStringLiteral("Unable to create mimetype '") % cmd.mimeType() % QStringLiteral("'."));
52 }
53
54 item.setRev(0);
55 item.setSize(cmd.itemSize());
56 item.setMimeTypeId(mimeType.id());
57 item.setCollectionId(parentCol.id());
58 item.setDatetime(cmd.dateTime());
59 if (cmd.remoteId().isEmpty()) {
60 // from application
61 item.setDirty(true);
62 } else {
63 // from resource
64 item.setRemoteId(cmd.remoteId());
65 item.setDirty(false);
66 }
67 item.setRemoteRevision(cmd.remoteRevision());
68 item.setGid(cmd.gid());
69
70 item.setAtime(cmd.modificationTime().isValid() ? cmd.modificationTime() : QDateTime::currentDateTimeUtc());
71
72 return true;
73}
74
75bool ItemCreateHandler::insertItem(const Protocol::CreateItemCommand &cmd, PimItem &item, const Collection &parentCol)
76{
77 if (!item.datetime().isValid()) {
78 item.setDatetime(QDateTime::currentDateTimeUtc());
79 }
80
81 if (!item.insert()) {
82 return failureResponse(QStringLiteral("Failed to append item"));
83 }
84
85 // set message flags
86 const QSet<QByteArray> flags = cmd.mergeModes() == Protocol::CreateItemCommand::None ? cmd.flags() : cmd.addedFlags();
87 if (!flags.isEmpty()) {
88 // This will hit an entry in cache inserted there in buildPimItem()
89 const Flag::List flagList = HandlerHelper::resolveFlags(flags);
90 bool flagsChanged = false;
91 if (!storageBackend()->appendItemsFlags({item}, flagList, &flagsChanged, false, parentCol, true)) {
92 return failureResponse("Unable to append item flags.");
93 }
94 }
95
96 const Scope tags = cmd.mergeModes() == Protocol::CreateItemCommand::None ? cmd.tags() : cmd.addedTags();
97 if (!tags.isEmpty()) {
98 const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()->context());
99 bool tagsChanged = false;
100 if (!storageBackend()->appendItemsTags({item}, tagList, &tagsChanged, false, parentCol, true)) {
101 return failureResponse(QStringLiteral("Unable to append item tags."));
102 }
103 }
104
105 // Handle individual parts
106 qint64 partSizes = 0;
107 PartStreamer streamer(connection(), item);
108 const auto parts = cmd.parts();
109 for (const QByteArray &partName : parts) {
110 qint64 partSize = 0;
111 try {
112 streamer.stream(true, partName, partSize);
113 } catch (const PartStreamerException &e) {
114 return failureResponse(e.what());
115 }
116 partSizes += partSize;
117 }
118 const Protocol::Attributes attrs = cmd.attributes();
119 for (auto iter = attrs.cbegin(), end = attrs.cend(); iter != end; ++iter) {
120 try {
121 streamer.streamAttribute(true, iter.key(), iter.value());
122 } catch (const PartStreamerException &e) {
123 return failureResponse(e.what());
124 }
125 }
126
127 // TODO: Try to avoid this addition query
128 if (partSizes > item.size()) {
129 item.setSize(partSizes);
130 item.update();
131 }
132
133 // Preprocessing
134 if (akonadi().preprocessorManager().isActive()) {
135 Part hiddenAttribute;
136 hiddenAttribute.setPimItemId(item.id());
137 hiddenAttribute.setPartType(PartTypeHelper::fromFqName(QStringLiteral(AKONADI_ATTRIBUTE_HIDDEN)));
138 hiddenAttribute.setData(QByteArray());
139 hiddenAttribute.setDatasize(0);
140 // TODO: Handle errors? Technically, this is not a critical issue as no data are lost
141 PartHelper::insert(&hiddenAttribute);
142 }
143
144 const bool seen = flags.contains(AKONADI_FLAG_SEEN) || flags.contains(AKONADI_FLAG_IGNORED);
145 notify(item, seen, item.collection());
146 sendResponse(item, Protocol::CreateItemCommand::None);
147
148 return true;
149}
150
151bool ItemCreateHandler::mergeItem(const Protocol::CreateItemCommand &cmd, PimItem &newItem, PimItem &currentItem, const Collection &parentCol)
152{
153 bool needsUpdate = false;
154 bool ignoreFlagsChanges = false;
155 QSet<QByteArray> changedParts;
156
157 if (currentItem.atime() > newItem.atime()) {
158 qCDebug(AKONADISERVER_LOG) << "Akoandi has newer atime of Item " << currentItem.id() << " than the resource (local atime =" << currentItem.atime()
159 << ", remote atime =" << newItem.atime() << "), ignoring flags changes.";
160 // This handles a race that is rather specific to IMAP: if I change flags in KMail while the folder is syncing, the flags from sync will
161 // overwrite my local changes.
162 // Without server-side change recording we don't have any way to know what has really changed locally, so we just assume it's flags and
163 // we will assume that the flags have not changed on the server as well (and if so, we will consider the local state superior to remote).
164 ignoreFlagsChanges = true;
165 }
166
167 if (!newItem.remoteId().isEmpty() && currentItem.remoteId() != newItem.remoteId()) {
168 currentItem.setRemoteId(newItem.remoteId());
169 changedParts.insert(AKONADI_PARAM_REMOTEID);
170 needsUpdate = true;
171 }
172 if (!newItem.remoteRevision().isEmpty() && currentItem.remoteRevision() != newItem.remoteRevision()) {
173 currentItem.setRemoteRevision(newItem.remoteRevision());
174 changedParts.insert(AKONADI_PARAM_REMOTEREVISION);
175 needsUpdate = true;
176 }
177 if (!newItem.gid().isEmpty() && currentItem.gid() != newItem.gid()) {
178 currentItem.setGid(newItem.gid());
179 changedParts.insert(AKONADI_PARAM_GID);
180 needsUpdate = true;
181 }
182 if (newItem.datetime().isValid() && newItem.datetime() != currentItem.datetime()) {
183 currentItem.setDatetime(newItem.datetime());
184 needsUpdate = true;
185 }
186
187 if (newItem.size() > 0 && newItem.size() != currentItem.size()) {
188 currentItem.setSize(newItem.size());
189 needsUpdate = true;
190 }
191
192 const Collection col = Collection::retrieveById(parentCol.id());
193 if (cmd.flags().isEmpty() && !cmd.flagsOverwritten()) {
194 bool flagsAdded = false;
195 bool flagsRemoved = false;
196 if (!cmd.addedFlags().isEmpty()) {
197 const auto addedFlags = HandlerHelper::resolveFlags(cmd.addedFlags());
198 storageBackend()->appendItemsFlags({currentItem}, addedFlags, &flagsAdded, true, col, true);
199 }
200 if (!cmd.removedFlags().isEmpty()) {
201 const auto removedFlags = HandlerHelper::resolveFlags(cmd.removedFlags());
202 storageBackend()->removeItemsFlags({currentItem}, removedFlags, &flagsRemoved, col, true);
203 }
204 if (flagsAdded || flagsRemoved) {
205 changedParts.insert(AKONADI_PARAM_FLAGS);
206 needsUpdate = true;
207 }
208 } else if (!ignoreFlagsChanges) {
209 bool flagsChanged = false;
210 QSet<QByteArray> flagNames = cmd.flags();
211
212 static QList<QByteArray> localFlagsToPreserve = {"$ATTACHMENT", "$INVITATION", "$ENCRYPTED", "$SIGNED", "$WATCHED"};
213
214 // Make sure we don't overwrite some local-only flags that can't come
215 // through from Resource during ItemSync, like $ATTACHMENT, because the
216 // resource is not aware of them (they are usually assigned by client
217 // upon inspecting the payload)
218 const Flag::List currentFlags = currentItem.flags();
219 for (const Flag &currentFlag : currentFlags) {
220 const QByteArray currentFlagName = currentFlag.name().toLatin1();
221 if (localFlagsToPreserve.contains(currentFlagName)) {
222 flagNames.insert(currentFlagName);
223 }
224 }
225 const auto flags = HandlerHelper::resolveFlags(flagNames);
226 storageBackend()->setItemsFlags({currentItem}, &currentFlags, flags, &flagsChanged, col, true);
227 if (flagsChanged) {
228 changedParts.insert(AKONADI_PARAM_FLAGS);
229 needsUpdate = true;
230 }
231 }
232
233 if (cmd.tags().isEmpty()) {
234 bool tagsAdded = false;
235 bool tagsRemoved = false;
236 if (!cmd.addedTags().isEmpty()) {
237 const auto addedTags = HandlerHelper::tagsFromScope(cmd.addedTags(), connection()->context());
238 storageBackend()->appendItemsTags({currentItem}, addedTags, &tagsAdded, true, col, true);
239 }
240 if (!cmd.removedTags().isEmpty()) {
241 const Tag::List removedTags = HandlerHelper::tagsFromScope(cmd.removedTags(), connection()->context());
242 storageBackend()->removeItemsTags({currentItem}, removedTags, &tagsRemoved, true);
243 }
244
245 if (tagsAdded || tagsRemoved) {
246 changedParts.insert(AKONADI_PARAM_TAGS);
247 needsUpdate = true;
248 }
249 } else {
250 bool tagsChanged = false;
251 const auto tags = HandlerHelper::tagsFromScope(cmd.tags(), connection()->context());
252 storageBackend()->setItemsTags({currentItem}, tags, &tagsChanged, true);
253 if (tagsChanged) {
254 changedParts.insert(AKONADI_PARAM_TAGS);
255 needsUpdate = true;
256 }
257 }
258
259 const Part::List existingParts = Part::retrieveFiltered(Part::pimItemIdColumn(), currentItem.id());
260 QMap<QByteArray, qint64> partsSizes;
261 for (const Part &part : existingParts) {
262 partsSizes.insert(PartTypeHelper::fullName(part.partType()).toLatin1(), part.datasize());
263 }
264
265 PartStreamer streamer(connection(), currentItem);
266 const auto partNames = cmd.parts();
267 for (const QByteArray &partName : partNames) {
268 bool changed = false;
269 qint64 partSize = 0;
270 try {
271 streamer.stream(true, partName, partSize, &changed);
272 } catch (const PartStreamerException &e) {
273 return failureResponse(e.what());
274 }
275
276 if (changed) {
277 changedParts.insert(partName);
278 partsSizes.insert(partName, partSize);
279 needsUpdate = true;
280 }
281 }
282
283 const qint64 size = std::accumulate(partsSizes.begin(), partsSizes.end(), 0LL);
284 if (size > currentItem.size()) {
285 currentItem.setSize(size);
286 needsUpdate = true;
287 }
288
289 if (needsUpdate) {
290 currentItem.setRev(qMax(newItem.rev(), currentItem.rev()) + 1);
291 currentItem.setAtime(QDateTime::currentDateTimeUtc());
292 // Only mark dirty when merged from application
293 currentItem.setDirty(!connection()->context().resource().isValid());
294
295 // Store all changes
296 if (!currentItem.update()) {
297 return failureResponse("Failed to store merged item");
298 }
299
300 notify(currentItem, currentItem.collection(), changedParts);
301 }
302
303 sendResponse(currentItem, cmd.mergeModes());
304
305 return true;
306}
307
308bool ItemCreateHandler::sendResponse(const PimItem &item, Protocol::CreateItemCommand::MergeModes mergeModes)
309{
310 if (mergeModes & Protocol::CreateItemCommand::Silent || mergeModes & Protocol::CreateItemCommand::None) {
311 Protocol::FetchItemsResponse resp;
312 resp.setId(item.id());
313 resp.setMTime(item.datetime());
314 Handler::sendResponse(std::move(resp));
315 return true;
316 }
317
318 Protocol::ItemFetchScope fetchScope;
319 fetchScope.setAncestorDepth(Protocol::ItemFetchScope::ParentAncestor);
320 fetchScope.setFetch(Protocol::ItemFetchScope::AllAttributes | Protocol::ItemFetchScope::FullPayload | Protocol::ItemFetchScope::CacheOnly
321 | Protocol::ItemFetchScope::Flags | Protocol::ItemFetchScope::GID | Protocol::ItemFetchScope::MTime | Protocol::ItemFetchScope::RemoteID
322 | Protocol::ItemFetchScope::RemoteRevision | Protocol::ItemFetchScope::Size | Protocol::ItemFetchScope::Tags);
323 ItemFetchHelper fetchHelper(connection(), Scope{item.id()}, fetchScope, Protocol::TagFetchScope{}, akonadi());
324 if (!fetchHelper.fetchItems()) {
325 return failureResponse("Failed to retrieve item");
326 }
327
328 return true;
329}
330
331bool ItemCreateHandler::notify(const PimItem &item, bool seen, const Collection &collection)
332{
333 storageBackend()->notificationCollector()->itemAdded(item, seen, collection);
334
335 if (akonadi().preprocessorManager().isActive()) {
336 // enqueue the item for preprocessing
337 akonadi().preprocessorManager().beginHandleItem(item, storageBackend());
338 }
339 return true;
340}
341
342bool ItemCreateHandler::notify(const PimItem &item, const Collection &collection, const QSet<QByteArray> &changedParts)
343{
344 if (!changedParts.isEmpty()) {
345 storageBackend()->notificationCollector()->itemChanged(item, changedParts, collection);
346 }
347 return true;
348}
349
350void ItemCreateHandler::recoverFromMultipleMergeCandidates(const PimItem::List &items, const Collection &collection)
351{
352 // HACK HACK HACK: When this happens within ItemSync, we are running inside a client-side
353 // transaction, so just calling commit here won't have any effect, since this handler will
354 // ultimately fail and the client will rollback the transaction. To circumvent this, we
355 // will forcibly commit the transaction, do our changes here within a new transaction and
356 // then we open a new transaction so that the client won't notice.
357
358 int transactionDepth = 0;
359 while (storageBackend()->inTransaction()) {
360 ++transactionDepth;
361 storageBackend()->commitTransaction();
362 }
363 const auto restoreTransaction = qScopeGuard([&]() {
364 for (int i = 0; i < transactionDepth; ++i) {
365 storageBackend()->beginTransaction(QStringLiteral("RestoredTransactionAfterMMCRecovery"));
366 }
367 });
368
369 Transaction transaction(storageBackend(), QStringLiteral("MMC Recovery Transaction"));
370
371 // If any of the conflicting items is dirty or does not have a remote ID, we don't want to remove
372 // them as it would cause data loss. There's a chance next changeReplay will fix this, so
373 // next time the ItemSync hits this multiple merge candidates, all changes will be committed
374 // and this check will succeed
375 if (items | Actions::any([](const auto &item) {
376 return item.dirty() || item.remoteId().isEmpty();
377 })) {
378 qCWarning(AKONADISERVER_LOG) << "Automatic multiple merge candidates recovery failed: at least one of the candidates has uncommitted changes!";
379 return;
380 }
381
382 // This cannot happen with ItemSync, but in theory could happen during individual GID merge.
383 if (items | Actions::any([collection](const auto &item) {
384 return item.collectionId() != collection.id();
385 })) {
386 qCWarning(AKONADISERVER_LOG) << "Automatic multiple merge candidates recovery failed: all candidates do not belong to the same collection.";
387 return;
388 }
389
390 storageBackend()->cleanupPimItems(items, DataStore::Silent);
391 if (!transaction.commit()) {
392 qCWarning(AKONADISERVER_LOG) << "Automatic multiple merge candidates recovery failed: failed to commit database transaction.";
393 return;
394 }
395
396 // Schedule a new sync of the collection, one that will succeed
397 akonadi().itemRetrievalManager().triggerCollectionSync(collection.resource().name(), collection.id());
398
399 qCInfo(AKONADISERVER_LOG) << "Automatic multiple merge candidates recovery successful: conflicting items"
400 << (items | Views::transform([](const auto &i) {
401 return i.id();
402 })
403 | Actions::toQVector)
404 << "in collection" << collection.name() << "(ID:" << collection.id()
405 << ") were removed and a new sync was scheduled in the resource" << collection.resource().name();
406}
407
409{
410 const auto &cmd = Protocol::cmdCast<Protocol::CreateItemCommand>(m_command);
411
412 // FIXME: The streaming/reading of all item parts can hold the transaction for
413 // unnecessary long time -> should we wrap the PimItem into one transaction
414 // and try to insert Parts independently? In case we fail to insert a part,
415 // it's not a problem as it can be re-fetched at any time, except for attributes.
416 Transaction transaction(storageBackend(), QStringLiteral("ItemCreateHandler"));
417 ExternalPartStorageTransaction storageTrx;
418
419 PimItem item;
420 Collection parentCol;
421 if (!buildPimItem(cmd, item, parentCol)) {
422 return false;
423 }
424
425 if ((cmd.mergeModes() & ~Protocol::CreateItemCommand::Silent) == 0) {
426 if (!insertItem(cmd, item, parentCol)) {
427 return false;
428 }
429 if (!transaction.commit()) {
430 return failureResponse(QStringLiteral("Failed to commit transaction"));
431 }
432 storageTrx.commit();
433 } else {
434 // Merging is always restricted to the same collection
436 qb.setForUpdate();
437 qb.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, parentCol.id());
438 Query::Condition rootCondition(Query::Or);
439
440 Query::Condition mergeCondition(Query::And);
441 if (cmd.mergeModes() & Protocol::CreateItemCommand::GID) {
442 mergeCondition.addValueCondition(PimItem::gidColumn(), Query::Equals, item.gid());
443 }
444 if (cmd.mergeModes() & Protocol::CreateItemCommand::RemoteID) {
445 mergeCondition.addValueCondition(PimItem::remoteIdColumn(), Query::Equals, item.remoteId());
446 }
447 rootCondition.addCondition(mergeCondition);
448
449 // If an Item with matching RID but empty GID exists during GID merge,
450 // merge into this item instead of creating a new one
451 if (cmd.mergeModes() & Protocol::CreateItemCommand::GID && !item.remoteId().isEmpty()) {
452 mergeCondition = Query::Condition(Query::And);
453 mergeCondition.addValueCondition(PimItem::remoteIdColumn(), Query::Equals, item.remoteId());
454 mergeCondition.addValueCondition(PimItem::gidColumn(), Query::Equals, QLatin1StringView(""));
455 rootCondition.addCondition(mergeCondition);
456 }
457 qb.addCondition(rootCondition);
458
459 if (!qb.exec()) {
460 return failureResponse("Failed to query database for item");
461 }
462
463 const QList<PimItem> result = qb.result();
464 if (result.isEmpty()) {
465 // No item with such GID/RID exists, so call ItemCreateHandler::insert() and behave
466 // like if this was a new item
467 if (!insertItem(cmd, item, parentCol)) {
468 return false;
469 }
470 if (!transaction.commit()) {
471 return failureResponse("Failed to commit transaction");
472 }
473 storageTrx.commit();
474
475 } else if (result.count() == 1) {
476 // Item with matching GID/RID combination exists, so merge this item into it
477 // and send itemChanged()
478 PimItem existingItem = result.at(0);
479
480 if (!mergeItem(cmd, item, existingItem, parentCol)) {
481 return false;
482 }
483 if (!transaction.commit()) {
484 return failureResponse("Failed to commit transaction");
485 }
486 storageTrx.commit();
487 } else {
488 qCWarning(AKONADISERVER_LOG) << "Multiple merge candidates, will attempt to recover:";
489 for (const PimItem &item : result) {
490 qCWarning(AKONADISERVER_LOG) << "\tID:" << item.id() << ", RID:" << item.remoteId() << ", GID:" << item.gid()
491 << ", Collection:" << item.collection().name() << "(" << item.collectionId() << ")"
492 << ", Resource:" << item.collection().resource().name() << "(" << item.collection().resourceId() << ")";
493 }
494
495 transaction.commit(); // commit the current transaction, before we attempt MMC recovery
496 recoverFromMultipleMergeCandidates(result, parentCol);
497
498 // Even if the recovery was successful, indicate error to force the client to abort the
499 // sync, since we've interfered with the overall state.
500 return failureResponse(QStringLiteral("Multiple merge candidates in collection '%1', aborting").arg(item.collection().name()));
501 }
502 }
503
504 return successResponse<Protocol::CreateItemResponse>();
505}
Represents a collection of PIM items.
Definition collection.h:62
virtual bool beginTransaction(const QString &name)
Begins a transaction.
virtual bool cleanupPimItems(const PimItem::List &items, bool silent=false)
Removes the pim item and all referenced data ( e.g.
NotificationCollector * notificationCollector()
Returns the notification collector of this DataStore object.
virtual bool commitTransaction()
Commits all changes within the current transaction and emits all collected notification signals.
static Flag::List resolveFlags(const QSet< QByteArray > &flagNames)
Converts a bytearray list of flag names into flag records.
The handler interfaces describes an entity capable of handling an AkonadiIMAP command.
Definition handler.h:32
bool parseStream() override
Parse and handle the IMAP message using the streaming parser.
void itemChanged(const PimItem &item, const QSet< QByteArray > &changedParts, const Collection &collection=Collection(), const QByteArray &resource=QByteArray())
Notify about a changed item.
void itemAdded(const PimItem &item, bool seen, const Collection &collection=Collection(), const QByteArray &resource=QByteArray())
Notify about an added item.
void beginHandleItem(const PimItem &item, const DataStore *dataStore)
Trigger the preprocessor chain for the specified item.
void addValueCondition(const QString &column, Query::CompareOperator op, const QVariant &value, ConditionType type=WhereCondition)
Add a WHERE or HAVING condition which compares a column with a given value.
bool exec()
Executes the query, returns true on success.
void addCondition(const Query::Condition &condition, ConditionType type=WhereCondition)
Add a WHERE condition.
void setForUpdate(bool forUpdate=true)
Indicate to the database to acquire an exclusive lock on the rows already during SELECT statement.
Represents a WHERE condition tree.
Definition query.h:62
void addValueCondition(const QString &column, CompareOperator op, const QVariant &value)
Add a WHERE condition which compares a column with a given value.
Definition query.cpp:12
void addCondition(const Condition &condition)
Add a WHERE condition.
Definition query.cpp:54
Helper class for creating and executing database SELECT queries.
QList< T > result()
Returns the result of this SELECT query.
Helper class for DataStore transaction handling.
Definition transaction.h:23
bool commit()
Commits the transaction.
bool insert(Part *part, qint64 *insertId=nullptr)
Adds a new part to the database and if necessary to the filesystem.
PartType fromFqName(const QString &fqName)
Retrieve (or create) PartType for the given fully qualified name.
QString fullName(const PartType &type)
Returns full part name.
Helper integration between Akonadi and Qt.
KCALUTILS_EXPORT QString mimeType()
bool isValid(QStringView ifopt)
QDateTime currentDateTimeUtc()
const_reference at(qsizetype i) const const
bool contains(const AT &value) const const
qsizetype count() const const
bool isEmpty() const const
iterator begin()
iterator end()
iterator insert(const Key &key, const T &value)
bool contains(const QSet< T > &other) const const
iterator insert(const T &value)
bool isEmpty() const const
QByteArray toLatin1() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Oct 11 2024 12:11:39 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.