File indexing completed on 2024-04-28 04:42:41

0001 /*
0002  * SPDX-FileCopyrightText: 2021 Anjani Kumar <anjanik012@gmail.com>
0003  * SPDX-FileCopyrightText: 2021 Han Young <hanyoung@protonmail.com>
0004  * SPDX-License-Identifier: LGPL-2.0-or-later
0005  */
0006 
0007 #include "capparser.h"
0008 #include "capalertinfo.h"
0009 #include "capalertmessage.h"
0010 #include "caparea.h"
0011 #include "capnamedvalue.h"
0012 #include "capreference.h"
0013 #include "kweathercore_p.h"
0014 #include <KLocalizedString>
0015 #include <QDateTime>
0016 #include <QDebug>
0017 
0018 #include <optional>
0019 
0020 namespace KWeatherCore
0021 {
0022 
0023 template<typename T>
0024 struct MapEntry {
0025     const char *name;
0026     T value;
0027 };
0028 
0029 template<typename QStringT, typename EnumT, std::size_t N>
0030 static std::optional<EnumT> stringToValue(const QStringT &s, const MapEntry<EnumT> (&map)[N])
0031 {
0032     const auto it = std::lower_bound(std::begin(map), std::end(map), s, [](auto lhs, auto rhs) {
0033         return QLatin1String(lhs.name) < rhs;
0034     });
0035     if (it != std::end(map) && QLatin1String((*it).name) == s) {
0036         return (*it).value;
0037     }
0038     return {};
0039 }
0040 
0041 // ### important: keep all the following tables sorted by name!
0042 static constexpr const MapEntry<CAPAlertInfo::Category> category_map[] = {
0043     {"CBRNE", CAPAlertInfo::Category::CBRNE},
0044     {"Env", CAPAlertInfo::Category::Environmental},
0045     {"Fire", CAPAlertInfo::Category::Fire},
0046     {"Geo", CAPAlertInfo::Category::Geophysical},
0047     {"Health", CAPAlertInfo::Category::Health},
0048     {"Infra", CAPAlertInfo::Category::Infrastructure},
0049     {"Met", CAPAlertInfo::Category::Meteorological},
0050     {"Other", CAPAlertInfo::Category::Other},
0051     {"Rescue", CAPAlertInfo::Category::Rescue},
0052     {"Safety", CAPAlertInfo::Category::Safety},
0053     {"Security", CAPAlertInfo::Category::Security},
0054     {"Transport", CAPAlertInfo::Category::Transport},
0055 };
0056 
0057 enum class Tags { ALERT, IDENTIFIER, SENDER, SENT_TIME, STATUS, MSG_TYPE, SCOPE, NOTE, INFO, REFERENCES };
0058 
0059 static constexpr const MapEntry<Tags> tag_map[] = {
0060     {"alert", Tags::ALERT},
0061     {"identifier", Tags::IDENTIFIER},
0062     {"info", Tags::INFO},
0063     {"msgType", Tags::MSG_TYPE},
0064     {"note", Tags::NOTE},
0065     {"references", Tags::REFERENCES},
0066     {"scope", Tags::SCOPE},
0067     {"sender", Tags::SENDER},
0068     {"sent", Tags::SENT_TIME},
0069     {"status", Tags::STATUS},
0070 };
0071 
0072 enum class InfoTags {
0073     HEADLINE,
0074     DESCRIPTION,
0075     EVENT,
0076     EVENTCODE,
0077     EFFECTIVE_TIME,
0078     ONSET_TIME,
0079     EXPIRE_TIME,
0080     CATEGORY,
0081     INSTRUCTION,
0082     URGENCY,
0083     SEVERITY,
0084     CERTAINITY,
0085     PARAMETER,
0086     AREA,
0087     SENDERNAME,
0088     LANGUAGE,
0089     RESPONSETYPE,
0090     CONTACT,
0091     WEB,
0092 };
0093 
0094 static constexpr const MapEntry<InfoTags> info_tag_map[] = {
0095     {"area", InfoTags::AREA},
0096     {"category", InfoTags::CATEGORY},
0097     {"certainty", InfoTags::CERTAINITY},
0098     {"contact", InfoTags::CONTACT},
0099     {"description", InfoTags::DESCRIPTION},
0100     {"effective", InfoTags::EFFECTIVE_TIME},
0101     {"event", InfoTags::EVENT},
0102     {"eventCode", InfoTags::EVENTCODE},
0103     {"expires", InfoTags::EXPIRE_TIME},
0104     {"headline", InfoTags::HEADLINE},
0105     {"instruction", InfoTags::INSTRUCTION},
0106     {"language", InfoTags::LANGUAGE},
0107     {"onset", InfoTags::ONSET_TIME},
0108     {"parameter", InfoTags::PARAMETER},
0109     {"responseType", InfoTags::RESPONSETYPE},
0110     {"senderName", InfoTags::SENDERNAME},
0111     {"severity", InfoTags::SEVERITY},
0112     {"urgency", InfoTags::URGENCY},
0113     {"web", InfoTags::WEB},
0114 };
0115 
0116 static constexpr const MapEntry<CAPAlertMessage::Status> status_map[] = {
0117     {"Actual", CAPAlertMessage::Status::Actual},
0118     {"Draft", CAPAlertMessage::Status::Draft},
0119     {"Excercise", CAPAlertMessage::Status::Exercise},
0120     {"System", CAPAlertMessage::Status::System},
0121     {"Test", CAPAlertMessage::Status::Test},
0122 };
0123 
0124 static constexpr const MapEntry<CAPAlertMessage::MessageType> msgtype_map[] = {
0125     {"Ack", CAPAlertMessage::MessageType::Acknowledge},
0126     {"Alert", CAPAlertMessage::MessageType::Alert},
0127     {"Cancel", CAPAlertMessage::MessageType::Cancel},
0128     {"Error", CAPAlertMessage::MessageType::Error},
0129     {"Update", CAPAlertMessage::MessageType::Update},
0130 };
0131 
0132 static constexpr const MapEntry<CAPAlertMessage::Scope> scope_map[] = {
0133     {"Private", CAPAlertMessage::Scope::Private},
0134     {"Public", CAPAlertMessage::Scope::Public},
0135     {"Restricted", CAPAlertMessage::Scope::Restricted},
0136 };
0137 
0138 static constexpr const MapEntry<CAPAlertInfo::ResponseType> response_type_map[] = {
0139     {"AllClear", CAPAlertInfo::ResponseType::AllClear},
0140     {"Assess", CAPAlertInfo::ResponseType::Assess},
0141     {"Avoid", CAPAlertInfo::ResponseType::Avoid},
0142     {"Evacuate", CAPAlertInfo::ResponseType::Evacuate},
0143     {"Execute", CAPAlertInfo::ResponseType::Execute},
0144     {"Monitor", CAPAlertInfo::ResponseType::Monitor},
0145     {"None", CAPAlertInfo::ResponseType::None},
0146     {"Prepare", CAPAlertInfo::ResponseType::Prepare},
0147     {"Shelter", CAPAlertInfo::ResponseType::Shelter},
0148 };
0149 
0150 CAPParser::CAPParser(const QByteArray &data)
0151     : m_xml(data)
0152 {
0153     bool flag = false;
0154     if (!data.isEmpty()) {
0155         while (m_xml.readNextStartElement()) {
0156             if (m_xml.name() == QStringLiteral("alert")) {
0157                 flag = true;
0158                 break;
0159             }
0160         }
0161         if (!flag) {
0162             qWarning() << "Not a CAP XML";
0163         }
0164     }
0165 }
0166 
0167 CAPAlertMessage CAPParser::parse()
0168 {
0169     CAPAlertMessage entry;
0170     while (m_xml.readNextStartElement()) {
0171         const auto tag = stringToValue(m_xml.name(), tag_map);
0172         if (!tag) {
0173             m_xml.skipCurrentElement();
0174             continue;
0175         }
0176         switch (*tag) {
0177         case Tags::IDENTIFIER:
0178             entry.setIdentifier(m_xml.readElementText());
0179             break;
0180         case Tags::SENDER:
0181             entry.setSender(m_xml.readElementText());
0182             break;
0183         case Tags::SENT_TIME:
0184             entry.setSentTime(QDateTime::fromString(m_xml.readElementText(), Qt::ISODate));
0185             break;
0186         case Tags::STATUS: {
0187             const auto elementText = m_xml.readElementText();
0188             const auto status = stringToValue(elementText, status_map);
0189             if (status) {
0190                 entry.setStatus(*status);
0191             } else {
0192                 qWarning() << "Unknown status field" << elementText;
0193             }
0194             break;
0195         }
0196         case Tags::MSG_TYPE: {
0197             const auto elementText = m_xml.readElementText();
0198             const auto msgType = stringToValue(elementText, msgtype_map);
0199             if (msgType) {
0200                 entry.setMessageType(*msgType);
0201             } else {
0202                 qWarning() << "Unknown msgType field" << elementText;
0203             }
0204             break;
0205         }
0206         case Tags::SCOPE: {
0207             const auto elementText = m_xml.readElementText();
0208             const auto scope = stringToValue(elementText, scope_map);
0209             if (scope) {
0210                 entry.setScope(*scope);
0211             } else {
0212                 qWarning() << "Unknown scope field" << elementText;
0213             }
0214             break;
0215         }
0216         case Tags::NOTE:
0217             entry.setNote(m_xml.readElementText());
0218             break;
0219         case Tags::INFO: {
0220             auto info = parseInfo();
0221             entry.addInfo(std::move(info));
0222             break;
0223         }
0224         case Tags::REFERENCES:
0225             entry.setReferences(parseReferences(m_xml.readElementText()));
0226             break;
0227         default:
0228             m_xml.skipCurrentElement();
0229         }
0230     }
0231     return entry;
0232 }
0233 
0234 CAPAlertInfo CAPParser::parseInfo()
0235 {
0236     CAPAlertInfo info;
0237 
0238     if (m_xml.name() == QLatin1String("info")) {
0239         while (!m_xml.atEnd() && !(m_xml.isEndElement() && m_xml.name() == QLatin1String("info"))) {
0240             m_xml.readNext();
0241             if (!m_xml.isStartElement()) {
0242                 continue;
0243             }
0244             const auto tag = stringToValue(m_xml.name(), info_tag_map);
0245             if (tag) {
0246                 switch (*tag) {
0247                 case InfoTags::CATEGORY: {
0248                     const auto s = m_xml.readElementText();
0249                     const auto category = stringToValue(s, category_map);
0250                     if (category) {
0251                         info.addCategory(*category);
0252                     }
0253                     break;
0254                 }
0255                 case InfoTags::EVENT:
0256                     info.setEvent(m_xml.readElementText());
0257                     break;
0258                 case InfoTags::URGENCY:
0259                     info.setUrgency(KWeatherCorePrivate::urgencyStringToEnum(m_xml.readElementText()));
0260                     break;
0261                 case InfoTags::SEVERITY:
0262                     info.setSeverity(KWeatherCorePrivate::severityStringToEnum(m_xml.readElementText()));
0263                     break;
0264                 case InfoTags::CERTAINITY:
0265                     info.setCertainty(KWeatherCorePrivate::certaintyStringToEnum(m_xml.readElementText()));
0266                     break;
0267                 case InfoTags::EFFECTIVE_TIME:
0268                     info.setEffectiveTime(QDateTime::fromString(m_xml.readElementText(), Qt::ISODate));
0269                     break;
0270                 case InfoTags::ONSET_TIME:
0271                     info.setOnsetTime(QDateTime::fromString(m_xml.readElementText(), Qt::ISODate));
0272                     break;
0273                 case InfoTags::EXPIRE_TIME:
0274                     info.setExpireTime(QDateTime::fromString(m_xml.readElementText(), Qt::ISODate));
0275                     break;
0276                 case InfoTags::HEADLINE:
0277                     info.setHeadline(m_xml.readElementText());
0278                     break;
0279                 case InfoTags::DESCRIPTION:
0280                     info.setDescription(m_xml.readElementText());
0281                     break;
0282                 case InfoTags::INSTRUCTION:
0283                     info.setInstruction(m_xml.readElementText());
0284                     break;
0285                 case InfoTags::PARAMETER: {
0286                     info.addParameter(parseNamedValue());
0287                     break;
0288                 }
0289                 case InfoTags::AREA: {
0290                     info.addArea(parseArea());
0291                     break;
0292                 }
0293                 case InfoTags::SENDERNAME: {
0294                     info.setSender(m_xml.readElementText());
0295                     break;
0296                 }
0297                 case InfoTags::LANGUAGE:
0298                     info.setLanguage(m_xml.readElementText());
0299                     break;
0300                 case InfoTags::RESPONSETYPE: {
0301                     const auto elementText = m_xml.readElementText();
0302                     if (const auto respType = stringToValue(elementText, response_type_map)) {
0303                         info.addResponseType(*respType);
0304                     } else {
0305                         qWarning() << "Unknown respone type value" << elementText;
0306                     }
0307                     break;
0308                 }
0309                 case InfoTags::CONTACT:
0310                     info.setContact(m_xml.readElementText());
0311                     break;
0312                 case InfoTags::WEB:
0313                     info.setWeb(m_xml.readElementText());
0314                     break;
0315                 case InfoTags::EVENTCODE:
0316                     info.addEventCode(parseNamedValue());
0317                     break;
0318                 }
0319             } else {
0320                 if (m_xml.isStartElement()) {
0321                     qWarning() << "unknown element: " << m_xml.name();
0322                 }
0323             }
0324         }
0325     }
0326     return info;
0327 }
0328 
0329 CAPArea CAPParser::parseArea()
0330 {
0331     CAPArea area;
0332     while (!(m_xml.isEndElement() && m_xml.name() == QLatin1String("area"))) {
0333         m_xml.readNext();
0334         if (m_xml.name() == QLatin1String("areaDesc") && !m_xml.isEndElement()) {
0335             area.setDescription(m_xml.readElementText());
0336         } else if (m_xml.name() == QLatin1String("geocode") && !m_xml.isEndElement()) {
0337             area.addGeoCode(parseNamedValue());
0338         } else if (m_xml.name() == QLatin1String("polygon") && !m_xml.isEndElement()) {
0339             area.addPolygon(KWeatherCorePrivate::stringToPolygon(m_xml.readElementText()));
0340         } else if (m_xml.name() == QLatin1String("circle") && !m_xml.isEndElement()) {
0341             const auto t = m_xml.readElementText();
0342             const auto commaIdx = t.indexOf(QLatin1Char(','));
0343             const auto spaceIdx = t.indexOf(QLatin1Char(' '));
0344             if (commaIdx > 0 && spaceIdx > commaIdx && commaIdx < t.size()) {
0345                 CAPCircle circle;
0346                 circle.latitude = QStringView(t).left(commaIdx).toFloat();
0347                 circle.longitude = QStringView(t).mid(commaIdx + 1, spaceIdx - commaIdx - 1).toFloat();
0348                 circle.radius = QStringView(t).mid(spaceIdx).toFloat();
0349                 area.addCircle(std::move(circle));
0350             }
0351         } else if (m_xml.name() == QLatin1String("altitude") && !m_xml.isEndElement()) {
0352             area.setAltitude(m_xml.readElementText().toFloat());
0353         } else if (m_xml.name() == QLatin1String("ceiling") && !m_xml.isEndElement()) {
0354             area.setCeiling(m_xml.readElementText().toFloat());
0355         } else if (m_xml.isStartElement()) {
0356             qDebug() << "unknown area element:" << m_xml.name();
0357         }
0358     }
0359     return area;
0360 }
0361 
0362 CAPNamedValue CAPParser::parseNamedValue()
0363 {
0364     CAPNamedValue value;
0365     const auto elementName = m_xml.name().toString();
0366     while (!m_xml.isEndElement() || m_xml.name() != elementName) {
0367         m_xml.readNext();
0368         if (m_xml.isStartElement() && m_xml.name() == QLatin1String("valueName")) {
0369             value.name = m_xml.readElementText();
0370         } else if (m_xml.isStartElement() && m_xml.name() == QLatin1String("value")) {
0371             value.value = m_xml.readElementText();
0372         } else if (m_xml.isStartElement()) {
0373             qDebug() << "unknown named value element:" << m_xml.name();
0374         }
0375     }
0376     return value;
0377 }
0378 
0379 std::vector<CAPReference> CAPParser::parseReferences(const QString &refsString)
0380 {
0381     std::vector<CAPReference> refs;
0382     // TODO for Qt 6: use QStringTokenizer
0383     const auto refsSplit = refsString.split(QLatin1Char(' '), Qt::SkipEmptyParts);
0384     refs.reserve(refsSplit.size());
0385     for (const auto &refString : refsSplit) {
0386         const auto refSplit = refString.split(QLatin1Char(','));
0387         if (refSplit.size() != 3) {
0388             qDebug() << "failed to parse CAP reference:" << refString;
0389             continue;
0390         }
0391         refs.emplace_back(refSplit.at(0), refSplit.at(1), QDateTime::fromString(refSplit.at(2), Qt::ISODate));
0392     }
0393 
0394     return refs;
0395 }
0396 }