#include "atomchannel.h" #include #include #include #include #include "constants.h" #include "macros.h" #include "rsshit_db.h" AtomChannel::AtomChannel(QXmlStreamReader *xmlReader) { Q_ASSERT(xmlReader != nullptr); const QString titleTag{"title"}; const QString linkTag{"link"}; const QString descriptionTag{"description"}; const QString lastBuildDateTag{"lastBuildDate"}; const QString languageTag{"language"}; while (!xmlReader->atEnd() && !xmlReader->hasError()) { const auto itemNext = xmlReader->readNext(); switch (itemNext) { case QXmlStreamReader::TokenType::StartElement: { const auto name = xmlReader->name(); // qDebug() << __func__ << ": StartElement" << name; // qDebug() << __func__ << "namespaceUri:" << xmlReader->namespaceUri(); // qDebug() << __func__ << "prefix:" << xmlReader->prefix(); // qDebug() << __func__ << "qualifiedName:" << xmlReader->qualifiedName(); // const auto elementText = xmlReader->readElementText(); if (name == titleTag) title = xmlReader->readElementText(); else if (name == linkTag) link = xmlReader->readElementText(); else if (name == descriptionTag) description = xmlReader->readElementText(); else if (name == lastBuildDateTag) lastBuildDate = QDateTime::fromString(xmlReader->readElementText(), Qt::DateFormat::RFC2822Date); else if (name == languageTag) language = QLocale{xmlReader->readElementText()}.language(); else if (name == AtomChannelImage::tag) { // qDebug() << "got image tag"; image = AtomChannelImage{xmlReader}; // qDebug() << image; } else if (name == AtomItem::tag) { items << AtomItem{xmlReader}; // qDebug() << items.constLast(); // qDebug() << __func__ << "got feed item"; } else { // qDebug() << "exit" << __func__; // qDebug() << __func__ << "unknown tag:" << name; continue; } break; } case QXmlStreamReader::TokenType::EndElement: { // qDebug() << "EndElement: " << xmlReader->name(); if (xmlReader->name() == AtomChannel::xmlTag) return; } case QXmlStreamReader::TokenType::Characters: { const auto characters = xmlReader->text().toString().simplified(); if (characters.isEmpty()) break; qDebug() << "channel: characters: " << characters; break; } } } qDebug() << "exit " << __func__; } // TODO: move to a separate function, code is almost identical in all methods called `getDbId` std::optional AtomChannel::getDbId() { if (m_id) return m_id; const auto db = rsshit::db::open(); if (!db) return rsshit::db::IdNotFound; QSqlQuery selectQ{"select id from feeds where link=?"}; selectQ.addBindValue(link); if (!selectQ.exec()) { qWarning() << "cannot exec query" << selectQ.lastQuery() << ":" << selectQ.lastError().text(); return rsshit::db::IdNotFound; } if (!selectQ.next()) return rsshit::db::IdNotFound; const auto idVariant = selectQ.value(rsshit::db::idTag); if (!idVariant.isValid() || !idVariant.canConvert()) return rsshit::db::IdNotFound; bool ok{false}; const auto result = idVariant.toInt(&ok); if (!ok) { qWarning() << "got invalid id from db:" << idVariant; return rsshit::db::IdNotFound; } return result; } // TODO: move to a separate function, code is almost identical in all methods called `createInDb` std::optional AtomChannel::createInDb() { if (m_id) return m_id; const auto db = rsshit::db::open(); if (!db) { return rsshit::db::IdNotFound; } QSqlQuery insertQ; if (!insertQ.prepare("insert into feeds(link, title, image_url) values(?, ?, ?)")) { qCritical() << "cannot prepare query:" << insertQ.lastError().text(); return rsshit::db::IdNotFound; } insertQ.addBindValue(link); insertQ.addBindValue(title); insertQ.addBindValue(image.url); if (!insertQ.exec()) { qWarning() << "cannot exec query" << insertQ.lastQuery() << ":" << insertQ.lastError().text(); return rsshit::db::IdNotFound; } m_id = insertQ.lastInsertId().toInt(); return m_id; } // TODO: can be moved to IDbObject std::optional AtomChannel::getOrInsertDbId() { const auto id = getDbId(); if (id != rsshit::db::IdNotFound) return id; return createInDb(); } QList AtomChannel::syncDbItems() { if (!m_id) m_id = getOrInsertDbId(); if (!m_id) return {}; QList result; result.reserve(items.count()); for (auto &item : items) { auto id = item.getOrInsertDbId(*this->m_id); if (id) result << id; } return result; } QDebug operator<<(QDebug debug, const AtomChannel &feed) { QDebugStateSaver saver{debug}; debug.nospace() << typeid(AtomChannel).name() << " {" << Qt::endl; PRINT_ATOM_FIELD(feed, title); PRINT_ATOM_FIELD(feed, link); PRINT_ATOM_FIELD(feed, description); PRINT_ATOM_FIELD(feed, lastBuildDate); PRINT_ATOM_FIELD(feed, language); debug << "\timage:\n" << feed.image << Qt::endl; debug << "\titems count:" << feed.items.size() << Qt::endl; for (const auto &item : feed.items) debug << item << Qt::endl; debug.nospace() << "}"; return debug; } QDebug operator<<(QDebug debug, const Feed &feed) { QDebugStateSaver saver{debug}; // const auto &mo = Channel::staticMetaObject; const auto &mo = *feed.metaObject(); debug.nospace() << mo.className() << " {" << Qt::endl; for (int i = mo.propertyOffset(); i < mo.propertyCount(); ++i) { auto prop = mo.property(i); debug.nospace() << "\t" << prop.name() << ": " << prop.readOnGadget(&feed) << Qt::endl; } debug.nospace() << "}"; return debug; } QDebug operator<<(QDebug debug, const Item &item) { QDebugStateSaver saver{debug}; // const auto &mo = Item::staticMetaObject; const auto &mo = *item.metaObject(); debug.nospace() << mo.className() << " {" << Qt::endl; for (int i = mo.propertyOffset(); i < mo.propertyCount(); ++i) { auto prop = mo.property(i); debug.nospace() << "\t" << prop.name() << ": " << prop.readOnGadget(&item) << Qt::endl; } debug.nospace() << "}"; return debug; } Feed::Feed(const QDomNode &node) : DbItem{node} { init(node); // use 'atom:link href="LINK"' if any const QString atomLinkTag = QStringLiteral("atom:link"); const QString hrefTag = QStringLiteral("href"); set_link(node.toElement() .elementsByTagName(atomLinkTag) .at(0) .toElement() .attribute(hrefTag, get_link().toString())); } bool Feed::isValid() const { return !m_title.isEmpty() && m_link.isValid(); } const QStringList &Feed::uniqueFields() const { // TODO: validate once that this class has corresponding fields return m_uniqueFields; } DbItem::DbItem(const QDomNode &node) { init(node); } void DbItem::init(const QDomNode &node) { // for (int i = mo.propertyOffset(); i < mo.propertyCount(); ++i) { for (int i = metaObject()->propertyOffset(); i < metaObject()->propertyCount(); ++i) { const auto prop = metaObject()->property(i); const auto propName = prop.name(); const auto propElement = node.toElement().elementsByTagName(propName).at(0); const auto propText = propElement.toElement().text(); const auto propType = QMetaType{prop.typeId()}; switch (propType.id()) { case QMetaType::Type::QDateTime: { prop.writeOnGadget(this, QDateTime::fromString(propText, dateFormat)); break; } case QMetaType::Type::QStringList: { const auto elements = node.toElement().elementsByTagName(propName); QStringList values; values.reserve(elements.size()); for (const auto &element : elements) { values << element.toElement().text(); } prop.writeOnGadget(this, std::move(values)); break; } default: { QVariant variant{propText}; variant.convert(propType); prop.writeOnGadget(this, std::move(variant)); } } } } QStringList DbItem::propertyNames() const { // TODO: init once to static var const auto propOffset = QObject::staticMetaObject.propertyCount(); const auto propCount = metaObject()->propertyCount(); QStringList result; result.reserve(propOffset - propCount); for (int i = propOffset; i < propCount; ++i) { result << metaObject()->property(i).name(); } return result; } QList DbItem::properties() const { // TODO: init once to static var const auto propOffset = QObject::staticMetaObject.propertyCount(); const auto propCount = metaObject()->propertyCount(); QList result; result.reserve(propOffset - propCount); for (int i = propOffset; i < propCount; ++i) { result << metaObject()->property(i); } return result; } DbItem::DbId DbItem::syncWithDb() { // const auto propOffset = QObject::staticMetaObject.propertyCount(); // const auto propCount = metaObject()->propertyCount(); // const auto propNames = propertyNames(); // "Item" -> "items" QString tableName{metaObject()->className()}; tableName[0] = tableName.at(0).toLower(); tableName.append('s'); qDebug() << Q_FUNC_INFO << "table name:" << tableName; // exists in db, update data if (m_id.has_value()) { // TODO: what to do if item was removed from db? return m_id; } // select id from db or insert else { QSqlQuery q; // TODO: validate once statically if (uniqueFields().isEmpty()) { qFatal() << metaObject()->className() << "has no unique fields, cannot select"; return {}; } // TODO: init once QStringList whereConditions; // for (const auto &name : propertyNames()) { for (const auto &name : uniqueFields()) { whereConditions << name + QStringLiteral("=:") + name; } const QString where{QStringLiteral(" where (") + whereConditions.join(QStringLiteral(" and ")) + ')'}; // const QString queryTemplate{QStringLiteral("select * from ") + tableName + ' ' + where}; qDebug() << "where:" << where; const QString queryTemplate{QStringLiteral("select * from ") + tableName + where}; qDebug().noquote() << "template:" << queryTemplate; const bool prepared = q.prepare(queryTemplate); if (!prepared) { qCritical() << tableName + ": cannot prepare select query:" << q.lastError().text(); qCritical().noquote() << "template: " << queryTemplate; return {}; } for (const auto &prop : properties()) { // FIXME: create uniqueProperties if (!uniqueFields().contains(prop.name())) continue; q.addBindValue(prop.read(this).toString()); // q.addBindValue("'asdfsdf'"); } if (!q.exec()) { qCritical() << "cannot select" << tableName << "from db:" << q.lastError().text(); qCritical().noquote() << "template: " << queryTemplate; return {}; } if (!q.next()) { qCritical() << "no results for" << tableName; // TODO: inset this shit. always inset it return {}; } const auto rec = q.record(); if (rec.isEmpty()) { qCritical() << "got empty record for" << tableName; return {}; } // qDebug() << "rec:" << rec; bool dbNeedsUpdate{false}; for (const auto &prop : properties()) { const auto val = rec.value(prop.name()); if (val.isNull() || !val.isValid()) { qCritical() << "got invalid value for table" << tableName << "fieldl" << prop.name(); return {}; } // if (prop.read(this) != val) { // // TODO: don't use this shit with tmp vars, call update here and break // dbNeedsUpdate = true; // break; // } prop.write(this, val); } if (dbNeedsUpdate) { } qDebug() << "finished reading from" << tableName; } // AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA // FIXME: implement return {}; } Item::Item(const QDomNode &node) : DbItem{node} { init(node); if (m_content.isEmpty()) { const QString contentEncodedTag = QStringLiteral("content:encoded"); set_content(node.toElement().elementsByTagName(contentEncodedTag).at(0).toElement().text()); } } bool Item::isValid() const { // TODO: check if isNull is needed return !m_pubDate.isNull() && m_pubDate.isValid() && !m_title.isEmpty() && !m_link.isEmpty() && !m_description.isEmpty(); } const QStringList &Item::uniqueFields() const { return m_uniqueFields; }