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 }