Akonadi

collectionfetchhandler.cpp
1/*
2 SPDX-FileCopyrightText: 2007 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "collectionfetchhandler.h"
8#include "akonadiserver_debug.h"
9
10#include "connection.h"
11#include "handlerhelper.h"
12#include "storage/collectionqueryhelper.h"
13#include "storage/datastore.h"
14#include "storage/selectquerybuilder.h"
15
16#include "private/scope_p.h"
17
18using namespace Akonadi;
19using namespace Akonadi::Server;
20
21template<typename T>
22static bool intersect(const QList<typename T::Id> &l1, const QList<T> &l2)
23{
24 for (const T &e2 : l2) {
25 if (l1.contains(e2.id())) {
26 return true;
27 }
28 }
29 return false;
30}
31
32[[nodiscard]] static bool isRootCollection(const Scope &scope)
33{
34 return scope.isEmpty() || (scope.scope() == Scope::Uid && scope.uidSet().size() == 1 && scope.uid() == 0);
35}
36
37CollectionFetchHandler::CollectionFetchHandler(AkonadiServer &akonadi)
38 : Handler(akonadi)
39{
40}
41
42QStack<Collection> CollectionFetchHandler::ancestorsForCollection(const Collection &col)
43{
44 if (mAncestorDepth <= 0) {
45 return QStack<Collection>();
46 }
47 QStack<Collection> ancestors;
48 Collection parent = col;
49 for (int i = 0; i < mAncestorDepth; ++i) {
50 if (parent.parentId() == 0) {
51 break;
52 }
53 if (mAncestors.contains(parent.parentId())) {
54 parent = mAncestors.value(parent.parentId());
55 } else {
56 parent = mCollections.value(parent.parentId());
57 }
58 if (!parent.isValid()) {
59 qCWarning(AKONADISERVER_LOG) << "Found an invalid parent in ancestors of Collection" << col.name() << "(ID:" << col.id() << ")";
60 throw HandlerException("Found invalid parent in ancestors");
61 }
62 ancestors.prepend(parent);
63 }
64 return ancestors;
65}
66
67CollectionAttribute::List CollectionFetchHandler::getAttributes(const Collection &col, const QSet<QByteArray> &filter)
68{
69 CollectionAttribute::List attributes;
70 auto it = mCollectionAttributes.find(col.id());
71 while (it != mCollectionAttributes.end() && it.key() == col.id()) {
72 if (filter.isEmpty() || filter.contains(it.value().type())) {
73 attributes << it.value();
74 }
75 ++it;
76 }
77
78 {
79 CollectionAttribute attr;
80 attr.setType(AKONADI_PARAM_ENABLED);
81 attr.setValue(col.enabled() ? "TRUE" : "FALSE");
82 attributes << attr;
83 }
84
85 return attributes;
86}
87
88void CollectionFetchHandler::listCollection(const Collection &root,
89 const QStack<Collection> &ancestors,
90 const QStringList &mimeTypes,
91 const CollectionAttribute::List &attributes)
92{
93 QStack<CollectionAttribute::List> ancestorAttributes;
94 // backwards compatibility, collectionToByteArray will automatically fall-back to id + remoteid
95 if (!mAncestorAttributes.isEmpty()) {
96 ancestorAttributes.reserve(ancestors.size());
97 for (const Collection &col : ancestors) {
98 ancestorAttributes.push(getAttributes(col, mAncestorAttributes));
99 }
100 }
101
102 // write out collection details
103 Collection dummy = root;
104 storageBackend()->activeCachePolicy(dummy);
105
106 sendResponse(
107 HandlerHelper::fetchCollectionsResponse(akonadi(), dummy, attributes, mIncludeStatistics, mAncestorDepth, ancestors, ancestorAttributes, mimeTypes));
108}
109
110static Query::Condition filterCondition(const QString &column)
111{
112 Query::Condition orCondition(Query::Or);
113 orCondition.addValueCondition(column, Query::Equals, static_cast<int>(Collection::True));
114 Query::Condition andCondition(Query::And);
115 andCondition.addValueCondition(column, Query::Equals, static_cast<int>(Collection::Undefined));
116 andCondition.addValueCondition(Collection::enabledFullColumnName(), Query::Equals, true);
117 orCondition.addCondition(andCondition);
118 return orCondition;
119}
120
121bool CollectionFetchHandler::checkFilterCondition(const Collection &col) const
122{
123 // Don't include the collection when only looking for enabled collections
124 if (mEnabledCollections && !col.enabled()) {
125 return false;
126 }
127 // Don't include the collection when only looking for collections to display/index/sync
128 if (mCollectionsToDisplay && (((col.displayPref() == Collection::Undefined) && !col.enabled()) || (col.displayPref() == Collection::False))) {
129 return false;
130 }
131 if (mCollectionsToIndex && (((col.indexPref() == Collection::Undefined) && !col.enabled()) || (col.indexPref() == Collection::False))) {
132 return false;
133 }
134 // Single collection sync will still work since that is using a base fetch
135 if (mCollectionsToSynchronize && (((col.syncPref() == Collection::Undefined) && !col.enabled()) || (col.syncPref() == Collection::False))) {
136 return false;
137 }
138 return true;
139}
140
141static QSqlQuery getAttributeQuery(const QVariantList &ids, const QSet<QByteArray> &requestedAttributes)
142{
143 QueryBuilder qb(CollectionAttribute::tableName());
144
145 qb.addValueCondition(CollectionAttribute::collectionIdFullColumnName(), Query::In, ids);
146
147 qb.addColumn(CollectionAttribute::collectionIdFullColumnName());
148 qb.addColumn(CollectionAttribute::typeFullColumnName());
149 qb.addColumn(CollectionAttribute::valueFullColumnName());
150
151 if (!requestedAttributes.isEmpty()) {
152 QVariantList attributes;
153 attributes.reserve(requestedAttributes.size());
154 for (const QByteArray &type : requestedAttributes) {
155 attributes << type;
156 }
157 qb.addValueCondition(CollectionAttribute::typeFullColumnName(), Query::In, attributes);
158 }
159
160 qb.addSortColumn(CollectionAttribute::collectionIdFullColumnName(), Query::Ascending);
161
162 if (!qb.exec()) {
163 throw HandlerException("Unable to retrieve attributes for listing");
164 }
165 return qb.query();
166}
167
168void CollectionFetchHandler::retrieveAttributes(const QVariantList &collectionIds)
169{
170 // We are querying for the attributes in batches because something can't handle WHERE IN queries with sets larger than 999
171 int start = 0;
172 const int size = 999;
173 while (start < collectionIds.size()) {
174 const QVariantList ids = collectionIds.mid(start, size);
175 QSqlQuery attributeQuery = getAttributeQuery(ids, mAncestorAttributes);
176 while (attributeQuery.next()) {
177 CollectionAttribute attr;
178 attr.setType(attributeQuery.value(1).toByteArray());
179 attr.setValue(attributeQuery.value(2).toByteArray());
180 // qCDebug(AKONADISERVER_LOG) << "found attribute " << attr.type() << attr.value();
181 mCollectionAttributes.insert(attributeQuery.value(0).toLongLong(), attr);
182 }
183 attributeQuery.finish();
184 start += size;
185 }
186}
187
188static QSqlQuery getMimeTypeQuery(const QVariantList &ids)
189{
190 QueryBuilder qb(CollectionMimeTypeRelation::tableName());
191
192 qb.addJoin(QueryBuilder::LeftJoin, MimeType::tableName(), MimeType::idFullColumnName(), CollectionMimeTypeRelation::rightFullColumnName());
193 qb.addValueCondition(CollectionMimeTypeRelation::leftFullColumnName(), Query::In, ids);
194
195 qb.addColumn(CollectionMimeTypeRelation::leftFullColumnName());
196 qb.addColumn(CollectionMimeTypeRelation::rightFullColumnName());
197 qb.addColumn(MimeType::nameFullColumnName());
198 qb.addSortColumn(CollectionMimeTypeRelation::leftFullColumnName(), Query::Ascending);
199
200 if (!qb.exec()) {
201 throw HandlerException("Unable to retrieve mimetypes for listing");
202 }
203 return qb.query();
204}
205
206void CollectionFetchHandler::retrieveCollections(const Collection &topParent, int depth)
207{
208 /*
209 * Retrieval of collections:
210 * The aim is to reduce the amount of queries as much as possible, as this has the largest performance impact for large queries.
211 * * First all collections that match the given criteria are queried
212 * * We then filter the false positives:
213 * ** all collections out that are not part of the tree we asked for are filtered
214 * * Finally we complete the tree by adding missing collections
215 *
216 * Mimetypes and attributes are also retrieved in single queries to avoid spawning two queries per collection (the N+1 problem).
217 * Note that we're not querying attributes and mimetypes for the collections that are only included to complete the tree,
218 * this results in no items being queried for those collections.
219 */
220
221 const qint64 parentId = topParent.isValid() ? topParent.id() : 0;
222 {
224
225 if (depth == 0) {
226 qb.addValueCondition(Collection::idFullColumnName(), Query::Equals, parentId);
227 } else if (depth == 1) {
228 if (topParent.isValid()) {
229 qb.addValueCondition(Collection::parentIdFullColumnName(), Query::Equals, parentId);
230 } else {
231 qb.addValueCondition(Collection::parentIdFullColumnName(), Query::Is, QVariant());
232 }
233 } else {
234 if (topParent.isValid()) {
235 qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::Equals, topParent.resourceId());
236 } else {
237 // Gimme gimme gimme...everything!
238 }
239 }
240
241 // Base listings should succeed always
242 if (depth != 0) {
243 if (mCollectionsToSynchronize) {
244 qb.addCondition(filterCondition(Collection::syncPrefFullColumnName()));
245 } else if (mCollectionsToDisplay) {
246 qb.addCondition(filterCondition(Collection::displayPrefFullColumnName()));
247 } else if (mCollectionsToIndex) {
248 qb.addCondition(filterCondition(Collection::indexPrefFullColumnName()));
249 } else if (mEnabledCollections) {
250 qb.addValueCondition(Collection::enabledFullColumnName(), Query::Equals, true);
251 }
252 if (mResource.isValid()) {
253 qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::Equals, mResource.id());
254 }
255
256 if (!mMimeTypes.isEmpty()) {
258 CollectionMimeTypeRelation::tableName(),
259 CollectionMimeTypeRelation::leftColumn(),
260 Collection::idFullColumnName());
261 QVariantList mimeTypeFilter;
262 mimeTypeFilter.reserve(mMimeTypes.size());
263 for (MimeType::Id mtId : std::as_const(mMimeTypes)) {
264 mimeTypeFilter << mtId;
265 }
266 qb.addValueCondition(CollectionMimeTypeRelation::rightColumn(), Query::In, mimeTypeFilter);
267 qb.addGroupColumn(Collection::idFullColumnName());
268 }
269 }
270
271 if (!qb.exec()) {
272 throw HandlerException("Unable to retrieve collection for listing");
273 }
274 const auto result{qb.result()};
275 for (const Collection &col : result) {
276 mCollections.insert(col.id(), col);
277 }
278 }
279
280 // Post filtering that we couldn't do as part of the sql query
281 if (depth > 0) {
282 auto it = mCollections.begin();
283 while (it != mCollections.end()) {
284 if (topParent.isValid()) {
285 // Check that each collection is linked to the root collection
286 bool foundParent = false;
287 // We iterate over parents to link it to topParent if possible
288 Collection::Id id = it->parentId();
289 while (id > 0) {
290 if (id == parentId) {
291 foundParent = true;
292 break;
293 }
294 Collection col = mCollections.value(id);
295 if (!col.isValid()) {
296 col = Collection::retrieveById(id);
297 }
298 id = col.parentId();
299 }
300 if (!foundParent) {
301 it = mCollections.erase(it);
302 continue;
303 }
304 }
305 ++it;
306 }
307 }
308
309 QVariantList mimeTypeIds;
310 QVariantList attributeIds;
311 QVariantList ancestorIds;
312 const auto collectionSize{mCollections.size()};
313 mimeTypeIds.reserve(collectionSize);
314 attributeIds.reserve(collectionSize);
315 // We'd only require the non-leaf collections, but we don't know which those are, so we take all.
316 ancestorIds.reserve(collectionSize);
317 for (auto it = mCollections.cbegin(), end = mCollections.cend(); it != end; ++it) {
318 mimeTypeIds << it.key();
319 attributeIds << it.key();
320 ancestorIds << it.key();
321 }
322
323 if (mAncestorDepth > 0 && topParent.isValid()) {
324 // unless depth is 0 the base collection is not part of the listing
325 mAncestors.insert(topParent.id(), topParent);
326 ancestorIds << topParent.id();
327 // We need to retrieve additional ancestors to what we already have in the tree
328 Collection parent = topParent;
329 for (int i = 0; i < mAncestorDepth; ++i) {
330 if (parent.parentId() == 0) {
331 break;
332 }
333 parent = parent.parent();
334 mAncestors.insert(parent.id(), parent);
335 // We also require the attributes
336 ancestorIds << parent.id();
337 }
338 }
339
340 QSet<qint64> missingCollections;
341 if (depth > 0) {
342 for (const Collection &col : std::as_const(mCollections)) {
343 if (col.parentId() != parentId && !mCollections.contains(col.parentId())) {
344 missingCollections.insert(col.parentId());
345 }
346 }
347 }
348
349 /*
350 QSet<qint64> knownIds;
351 for (const Collection &col : mCollections) {
352 knownIds.insert(col.id());
353 }
354 qCDebug(AKONADISERVER_LOG) << "HAS:" << knownIds;
355 qCDebug(AKONADISERVER_LOG) << "MISSING:" << missingCollections;
356 */
357
358 // Fetch missing collections that are part of the tree
359 while (!missingCollections.isEmpty()) {
361 QVariantList ids;
362 ids.reserve(missingCollections.size());
363 for (qint64 id : std::as_const(missingCollections)) {
364 ids << id;
365 }
366 qb.addValueCondition(Collection::idFullColumnName(), Query::In, ids);
367 if (!qb.exec()) {
368 throw HandlerException("Unable to retrieve collections for listing");
369 }
370
371 missingCollections.clear();
372 const auto missingCols = qb.result();
373 for (const Collection &missingCol : missingCols) {
374 mCollections.insert(missingCol.id(), missingCol);
375 ancestorIds << missingCol.id();
376 attributeIds << missingCol.id();
377 mimeTypeIds << missingCol.id();
378 // We have to do another round if the parents parent is missing
379 if (missingCol.parentId() != parentId && !mCollections.contains(missingCol.parentId())) {
380 missingCollections.insert(missingCol.parentId());
381 }
382 }
383 }
384
385 // Since we don't know when we'll need the ancestor attributes, we have to fetch them all together.
386 // The alternative would be to query for each collection which would reintroduce the N+1 query performance problem.
387 if (!mAncestorAttributes.isEmpty()) {
388 retrieveAttributes(ancestorIds);
389 }
390
391 // We are querying in batches because something can't handle WHERE IN queries with sets larger than 999
392 const int querySizeLimit = 999;
393 int mimetypeQueryStart = 0;
394 int attributeQueryStart = 0;
395 QSqlQuery mimeTypeQuery(storageBackend()->database());
396 QSqlQuery attributeQuery(storageBackend()->database());
397 auto it = mCollections.begin();
398 while (it != mCollections.end()) {
399 const Collection col = it.value();
400
401 QStringList mimeTypes;
402 {
403 // Get new query if necessary
404 if (!mimeTypeQuery.isValid() && mimetypeQueryStart < mimeTypeIds.size()) {
405 const QVariantList ids = mimeTypeIds.mid(mimetypeQueryStart, querySizeLimit);
406 mimetypeQueryStart += querySizeLimit;
407 mimeTypeQuery = getMimeTypeQuery(ids);
408 mimeTypeQuery.next(); // place at first record
409 }
410
411 while (mimeTypeQuery.isValid() && mimeTypeQuery.value(0).toLongLong() < col.id()) {
412 if (!mimeTypeQuery.next()) {
413 break;
414 }
415 }
416 // Advance query while a mimetype for this collection is returned
417 while (mimeTypeQuery.isValid() && mimeTypeQuery.value(0).toLongLong() == col.id()) {
418 mimeTypes << mimeTypeQuery.value(2).toString();
419 if (!mimeTypeQuery.next()) {
420 break;
421 }
422 }
423 }
424
425 CollectionAttribute::List attributes;
426 {
427 // Get new query if necessary
428 if (!attributeQuery.isValid() && attributeQueryStart < attributeIds.size()) {
429 const QVariantList ids = attributeIds.mid(attributeQueryStart, querySizeLimit);
430 attributeQueryStart += querySizeLimit;
431 attributeQuery = getAttributeQuery(ids, QSet<QByteArray>());
432 attributeQuery.next(); // place at first record
433 }
434
435 while (attributeQuery.isValid() && attributeQuery.value(0).toLongLong() < col.id()) {
436 if (!attributeQuery.next()) {
437 break;
438 }
439 }
440 // Advance query while a mimetype for this collection is returned
441 while (attributeQuery.isValid() && attributeQuery.value(0).toLongLong() == col.id()) {
442 CollectionAttribute attr;
443 attr.setType(attributeQuery.value(1).toByteArray());
444 attr.setValue(attributeQuery.value(2).toByteArray());
445 attributes << attr;
446
447 if (!attributeQuery.next()) {
448 break;
449 }
450 }
451 }
452
453 listCollection(col, ancestorsForCollection(col), mimeTypes, attributes);
454 it++;
455 }
456 attributeQuery.finish();
457 mimeTypeQuery.finish();
458}
459
461{
462 const auto &cmd = Protocol::cmdCast<Protocol::FetchCollectionsCommand>(m_command);
463
464 if (!cmd.resource().isEmpty()) {
465 mResource = Resource::retrieveByName(cmd.resource());
466 if (!mResource.isValid()) {
467 return failureResponse(QStringLiteral("Unknown resource %1").arg(cmd.resource()));
468 }
469 }
470 const QStringList lstMimeTypes = cmd.mimeTypes();
471 for (const QString &mtName : lstMimeTypes) {
472 const MimeType mt = MimeType::retrieveByNameOrCreate(mtName);
473 if (!mt.isValid()) {
474 return failureResponse("Failed to create mimetype record");
475 }
476 mMimeTypes.append(mt.id());
477 }
478
479 mEnabledCollections = cmd.enabled();
480 mCollectionsToSynchronize = cmd.syncPref();
481 mCollectionsToDisplay = cmd.displayPref();
482 mCollectionsToIndex = cmd.indexPref();
483 mIncludeStatistics = cmd.fetchStats();
484
485 int depth = 0;
486 switch (cmd.depth()) {
487 case Protocol::FetchCollectionsCommand::BaseCollection:
488 depth = 0;
489 break;
490 case Protocol::FetchCollectionsCommand::ParentCollection:
491 depth = 1;
492 break;
493 case Protocol::FetchCollectionsCommand::AllCollections:
494 depth = INT_MAX;
495 break;
496 }
497
498 switch (cmd.ancestorsDepth()) {
499 case Protocol::Ancestor::NoAncestor:
500 mAncestorDepth = 0;
501 break;
502 case Protocol::Ancestor::ParentAncestor:
503 mAncestorDepth = 1;
504 break;
505 case Protocol::Ancestor::AllAncestors:
506 mAncestorDepth = INT_MAX;
507 break;
508 }
509 mAncestorAttributes = cmd.ancestorsAttributes();
510
511 Scope scope = cmd.collections();
512 if (!isRootCollection(scope)) {
513 Collection col;
514 if (scope.scope() == Scope::Uid) {
515 col = Collection::retrieveById(scope.uid());
516 } else if (scope.scope() == Scope::Rid) {
518 qb.addValueCondition(Collection::remoteIdFullColumnName(), Query::Equals, scope.rid());
519 qb.addJoin(QueryBuilder::InnerJoin, Resource::tableName(), Collection::resourceIdFullColumnName(), Resource::idFullColumnName());
520 if (mCollectionsToSynchronize) {
521 qb.addCondition(filterCondition(Collection::syncPrefFullColumnName()));
522 } else if (mCollectionsToDisplay) {
523 qb.addCondition(filterCondition(Collection::displayPrefFullColumnName()));
524 } else if (mCollectionsToIndex) {
525 qb.addCondition(filterCondition(Collection::indexPrefFullColumnName()));
526 }
527 if (mResource.isValid()) {
528 qb.addValueCondition(Resource::idFullColumnName(), Query::Equals, mResource.id());
529 } else if (connection()->context().resource().isValid()) {
530 qb.addValueCondition(Resource::idFullColumnName(), Query::Equals, connection()->context().resource().id());
531 } else {
532 return failureResponse("Cannot retrieve collection based on remote identifier without a resource context");
533 }
534 if (!qb.exec()) {
535 return failureResponse("Unable to retrieve collection for listing");
536 }
537 Collection::List results = qb.result();
538 if (results.count() != 1) {
539 return failureResponse(QString::number(results.count()) + QStringLiteral(" collections found"));
540 }
541 col = results.first();
542 } else if (scope.scope() == Scope::HierarchicalRid) {
543 if (!connection()->context().resource().isValid()) {
544 return failureResponse("Cannot retrieve collection based on hierarchical remote identifier without a resource context");
545 }
546 col = CollectionQueryHelper::resolveHierarchicalRID(scope.hridChain(), connection()->context().resource().id());
547 } else {
548 return failureResponse("Unexpected error");
549 }
550
551 if (!col.isValid()) {
552 return failureResponse("Collection does not exist");
553 }
554
555 retrieveCollections(col, depth);
556 } else { // Root folder listing
557 if (depth != 0) {
558 retrieveCollections(Collection(), depth);
559 }
560 }
561
562 return successResponse<Protocol::FetchCollectionsResponse>();
563}
Represents a collection of PIM items.
Definition collection.h:62
qint64 Id
Describes the unique id type.
Definition collection.h:79
bool parseStream() override
Parse and handle the IMAP message using the streaming parser.
virtual void activeCachePolicy(Collection &col)
Determines the active cache policy for this Collection.
static Protocol::FetchCollectionsResponse fetchCollectionsResponse(AkonadiServer &akonadi, const Collection &col)
Returns the protocol representation of the given collection.
The handler interfaces describes an entity capable of handling an AkonadiIMAP command.
Definition handler.h:32
Helper class to construct arbitrary SQL queries.
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.
void addGroupColumn(const QString &column)
Add a GROUP BY column.
void addJoin(JoinType joinType, const QString &table, const Query::Condition &condition)
Join a table to the query.
bool exec()
Executes the query, returns true on success.
void addCondition(const Query::Condition &condition, ConditionType type=WhereCondition)
Add a WHERE condition.
@ InnerJoin
NOTE: only supported for UPDATE and SELECT queries.
@ LeftJoin
NOTE: only supported for SELECT queries.
Represents a WHERE condition tree.
Definition query.h:62
Helper class for creating and executing database SELECT queries.
QList< T > result()
Returns the result of this SELECT query.
Q_SCRIPTABLE Q_NOREPLY void start()
Collection resolveHierarchicalRID(const QList< Scope::HRID > &hridChain, Resource::Id resId)
Retrieve the collection referred to by the given hierarchical RID chain.
Helper integration between Akonadi and Qt.
VehicleSection::Type type(QStringView coachNumber, QStringView coachClassification)
const QList< QKeySequence > & end()
A glue between Qt and the standard library.
void append(QList< T > &&value)
bool contains(const AT &value) const const
qsizetype count() const const
T & first()
bool isEmpty() const const
QList< T > mid(qsizetype pos, qsizetype length) const const
void prepend(parameter_type value)
void reserve(qsizetype size)
qsizetype size() const const
T value(qsizetype i) const const
iterator end()
iterator find(const Key &key, const T &value)
iterator insert(const Key &key, const T &value)
T qobject_cast(QObject *object)
void clear()
iterator insert(const T &value)
bool isEmpty() const const
qsizetype size() const const
void finish()
bool isValid() const const
bool next()
QVariant value(const QString &name) const const
void push(const T &t)
QString number(double n, char format, int precision)
QFuture< void > filter(QThreadPool *pool, Sequence &sequence, KeepFunctor &&filterFunction)
QByteArray toByteArray() const const
qlonglong toLongLong(bool *ok) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:13:38 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.