diff --git b/CMakeLists.txt a/CMakeLists.txt new file mode 100644 index 0000000..6ebae8f --- /dev/null +++ a/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.0) +LIST(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) + +# Check to see if there is versioning information available +if(IS_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/osdev_versioning/cmake) + LIST(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/osdev_versioning/cmake) + include(osdevversion) +endif() + +include(projectheader) +project_header(osdev_mqtt) + +add_subdirectory(src) +add_subdirectory(tests/pub) +add_subdirectory(tests/sub) + + +# include(packaging) +# package_component() diff --git b/src/CMakeLists.txt a/src/CMakeLists.txt new file mode 100644 index 0000000..f6c58b3 --- /dev/null +++ a/src/CMakeLists.txt @@ -0,0 +1,69 @@ +cmake_minimum_required(VERSION 3.12) +LIST(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../cmake) +include(projectheader) +project_header(mqtt) + +find_package( Boost REQUIRED COMPONENTS regex ) + +include(compiler) + +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR}/../logutils +) + +set(SRC_LIST + ${CMAKE_CURRENT_SOURCE_DIR}/clientpaho.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/commondefs.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/connectionstatus.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/compiletimedigits.h + ${CMAKE_CURRENT_SOURCE_DIR}/compiletimestring.h + ${CMAKE_CURRENT_SOURCE_DIR}/credentials.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/errorcode.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/token.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ihistogram.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/timemeasurement.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mqttidgenerator.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mqtttypeconverter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mqttutil.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mqttmessage.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mqttclient.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mqttfailure.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mqttsuccess.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/utils.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/imqttclientimpl.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/istatecallback.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/scopeguard.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/serverstate.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/sharedreaderlock.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/stringutils.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/uriparser.cpp + # Helper files ( Utillities ) + ${CMAKE_CURRENT_SOURCE_DIR}/bimap.h + ${CMAKE_CURRENT_SOURCE_DIR}/compat-c++14.h + ${CMAKE_CURRENT_SOURCE_DIR}/compat-chrono.h + ${CMAKE_CURRENT_SOURCE_DIR}/histogram.h + ${CMAKE_CURRENT_SOURCE_DIR}/histogramprovider.h + ${CMAKE_CURRENT_SOURCE_DIR}/imqttclient.h + ${CMAKE_CURRENT_SOURCE_DIR}/imqttclientimpl.h + ${CMAKE_CURRENT_SOURCE_DIR}/lockguard.h + ${CMAKE_CURRENT_SOURCE_DIR}/macrodefs.h + ${CMAKE_CURRENT_SOURCE_DIR}/measure.h + ${CMAKE_CURRENT_SOURCE_DIR}/metaprogrammingdefs.h + ${CMAKE_CURRENT_SOURCE_DIR}/mqttstream.h + ${CMAKE_CURRENT_SOURCE_DIR}/stringify.h + ${CMAKE_CURRENT_SOURCE_DIR}/stringutils.h + ${CMAKE_CURRENT_SOURCE_DIR}/synchronizedqueue.h + ${CMAKE_CURRENT_SOURCE_DIR}/utils.h + ${CMAKE_CURRENT_SOURCE_DIR}/uriutils.h +) + +include(library) +add_libraries( + PUBLIC + Boost::boost + Boost::regex + paho-mqtt3a +) + +include(installation) +install_component() diff --git b/src/bimap.h a/src/bimap.h new file mode 100644 index 0000000..bd4cddf --- /dev/null +++ a/src/bimap.h @@ -0,0 +1,48 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ + +#ifndef OSDEV_COMPONENTS_MQTT_BIMAP_H +#define OSDEV_COMPONENTS_MQTT_BIMAP_H + +// boost +#include + +namespace osdev { +namespace components { +namespace mqtt { + +/** + * @brief Factory function to create boost::bimap from an initializer list + * + * Usage: + * @code + * auto myBimap = makeBimap( { { 1, 2 }, { 2, 3 } } ); + * @endcode + */ +template +boost::bimap makeBimap(std::initializer_list::value_type> list) +{ + return boost::bimap(list.begin(), list.end()); +} + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_BIMAP_H diff --git b/src/clientpaho.cpp a/src/clientpaho.cpp new file mode 100644 index 0000000..7ecbd64 --- /dev/null +++ a/src/clientpaho.cpp @@ -0,0 +1,1288 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "clientpaho.h" + +#include "errorcode.h" +#include "mqttutil.h" +#include "lockguard.h" +#include "metaprogrammingdefs.h" +#include "mqttstream.h" +#include "scopeguard.h" +#include "uriparser.h" + +// std::chrono +#include "compat-chrono.h" + +// std +#include +#include + +using namespace osdev::components::mqtt; + +namespace { + +#if defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-template" +#endif + +OSDEV_COMPONENTS_HASMEMBER_TRAIT(onSuccess5) + +template +inline typename std::enable_if::value, TRet>::type initializeMqttStruct(TRet*) +{ + return MQTTAsync_disconnectOptions_initializer; +} + +template +inline typename std::enable_if::value, TRet>::type initializeMqttStruct(TRet*) +{ +// For some reason g++ on centos7 evaluates the function body even when it is discarded by SFINAE. +// This leads to a compile error on an undefined symbol. We will use the old initializer macro, but this +// method should not be chosen when the struct does not contain member onSuccess5! +// On yocto warrior mqtt-paho-c 1.3.0 the macro MQTTAsync_disconnectOptions_initializer5 is not defined. +// while the struct does have an onSuccess5 member. In that case we do need correct initializer code. +// We fall back to the MQTTAsync_disconnectOptions_initializer macro and initialize +// additional fields ourself (which unfortunately results in a pesky compiler warning about missing field initializers). +#ifndef MQTTAsync_disconnectOptions_initializer5 +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + TRet ret = MQTTAsync_disconnectOptions_initializer; + ret.struct_version = 1; + ret.onSuccess5 = nullptr; + ret.onFailure5 = nullptr; + return ret; +#pragma GCC diagnostic pop +#else + return MQTTAsync_disconnectOptions_initializer5; +#endif +} + +template +struct Init +{ + static TRet initialize() + { + return initializeMqttStruct(static_cast(nullptr)); + } +}; +#if defined(__clang__) +#pragma GCC diagnostic pop +#endif + +} // namespace + +std::atomic_int ClientPaho::s_numberOfInstances(0); + +ClientPaho::ClientPaho(const std::string& _endpoint, + const std::string& _id, + const std::function& connectionStatusCallback, + const std::function& deliveryCompleteCallback) + : m_mutex() + , m_endpoint() + , m_username() + , m_password() + , m_clientId(_id) + , m_pendingOperations() + , m_operationResult() + , m_operationsCompleteCV() + , m_subscriptions() + , m_pendingSubscriptions() + , m_subscribeTokenToTopic() + , m_unsubscribeTokenToTopic() + , m_pendingPublishes() + , m_processPendingPublishes(false) + , m_pendingPublishesReadyCV() + , m_client() + , m_connectionStatus(ConnectionStatus::Disconnected) + , m_connectionStatusCallback(connectionStatusCallback) + , m_deliveryCompleteCallback(deliveryCompleteCallback) + , m_lastUnsubscribe(-1) + , m_connectPromise() + , m_disconnectPromise() + , m_callbackEventQueue(m_clientId) + , m_workerThread() +{ + if (0 == s_numberOfInstances++) { + MQTTAsync_setTraceCallback(&ClientPaho::onLogPaho); + } + // MLOGIC_COMMON_DEBUG("ClientPaho", "%1 - ctor ClientPaho %2", m_clientId, this); + parseEndpoint(_endpoint); + auto rc = MQTTAsync_create(&m_client, m_endpoint.c_str(), m_clientId.c_str(), MQTTCLIENT_PERSISTENCE_NONE, nullptr); + if (MQTTASYNC_SUCCESS == rc) + { + MQTTAsync_setCallbacks(m_client, reinterpret_cast(this), ClientPaho::onConnectionLost, ClientPaho::onMessageArrived, ClientPaho::onDeliveryComplete); + m_workerThread = std::thread(&ClientPaho::callbackEventHandler, this); + } + else + { + // Do something sensible here. + } +} + +ClientPaho::~ClientPaho() +{ + if( MQTTAsync_isConnected( m_client ) ) + { + this->unsubscribeAll(); + + this->waitForCompletion(std::chrono::milliseconds(2000), std::set{}); + this->disconnect(true, 5000); + } + else + { + // If the status was already disconnected this call does nothing + setConnectionStatus(ConnectionStatus::Disconnected); + } + + if (0 == --s_numberOfInstances) + { + // encountered a case where termination of the logging system within paho led to a segfault. + // This was a paho thread that was cleaned while at the same time the logging system was terminated. + // Removing the trace callback will not solve the underlying problem but hopefully will trigger it less + // frequently. + MQTTAsync_setTraceCallback(nullptr); + } + + MQTTAsync_destroy(&m_client); + + m_callbackEventQueue.stop(); + if (m_workerThread.joinable()) + { + m_workerThread.join(); + } +} + +std::string ClientPaho::clientId() const +{ + return m_clientId; +} + +ConnectionStatus ClientPaho::connectionStatus() const +{ + return m_connectionStatus; +} + +std::int32_t ClientPaho::connect(bool wait) +{ + { + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + if (ConnectionStatus::Disconnected != m_connectionStatus) + { + return -1; + } + setConnectionStatus(ConnectionStatus::ConnectInProgress); + } + + MQTTAsync_connectOptions conn_opts = MQTTAsync_connectOptions_initializer; + conn_opts.keepAliveInterval = 20; + conn_opts.cleansession = 1; + conn_opts.onSuccess = &ClientPaho::onConnectSuccess; + conn_opts.onFailure = &ClientPaho::onConnectFailure; + conn_opts.context = this; + conn_opts.automaticReconnect = 1; + if (!m_username.empty()) + { + conn_opts.username = m_username.c_str(); + } + + if (!m_password.empty()) + { + conn_opts.password = m_password.c_str(); + } + + std::promise waitForConnectPromise{}; + auto waitForConnect = waitForConnectPromise.get_future(); + m_connectPromise.reset(); + if (wait) + { + m_connectPromise = std::make_unique>(std::move(waitForConnectPromise)); + } + + { + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + if (!m_pendingOperations.insert(-100).second) + { + // Write something + } + m_operationResult.erase(-100); + } + + int rc = MQTTAsync_connect(m_client, &conn_opts); + if (MQTTASYNC_SUCCESS != rc) + { + setConnectionStatus(ConnectionStatus::Disconnected); + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + m_operationResult[-100] = false; + m_pendingOperations.erase(-100); + } + + if (wait) + { + waitForConnect.get(); + m_connectPromise.reset(); + } + return -100; +} + +std::int32_t ClientPaho::disconnect(bool wait, int timeoutMs) +{ + ConnectionStatus currentStatus = m_connectionStatus; + + { + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + if (ConnectionStatus::Disconnected == m_connectionStatus || ConnectionStatus::DisconnectInProgress == m_connectionStatus) { + return -1; + } + + currentStatus = m_connectionStatus; + setConnectionStatus(ConnectionStatus::DisconnectInProgress); + } + + MQTTAsync_disconnectOptions disconn_opts = Init::initialize(); + disconn_opts.timeout = timeoutMs; + disconn_opts.onSuccess = &ClientPaho::onDisconnectSuccess; + disconn_opts.onFailure = &ClientPaho::onDisconnectFailure; + disconn_opts.context = this; + + std::promise waitForDisconnectPromise{}; + auto waitForDisconnect = waitForDisconnectPromise.get_future(); + m_disconnectPromise.reset(); + if (wait) { + m_disconnectPromise = std::make_unique>(std::move(waitForDisconnectPromise)); + } + + { + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + if (!m_pendingOperations.insert(-200).second) + { + // "ClientPaho", "%1 disconnect - token %2 already in use", m_clientId, -200) + } + m_operationResult.erase(-200); + } + + int rc = MQTTAsync_disconnect(m_client, &disconn_opts); + if (MQTTASYNC_SUCCESS != rc) { + if (MQTTASYNC_DISCONNECTED == rc) { + currentStatus = ConnectionStatus::Disconnected; + } + + setConnectionStatus(currentStatus); + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + m_operationResult[-200] = false; + m_pendingOperations.erase(-200); + + if (MQTTASYNC_DISCONNECTED == rc) { + return -1; + } + // ("ClientPaho", "%1 - failed to disconnect, return code %2", m_clientId, pahoAsyncErrorCodeToString(rc)); + } + + if (wait) { + if (std::future_status::timeout == waitForDisconnect.wait_for(std::chrono::milliseconds(timeoutMs + 100))) + { + // ("ClientPaho", "%1 - timeout occurred on disconnect", m_clientId); + + } + waitForDisconnect.get(); + m_disconnectPromise.reset(); + } + return -200; +} + +std::int32_t ClientPaho::publish(const MqttMessage& message, int qos) +{ + if (ConnectionStatus::DisconnectInProgress == m_connectionStatus) + { + // ("ClientPaho", "%1 - disconnect in progress, ignoring publish with qos %2 on topic %3", m_clientId, qos, message.topic()); + return -1; + } + else if (ConnectionStatus::Disconnected == m_connectionStatus) + { + // ("ClientPaho", "%1 - unable to publish, not connected", m_clientId); + } + + if (!isValidTopic(message.topic())) + { + // ("ClientPaho", "%1 - topic %2 is invalid", m_clientId, message.topic()); + } + + if (qos > 2) + { + qos = 2; + } + else if (qos < 0) + { + qos = 0; + } + + + std::unique_lock lck(m_mutex); + if (ConnectionStatus::ReconnectInProgress == m_connectionStatus || m_processPendingPublishes) { + m_pendingPublishesReadyCV.wait(lck, [this]() { return !m_processPendingPublishes; }); + if (ConnectionStatus::ReconnectInProgress == m_connectionStatus) { + // ("ClientPaho", "Adding publish to pending queue."); + m_pendingPublishes.push_front(Publish{ qos, message }); + return -1; + } + } + + return publishInternal(message, qos); +} + +void ClientPaho::publishPending() +{ + { + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + if (!m_processPendingPublishes) { + return; + } + } + + if (ConnectionStatus::Connected != m_connectionStatus) + { + // MqttException, "Not connected"); + } + + while (!m_pendingPublishes.empty()) + { + const auto& pub = m_pendingPublishes.back(); + publishInternal(pub.data, pub.qos); + // else ("ClientPaho", "%1 - pending publish on topic %2 failed : %3", m_clientId, pub.data.topic(), e.what()); + + m_pendingPublishes.pop_back(); + } + + { + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + m_processPendingPublishes = false; + } + m_pendingPublishesReadyCV.notify_all(); +} + +std::int32_t ClientPaho::subscribe(const std::string& topic, int qos, const std::function& cb) +{ + if (ConnectionStatus::Connected != m_connectionStatus) + { + // MqttException, "Not connected" + } + + if (!isValidTopic(topic)) + { + // ("ClientPaho", "%1 - topic %2 is invalid", m_clientId, topic); + } + + if (qos > 2) + { + qos = 2; + } + else if (qos < 0) + { + qos = 0; + } + + { + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + + auto itExisting = m_subscriptions.find(topic); + if (m_subscriptions.end() != itExisting) { + if (itExisting->second.qos == qos) { + return -1; + } + // (OverlappingTopicException, "existing subscription with same topic, but different qos", topic); + } + + auto itPending = m_pendingSubscriptions.find(topic); + if (m_pendingSubscriptions.end() != itPending) { + if (itPending->second.qos == qos) { + auto itToken = std::find_if(m_subscribeTokenToTopic.begin(), m_subscribeTokenToTopic.end(), [&topic](const std::pair& item) { return topic == item.second; }); + if (m_subscribeTokenToTopic.end() != itToken) { + return itToken->first; + } + else { + return -1; + } + } + // (OverlappingTopicException, "pending subscription with same topic, but different qos", topic); + } + + std::string existingTopic{}; + if (isOverlappingInternal(topic, existingTopic)) + { + // (OverlappingTopicException, "overlapping topic", existingTopic, topic); + } + + // ("ClientPaho", "%1 - adding subscription on topic %2 to the pending subscriptions", m_clientId, topic); + m_pendingSubscriptions.emplace(std::make_pair(topic, Subscription{ qos, boost::regex(convertTopicToRegex(topic)), cb })); + } + return subscribeInternal(topic, qos); +} + +void ClientPaho::resubscribe() +{ + decltype(m_pendingSubscriptions) pendingSubscriptions{}; + { + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + std::copy(m_pendingSubscriptions.begin(), m_pendingSubscriptions.end(), std::inserter(pendingSubscriptions, pendingSubscriptions.end())); + } + + for (const auto& s : pendingSubscriptions) + { + subscribeInternal(s.first, s.second.qos); + } +} + +std::int32_t ClientPaho::unsubscribe(const std::string& topic, int qos) +{ + { + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + bool found = false; + for (const auto& s : m_subscriptions) + { + if (topic == s.first && qos == s.second.qos) + { + found = true; + break; + } + } + if (!found) + { + return -1; + } + } + + MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer; + opts.onSuccess = &ClientPaho::onUnsubscribeSuccess; + opts.onFailure = &ClientPaho::onUnsubscribeFailure; + opts.context = this; + + { + // Need to lock the mutex because it is possible that the callback is faster than + // the insertion of the token into the pending operations. + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + auto rc = MQTTAsync_unsubscribe(m_client, topic.c_str(), &opts); + if (MQTTASYNC_SUCCESS != rc) + { + // ("ClientPaho", "%1 - unsubscribe on topic %2 failed with code %3", m_clientId, topic, pahoAsyncErrorCodeToString(rc)); + } + + if (!m_pendingOperations.insert(opts.token).second) + { + // ("ClientPaho", "%1 unsubscribe - token %2 already in use", m_clientId, opts.token); + } + + m_operationResult.erase(opts.token); + if (m_unsubscribeTokenToTopic.count(opts.token) > 0) + { + // ("ClientPaho", "%1 - token already in use, replacing unsubscribe from topic %2 with topic %3", m_clientId, m_unsubscribeTokenToTopic[opts.token], topic); + } + m_lastUnsubscribe = opts.token; // centos7 workaround + m_unsubscribeTokenToTopic[opts.token] = topic; + } + + // Because of a bug in paho-c on centos7 the unsubscribes need to be sequential (best effort). + this->waitForCompletion(std::chrono::seconds(1), std::set{ opts.token }); + + return opts.token; +} + +void ClientPaho::unsubscribeAll() +{ + decltype(m_subscriptions) subscriptions{}; + { + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + subscriptions = m_subscriptions; + } + + for (const auto& s : subscriptions) { + this->unsubscribe(s.first, s.second.qos); + } +} + +std::chrono::milliseconds ClientPaho::waitForCompletion(std::chrono::milliseconds waitFor, const std::set& tokens) const +{ + if (waitFor <= std::chrono::milliseconds(0)) { + return std::chrono::milliseconds(0); + } + std::chrono::milliseconds timeElapsed{}; + { + osdev::components::mqtt::measurement::TimeMeasurement msr("waitForCompletion", [&timeElapsed](const std::string&, std::chrono::steady_clock::time_point, std::chrono::microseconds sinceStart, std::chrono::microseconds) + { + timeElapsed = std::chrono::ceil(sinceStart); + }); + std::unique_lock lck(m_mutex); + // ("ClientPaho", "%1 waitForCompletion - pending operations : %2", m_clientId, m_pendingOperations); + m_operationsCompleteCV.wait_for(lck, waitFor, [this, &tokens]() + { + if (tokens.empty()) + { // wait for all operations to end + return m_pendingOperations.empty(); + } + else if (tokens.size() == 1) + { + return m_pendingOperations.find(*tokens.cbegin()) == m_pendingOperations.end(); + } + std::vector intersect{}; + std::set_intersection(m_pendingOperations.begin(), m_pendingOperations.end(), tokens.begin(), tokens.end(), std::back_inserter(intersect)); + return intersect.empty(); + } ); + } + return timeElapsed; +} + +bool ClientPaho::isOverlapping(const std::string& topic) const +{ + std::string existingTopic{}; + return isOverlapping(topic, existingTopic); +} + +bool ClientPaho::isOverlapping(const std::string& topic, std::string& existingTopic) const +{ + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + return isOverlappingInternal(topic, existingTopic); +} + +std::vector ClientPaho::pendingOperations() const +{ + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + std::vector retval{}; + retval.resize(m_pendingOperations.size()); + std::copy(m_pendingOperations.begin(), m_pendingOperations.end(), retval.begin()); + return retval; +} + +bool ClientPaho::hasPendingSubscriptions() const +{ + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + return !m_pendingSubscriptions.empty(); +} + +boost::optional ClientPaho::operationResult(std::int32_t token) const +{ + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + boost::optional ret{}; + auto cit = m_operationResult.find(token); + if (m_operationResult.end() != cit) + { + ret = cit->second; + } + return ret; +} + +void ClientPaho::parseEndpoint(const std::string& _endpoint) +{ + auto ep = UriParser::parse(_endpoint); + if (ep.find("user") != ep.end()) + { + m_username = ep["user"]; + ep["user"].clear(); + } + + if (ep.find("password") != ep.end()) + { + m_password = ep["password"]; + ep["password"].clear(); + } + m_endpoint = UriParser::toString(ep); +} + +std::int32_t ClientPaho::publishInternal(const MqttMessage& message, int qos) +{ + MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer; + opts.onSuccess = &ClientPaho::onPublishSuccess; + opts.onFailure = &ClientPaho::onPublishFailure; + opts.context = this; + auto msg = message.toAsyncMessage(); + msg.qos = qos; + + // Need to lock the mutex because it is possible that the callback is faster than + // the insertion of the token into the pending operations. + + // OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + auto rc = MQTTAsync_sendMessage(m_client, message.topic().c_str(), &msg, &opts); + if (MQTTASYNC_SUCCESS != rc) + { + // ("ClientPaho", "%1 - publish on topic %2 failed with code %3", m_clientId, message.topic(), pahoAsyncErrorCodeToString(rc)); + } + + if (!m_pendingOperations.insert(opts.token).second) + { + // ("ClientPaho", "%1 publishInternal - token %2 already in use", m_clientId, opts.token); + } + m_operationResult.erase(opts.token); + return opts.token; +} + +std::int32_t ClientPaho::subscribeInternal(const std::string& topic, int qos) +{ + MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer; + opts.onSuccess = &ClientPaho::onSubscribeSuccess; + opts.onFailure = &ClientPaho::onSubscribeFailure; + opts.context = this; + + // Need to lock the mutex because it is possible that the callback is faster than + // the insertion of the token into the pending operations. + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + auto rc = MQTTAsync_subscribe(m_client, topic.c_str(), qos, &opts); + if (MQTTASYNC_SUCCESS != rc) + { + m_pendingSubscriptions.erase(topic); + // ("ClientPaho", "%1 - subscription on topic %2 failed with code %3", m_clientId, topic, pahoAsyncErrorCodeToString(rc)); + // (MqttException, "Subscription failed"); + } + + if (!m_pendingOperations.insert(opts.token).second) + { + // ("ClientPaho", "%1 subscribe - token %2 already in use", m_clientId, opts.token); + } + m_operationResult.erase(opts.token); + if (m_subscribeTokenToTopic.count(opts.token) > 0) + { + // ("ClientPaho", "%1 - overwriting pending subscription on topic %2 with topic %3", m_clientId, m_subscribeTokenToTopic[opts.token], topic); + } + m_subscribeTokenToTopic[opts.token] = topic; + return opts.token; +} + +void ClientPaho::setConnectionStatus(ConnectionStatus status) +{ + ConnectionStatus curStatus = m_connectionStatus; + m_connectionStatus = status; + if (status != curStatus && m_connectionStatusCallback) + { + m_connectionStatusCallback(m_clientId, status); + } +} + +bool ClientPaho::isOverlappingInternal(const std::string& topic, std::string& existingTopic) const +{ + existingTopic.clear(); + for (const auto& s : m_pendingSubscriptions) + { + if (testForOverlap(s.first, topic)) + { + existingTopic = s.first; + return true; + } + } + + for (const auto& s : m_subscriptions) + { + if (testForOverlap(s.first, topic)) + { + existingTopic = s.first; + return true; + } + } + return false; +} + +void ClientPaho::pushIncomingEvent(std::function ev) +{ + m_callbackEventQueue.push(ev); +} + +void ClientPaho::callbackEventHandler() +{ + // ("ClientPaho", "%1 - starting callback event handler", m_clientId); + for (;;) { + std::vector> events; + if (!m_callbackEventQueue.pop(events)) + { + break; + } + + for (const auto& ev : events) + { + ev(); + // ("ClientPaho", "%1 - Exception occurred: %2", m_clientId, mlogicException); + } + } + // ("ClientPaho", "%1 - leaving callback event handler", m_clientId); +} + +void ClientPaho::onConnectOnInstance(const std::string& cause) +{ + (void)cause; + // toLogFile ("ClientPaho", "onConnectOnInstance %1 - reconnected (cause %2)", m_clientId, cause); + { + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + std::copy(m_subscriptions.begin(), m_subscriptions.end(), std::inserter(m_pendingSubscriptions, m_pendingSubscriptions.end())); + m_subscriptions.clear(); + m_processPendingPublishes = true; // all publishes are on hold until publishPending is called. + } + + setConnectionStatus(ConnectionStatus::Connected); +} + +void ClientPaho::onConnectSuccessOnInstance(const MqttSuccess& response) +{ + auto connectData = response.connectionData(); + // ("ClientPaho", "onConnectSuccessOnInstance %1 - connected to endpoint %2 (mqtt version %3, session present %4)", + // m_clientId, connectData.serverUri(), connectData.mqttVersion(), connectData.sessionPresent()); + { + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + // Register the connect callback that is used in reconnect scenarios. + auto rc = MQTTAsync_setConnected(m_client, this, &ClientPaho::onConnect); + if (MQTTASYNC_SUCCESS != rc) + { + // ("ClientPaho", "onConnectSuccessOnInstance %1 - registering the connected callback failed with code %2", m_clientId, pahoAsyncErrorCodeToString(rc)); + } + // For MQTTV5 + //rc = MQTTAsync_setDisconnected(m_client, this, &ClientPaho::onDisconnect); + //if (MQTTASYNC_SUCCESS != rc) { + // // ("ClientPaho", "onConnectSuccessOnInstance %1 - registering the disconnected callback failed with code %2", m_clientId, pahoAsyncErrorCodeToString(rc)); + //} + // ("ClientPaho", "onConnectSuccessOnInstance %1 - pending operations : %2, removing operation -100", m_clientId, m_pendingOperations); + m_operationResult[-100] = true; + m_pendingOperations.erase(-100); + } + setConnectionStatus(ConnectionStatus::Connected); + if (m_connectPromise) + { + m_connectPromise->set_value(); + } + m_operationsCompleteCV.notify_all(); +} + +void ClientPaho::onConnectFailureOnInstance(const MqttFailure& response) +{ + (void)response; + // ("ClientPaho", "onConnectFailureOnInstance %1 - connection failed with code %2 (%3)", m_clientId, response.codeToString(), response.message()); + { + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + if (m_connectPromise) { + m_connectPromise->set_value(); + } + // ("ClientPaho", "onConnectFailureOnInstance %1 - pending operations : %2, removing operation -100", m_clientId, m_pendingOperations); + m_operationResult[-100] = false; + m_pendingOperations.erase(-100); + } + if (ConnectionStatus::ConnectInProgress == m_connectionStatus) + { + setConnectionStatus(ConnectionStatus::Disconnected); + } + m_operationsCompleteCV.notify_all(); +} + +//void ClientPaho::onDisconnectOnInstance(enum MQTTReasonCodes reasonCode) +//{ +// MLOGIC_COMMON_INFO("ClientPaho", "onDisconnectOnInstance %1 - disconnect (reason %2)", MQTTReasonCode_toString(reasonCode)); +//} + +void ClientPaho::onDisconnectSuccessOnInstance(const MqttSuccess&) +{ + // ("ClientPaho", "onDisconnectSuccessOnInstance %1 - disconnected from endpoint %2", m_clientId, m_endpoint); + { + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + m_subscriptions.clear(); + m_pendingSubscriptions.clear(); + m_subscribeTokenToTopic.clear(); + m_unsubscribeTokenToTopic.clear(); + + // ("ClientPaho", "onDisconnectSuccessOnInstance %1 - pending operations : %2, removing all operations", m_clientId, m_pendingOperations); + m_operationResult[-200] = true; + m_pendingOperations.clear(); + } + + setConnectionStatus(ConnectionStatus::Disconnected); + + if (m_disconnectPromise) { + m_disconnectPromise->set_value(); + } + m_operationsCompleteCV.notify_all(); +} + +void ClientPaho::onDisconnectFailureOnInstance(const MqttFailure& response) +{ + (void)response; + // ("ClientPaho", "onDisconnectFailureOnInstance %1 - disconnect failed with code %2 (%3)", m_clientId, response.codeToString(), response.message()); + { + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + // ("ClientPaho", "onDisconnectFailureOnInstance %1 - pending operations : %2, removing operation -200", m_clientId, m_pendingOperations); + m_operationResult[-200] = false; + m_pendingOperations.erase(-200); + } + + if (MQTTAsync_isConnected(m_client)) + { + setConnectionStatus(ConnectionStatus::Connected); + } + else + { + setConnectionStatus(ConnectionStatus::Disconnected); + } + + if (m_disconnectPromise) + { + m_disconnectPromise->set_value(); + } + m_operationsCompleteCV.notify_all(); +} + +void ClientPaho::onPublishSuccessOnInstance(const MqttSuccess& response) +{ + auto pd = response.publishData(); + // ("ClientPaho", "onPublishSuccessOnInstance %1 - publish with token %2 succeeded (message was %3)", m_clientId, response.token(), pd.payload()); + { + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + // ("ClientPaho", "onPublishSuccessOnInstance %1 - pending operations : %2, removing operation %3", m_clientId, m_pendingOperations, response.token()); + m_operationResult[response.token()] = true; + m_pendingOperations.erase(response.token()); + } + m_operationsCompleteCV.notify_all(); +} + +void ClientPaho::onPublishFailureOnInstance(const MqttFailure& response) +{ + // ("ClientPaho", "onPublishFailureOnInstance %1 - publish with token %2 failed with code %3 (%4)", m_clientId, response.token(), response.codeToString(), response.message()); + { + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + // ("ClientPaho", "onPublishFailureOnInstance %1 - pending operations : %2, removing operation %3", m_clientId, m_pendingOperations, response.token()); + m_operationResult[response.token()] = false; + m_pendingOperations.erase(response.token()); + } + m_operationsCompleteCV.notify_all(); +} + +void ClientPaho::onSubscribeSuccessOnInstance(const MqttSuccess& response) +{ + // ("ClientPaho", "onSubscribeSuccessOnInstance %1 - subscribe with token %2 succeeded", m_clientId, response.token()); + OSDEV_COMPONENTS_SCOPEGUARD(m_operationsCompleteCV, [this]() { m_operationsCompleteCV.notify_all(); }); + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + bool operationOk = false; + OSDEV_COMPONENTS_SCOPEGUARD(m_pendingOperations, [this, &response, &operationOk]() + { + // ("ClientPaho", "onSubscribeSuccessOnInstance %1 - pending operations : %2, removing operation %3", m_clientId, m_pendingOperations, response.token()); + m_operationResult[response.token()] = operationOk; + m_pendingOperations.erase(response.token()); + }); + auto it = m_subscribeTokenToTopic.find(response.token()); + if (m_subscribeTokenToTopic.end() == it) { + // ("ClientPaho", "onSubscribeSuccessOnInstance %1 - unknown token %2", m_clientId, response.token()); + return; + } + auto topic = it->second; + m_subscribeTokenToTopic.erase(it); + + auto pendingIt = m_pendingSubscriptions.find(topic); + if (m_pendingSubscriptions.end() == pendingIt) + { + // ("ClientPaho", "onSubscribeSuccessOnInstance %1 - cannot find pending subscription for token %2", m_clientId, response.token()); + return; + } + if (response.qos() != pendingIt->second.qos) + { + // ("ClientPaho", "onSubscribeSuccessOnInstance %1 - subscription requested qos %2, endpoint assigned qos %3", m_clientId, pendingIt->second.qos, response.qos()); + } + // ("ClientPaho", "onSubscribeSuccessOnInstance %1 - move pending subscription on topic %2 to the registered subscriptions", m_clientId, topic); + m_subscriptions.emplace(std::make_pair(pendingIt->first, std::move(pendingIt->second))); + m_pendingSubscriptions.erase(pendingIt); + operationOk = true; +} + +void ClientPaho::onSubscribeFailureOnInstance(const MqttFailure& response) +{ + // ("ClientPaho", "onSubscribeFailureOnInstance %1 - subscription failed with code %2 (%3)", m_clientId, response.codeToString(), response.message()); + OSDEV_COMPONENTS_SCOPEGUARD(m_operationsCompleteCV, [this]() { m_operationsCompleteCV.notify_all(); }); + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + OSDEV_COMPONENTS_SCOPEGUARD(m_pendingOperations, [this, &response]() + { + // MLOGIC_COMMON_DEBUG("ClientPaho", "onSubscribeFailureOnInstance %1 - pending operations : %2, removing operation %3", m_clientId, m_pendingOperations, response.token()); + m_operationResult[response.token()] = false; + m_pendingOperations.erase(response.token()); + }); + + auto it = m_subscribeTokenToTopic.find(response.token()); + if (m_subscribeTokenToTopic.end() == it) + { + // ("ClientPaho", "onSubscribeFailureOnInstance %1 - unknown token %2", m_clientId, response.token()); + return; + } + auto topic = it->second; + m_subscribeTokenToTopic.erase(it); + + auto pendingIt = m_pendingSubscriptions.find(topic); + if (m_pendingSubscriptions.end() == pendingIt) + { + // ("ClientPaho", "onSubscribeFailureOnInstance %1 - cannot find pending subscription for token %2", m_clientId, response.token()); + return; + } + // ("ClientPaho", "onSubscribeFailureOnInstance %1 - remove pending subscription on topic %2", m_clientId, topic); + m_pendingSubscriptions.erase(pendingIt); +} + +void ClientPaho::onUnsubscribeSuccessOnInstance(const MqttSuccess& response) +{ + // ("ClientPaho", "onUnsubscribeSuccessOnInstance %1 - unsubscribe with token %2 succeeded", m_clientId, response.token()); + + OSDEV_COMPONENTS_SCOPEGUARD(m_operationsCompleteCV, [this]() { m_operationsCompleteCV.notify_all(); }); + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + + // On centos7 the unsubscribe response is a nullptr, so we do not have a valid token. + // As a workaround the last unsubscribe token is stored and is used when no valid token is available. + // This is by no means bullet proof because rapid unsubscribes in succession will overwrite this member + // before the callback on the earlier unsubscribe has arrived. On centos7 the unsubscribes have to be handled + // sequentially (see ClientPaho::unsubscribe)! + auto token = response.token(); + if (-1 == token) + { + token = m_lastUnsubscribe; + m_lastUnsubscribe = -1; + } + + bool operationOk = false; + OSDEV_COMPONENTS_SCOPEGUARD(m_pendingOperations, [this, token, &operationOk]() + { + // ("ClientPaho", "onUnsubscribeSuccessOnInstance %1 - pending operations : %2, removing operation %3", m_clientId, m_pendingOperations, token); + m_operationResult[token] = operationOk; + m_pendingOperations.erase(token); + }); + + auto it = m_unsubscribeTokenToTopic.find(token); + if (m_unsubscribeTokenToTopic.end() == it) + { + // ("ClientPaho", "onUnsubscribeSuccessOnInstance %1 - unknown token %2", m_clientId, token); + return; + } + auto topic = it->second; + m_unsubscribeTokenToTopic.erase(it); + + auto registeredIt = m_subscriptions.find(topic); + if (m_subscriptions.end() == registeredIt) { + // ("ClientPaho", "onUnsubscribeSuccessOnInstance %1 - cannot find subscription for token %2", m_clientId, response.token()); + return; + } + // ("ClientPaho", "onUnsubscribeSuccessOnInstance %1 - remove subscription on topic %2 from the registered subscriptions", m_clientId, topic); + m_subscriptions.erase(registeredIt); + operationOk = true; +} + +void ClientPaho::onUnsubscribeFailureOnInstance(const MqttFailure& response) +{ + // ("ClientPaho", "onUnsubscribeFailureOnInstance %1 - subscription failed with code %2 (%3)", m_clientId, response.codeToString(), response.message()); + OSDEV_COMPONENTS_SCOPEGUARD(m_operationsCompleteCV, [this]() { m_operationsCompleteCV.notify_all(); }); + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + OSDEV_COMPONENTS_SCOPEGUARD(m_pendingOperations, [this, &response]() + { + // ("ClientPaho", "onUnsubscribeFailureOnInstance %1 - pending operations : %2, removing operation %3", m_clientId, m_pendingOperations, response.token()); + m_operationResult[response.token()] = false; + m_pendingOperations.erase(response.token()); + }); + + auto it = m_unsubscribeTokenToTopic.find(response.token()); + if (m_unsubscribeTokenToTopic.end() == it) + { + // ("ClientPaho", "onUnsubscribeFailureOnInstance %1 - unknown token %2", m_clientId, response.token()); + return; + } + auto topic = it->second; + m_unsubscribeTokenToTopic.erase(it); +} + +int ClientPaho::onMessageArrivedOnInstance(const MqttMessage& message) +{ + // ("ClientPaho", "onMessageArrivedOnInstance %1 - received message on topic %2, retained : %3, dup : %4", m_clientId, message.topic(), message.retained(), message.duplicate()); + + std::function cb; + + { + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + for (const auto& s : m_subscriptions) + { + if (boost::regex_match(message.topic(), s.second.topicRegex)) + { + cb = s.second.callback; + } + } + } + + if (cb) + { + cb(message); + } + else + { + // ("ClientPaho", "onMessageArrivedOnInstance %1 - no topic filter found for message received on topic %2", m_clientId, message.topic()); + } + return 1; +} + +void ClientPaho::onDeliveryCompleteOnInstance(MQTTAsync_token token) +{ + // ("ClientPaho", "onDeliveryCompleteOnInstance %1 - message with token %2 is delivered", m_clientId, token); + if (m_deliveryCompleteCallback) + { + m_deliveryCompleteCallback(m_clientId, static_cast(token)); + } +} + +void ClientPaho::onConnectionLostOnInstance(const std::string& cause) +{ + (void)cause; + // ("ClientPaho", "onConnectionLostOnInstance %1 - connection lost (%2)", m_clientId, cause); + setConnectionStatus(ConnectionStatus::ReconnectInProgress); + + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + // Remove all tokens related to subscriptions from the active operations. + for (const auto& p : m_subscribeTokenToTopic) + { + // ("ClientPaho", "onConnectionLostOnInstance %1 - pending operations : %2, removing operation %3", m_clientId, m_pendingOperations, p.first); + m_pendingOperations.erase(p.first); + } + + for (const auto& p : m_unsubscribeTokenToTopic) + { + // ("ClientPaho", "onConnectionLostOnInstance %1 - pending operations : %2, removing operation %3", m_clientId, m_pendingOperations, p.first); + m_pendingOperations.erase(p.first); + } + // Clear the administration used in the subscribe process. + m_subscribeTokenToTopic.clear(); + m_unsubscribeTokenToTopic.clear(); +} + +// static +void ClientPaho::onConnect(void* context, char* cause) +{ + if (context) + { + auto* cl = reinterpret_cast(context); + std::string reason(nullptr == cause ? "unknown cause" : cause); + cl->pushIncomingEvent([cl, reason]() { cl->onConnectOnInstance(reason); }); + } +} + +// static +void ClientPaho::onConnectSuccess(void* context, MQTTAsync_successData* response) +{ + if (context) + { + auto* cl = reinterpret_cast(context); + if (!response) { + // connect should always have a valid response struct. + // ("ClientPaho", "onConnectSuccess - no response data"); + } + MqttSuccess resp(response->token, ConnectionData(response->alt.connect.serverURI, response->alt.connect.MQTTVersion, response->alt.connect.sessionPresent)); + cl->pushIncomingEvent([cl, resp]() { cl->onConnectSuccessOnInstance(resp); }); + } +} + +// static +void ClientPaho::onConnectFailure(void* context, MQTTAsync_failureData* response) +{ + if (context) + { + auto* cl = reinterpret_cast(context); + MqttFailure resp(response); + cl->pushIncomingEvent([cl, resp]() { cl->onConnectFailureOnInstance(resp); }); + } +} + +//// static +//void ClientPaho::onDisconnect(void* context, MQTTProperties* properties, enum MQTTReasonCodes reasonCode) +//{ +// apply_unused_parameters(properties); +// try { +// if (context) { +// auto* cl = reinterpret_cast(context); +// cl->pushIncomingEvent([cl, reasonCode]() { cl->onDisconnectOnInstance(reasonCode); }); +// } +// } +// catch (...) { +// } +// catch (const std::exception& e) { +// MLOGIC_COMMON_ERROR("ClientPaho", "onDisconnect - exception : %1", e.what()); +// } +// catch (...) { +// MLOGIC_COMMON_ERROR("ClientPaho", "onDisconnect - unknown exception"); +// } +//} + +// static +void ClientPaho::onDisconnectSuccess(void* context, MQTTAsync_successData* response) +{ + if (context) + { + auto* cl = reinterpret_cast(context); + MqttSuccess resp(response ? response->token : 0); + cl->pushIncomingEvent([cl, resp]() { cl->onDisconnectSuccessOnInstance(resp); }); + } +} + +// static +void ClientPaho::onDisconnectFailure(void* context, MQTTAsync_failureData* response) +{ + if (context) + { + auto* cl = reinterpret_cast(context); + MqttFailure resp(response); + cl->pushIncomingEvent([cl, resp]() { cl->onDisconnectFailureOnInstance(resp); }); + } +} + +// static +void ClientPaho::onPublishSuccess(void* context, MQTTAsync_successData* response) +{ + if (context) + { + auto* cl = reinterpret_cast(context); + if (!response) + { + // publish should always have a valid response struct. + // toLogFile ("ClientPaho", "onPublishSuccess - no response data"); + } + MqttSuccess resp(response->token, MqttMessage(response->alt.pub.destinationName == nullptr ? "null" : response->alt.pub.destinationName, response->alt.pub.message)); + cl->pushIncomingEvent([cl, resp]() { cl->onPublishSuccessOnInstance(resp); }); + } +} + +// static +void ClientPaho::onPublishFailure(void* context, MQTTAsync_failureData* response) +{ + (void)response; + if (context) + { + auto* cl = reinterpret_cast(context); + MqttFailure resp(response); + cl->pushIncomingEvent([cl, resp]() { cl->onPublishFailureOnInstance(resp); }); + } +} + +// static +void ClientPaho::onSubscribeSuccess(void* context, MQTTAsync_successData* response) +{ + if (context) + { + auto* cl = reinterpret_cast(context); + if (!response) + { + // subscribe should always have a valid response struct. + // MLOGIC_COMMON_FATAL("ClientPaho", "onSubscribeSuccess - no response data"); + } + MqttSuccess resp(response->token, response->alt.qos); + cl->pushIncomingEvent([cl, resp]() { cl->onSubscribeSuccessOnInstance(resp); }); + } +} + +// static +void ClientPaho::onSubscribeFailure(void* context, MQTTAsync_failureData* response) +{ + if (context) + { + auto* cl = reinterpret_cast(context); + MqttFailure resp(response); + cl->pushIncomingEvent([cl, resp]() { cl->onSubscribeFailureOnInstance(resp); }); + } +} + +// static +void ClientPaho::onUnsubscribeSuccess(void* context, MQTTAsync_successData* response) +{ + if (context) + { + auto* cl = reinterpret_cast(context); + MqttSuccess resp(response ? response->token : -1); + cl->pushIncomingEvent([cl, resp]() { cl->onUnsubscribeSuccessOnInstance(resp); }); + } +} + +// static +void ClientPaho::onUnsubscribeFailure(void* context, MQTTAsync_failureData* response) +{ + if (context) + { + auto* cl = reinterpret_cast(context); + MqttFailure resp(response); + cl->pushIncomingEvent([cl, resp]() { cl->onUnsubscribeFailureOnInstance(resp); }); + } +} + +// static +int ClientPaho::onMessageArrived(void* context, char* topicName, int, MQTTAsync_message* message) +{ + + OSDEV_COMPONENTS_SCOPEGUARD(freeMessage, [&topicName, &message]() + { + MQTTAsync_freeMessage(&message); + MQTTAsync_free(topicName); + }); + + if (context) + { + auto* cl = reinterpret_cast(context); + MqttMessage msg(topicName, *message); + cl->pushIncomingEvent([cl, msg]() { cl->onMessageArrivedOnInstance(msg); }); + } + + return 1; // always return true. Otherwise this callback is triggered again. +} + +// static +void ClientPaho::onDeliveryComplete(void* context, MQTTAsync_token token) +{ + if (context) + { + auto* cl = reinterpret_cast(context); + cl->pushIncomingEvent([cl, token]() { cl->onDeliveryCompleteOnInstance(token); }); + } +} + +// static +void ClientPaho::onConnectionLost(void* context, char* cause) +{ + OSDEV_COMPONENTS_SCOPEGUARD(freeCause, [&cause]() + { + if (cause) + { + MQTTAsync_free(cause); + } + }); + + if (context) + { + auto* cl = reinterpret_cast(context); + std::string msg(nullptr == cause ? "cause unknown" : cause); + cl->pushIncomingEvent([cl, msg]() { cl->onConnectionLostOnInstance(msg); }); + } +} + +// static +void ClientPaho::onLogPaho(enum MQTTASYNC_TRACE_LEVELS level, char* message) +{ + (void)message; + switch (level) + { + case MQTTASYNC_TRACE_MAXIMUM: + case MQTTASYNC_TRACE_MEDIUM: + case MQTTASYNC_TRACE_MINIMUM: { + // ("ClientPaho", "paho - %1", message) + break; + } + case MQTTASYNC_TRACE_PROTOCOL: { + // ("ClientPaho", "paho - %1", message) + break; + } + case MQTTASYNC_TRACE_ERROR: + case MQTTASYNC_TRACE_SEVERE: + case MQTTASYNC_TRACE_FATAL: { + // ("ClientPaho", "paho - %1", message) + break; + } + } +} diff --git b/src/clientpaho.h a/src/clientpaho.h new file mode 100644 index 0000000..40b4d61 --- /dev/null +++ a/src/clientpaho.h @@ -0,0 +1,334 @@ +#ifndef OSDEV_COMPONENTS_MQTT_CLIENTPAHO_H +#define OSDEV_COMPONENTS_MQTT_CLIENTPAHO_H + +// std +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// boost +#include + +// paho +#include + +// osdev::components::mqtt +#include "synchronizedqueue.h" +#include "imqttclientimpl.h" +#include "mqttfailure.h" +#include "mqttsuccess.h" + +namespace osdev { +namespace components { +namespace mqtt { + +/** + * @brief Wrapper class for the paho-c library. + * This implementation uses the clean session flag and recreates subscriptions on reconnect. + * + * The implementation allows multiple subscriptions as long as the subscriptions do not have overlap. For mqtt 3 it is not + * possible to track down the subscription based on the incoming message meta information. By matching the topic against + * the various topic filters a subscription can be identified (but only when there is only one subscription that matches). + */ +class ClientPaho : public IMqttClientImpl +{ +public: + /** + * @brief Construct a ClientPaho instance. + * @param endpoint The endpoint to connect to + * @param id The clientId that is used in the connection. + * @param connectionStatusCallback The callback on which connection status information is communicated. + * @param deliveryCompleteCallback Callback that is called with the publish message tokens for messages that are delivered. + * Being delivered has different meanings depending on the quality of service. + */ + ClientPaho(const std::string& endpoint, + const std::string& id, + const std::function& connectionStatusCallback, + const std::function& deliveryCompleteCallback); + virtual ~ClientPaho() override; + + // Non copyable, non movable. + ClientPaho(const ClientPaho&) = delete; + ClientPaho& operator=(const ClientPaho&) = delete; + ClientPaho(ClientPaho&&) = delete; + ClientPaho& operator=(ClientPaho&&) = delete; + + /** + * @see IMqttClientImpl + */ + virtual std::string clientId() const override; + + /** + * @see IMqttClientImpl + */ + virtual ConnectionStatus connectionStatus() const override; + + /** + * @see IMqttClientImpl + */ + virtual std::int32_t connect(bool wait) override; + + /** + * @see IMqttClientImpl + */ + virtual std::int32_t disconnect(bool wait, int timeoutMs) override; + + /** + * @see IMqttClientImpl + */ + virtual std::int32_t publish(const MqttMessage& message, int qos) override; + + /** + * @see IMqttClientImpl + */ + virtual void publishPending() override; + + /** + * @see IMqttClientImpl + */ + virtual std::int32_t subscribe(const std::string& topic, int qos, const std::function& cb) override; + + /** + * @see IMqttClientImpl + */ + virtual void resubscribe() override; + + /** + * @see IMqttClientImpl + */ + virtual std::int32_t unsubscribe(const std::string& topic, int qos) override; + + /** + * @see IMqttClientImpl + */ + virtual void unsubscribeAll() override; + + /** + * @see IMqttClientImpl + */ + virtual std::chrono::milliseconds waitForCompletion(std::chrono::milliseconds waitFor, const std::set& tokens) const override; + + /** + * @see IMqttClientImpl + */ + virtual bool isOverlapping(const std::string& topic) const override; + + /** + * @see IMqttClientImpl + */ + virtual bool isOverlapping(const std::string& topic, std::string& existingTopic) const override; + + /** + * @see IMqttClientImpl + */ + virtual std::vector pendingOperations() const override; + + /** + * @see IMqttClientImpl + */ + virtual bool hasPendingSubscriptions() const override; + + /** + * @see IMqttClientImpl + */ + virtual boost::optional operationResult(std::int32_t token) const override; + +private: + void parseEndpoint(const std::string& endpoint); + + std::int32_t publishInternal(const MqttMessage& message, int qos); + std::int32_t subscribeInternal(const std::string& topic, int qos); + + void setConnectionStatus(ConnectionStatus status); + bool isOverlappingInternal(const std::string& topic, std::string& existingTopic) const; + + /** + * @brief Internal struct for subscriber information. + * Used to store subscriptions. + */ + struct Subscription + { + int qos; + boost::regex topicRegex; + std::function callback; + }; + + /** + * @brief Internal struct for publisher information. + * Used to store pending publishes during reconnection. + */ + struct Publish + { + int qos; + MqttMessage data; + }; + + /** + * @brief Add an incoming callback event to the synchronized queue. + * @param ev A function object that calls one of the event handlers on a ClientPaho instance, but other types of actions are also possible. + */ + void pushIncomingEvent(std::function ev); + + /** + * @brief Worker method that executes the events. + */ + void callbackEventHandler(); + + /** + * @brief Callback method that is called when a reconnect succeeds. + * @param cause The cause of the original disconnect. + */ + void onConnectOnInstance(const std::string& cause); + + /** + * @brief Callback that is called when a connect call succeeds. + * This callback is also called when a reconnect succeeds because the paho library reuses the initial connect command! + * The connection status is set to Connected. + * @param response A success response with connection data. + */ + void onConnectSuccessOnInstance(const MqttSuccess& response); + + /** + * @brief Callback that is called when a connect call fails after being sent to the endpoint. + * This callback is also called when a reconnect fails because the paho library reuses the initial connect command! + * The connection status is set to Disconnected when the connection state is ConnectInProgress, othwerwise the connection status is left as is. + * @param response A failure response. + */ + void onConnectFailureOnInstance(const MqttFailure& response); + + //void onDisconnectOnInstance(enum MQTTReasonCodes reasonCode); for MQTT V5 which is not supported by centos7 paho-c + + /** + * @brief Callback that is called when a disconnect call succeeds. + * The connection status is set to Disconnected. + * @param response A success response with no specific data. + */ + void onDisconnectSuccessOnInstance(const MqttSuccess& response); + + /** + * @brief Callback that is called when a disconnect call fails after being sent to the endpoint. + * Based on the result returned by the paho library The connection status is set to Disconnected or Connected. + * @param response A failure response. + */ + void onDisconnectFailureOnInstance(const MqttFailure& response); + + /** + * @brief Callback that is called when a publish call succeeds. + * This callback is called before the delivery complete callback. + * @param response A success response with the published message. + */ + void onPublishSuccessOnInstance(const MqttSuccess& response); + + /** + * @brief Callback that is called when a publish call fails after being sent to the endpoint. + * @param response A failure response. + */ + void onPublishFailureOnInstance(const MqttFailure& response); + + /** + * @brief Callback that is called when a subscribe call succeeds. + * @param response A success response with the subscription information. The actual used qos is conveyed in this response. + */ + void onSubscribeSuccessOnInstance(const MqttSuccess& response); + + /** + * @brief Callback that is called when a subscribe call fails after being sent to the endpoint. + * @param response A failure response. + */ + void onSubscribeFailureOnInstance(const MqttFailure& response); + + /** + * @brief Callback that is called when an unsubscribe call succeeds. + * @param response A success response with no specific data. + */ + void onUnsubscribeSuccessOnInstance(const MqttSuccess& response); + + /** + * @brief Callback that is called when an unsubscribe call fails after being sent to the endpoint. + * @param response A failure response. + */ + void onUnsubscribeFailureOnInstance(const MqttFailure& response); + + /** + * @brief Callback that is called when a message is received. + * @param message The message payload and meta data. + */ + int onMessageArrivedOnInstance(const MqttMessage& message); + + /** + * @brief Callback that is called when the delivery of a publish message is considered complete. + * The definition of complete depends on the quality of service used in the publish command. + * @param token The token with the publish command is sent. + */ + void onDeliveryCompleteOnInstance(MQTTAsync_token token); + + /** + * @brief Callback that is called when the connection is broken. + * @param cause The reason string. Always "cause unknown" for mqtt3 endpoints. + */ + void onConnectionLostOnInstance(const std::string& cause); + + // Static callback functions that are registered on the paho library. Functions call their *OnInstance() counterparts. + static void onConnect(void* context, char* cause); + static void onConnectSuccess(void* context, MQTTAsync_successData* response); + static void onConnectFailure(void* context, MQTTAsync_failureData* response); + //static void onDisconnect(void* context, MQTTProperties* properties, enum MQTTReasonCodes reasonCode); for MQTT V5 which is not supported by centos7 paho-c + static void onDisconnectSuccess(void* context, MQTTAsync_successData* response); + static void onDisconnectFailure(void* context, MQTTAsync_failureData* response); + static void onPublishSuccess(void* context, MQTTAsync_successData* response); + static void onPublishFailure(void* context, MQTTAsync_failureData* response); + static void onSubscribeSuccess(void* context, MQTTAsync_successData* response); + static void onSubscribeFailure(void* context, MQTTAsync_failureData* response); + static void onUnsubscribeSuccess(void* context, MQTTAsync_successData* response); + static void onUnsubscribeFailure(void* context, MQTTAsync_failureData* response); + static int onMessageArrived(void* context, char* topicName, int topicLen, MQTTAsync_message* message); + static void onDeliveryComplete(void* context, MQTTAsync_token token); + static void onConnectionLost(void* context, char* cause); + + /** + * @brief Connects the paho logging to the mlogic logging system. + * This callback is registered the first time a ClientPaho instance is constructed. + */ + static void onLogPaho(enum MQTTASYNC_TRACE_LEVELS level, char* message); + + mutable std::mutex m_mutex; + std::string m_endpoint; + std::string m_username; + std::string m_password; + std::string m_clientId; + std::set m_pendingOperations; + std::map m_operationResult; + mutable std::condition_variable m_operationsCompleteCV; + std::map m_subscriptions; + std::map m_pendingSubscriptions; + std::map m_subscribeTokenToTopic; + std::map m_unsubscribeTokenToTopic; + std::deque m_pendingPublishes; + bool m_processPendingPublishes; + mutable std::condition_variable m_pendingPublishesReadyCV; + ::MQTTAsync m_client; + std::atomic m_connectionStatus; + std::function m_connectionStatusCallback; + std::function m_deliveryCompleteCallback; + MQTTAsync_token m_lastUnsubscribe; ///< centos7 workaround + std::unique_ptr> m_connectPromise; + std::unique_ptr> m_disconnectPromise; + + SynchronizedQueue> m_callbackEventQueue; + std::thread m_workerThread; + + static std::atomic_int s_numberOfInstances; +}; + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_CLIENTPAHO_H diff --git b/src/commondefs.cpp a/src/commondefs.cpp new file mode 100644 index 0000000..2538c3e --- /dev/null +++ a/src/commondefs.cpp @@ -0,0 +1,82 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "commondefs.h" + +// std +#include +#include + +namespace osdev { +namespace components { +namespace mqtt { + +std::ostream& operator<<(std::ostream& os, const StdTimeMs& rhs) +{ + const std::time_t rhsTimeT = std::chrono::system_clock::to_time_t(rhs); + struct tm tms + { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, nullptr + }; + if (nullptr == gmtime_r(&rhsTimeT, &tms)) { + os << "unknown"; + } + else { + char str[26] = { 0 }; + if (std::strftime(str, sizeof(str), "%Y-%m-%d %H:%M:%S.", &tms) > 0) { + std::ostringstream oss; + oss << str << std::setfill('0') << std::setw(3) << std::chrono::duration_cast(rhs.timePoint - std::chrono::system_clock::from_time_t(rhsTimeT)).count() << ' '; + if (std::strftime(str, sizeof(str), "%z", &tms) == 0) { + return os; + } + oss << str; + os << oss.str(); + } + } + return os; +} + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +namespace std { + +std::ostream& operator<<(std::ostream& os, const osdev::components::mqtt::StdTime& rhs) +{ + /// @todo: Use put_time when it's implemented in gcc (5.0) + // auto rhsTimeT = std::chrono::system_clock::to_time_t(rhs); + // os << std::put_time(std::localtime(&rhsTimeT), "%F %T"); + const std::time_t rhsTimeT = std::chrono::system_clock::to_time_t(rhs); + struct tm tms + { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, nullptr + }; + if (nullptr == gmtime_r(&rhsTimeT, &tms)) { + os << "unknown"; + } + else { + char str[26]; + if (std::strftime(str, sizeof(str), "%Y-%m-%d %H:%M:%S %z", &tms) > 0) { + os << str; + } + } + return os; +} + +} // End namespace std diff --git b/src/commondefs.h a/src/commondefs.h new file mode 100644 index 0000000..1bcd396 --- /dev/null +++ a/src/commondefs.h @@ -0,0 +1,108 @@ +#ifndef OSDEV_COMPONENTS_MQTT_COMMONDEFS_H +#define OSDEV_COMPONENTS_MQTT_COMMONDEFS_H + +// std +#include +#include +#include +#include +#include +#include +#include + +// boost +#include +#include +#include +#include + +#include "utils.h" + + +/// @brief Check if an id is valid +/// @throws InvalidArgumentException if id is invalid +#define OSDEV_COMPONENTS_CHECKMQTTID(id) \ + [&] { \ + if (id.is_nil()) { \ + } \ + }() + + +namespace osdev { +namespace components { +namespace mqtt { + +using MqttId = boost::uuids::uuid; +using OptionalId = MqttId; +using MqttIdSet = std::set; +using MqttIdSetIterator = MqttIdSet::const_iterator; +using MqttIdSetDelta = std::pair; +using StdTime = std::chrono::system_clock::time_point; +using OptionalTime = boost::optional; +using StdTimeVec = std::vector; +using SequenceNumber = std::uint32_t; +using OptionalSequenceNumber = boost::optional; +using CustomField = std::string; +using CustomFieldCollection = std::vector; + +using CountryCodeEnum = std::int32_t; + +/** + * @brief Defines a parsed uri. + */ +using ParsedUri = std::map; + +/** + * @brief Type for the parsed query part of a uri. + */ +using ParsedQuery = std::map; + +/** + * @brief Type for the parsed path part of a uri. + */ +using ParsedPath = std::vector; + +/** + * @brief Defines a duration with the granularity of a day in seconds (24 * 60 * 60 = 86400). + * This duration can be used to create a time_point at midnight of a given DateTime amongst others. + * + * The representation is a signed type so that negative durations are also possible. + */ +using days = std::chrono::duration>; + +/** + * @brief Defines a duration with the granularity of a year in seconds. A year is a bit longer than 365 days (365.2425). If a year is + * subtracted from a date the time part of the new date will therefore differ from the time part of the subtracted from date. + * + * The representation is a signed type so that negative durations are also possible. + */ +using years = std::chrono::duration>; // excactly 365 days would give 31536000 + +/** + * A timepoint type that is printed with millisecond resolution. + */ +struct StdTimeMs +{ + StdTimeMs(const StdTime& tp) + : timePoint(tp) + { + } + + operator StdTime() const { return timePoint; } + + StdTime timePoint; +}; + +std::ostream& operator<<(std::ostream& os, const StdTimeMs& rhs); + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +namespace std { + +std::ostream& operator<<(std::ostream& os, const osdev::components::mqtt::StdTime& rhs); + +} // End namespace std + +#endif // OSDEV_COMPONENTS_MQTT_COMMONDEFS_H diff --git b/src/compat-c++14.h a/src/compat-c++14.h new file mode 100644 index 0000000..07055ba --- /dev/null +++ a/src/compat-c++14.h @@ -0,0 +1,72 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#ifndef OSDEV_COMPONENTS_COMPATCXX14 +#define OSDEV_COMPONENTS_COMPATCXX14 + +#include + +// The below code must be skipped if we use a C++14 or newer compiler +#if __cplusplus == 201103L + +namespace std { +/// Copied from libstdc++ 4.9.2 bits/unique_ptr.h + +template +struct _MakeUniq +{ + typedef unique_ptr<_Tp> __single_object; +}; + +template +struct _MakeUniq<_Tp[]> +{ + typedef unique_ptr<_Tp[]> __array; +}; + +template +struct _MakeUniq<_Tp[_Bound]> +{ + struct __invalid_type + { + }; +}; + +/// std::make_unique for single objects +template +inline typename _MakeUniq<_Tp>::__single_object make_unique(_Args&&... __args) +{ + return unique_ptr<_Tp>(new _Tp(std::forward<_Args>(__args)...)); +} + +/// std::make_unique for arrays of unknown bound +template +inline typename _MakeUniq<_Tp>::__array make_unique(size_t __num) +{ + return unique_ptr<_Tp>(new typename remove_extent<_Tp>::type[__num]()); +} + +/// Disable std::make_unique for arrays of known bound +template +inline typename _MakeUniq<_Tp>::__invalid_type make_unique(_Args&&...) = delete; + +} // End namespace std + +#endif // Check for C++14 + +#endif diff --git b/src/compat-chrono.h a/src/compat-chrono.h new file mode 100644 index 0000000..29a29c3 --- /dev/null +++ a/src/compat-chrono.h @@ -0,0 +1,55 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include +#include + +// function std::chrono::ceil is added in the c++17 standard. +// Added implementation from https://en.cppreference.com/w/cpp/chrono/duration/ceil +// to this compatibility header. The header only defines this implementation when +// the c++ compiler is used with an older standard. + +#if !defined __cpp_lib_chrono || __cpp_lib_chrono < 201611 + +namespace std { +namespace chrono { + +template +struct is_duration : std::false_type +{ +}; +template +struct is_duration< + std::chrono::duration> : std::true_type +{ +}; + +template {}>::type> +To ceil(const std::chrono::duration& d) +{ + To t = std::chrono::duration_cast(d); + if (t < d) + return t + To{ 1 }; + return t; +} + +} // End namespace chrono +} // End namespace std + +#endif diff --git b/src/compiletimedigits.h a/src/compiletimedigits.h new file mode 100644 index 0000000..4240659 --- /dev/null +++ a/src/compiletimedigits.h @@ -0,0 +1,118 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#ifndef OSDEV_COMPONENTS_MQTT_COMPILETIMEDIGITS_H +#define OSDEV_COMPONENTS_MQTT_COMPILETIMEDIGITS_H + +// std +#include + +#include "compiletimestring.h" + +namespace osdev { +namespace components { +namespace mqtt { + +/** + * @brief Calculate the number of digits needed to represent a given value in a given base. + * @tparam B The base to use. + * @param value The value to represent. + * @return The number of digits. + */ +template +constexpr std::size_t numberOfDigits(unsigned value) noexcept +{ + static_assert(B > 0, "base must be larger than zero"); + return (value / B) == 0 ? 1 : (1 + numberOfDigits(value / B)); +} + +/** + * @return stringified digit that represents the given single digit value. + * @note Values can range from 0 to 37 inclusive which maps on 0-9A-Z. + */ +template +constexpr char digit() noexcept +{ + static_assert(value <= 37, "Value must lie between 0 and 37 both inclusive"); + return (value < 10 ? '0' + static_cast(value) : 'A' + static_cast(value) - 10); +} + +/** + * @brief Recursive template definition that is used to determine the value of a given number N in a given base B. + * @tparam N The number to obtain the representation for. + * @tparam B The base to use. + * @tparam Len The length of the representation including the 0 terminator. + * If not provided the length is calculated on first instantiation of this template. + * @tparam remains Empty parameter pack on first instantiation. Will be filled further by every following instantiation. + */ +template (N) + 1, unsigned... remains> +struct Digits +{ + + static constexpr auto create() noexcept -> std::array + { + static_assert(B > 0, "base must be larger than zero"); + return Digits(), remains...>::create(); + } +}; + +/** + * @brief Termination template that will return the actual hex digit array that represents the given value. + * @tparam B The base to use. + * @tparam Len The length of the hexadecimal representation including 0 terminator. + * @tparam remains Parameter pack that contains the hexadecimal character representations. + */ +template +struct Digits<0, B, Len, remains...> +{ + + static constexpr auto create() noexcept -> std::array + { + static_assert(sizeof...(remains) + 1 == Len, "Parameter 'Len' must be equal to the length of the parameter pack 'remains' including the termination zero"); + return std::array{ { remains..., 0 } }; + } +}; + +/** + * @brief Digits builder can be used in combination with the compiletime_string. + * @tparam N The number to obtain the compile time string representation for. + * @tparam B The base to use (up to 37). + * This template struct defines a produce() method and the return type of that method. + */ +template +struct ProduceDigits +{ + /** + * @brief Return type definition of the produce method. + */ + using return_t = std::array(N) + 1>; + + /** + * @brief Produce the actual representation. + */ + static constexpr return_t produce() noexcept + { + return Digits::create(); + } +}; + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_COMPILETIMEDIGITS_H diff --git b/src/compiletimestring.h a/src/compiletimestring.h new file mode 100644 index 0000000..70c9d27 --- /dev/null +++ a/src/compiletimestring.h @@ -0,0 +1,155 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#ifndef OSDEV_COMPONENTS_MQTT_COMPILETIMESTRING_H +#define OSDEV_COMPONENTS_MQTT_COMPILETIMESTRING_H + +/// @file +/// +/// Support for compiletime string building. +/// +/// In some cases it is necessary to embed string messages as read only string literals +/// so that they can be used in low memory conditions for example. +/// This header defines two macro's that can be used to build managed string literals. +/// These managed string literals can be concatenated and searched at compiletime. +/// When the compiler is done with this code only the embedded string literal that are +/// used by normal expressions remain in the binary. +/// The implementation is a mix of meta template programming and the use of constexpr +/// expressions and is based on an idea found on stackoverflow. +/// This code is meant to handle small strings (up to say 100 characters). Larger strings can trigger the maximum +/// template recursion depth of the compiler (-ftemplate-depth). This depth is set at 900. So in principle strings +/// of 900 characters can be handled. However this is very inefficient and will prolong compiletime severly. +/// +/// Here follows an example of how this code can be used. +/// @code +/// auto str1 = OSDEV_COMPONENTS_CSTRING("This is a test"); +/// auto str2 = OSDEV_COMPONENTS_CSTRING(" with concatenation"); +/// auto str3 = str1 + str2; +/// const char* s = str3.chars; +/// @endcode +/// str3.chars contains the compiletime concatenated string. +/// +/// When examing the binary one will see that it contains the null terminated string literal +/// @code +/// This is a test with concatenation^@ +/// @endcode +/// The str1 and str2 literals are discarded since they are never used in a non compiletime way. +/// @note This code is not meant to handle string literals with internal \0 characters. Beware! + +#include + +#include "metaprogrammingdefs.h" + +/// @brief Build a managed substring string literal from a string literal input +/// The strings are build compile time. +/// The upper bound is one past the last character that the caller is interested in. +#define OSDEV_COMPONENTS_CSTRING_BOUNDED(string_literal, lower, upper) \ + [] { \ + constexpr std::size_t lb = (lower); \ + constexpr std::size_t ub = (upper); \ + static_assert(lb <= ub, "lower bound must be smaller than or equal to upper bound"); \ + static_assert(ub < sizeof(string_literal), \ + "upper bound must be smaller than or equal to the length of the string"); \ + struct constexpr_string_type \ + { \ + const char* chars = string_literal; \ + }; \ + return typename osdev::components::mqtt::apply_bounded_range::template produce>::result{}; \ + }() + +/// @brief Build a managed string literal from a string literal input +#define OSDEV_COMPONENTS_CSTRING(string_literal) \ + OSDEV_COMPONENTS_CSTRING_BOUNDED(string_literal, 0, sizeof(string_literal) - 1) + +namespace osdev { +namespace components { +namespace mqtt { + +/// @brief Managed string literal. +/// This class is used to hold string literals at compile time. +template +struct compiletime_string +{ + /// @brief Declaration of the string + static constexpr const char chars[sizeof...(str) + 1] = { str..., '\0' }; +}; + +/// @brief Definition of the string +template +constexpr const char compiletime_string::chars[sizeof...(str) + 1]; + +/// @brief Managed string literal builder. +/// This class is used to build string literals at compile time. +template +struct string_builder +{ + /// @brief maps indices list on the char array + template + struct produce + { + typedef compiletime_string result; + }; +}; + +/// @brief Adapter for coupling other producers to string_builder. +/// tparam T The type of producer to adapt. +/// @note The adapter must define its return type as return_t and provide a static method produce() which produces the result. +/// The return type must be a kind of array type of chars (char[len], std::array, etc). +template +struct adapt_for_string_builder +{ + static constexpr typename T::return_t chars = T::produce(); +}; + +/// @brief Concatenate two managed string literals +/// The literals are packed in a variadic template. +/// These templates are formed by the MLOGIC_COMMON_CSTRING* macro's +/// The concatenation is done at compiletime. +template +compiletime_string operator+(compiletime_string, compiletime_string) +{ + return {}; +} + +/// @brief Search for a character in a string literal from the right side. +/// The character index is returned relative to the left side. +/// If the character is not found a std::size_t(-1) is returned. +/// The default search starts at the end of the string. The index +/// can be given to start the search at another point in the string. +/// The index is given as nr of characters from the right end side of +/// the string. To proceed with a search see following example. +/// @code +/// constexpr const char s[] = "This is a test"; +/// constexpr std::size_t index = osdev_components::mqtt::rfind(s, 'i'); // gives 5 +/// static_assert(index == 5, ""); +/// constexpr std::size_t index2 = osdev::components::mqtt::rfind(s, 'i', sizeof(s) - index); // gives 2 +/// static_assert(index2 == 2, ""); +/// @endcode +/// This function should only be used at compile time! +template +constexpr std::size_t rfind(const char (&str)[N], char c, std::size_t index = 0) +{ + return index >= N ? std::numeric_limits::max() : str[N - index - 1] == c ? N - index - 1 : rfind(str, c, index + 1); +} + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_COMPILETIMESTRING_H diff --git b/src/connectionstatus.cpp a/src/connectionstatus.cpp new file mode 100644 index 0000000..fd2305c --- /dev/null +++ a/src/connectionstatus.cpp @@ -0,0 +1,39 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "connectionstatus.h" + +using namespace osdev::components::mqtt; + +std::ostream& operator<<(std::ostream &os, ConnectionStatus rhs) +{ + switch( rhs ) + { + case ConnectionStatus::Disconnected: + return os << "Disconnected"; + case ConnectionStatus::DisconnectInProgress: + return os << "DisconnectInProgress"; + case ConnectionStatus::ConnectInProgress: + return os << "ConnectInProgress"; + case ConnectionStatus::ReconnectInProgress: + return os << "ReconnectInProgress"; + case ConnectionStatus::Connected: + return os << "Connected"; + } + return os << "Unknown"; // Should never be reached. +} diff --git b/src/connectionstatus.h a/src/connectionstatus.h new file mode 100644 index 0000000..caf00cf --- /dev/null +++ a/src/connectionstatus.h @@ -0,0 +1,32 @@ +#ifndef OSDEV_COMPONENTS_MQTT_CONNECTIONSTATUS_H +#define OSDEV_COMPONENTS_MQTT_CONNECTIONSTATUS_H + +// std +#include + +namespace osdev { +namespace components { +namespace mqtt { + +/*! + * \brief Enumeration for MQTT connection Status + */ +enum class ConnectionStatus +{ + Disconnected, ///< Client is disconnected. + DisconnectInProgress, ///< Client is being disconnected. + ConnectInProgress, ///< Client is being connected. + ReconnectInProgress, ///< Client tries to reconnect. + Connected, ///< Client is connected. +}; + +/*! + * \brief Stream operator for the connection status + */ +std::ostream& operator<<(std::ostream &os, ConnectionStatus rhs); + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_CONNECTIONSTATUS_H diff --git b/src/credentials.cpp a/src/credentials.cpp new file mode 100644 index 0000000..9df0bda --- /dev/null +++ a/src/credentials.cpp @@ -0,0 +1,31 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "credentials.h" + +using namespace osdev::components::mqtt; + +Credentials::Credentials() + : m_username() + , m_password() +{} + +Credentials::Credentials( const std::string &username, const std::string &password ) + : m_username( username ) + , m_password( password ) +{} diff --git b/src/credentials.h a/src/credentials.h new file mode 100644 index 0000000..fb6cb13 --- /dev/null +++ a/src/credentials.h @@ -0,0 +1,40 @@ +#ifndef OSDEV_COMPONENTS_MQTT_CREDENTIALS_H +#define OSDEV_COMPONENTS_MQTT_CREDENTIALS_H + +// std +#include + +namespace osdev { +namespace components { +namespace mqtt { + +/*! + * \brief Class that holds user credentials + */ +class Credentials +{ +public: + /*! + * \brief Default CTor, empty credentials + */ + Credentials(); + + /*! + * \brief Constructor for username/password credentials + * \param username - The username + * \param password - The password + */ + Credentials(const std::string &username, const std::string &password); + + const std::string& username() const { return m_username; } + const std::string& password() const { return m_password; } +private: + std::string m_username; + std::string m_password; +}; + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_CREDENTIALS_H diff --git b/src/date.h a/src/date.h new file mode 100644 index 0000000..8e4ee81 --- /dev/null +++ a/src/date.h @@ -0,0 +1,8235 @@ +#ifndef DATE_H +#define DATE_H + +// The MIT License (MIT) +// +// Copyright (c) 2015, 2016, 2017 Howard Hinnant +// Copyright (c) 2016 Adrian Colomitchi +// Copyright (c) 2017 Florian Dang +// Copyright (c) 2017 Paul Thompson +// Copyright (c) 2018, 2019 Tomasz Kamiński +// Copyright (c) 2019 Jiangang Zhuang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Our apologies. When the previous paragraph was written, lowercase had not yet +// been invented (that would involve another several millennia of evolution). +// We did not mean to shout. + +#ifndef HAS_STRING_VIEW +# if __cplusplus >= 201703 || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) +# define HAS_STRING_VIEW 1 +# else +# define HAS_STRING_VIEW 0 +# endif +#endif // HAS_STRING_VIEW + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if HAS_STRING_VIEW +# include +#endif +#include +#include + +#ifdef __GNUC__ +# pragma GCC diagnostic push +# if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ > 7) +# pragma GCC diagnostic ignored "-Wpedantic" +# endif +# if __GNUC__ < 5 + // GCC 4.9 Bug 61489 Wrong warning with -Wmissing-field-initializers +# pragma GCC diagnostic ignored "-Wmissing-field-initializers" +# endif +#endif + +#ifdef _MSC_VER +# pragma warning(push) +// warning C4127: conditional expression is constant +# pragma warning(disable : 4127) +#endif + +namespace date +{ + +//---------------+ +// Configuration | +//---------------+ + +#ifndef ONLY_C_LOCALE +# define ONLY_C_LOCALE 0 +#endif + +#if defined(_MSC_VER) && (!defined(__clang__) || (_MSC_VER < 1910)) +// MSVC +# ifndef _SILENCE_CXX17_UNCAUGHT_EXCEPTION_DEPRECATION_WARNING +# define _SILENCE_CXX17_UNCAUGHT_EXCEPTION_DEPRECATION_WARNING +# endif +# if _MSC_VER < 1910 +// before VS2017 +# define CONSTDATA const +# define CONSTCD11 +# define CONSTCD14 +# define NOEXCEPT _NOEXCEPT +# else +// VS2017 and later +# define CONSTDATA constexpr const +# define CONSTCD11 constexpr +# define CONSTCD14 constexpr +# define NOEXCEPT noexcept +# endif + +#elif defined(__SUNPRO_CC) && __SUNPRO_CC <= 0x5150 +// Oracle Developer Studio 12.6 and earlier +# define CONSTDATA constexpr const +# define CONSTCD11 constexpr +# define CONSTCD14 +# define NOEXCEPT noexcept + +#elif __cplusplus >= 201402 +// C++14 +# define CONSTDATA constexpr const +# define CONSTCD11 constexpr +# define CONSTCD14 constexpr +# define NOEXCEPT noexcept +#else +// C++11 +# define CONSTDATA constexpr const +# define CONSTCD11 constexpr +# define CONSTCD14 +# define NOEXCEPT noexcept +#endif + +#ifndef HAS_UNCAUGHT_EXCEPTIONS +# if __cplusplus >= 201703 || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) +# define HAS_UNCAUGHT_EXCEPTIONS 1 +# else +# define HAS_UNCAUGHT_EXCEPTIONS 0 +# endif +#endif // HAS_UNCAUGHT_EXCEPTIONS + +#ifndef HAS_VOID_T +# if __cplusplus >= 201703 || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) +# define HAS_VOID_T 1 +# else +# define HAS_VOID_T 0 +# endif +#endif // HAS_VOID_T + +// Protect from Oracle sun macro +#ifdef sun +# undef sun +#endif + +// Work around for a NVCC compiler bug which causes it to fail +// to compile std::ratio_{multiply,divide} when used directly +// in the std::chrono::duration template instantiations below +namespace detail { +template +using ratio_multiply = decltype(std::ratio_multiply{}); + +template +using ratio_divide = decltype(std::ratio_divide{}); +} // namespace detail + +//-----------+ +// Interface | +//-----------+ + +// durations + +using days = std::chrono::duration + , std::chrono::hours::period>>; + +using weeks = std::chrono::duration + , days::period>>; + +using years = std::chrono::duration + , days::period>>; + +using months = std::chrono::duration + >>; + +// time_point + +template + using sys_time = std::chrono::time_point; + +using sys_days = sys_time; +using sys_seconds = sys_time; + +struct local_t {}; + +template + using local_time = std::chrono::time_point; + +using local_seconds = local_time; +using local_days = local_time; + +// types + +struct last_spec +{ + explicit last_spec() = default; +}; + +class day; +class month; +class year; + +class weekday; +class weekday_indexed; +class weekday_last; + +class month_day; +class month_day_last; +class month_weekday; +class month_weekday_last; + +class year_month; + +class year_month_day; +class year_month_day_last; +class year_month_weekday; +class year_month_weekday_last; + +// date composition operators + +CONSTCD11 year_month operator/(const year& y, const month& m) NOEXCEPT; +CONSTCD11 year_month operator/(const year& y, int m) NOEXCEPT; + +CONSTCD11 month_day operator/(const day& d, const month& m) NOEXCEPT; +CONSTCD11 month_day operator/(const day& d, int m) NOEXCEPT; +CONSTCD11 month_day operator/(const month& m, const day& d) NOEXCEPT; +CONSTCD11 month_day operator/(const month& m, int d) NOEXCEPT; +CONSTCD11 month_day operator/(int m, const day& d) NOEXCEPT; + +CONSTCD11 month_day_last operator/(const month& m, last_spec) NOEXCEPT; +CONSTCD11 month_day_last operator/(int m, last_spec) NOEXCEPT; +CONSTCD11 month_day_last operator/(last_spec, const month& m) NOEXCEPT; +CONSTCD11 month_day_last operator/(last_spec, int m) NOEXCEPT; + +CONSTCD11 month_weekday operator/(const month& m, const weekday_indexed& wdi) NOEXCEPT; +CONSTCD11 month_weekday operator/(int m, const weekday_indexed& wdi) NOEXCEPT; +CONSTCD11 month_weekday operator/(const weekday_indexed& wdi, const month& m) NOEXCEPT; +CONSTCD11 month_weekday operator/(const weekday_indexed& wdi, int m) NOEXCEPT; + +CONSTCD11 month_weekday_last operator/(const month& m, const weekday_last& wdl) NOEXCEPT; +CONSTCD11 month_weekday_last operator/(int m, const weekday_last& wdl) NOEXCEPT; +CONSTCD11 month_weekday_last operator/(const weekday_last& wdl, const month& m) NOEXCEPT; +CONSTCD11 month_weekday_last operator/(const weekday_last& wdl, int m) NOEXCEPT; + +CONSTCD11 year_month_day operator/(const year_month& ym, const day& d) NOEXCEPT; +CONSTCD11 year_month_day operator/(const year_month& ym, int d) NOEXCEPT; +CONSTCD11 year_month_day operator/(const year& y, const month_day& md) NOEXCEPT; +CONSTCD11 year_month_day operator/(int y, const month_day& md) NOEXCEPT; +CONSTCD11 year_month_day operator/(const month_day& md, const year& y) NOEXCEPT; +CONSTCD11 year_month_day operator/(const month_day& md, int y) NOEXCEPT; + +CONSTCD11 + year_month_day_last operator/(const year_month& ym, last_spec) NOEXCEPT; +CONSTCD11 + year_month_day_last operator/(const year& y, const month_day_last& mdl) NOEXCEPT; +CONSTCD11 + year_month_day_last operator/(int y, const month_day_last& mdl) NOEXCEPT; +CONSTCD11 + year_month_day_last operator/(const month_day_last& mdl, const year& y) NOEXCEPT; +CONSTCD11 + year_month_day_last operator/(const month_day_last& mdl, int y) NOEXCEPT; + +CONSTCD11 +year_month_weekday +operator/(const year_month& ym, const weekday_indexed& wdi) NOEXCEPT; + +CONSTCD11 +year_month_weekday +operator/(const year& y, const month_weekday& mwd) NOEXCEPT; + +CONSTCD11 +year_month_weekday +operator/(int y, const month_weekday& mwd) NOEXCEPT; + +CONSTCD11 +year_month_weekday +operator/(const month_weekday& mwd, const year& y) NOEXCEPT; + +CONSTCD11 +year_month_weekday +operator/(const month_weekday& mwd, int y) NOEXCEPT; + +CONSTCD11 +year_month_weekday_last +operator/(const year_month& ym, const weekday_last& wdl) NOEXCEPT; + +CONSTCD11 +year_month_weekday_last +operator/(const year& y, const month_weekday_last& mwdl) NOEXCEPT; + +CONSTCD11 +year_month_weekday_last +operator/(int y, const month_weekday_last& mwdl) NOEXCEPT; + +CONSTCD11 +year_month_weekday_last +operator/(const month_weekday_last& mwdl, const year& y) NOEXCEPT; + +CONSTCD11 +year_month_weekday_last +operator/(const month_weekday_last& mwdl, int y) NOEXCEPT; + +// Detailed interface + +// day + +class day +{ + unsigned char d_; + +public: + day() = default; + explicit CONSTCD11 day(unsigned d) NOEXCEPT; + + CONSTCD14 day& operator++() NOEXCEPT; + CONSTCD14 day operator++(int) NOEXCEPT; + CONSTCD14 day& operator--() NOEXCEPT; + CONSTCD14 day operator--(int) NOEXCEPT; + + CONSTCD14 day& operator+=(const days& d) NOEXCEPT; + CONSTCD14 day& operator-=(const days& d) NOEXCEPT; + + CONSTCD11 explicit operator unsigned() const NOEXCEPT; + CONSTCD11 bool ok() const NOEXCEPT; +}; + +CONSTCD11 bool operator==(const day& x, const day& y) NOEXCEPT; +CONSTCD11 bool operator!=(const day& x, const day& y) NOEXCEPT; +CONSTCD11 bool operator< (const day& x, const day& y) NOEXCEPT; +CONSTCD11 bool operator> (const day& x, const day& y) NOEXCEPT; +CONSTCD11 bool operator<=(const day& x, const day& y) NOEXCEPT; +CONSTCD11 bool operator>=(const day& x, const day& y) NOEXCEPT; + +CONSTCD11 day operator+(const day& x, const days& y) NOEXCEPT; +CONSTCD11 day operator+(const days& x, const day& y) NOEXCEPT; +CONSTCD11 day operator-(const day& x, const days& y) NOEXCEPT; +CONSTCD11 days operator-(const day& x, const day& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const day& d); + +// month + +class month +{ + unsigned char m_; + +public: + month() = default; + explicit CONSTCD11 month(unsigned m) NOEXCEPT; + + CONSTCD14 month& operator++() NOEXCEPT; + CONSTCD14 month operator++(int) NOEXCEPT; + CONSTCD14 month& operator--() NOEXCEPT; + CONSTCD14 month operator--(int) NOEXCEPT; + + CONSTCD14 month& operator+=(const months& m) NOEXCEPT; + CONSTCD14 month& operator-=(const months& m) NOEXCEPT; + + CONSTCD11 explicit operator unsigned() const NOEXCEPT; + CONSTCD11 bool ok() const NOEXCEPT; +}; + +CONSTCD11 bool operator==(const month& x, const month& y) NOEXCEPT; +CONSTCD11 bool operator!=(const month& x, const month& y) NOEXCEPT; +CONSTCD11 bool operator< (const month& x, const month& y) NOEXCEPT; +CONSTCD11 bool operator> (const month& x, const month& y) NOEXCEPT; +CONSTCD11 bool operator<=(const month& x, const month& y) NOEXCEPT; +CONSTCD11 bool operator>=(const month& x, const month& y) NOEXCEPT; + +CONSTCD14 month operator+(const month& x, const months& y) NOEXCEPT; +CONSTCD14 month operator+(const months& x, const month& y) NOEXCEPT; +CONSTCD14 month operator-(const month& x, const months& y) NOEXCEPT; +CONSTCD14 months operator-(const month& x, const month& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const month& m); + +// year + +class year +{ + short y_; + +public: + year() = default; + explicit CONSTCD11 year(int y) NOEXCEPT; + + CONSTCD14 year& operator++() NOEXCEPT; + CONSTCD14 year operator++(int) NOEXCEPT; + CONSTCD14 year& operator--() NOEXCEPT; + CONSTCD14 year operator--(int) NOEXCEPT; + + CONSTCD14 year& operator+=(const years& y) NOEXCEPT; + CONSTCD14 year& operator-=(const years& y) NOEXCEPT; + + CONSTCD11 year operator-() const NOEXCEPT; + CONSTCD11 year operator+() const NOEXCEPT; + + CONSTCD11 bool is_leap() const NOEXCEPT; + + CONSTCD11 explicit operator int() const NOEXCEPT; + CONSTCD11 bool ok() const NOEXCEPT; + + static CONSTCD11 year min() NOEXCEPT { return year{-32767}; } + static CONSTCD11 year max() NOEXCEPT { return year{32767}; } +}; + +CONSTCD11 bool operator==(const year& x, const year& y) NOEXCEPT; +CONSTCD11 bool operator!=(const year& x, const year& y) NOEXCEPT; +CONSTCD11 bool operator< (const year& x, const year& y) NOEXCEPT; +CONSTCD11 bool operator> (const year& x, const year& y) NOEXCEPT; +CONSTCD11 bool operator<=(const year& x, const year& y) NOEXCEPT; +CONSTCD11 bool operator>=(const year& x, const year& y) NOEXCEPT; + +CONSTCD11 year operator+(const year& x, const years& y) NOEXCEPT; +CONSTCD11 year operator+(const years& x, const year& y) NOEXCEPT; +CONSTCD11 year operator-(const year& x, const years& y) NOEXCEPT; +CONSTCD11 years operator-(const year& x, const year& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const year& y); + +// weekday + +class weekday +{ + unsigned char wd_; +public: + weekday() = default; + explicit CONSTCD11 weekday(unsigned wd) NOEXCEPT; + CONSTCD14 weekday(const sys_days& dp) NOEXCEPT; + CONSTCD14 explicit weekday(const local_days& dp) NOEXCEPT; + + CONSTCD14 weekday& operator++() NOEXCEPT; + CONSTCD14 weekday operator++(int) NOEXCEPT; + CONSTCD14 weekday& operator--() NOEXCEPT; + CONSTCD14 weekday operator--(int) NOEXCEPT; + + CONSTCD14 weekday& operator+=(const days& d) NOEXCEPT; + CONSTCD14 weekday& operator-=(const days& d) NOEXCEPT; + + CONSTCD11 bool ok() const NOEXCEPT; + + CONSTCD11 unsigned c_encoding() const NOEXCEPT; + CONSTCD11 unsigned iso_encoding() const NOEXCEPT; + + CONSTCD11 weekday_indexed operator[](unsigned index) const NOEXCEPT; + CONSTCD11 weekday_last operator[](last_spec) const NOEXCEPT; + +private: + static CONSTCD14 unsigned char weekday_from_days(int z) NOEXCEPT; + + friend CONSTCD11 bool operator==(const weekday& x, const weekday& y) NOEXCEPT; + friend CONSTCD14 days operator-(const weekday& x, const weekday& y) NOEXCEPT; + friend CONSTCD14 weekday operator+(const weekday& x, const days& y) NOEXCEPT; + template + friend std::basic_ostream& + operator<<(std::basic_ostream& os, const weekday& wd); + friend class weekday_indexed; +}; + +CONSTCD11 bool operator==(const weekday& x, const weekday& y) NOEXCEPT; +CONSTCD11 bool operator!=(const weekday& x, const weekday& y) NOEXCEPT; + +CONSTCD14 weekday operator+(const weekday& x, const days& y) NOEXCEPT; +CONSTCD14 weekday operator+(const days& x, const weekday& y) NOEXCEPT; +CONSTCD14 weekday operator-(const weekday& x, const days& y) NOEXCEPT; +CONSTCD14 days operator-(const weekday& x, const weekday& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const weekday& wd); + +// weekday_indexed + +class weekday_indexed +{ + unsigned char wd_ : 4; + unsigned char index_ : 4; + +public: + weekday_indexed() = default; + CONSTCD11 weekday_indexed(const date::weekday& wd, unsigned index) NOEXCEPT; + + CONSTCD11 date::weekday weekday() const NOEXCEPT; + CONSTCD11 unsigned index() const NOEXCEPT; + CONSTCD11 bool ok() const NOEXCEPT; +}; + +CONSTCD11 bool operator==(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT; +CONSTCD11 bool operator!=(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const weekday_indexed& wdi); + +// weekday_last + +class weekday_last +{ + date::weekday wd_; + +public: + explicit CONSTCD11 weekday_last(const date::weekday& wd) NOEXCEPT; + + CONSTCD11 date::weekday weekday() const NOEXCEPT; + CONSTCD11 bool ok() const NOEXCEPT; +}; + +CONSTCD11 bool operator==(const weekday_last& x, const weekday_last& y) NOEXCEPT; +CONSTCD11 bool operator!=(const weekday_last& x, const weekday_last& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const weekday_last& wdl); + +namespace detail +{ + +struct unspecified_month_disambiguator {}; + +} // namespace detail + +// year_month + +class year_month +{ + date::year y_; + date::month m_; + +public: + year_month() = default; + CONSTCD11 year_month(const date::year& y, const date::month& m) NOEXCEPT; + + CONSTCD11 date::year year() const NOEXCEPT; + CONSTCD11 date::month month() const NOEXCEPT; + + template + CONSTCD14 year_month& operator+=(const months& dm) NOEXCEPT; + template + CONSTCD14 year_month& operator-=(const months& dm) NOEXCEPT; + CONSTCD14 year_month& operator+=(const years& dy) NOEXCEPT; + CONSTCD14 year_month& operator-=(const years& dy) NOEXCEPT; + + CONSTCD11 bool ok() const NOEXCEPT; +}; + +CONSTCD11 bool operator==(const year_month& x, const year_month& y) NOEXCEPT; +CONSTCD11 bool operator!=(const year_month& x, const year_month& y) NOEXCEPT; +CONSTCD11 bool operator< (const year_month& x, const year_month& y) NOEXCEPT; +CONSTCD11 bool operator> (const year_month& x, const year_month& y) NOEXCEPT; +CONSTCD11 bool operator<=(const year_month& x, const year_month& y) NOEXCEPT; +CONSTCD11 bool operator>=(const year_month& x, const year_month& y) NOEXCEPT; + +template +CONSTCD14 year_month operator+(const year_month& ym, const months& dm) NOEXCEPT; +template +CONSTCD14 year_month operator+(const months& dm, const year_month& ym) NOEXCEPT; +template +CONSTCD14 year_month operator-(const year_month& ym, const months& dm) NOEXCEPT; + +CONSTCD11 months operator-(const year_month& x, const year_month& y) NOEXCEPT; +CONSTCD11 year_month operator+(const year_month& ym, const years& dy) NOEXCEPT; +CONSTCD11 year_month operator+(const years& dy, const year_month& ym) NOEXCEPT; +CONSTCD11 year_month operator-(const year_month& ym, const years& dy) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month& ym); + +// month_day + +class month_day +{ + date::month m_; + date::day d_; + +public: + month_day() = default; + CONSTCD11 month_day(const date::month& m, const date::day& d) NOEXCEPT; + + CONSTCD11 date::month month() const NOEXCEPT; + CONSTCD11 date::day day() const NOEXCEPT; + + CONSTCD14 bool ok() const NOEXCEPT; +}; + +CONSTCD11 bool operator==(const month_day& x, const month_day& y) NOEXCEPT; +CONSTCD11 bool operator!=(const month_day& x, const month_day& y) NOEXCEPT; +CONSTCD11 bool operator< (const month_day& x, const month_day& y) NOEXCEPT; +CONSTCD11 bool operator> (const month_day& x, const month_day& y) NOEXCEPT; +CONSTCD11 bool operator<=(const month_day& x, const month_day& y) NOEXCEPT; +CONSTCD11 bool operator>=(const month_day& x, const month_day& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const month_day& md); + +// month_day_last + +class month_day_last +{ + date::month m_; + +public: + CONSTCD11 explicit month_day_last(const date::month& m) NOEXCEPT; + + CONSTCD11 date::month month() const NOEXCEPT; + CONSTCD11 bool ok() const NOEXCEPT; +}; + +CONSTCD11 bool operator==(const month_day_last& x, const month_day_last& y) NOEXCEPT; +CONSTCD11 bool operator!=(const month_day_last& x, const month_day_last& y) NOEXCEPT; +CONSTCD11 bool operator< (const month_day_last& x, const month_day_last& y) NOEXCEPT; +CONSTCD11 bool operator> (const month_day_last& x, const month_day_last& y) NOEXCEPT; +CONSTCD11 bool operator<=(const month_day_last& x, const month_day_last& y) NOEXCEPT; +CONSTCD11 bool operator>=(const month_day_last& x, const month_day_last& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const month_day_last& mdl); + +// month_weekday + +class month_weekday +{ + date::month m_; + date::weekday_indexed wdi_; +public: + CONSTCD11 month_weekday(const date::month& m, + const date::weekday_indexed& wdi) NOEXCEPT; + + CONSTCD11 date::month month() const NOEXCEPT; + CONSTCD11 date::weekday_indexed weekday_indexed() const NOEXCEPT; + + CONSTCD11 bool ok() const NOEXCEPT; +}; + +CONSTCD11 bool operator==(const month_weekday& x, const month_weekday& y) NOEXCEPT; +CONSTCD11 bool operator!=(const month_weekday& x, const month_weekday& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const month_weekday& mwd); + +// month_weekday_last + +class month_weekday_last +{ + date::month m_; + date::weekday_last wdl_; + +public: + CONSTCD11 month_weekday_last(const date::month& m, + const date::weekday_last& wd) NOEXCEPT; + + CONSTCD11 date::month month() const NOEXCEPT; + CONSTCD11 date::weekday_last weekday_last() const NOEXCEPT; + + CONSTCD11 bool ok() const NOEXCEPT; +}; + +CONSTCD11 + bool operator==(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT; +CONSTCD11 + bool operator!=(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const month_weekday_last& mwdl); + +// class year_month_day + +class year_month_day +{ + date::year y_; + date::month m_; + date::day d_; + +public: + year_month_day() = default; + CONSTCD11 year_month_day(const date::year& y, const date::month& m, + const date::day& d) NOEXCEPT; + CONSTCD14 year_month_day(const year_month_day_last& ymdl) NOEXCEPT; + + CONSTCD14 year_month_day(sys_days dp) NOEXCEPT; + CONSTCD14 explicit year_month_day(local_days dp) NOEXCEPT; + + template + CONSTCD14 year_month_day& operator+=(const months& m) NOEXCEPT; + template + CONSTCD14 year_month_day& operator-=(const months& m) NOEXCEPT; + CONSTCD14 year_month_day& operator+=(const years& y) NOEXCEPT; + CONSTCD14 year_month_day& operator-=(const years& y) NOEXCEPT; + + CONSTCD11 date::year year() const NOEXCEPT; + CONSTCD11 date::month month() const NOEXCEPT; + CONSTCD11 date::day day() const NOEXCEPT; + + CONSTCD14 operator sys_days() const NOEXCEPT; + CONSTCD14 explicit operator local_days() const NOEXCEPT; + CONSTCD14 bool ok() const NOEXCEPT; + +private: + static CONSTCD14 year_month_day from_days(days dp) NOEXCEPT; + CONSTCD14 days to_days() const NOEXCEPT; +}; + +CONSTCD11 bool operator==(const year_month_day& x, const year_month_day& y) NOEXCEPT; +CONSTCD11 bool operator!=(const year_month_day& x, const year_month_day& y) NOEXCEPT; +CONSTCD11 bool operator< (const year_month_day& x, const year_month_day& y) NOEXCEPT; +CONSTCD11 bool operator> (const year_month_day& x, const year_month_day& y) NOEXCEPT; +CONSTCD11 bool operator<=(const year_month_day& x, const year_month_day& y) NOEXCEPT; +CONSTCD11 bool operator>=(const year_month_day& x, const year_month_day& y) NOEXCEPT; + +template +CONSTCD14 year_month_day operator+(const year_month_day& ymd, const months& dm) NOEXCEPT; +template +CONSTCD14 year_month_day operator+(const months& dm, const year_month_day& ymd) NOEXCEPT; +template +CONSTCD14 year_month_day operator-(const year_month_day& ymd, const months& dm) NOEXCEPT; +CONSTCD11 year_month_day operator+(const year_month_day& ymd, const years& dy) NOEXCEPT; +CONSTCD11 year_month_day operator+(const years& dy, const year_month_day& ymd) NOEXCEPT; +CONSTCD11 year_month_day operator-(const year_month_day& ymd, const years& dy) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month_day& ymd); + +// year_month_day_last + +class year_month_day_last +{ + date::year y_; + date::month_day_last mdl_; + +public: + CONSTCD11 year_month_day_last(const date::year& y, + const date::month_day_last& mdl) NOEXCEPT; + + template + CONSTCD14 year_month_day_last& operator+=(const months& m) NOEXCEPT; + template + CONSTCD14 year_month_day_last& operator-=(const months& m) NOEXCEPT; + CONSTCD14 year_month_day_last& operator+=(const years& y) NOEXCEPT; + CONSTCD14 year_month_day_last& operator-=(const years& y) NOEXCEPT; + + CONSTCD11 date::year year() const NOEXCEPT; + CONSTCD11 date::month month() const NOEXCEPT; + CONSTCD11 date::month_day_last month_day_last() const NOEXCEPT; + CONSTCD14 date::day day() const NOEXCEPT; + + CONSTCD14 operator sys_days() const NOEXCEPT; + CONSTCD14 explicit operator local_days() const NOEXCEPT; + CONSTCD11 bool ok() const NOEXCEPT; +}; + +CONSTCD11 + bool operator==(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; +CONSTCD11 + bool operator!=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; +CONSTCD11 + bool operator< (const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; +CONSTCD11 + bool operator> (const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; +CONSTCD11 + bool operator<=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; +CONSTCD11 + bool operator>=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; + +template +CONSTCD14 +year_month_day_last +operator+(const year_month_day_last& ymdl, const months& dm) NOEXCEPT; + +template +CONSTCD14 +year_month_day_last +operator+(const months& dm, const year_month_day_last& ymdl) NOEXCEPT; + +CONSTCD11 +year_month_day_last +operator+(const year_month_day_last& ymdl, const years& dy) NOEXCEPT; + +CONSTCD11 +year_month_day_last +operator+(const years& dy, const year_month_day_last& ymdl) NOEXCEPT; + +template +CONSTCD14 +year_month_day_last +operator-(const year_month_day_last& ymdl, const months& dm) NOEXCEPT; + +CONSTCD11 +year_month_day_last +operator-(const year_month_day_last& ymdl, const years& dy) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month_day_last& ymdl); + +// year_month_weekday + +class year_month_weekday +{ + date::year y_; + date::month m_; + date::weekday_indexed wdi_; + +public: + year_month_weekday() = default; + CONSTCD11 year_month_weekday(const date::year& y, const date::month& m, + const date::weekday_indexed& wdi) NOEXCEPT; + CONSTCD14 year_month_weekday(const sys_days& dp) NOEXCEPT; + CONSTCD14 explicit year_month_weekday(const local_days& dp) NOEXCEPT; + + template + CONSTCD14 year_month_weekday& operator+=(const months& m) NOEXCEPT; + template + CONSTCD14 year_month_weekday& operator-=(const months& m) NOEXCEPT; + CONSTCD14 year_month_weekday& operator+=(const years& y) NOEXCEPT; + CONSTCD14 year_month_weekday& operator-=(const years& y) NOEXCEPT; + + CONSTCD11 date::year year() const NOEXCEPT; + CONSTCD11 date::month month() const NOEXCEPT; + CONSTCD11 date::weekday weekday() const NOEXCEPT; + CONSTCD11 unsigned index() const NOEXCEPT; + CONSTCD11 date::weekday_indexed weekday_indexed() const NOEXCEPT; + + CONSTCD14 operator sys_days() const NOEXCEPT; + CONSTCD14 explicit operator local_days() const NOEXCEPT; + CONSTCD14 bool ok() const NOEXCEPT; + +private: + static CONSTCD14 year_month_weekday from_days(days dp) NOEXCEPT; + CONSTCD14 days to_days() const NOEXCEPT; +}; + +CONSTCD11 + bool operator==(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT; +CONSTCD11 + bool operator!=(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT; + +template +CONSTCD14 +year_month_weekday +operator+(const year_month_weekday& ymwd, const months& dm) NOEXCEPT; + +template +CONSTCD14 +year_month_weekday +operator+(const months& dm, const year_month_weekday& ymwd) NOEXCEPT; + +CONSTCD11 +year_month_weekday +operator+(const year_month_weekday& ymwd, const years& dy) NOEXCEPT; + +CONSTCD11 +year_month_weekday +operator+(const years& dy, const year_month_weekday& ymwd) NOEXCEPT; + +template +CONSTCD14 +year_month_weekday +operator-(const year_month_weekday& ymwd, const months& dm) NOEXCEPT; + +CONSTCD11 +year_month_weekday +operator-(const year_month_weekday& ymwd, const years& dy) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month_weekday& ymwdi); + +// year_month_weekday_last + +class year_month_weekday_last +{ + date::year y_; + date::month m_; + date::weekday_last wdl_; + +public: + CONSTCD11 year_month_weekday_last(const date::year& y, const date::month& m, + const date::weekday_last& wdl) NOEXCEPT; + + template + CONSTCD14 year_month_weekday_last& operator+=(const months& m) NOEXCEPT; + template + CONSTCD14 year_month_weekday_last& operator-=(const months& m) NOEXCEPT; + CONSTCD14 year_month_weekday_last& operator+=(const years& y) NOEXCEPT; + CONSTCD14 year_month_weekday_last& operator-=(const years& y) NOEXCEPT; + + CONSTCD11 date::year year() const NOEXCEPT; + CONSTCD11 date::month month() const NOEXCEPT; + CONSTCD11 date::weekday weekday() const NOEXCEPT; + CONSTCD11 date::weekday_last weekday_last() const NOEXCEPT; + + CONSTCD14 operator sys_days() const NOEXCEPT; + CONSTCD14 explicit operator local_days() const NOEXCEPT; + CONSTCD11 bool ok() const NOEXCEPT; + +private: + CONSTCD14 days to_days() const NOEXCEPT; +}; + +CONSTCD11 +bool +operator==(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT; + +CONSTCD11 +bool +operator!=(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT; + +template +CONSTCD14 +year_month_weekday_last +operator+(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT; + +template +CONSTCD14 +year_month_weekday_last +operator+(const months& dm, const year_month_weekday_last& ymwdl) NOEXCEPT; + +CONSTCD11 +year_month_weekday_last +operator+(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT; + +CONSTCD11 +year_month_weekday_last +operator+(const years& dy, const year_month_weekday_last& ymwdl) NOEXCEPT; + +template +CONSTCD14 +year_month_weekday_last +operator-(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT; + +CONSTCD11 +year_month_weekday_last +operator-(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month_weekday_last& ymwdl); + +#if !defined(_MSC_VER) || (_MSC_VER >= 1900) +inline namespace literals +{ + +CONSTCD11 date::day operator "" _d(unsigned long long d) NOEXCEPT; +CONSTCD11 date::year operator "" _y(unsigned long long y) NOEXCEPT; + +} // inline namespace literals +#endif // !defined(_MSC_VER) || (_MSC_VER >= 1900) + +// CONSTDATA date::month January{1}; +// CONSTDATA date::month February{2}; +// CONSTDATA date::month March{3}; +// CONSTDATA date::month April{4}; +// CONSTDATA date::month May{5}; +// CONSTDATA date::month June{6}; +// CONSTDATA date::month July{7}; +// CONSTDATA date::month August{8}; +// CONSTDATA date::month September{9}; +// CONSTDATA date::month October{10}; +// CONSTDATA date::month November{11}; +// CONSTDATA date::month December{12}; +// +// CONSTDATA date::weekday Sunday{0u}; +// CONSTDATA date::weekday Monday{1u}; +// CONSTDATA date::weekday Tuesday{2u}; +// CONSTDATA date::weekday Wednesday{3u}; +// CONSTDATA date::weekday Thursday{4u}; +// CONSTDATA date::weekday Friday{5u}; +// CONSTDATA date::weekday Saturday{6u}; + +#if HAS_VOID_T + +template > +struct is_clock + : std::false_type +{}; + +template +struct is_clock> + : std::true_type +{}; + +template inline constexpr bool is_clock_v = is_clock::value; + +#endif // HAS_VOID_T + +//----------------+ +// Implementation | +//----------------+ + +// utilities +namespace detail { + +template> +class save_istream +{ +protected: + std::basic_ios& is_; + CharT fill_; + std::ios::fmtflags flags_; + std::streamsize precision_; + std::streamsize width_; + std::basic_ostream* tie_; + std::locale loc_; + +public: + ~save_istream() + { + is_.fill(fill_); + is_.flags(flags_); + is_.precision(precision_); + is_.width(width_); + is_.imbue(loc_); + is_.tie(tie_); + } + + save_istream(const save_istream&) = delete; + save_istream& operator=(const save_istream&) = delete; + + explicit save_istream(std::basic_ios& is) + : is_(is) + , fill_(is.fill()) + , flags_(is.flags()) + , precision_(is.precision()) + , width_(is.width(0)) + , tie_(is.tie(nullptr)) + , loc_(is.getloc()) + { + if (tie_ != nullptr) + tie_->flush(); + } +}; + +template> +class save_ostream + : private save_istream +{ +public: + ~save_ostream() + { + if ((this->flags_ & std::ios::unitbuf) && +#if HAS_UNCAUGHT_EXCEPTIONS + std::uncaught_exceptions() == 0 && +#else + !std::uncaught_exception() && +#endif + this->is_.good()) + this->is_.rdbuf()->pubsync(); + } + + save_ostream(const save_ostream&) = delete; + save_ostream& operator=(const save_ostream&) = delete; + + explicit save_ostream(std::basic_ios& os) + : save_istream(os) + { + } +}; + +template +struct choose_trunc_type +{ + static const int digits = std::numeric_limits::digits; + using type = typename std::conditional + < + digits < 32, + std::int32_t, + typename std::conditional + < + digits < 64, + std::int64_t, +#ifdef __SIZEOF_INT128__ + __int128 +#else + std::int64_t +#endif + >::type + >::type; +}; + +template +CONSTCD11 +inline +typename std::enable_if +< + !std::chrono::treat_as_floating_point::value, + T +>::type +trunc(T t) NOEXCEPT +{ + return t; +} + +template +CONSTCD14 +inline +typename std::enable_if +< + std::chrono::treat_as_floating_point::value, + T +>::type +trunc(T t) NOEXCEPT +{ + using std::numeric_limits; + using I = typename choose_trunc_type::type; + CONSTDATA auto digits = numeric_limits::digits; + static_assert(digits < numeric_limits::digits, ""); + CONSTDATA auto max = I{1} << (digits-1); + CONSTDATA auto min = -max; + const auto negative = t < T{0}; + if (min <= t && t <= max && t != 0 && t == t) + { + t = static_cast(static_cast(t)); + if (t == 0 && negative) + t = -t; + } + return t; +} + +template +struct static_gcd +{ + static const std::intmax_t value = static_gcd::value; +}; + +template +struct static_gcd +{ + static const std::intmax_t value = Xp; +}; + +template <> +struct static_gcd<0, 0> +{ + static const std::intmax_t value = 1; +}; + +template +struct no_overflow +{ +private: + static const std::intmax_t gcd_n1_n2 = static_gcd::value; + static const std::intmax_t gcd_d1_d2 = static_gcd::value; + static const std::intmax_t n1 = R1::num / gcd_n1_n2; + static const std::intmax_t d1 = R1::den / gcd_d1_d2; + static const std::intmax_t n2 = R2::num / gcd_n1_n2; + static const std::intmax_t d2 = R2::den / gcd_d1_d2; +#ifdef __cpp_constexpr + static const std::intmax_t max = std::numeric_limits::max(); +#else + static const std::intmax_t max = LLONG_MAX; +#endif + + template + struct mul // overflow == false + { + static const std::intmax_t value = Xp * Yp; + }; + + template + struct mul + { + static const std::intmax_t value = 1; + }; + +public: + static const bool value = (n1 <= max / d2) && (n2 <= max / d1); + typedef std::ratio::value, + mul::value> type; +}; + +} // detail + +// trunc towards zero +template +CONSTCD11 +inline +typename std::enable_if +< + detail::no_overflow::value, + To +>::type +trunc(const std::chrono::duration& d) +{ + return To{detail::trunc(std::chrono::duration_cast(d).count())}; +} + +template +CONSTCD11 +inline +typename std::enable_if +< + !detail::no_overflow::value, + To +>::type +trunc(const std::chrono::duration& d) +{ + using std::chrono::duration_cast; + using std::chrono::duration; + using rep = typename std::common_type::type; + return To{detail::trunc(duration_cast(duration_cast>(d)).count())}; +} + +#ifndef HAS_CHRONO_ROUNDING +# if defined(_MSC_FULL_VER) && (_MSC_FULL_VER >= 190023918 || (_MSC_FULL_VER >= 190000000 && defined (__clang__))) +# define HAS_CHRONO_ROUNDING 1 +# elif defined(__cpp_lib_chrono) && __cplusplus > 201402 && __cpp_lib_chrono >= 201510 +# define HAS_CHRONO_ROUNDING 1 +# elif defined(_LIBCPP_VERSION) && __cplusplus > 201402 && _LIBCPP_VERSION >= 3800 +# define HAS_CHRONO_ROUNDING 1 +# else +# define HAS_CHRONO_ROUNDING 0 +# endif +#endif // HAS_CHRONO_ROUNDING + +#if HAS_CHRONO_ROUNDING == 0 + +// round down +template +CONSTCD14 +inline +typename std::enable_if +< + detail::no_overflow::value, + To +>::type +floor(const std::chrono::duration& d) +{ + auto t = trunc(d); + if (t > d) + return t - To{1}; + return t; +} + +template +CONSTCD14 +inline +typename std::enable_if +< + !detail::no_overflow::value, + To +>::type +floor(const std::chrono::duration& d) +{ + using rep = typename std::common_type::type; + return floor(floor>(d)); +} + +// round to nearest, to even on tie +template +CONSTCD14 +inline +To +round(const std::chrono::duration& d) +{ + auto t0 = floor(d); + auto t1 = t0 + To{1}; + if (t1 == To{0} && t0 < To{0}) + t1 = -t1; + auto diff0 = d - t0; + auto diff1 = t1 - d; + if (diff0 == diff1) + { + if (t0 - trunc(t0/2)*2 == To{0}) + return t0; + return t1; + } + if (diff0 < diff1) + return t0; + return t1; +} + +// round up +template +CONSTCD14 +inline +To +ceil(const std::chrono::duration& d) +{ + auto t = trunc(d); + if (t < d) + return t + To{1}; + return t; +} + +template ::is_signed + >::type> +CONSTCD11 +std::chrono::duration +abs(std::chrono::duration d) +{ + return d >= d.zero() ? d : -d; +} + +// round down +template +CONSTCD11 +inline +std::chrono::time_point +floor(const std::chrono::time_point& tp) +{ + using std::chrono::time_point; + return time_point{date::floor(tp.time_since_epoch())}; +} + +// round to nearest, to even on tie +template +CONSTCD11 +inline +std::chrono::time_point +round(const std::chrono::time_point& tp) +{ + using std::chrono::time_point; + return time_point{round(tp.time_since_epoch())}; +} + +// round up +template +CONSTCD11 +inline +std::chrono::time_point +ceil(const std::chrono::time_point& tp) +{ + using std::chrono::time_point; + return time_point{ceil(tp.time_since_epoch())}; +} + +#else // HAS_CHRONO_ROUNDING == 1 + +using std::chrono::floor; +using std::chrono::ceil; +using std::chrono::round; +using std::chrono::abs; + +#endif // HAS_CHRONO_ROUNDING + +namespace detail +{ + +template +CONSTCD14 +inline +typename std::enable_if +< + !std::chrono::treat_as_floating_point::value, + To +>::type +round_i(const std::chrono::duration& d) +{ + return round(d); +} + +template +CONSTCD14 +inline +typename std::enable_if +< + std::chrono::treat_as_floating_point::value, + To +>::type +round_i(const std::chrono::duration& d) +{ + return d; +} + +template +CONSTCD11 +inline +std::chrono::time_point +round_i(const std::chrono::time_point& tp) +{ + using std::chrono::time_point; + return time_point{round_i(tp.time_since_epoch())}; +} + +} // detail + +// trunc towards zero +template +CONSTCD11 +inline +std::chrono::time_point +trunc(const std::chrono::time_point& tp) +{ + using std::chrono::time_point; + return time_point{trunc(tp.time_since_epoch())}; +} + +// day + +CONSTCD11 inline day::day(unsigned d) NOEXCEPT : d_(static_cast(d)) {} +CONSTCD14 inline day& day::operator++() NOEXCEPT {++d_; return *this;} +CONSTCD14 inline day day::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} +CONSTCD14 inline day& day::operator--() NOEXCEPT {--d_; return *this;} +CONSTCD14 inline day day::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} +CONSTCD14 inline day& day::operator+=(const days& d) NOEXCEPT {*this = *this + d; return *this;} +CONSTCD14 inline day& day::operator-=(const days& d) NOEXCEPT {*this = *this - d; return *this;} +CONSTCD11 inline day::operator unsigned() const NOEXCEPT {return d_;} +CONSTCD11 inline bool day::ok() const NOEXCEPT {return 1 <= d_ && d_ <= 31;} + +CONSTCD11 +inline +bool +operator==(const day& x, const day& y) NOEXCEPT +{ + return static_cast(x) == static_cast(y); +} + +CONSTCD11 +inline +bool +operator!=(const day& x, const day& y) NOEXCEPT +{ + return !(x == y); +} + +CONSTCD11 +inline +bool +operator<(const day& x, const day& y) NOEXCEPT +{ + return static_cast(x) < static_cast(y); +} + +CONSTCD11 +inline +bool +operator>(const day& x, const day& y) NOEXCEPT +{ + return y < x; +} + +CONSTCD11 +inline +bool +operator<=(const day& x, const day& y) NOEXCEPT +{ + return !(y < x); +} + +CONSTCD11 +inline +bool +operator>=(const day& x, const day& y) NOEXCEPT +{ + return !(x < y); +} + +CONSTCD11 +inline +days +operator-(const day& x, const day& y) NOEXCEPT +{ + return days{static_cast(static_cast(x) + - static_cast(y))}; +} + +CONSTCD11 +inline +day +operator+(const day& x, const days& y) NOEXCEPT +{ + return day{static_cast(x) + static_cast(y.count())}; +} + +CONSTCD11 +inline +day +operator+(const days& x, const day& y) NOEXCEPT +{ + return y + x; +} + +CONSTCD11 +inline +day +operator-(const day& x, const days& y) NOEXCEPT +{ + return x + -y; +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const day& d) +{ + detail::save_ostream _(os); + os.fill('0'); + os.flags(std::ios::dec | std::ios::right); + os.width(2); + os << static_cast(d); + return os; +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const day& d) +{ + detail::low_level_fmt(os, d); + if (!d.ok()) + os << " is not a valid day"; + return os; +} + +// month + +CONSTCD11 inline month::month(unsigned m) NOEXCEPT : m_(static_cast(m)) {} +CONSTCD14 inline month& month::operator++() NOEXCEPT {*this += months{1}; return *this;} +CONSTCD14 inline month month::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} +CONSTCD14 inline month& month::operator--() NOEXCEPT {*this -= months{1}; return *this;} +CONSTCD14 inline month month::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} + +CONSTCD14 +inline +month& +month::operator+=(const months& m) NOEXCEPT +{ + *this = *this + m; + return *this; +} + +CONSTCD14 +inline +month& +month::operator-=(const months& m) NOEXCEPT +{ + *this = *this - m; + return *this; +} + +CONSTCD11 inline month::operator unsigned() const NOEXCEPT {return m_;} +CONSTCD11 inline bool month::ok() const NOEXCEPT {return 1 <= m_ && m_ <= 12;} + +CONSTCD11 +inline +bool +operator==(const month& x, const month& y) NOEXCEPT +{ + return static_cast(x) == static_cast(y); +} + +CONSTCD11 +inline +bool +operator!=(const month& x, const month& y) NOEXCEPT +{ + return !(x == y); +} + +CONSTCD11 +inline +bool +operator<(const month& x, const month& y) NOEXCEPT +{ + return static_cast(x) < static_cast(y); +} + +CONSTCD11 +inline +bool +operator>(const month& x, const month& y) NOEXCEPT +{ + return y < x; +} + +CONSTCD11 +inline +bool +operator<=(const month& x, const month& y) NOEXCEPT +{ + return !(y < x); +} + +CONSTCD11 +inline +bool +operator>=(const month& x, const month& y) NOEXCEPT +{ + return !(x < y); +} + +CONSTCD14 +inline +months +operator-(const month& x, const month& y) NOEXCEPT +{ + auto const d = static_cast(x) - static_cast(y); + return months(d <= 11 ? d : d + 12); +} + +CONSTCD14 +inline +month +operator+(const month& x, const months& y) NOEXCEPT +{ + auto const mu = static_cast(static_cast(x)) + y.count() - 1; + auto const yr = (mu >= 0 ? mu : mu-11) / 12; + return month{static_cast(mu - yr * 12 + 1)}; +} + +CONSTCD14 +inline +month +operator+(const months& x, const month& y) NOEXCEPT +{ + return y + x; +} + +CONSTCD14 +inline +month +operator-(const month& x, const months& y) NOEXCEPT +{ + return x + -y; +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const month& m) +{ + if (m.ok()) + { + CharT fmt[] = {'%', 'b', 0}; + os << format(os.getloc(), fmt, m); + } + else + os << static_cast(m); + return os; +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const month& m) +{ + detail::low_level_fmt(os, m); + if (!m.ok()) + os << " is not a valid month"; + return os; +} + +// year + +CONSTCD11 inline year::year(int y) NOEXCEPT : y_(static_cast(y)) {} +CONSTCD14 inline year& year::operator++() NOEXCEPT {++y_; return *this;} +CONSTCD14 inline year year::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} +CONSTCD14 inline year& year::operator--() NOEXCEPT {--y_; return *this;} +CONSTCD14 inline year year::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} +CONSTCD14 inline year& year::operator+=(const years& y) NOEXCEPT {*this = *this + y; return *this;} +CONSTCD14 inline year& year::operator-=(const years& y) NOEXCEPT {*this = *this - y; return *this;} +CONSTCD11 inline year year::operator-() const NOEXCEPT {return year{-y_};} +CONSTCD11 inline year year::operator+() const NOEXCEPT {return *this;} + +CONSTCD11 +inline +bool +year::is_leap() const NOEXCEPT +{ + return y_ % 4 == 0 && (y_ % 100 != 0 || y_ % 400 == 0); +} + +CONSTCD11 inline year::operator int() const NOEXCEPT {return y_;} + +CONSTCD11 +inline +bool +year::ok() const NOEXCEPT +{ + return y_ != std::numeric_limits::min(); +} + +CONSTCD11 +inline +bool +operator==(const year& x, const year& y) NOEXCEPT +{ + return static_cast(x) == static_cast(y); +} + +CONSTCD11 +inline +bool +operator!=(const year& x, const year& y) NOEXCEPT +{ + return !(x == y); +} + +CONSTCD11 +inline +bool +operator<(const year& x, const year& y) NOEXCEPT +{ + return static_cast(x) < static_cast(y); +} + +CONSTCD11 +inline +bool +operator>(const year& x, const year& y) NOEXCEPT +{ + return y < x; +} + +CONSTCD11 +inline +bool +operator<=(const year& x, const year& y) NOEXCEPT +{ + return !(y < x); +} + +CONSTCD11 +inline +bool +operator>=(const year& x, const year& y) NOEXCEPT +{ + return !(x < y); +} + +CONSTCD11 +inline +years +operator-(const year& x, const year& y) NOEXCEPT +{ + return years{static_cast(x) - static_cast(y)}; +} + +CONSTCD11 +inline +year +operator+(const year& x, const years& y) NOEXCEPT +{ + return year{static_cast(x) + y.count()}; +} + +CONSTCD11 +inline +year +operator+(const years& x, const year& y) NOEXCEPT +{ + return y + x; +} + +CONSTCD11 +inline +year +operator-(const year& x, const years& y) NOEXCEPT +{ + return year{static_cast(x) - y.count()}; +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const year& y) +{ + detail::save_ostream _(os); + os.fill('0'); + os.flags(std::ios::dec | std::ios::internal); + os.width(4 + (y < year{0})); + os.imbue(std::locale::classic()); + os << static_cast(y); + return os; +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const year& y) +{ + detail::low_level_fmt(os, y); + if (!y.ok()) + os << " is not a valid year"; + return os; +} + +// weekday + +CONSTCD14 +inline +unsigned char +weekday::weekday_from_days(int z) NOEXCEPT +{ + auto u = static_cast(z); + return static_cast(z >= -4 ? (u+4) % 7 : u % 7); +} + +CONSTCD11 +inline +weekday::weekday(unsigned wd) NOEXCEPT + : wd_(static_cast(wd != 7 ? wd : 0)) + {} + +CONSTCD14 +inline +weekday::weekday(const sys_days& dp) NOEXCEPT + : wd_(weekday_from_days(dp.time_since_epoch().count())) + {} + +CONSTCD14 +inline +weekday::weekday(const local_days& dp) NOEXCEPT + : wd_(weekday_from_days(dp.time_since_epoch().count())) + {} + +CONSTCD14 inline weekday& weekday::operator++() NOEXCEPT {*this += days{1}; return *this;} +CONSTCD14 inline weekday weekday::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} +CONSTCD14 inline weekday& weekday::operator--() NOEXCEPT {*this -= days{1}; return *this;} +CONSTCD14 inline weekday weekday::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} + +CONSTCD14 +inline +weekday& +weekday::operator+=(const days& d) NOEXCEPT +{ + *this = *this + d; + return *this; +} + +CONSTCD14 +inline +weekday& +weekday::operator-=(const days& d) NOEXCEPT +{ + *this = *this - d; + return *this; +} + +CONSTCD11 inline bool weekday::ok() const NOEXCEPT {return wd_ <= 6;} + +CONSTCD11 +inline +unsigned weekday::c_encoding() const NOEXCEPT +{ + return unsigned{wd_}; +} + +CONSTCD11 +inline +unsigned weekday::iso_encoding() const NOEXCEPT +{ + return unsigned{((wd_ == 0u) ? 7u : wd_)}; +} + +CONSTCD11 +inline +bool +operator==(const weekday& x, const weekday& y) NOEXCEPT +{ + return x.wd_ == y.wd_; +} + +CONSTCD11 +inline +bool +operator!=(const weekday& x, const weekday& y) NOEXCEPT +{ + return !(x == y); +} + +CONSTCD14 +inline +days +operator-(const weekday& x, const weekday& y) NOEXCEPT +{ + auto const wdu = x.wd_ - y.wd_; + auto const wk = (wdu >= 0 ? wdu : wdu-6) / 7; + return days{wdu - wk * 7}; +} + +CONSTCD14 +inline +weekday +operator+(const weekday& x, const days& y) NOEXCEPT +{ + auto const wdu = static_cast(static_cast(x.wd_)) + y.count(); + auto const wk = (wdu >= 0 ? wdu : wdu-6) / 7; + return weekday{static_cast(wdu - wk * 7)}; +} + +CONSTCD14 +inline +weekday +operator+(const days& x, const weekday& y) NOEXCEPT +{ + return y + x; +} + +CONSTCD14 +inline +weekday +operator-(const weekday& x, const days& y) NOEXCEPT +{ + return x + -y; +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const weekday& wd) +{ + if (wd.ok()) + { + CharT fmt[] = {'%', 'a', 0}; + os << format(fmt, wd); + } + else + os << wd.c_encoding(); + return os; +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const weekday& wd) +{ + detail::low_level_fmt(os, wd); + if (!wd.ok()) + os << " is not a valid weekday"; + return os; +} + +#if !defined(_MSC_VER) || (_MSC_VER >= 1900) +inline namespace literals +{ + +CONSTCD11 +inline +date::day +operator "" _d(unsigned long long d) NOEXCEPT +{ + return date::day{static_cast(d)}; +} + +CONSTCD11 +inline +date::year +operator "" _y(unsigned long long y) NOEXCEPT +{ + return date::year(static_cast(y)); +} +#endif // !defined(_MSC_VER) || (_MSC_VER >= 1900) + +CONSTDATA date::last_spec last{}; + +CONSTDATA date::month jan{1}; +CONSTDATA date::month feb{2}; +CONSTDATA date::month mar{3}; +CONSTDATA date::month apr{4}; +CONSTDATA date::month may{5}; +CONSTDATA date::month jun{6}; +CONSTDATA date::month jul{7}; +CONSTDATA date::month aug{8}; +CONSTDATA date::month sep{9}; +CONSTDATA date::month oct{10}; +CONSTDATA date::month nov{11}; +CONSTDATA date::month dec{12}; + +CONSTDATA date::weekday sun{0u}; +CONSTDATA date::weekday mon{1u}; +CONSTDATA date::weekday tue{2u}; +CONSTDATA date::weekday wed{3u}; +CONSTDATA date::weekday thu{4u}; +CONSTDATA date::weekday fri{5u}; +CONSTDATA date::weekday sat{6u}; + +#if !defined(_MSC_VER) || (_MSC_VER >= 1900) +} // inline namespace literals +#endif + +CONSTDATA date::month January{1}; +CONSTDATA date::month February{2}; +CONSTDATA date::month March{3}; +CONSTDATA date::month April{4}; +CONSTDATA date::month May{5}; +CONSTDATA date::month June{6}; +CONSTDATA date::month July{7}; +CONSTDATA date::month August{8}; +CONSTDATA date::month September{9}; +CONSTDATA date::month October{10}; +CONSTDATA date::month November{11}; +CONSTDATA date::month December{12}; + +CONSTDATA date::weekday Monday{1}; +CONSTDATA date::weekday Tuesday{2}; +CONSTDATA date::weekday Wednesday{3}; +CONSTDATA date::weekday Thursday{4}; +CONSTDATA date::weekday Friday{5}; +CONSTDATA date::weekday Saturday{6}; +CONSTDATA date::weekday Sunday{7}; + +// weekday_indexed + +CONSTCD11 +inline +weekday +weekday_indexed::weekday() const NOEXCEPT +{ + return date::weekday{static_cast(wd_)}; +} + +CONSTCD11 inline unsigned weekday_indexed::index() const NOEXCEPT {return index_;} + +CONSTCD11 +inline +bool +weekday_indexed::ok() const NOEXCEPT +{ + return weekday().ok() && 1 <= index_ && index_ <= 5; +} + +#ifdef __GNUC__ +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wconversion" +#endif // __GNUC__ + +CONSTCD11 +inline +weekday_indexed::weekday_indexed(const date::weekday& wd, unsigned index) NOEXCEPT + : wd_(static_cast(static_cast(wd.wd_))) + , index_(static_cast(index)) + {} + +#ifdef __GNUC__ +# pragma GCC diagnostic pop +#endif // __GNUC__ + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const weekday_indexed& wdi) +{ + return low_level_fmt(os, wdi.weekday()) << '[' << wdi.index() << ']'; +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const weekday_indexed& wdi) +{ + detail::low_level_fmt(os, wdi); + if (!wdi.ok()) + os << " is not a valid weekday_indexed"; + return os; +} + +CONSTCD11 +inline +weekday_indexed +weekday::operator[](unsigned index) const NOEXCEPT +{ + return {*this, index}; +} + +CONSTCD11 +inline +bool +operator==(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT +{ + return x.weekday() == y.weekday() && x.index() == y.index(); +} + +CONSTCD11 +inline +bool +operator!=(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT +{ + return !(x == y); +} + +// weekday_last + +CONSTCD11 inline date::weekday weekday_last::weekday() const NOEXCEPT {return wd_;} +CONSTCD11 inline bool weekday_last::ok() const NOEXCEPT {return wd_.ok();} +CONSTCD11 inline weekday_last::weekday_last(const date::weekday& wd) NOEXCEPT : wd_(wd) {} + +CONSTCD11 +inline +bool +operator==(const weekday_last& x, const weekday_last& y) NOEXCEPT +{ + return x.weekday() == y.weekday(); +} + +CONSTCD11 +inline +bool +operator!=(const weekday_last& x, const weekday_last& y) NOEXCEPT +{ + return !(x == y); +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const weekday_last& wdl) +{ + return low_level_fmt(os, wdl.weekday()) << "[last]"; +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const weekday_last& wdl) +{ + detail::low_level_fmt(os, wdl); + if (!wdl.ok()) + os << " is not a valid weekday_last"; + return os; +} + +CONSTCD11 +inline +weekday_last +weekday::operator[](last_spec) const NOEXCEPT +{ + return weekday_last{*this}; +} + +// year_month + +CONSTCD11 +inline +year_month::year_month(const date::year& y, const date::month& m) NOEXCEPT + : y_(y) + , m_(m) + {} + +CONSTCD11 inline year year_month::year() const NOEXCEPT {return y_;} +CONSTCD11 inline month year_month::month() const NOEXCEPT {return m_;} +CONSTCD11 inline bool year_month::ok() const NOEXCEPT {return y_.ok() && m_.ok();} + +template +CONSTCD14 +inline +year_month& +year_month::operator+=(const months& dm) NOEXCEPT +{ + *this = *this + dm; + return *this; +} + +template +CONSTCD14 +inline +year_month& +year_month::operator-=(const months& dm) NOEXCEPT +{ + *this = *this - dm; + return *this; +} + +CONSTCD14 +inline +year_month& +year_month::operator+=(const years& dy) NOEXCEPT +{ + *this = *this + dy; + return *this; +} + +CONSTCD14 +inline +year_month& +year_month::operator-=(const years& dy) NOEXCEPT +{ + *this = *this - dy; + return *this; +} + +CONSTCD11 +inline +bool +operator==(const year_month& x, const year_month& y) NOEXCEPT +{ + return x.year() == y.year() && x.month() == y.month(); +} + +CONSTCD11 +inline +bool +operator!=(const year_month& x, const year_month& y) NOEXCEPT +{ + return !(x == y); +} + +CONSTCD11 +inline +bool +operator<(const year_month& x, const year_month& y) NOEXCEPT +{ + return x.year() < y.year() ? true + : (x.year() > y.year() ? false + : (x.month() < y.month())); +} + +CONSTCD11 +inline +bool +operator>(const year_month& x, const year_month& y) NOEXCEPT +{ + return y < x; +} + +CONSTCD11 +inline +bool +operator<=(const year_month& x, const year_month& y) NOEXCEPT +{ + return !(y < x); +} + +CONSTCD11 +inline +bool +operator>=(const year_month& x, const year_month& y) NOEXCEPT +{ + return !(x < y); +} + +template +CONSTCD14 +inline +year_month +operator+(const year_month& ym, const months& dm) NOEXCEPT +{ + auto dmi = static_cast(static_cast(ym.month())) - 1 + dm.count(); + auto dy = (dmi >= 0 ? dmi : dmi-11) / 12; + dmi = dmi - dy * 12 + 1; + return (ym.year() + years(dy)) / month(static_cast(dmi)); +} + +template +CONSTCD14 +inline +year_month +operator+(const months& dm, const year_month& ym) NOEXCEPT +{ + return ym + dm; +} + +template +CONSTCD14 +inline +year_month +operator-(const year_month& ym, const months& dm) NOEXCEPT +{ + return ym + -dm; +} + +CONSTCD11 +inline +months +operator-(const year_month& x, const year_month& y) NOEXCEPT +{ + return (x.year() - y.year()) + + months(static_cast(x.month()) - static_cast(y.month())); +} + +CONSTCD11 +inline +year_month +operator+(const year_month& ym, const years& dy) NOEXCEPT +{ + return (ym.year() + dy) / ym.month(); +} + +CONSTCD11 +inline +year_month +operator+(const years& dy, const year_month& ym) NOEXCEPT +{ + return ym + dy; +} + +CONSTCD11 +inline +year_month +operator-(const year_month& ym, const years& dy) NOEXCEPT +{ + return ym + -dy; +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const year_month& ym) +{ + low_level_fmt(os, ym.year()) << '/'; + return low_level_fmt(os, ym.month()); +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month& ym) +{ + detail::low_level_fmt(os, ym); + if (!ym.ok()) + os << " is not a valid year_month"; + return os; +} + +// month_day + +CONSTCD11 +inline +month_day::month_day(const date::month& m, const date::day& d) NOEXCEPT + : m_(m) + , d_(d) + {} + +CONSTCD11 inline date::month month_day::month() const NOEXCEPT {return m_;} +CONSTCD11 inline date::day month_day::day() const NOEXCEPT {return d_;} + +CONSTCD14 +inline +bool +month_day::ok() const NOEXCEPT +{ + CONSTDATA date::day d[] = + { + date::day(31), date::day(29), date::day(31), + date::day(30), date::day(31), date::day(30), + date::day(31), date::day(31), date::day(30), + date::day(31), date::day(30), date::day(31) + }; + return m_.ok() && date::day{1} <= d_ && d_ <= d[static_cast(m_)-1]; +} + +CONSTCD11 +inline +bool +operator==(const month_day& x, const month_day& y) NOEXCEPT +{ + return x.month() == y.month() && x.day() == y.day(); +} + +CONSTCD11 +inline +bool +operator!=(const month_day& x, const month_day& y) NOEXCEPT +{ + return !(x == y); +} + +CONSTCD11 +inline +bool +operator<(const month_day& x, const month_day& y) NOEXCEPT +{ + return x.month() < y.month() ? true + : (x.month() > y.month() ? false + : (x.day() < y.day())); +} + +CONSTCD11 +inline +bool +operator>(const month_day& x, const month_day& y) NOEXCEPT +{ + return y < x; +} + +CONSTCD11 +inline +bool +operator<=(const month_day& x, const month_day& y) NOEXCEPT +{ + return !(y < x); +} + +CONSTCD11 +inline +bool +operator>=(const month_day& x, const month_day& y) NOEXCEPT +{ + return !(x < y); +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const month_day& md) +{ + low_level_fmt(os, md.month()) << '/'; + return low_level_fmt(os, md.day()); +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const month_day& md) +{ + detail::low_level_fmt(os, md); + if (!md.ok()) + os << " is not a valid month_day"; + return os; +} + +// month_day_last + +CONSTCD11 inline month month_day_last::month() const NOEXCEPT {return m_;} +CONSTCD11 inline bool month_day_last::ok() const NOEXCEPT {return m_.ok();} +CONSTCD11 inline month_day_last::month_day_last(const date::month& m) NOEXCEPT : m_(m) {} + +CONSTCD11 +inline +bool +operator==(const month_day_last& x, const month_day_last& y) NOEXCEPT +{ + return x.month() == y.month(); +} + +CONSTCD11 +inline +bool +operator!=(const month_day_last& x, const month_day_last& y) NOEXCEPT +{ + return !(x == y); +} + +CONSTCD11 +inline +bool +operator<(const month_day_last& x, const month_day_last& y) NOEXCEPT +{ + return x.month() < y.month(); +} + +CONSTCD11 +inline +bool +operator>(const month_day_last& x, const month_day_last& y) NOEXCEPT +{ + return y < x; +} + +CONSTCD11 +inline +bool +operator<=(const month_day_last& x, const month_day_last& y) NOEXCEPT +{ + return !(y < x); +} + +CONSTCD11 +inline +bool +operator>=(const month_day_last& x, const month_day_last& y) NOEXCEPT +{ + return !(x < y); +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const month_day_last& mdl) +{ + return low_level_fmt(os, mdl.month()) << "/last"; +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const month_day_last& mdl) +{ + detail::low_level_fmt(os, mdl); + if (!mdl.ok()) + os << " is not a valid month_day_last"; + return os; +} + +// month_weekday + +CONSTCD11 +inline +month_weekday::month_weekday(const date::month& m, + const date::weekday_indexed& wdi) NOEXCEPT + : m_(m) + , wdi_(wdi) + {} + +CONSTCD11 inline month month_weekday::month() const NOEXCEPT {return m_;} + +CONSTCD11 +inline +weekday_indexed +month_weekday::weekday_indexed() const NOEXCEPT +{ + return wdi_; +} + +CONSTCD11 +inline +bool +month_weekday::ok() const NOEXCEPT +{ + return m_.ok() && wdi_.ok(); +} + +CONSTCD11 +inline +bool +operator==(const month_weekday& x, const month_weekday& y) NOEXCEPT +{ + return x.month() == y.month() && x.weekday_indexed() == y.weekday_indexed(); +} + +CONSTCD11 +inline +bool +operator!=(const month_weekday& x, const month_weekday& y) NOEXCEPT +{ + return !(x == y); +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const month_weekday& mwd) +{ + low_level_fmt(os, mwd.month()) << '/'; + return low_level_fmt(os, mwd.weekday_indexed()); +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const month_weekday& mwd) +{ + detail::low_level_fmt(os, mwd); + if (!mwd.ok()) + os << " is not a valid month_weekday"; + return os; +} + +// month_weekday_last + +CONSTCD11 +inline +month_weekday_last::month_weekday_last(const date::month& m, + const date::weekday_last& wdl) NOEXCEPT + : m_(m) + , wdl_(wdl) + {} + +CONSTCD11 inline month month_weekday_last::month() const NOEXCEPT {return m_;} + +CONSTCD11 +inline +weekday_last +month_weekday_last::weekday_last() const NOEXCEPT +{ + return wdl_; +} + +CONSTCD11 +inline +bool +month_weekday_last::ok() const NOEXCEPT +{ + return m_.ok() && wdl_.ok(); +} + +CONSTCD11 +inline +bool +operator==(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT +{ + return x.month() == y.month() && x.weekday_last() == y.weekday_last(); +} + +CONSTCD11 +inline +bool +operator!=(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT +{ + return !(x == y); +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const month_weekday_last& mwdl) +{ + low_level_fmt(os, mwdl.month()) << '/'; + return low_level_fmt(os, mwdl.weekday_last()); +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const month_weekday_last& mwdl) +{ + detail::low_level_fmt(os, mwdl); + if (!mwdl.ok()) + os << " is not a valid month_weekday_last"; + return os; +} + +// year_month_day_last + +CONSTCD11 +inline +year_month_day_last::year_month_day_last(const date::year& y, + const date::month_day_last& mdl) NOEXCEPT + : y_(y) + , mdl_(mdl) + {} + +template +CONSTCD14 +inline +year_month_day_last& +year_month_day_last::operator+=(const months& m) NOEXCEPT +{ + *this = *this + m; + return *this; +} + +template +CONSTCD14 +inline +year_month_day_last& +year_month_day_last::operator-=(const months& m) NOEXCEPT +{ + *this = *this - m; + return *this; +} + +CONSTCD14 +inline +year_month_day_last& +year_month_day_last::operator+=(const years& y) NOEXCEPT +{ + *this = *this + y; + return *this; +} + +CONSTCD14 +inline +year_month_day_last& +year_month_day_last::operator-=(const years& y) NOEXCEPT +{ + *this = *this - y; + return *this; +} + +CONSTCD11 inline year year_month_day_last::year() const NOEXCEPT {return y_;} +CONSTCD11 inline month year_month_day_last::month() const NOEXCEPT {return mdl_.month();} + +CONSTCD11 +inline +month_day_last +year_month_day_last::month_day_last() const NOEXCEPT +{ + return mdl_; +} + +CONSTCD14 +inline +day +year_month_day_last::day() const NOEXCEPT +{ + CONSTDATA date::day d[] = + { + date::day(31), date::day(28), date::day(31), + date::day(30), date::day(31), date::day(30), + date::day(31), date::day(31), date::day(30), + date::day(31), date::day(30), date::day(31) + }; + return (month() != February || !y_.is_leap()) && mdl_.ok() ? + d[static_cast(month()) - 1] : date::day{29}; +} + +CONSTCD14 +inline +year_month_day_last::operator sys_days() const NOEXCEPT +{ + return sys_days(year()/month()/day()); +} + +CONSTCD14 +inline +year_month_day_last::operator local_days() const NOEXCEPT +{ + return local_days(year()/month()/day()); +} + +CONSTCD11 +inline +bool +year_month_day_last::ok() const NOEXCEPT +{ + return y_.ok() && mdl_.ok(); +} + +CONSTCD11 +inline +bool +operator==(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT +{ + return x.year() == y.year() && x.month_day_last() == y.month_day_last(); +} + +CONSTCD11 +inline +bool +operator!=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT +{ + return !(x == y); +} + +CONSTCD11 +inline +bool +operator<(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT +{ + return x.year() < y.year() ? true + : (x.year() > y.year() ? false + : (x.month_day_last() < y.month_day_last())); +} + +CONSTCD11 +inline +bool +operator>(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT +{ + return y < x; +} + +CONSTCD11 +inline +bool +operator<=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT +{ + return !(y < x); +} + +CONSTCD11 +inline +bool +operator>=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT +{ + return !(x < y); +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const year_month_day_last& ymdl) +{ + low_level_fmt(os, ymdl.year()) << '/'; + return low_level_fmt(os, ymdl.month_day_last()); +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month_day_last& ymdl) +{ + detail::low_level_fmt(os, ymdl); + if (!ymdl.ok()) + os << " is not a valid year_month_day_last"; + return os; +} + +template +CONSTCD14 +inline +year_month_day_last +operator+(const year_month_day_last& ymdl, const months& dm) NOEXCEPT +{ + return (ymdl.year() / ymdl.month() + dm) / last; +} + +template +CONSTCD14 +inline +year_month_day_last +operator+(const months& dm, const year_month_day_last& ymdl) NOEXCEPT +{ + return ymdl + dm; +} + +template +CONSTCD14 +inline +year_month_day_last +operator-(const year_month_day_last& ymdl, const months& dm) NOEXCEPT +{ + return ymdl + (-dm); +} + +CONSTCD11 +inline +year_month_day_last +operator+(const year_month_day_last& ymdl, const years& dy) NOEXCEPT +{ + return {ymdl.year()+dy, ymdl.month_day_last()}; +} + +CONSTCD11 +inline +year_month_day_last +operator+(const years& dy, const year_month_day_last& ymdl) NOEXCEPT +{ + return ymdl + dy; +} + +CONSTCD11 +inline +year_month_day_last +operator-(const year_month_day_last& ymdl, const years& dy) NOEXCEPT +{ + return ymdl + (-dy); +} + +// year_month_day + +CONSTCD11 +inline +year_month_day::year_month_day(const date::year& y, const date::month& m, + const date::day& d) NOEXCEPT + : y_(y) + , m_(m) + , d_(d) + {} + +CONSTCD14 +inline +year_month_day::year_month_day(const year_month_day_last& ymdl) NOEXCEPT + : y_(ymdl.year()) + , m_(ymdl.month()) + , d_(ymdl.day()) + {} + +CONSTCD14 +inline +year_month_day::year_month_day(sys_days dp) NOEXCEPT + : year_month_day(from_days(dp.time_since_epoch())) + {} + +CONSTCD14 +inline +year_month_day::year_month_day(local_days dp) NOEXCEPT + : year_month_day(from_days(dp.time_since_epoch())) + {} + +CONSTCD11 inline year year_month_day::year() const NOEXCEPT {return y_;} +CONSTCD11 inline month year_month_day::month() const NOEXCEPT {return m_;} +CONSTCD11 inline day year_month_day::day() const NOEXCEPT {return d_;} + +template +CONSTCD14 +inline +year_month_day& +year_month_day::operator+=(const months& m) NOEXCEPT +{ + *this = *this + m; + return *this; +} + +template +CONSTCD14 +inline +year_month_day& +year_month_day::operator-=(const months& m) NOEXCEPT +{ + *this = *this - m; + return *this; +} + +CONSTCD14 +inline +year_month_day& +year_month_day::operator+=(const years& y) NOEXCEPT +{ + *this = *this + y; + return *this; +} + +CONSTCD14 +inline +year_month_day& +year_month_day::operator-=(const years& y) NOEXCEPT +{ + *this = *this - y; + return *this; +} + +CONSTCD14 +inline +days +year_month_day::to_days() const NOEXCEPT +{ + static_assert(std::numeric_limits::digits >= 18, + "This algorithm has not been ported to a 16 bit unsigned integer"); + static_assert(std::numeric_limits::digits >= 20, + "This algorithm has not been ported to a 16 bit signed integer"); + auto const y = static_cast(y_) - (m_ <= February); + auto const m = static_cast(m_); + auto const d = static_cast(d_); + auto const era = (y >= 0 ? y : y-399) / 400; + auto const yoe = static_cast(y - era * 400); // [0, 399] + auto const doy = (153*(m > 2 ? m-3 : m+9) + 2)/5 + d-1; // [0, 365] + auto const doe = yoe * 365 + yoe/4 - yoe/100 + doy; // [0, 146096] + return days{era * 146097 + static_cast(doe) - 719468}; +} + +CONSTCD14 +inline +year_month_day::operator sys_days() const NOEXCEPT +{ + return sys_days{to_days()}; +} + +CONSTCD14 +inline +year_month_day::operator local_days() const NOEXCEPT +{ + return local_days{to_days()}; +} + +CONSTCD14 +inline +bool +year_month_day::ok() const NOEXCEPT +{ + if (!(y_.ok() && m_.ok())) + return false; + return date::day{1} <= d_ && d_ <= (y_ / m_ / last).day(); +} + +CONSTCD11 +inline +bool +operator==(const year_month_day& x, const year_month_day& y) NOEXCEPT +{ + return x.year() == y.year() && x.month() == y.month() && x.day() == y.day(); +} + +CONSTCD11 +inline +bool +operator!=(const year_month_day& x, const year_month_day& y) NOEXCEPT +{ + return !(x == y); +} + +CONSTCD11 +inline +bool +operator<(const year_month_day& x, const year_month_day& y) NOEXCEPT +{ + return x.year() < y.year() ? true + : (x.year() > y.year() ? false + : (x.month() < y.month() ? true + : (x.month() > y.month() ? false + : (x.day() < y.day())))); +} + +CONSTCD11 +inline +bool +operator>(const year_month_day& x, const year_month_day& y) NOEXCEPT +{ + return y < x; +} + +CONSTCD11 +inline +bool +operator<=(const year_month_day& x, const year_month_day& y) NOEXCEPT +{ + return !(y < x); +} + +CONSTCD11 +inline +bool +operator>=(const year_month_day& x, const year_month_day& y) NOEXCEPT +{ + return !(x < y); +} + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month_day& ymd) +{ + detail::save_ostream _(os); + os.fill('0'); + os.flags(std::ios::dec | std::ios::right); + os.imbue(std::locale::classic()); + os << static_cast(ymd.year()) << '-'; + os.width(2); + os << static_cast(ymd.month()) << '-'; + os.width(2); + os << static_cast(ymd.day()); + if (!ymd.ok()) + os << " is not a valid year_month_day"; + return os; +} + +CONSTCD14 +inline +year_month_day +year_month_day::from_days(days dp) NOEXCEPT +{ + static_assert(std::numeric_limits::digits >= 18, + "This algorithm has not been ported to a 16 bit unsigned integer"); + static_assert(std::numeric_limits::digits >= 20, + "This algorithm has not been ported to a 16 bit signed integer"); + auto const z = dp.count() + 719468; + auto const era = (z >= 0 ? z : z - 146096) / 146097; + auto const doe = static_cast(z - era * 146097); // [0, 146096] + auto const yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365; // [0, 399] + auto const y = static_cast(yoe) + era * 400; + auto const doy = doe - (365*yoe + yoe/4 - yoe/100); // [0, 365] + auto const mp = (5*doy + 2)/153; // [0, 11] + auto const d = doy - (153*mp+2)/5 + 1; // [1, 31] + auto const m = mp < 10 ? mp+3 : mp-9; // [1, 12] + return year_month_day{date::year{y + (m <= 2)}, date::month(m), date::day(d)}; +} + +template +CONSTCD14 +inline +year_month_day +operator+(const year_month_day& ymd, const months& dm) NOEXCEPT +{ + return (ymd.year() / ymd.month() + dm) / ymd.day(); +} + +template +CONSTCD14 +inline +year_month_day +operator+(const months& dm, const year_month_day& ymd) NOEXCEPT +{ + return ymd + dm; +} + +template +CONSTCD14 +inline +year_month_day +operator-(const year_month_day& ymd, const months& dm) NOEXCEPT +{ + return ymd + (-dm); +} + +CONSTCD11 +inline +year_month_day +operator+(const year_month_day& ymd, const years& dy) NOEXCEPT +{ + return (ymd.year() + dy) / ymd.month() / ymd.day(); +} + +CONSTCD11 +inline +year_month_day +operator+(const years& dy, const year_month_day& ymd) NOEXCEPT +{ + return ymd + dy; +} + +CONSTCD11 +inline +year_month_day +operator-(const year_month_day& ymd, const years& dy) NOEXCEPT +{ + return ymd + (-dy); +} + +// year_month_weekday + +CONSTCD11 +inline +year_month_weekday::year_month_weekday(const date::year& y, const date::month& m, + const date::weekday_indexed& wdi) + NOEXCEPT + : y_(y) + , m_(m) + , wdi_(wdi) + {} + +CONSTCD14 +inline +year_month_weekday::year_month_weekday(const sys_days& dp) NOEXCEPT + : year_month_weekday(from_days(dp.time_since_epoch())) + {} + +CONSTCD14 +inline +year_month_weekday::year_month_weekday(const local_days& dp) NOEXCEPT + : year_month_weekday(from_days(dp.time_since_epoch())) + {} + +template +CONSTCD14 +inline +year_month_weekday& +year_month_weekday::operator+=(const months& m) NOEXCEPT +{ + *this = *this + m; + return *this; +} + +template +CONSTCD14 +inline +year_month_weekday& +year_month_weekday::operator-=(const months& m) NOEXCEPT +{ + *this = *this - m; + return *this; +} + +CONSTCD14 +inline +year_month_weekday& +year_month_weekday::operator+=(const years& y) NOEXCEPT +{ + *this = *this + y; + return *this; +} + +CONSTCD14 +inline +year_month_weekday& +year_month_weekday::operator-=(const years& y) NOEXCEPT +{ + *this = *this - y; + return *this; +} + +CONSTCD11 inline year year_month_weekday::year() const NOEXCEPT {return y_;} +CONSTCD11 inline month year_month_weekday::month() const NOEXCEPT {return m_;} + +CONSTCD11 +inline +weekday +year_month_weekday::weekday() const NOEXCEPT +{ + return wdi_.weekday(); +} + +CONSTCD11 +inline +unsigned +year_month_weekday::index() const NOEXCEPT +{ + return wdi_.index(); +} + +CONSTCD11 +inline +weekday_indexed +year_month_weekday::weekday_indexed() const NOEXCEPT +{ + return wdi_; +} + +CONSTCD14 +inline +year_month_weekday::operator sys_days() const NOEXCEPT +{ + return sys_days{to_days()}; +} + +CONSTCD14 +inline +year_month_weekday::operator local_days() const NOEXCEPT +{ + return local_days{to_days()}; +} + +CONSTCD14 +inline +bool +year_month_weekday::ok() const NOEXCEPT +{ + if (!y_.ok() || !m_.ok() || !wdi_.weekday().ok() || wdi_.index() < 1) + return false; + if (wdi_.index() <= 4) + return true; + auto d2 = wdi_.weekday() - date::weekday(static_cast(y_/m_/1)) + + days((wdi_.index()-1)*7 + 1); + return static_cast(d2.count()) <= static_cast((y_/m_/last).day()); +} + +CONSTCD14 +inline +year_month_weekday +year_month_weekday::from_days(days d) NOEXCEPT +{ + sys_days dp{d}; + auto const wd = date::weekday(dp); + auto const ymd = year_month_day(dp); + return {ymd.year(), ymd.month(), wd[(static_cast(ymd.day())-1)/7+1]}; +} + +CONSTCD14 +inline +days +year_month_weekday::to_days() const NOEXCEPT +{ + auto d = sys_days(y_/m_/1); + return (d + (wdi_.weekday() - date::weekday(d) + days{(wdi_.index()-1)*7}) + ).time_since_epoch(); +} + +CONSTCD11 +inline +bool +operator==(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT +{ + return x.year() == y.year() && x.month() == y.month() && + x.weekday_indexed() == y.weekday_indexed(); +} + +CONSTCD11 +inline +bool +operator!=(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT +{ + return !(x == y); +} + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month_weekday& ymwdi) +{ + detail::low_level_fmt(os, ymwdi.year()) << '/'; + detail::low_level_fmt(os, ymwdi.month()) << '/'; + detail::low_level_fmt(os, ymwdi.weekday_indexed()); + if (!ymwdi.ok()) + os << " is not a valid year_month_weekday"; + return os; +} + +template +CONSTCD14 +inline +year_month_weekday +operator+(const year_month_weekday& ymwd, const months& dm) NOEXCEPT +{ + return (ymwd.year() / ymwd.month() + dm) / ymwd.weekday_indexed(); +} + +template +CONSTCD14 +inline +year_month_weekday +operator+(const months& dm, const year_month_weekday& ymwd) NOEXCEPT +{ + return ymwd + dm; +} + +template +CONSTCD14 +inline +year_month_weekday +operator-(const year_month_weekday& ymwd, const months& dm) NOEXCEPT +{ + return ymwd + (-dm); +} + +CONSTCD11 +inline +year_month_weekday +operator+(const year_month_weekday& ymwd, const years& dy) NOEXCEPT +{ + return {ymwd.year()+dy, ymwd.month(), ymwd.weekday_indexed()}; +} + +CONSTCD11 +inline +year_month_weekday +operator+(const years& dy, const year_month_weekday& ymwd) NOEXCEPT +{ + return ymwd + dy; +} + +CONSTCD11 +inline +year_month_weekday +operator-(const year_month_weekday& ymwd, const years& dy) NOEXCEPT +{ + return ymwd + (-dy); +} + +// year_month_weekday_last + +CONSTCD11 +inline +year_month_weekday_last::year_month_weekday_last(const date::year& y, + const date::month& m, + const date::weekday_last& wdl) NOEXCEPT + : y_(y) + , m_(m) + , wdl_(wdl) + {} + +template +CONSTCD14 +inline +year_month_weekday_last& +year_month_weekday_last::operator+=(const months& m) NOEXCEPT +{ + *this = *this + m; + return *this; +} + +template +CONSTCD14 +inline +year_month_weekday_last& +year_month_weekday_last::operator-=(const months& m) NOEXCEPT +{ + *this = *this - m; + return *this; +} + +CONSTCD14 +inline +year_month_weekday_last& +year_month_weekday_last::operator+=(const years& y) NOEXCEPT +{ + *this = *this + y; + return *this; +} + +CONSTCD14 +inline +year_month_weekday_last& +year_month_weekday_last::operator-=(const years& y) NOEXCEPT +{ + *this = *this - y; + return *this; +} + +CONSTCD11 inline year year_month_weekday_last::year() const NOEXCEPT {return y_;} +CONSTCD11 inline month year_month_weekday_last::month() const NOEXCEPT {return m_;} + +CONSTCD11 +inline +weekday +year_month_weekday_last::weekday() const NOEXCEPT +{ + return wdl_.weekday(); +} + +CONSTCD11 +inline +weekday_last +year_month_weekday_last::weekday_last() const NOEXCEPT +{ + return wdl_; +} + +CONSTCD14 +inline +year_month_weekday_last::operator sys_days() const NOEXCEPT +{ + return sys_days{to_days()}; +} + +CONSTCD14 +inline +year_month_weekday_last::operator local_days() const NOEXCEPT +{ + return local_days{to_days()}; +} + +CONSTCD11 +inline +bool +year_month_weekday_last::ok() const NOEXCEPT +{ + return y_.ok() && m_.ok() && wdl_.ok(); +} + +CONSTCD14 +inline +days +year_month_weekday_last::to_days() const NOEXCEPT +{ + auto const d = sys_days(y_/m_/last); + return (d - (date::weekday{d} - wdl_.weekday())).time_since_epoch(); +} + +CONSTCD11 +inline +bool +operator==(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT +{ + return x.year() == y.year() && x.month() == y.month() && + x.weekday_last() == y.weekday_last(); +} + +CONSTCD11 +inline +bool +operator!=(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT +{ + return !(x == y); +} + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month_weekday_last& ymwdl) +{ + detail::low_level_fmt(os, ymwdl.year()) << '/'; + detail::low_level_fmt(os, ymwdl.month()) << '/'; + detail::low_level_fmt(os, ymwdl.weekday_last()); + if (!ymwdl.ok()) + os << " is not a valid year_month_weekday_last"; + return os; +} + +template +CONSTCD14 +inline +year_month_weekday_last +operator+(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT +{ + return (ymwdl.year() / ymwdl.month() + dm) / ymwdl.weekday_last(); +} + +template +CONSTCD14 +inline +year_month_weekday_last +operator+(const months& dm, const year_month_weekday_last& ymwdl) NOEXCEPT +{ + return ymwdl + dm; +} + +template +CONSTCD14 +inline +year_month_weekday_last +operator-(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT +{ + return ymwdl + (-dm); +} + +CONSTCD11 +inline +year_month_weekday_last +operator+(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT +{ + return {ymwdl.year()+dy, ymwdl.month(), ymwdl.weekday_last()}; +} + +CONSTCD11 +inline +year_month_weekday_last +operator+(const years& dy, const year_month_weekday_last& ymwdl) NOEXCEPT +{ + return ymwdl + dy; +} + +CONSTCD11 +inline +year_month_weekday_last +operator-(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT +{ + return ymwdl + (-dy); +} + +// year_month from operator/() + +CONSTCD11 +inline +year_month +operator/(const year& y, const month& m) NOEXCEPT +{ + return {y, m}; +} + +CONSTCD11 +inline +year_month +operator/(const year& y, int m) NOEXCEPT +{ + return y / month(static_cast(m)); +} + +// month_day from operator/() + +CONSTCD11 +inline +month_day +operator/(const month& m, const day& d) NOEXCEPT +{ + return {m, d}; +} + +CONSTCD11 +inline +month_day +operator/(const day& d, const month& m) NOEXCEPT +{ + return m / d; +} + +CONSTCD11 +inline +month_day +operator/(const month& m, int d) NOEXCEPT +{ + return m / day(static_cast(d)); +} + +CONSTCD11 +inline +month_day +operator/(int m, const day& d) NOEXCEPT +{ + return month(static_cast(m)) / d; +} + +CONSTCD11 inline month_day operator/(const day& d, int m) NOEXCEPT {return m / d;} + +// month_day_last from operator/() + +CONSTCD11 +inline +month_day_last +operator/(const month& m, last_spec) NOEXCEPT +{ + return month_day_last{m}; +} + +CONSTCD11 +inline +month_day_last +operator/(last_spec, const month& m) NOEXCEPT +{ + return m/last; +} + +CONSTCD11 +inline +month_day_last +operator/(int m, last_spec) NOEXCEPT +{ + return month(static_cast(m))/last; +} + +CONSTCD11 +inline +month_day_last +operator/(last_spec, int m) NOEXCEPT +{ + return m/last; +} + +// month_weekday from operator/() + +CONSTCD11 +inline +month_weekday +operator/(const month& m, const weekday_indexed& wdi) NOEXCEPT +{ + return {m, wdi}; +} + +CONSTCD11 +inline +month_weekday +operator/(const weekday_indexed& wdi, const month& m) NOEXCEPT +{ + return m / wdi; +} + +CONSTCD11 +inline +month_weekday +operator/(int m, const weekday_indexed& wdi) NOEXCEPT +{ + return month(static_cast(m)) / wdi; +} + +CONSTCD11 +inline +month_weekday +operator/(const weekday_indexed& wdi, int m) NOEXCEPT +{ + return m / wdi; +} + +// month_weekday_last from operator/() + +CONSTCD11 +inline +month_weekday_last +operator/(const month& m, const weekday_last& wdl) NOEXCEPT +{ + return {m, wdl}; +} + +CONSTCD11 +inline +month_weekday_last +operator/(const weekday_last& wdl, const month& m) NOEXCEPT +{ + return m / wdl; +} + +CONSTCD11 +inline +month_weekday_last +operator/(int m, const weekday_last& wdl) NOEXCEPT +{ + return month(static_cast(m)) / wdl; +} + +CONSTCD11 +inline +month_weekday_last +operator/(const weekday_last& wdl, int m) NOEXCEPT +{ + return m / wdl; +} + +// year_month_day from operator/() + +CONSTCD11 +inline +year_month_day +operator/(const year_month& ym, const day& d) NOEXCEPT +{ + return {ym.year(), ym.month(), d}; +} + +CONSTCD11 +inline +year_month_day +operator/(const year_month& ym, int d) NOEXCEPT +{ + return ym / day(static_cast(d)); +} + +CONSTCD11 +inline +year_month_day +operator/(const year& y, const month_day& md) NOEXCEPT +{ + return y / md.month() / md.day(); +} + +CONSTCD11 +inline +year_month_day +operator/(int y, const month_day& md) NOEXCEPT +{ + return year(y) / md; +} + +CONSTCD11 +inline +year_month_day +operator/(const month_day& md, const year& y) NOEXCEPT +{ + return y / md; +} + +CONSTCD11 +inline +year_month_day +operator/(const month_day& md, int y) NOEXCEPT +{ + return year(y) / md; +} + +// year_month_day_last from operator/() + +CONSTCD11 +inline +year_month_day_last +operator/(const year_month& ym, last_spec) NOEXCEPT +{ + return {ym.year(), month_day_last{ym.month()}}; +} + +CONSTCD11 +inline +year_month_day_last +operator/(const year& y, const month_day_last& mdl) NOEXCEPT +{ + return {y, mdl}; +} + +CONSTCD11 +inline +year_month_day_last +operator/(int y, const month_day_last& mdl) NOEXCEPT +{ + return year(y) / mdl; +} + +CONSTCD11 +inline +year_month_day_last +operator/(const month_day_last& mdl, const year& y) NOEXCEPT +{ + return y / mdl; +} + +CONSTCD11 +inline +year_month_day_last +operator/(const month_day_last& mdl, int y) NOEXCEPT +{ + return year(y) / mdl; +} + +// year_month_weekday from operator/() + +CONSTCD11 +inline +year_month_weekday +operator/(const year_month& ym, const weekday_indexed& wdi) NOEXCEPT +{ + return {ym.year(), ym.month(), wdi}; +} + +CONSTCD11 +inline +year_month_weekday +operator/(const year& y, const month_weekday& mwd) NOEXCEPT +{ + return {y, mwd.month(), mwd.weekday_indexed()}; +} + +CONSTCD11 +inline +year_month_weekday +operator/(int y, const month_weekday& mwd) NOEXCEPT +{ + return year(y) / mwd; +} + +CONSTCD11 +inline +year_month_weekday +operator/(const month_weekday& mwd, const year& y) NOEXCEPT +{ + return y / mwd; +} + +CONSTCD11 +inline +year_month_weekday +operator/(const month_weekday& mwd, int y) NOEXCEPT +{ + return year(y) / mwd; +} + +// year_month_weekday_last from operator/() + +CONSTCD11 +inline +year_month_weekday_last +operator/(const year_month& ym, const weekday_last& wdl) NOEXCEPT +{ + return {ym.year(), ym.month(), wdl}; +} + +CONSTCD11 +inline +year_month_weekday_last +operator/(const year& y, const month_weekday_last& mwdl) NOEXCEPT +{ + return {y, mwdl.month(), mwdl.weekday_last()}; +} + +CONSTCD11 +inline +year_month_weekday_last +operator/(int y, const month_weekday_last& mwdl) NOEXCEPT +{ + return year(y) / mwdl; +} + +CONSTCD11 +inline +year_month_weekday_last +operator/(const month_weekday_last& mwdl, const year& y) NOEXCEPT +{ + return y / mwdl; +} + +CONSTCD11 +inline +year_month_weekday_last +operator/(const month_weekday_last& mwdl, int y) NOEXCEPT +{ + return year(y) / mwdl; +} + +template +struct fields; + +template +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, + const fields& fds, const std::string* abbrev = nullptr, + const std::chrono::seconds* offset_sec = nullptr); + +template +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, + fields& fds, std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr); + +// hh_mm_ss + +namespace detail +{ + +struct undocumented {explicit undocumented() = default;}; + +// width::value is the number of fractional decimal digits in 1/n +// width<0>::value and width<1>::value are defined to be 0 +// If 1/n takes more than 18 fractional decimal digits, +// the result is truncated to 19. +// Example: width<2>::value == 1 +// Example: width<3>::value == 19 +// Example: width<4>::value == 2 +// Example: width<10>::value == 1 +// Example: width<1000>::value == 3 +template +struct width +{ + static_assert(d > 0, "width called with zero denominator"); + static CONSTDATA unsigned value = 1 + width::value; +}; + +template +struct width +{ + static CONSTDATA unsigned value = 0; +}; + +template +struct static_pow10 +{ +private: + static CONSTDATA std::uint64_t h = static_pow10::value; +public: + static CONSTDATA std::uint64_t value = h * h * (exp % 2 ? 10 : 1); +}; + +template <> +struct static_pow10<0> +{ + static CONSTDATA std::uint64_t value = 1; +}; + +template +class decimal_format_seconds +{ + using CT = typename std::common_type::type; + using rep = typename CT::rep; + static unsigned CONSTDATA trial_width = + detail::width::value; +public: + static unsigned CONSTDATA width = trial_width < 19 ? trial_width : 6u; + using precision = std::chrono::duration::value>>; + +private: + std::chrono::seconds s_; + precision sub_s_; + +public: + CONSTCD11 decimal_format_seconds() + : s_() + , sub_s_() + {} + + CONSTCD11 explicit decimal_format_seconds(const Duration& d) NOEXCEPT + : s_(std::chrono::duration_cast(d)) + , sub_s_(std::chrono::duration_cast(d - s_)) + {} + + CONSTCD14 std::chrono::seconds& seconds() NOEXCEPT {return s_;} + CONSTCD11 std::chrono::seconds seconds() const NOEXCEPT {return s_;} + CONSTCD11 precision subseconds() const NOEXCEPT {return sub_s_;} + + CONSTCD14 precision to_duration() const NOEXCEPT + { + return s_ + sub_s_; + } + + CONSTCD11 bool in_conventional_range() const NOEXCEPT + { + return sub_s_ < std::chrono::seconds{1} && s_ < std::chrono::minutes{1}; + } + + template + friend + std::basic_ostream& + operator<<(std::basic_ostream& os, const decimal_format_seconds& x) + { + return x.print(os, std::chrono::treat_as_floating_point{}); + } + + template + std::basic_ostream& + print(std::basic_ostream& os, std::true_type) const + { + date::detail::save_ostream _(os); + std::chrono::duration d = s_ + sub_s_; + if (d < std::chrono::seconds{10}) + os << '0'; + os.precision(width+6); + os << std::fixed << d.count(); + return os; + } + + template + std::basic_ostream& + print(std::basic_ostream& os, std::false_type) const + { + date::detail::save_ostream _(os); + os.fill('0'); + os.flags(std::ios::dec | std::ios::right); + os.width(2); + os << s_.count(); + if (width > 0) + { +#if !ONLY_C_LOCALE + os << std::use_facet>(os.getloc()).decimal_point(); +#else + os << '.'; +#endif + date::detail::save_ostream _s(os); + os.imbue(std::locale::classic()); + os.width(width); + os << sub_s_.count(); + } + return os; + } +}; + +template +inline +CONSTCD11 +typename std::enable_if + < + std::numeric_limits::is_signed, + std::chrono::duration + >::type +abs(std::chrono::duration d) +{ + return d >= d.zero() ? +d : -d; +} + +template +inline +CONSTCD11 +typename std::enable_if + < + !std::numeric_limits::is_signed, + std::chrono::duration + >::type +abs(std::chrono::duration d) +{ + return d; +} + +} // namespace detail + +template +class hh_mm_ss +{ + using dfs = detail::decimal_format_seconds::type>; + + std::chrono::hours h_; + std::chrono::minutes m_; + dfs s_; + bool neg_; + +public: + static unsigned CONSTDATA fractional_width = dfs::width; + using precision = typename dfs::precision; + + CONSTCD11 hh_mm_ss() NOEXCEPT + : hh_mm_ss(Duration::zero()) + {} + + CONSTCD11 explicit hh_mm_ss(Duration d) NOEXCEPT + : h_(std::chrono::duration_cast(detail::abs(d))) + , m_(std::chrono::duration_cast(detail::abs(d)) - h_) + , s_(detail::abs(d) - h_ - m_) + , neg_(d < Duration::zero()) + {} + + CONSTCD11 std::chrono::hours hours() const NOEXCEPT {return h_;} + CONSTCD11 std::chrono::minutes minutes() const NOEXCEPT {return m_;} + CONSTCD11 std::chrono::seconds seconds() const NOEXCEPT {return s_.seconds();} + CONSTCD14 std::chrono::seconds& + seconds(detail::undocumented) NOEXCEPT {return s_.seconds();} + CONSTCD11 precision subseconds() const NOEXCEPT {return s_.subseconds();} + CONSTCD11 bool is_negative() const NOEXCEPT {return neg_;} + + CONSTCD11 explicit operator precision() const NOEXCEPT {return to_duration();} + CONSTCD11 precision to_duration() const NOEXCEPT + {return (s_.to_duration() + m_ + h_) * (1-2*neg_);} + + CONSTCD11 bool in_conventional_range() const NOEXCEPT + { + return !neg_ && h_ < days{1} && m_ < std::chrono::hours{1} && + s_.in_conventional_range(); + } + +private: + + template + friend + std::basic_ostream& + operator<<(std::basic_ostream& os, hh_mm_ss const& tod) + { + if (tod.is_negative()) + os << '-'; + if (tod.h_ < std::chrono::hours{10}) + os << '0'; + os << tod.h_.count() << ':'; + if (tod.m_ < std::chrono::minutes{10}) + os << '0'; + os << tod.m_.count() << ':' << tod.s_; + return os; + } + + template + friend + std::basic_ostream& + date::to_stream(std::basic_ostream& os, const CharT* fmt, + const fields& fds, const std::string* abbrev, + const std::chrono::seconds* offset_sec); + + template + friend + std::basic_istream& + date::from_stream(std::basic_istream& is, const CharT* fmt, + fields& fds, + std::basic_string* abbrev, std::chrono::minutes* offset); +}; + +inline +CONSTCD14 +bool +is_am(std::chrono::hours const& h) NOEXCEPT +{ + using std::chrono::hours; + return hours{0} <= h && h < hours{12}; +} + +inline +CONSTCD14 +bool +is_pm(std::chrono::hours const& h) NOEXCEPT +{ + using std::chrono::hours; + return hours{12} <= h && h < hours{24}; +} + +inline +CONSTCD14 +std::chrono::hours +make12(std::chrono::hours h) NOEXCEPT +{ + using std::chrono::hours; + if (h < hours{12}) + { + if (h == hours{0}) + h = hours{12}; + } + else + { + if (h != hours{12}) + h = h - hours{12}; + } + return h; +} + +inline +CONSTCD14 +std::chrono::hours +make24(std::chrono::hours h, bool is_pm) NOEXCEPT +{ + using std::chrono::hours; + if (is_pm) + { + if (h != hours{12}) + h = h + hours{12}; + } + else if (h == hours{12}) + h = hours{0}; + return h; +} + +template +using time_of_day = hh_mm_ss; + +template +CONSTCD11 +inline +hh_mm_ss> +make_time(const std::chrono::duration& d) +{ + return hh_mm_ss>(d); +} + +template +inline +typename std::enable_if +< + std::ratio_less::value + , std::basic_ostream& +>::type +operator<<(std::basic_ostream& os, const sys_time& tp) +{ + auto const dp = date::floor(tp); + return os << year_month_day(dp) << ' ' << make_time(tp-dp); +} + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const sys_days& dp) +{ + return os << year_month_day(dp); +} + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const local_time& ut) +{ + return (os << sys_time{ut.time_since_epoch()}); +} + +namespace detail +{ + +template +class string_literal; + +template +inline +CONSTCD14 +string_literal::type, + N1 + N2 - 1> +operator+(const string_literal& x, const string_literal& y) NOEXCEPT; + +template +class string_literal +{ + CharT p_[N]; + + CONSTCD11 string_literal() NOEXCEPT + : p_{} + {} + +public: + using const_iterator = const CharT*; + + string_literal(string_literal const&) = default; + string_literal& operator=(string_literal const&) = delete; + + template ::type> + CONSTCD11 string_literal(CharT c) NOEXCEPT + : p_{c} + { + } + + template ::type> + CONSTCD11 string_literal(CharT c1, CharT c2) NOEXCEPT + : p_{c1, c2} + { + } + + template ::type> + CONSTCD11 string_literal(CharT c1, CharT c2, CharT c3) NOEXCEPT + : p_{c1, c2, c3} + { + } + + CONSTCD14 string_literal(const CharT(&a)[N]) NOEXCEPT + : p_{} + { + for (std::size_t i = 0; i < N; ++i) + p_[i] = a[i]; + } + + template ::type> + CONSTCD14 string_literal(const char(&a)[N]) NOEXCEPT + : p_{} + { + for (std::size_t i = 0; i < N; ++i) + p_[i] = a[i]; + } + + template ::value>::type> + CONSTCD14 string_literal(string_literal const& a) NOEXCEPT + : p_{} + { + for (std::size_t i = 0; i < N; ++i) + p_[i] = a[i]; + } + + CONSTCD11 const CharT* data() const NOEXCEPT {return p_;} + CONSTCD11 std::size_t size() const NOEXCEPT {return N-1;} + + CONSTCD11 const_iterator begin() const NOEXCEPT {return p_;} + CONSTCD11 const_iterator end() const NOEXCEPT {return p_ + N-1;} + + CONSTCD11 CharT const& operator[](std::size_t n) const NOEXCEPT + { + return p_[n]; + } + + template + friend + std::basic_ostream& + operator<<(std::basic_ostream& os, const string_literal& s) + { + return os << s.p_; + } + + template + friend + CONSTCD14 + string_literal::type, + N1 + N2 - 1> + operator+(const string_literal& x, const string_literal& y) NOEXCEPT; +}; + +template +CONSTCD11 +inline +string_literal +operator+(const string_literal& x, const string_literal& y) NOEXCEPT +{ + return string_literal(x[0], y[0]); +} + +template +CONSTCD11 +inline +string_literal +operator+(const string_literal& x, const string_literal& y) NOEXCEPT +{ + return string_literal(x[0], x[1], y[0]); +} + +template +CONSTCD14 +inline +string_literal::type, + N1 + N2 - 1> +operator+(const string_literal& x, const string_literal& y) NOEXCEPT +{ + using CT = typename std::conditional::type; + + string_literal r; + std::size_t i = 0; + for (; i < N1-1; ++i) + r.p_[i] = CT(x.p_[i]); + for (std::size_t j = 0; j < N2; ++j, ++i) + r.p_[i] = CT(y.p_[j]); + + return r; +} + + +template +inline +std::basic_string +operator+(std::basic_string x, const string_literal& y) +{ + x.append(y.data(), y.size()); + return x; +} + +#if __cplusplus >= 201402 && (!defined(__EDG_VERSION__) || __EDG_VERSION__ > 411) \ + && (!defined(__SUNPRO_CC) || __SUNPRO_CC > 0x5150) + +template ::value || + std::is_same::value || + std::is_same::value || + std::is_same::value>> +CONSTCD14 +inline +string_literal +msl(CharT c) NOEXCEPT +{ + return string_literal{c}; +} + +CONSTCD14 +inline +std::size_t +to_string_len(std::intmax_t i) +{ + std::size_t r = 0; + do + { + i /= 10; + ++r; + } while (i > 0); + return r; +} + +template +CONSTCD14 +inline +std::enable_if_t +< + N < 10, + string_literal +> +msl() NOEXCEPT +{ + return msl(char(N % 10 + '0')); +} + +template +CONSTCD14 +inline +std::enable_if_t +< + 10 <= N, + string_literal +> +msl() NOEXCEPT +{ + return msl() + msl(char(N % 10 + '0')); +} + +template +CONSTCD14 +inline +std::enable_if_t +< + std::ratio::type::den != 1, + string_literal::type::num) + + to_string_len(std::ratio::type::den) + 4> +> +msl(std::ratio) NOEXCEPT +{ + using R = typename std::ratio::type; + return msl(CharT{'['}) + msl() + msl(CharT{'/'}) + + msl() + msl(CharT{']'}); +} + +template +CONSTCD14 +inline +std::enable_if_t +< + std::ratio::type::den == 1, + string_literal::type::num) + 3> +> +msl(std::ratio) NOEXCEPT +{ + using R = typename std::ratio::type; + return msl(CharT{'['}) + msl() + msl(CharT{']'}); +} + + +#else // __cplusplus < 201402 || (defined(__EDG_VERSION__) && __EDG_VERSION__ <= 411) + +inline +std::string +to_string(std::uint64_t x) +{ + return std::to_string(x); +} + +template +inline +std::basic_string +to_string(std::uint64_t x) +{ + auto y = std::to_string(x); + return std::basic_string(y.begin(), y.end()); +} + +template +inline +typename std::enable_if +< + std::ratio::type::den != 1, + std::basic_string +>::type +msl(std::ratio) +{ + using R = typename std::ratio::type; + return std::basic_string(1, '[') + to_string(R::num) + CharT{'/'} + + to_string(R::den) + CharT{']'}; +} + +template +inline +typename std::enable_if +< + std::ratio::type::den == 1, + std::basic_string +>::type +msl(std::ratio) +{ + using R = typename std::ratio::type; + return std::basic_string(1, '[') + to_string(R::num) + CharT{']'}; +} + +#endif // __cplusplus < 201402 || (defined(__EDG_VERSION__) && __EDG_VERSION__ <= 411) + +template +CONSTCD11 +inline +string_literal +msl(std::atto) NOEXCEPT +{ + return string_literal{'a'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::femto) NOEXCEPT +{ + return string_literal{'f'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::pico) NOEXCEPT +{ + return string_literal{'p'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::nano) NOEXCEPT +{ + return string_literal{'n'}; +} + +template +CONSTCD11 +inline +typename std::enable_if +< + std::is_same::value, + string_literal +>::type +msl(std::micro) NOEXCEPT +{ + return string_literal{'\xC2', '\xB5'}; +} + +template +CONSTCD11 +inline +typename std::enable_if +< + !std::is_same::value, + string_literal +>::type +msl(std::micro) NOEXCEPT +{ + return string_literal{CharT{static_cast('\xB5')}}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::milli) NOEXCEPT +{ + return string_literal{'m'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::centi) NOEXCEPT +{ + return string_literal{'c'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::deca) NOEXCEPT +{ + return string_literal{'d', 'a'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::deci) NOEXCEPT +{ + return string_literal{'d'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::hecto) NOEXCEPT +{ + return string_literal{'h'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::kilo) NOEXCEPT +{ + return string_literal{'k'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::mega) NOEXCEPT +{ + return string_literal{'M'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::giga) NOEXCEPT +{ + return string_literal{'G'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::tera) NOEXCEPT +{ + return string_literal{'T'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::peta) NOEXCEPT +{ + return string_literal{'P'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::exa) NOEXCEPT +{ + return string_literal{'E'}; +} + +template +CONSTCD11 +inline +auto +get_units(Period p) + -> decltype(msl(p) + string_literal{'s'}) +{ + return msl(p) + string_literal{'s'}; +} + +template +CONSTCD11 +inline +string_literal +get_units(std::ratio<1>) +{ + return string_literal{'s'}; +} + +template +CONSTCD11 +inline +string_literal +get_units(std::ratio<3600>) +{ + return string_literal{'h'}; +} + +template +CONSTCD11 +inline +string_literal +get_units(std::ratio<60>) +{ + return string_literal{'m', 'i', 'n'}; +} + +template +CONSTCD11 +inline +string_literal +get_units(std::ratio<86400>) +{ + return string_literal{'d'}; +} + +template > +struct make_string; + +template <> +struct make_string +{ + template + static + std::string + from(Rep n) + { + return std::to_string(n); + } +}; + +template +struct make_string +{ + template + static + std::basic_string + from(Rep n) + { + auto s = std::to_string(n); + return std::basic_string(s.begin(), s.end()); + } +}; + +template <> +struct make_string +{ + template + static + std::wstring + from(Rep n) + { + return std::to_wstring(n); + } +}; + +template +struct make_string +{ + template + static + std::basic_string + from(Rep n) + { + auto s = std::to_wstring(n); + return std::basic_string(s.begin(), s.end()); + } +}; + +} // namespace detail + +// to_stream + +CONSTDATA year nanyear{-32768}; + +template +struct fields +{ + year_month_day ymd{nanyear/0/0}; + weekday wd{8u}; + hh_mm_ss tod{}; + bool has_tod = false; + +#if !defined(__clang__) && defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__ <= 409) + fields() : ymd{nanyear/0/0}, wd{8u}, tod{}, has_tod{false} {} +#else + fields() = default; +#endif + + fields(year_month_day ymd_) : ymd(ymd_) {} + fields(weekday wd_) : wd(wd_) {} + fields(hh_mm_ss tod_) : tod(tod_), has_tod(true) {} + + fields(year_month_day ymd_, weekday wd_) : ymd(ymd_), wd(wd_) {} + fields(year_month_day ymd_, hh_mm_ss tod_) : ymd(ymd_), tod(tod_), + has_tod(true) {} + + fields(weekday wd_, hh_mm_ss tod_) : wd(wd_), tod(tod_), has_tod(true) {} + + fields(year_month_day ymd_, weekday wd_, hh_mm_ss tod_) + : ymd(ymd_) + , wd(wd_) + , tod(tod_) + , has_tod(true) + {} +}; + +namespace detail +{ + +template +unsigned +extract_weekday(std::basic_ostream& os, const fields& fds) +{ + if (!fds.ymd.ok() && !fds.wd.ok()) + { + // fds does not contain a valid weekday + os.setstate(std::ios::failbit); + return 8; + } + weekday wd; + if (fds.ymd.ok()) + { + wd = weekday{sys_days(fds.ymd)}; + if (fds.wd.ok() && wd != fds.wd) + { + // fds.ymd and fds.wd are inconsistent + os.setstate(std::ios::failbit); + return 8; + } + } + else + wd = fds.wd; + return static_cast((wd - Sunday).count()); +} + +template +unsigned +extract_month(std::basic_ostream& os, const fields& fds) +{ + if (!fds.ymd.month().ok()) + { + // fds does not contain a valid month + os.setstate(std::ios::failbit); + return 0; + } + return static_cast(fds.ymd.month()); +} + +} // namespace detail + +#if ONLY_C_LOCALE + +namespace detail +{ + +inline +std::pair +weekday_names() +{ + static const std::string nm[] = + { + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat" + }; + return std::make_pair(nm, nm+sizeof(nm)/sizeof(nm[0])); +} + +inline +std::pair +month_names() +{ + static const std::string nm[] = + { + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + }; + return std::make_pair(nm, nm+sizeof(nm)/sizeof(nm[0])); +} + +inline +std::pair +ampm_names() +{ + static const std::string nm[] = + { + "AM", + "PM" + }; + return std::make_pair(nm, nm+sizeof(nm)/sizeof(nm[0])); +} + +template +FwdIter +scan_keyword(std::basic_istream& is, FwdIter kb, FwdIter ke) +{ + size_t nkw = static_cast(std::distance(kb, ke)); + const unsigned char doesnt_match = '\0'; + const unsigned char might_match = '\1'; + const unsigned char does_match = '\2'; + unsigned char statbuf[100]; + unsigned char* status = statbuf; + std::unique_ptr stat_hold(0, free); + if (nkw > sizeof(statbuf)) + { + status = (unsigned char*)std::malloc(nkw); + if (status == nullptr) + throw std::bad_alloc(); + stat_hold.reset(status); + } + size_t n_might_match = nkw; // At this point, any keyword might match + size_t n_does_match = 0; // but none of them definitely do + // Initialize all statuses to might_match, except for "" keywords are does_match + unsigned char* st = status; + for (auto ky = kb; ky != ke; ++ky, ++st) + { + if (!ky->empty()) + *st = might_match; + else + { + *st = does_match; + --n_might_match; + ++n_does_match; + } + } + // While there might be a match, test keywords against the next CharT + for (size_t indx = 0; is && n_might_match > 0; ++indx) + { + // Peek at the next CharT but don't consume it + auto ic = is.peek(); + if (ic == EOF) + { + is.setstate(std::ios::eofbit); + break; + } + auto c = static_cast(toupper(static_cast(ic))); + bool consume = false; + // For each keyword which might match, see if the indx character is c + // If a match if found, consume c + // If a match is found, and that is the last character in the keyword, + // then that keyword matches. + // If the keyword doesn't match this character, then change the keyword + // to doesn't match + st = status; + for (auto ky = kb; ky != ke; ++ky, ++st) + { + if (*st == might_match) + { + if (c == static_cast(toupper(static_cast((*ky)[indx])))) + { + consume = true; + if (ky->size() == indx+1) + { + *st = does_match; + --n_might_match; + ++n_does_match; + } + } + else + { + *st = doesnt_match; + --n_might_match; + } + } + } + // consume if we matched a character + if (consume) + { + (void)is.get(); + // If we consumed a character and there might be a matched keyword that + // was marked matched on a previous iteration, then such keywords + // are now marked as not matching. + if (n_might_match + n_does_match > 1) + { + st = status; + for (auto ky = kb; ky != ke; ++ky, ++st) + { + if (*st == does_match && ky->size() != indx+1) + { + *st = doesnt_match; + --n_does_match; + } + } + } + } + } + // We've exited the loop because we hit eof and/or we have no more "might matches". + // Return the first matching result + for (st = status; kb != ke; ++kb, ++st) + if (*st == does_match) + break; + if (kb == ke) + is.setstate(std::ios::failbit); + return kb; +} + +} // namespace detail + +#endif // ONLY_C_LOCALE + +template +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, + const fields& fds, const std::string* abbrev, + const std::chrono::seconds* offset_sec) +{ +#if ONLY_C_LOCALE + using detail::weekday_names; + using detail::month_names; + using detail::ampm_names; +#endif + using detail::save_ostream; + using detail::get_units; + using detail::extract_weekday; + using detail::extract_month; + using std::ios; + using std::chrono::duration_cast; + using std::chrono::seconds; + using std::chrono::minutes; + using std::chrono::hours; + date::detail::save_ostream ss(os); + os.fill(' '); + os.flags(std::ios::skipws | std::ios::dec); + os.width(0); + tm tm{}; + bool insert_negative = fds.has_tod && fds.tod.to_duration() < Duration::zero(); +#if !ONLY_C_LOCALE + auto& facet = std::use_facet>(os.getloc()); +#endif + const CharT* command = nullptr; + CharT modified = CharT{}; + for (; *fmt; ++fmt) + { + switch (*fmt) + { + case 'a': + case 'A': + if (command) + { + if (modified == CharT{}) + { + tm.tm_wday = static_cast(extract_weekday(os, fds)); + if (os.fail()) + return os; +#if !ONLY_C_LOCALE + const CharT f[] = {'%', *fmt}; + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); +#else // ONLY_C_LOCALE + os << weekday_names().first[tm.tm_wday+7*(*fmt == 'a')]; +#endif // ONLY_C_LOCALE + } + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'b': + case 'B': + case 'h': + if (command) + { + if (modified == CharT{}) + { + tm.tm_mon = static_cast(extract_month(os, fds)) - 1; +#if !ONLY_C_LOCALE + const CharT f[] = {'%', *fmt}; + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); +#else // ONLY_C_LOCALE + os << month_names().first[tm.tm_mon+12*(*fmt != 'B')]; +#endif // ONLY_C_LOCALE + } + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'c': + case 'x': + if (command) + { + if (modified == CharT{'O'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.ymd.ok()) + os.setstate(std::ios::failbit); + if (*fmt == 'c' && !fds.has_tod) + os.setstate(std::ios::failbit); +#if !ONLY_C_LOCALE + tm = std::tm{}; + auto const& ymd = fds.ymd; + auto ld = local_days(ymd); + if (*fmt == 'c') + { + tm.tm_sec = static_cast(fds.tod.seconds().count()); + tm.tm_min = static_cast(fds.tod.minutes().count()); + tm.tm_hour = static_cast(fds.tod.hours().count()); + } + tm.tm_mday = static_cast(static_cast(ymd.day())); + tm.tm_mon = static_cast(extract_month(os, fds) - 1); + tm.tm_year = static_cast(ymd.year()) - 1900; + tm.tm_wday = static_cast(extract_weekday(os, fds)); + if (os.fail()) + return os; + tm.tm_yday = static_cast((ld - local_days(ymd.year()/1/1)).count()); + CharT f[3] = {'%'}; + auto fe = std::begin(f) + 1; + if (modified == CharT{'E'}) + *fe++ = modified; + *fe++ = *fmt; + facet.put(os, os, os.fill(), &tm, std::begin(f), fe); +#else // ONLY_C_LOCALE + if (*fmt == 'c') + { + auto wd = static_cast(extract_weekday(os, fds)); + os << weekday_names().first[static_cast(wd)+7] + << ' '; + os << month_names().first[extract_month(os, fds)-1+12] << ' '; + auto d = static_cast(static_cast(fds.ymd.day())); + if (d < 10) + os << ' '; + os << d << ' ' + << make_time(duration_cast(fds.tod.to_duration())) + << ' ' << fds.ymd.year(); + + } + else // *fmt == 'x' + { + auto const& ymd = fds.ymd; + save_ostream _(os); + os.fill('0'); + os.flags(std::ios::dec | std::ios::right); + os.width(2); + os << static_cast(ymd.month()) << CharT{'/'}; + os.width(2); + os << static_cast(ymd.day()) << CharT{'/'}; + os.width(2); + os << static_cast(ymd.year()) % 100; + } +#endif // ONLY_C_LOCALE + } + command = nullptr; + modified = CharT{}; + } + else + os << *fmt; + break; + case 'C': + if (command) + { + if (modified == CharT{'O'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.ymd.year().ok()) + os.setstate(std::ios::failbit); + auto y = static_cast(fds.ymd.year()); +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + save_ostream _(os); + os.fill('0'); + os.flags(std::ios::dec | std::ios::right); + if (y >= 0) + { + os.width(2); + os << y/100; + } + else + { + os << CharT{'-'}; + os.width(2); + os << -(y-99)/100; + } + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'E'}) + { + tm.tm_year = y - 1900; + CharT f[3] = {'%', 'E', 'C'}; + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + command = nullptr; + modified = CharT{}; + } + else + os << *fmt; + break; + case 'd': + case 'e': + if (command) + { + if (modified == CharT{'E'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.ymd.day().ok()) + os.setstate(std::ios::failbit); + auto d = static_cast(static_cast(fds.ymd.day())); +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + save_ostream _(os); + if (*fmt == CharT{'d'}) + os.fill('0'); + else + os.fill(' '); + os.flags(std::ios::dec | std::ios::right); + os.width(2); + os << d; + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + tm.tm_mday = d; + CharT f[3] = {'%', 'O', *fmt}; + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + command = nullptr; + modified = CharT{}; + } + else + os << *fmt; + break; + case 'D': + if (command) + { + if (modified == CharT{}) + { + if (!fds.ymd.ok()) + os.setstate(std::ios::failbit); + auto const& ymd = fds.ymd; + save_ostream _(os); + os.fill('0'); + os.flags(std::ios::dec | std::ios::right); + os.width(2); + os << static_cast(ymd.month()) << CharT{'/'}; + os.width(2); + os << static_cast(ymd.day()) << CharT{'/'}; + os.width(2); + os << static_cast(ymd.year()) % 100; + } + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'F': + if (command) + { + if (modified == CharT{}) + { + if (!fds.ymd.ok()) + os.setstate(std::ios::failbit); + auto const& ymd = fds.ymd; + save_ostream _(os); + os.imbue(std::locale::classic()); + os.fill('0'); + os.flags(std::ios::dec | std::ios::right); + os.width(4); + os << static_cast(ymd.year()) << CharT{'-'}; + os.width(2); + os << static_cast(ymd.month()) << CharT{'-'}; + os.width(2); + os << static_cast(ymd.day()); + } + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'g': + case 'G': + if (command) + { + if (modified == CharT{}) + { + if (!fds.ymd.ok()) + os.setstate(std::ios::failbit); + auto ld = local_days(fds.ymd); + auto y = year_month_day{ld + days{3}}.year(); + auto start = local_days((y-years{1})/December/Thursday[last]) + + (Monday-Thursday); + if (ld < start) + --y; + if (*fmt == CharT{'G'}) + os << y; + else + { + save_ostream _(os); + os.fill('0'); + os.flags(std::ios::dec | std::ios::right); + os.width(2); + os << std::abs(static_cast(y)) % 100; + } + } + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'H': + case 'I': + if (command) + { + if (modified == CharT{'E'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.has_tod) + os.setstate(std::ios::failbit); + if (insert_negative) + { + os << '-'; + insert_negative = false; + } + auto hms = fds.tod; +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + auto h = *fmt == CharT{'I'} ? date::make12(hms.hours()) : hms.hours(); + if (h < hours{10}) + os << CharT{'0'}; + os << h.count(); + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_hour = static_cast(hms.hours().count()); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'j': + if (command) + { + if (modified == CharT{}) + { + if (fds.ymd.ok() || fds.has_tod) + { + days doy; + if (fds.ymd.ok()) + { + auto ld = local_days(fds.ymd); + auto y = fds.ymd.year(); + doy = ld - local_days(y/January/1) + days{1}; + } + else + { + doy = duration_cast(fds.tod.to_duration()); + } + save_ostream _(os); + os.fill('0'); + os.flags(std::ios::dec | std::ios::right); + os.width(3); + os << doy.count(); + } + else + { + os.setstate(std::ios::failbit); + } + } + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'm': + if (command) + { + if (modified == CharT{'E'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.ymd.month().ok()) + os.setstate(std::ios::failbit); + auto m = static_cast(fds.ymd.month()); +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + if (m < 10) + os << CharT{'0'}; + os << m; + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_mon = static_cast(m-1); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'M': + if (command) + { + if (modified == CharT{'E'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.has_tod) + os.setstate(std::ios::failbit); + if (insert_negative) + { + os << '-'; + insert_negative = false; + } +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + if (fds.tod.minutes() < minutes{10}) + os << CharT{'0'}; + os << fds.tod.minutes().count(); + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_min = static_cast(fds.tod.minutes().count()); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'n': + if (command) + { + if (modified == CharT{}) + os << CharT{'\n'}; + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'p': + if (command) + { + if (modified == CharT{}) + { + if (!fds.has_tod) + os.setstate(std::ios::failbit); +#if !ONLY_C_LOCALE + const CharT f[] = {'%', *fmt}; + tm.tm_hour = static_cast(fds.tod.hours().count()); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); +#else + if (date::is_am(fds.tod.hours())) + os << ampm_names().first[0]; + else + os << ampm_names().first[1]; +#endif + } + else + { + os << CharT{'%'} << modified << *fmt; + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'Q': + case 'q': + if (command) + { + if (modified == CharT{}) + { + if (!fds.has_tod) + os.setstate(std::ios::failbit); + auto d = fds.tod.to_duration(); + if (*fmt == 'q') + os << get_units(typename decltype(d)::period::type{}); + else + os << d.count(); + } + else + { + os << CharT{'%'} << modified << *fmt; + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'r': + if (command) + { + if (modified == CharT{}) + { + if (!fds.has_tod) + os.setstate(std::ios::failbit); +#if !ONLY_C_LOCALE + const CharT f[] = {'%', *fmt}; + tm.tm_hour = static_cast(fds.tod.hours().count()); + tm.tm_min = static_cast(fds.tod.minutes().count()); + tm.tm_sec = static_cast(fds.tod.seconds().count()); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); +#else + hh_mm_ss tod(duration_cast(fds.tod.to_duration())); + save_ostream _(os); + os.fill('0'); + os.width(2); + os << date::make12(tod.hours()).count() << CharT{':'}; + os.width(2); + os << tod.minutes().count() << CharT{':'}; + os.width(2); + os << tod.seconds().count() << CharT{' '}; + if (date::is_am(tod.hours())) + os << ampm_names().first[0]; + else + os << ampm_names().first[1]; +#endif + } + else + { + os << CharT{'%'} << modified << *fmt; + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'R': + if (command) + { + if (modified == CharT{}) + { + if (!fds.has_tod) + os.setstate(std::ios::failbit); + if (fds.tod.hours() < hours{10}) + os << CharT{'0'}; + os << fds.tod.hours().count() << CharT{':'}; + if (fds.tod.minutes() < minutes{10}) + os << CharT{'0'}; + os << fds.tod.minutes().count(); + } + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'S': + if (command) + { + if (modified == CharT{'E'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.has_tod) + os.setstate(std::ios::failbit); + if (insert_negative) + { + os << '-'; + insert_negative = false; + } +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + os << fds.tod.s_; + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_sec = static_cast(fds.tod.s_.seconds().count()); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 't': + if (command) + { + if (modified == CharT{}) + os << CharT{'\t'}; + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'T': + if (command) + { + if (modified == CharT{}) + { + if (!fds.has_tod) + os.setstate(std::ios::failbit); + os << fds.tod; + } + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'u': + if (command) + { + if (modified == CharT{'E'}) + os << CharT{'%'} << modified << *fmt; + else + { + auto wd = extract_weekday(os, fds); +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + os << (wd != 0 ? wd : 7u); + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_wday = static_cast(wd); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'U': + if (command) + { + if (modified == CharT{'E'}) + os << CharT{'%'} << modified << *fmt; + else + { + auto const& ymd = fds.ymd; + if (!ymd.ok()) + os.setstate(std::ios::failbit); + auto ld = local_days(ymd); +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + auto st = local_days(Sunday[1]/January/ymd.year()); + if (ld < st) + os << CharT{'0'} << CharT{'0'}; + else + { + auto wn = duration_cast(ld - st).count() + 1; + if (wn < 10) + os << CharT{'0'}; + os << wn; + } + } + #if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_year = static_cast(ymd.year()) - 1900; + tm.tm_wday = static_cast(extract_weekday(os, fds)); + if (os.fail()) + return os; + tm.tm_yday = static_cast((ld - local_days(ymd.year()/1/1)).count()); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'V': + if (command) + { + if (modified == CharT{'E'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.ymd.ok()) + os.setstate(std::ios::failbit); + auto ld = local_days(fds.ymd); +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + auto y = year_month_day{ld + days{3}}.year(); + auto st = local_days((y-years{1})/12/Thursday[last]) + + (Monday-Thursday); + if (ld < st) + { + --y; + st = local_days((y - years{1})/12/Thursday[last]) + + (Monday-Thursday); + } + auto wn = duration_cast(ld - st).count() + 1; + if (wn < 10) + os << CharT{'0'}; + os << wn; + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + const CharT f[] = {'%', modified, *fmt}; + auto const& ymd = fds.ymd; + tm.tm_year = static_cast(ymd.year()) - 1900; + tm.tm_wday = static_cast(extract_weekday(os, fds)); + if (os.fail()) + return os; + tm.tm_yday = static_cast((ld - local_days(ymd.year()/1/1)).count()); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'w': + if (command) + { + auto wd = extract_weekday(os, fds); + if (os.fail()) + return os; +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#else + if (modified != CharT{'E'}) +#endif + { + os << wd; + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_wday = static_cast(wd); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + else + { + os << CharT{'%'} << modified << *fmt; + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'W': + if (command) + { + if (modified == CharT{'E'}) + os << CharT{'%'} << modified << *fmt; + else + { + auto const& ymd = fds.ymd; + if (!ymd.ok()) + os.setstate(std::ios::failbit); + auto ld = local_days(ymd); +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + auto st = local_days(Monday[1]/January/ymd.year()); + if (ld < st) + os << CharT{'0'} << CharT{'0'}; + else + { + auto wn = duration_cast(ld - st).count() + 1; + if (wn < 10) + os << CharT{'0'}; + os << wn; + } + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_year = static_cast(ymd.year()) - 1900; + tm.tm_wday = static_cast(extract_weekday(os, fds)); + if (os.fail()) + return os; + tm.tm_yday = static_cast((ld - local_days(ymd.year()/1/1)).count()); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'X': + if (command) + { + if (modified == CharT{'O'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.has_tod) + os.setstate(std::ios::failbit); +#if !ONLY_C_LOCALE + tm = std::tm{}; + tm.tm_sec = static_cast(fds.tod.seconds().count()); + tm.tm_min = static_cast(fds.tod.minutes().count()); + tm.tm_hour = static_cast(fds.tod.hours().count()); + CharT f[3] = {'%'}; + auto fe = std::begin(f) + 1; + if (modified == CharT{'E'}) + *fe++ = modified; + *fe++ = *fmt; + facet.put(os, os, os.fill(), &tm, std::begin(f), fe); +#else + os << fds.tod; +#endif + } + command = nullptr; + modified = CharT{}; + } + else + os << *fmt; + break; + case 'y': + if (command) + { + if (!fds.ymd.year().ok()) + os.setstate(std::ios::failbit); + auto y = static_cast(fds.ymd.year()); +#if !ONLY_C_LOCALE + if (modified == CharT{}) + { +#endif + y = std::abs(y) % 100; + if (y < 10) + os << CharT{'0'}; + os << y; +#if !ONLY_C_LOCALE + } + else + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_year = y - 1900; + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'Y': + if (command) + { + if (modified == CharT{'O'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.ymd.year().ok()) + os.setstate(std::ios::failbit); + auto y = fds.ymd.year(); +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + save_ostream _(os); + os.imbue(std::locale::classic()); + os << y; + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'E'}) + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_year = static_cast(y) - 1900; + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'z': + if (command) + { + if (offset_sec == nullptr) + { + // Can not format %z with unknown offset + os.setstate(ios::failbit); + return os; + } + auto m = duration_cast(*offset_sec); + auto neg = m < minutes{0}; + m = date::abs(m); + auto h = duration_cast(m); + m -= h; + if (neg) + os << CharT{'-'}; + else + os << CharT{'+'}; + if (h < hours{10}) + os << CharT{'0'}; + os << h.count(); + if (modified != CharT{}) + os << CharT{':'}; + if (m < minutes{10}) + os << CharT{'0'}; + os << m.count(); + command = nullptr; + modified = CharT{}; + } + else + os << *fmt; + break; + case 'Z': + if (command) + { + if (modified == CharT{}) + { + if (abbrev == nullptr) + { + // Can not format %Z with unknown time_zone + os.setstate(ios::failbit); + return os; + } + for (auto c : *abbrev) + os << CharT(c); + } + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'E': + case 'O': + if (command) + { + if (modified == CharT{}) + { + modified = *fmt; + } + else + { + os << CharT{'%'} << modified << *fmt; + command = nullptr; + modified = CharT{}; + } + } + else + os << *fmt; + break; + case '%': + if (command) + { + if (modified == CharT{}) + { + os << CharT{'%'}; + command = nullptr; + } + else + { + os << CharT{'%'} << modified << CharT{'%'}; + command = nullptr; + modified = CharT{}; + } + } + else + command = fmt; + break; + default: + if (command) + { + os << CharT{'%'}; + command = nullptr; + } + if (modified != CharT{}) + { + os << modified; + modified = CharT{}; + } + os << *fmt; + break; + } + } + if (command) + os << CharT{'%'}; + if (modified != CharT{}) + os << modified; + return os; +} + +template +inline +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, const year& y) +{ + using CT = std::chrono::seconds; + fields fds{y/0/0}; + return to_stream(os, fmt, fds); +} + +template +inline +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, const month& m) +{ + using CT = std::chrono::seconds; + fields fds{m/0/nanyear}; + return to_stream(os, fmt, fds); +} + +template +inline +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, const day& d) +{ + using CT = std::chrono::seconds; + fields fds{d/0/nanyear}; + return to_stream(os, fmt, fds); +} + +template +inline +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, const weekday& wd) +{ + using CT = std::chrono::seconds; + fields fds{wd}; + return to_stream(os, fmt, fds); +} + +template +inline +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, const year_month& ym) +{ + using CT = std::chrono::seconds; + fields fds{ym/0}; + return to_stream(os, fmt, fds); +} + +template +inline +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, const month_day& md) +{ + using CT = std::chrono::seconds; + fields fds{md/nanyear}; + return to_stream(os, fmt, fds); +} + +template +inline +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, + const year_month_day& ymd) +{ + using CT = std::chrono::seconds; + fields fds{ymd}; + return to_stream(os, fmt, fds); +} + +template +inline +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, + const std::chrono::duration& d) +{ + using Duration = std::chrono::duration; + using CT = typename std::common_type::type; + fields fds{hh_mm_ss{d}}; + return to_stream(os, fmt, fds); +} + +template +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, + const local_time& tp, const std::string* abbrev = nullptr, + const std::chrono::seconds* offset_sec = nullptr) +{ + using CT = typename std::common_type::type; + auto ld = std::chrono::time_point_cast(tp); + fields fds; + if (ld <= tp) + fds = fields{year_month_day{ld}, hh_mm_ss{tp-local_seconds{ld}}}; + else + fds = fields{year_month_day{ld - days{1}}, + hh_mm_ss{days{1} - (local_seconds{ld} - tp)}}; + return to_stream(os, fmt, fds, abbrev, offset_sec); +} + +template +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, + const sys_time& tp) +{ + using std::chrono::seconds; + using CT = typename std::common_type::type; + const std::string abbrev("UTC"); + CONSTDATA seconds offset{0}; + auto sd = std::chrono::time_point_cast(tp); + fields fds; + if (sd <= tp) + fds = fields{year_month_day{sd}, hh_mm_ss{tp-sys_seconds{sd}}}; + else + fds = fields{year_month_day{sd - days{1}}, + hh_mm_ss{days{1} - (sys_seconds{sd} - tp)}}; + return to_stream(os, fmt, fds, &abbrev, &offset); +} + +// format + +template +auto +format(const std::locale& loc, const CharT* fmt, const Streamable& tp) + -> decltype(to_stream(std::declval&>(), fmt, tp), + std::basic_string{}) +{ + std::basic_ostringstream os; + os.exceptions(std::ios::failbit | std::ios::badbit); + os.imbue(loc); + to_stream(os, fmt, tp); + return os.str(); +} + +template +auto +format(const CharT* fmt, const Streamable& tp) + -> decltype(to_stream(std::declval&>(), fmt, tp), + std::basic_string{}) +{ + std::basic_ostringstream os; + os.exceptions(std::ios::failbit | std::ios::badbit); + to_stream(os, fmt, tp); + return os.str(); +} + +template +auto +format(const std::locale& loc, const std::basic_string& fmt, + const Streamable& tp) + -> decltype(to_stream(std::declval&>(), fmt.c_str(), tp), + std::basic_string{}) +{ + std::basic_ostringstream os; + os.exceptions(std::ios::failbit | std::ios::badbit); + os.imbue(loc); + to_stream(os, fmt.c_str(), tp); + return os.str(); +} + +template +auto +format(const std::basic_string& fmt, const Streamable& tp) + -> decltype(to_stream(std::declval&>(), fmt.c_str(), tp), + std::basic_string{}) +{ + std::basic_ostringstream os; + os.exceptions(std::ios::failbit | std::ios::badbit); + to_stream(os, fmt.c_str(), tp); + return os.str(); +} + +// parse + +namespace detail +{ + +template +bool +read_char(std::basic_istream& is, CharT fmt, std::ios::iostate& err) +{ + auto ic = is.get(); + if (Traits::eq_int_type(ic, Traits::eof()) || + !Traits::eq(Traits::to_char_type(ic), fmt)) + { + err |= std::ios::failbit; + is.setstate(std::ios::failbit); + return false; + } + return true; +} + +template +unsigned +read_unsigned(std::basic_istream& is, unsigned m = 1, unsigned M = 10) +{ + unsigned x = 0; + unsigned count = 0; + while (true) + { + auto ic = is.peek(); + if (Traits::eq_int_type(ic, Traits::eof())) + break; + auto c = static_cast(Traits::to_char_type(ic)); + if (!('0' <= c && c <= '9')) + break; + (void)is.get(); + ++count; + x = 10*x + static_cast(c - '0'); + if (count == M) + break; + } + if (count < m) + is.setstate(std::ios::failbit); + return x; +} + +template +int +read_signed(std::basic_istream& is, unsigned m = 1, unsigned M = 10) +{ + auto ic = is.peek(); + if (!Traits::eq_int_type(ic, Traits::eof())) + { + auto c = static_cast(Traits::to_char_type(ic)); + if (('0' <= c && c <= '9') || c == '-' || c == '+') + { + if (c == '-' || c == '+') + (void)is.get(); + auto x = static_cast(read_unsigned(is, std::max(m, 1u), M)); + if (!is.fail()) + { + if (c == '-') + x = -x; + return x; + } + } + } + if (m > 0) + is.setstate(std::ios::failbit); + return 0; +} + +template +long double +read_long_double(std::basic_istream& is, unsigned m = 1, unsigned M = 10) +{ + unsigned count = 0; + unsigned fcount = 0; + unsigned long long i = 0; + unsigned long long f = 0; + bool parsing_fraction = false; +#if ONLY_C_LOCALE + typename Traits::int_type decimal_point = '.'; +#else + auto decimal_point = Traits::to_int_type( + std::use_facet>(is.getloc()).decimal_point()); +#endif + while (true) + { + auto ic = is.peek(); + if (Traits::eq_int_type(ic, Traits::eof())) + break; + if (Traits::eq_int_type(ic, decimal_point)) + { + decimal_point = Traits::eof(); + parsing_fraction = true; + } + else + { + auto c = static_cast(Traits::to_char_type(ic)); + if (!('0' <= c && c <= '9')) + break; + if (!parsing_fraction) + { + i = 10*i + static_cast(c - '0'); + } + else + { + f = 10*f + static_cast(c - '0'); + ++fcount; + } + } + (void)is.get(); + if (++count == M) + break; + } + if (count < m) + { + is.setstate(std::ios::failbit); + return 0; + } + return static_cast(i) + static_cast(f)/std::pow(10.L, fcount); +} + +struct rs +{ + int& i; + unsigned m; + unsigned M; +}; + +struct ru +{ + int& i; + unsigned m; + unsigned M; +}; + +struct rld +{ + long double& i; + unsigned m; + unsigned M; +}; + +template +void +read(std::basic_istream&) +{ +} + +template +void +read(std::basic_istream& is, CharT a0, Args&& ...args); + +template +void +read(std::basic_istream& is, rs a0, Args&& ...args); + +template +void +read(std::basic_istream& is, ru a0, Args&& ...args); + +template +void +read(std::basic_istream& is, int a0, Args&& ...args); + +template +void +read(std::basic_istream& is, rld a0, Args&& ...args); + +template +void +read(std::basic_istream& is, CharT a0, Args&& ...args) +{ + // No-op if a0 == CharT{} + if (a0 != CharT{}) + { + auto ic = is.peek(); + if (Traits::eq_int_type(ic, Traits::eof())) + { + is.setstate(std::ios::failbit | std::ios::eofbit); + return; + } + if (!Traits::eq(Traits::to_char_type(ic), a0)) + { + is.setstate(std::ios::failbit); + return; + } + (void)is.get(); + } + read(is, std::forward(args)...); +} + +template +void +read(std::basic_istream& is, rs a0, Args&& ...args) +{ + auto x = read_signed(is, a0.m, a0.M); + if (is.fail()) + return; + a0.i = x; + read(is, std::forward(args)...); +} + +template +void +read(std::basic_istream& is, ru a0, Args&& ...args) +{ + auto x = read_unsigned(is, a0.m, a0.M); + if (is.fail()) + return; + a0.i = static_cast(x); + read(is, std::forward(args)...); +} + +template +void +read(std::basic_istream& is, int a0, Args&& ...args) +{ + if (a0 != -1) + { + auto u = static_cast(a0); + CharT buf[std::numeric_limits::digits10+2u] = {}; + auto e = buf; + do + { + *e++ = static_cast(CharT(u % 10) + CharT{'0'}); + u /= 10; + } while (u > 0); + std::reverse(buf, e); + for (auto p = buf; p != e && is.rdstate() == std::ios::goodbit; ++p) + read(is, *p); + } + if (is.rdstate() == std::ios::goodbit) + read(is, std::forward(args)...); +} + +template +void +read(std::basic_istream& is, rld a0, Args&& ...args) +{ + auto x = read_long_double(is, a0.m, a0.M); + if (is.fail()) + return; + a0.i = x; + read(is, std::forward(args)...); +} + +template +inline +void +checked_set(T& value, T from, T not_a_value, std::basic_ios& is) +{ + if (!is.fail()) + { + if (value == not_a_value) + value = std::move(from); + else if (value != from) + is.setstate(std::ios::failbit); + } +} + +} // namespace detail; + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, + fields& fds, std::basic_string* abbrev, + std::chrono::minutes* offset) +{ + using std::numeric_limits; + using std::ios; + using std::chrono::duration; + using std::chrono::duration_cast; + using std::chrono::seconds; + using std::chrono::minutes; + using std::chrono::hours; + using detail::round_i; + typename std::basic_istream::sentry ok{is, true}; + if (ok) + { + date::detail::save_istream ss(is); + is.fill(' '); + is.flags(std::ios::skipws | std::ios::dec); + is.width(0); +#if !ONLY_C_LOCALE + auto& f = std::use_facet>(is.getloc()); + std::tm tm{}; +#endif + const CharT* command = nullptr; + auto modified = CharT{}; + auto width = -1; + + CONSTDATA int not_a_year = numeric_limits::min(); + CONSTDATA int not_a_2digit_year = 100; + CONSTDATA int not_a_century = not_a_year / 100; + CONSTDATA int not_a_month = 0; + CONSTDATA int not_a_day = 0; + CONSTDATA int not_a_hour = numeric_limits::min(); + CONSTDATA int not_a_hour_12_value = 0; + CONSTDATA int not_a_minute = not_a_hour; + CONSTDATA Duration not_a_second = Duration::min(); + CONSTDATA int not_a_doy = -1; + CONSTDATA int not_a_weekday = 8; + CONSTDATA int not_a_week_num = 100; + CONSTDATA int not_a_ampm = -1; + CONSTDATA minutes not_a_offset = minutes::min(); + + int Y = not_a_year; // c, F, Y * + int y = not_a_2digit_year; // D, x, y * + int g = not_a_2digit_year; // g * + int G = not_a_year; // G * + int C = not_a_century; // C * + int m = not_a_month; // b, B, h, m, c, D, F, x * + int d = not_a_day; // c, d, D, e, F, x * + int j = not_a_doy; // j * + int wd = not_a_weekday; // a, A, u, w * + int H = not_a_hour; // c, H, R, T, X * + int I = not_a_hour_12_value; // I, r * + int p = not_a_ampm; // p, r * + int M = not_a_minute; // c, M, r, R, T, X * + Duration s = not_a_second; // c, r, S, T, X * + int U = not_a_week_num; // U * + int V = not_a_week_num; // V * + int W = not_a_week_num; // W * + std::basic_string temp_abbrev; // Z * + minutes temp_offset = not_a_offset; // z * + + using detail::read; + using detail::rs; + using detail::ru; + using detail::rld; + using detail::checked_set; + for (; *fmt != CharT{} && !is.fail(); ++fmt) + { + switch (*fmt) + { + case 'a': + case 'A': + case 'u': + case 'w': // wd: a, A, u, w + if (command) + { + int trial_wd = not_a_weekday; + if (*fmt == 'a' || *fmt == 'A') + { + if (modified == CharT{}) + { +#if !ONLY_C_LOCALE + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + is.setstate(err); + if (!is.fail()) + trial_wd = tm.tm_wday; +#else + auto nm = detail::weekday_names(); + auto i = detail::scan_keyword(is, nm.first, nm.second) - nm.first; + if (!is.fail()) + trial_wd = i % 7; +#endif + } + else + read(is, CharT{'%'}, width, modified, *fmt); + } + else // *fmt == 'u' || *fmt == 'w' + { +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#else + if (modified != CharT{'E'}) +#endif + { + read(is, ru{trial_wd, 1, width == -1 ? + 1u : static_cast(width)}); + if (!is.fail()) + { + if (*fmt == 'u') + { + if (!(1 <= trial_wd && trial_wd <= 7)) + { + trial_wd = not_a_weekday; + is.setstate(ios::failbit); + } + else if (trial_wd == 7) + trial_wd = 0; + } + else // *fmt == 'w' + { + if (!(0 <= trial_wd && trial_wd <= 6)) + { + trial_wd = not_a_weekday; + is.setstate(ios::failbit); + } + } + } + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + is.setstate(err); + if (!is.fail()) + trial_wd = tm.tm_wday; + } +#endif + else + read(is, CharT{'%'}, width, modified, *fmt); + } + if (trial_wd != not_a_weekday) + checked_set(wd, trial_wd, not_a_weekday, is); + } + else // !command + read(is, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + break; + case 'b': + case 'B': + case 'h': + if (command) + { + if (modified == CharT{}) + { + int ttm = not_a_month; +#if !ONLY_C_LOCALE + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + ttm = tm.tm_mon + 1; + is.setstate(err); +#else + auto nm = detail::month_names(); + auto i = detail::scan_keyword(is, nm.first, nm.second) - nm.first; + if (!is.fail()) + ttm = i % 12 + 1; +#endif + checked_set(m, ttm, not_a_month, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'c': + if (command) + { + if (modified != CharT{'O'}) + { +#if !ONLY_C_LOCALE + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + { + checked_set(Y, tm.tm_year + 1900, not_a_year, is); + checked_set(m, tm.tm_mon + 1, not_a_month, is); + checked_set(d, tm.tm_mday, not_a_day, is); + checked_set(H, tm.tm_hour, not_a_hour, is); + checked_set(M, tm.tm_min, not_a_minute, is); + checked_set(s, duration_cast(seconds{tm.tm_sec}), + not_a_second, is); + } + is.setstate(err); +#else + // "%a %b %e %T %Y" + auto nm = detail::weekday_names(); + auto i = detail::scan_keyword(is, nm.first, nm.second) - nm.first; + checked_set(wd, static_cast(i % 7), not_a_weekday, is); + ws(is); + nm = detail::month_names(); + i = detail::scan_keyword(is, nm.first, nm.second) - nm.first; + checked_set(m, static_cast(i % 12 + 1), not_a_month, is); + ws(is); + int td = not_a_day; + read(is, rs{td, 1, 2}); + checked_set(d, td, not_a_day, is); + ws(is); + using dfs = detail::decimal_format_seconds; + CONSTDATA auto w = Duration::period::den == 1 ? 2 : 3 + dfs::width; + int tH; + int tM; + long double S{}; + read(is, ru{tH, 1, 2}, CharT{':'}, ru{tM, 1, 2}, + CharT{':'}, rld{S, 1, w}); + checked_set(H, tH, not_a_hour, is); + checked_set(M, tM, not_a_minute, is); + checked_set(s, round_i(duration{S}), + not_a_second, is); + ws(is); + int tY = not_a_year; + read(is, rs{tY, 1, 4u}); + checked_set(Y, tY, not_a_year, is); +#endif + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'x': + if (command) + { + if (modified != CharT{'O'}) + { +#if !ONLY_C_LOCALE + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + { + checked_set(Y, tm.tm_year + 1900, not_a_year, is); + checked_set(m, tm.tm_mon + 1, not_a_month, is); + checked_set(d, tm.tm_mday, not_a_day, is); + } + is.setstate(err); +#else + // "%m/%d/%y" + int ty = not_a_2digit_year; + int tm = not_a_month; + int td = not_a_day; + read(is, ru{tm, 1, 2}, CharT{'/'}, ru{td, 1, 2}, CharT{'/'}, + rs{ty, 1, 2}); + checked_set(y, ty, not_a_2digit_year, is); + checked_set(m, tm, not_a_month, is); + checked_set(d, td, not_a_day, is); +#endif + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'X': + if (command) + { + if (modified != CharT{'O'}) + { +#if !ONLY_C_LOCALE + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + { + checked_set(H, tm.tm_hour, not_a_hour, is); + checked_set(M, tm.tm_min, not_a_minute, is); + checked_set(s, duration_cast(seconds{tm.tm_sec}), + not_a_second, is); + } + is.setstate(err); +#else + // "%T" + using dfs = detail::decimal_format_seconds; + CONSTDATA auto w = Duration::period::den == 1 ? 2 : 3 + dfs::width; + int tH = not_a_hour; + int tM = not_a_minute; + long double S{}; + read(is, ru{tH, 1, 2}, CharT{':'}, ru{tM, 1, 2}, + CharT{':'}, rld{S, 1, w}); + checked_set(H, tH, not_a_hour, is); + checked_set(M, tM, not_a_minute, is); + checked_set(s, round_i(duration{S}), + not_a_second, is); +#endif + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'C': + if (command) + { + int tC = not_a_century; +#if !ONLY_C_LOCALE + if (modified == CharT{}) + { +#endif + read(is, rs{tC, 1, width == -1 ? 2u : static_cast(width)}); +#if !ONLY_C_LOCALE + } + else + { + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + { + auto tY = tm.tm_year + 1900; + tC = (tY >= 0 ? tY : tY-99) / 100; + } + is.setstate(err); + } +#endif + checked_set(C, tC, not_a_century, is); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'D': + if (command) + { + if (modified == CharT{}) + { + int tn = not_a_month; + int td = not_a_day; + int ty = not_a_2digit_year; + read(is, ru{tn, 1, 2}, CharT{'\0'}, CharT{'/'}, CharT{'\0'}, + ru{td, 1, 2}, CharT{'\0'}, CharT{'/'}, CharT{'\0'}, + rs{ty, 1, 2}); + checked_set(y, ty, not_a_2digit_year, is); + checked_set(m, tn, not_a_month, is); + checked_set(d, td, not_a_day, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'F': + if (command) + { + if (modified == CharT{}) + { + int tY = not_a_year; + int tn = not_a_month; + int td = not_a_day; + read(is, rs{tY, 1, width == -1 ? 4u : static_cast(width)}, + CharT{'-'}, ru{tn, 1, 2}, CharT{'-'}, ru{td, 1, 2}); + checked_set(Y, tY, not_a_year, is); + checked_set(m, tn, not_a_month, is); + checked_set(d, td, not_a_day, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'd': + case 'e': + if (command) + { +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#else + if (modified != CharT{'E'}) +#endif + { + int td = not_a_day; + read(is, rs{td, 1, width == -1 ? 2u : static_cast(width)}); + checked_set(d, td, not_a_day, is); + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + command = nullptr; + width = -1; + modified = CharT{}; + if ((err & ios::failbit) == 0) + checked_set(d, tm.tm_mday, not_a_day, is); + is.setstate(err); + } +#endif + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'H': + if (command) + { +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#else + if (modified != CharT{'E'}) +#endif + { + int tH = not_a_hour; + read(is, ru{tH, 1, width == -1 ? 2u : static_cast(width)}); + checked_set(H, tH, not_a_hour, is); + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + checked_set(H, tm.tm_hour, not_a_hour, is); + is.setstate(err); + } +#endif + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'I': + if (command) + { + if (modified == CharT{}) + { + int tI = not_a_hour_12_value; + // reads in an hour into I, but most be in [1, 12] + read(is, rs{tI, 1, width == -1 ? 2u : static_cast(width)}); + if (!(1 <= tI && tI <= 12)) + is.setstate(ios::failbit); + checked_set(I, tI, not_a_hour_12_value, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'j': + if (command) + { + if (modified == CharT{}) + { + int tj = not_a_doy; + read(is, ru{tj, 1, width == -1 ? 3u : static_cast(width)}); + checked_set(j, tj, not_a_doy, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'M': + if (command) + { +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#else + if (modified != CharT{'E'}) +#endif + { + int tM = not_a_minute; + read(is, ru{tM, 1, width == -1 ? 2u : static_cast(width)}); + checked_set(M, tM, not_a_minute, is); + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + checked_set(M, tm.tm_min, not_a_minute, is); + is.setstate(err); + } +#endif + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'm': + if (command) + { +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#else + if (modified != CharT{'E'}) +#endif + { + int tn = not_a_month; + read(is, rs{tn, 1, width == -1 ? 2u : static_cast(width)}); + checked_set(m, tn, not_a_month, is); + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + checked_set(m, tm.tm_mon + 1, not_a_month, is); + is.setstate(err); + } +#endif + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'n': + case 't': + if (command) + { + if (modified == CharT{}) + { + // %n matches a single white space character + // %t matches 0 or 1 white space characters + auto ic = is.peek(); + if (Traits::eq_int_type(ic, Traits::eof())) + { + ios::iostate err = ios::eofbit; + if (*fmt == 'n') + err |= ios::failbit; + is.setstate(err); + break; + } + if (isspace(ic)) + { + (void)is.get(); + } + else if (*fmt == 'n') + is.setstate(ios::failbit); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'p': + if (command) + { + if (modified == CharT{}) + { + int tp = not_a_ampm; +#if !ONLY_C_LOCALE + tm = std::tm{}; + tm.tm_hour = 1; + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + is.setstate(err); + if (tm.tm_hour == 1) + tp = 0; + else if (tm.tm_hour == 13) + tp = 1; + else + is.setstate(err); +#else + auto nm = detail::ampm_names(); + auto i = detail::scan_keyword(is, nm.first, nm.second) - nm.first; + tp = static_cast(i); +#endif + checked_set(p, tp, not_a_ampm, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + + break; + case 'r': + if (command) + { + if (modified == CharT{}) + { +#if !ONLY_C_LOCALE + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + { + checked_set(H, tm.tm_hour, not_a_hour, is); + checked_set(M, tm.tm_min, not_a_hour, is); + checked_set(s, duration_cast(seconds{tm.tm_sec}), + not_a_second, is); + } + is.setstate(err); +#else + // "%I:%M:%S %p" + using dfs = detail::decimal_format_seconds; + CONSTDATA auto w = Duration::period::den == 1 ? 2 : 3 + dfs::width; + long double S{}; + int tI = not_a_hour_12_value; + int tM = not_a_minute; + read(is, ru{tI, 1, 2}, CharT{':'}, ru{tM, 1, 2}, + CharT{':'}, rld{S, 1, w}); + checked_set(I, tI, not_a_hour_12_value, is); + checked_set(M, tM, not_a_minute, is); + checked_set(s, round_i(duration{S}), + not_a_second, is); + ws(is); + auto nm = detail::ampm_names(); + auto i = detail::scan_keyword(is, nm.first, nm.second) - nm.first; + checked_set(p, static_cast(i), not_a_ampm, is); +#endif + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'R': + if (command) + { + if (modified == CharT{}) + { + int tH = not_a_hour; + int tM = not_a_minute; + read(is, ru{tH, 1, 2}, CharT{'\0'}, CharT{':'}, CharT{'\0'}, + ru{tM, 1, 2}, CharT{'\0'}); + checked_set(H, tH, not_a_hour, is); + checked_set(M, tM, not_a_minute, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'S': + if (command) + { + #if !ONLY_C_LOCALE + if (modified == CharT{}) +#else + if (modified != CharT{'E'}) +#endif + { + using dfs = detail::decimal_format_seconds; + CONSTDATA auto w = Duration::period::den == 1 ? 2 : 3 + dfs::width; + long double S{}; + read(is, rld{S, 1, width == -1 ? w : static_cast(width)}); + checked_set(s, round_i(duration{S}), + not_a_second, is); + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + checked_set(s, duration_cast(seconds{tm.tm_sec}), + not_a_second, is); + is.setstate(err); + } +#endif + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'T': + if (command) + { + if (modified == CharT{}) + { + using dfs = detail::decimal_format_seconds; + CONSTDATA auto w = Duration::period::den == 1 ? 2 : 3 + dfs::width; + int tH = not_a_hour; + int tM = not_a_minute; + long double S{}; + read(is, ru{tH, 1, 2}, CharT{':'}, ru{tM, 1, 2}, + CharT{':'}, rld{S, 1, w}); + checked_set(H, tH, not_a_hour, is); + checked_set(M, tM, not_a_minute, is); + checked_set(s, round_i(duration{S}), + not_a_second, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'Y': + if (command) + { +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#else + if (modified != CharT{'O'}) +#endif + { + int tY = not_a_year; + read(is, rs{tY, 1, width == -1 ? 4u : static_cast(width)}); + checked_set(Y, tY, not_a_year, is); + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'E'}) + { + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + checked_set(Y, tm.tm_year + 1900, not_a_year, is); + is.setstate(err); + } +#endif + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'y': + if (command) + { +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + int ty = not_a_2digit_year; + read(is, ru{ty, 1, width == -1 ? 2u : static_cast(width)}); + checked_set(y, ty, not_a_2digit_year, is); + } +#if !ONLY_C_LOCALE + else + { + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + checked_set(Y, tm.tm_year + 1900, not_a_year, is); + is.setstate(err); + } +#endif + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'g': + if (command) + { + if (modified == CharT{}) + { + int tg = not_a_2digit_year; + read(is, ru{tg, 1, width == -1 ? 2u : static_cast(width)}); + checked_set(g, tg, not_a_2digit_year, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'G': + if (command) + { + if (modified == CharT{}) + { + int tG = not_a_year; + read(is, rs{tG, 1, width == -1 ? 4u : static_cast(width)}); + checked_set(G, tG, not_a_year, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'U': + if (command) + { + if (modified == CharT{}) + { + int tU = not_a_week_num; + read(is, ru{tU, 1, width == -1 ? 2u : static_cast(width)}); + checked_set(U, tU, not_a_week_num, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'V': + if (command) + { + if (modified == CharT{}) + { + int tV = not_a_week_num; + read(is, ru{tV, 1, width == -1 ? 2u : static_cast(width)}); + checked_set(V, tV, not_a_week_num, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'W': + if (command) + { + if (modified == CharT{}) + { + int tW = not_a_week_num; + read(is, ru{tW, 1, width == -1 ? 2u : static_cast(width)}); + checked_set(W, tW, not_a_week_num, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'E': + case 'O': + if (command) + { + if (modified == CharT{}) + { + modified = *fmt; + } + else + { + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + } + else + read(is, *fmt); + break; + case '%': + if (command) + { + if (modified == CharT{}) + read(is, *fmt); + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + command = fmt; + break; + case 'z': + if (command) + { + int tH, tM; + minutes toff = not_a_offset; + bool neg = false; + auto ic = is.peek(); + if (!Traits::eq_int_type(ic, Traits::eof())) + { + auto c = static_cast(Traits::to_char_type(ic)); + if (c == '-') + neg = true; + } + if (modified == CharT{}) + { + read(is, rs{tH, 2, 2}); + if (!is.fail()) + toff = hours{std::abs(tH)}; + if (is.good()) + { + ic = is.peek(); + if (!Traits::eq_int_type(ic, Traits::eof())) + { + auto c = static_cast(Traits::to_char_type(ic)); + if ('0' <= c && c <= '9') + { + read(is, ru{tM, 2, 2}); + if (!is.fail()) + toff += minutes{tM}; + } + } + } + } + else + { + read(is, rs{tH, 1, 2}); + if (!is.fail()) + toff = hours{std::abs(tH)}; + if (is.good()) + { + ic = is.peek(); + if (!Traits::eq_int_type(ic, Traits::eof())) + { + auto c = static_cast(Traits::to_char_type(ic)); + if (c == ':') + { + (void)is.get(); + read(is, ru{tM, 2, 2}); + if (!is.fail()) + toff += minutes{tM}; + } + } + } + } + if (neg) + toff = -toff; + checked_set(temp_offset, toff, not_a_offset, is); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'Z': + if (command) + { + if (modified == CharT{}) + { + std::basic_string buf; + while (is.rdstate() == std::ios::goodbit) + { + auto i = is.rdbuf()->sgetc(); + if (Traits::eq_int_type(i, Traits::eof())) + { + is.setstate(ios::eofbit); + break; + } + auto wc = Traits::to_char_type(i); + auto c = static_cast(wc); + // is c a valid time zone name or abbreviation character? + if (!(CharT{1} < wc && wc < CharT{127}) || !(isalnum(c) || + c == '_' || c == '/' || c == '-' || c == '+')) + break; + buf.push_back(c); + is.rdbuf()->sbumpc(); + } + if (buf.empty()) + is.setstate(ios::failbit); + checked_set(temp_abbrev, buf, {}, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + default: + if (command) + { + if (width == -1 && modified == CharT{} && '0' <= *fmt && *fmt <= '9') + { + width = static_cast(*fmt) - '0'; + while ('0' <= fmt[1] && fmt[1] <= '9') + width = 10*width + static_cast(*++fmt) - '0'; + } + else + { + if (modified == CharT{}) + read(is, CharT{'%'}, width, *fmt); + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + } + else // !command + { + if (isspace(static_cast(*fmt))) + { + // space matches 0 or more white space characters + if (is.good()) + ws(is); + } + else + read(is, *fmt); + } + break; + } + } + // is.fail() || *fmt == CharT{} + if (is.rdstate() == ios::goodbit && command) + { + if (modified == CharT{}) + read(is, CharT{'%'}, width); + else + read(is, CharT{'%'}, width, modified); + } + if (!is.fail()) + { + if (y != not_a_2digit_year) + { + // Convert y and an optional C to Y + if (!(0 <= y && y <= 99)) + goto broken; + if (C == not_a_century) + { + if (Y == not_a_year) + { + if (y >= 69) + C = 19; + else + C = 20; + } + else + { + C = (Y >= 0 ? Y : Y-100) / 100; + } + } + int tY; + if (C >= 0) + tY = 100*C + y; + else + tY = 100*(C+1) - (y == 0 ? 100 : y); + if (Y != not_a_year && Y != tY) + goto broken; + Y = tY; + } + if (g != not_a_2digit_year) + { + // Convert g and an optional C to G + if (!(0 <= g && g <= 99)) + goto broken; + if (C == not_a_century) + { + if (G == not_a_year) + { + if (g >= 69) + C = 19; + else + C = 20; + } + else + { + C = (G >= 0 ? G : G-100) / 100; + } + } + int tG; + if (C >= 0) + tG = 100*C + g; + else + tG = 100*(C+1) - (g == 0 ? 100 : g); + if (G != not_a_year && G != tG) + goto broken; + G = tG; + } + if (Y < static_cast(year::min()) || Y > static_cast(year::max())) + Y = not_a_year; + bool computed = false; + if (G != not_a_year && V != not_a_week_num && wd != not_a_weekday) + { + year_month_day ymd_trial = sys_days(year{G-1}/December/Thursday[last]) + + (Monday-Thursday) + weeks{V-1} + + (weekday{static_cast(wd)}-Monday); + if (Y == not_a_year) + Y = static_cast(ymd_trial.year()); + else if (year{Y} != ymd_trial.year()) + goto broken; + if (m == not_a_month) + m = static_cast(static_cast(ymd_trial.month())); + else if (month(static_cast(m)) != ymd_trial.month()) + goto broken; + if (d == not_a_day) + d = static_cast(static_cast(ymd_trial.day())); + else if (day(static_cast(d)) != ymd_trial.day()) + goto broken; + computed = true; + } + if (Y != not_a_year && U != not_a_week_num && wd != not_a_weekday) + { + year_month_day ymd_trial = sys_days(year{Y}/January/Sunday[1]) + + weeks{U-1} + + (weekday{static_cast(wd)} - Sunday); + if (Y == not_a_year) + Y = static_cast(ymd_trial.year()); + else if (year{Y} != ymd_trial.year()) + goto broken; + if (m == not_a_month) + m = static_cast(static_cast(ymd_trial.month())); + else if (month(static_cast(m)) != ymd_trial.month()) + goto broken; + if (d == not_a_day) + d = static_cast(static_cast(ymd_trial.day())); + else if (day(static_cast(d)) != ymd_trial.day()) + goto broken; + computed = true; + } + if (Y != not_a_year && W != not_a_week_num && wd != not_a_weekday) + { + year_month_day ymd_trial = sys_days(year{Y}/January/Monday[1]) + + weeks{W-1} + + (weekday{static_cast(wd)} - Monday); + if (Y == not_a_year) + Y = static_cast(ymd_trial.year()); + else if (year{Y} != ymd_trial.year()) + goto broken; + if (m == not_a_month) + m = static_cast(static_cast(ymd_trial.month())); + else if (month(static_cast(m)) != ymd_trial.month()) + goto broken; + if (d == not_a_day) + d = static_cast(static_cast(ymd_trial.day())); + else if (day(static_cast(d)) != ymd_trial.day()) + goto broken; + computed = true; + } + if (j != not_a_doy && Y != not_a_year) + { + auto ymd_trial = year_month_day{local_days(year{Y}/1/1) + days{j-1}}; + if (m == not_a_month) + m = static_cast(static_cast(ymd_trial.month())); + else if (month(static_cast(m)) != ymd_trial.month()) + goto broken; + if (d == not_a_day) + d = static_cast(static_cast(ymd_trial.day())); + else if (day(static_cast(d)) != ymd_trial.day()) + goto broken; + j = not_a_doy; + } + auto ymd = year{Y}/m/d; + if (ymd.ok()) + { + if (wd == not_a_weekday) + wd = static_cast((weekday(sys_days(ymd)) - Sunday).count()); + else if (wd != static_cast((weekday(sys_days(ymd)) - Sunday).count())) + goto broken; + if (!computed) + { + if (G != not_a_year || V != not_a_week_num) + { + sys_days sd = ymd; + auto G_trial = year_month_day{sd + days{3}}.year(); + auto start = sys_days((G_trial - years{1})/December/Thursday[last]) + + (Monday - Thursday); + if (sd < start) + { + --G_trial; + if (V != not_a_week_num) + start = sys_days((G_trial - years{1})/December/Thursday[last]) + + (Monday - Thursday); + } + if (G != not_a_year && G != static_cast(G_trial)) + goto broken; + if (V != not_a_week_num) + { + auto V_trial = duration_cast(sd - start).count() + 1; + if (V != V_trial) + goto broken; + } + } + if (U != not_a_week_num) + { + auto start = sys_days(Sunday[1]/January/ymd.year()); + auto U_trial = floor(sys_days(ymd) - start).count() + 1; + if (U != U_trial) + goto broken; + } + if (W != not_a_week_num) + { + auto start = sys_days(Monday[1]/January/ymd.year()); + auto W_trial = floor(sys_days(ymd) - start).count() + 1; + if (W != W_trial) + goto broken; + } + } + } + fds.ymd = ymd; + if (I != not_a_hour_12_value) + { + if (!(1 <= I && I <= 12)) + goto broken; + if (p != not_a_ampm) + { + // p is in [0, 1] == [AM, PM] + // Store trial H in I + if (I == 12) + --p; + I += p*12; + // Either set H from I or make sure H and I are consistent + if (H == not_a_hour) + H = I; + else if (I != H) + goto broken; + } + else // p == not_a_ampm + { + // if H, make sure H and I could be consistent + if (H != not_a_hour) + { + if (I == 12) + { + if (H != 0 && H != 12) + goto broken; + } + else if (!(I == H || I == H+12)) + { + goto broken; + } + } + else // I is ambiguous, AM or PM? + goto broken; + } + } + if (H != not_a_hour) + { + fds.has_tod = true; + fds.tod = hh_mm_ss{hours{H}}; + } + if (M != not_a_minute) + { + fds.has_tod = true; + fds.tod.m_ = minutes{M}; + } + if (s != not_a_second) + { + fds.has_tod = true; + fds.tod.s_ = detail::decimal_format_seconds{s}; + } + if (j != not_a_doy) + { + fds.has_tod = true; + fds.tod.h_ += hours{days{j}}; + } + if (wd != not_a_weekday) + fds.wd = weekday{static_cast(wd)}; + if (abbrev != nullptr) + *abbrev = std::move(temp_abbrev); + if (offset != nullptr && temp_offset != not_a_offset) + *offset = temp_offset; + } + return is; + } +broken: + is.setstate(ios::failbit); + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, year& y, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using CT = std::chrono::seconds; + fields fds{}; + date::from_stream(is, fmt, fds, abbrev, offset); + if (!fds.ymd.year().ok()) + is.setstate(std::ios::failbit); + if (!is.fail()) + y = fds.ymd.year(); + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, month& m, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using CT = std::chrono::seconds; + fields fds{}; + date::from_stream(is, fmt, fds, abbrev, offset); + if (!fds.ymd.month().ok()) + is.setstate(std::ios::failbit); + if (!is.fail()) + m = fds.ymd.month(); + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, day& d, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using CT = std::chrono::seconds; + fields fds{}; + date::from_stream(is, fmt, fds, abbrev, offset); + if (!fds.ymd.day().ok()) + is.setstate(std::ios::failbit); + if (!is.fail()) + d = fds.ymd.day(); + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, weekday& wd, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using CT = std::chrono::seconds; + fields fds{}; + date::from_stream(is, fmt, fds, abbrev, offset); + if (!fds.wd.ok()) + is.setstate(std::ios::failbit); + if (!is.fail()) + wd = fds.wd; + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, year_month& ym, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using CT = std::chrono::seconds; + fields fds{}; + date::from_stream(is, fmt, fds, abbrev, offset); + if (!fds.ymd.month().ok()) + is.setstate(std::ios::failbit); + if (!is.fail()) + ym = fds.ymd.year()/fds.ymd.month(); + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, month_day& md, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using CT = std::chrono::seconds; + fields fds{}; + date::from_stream(is, fmt, fds, abbrev, offset); + if (!fds.ymd.month().ok() || !fds.ymd.day().ok()) + is.setstate(std::ios::failbit); + if (!is.fail()) + md = fds.ymd.month()/fds.ymd.day(); + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, + year_month_day& ymd, std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using CT = std::chrono::seconds; + fields fds{}; + date::from_stream(is, fmt, fds, abbrev, offset); + if (!fds.ymd.ok()) + is.setstate(std::ios::failbit); + if (!is.fail()) + ymd = fds.ymd; + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, + sys_time& tp, std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using CT = typename std::common_type::type; + using detail::round_i; + std::chrono::minutes offset_local{}; + auto offptr = offset ? offset : &offset_local; + fields fds{}; + fds.has_tod = true; + date::from_stream(is, fmt, fds, abbrev, offptr); + if (!fds.ymd.ok() || !fds.tod.in_conventional_range()) + is.setstate(std::ios::failbit); + if (!is.fail()) + tp = round_i(sys_days(fds.ymd) - *offptr + fds.tod.to_duration()); + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, + local_time& tp, std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using CT = typename std::common_type::type; + using detail::round_i; + fields fds{}; + fds.has_tod = true; + date::from_stream(is, fmt, fds, abbrev, offset); + if (!fds.ymd.ok() || !fds.tod.in_conventional_range()) + is.setstate(std::ios::failbit); + if (!is.fail()) + tp = round_i(local_seconds{local_days(fds.ymd)} + fds.tod.to_duration()); + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, + std::chrono::duration& d, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using Duration = std::chrono::duration; + using CT = typename std::common_type::type; + using detail::round_i; + fields fds{}; + date::from_stream(is, fmt, fds, abbrev, offset); + if (!fds.has_tod) + is.setstate(std::ios::failbit); + if (!is.fail()) + d = round_i(fds.tod.to_duration()); + return is; +} + +template , + class Alloc = std::allocator> +struct parse_manip +{ + const std::basic_string format_; + Parsable& tp_; + std::basic_string* abbrev_; + std::chrono::minutes* offset_; + +public: + parse_manip(std::basic_string format, Parsable& tp, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) + : format_(std::move(format)) + , tp_(tp) + , abbrev_(abbrev) + , offset_(offset) + {} + +#if HAS_STRING_VIEW + parse_manip(const CharT* format, Parsable& tp, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) + : format_(format) + , tp_(tp) + , abbrev_(abbrev) + , offset_(offset) + {} + + parse_manip(std::basic_string_view format, Parsable& tp, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) + : format_(format) + , tp_(tp) + , abbrev_(abbrev) + , offset_(offset) + {} +#endif // HAS_STRING_VIEW +}; + +template +std::basic_istream& +operator>>(std::basic_istream& is, + const parse_manip& x) +{ + return date::from_stream(is, x.format_.c_str(), x.tp_, x.abbrev_, x.offset_); +} + +template +inline +auto +parse(const std::basic_string& format, Parsable& tp) + -> decltype(date::from_stream(std::declval&>(), + format.c_str(), tp), + parse_manip{format, tp}) +{ + return {format, tp}; +} + +template +inline +auto +parse(const std::basic_string& format, Parsable& tp, + std::basic_string& abbrev) + -> decltype(date::from_stream(std::declval&>(), + format.c_str(), tp, &abbrev), + parse_manip{format, tp, &abbrev}) +{ + return {format, tp, &abbrev}; +} + +template +inline +auto +parse(const std::basic_string& format, Parsable& tp, + std::chrono::minutes& offset) + -> decltype(date::from_stream(std::declval&>(), + format.c_str(), tp, + std::declval*>(), + &offset), + parse_manip{format, tp, nullptr, &offset}) +{ + return {format, tp, nullptr, &offset}; +} + +template +inline +auto +parse(const std::basic_string& format, Parsable& tp, + std::basic_string& abbrev, std::chrono::minutes& offset) + -> decltype(date::from_stream(std::declval&>(), + format.c_str(), tp, &abbrev, &offset), + parse_manip{format, tp, &abbrev, &offset}) +{ + return {format, tp, &abbrev, &offset}; +} + +// const CharT* formats + +template +inline +auto +parse(const CharT* format, Parsable& tp) + -> decltype(date::from_stream(std::declval&>(), format, tp), + parse_manip{format, tp}) +{ + return {format, tp}; +} + +template +inline +auto +parse(const CharT* format, Parsable& tp, std::basic_string& abbrev) + -> decltype(date::from_stream(std::declval&>(), format, + tp, &abbrev), + parse_manip{format, tp, &abbrev}) +{ + return {format, tp, &abbrev}; +} + +template +inline +auto +parse(const CharT* format, Parsable& tp, std::chrono::minutes& offset) + -> decltype(date::from_stream(std::declval&>(), format, + tp, std::declval*>(), &offset), + parse_manip{format, tp, nullptr, &offset}) +{ + return {format, tp, nullptr, &offset}; +} + +template +inline +auto +parse(const CharT* format, Parsable& tp, + std::basic_string& abbrev, std::chrono::minutes& offset) + -> decltype(date::from_stream(std::declval&>(), format, + tp, &abbrev, &offset), + parse_manip{format, tp, &abbrev, &offset}) +{ + return {format, tp, &abbrev, &offset}; +} + +// duration streaming + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, + const std::chrono::duration& d) +{ + return os << detail::make_string::from(d.count()) + + detail::get_units(typename Period::type{}); +} + +} // namespace date + +#ifdef _MSC_VER +# pragma warning(pop) +#endif + +#ifdef __GNUC__ +# pragma GCC diagnostic pop +#endif + +#endif // DATE_H + diff --git b/src/errorcode.cpp a/src/errorcode.cpp new file mode 100644 index 0000000..ca3ecd4 --- /dev/null +++ a/src/errorcode.cpp @@ -0,0 +1,90 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "errorcode.h" + +// paho +#include + +namespace osdev { +namespace components { +namespace mqtt { + +std::string pahoAsyncErrorCodeToString( int errorCode ) +{ + if (MQTTASYNC_SUCCESS == errorCode) { + return "MQTTASYNC_SUCCESS"; + } + else if (MQTTASYNC_FAILURE == errorCode) { + return "MQTTASYNC_FAILURE"; + } + else if (MQTTASYNC_PERSISTENCE_ERROR == errorCode) { + return "MQTTASYNC_PERSISTENCE_ERROR"; + } + else if (MQTTASYNC_DISCONNECTED == errorCode) { + return "MQTTASYNC_DISCONNECTED"; + } + else if (MQTTASYNC_MAX_MESSAGES_INFLIGHT == errorCode) { + return "MQTTASYNC_MAX_MESSAGES_INFLIGHT"; + } + else if (MQTTASYNC_BAD_UTF8_STRING == errorCode) { + return "MQTTASYNC_BAD_UTF8_STRING"; + } + else if (MQTTASYNC_NULL_PARAMETER == errorCode) { + return "MQTTASYNC_NULL_PARAMETER"; + } + else if (MQTTASYNC_TOPICNAME_TRUNCATED == errorCode) { + return "MQTTASYNC_TOPICNAME_TRUNCATED"; + } + else if (MQTTASYNC_BAD_STRUCTURE == errorCode) { + return "MQTTASYNC_BAD_STRUCTURE"; + } + else if (MQTTASYNC_BAD_QOS == errorCode) { + return "MQTTASYNC_BAD_QOS"; + } + else if (MQTTASYNC_NO_MORE_MSGIDS == errorCode) { + return "MQTTASYNC_NO_MORE_MSGIDS"; + } + else if (MQTTASYNC_OPERATION_INCOMPLETE == errorCode) { + return "MQTTASYNC_OPERATION_INCOMPLETE"; + } + else if (MQTTASYNC_MAX_BUFFERED_MESSAGES == errorCode) { + return "MQTTASYNC_MAX_BUFFERED_MESSAGES"; + } + else if (MQTTASYNC_SSL_NOT_SUPPORTED == errorCode) { + return "MQTTASYNC_SSL_NOT_SUPPORTED"; + } + else if (MQTTASYNC_BAD_PROTOCOL == errorCode) { + return "MQTTASYNC_BAD_PROTOCOL"; + } +#if defined MQTTASYNC_BAD_MQTT_OPTION // Not supported by centos7 paho-c + else if (MQTTASYNC_BAD_MQTT_OPTION == errorCode) { + return "MQTTASYNC_BAD_MQTT_OPTION"; + } +#endif +#if defined MQTTASYNC_WRONG_MQTT_VERSION // Not supported by centos7 paho-c + else if (MQTTASYNC_WRONG_MQTT_VERSION == errorCode) { + return "MQTTASYNC_WRONG_MQTT_VERSION"; + } +#endif + return "Unknown(" + std::to_string(errorCode) + ")"; +} + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev diff --git b/src/errorcode.h a/src/errorcode.h new file mode 100644 index 0000000..41fd07a --- /dev/null +++ a/src/errorcode.h @@ -0,0 +1,21 @@ +#ifndef OSDEV_COMPONENTS_MQTT_ERRORCODE_H +#define OSDEV_COMPONENTS_MQTT_ERRORCODE_H + +// std +#include + +namespace osdev { +namespace components { +namespace mqtt { + +/*! + * \return A stringified paho error code... + */ +std::string pahoAsyncErrorCodeToString( int errorCode ); + + +} // End namespace mqtt +} // End nnamespace components +} // End namesapce osdev + +#endif // OSDEV_COMPONENTS_MQTT_ERRORCODE_H diff --git b/src/histogram.h a/src/histogram.h new file mode 100644 index 0000000..87b8454 --- /dev/null +++ a/src/histogram.h @@ -0,0 +1,352 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#ifndef OSDEV_COMPONENTS_MQTT_MEASUREMENT_HISTOGRAM_H +#define OSDEV_COMPONENTS_MQTT_MEASUREMENT_HISTOGRAM_H + +#include +#include +#include +#include +#include +#include + +#include "utils.h" + +#include "ihistogram.h" + +namespace osdev { +namespace components { +namespace mqtt { +namespace measurement { + +template +double fabs(T val) +{ + return std::fabs(val); +} + +template +double fabs(const std::chrono::duration& dur) +{ + return std::fabs(dur.count()); +} + +template +double to_double(T val) +{ + return static_cast(val); +} + +template +double to_double(const std::chrono::duration& dur) +{ + return static_cast(dur.count()); +} + +template +std::string to_string(T val) +{ + return std::to_string(val); +} + +template +std::string to_string(const std::chrono::duration& dur) +{ + return std::to_string(dur.count()); +} + +template +T min(T) +{ + return std::numeric_limits::min(); +} + +template +std::chrono::duration min(const std::chrono::duration&) +{ + return std::chrono::duration::min(); +} + +template +T max(T) +{ + return std::numeric_limits::max(); +} + +template +std::chrono::duration max(const std::chrono::duration&) +{ + return std::chrono::duration::max(); +} + +/** + * @brief This class implements a histogram for typed values. + * Outlier values to the left are counted on the first bin and to the right on the last bin. + * @tparam T The value type. T must be default constructable. + * @tparam Buckets The number of buckets to use for this histogram. Default is 10 buckets. + */ +template +class Histogram : public IHistogram +{ +public: + /** + * @brief Construct a histogram. + * @param id The histogram identification + * @param minValue The minimum value of the histogram. + * @param maxValue The maxmim value of the histogram. + * @param unit The unit of the values that are collected in this histogram. + */ + Histogram(const std::string& id, T minValue, T maxValue, const char* unit); + + /** + * @brief Set a value in the histogram. + */ + void setValue(T value); + + /** + * @return true when values are added since the last time a snapshot was taken of the histogram or a reset was performed, false otherwise. + */ + bool isUpdated() const; + + /** + * @brief Reset the histogram. + * All counts are made zero and the smallest and largest recorded values are set to default. + */ + void reset(); + + // IHistogram implementation + virtual const std::string& id() const override; + virtual std::size_t numBuckets() const override; + virtual double bucketWidth() const override; + virtual std::string minValueString() const override; + virtual std::string maxValueString() const override; + virtual const std::string& unit() const override; + virtual HistogramData histogramData() const override; + virtual std::size_t numberOfValuesSinceLastClear() const override; + virtual std::string smallestValueSinceLastClear() const override; + virtual std::string largestValueSinceLastClear() const override; + virtual std::string averageValueSinceLastClear() const override; + virtual void clearRunningValues() override; + virtual std::string toString() const override; + +private: + /** + * @brief Get the index on which to count the given value. + * The outliers to the left are counted on index 0 and to the right on index size() - 1. + * @param value The value to get the index for. + * @return the index in the histogram on which to count the given value. + */ + inline std::size_t index(T value) + { + if (value > m_maxValue) { + return m_histogram.size() - 1; + } + if (value < m_minValue) { + return 0; + } + + return 1 + static_cast(to_double((value - m_minValue) / m_bucketWidth)); + } + + const std::string m_id; + const std::string m_unit; + const T m_minValue; + const T m_maxValue; + const double m_bucketWidth; + T m_smallestValue; + T m_largestValue; + T m_smallestValueSinceLastClear; + T m_largestValueSinceLastClear; + T m_summedValueSinceLastClear; + std::size_t m_numberOfValuesSinceLastClear; + mutable std::mutex m_mutex; + std::vector m_histogram; + mutable std::atomic_bool m_isUpdated; +}; + +template +Histogram::Histogram(const std::string& _id, T minValue, T maxValue, const char* _unit) + : m_id(_id) + , m_unit(_unit) + , m_minValue(minValue) + , m_maxValue(maxValue) + , m_bucketWidth(fabs((m_maxValue - m_minValue)) / Buckets) + , m_smallestValue{ max(maxValue) } + , m_largestValue{ min(maxValue) } + , m_smallestValueSinceLastClear{ max(maxValue) } + , m_largestValueSinceLastClear{ min(maxValue) } + , m_summedValueSinceLastClear{} + , m_numberOfValuesSinceLastClear{} + , m_mutex() + , m_histogram(Buckets + 2) // + 2 for the out of bound buckets + , m_isUpdated(false) +{ +} + +template +void Histogram::setValue(T value) +{ + std::lock_guard lg(m_mutex); + apply_unused_parameters(lg); + + if (value > m_largestValueSinceLastClear) { + m_largestValueSinceLastClear = value; + } + + if (value < m_smallestValueSinceLastClear) { + m_smallestValueSinceLastClear = value; + } + + if (value > m_largestValue) { + m_largestValue = value; + } + + if (value < m_smallestValue) { + m_smallestValue = value; + } + + m_summedValueSinceLastClear += value; + ++m_numberOfValuesSinceLastClear; + + m_histogram[this->index(value)]++; + m_isUpdated = true; +} + +template +bool Histogram::isUpdated() const +{ + return m_isUpdated; +} + +template +void Histogram::reset() +{ + std::lock_guard lg(m_mutex); + apply_unused_parameters(lg); + + m_smallestValue = T{ max(m_maxValue) }; + m_largestValue = T{ min(m_maxValue) }; + m_smallestValueSinceLastClear = T{ max(m_maxValue) }; + m_largestValueSinceLastClear = T{ min(m_maxValue) }; + m_summedValueSinceLastClear = T{}; + m_numberOfValuesSinceLastClear = 0; + + std::fill(m_histogram.begin(), m_histogram.end(), 0); + m_isUpdated = false; +} + +template +const std::string& Histogram::id() const +{ + return m_id; +} + +template +std::size_t Histogram::numBuckets() const +{ + return Buckets; +} + +template +double Histogram::bucketWidth() const +{ + return m_bucketWidth; +} + +template +std::string Histogram::minValueString() const +{ + return to_string(m_minValue); +} + +template +std::string Histogram::maxValueString() const +{ + return to_string(m_maxValue); +} + +template +const std::string& Histogram::unit() const +{ + return m_unit; +} + +template +HistogramData Histogram::histogramData() const +{ + std::lock_guard lg(m_mutex); + apply_unused_parameters(lg); + + m_isUpdated = false; + return HistogramData(to_string(m_smallestValue), to_string(m_largestValue), + to_string(m_smallestValueSinceLastClear), to_string(m_largestValueSinceLastClear), averageValueSinceLastClear(), + m_numberOfValuesSinceLastClear, m_histogram); +} + +template +std::size_t Histogram::numberOfValuesSinceLastClear() const +{ + return m_numberOfValuesSinceLastClear; +} + +template +std::string Histogram::smallestValueSinceLastClear() const +{ + return to_string(m_smallestValueSinceLastClear); +} + +template +std::string Histogram::largestValueSinceLastClear() const +{ + return to_string(m_largestValueSinceLastClear); +} + +template +std::string Histogram::averageValueSinceLastClear() const +{ + if (0 == m_numberOfValuesSinceLastClear) { + return "undefined"; + } + return to_string(to_double(m_summedValueSinceLastClear) / m_numberOfValuesSinceLastClear); +} + +template +void Histogram::clearRunningValues() +{ + std::lock_guard lg(m_mutex); + apply_unused_parameters(lg); + + m_smallestValueSinceLastClear = T{ max(m_maxValue) }; + m_largestValueSinceLastClear = T{ min(m_maxValue) }; + m_summedValueSinceLastClear = T{}; + m_numberOfValuesSinceLastClear = 0; +} + +template +std::string Histogram::toString() const +{ + return visualizeHistogram(*this); +} + +} // End namespace measurement +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_MEASUREMENT_HISTOGRAM_H diff --git b/src/histogramprovider.h a/src/histogramprovider.h new file mode 100644 index 0000000..7164430 --- /dev/null +++ a/src/histogramprovider.h @@ -0,0 +1,201 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#ifndef OSDEV_COMPONENTS_MQTT_MEASUREMENT_HISTOGRAMPROVIDER_H +#define OSDEV_COMPONENTS_MQTT_MEASUREMENT_HISTOGRAMPROVIDER_H + +// std +#include +#include +#include +#include + +#include "compat-c++14.h" + +// mlogic::common +#include "sharedreaderlock.h" + +#include "histogram.h" + +namespace osdev { +namespace components { +namespace mqtt { +namespace measurement { + +/** + * @brief This class provides the user with Entry class for putting measurements in a histogram. + * @tparam TContext A tag type that is used to give the HistogramProvider a user defined context. + * @tparam TValue Defines the value type that the histograms of this provider use. + * @tparam Buckets The number of buckets to use on creation of a Histogram. The default number of buckets is 10. + * + * The TValue type must define the following publically visible members. + * + * value_type - The underlying value type used in the histogram + * const value_type minValue - The minimum value of the histogram + * const value_type maxValue - The maximum value of the histogram + * const char* unit - The value unit. + */ +template +class HistogramProvider +{ +public: + using HistogramType = Histogram; + + /** + * @brief Get the HistogramProvider instance. + */ + static HistogramProvider& instance(); + + /** + * @brief Add a value to the histogram identified by id. + * @param id Identifies the histogram to add the value to. + * @param value The value to add. + */ + void addValue(const std::string& id, typename TValue::value_type value); + + /** + * @brief Log the histogram identified by id via the ILogger framework. + * @param id The histogram to log. If no histogram is found no work is done. + */ + void log(const std::string& id); + + /** + * @brief Log the histograms via the ILogger framework. + * @param onlyChanged When set to true only log histograms that have changed. Default is false. + */ + void logAll(bool onlyChanged = false); + + /** + * @brief Reset all the histograms. + * @param logBeforeReset Flag that indicates whether the histogram needs to be logged before reset. Default is true. + * @note Only changed histograms are logged. + */ + void resetAll(bool logBeforeReset = true); + + /** + * @return The logOnDestroy flag value. + */ + bool logOnDestroy() const { return m_logOnDestroy; } + + /** + * @brief Set the logOnDestroy flag. + * @param logOnDest Log on destruction when true, do not log when set to false. + */ + void setLogOnDestroy(bool logOnDest) { m_logOnDestroy = logOnDest; } + +private: + HistogramProvider(); + + /** + * @brief On destruction the provider will log all its histogram information to stdout. + * Since the destruction will happen as one of the last things in the process, stdout is + * used for logging. + */ + ~HistogramProvider(); + + void log(const HistogramType& hist, bool onlyChanged); + + SharedReaderLock m_sharedLock; + std::unordered_map> m_histograms; + bool m_logOnDestroy; ///< Default is true. +}; + +// static +template +HistogramProvider& HistogramProvider::instance() +{ + static HistogramProvider s_provider; + return s_provider; +} + +template +void HistogramProvider::addValue(const std::string& id, typename TValue::value_type value) +{ + OSDEV_COMPONENTS_SHAREDLOCK_SCOPE(m_sharedLock); + auto it = m_histograms.find(id); + if (m_histograms.end() == it) { + OSDEV_COMPONENTS_EXCLUSIVELOCK_SCOPE(m_sharedLock); + constexpr TValue val; + it = (m_histograms.emplace(std::make_pair(id, std::make_unique(id, val.minValue, val.maxValue, val.unit)))).first; + } + it->second->setValue(value); +} + +template +void HistogramProvider::log(const std::string& id) +{ + OSDEV_COMPONENTS_SHAREDLOCK_SCOPE(m_sharedLock); + auto it = m_histograms.find(id); + if (m_histograms.end() != it) { + log(*(it->second), false); + } +} + +template +void HistogramProvider::logAll(bool onlyChanged) +{ + OSDEV_COMPONENTS_SHAREDLOCK_SCOPE(m_sharedLock); + for (const auto& h : m_histograms) { + log(*h.second, onlyChanged); + } +} + +template +void HistogramProvider::resetAll(bool logBeforeReset) +{ + OSDEV_COMPONENTS_SHAREDLOCK_SCOPE(m_sharedLock); + for (const auto& h : m_histograms) { + if (logBeforeReset) { + log(*h.second, true); // only log the histograms that have changed + } + h.second->reset(); + } +} + +template +HistogramProvider::HistogramProvider() + : m_sharedLock() + , m_histograms() + , m_logOnDestroy(true) +{ +} + +template +HistogramProvider::~HistogramProvider() +{ + if (m_logOnDestroy) { + for (const auto& h : m_histograms) { + std::cout << *h.second << std::endl; + } + } +} + +template +void HistogramProvider::log(const HistogramType& hist, bool onlyChanged) +{ + if ((onlyChanged && hist.isUpdated()) || !onlyChanged) { + MLOGIC_COMMON_INFO("HistogramProvider", "%1", hist); + } +} + +} // End namespace measurement +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_MEASUREMENT_HISTOGRAMPROVIDER_H diff --git b/src/ihistogram.cpp a/src/ihistogram.cpp new file mode 100644 index 0000000..3672c12 --- /dev/null +++ a/src/ihistogram.cpp @@ -0,0 +1,145 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "ihistogram.h" + +// std +#include +#include +#include +#include + +namespace osdev { +namespace components { +namespace mqtt { +namespace measurement { + +IHistogram::~IHistogram() = default; + +namespace { + +std::pair largestValueUnderThreshold(const std::vector& hist, double thresholdValue) +{ + int bin = -1; + std::size_t maxValue = 0; + // do not use the outlier bins for this calculation. + for (std::size_t i = 1; i < hist.size() - 1; ++i) { + if (hist[i] > maxValue && hist[i] < thresholdValue) { + bin = static_cast(i); + maxValue = hist[i]; + } + } + return { bin, maxValue }; +} + +} // namespace + +std::string visualizeHistogram(const IHistogram& histogram) +{ + const std::size_t diagramHeight = 10; + auto histData = histogram.histogramData(); // take a snapshot of the histogram data. + const auto& hist = histData.data(); + + std::ostringstream oss; + oss << "Histogram for " << histogram.id() << "\\n"; + auto minmax = std::minmax_element(hist.begin(), hist.end()); + + auto minCountValueString = std::to_string(*minmax.first); + auto maxCountValueString = std::to_string(*minmax.second); + + auto countValueFieldWidth = static_cast(std::max(minCountValueString.size(), maxCountValueString.size())); + auto verticalSize = (*minmax.second - *minmax.first) / static_cast(diagramHeight); + + auto totalMeasurements = std::accumulate(hist.begin(), hist.end(), 0); + oss << "Total nr of measurements: " << totalMeasurements << ", Nr of buckets: " << histogram.numBuckets() << ", bucket width: " << histogram.bucketWidth() << ", vertical resolution: " << verticalSize << "\\n"; + auto binValuePair = largestValueUnderThreshold(hist, verticalSize); + if (binValuePair.first >= 0) { + oss << "Largest value under threshold: " << binValuePair.second << " in bin " << binValuePair.first << "\\n"; + } + if (hist.front() > 0) { + oss << "Outliers to the left: " << hist.front() << ", smallest value: " << histData.smallestValueString() << histogram.unit() << "\\n"; + } + if (hist.back() > 0) { + oss << "Outliers to the right: " << hist.back() << ", largest value: " << histData.largestValueString() << histogram.unit() << "\\n"; + } + + oss << "\\nSince last clear:\\n" + << " number of values : " << histData.numberOfValuesSinceLastClear() << "\\n" + << " smallest value : " << histData.smallestValueSinceLastClearString() << histogram.unit() << "\\n" + << " largest value : " << histData.largestValueSinceLastClearString() << histogram.unit() << "\\n" + << " average value : " << histData.averageValueSinceLastClearString() << histogram.unit() << "\\n\\n"; + + oss << std::setw(countValueFieldWidth) << maxCountValueString << " |"; + for (std::size_t line = 0; line < diagramHeight; ++line) { + for (const auto& cnt : hist) { + if ((cnt - *minmax.first) > (diagramHeight - line - 1) * verticalSize) { + oss << '*'; + } + else { + oss << ' '; + } + } + oss << "|\\n"; + if (line + 1 < diagramHeight) { + oss << std::setw(countValueFieldWidth + 2) << '|'; + } + } + oss << std::setw(countValueFieldWidth) << minCountValueString << " |/" << std::string(hist.size() - 2, '-') << "\\| " << histogram.unit() << "\\n"; + + auto minValueString = histogram.minValueString(); + auto maxValueString = histogram.maxValueString(); + auto valueFieldWidth = std::max(minValueString.size(), maxValueString.size()); + + for (std::size_t line = 0; line < valueFieldWidth; ++line) { + oss << std::setw(countValueFieldWidth + 3 + static_cast(line)) << ""; + oss << (line < minValueString.size() ? minValueString[line] : ' '); + + if (line < maxValueString.size()) { + oss << std::string(histogram.numBuckets() - 1, ' ') << maxValueString[line]; + } + oss << "\\n"; + } + + // Add the non zero data to the log for later analysis + // format: binnumber:value;binnumber:value;.... + bool first = true; + for (std::size_t idx = 0; idx < hist.size(); ++idx) { + if (hist[idx] > 0) { + if (!first) { + oss << ';'; + } + else { + oss << "\\n"; + first = false; + } + oss << idx << ':' << hist[idx]; + } + } + + return oss.str(); +} + +std::ostream& operator<<(std::ostream& os, const IHistogram& rhs) +{ + return os << rhs.toString(); +} + +} // End namespace measurement +} // End namespace mqtt +} // End namespace components +} // End namespace osdev diff --git b/src/ihistogram.h a/src/ihistogram.h new file mode 100644 index 0000000..b5576bd --- /dev/null +++ a/src/ihistogram.h @@ -0,0 +1,209 @@ +#ifndef OSDEV_COMPONENTS_MQTT_MEASUREMENT_IHISTOGRAM_H +#define OSDEV_COMPONENTS_MQTT_MEASUREMENT_IHISTOGRAM_H + +// std +#include +#include +#include + +namespace osdev { +namespace components { +namespace mqtt { +namespace measurement { + +/** + * @brief Class that holds the dynamic part of a histogram. + * Used to take a snapshot of the histogram. + */ +class HistogramData +{ +public: + /** + * @brief Construct dynamic histrogram data. + * @param smallestValue A stringified version of the smallest recorded value since the last update. + * @param largestValue A stringified version of the largest recorded value since the last update. + * @param smallestValueSinceLastClear A stringified version of the smallest value as string since last clear. + * @param largestValueSinceLastClear A stringified version of the largest value as string since last clear. + * @param averageValueSinceLastClear A stringified version of the average value as string since last clear. + * @param nrOfValuesSinceLastClear Number of values since last clear. + * @param histCounts The histogram counts. + */ + HistogramData(const std::string& smallestValue, const std::string& largestValue, + const std::string& smallestValueSinceLastClear, + const std::string& largestValueSinceLastClear, + const std::string& averageValueSinceLastClear, + std::size_t nrOfValuesSinceLastClear, + const std::vector& histCounts) + : m_smallestValueString(smallestValue) + , m_largestValueString(largestValue) + , m_smallestValueSinceLastClearString(smallestValueSinceLastClear) + , m_largestValueSinceLastClearString(largestValueSinceLastClear) + , m_averageValueSinceLastClearString(averageValueSinceLastClear) + , m_numberOfValuesSinceLastClear(nrOfValuesSinceLastClear) + , m_data(histCounts) + { + } + + /** + * @return The smallest recorded value as a string. + */ + const std::string& smallestValueString() const + { + return m_smallestValueString; + } + + /** + * @return The largest recorded value as a string. + */ + const std::string& largestValueString() const + { + return m_largestValueString; + } + + /** + * @return The number of value since last clear. + */ + std::size_t numberOfValuesSinceLastClear() const + { + return m_numberOfValuesSinceLastClear; + } + + /** + * @return The smallest value since last clear as a string. + */ + const std::string& smallestValueSinceLastClearString() const + { + return m_smallestValueSinceLastClearString; + } + + /** + * @return The largest value since last clear as a string. + */ + const std::string& largestValueSinceLastClearString() const + { + return m_largestValueSinceLastClearString; + } + + /** + * @return The average value since last clear as a string. + */ + const std::string& averageValueSinceLastClearString() const + { + return m_averageValueSinceLastClearString; + } + + /** + * @return The histogram counts. + */ + const std::vector& data() const + { + return m_data; + } + +private: + std::string m_smallestValueString; + std::string m_largestValueString; + std::string m_smallestValueSinceLastClearString; + std::string m_largestValueSinceLastClearString; + std::string m_averageValueSinceLastClearString; + std::size_t m_numberOfValuesSinceLastClear; + std::vector m_data; +}; + +/** + * @brief Interface for histogram classes + * Can be used to store histograms in a container and visualize the histogram. + */ +class IHistogram +{ +public: + virtual ~IHistogram(); + + /** + * @return The histogram identification (title). + */ + virtual const std::string& id() const = 0; + + /** + * @return The number of buckets. + */ + virtual std::size_t numBuckets() const = 0; + + /** + * @return The bucket width as a floating point value. + */ + virtual double bucketWidth() const = 0; + + /** + * @return The minimum value of the histogram definition as a string. + */ + virtual std::string minValueString() const = 0; + + /** + * @return The maximum value of the histogram definition as a string. + */ + virtual std::string maxValueString() const = 0; + + /** + * @return The value unit as a a string. + */ + virtual const std::string& unit() const = 0; + + /** + * @return The histogram data. + * This takes a snapshot of the histogram data. + */ + virtual HistogramData histogramData() const = 0; + + /** + * @return The number of value since last clear. + */ + virtual std::size_t numberOfValuesSinceLastClear() const = 0; + + /** + * @return The smallest value since last clear as a string. + */ + virtual std::string smallestValueSinceLastClear() const = 0; + + /** + * @return The largest value since last clear as a string. + */ + virtual std::string largestValueSinceLastClear() const = 0; + + /** + * @return The average value since last clear as a string. + */ + virtual std::string averageValueSinceLastClear() const = 0; + + /** + * @brief Clears the values that are kept between successive clearRunningValues calls. + * These are: + * smallestValueSinceLastClear + * largestValueSinceLastClear + * averageValueSinceLastClear + */ + virtual void clearRunningValues() = 0; + + /** + * @return The ascii representation of the histogram. + */ + virtual std::string toString() const = 0; +}; + +/** + * @brief Make an ascii visualisation of the given histogram data. + * @param histogram The histogram to visualize. + */ +std::string visualizeHistogram(const IHistogram& histogram); + +/** + * @brief Stream operator for IHistogram instances. + */ +std::ostream& operator<<(std::ostream& os, const IHistogram& rhs); + +} // End namespace measurement +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_MEASUREMENT_IHISTOGRAM_H diff --git b/src/imqttclient.h a/src/imqttclient.h new file mode 100644 index 0000000..3891651 --- /dev/null +++ a/src/imqttclient.h @@ -0,0 +1,143 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#ifndef OSDEV_COMPONENTS_MQTT_IMQTTCLIENT_H +#define OSDEV_COMPONENTS_MQTT_IMQTTCLIENT_H + +// std +#include +#include +#include +#include + +// boost +#include + +// mlogic::client +#include "istatecallback.h" + +// mlogic::mqtt +#include "connectionstatus.h" +#include "credentials.h" +#include "mqttmessage.h" +#include "token.h" + +namespace osdev { +namespace components { +namespace mqtt { + +/** + * @brief Interface that describes the minimal interface that a wrapper implementation needs to have in + * order to coorporate with the MqttClient class. + */ +class IMqttClient : public virtual IStateCallback +{ +public: + virtual ~IMqttClient() {} + + /** + * @brief Connect to the endpoint + * @param host The host name or ip address. + * @param port The port to use. + * @param credentials The credentials to use. + */ + virtual void connect(const std::string& host, int port, const Credentials& credentials) = 0; + + /** + * @brief Connect to the endpoint + * @param endpoint an uri endpoint description. + */ + virtual void connect(const std::string& endpoint) = 0; + + /** + * @brief Disconnect the client from the broker + */ + virtual void disconnect() = 0; + + /** + * @brief Publish a message with a given quality of service (0, 1 or 2). + * @param message The message to publish. + * @param qos The quality of service to use. + * @return The token that identifies the publication (used in the deliveryCompleteCallback). + */ + virtual Token publish(const MqttMessage& message, int qos) = 0; + + /** + * @brief Subscribe to a topic(filter). + * The combination of topic and qos makes a subscription unique on a wrapper client. When a topic has overlap + * with an existing subscription it is guaranteed that the given callback is only called once. + * @param topic The topic to subscribe to (can have wildcards). The topic can have overlap with existing topic subscriptions. + * @param qos The quality of service to use (0, 1 or 2). + * @param cb The callback that is called when messages are received for this subscription. + * @return A token that identifies the subscribe command. + */ + virtual Token subscribe(const std::string& topic, int qos, const std::function& cb) = 0; + + /** + * @brief Unsubscribe an existing subscription. + * The combination of topic and qos can occur on multiple wrapper clients. All subscriptions that match are unsubscribed. + * @param topic The topic to unsubscribe. + * @param qos The quality of service of the subscription (0, 1 or 2). + * @return A set of unsubscribe tokens identifying the unsubscribe commands. Usually the set will contain only one item. + */ + virtual std::set unsubscribe(const std::string& topic, int qos) = 0; + + /** + * @brief Wait for all commands to complete. + * @param waitFor The number of milliseconds to wait for completetion of all commands. + * @return True when all commands have completed in time or false if not. + */ + virtual bool waitForCompletion(std::chrono::milliseconds waitFor) const = 0; + + /** + * @brief Wait for a single command to complete. + * @param waitFor The number of milliseconds to wait for completetion of the command. + * @param token The token to wait for. + * @return True when the command has completed in time or false if not. + * @note Non existent tokens are also reported as completed. + */ + virtual bool waitForCompletion(std::chrono::milliseconds waitFor, const Token& token) const = 0; + + /** + * @brief Wait for commands to complete. + * This method enables the user to wait for a set of commands. An empty set means to wait for all commands to complete. + * @param waitFor The number of milliseconds to wait for completetion of all commands. + * @param tokens The tokens to wait for. An empty set means to wait for all commands to complete. + * @return True when the commands have completed in time or false if not. + * @note Non existent tokens are also reported as completed. + */ + virtual bool waitForCompletion(std::chrono::milliseconds waitFor, const std::set& tokens) const = 0; + + /** + * @brief Get the result of a command. + * @param token The token identifying the result. + * @return The command result when available. + */ + virtual boost::optional commandResult(const Token& token) const = 0; + + /** + * @return The endpoint uri. + */ + virtual std::string endpoint() const = 0; +}; + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_IMQTTCLIENT_H diff --git b/src/imqttclientimpl.cpp a/src/imqttclientimpl.cpp new file mode 100644 index 0000000..a9250cb --- /dev/null +++ a/src/imqttclientimpl.cpp @@ -0,0 +1,23 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "imqttclientimpl.h" + +using namespace osdev::components::mqtt; + +IMqttClientImpl::~IMqttClientImpl() = default; diff --git b/src/imqttclientimpl.h a/src/imqttclientimpl.h new file mode 100644 index 0000000..1953951 --- /dev/null +++ a/src/imqttclientimpl.h @@ -0,0 +1,166 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#ifndef OSDEV_COMPONENTS_MQTT_IMQTTCLIENTIMPL_H +#define OSDEV_COMPONENTS_MQTT_IMQTTCLIENTIMPL_H + +// std +#include +#include +#include +#include +#include +#include + +// boost +#include + +// mlogic::mqtt +#include "connectionstatus.h" +#include "mqttmessage.h" + +namespace osdev { +namespace components { +namespace mqtt { + +/** + * @brief Interface that describes the minimal interface that a wrapper implementation needs to have in + * order to coorporate with the MqttClient class. + */ +class IMqttClientImpl +{ +public: + virtual ~IMqttClientImpl(); + + /** + * @return id of this client. + */ + virtual std::string clientId() const = 0; + + /** + * @return The connection status of the wrapper. + */ + virtual ConnectionStatus connectionStatus() const = 0; + + /** + * @brief Connect the wrapper to the endpoint. + * @param wait A flag that indicates if the method should wait for a succesful connection. + * @return the operation token. + */ + virtual std::int32_t connect(bool wait) = 0; + + /** + * @brief Disconnect the wrapper. + * @param wait A flag that indicates if the method should wait for a succesful disconnect. When true the method will wait for the timeoutMs value plus some additional time. + * @param timeoutMs A timeout in milliseconds. The timeout is used to give in flight messages a chance to get delivered. After the timeout the disconnect command is sent. + * @return the operation token. + */ + virtual std::int32_t disconnect(bool wait, int timeoutMs) = 0; + + /** + * @brief Publish a message to an mqtt endpoint with a given quality of service. + * When the connection is in state reconnect the publish is saved so that it can be published later. + * @param message The message to send. + * @param qos The quality of service to use (0, 1 or 2). + * @return The message token. This token identifies the publication. + */ + virtual std::int32_t publish(const MqttMessage& message, int qos) = 0; + + /** + * @brief Publish messages when client is reconnected. + */ + virtual void publishPending() = 0; + + /** + * @brief Subscribe to a topic(filter). + * The combination of topic and qos makes a subscription unique. Subscriptions with the same topic filter + * but different qos are considered different, but they will overlap and cannot exist in the same wrapper. + * @param topic The topic to subscribe to (can have wildcards). The topic cannot have overlap with existing topic subscriptions. + * @param qos The quality of service to use (0, 1 or 2). + * @param cb The callback that is called when messages are received for this subscription. + * @return the operation token. + */ + virtual std::int32_t subscribe(const std::string& topic, int qos, const std::function& cb) = 0; + + /** + * @brief Resubscribe existing topicfilters. + * This method should be called only after a reconnect when subscriptions need to be reinstated. + */ + virtual void resubscribe() = 0; + + /** + * @brief Unsubscribe an existing subscription. + * The combination of topic and qos make a subscription unique. + * @param topic The topic to unsubscribe. + * @param qos The quality of service of the subscription (0, 1 or 2). + * @return the operation token. + */ + virtual std::int32_t unsubscribe(const std::string& topic, int qos) = 0; + + /** + * @brief Unsubscribe all subscriptions. + */ + virtual void unsubscribeAll() = 0; + + /** + * @brief Wait for commands to complete. + * This method enables the user to wait for a set of tokens. An empty set means wait for all operations to complete. + * @param waitFor The number of milliseconds to wait for completetion of all commands. + * @param tokens The set of tokens to wait for. + * @return The number of milliseconds left. + */ + virtual std::chrono::milliseconds waitForCompletion(std::chrono::milliseconds waitFor, const std::set& tokens) const = 0; + + /** + * @brief Check if a topic overlaps with existing topic subscriptions. + * @param topic The topic to test. + * @return true when overlap is detected, false otherwise. + */ + virtual bool isOverlapping(const std::string& topic) const = 0; + + /** + * @brief Check if a topic overlaps with existing topic subscriptions. + * @param topic The topic to test. + * @param[out] existingTopic Contains the topic on which overlap is detected. + * @return true when overlap is detected. The existingTopic contains the topic on which overlap is detected, false otherwise. + */ + virtual bool isOverlapping(const std::string& topic, std::string& existingTopic) const = 0; + + /** + * @return A vector with the pending operation tokens. + */ + virtual std::vector pendingOperations() const = 0; + + /** + * @return true when client wrapper has pending subscriptions, false otherwise. + */ + virtual bool hasPendingSubscriptions() const = 0; + + /** + * @return The operation result. + * @retval true operation has succeeded. + * @retval false operation has failed. + */ + virtual boost::optional operationResult(std::int32_t token) const = 0; +}; + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_IMQTTCLIENTIMPL_H diff --git b/src/istatecallback.cpp a/src/istatecallback.cpp new file mode 100644 index 0000000..1864c55 --- /dev/null +++ a/src/istatecallback.cpp @@ -0,0 +1,72 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "istatecallback.h" + +#include + +namespace osdev { +namespace components { +namespace mqtt { + +std::ostream& operator<<(std::ostream& os, StateEnum rhs) +{ + switch (rhs) { + case StateEnum::Unknown: + os << "Unknown"; + break; + case StateEnum::CommunicationFailure: + os << "CommunicationFailure"; + break; + case StateEnum::GeneralFailure: + os << "GeneralFailure"; + break; + case StateEnum::Good: + os << "Good"; + break; + case StateEnum::Shutdown: + os << "Shutdown"; + break; + case StateEnum::ConnectionFailure: + os << "ConnectionFailure"; + break; + case StateEnum::Unregister: + os << "Unregister"; + break; + } + return os; +} + +std::string createIdentifier(const std::string& idStatic, const ClientIdentifier& clientId, const IStateCallback* idDynamic) +{ + std::ostringstream oss; + oss << idStatic; + if (!clientId.m_id.empty()) { + oss << "::" << clientId.m_id; + } + oss << " " << idDynamic; + return oss.str(); +} + +IStateCallback::~IStateCallback() +{ +} + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev diff --git b/src/istatecallback.h a/src/istatecallback.h new file mode 100644 index 0000000..164724d --- /dev/null +++ a/src/istatecallback.h @@ -0,0 +1,123 @@ +#ifndef OSDEV_COMPONENTS_MQTT_ISTATECALLBACK_H +#define OSDEV_COMPONENTS_MQTT_ISTATECALLBACK_H + +// std +#include + +// boost +#include + +namespace osdev { +namespace components { +namespace mqtt { + +/*! + * \brief Struct introduces a typed client identifier. + */ +struct ClientIdentifier +{ + /*! + * \brief Construct a ClientIdentifier + * \param id The client id. Default is empty. + */ + explicit ClientIdentifier(const std::string& id = "") + : m_id(id) + { + } + + const std::string m_id; ///< Client identifier +}; + +/*! + * \brief Enumeration of state codes. + * The states Unknown, CommunicationFailure, GeneralFailure, Good + * and Shutdown are used to communicate state about the server + * that is connected to. The states ConnectionFailure and Unregister + * communicate state about the client. + */ +enum class StateEnum +{ + Unknown, ///< State of underlying data source is unknown. + CommunicationFailure, ///< No communication with underlying data source. + GeneralFailure, ///< Some failure in the underlying data source. + Good, ///< Underlying data source is available. + Shutdown, ///< Underlying data source is shutting down. + ConnectionFailure, ///< Client connection has failed. + Unregister ///< Client is being unregistered. +}; + +/*! + * \brief Stream a StateEnum value + */ +std::ostream& operator<<(std::ostream& os, StateEnum rhs); + +/*! + * \brief Type for identifying state change callbacks. + */ +using StateChangeCallbackHandle = std::uint32_t; + +class IStateCallback; + +/*! + * \brief Create an id for an IStateCallback object. + * \param idStatic - Static part of the id (name of the class). + * \param clientId - Client identifier. Can contain information on how + * the client is used for instance. If the id is empty + * it is not used. + * \param idDynamic - Dynamic part (object address). + * \return identifier as string + */ +std::string createIdentifier(const std::string& idStatic, const ClientIdentifier& clientId, const IStateCallback* idDynamic); + +/*! + * \brief State callback interface. + * When a client implements this interface it can be registered on a + * server in order to handle state changes from the underlying data source. + * \note When the client is destroyed it is expected that it unregisters itself by calling + * clearAllStateChangeCallbacks(). + */ +class IStateCallback +{ +public: + using SigStateChange = boost::signals2::signal; + using SlotStateChange = SigStateChange::slot_type; + + virtual ~IStateCallback(); + + /*! + * \brief Get a string that identifies this interface. + */ + virtual std::string clientId() const = 0; + + /*! + * \brief Register a callback function that is called when the status changes. + * \param cb - The callback function to register. + * \return handle to the registered callback. + * \note Make sure that the callback function lives longer than the IStateCallback object. + * Otherwise unregister callback function before it is destroyed. + */ + virtual StateChangeCallbackHandle registerStateChangeCallback(const SlotStateChange& cb) = 0; + + /*! + * \brief Unregister a previously registered callback function. + * If the callback function was not registered nothing happens. + * \param handle - Identifies the callback function to to unregister. + */ + virtual void unregisterStateChangeCallback(StateChangeCallbackHandle handle) = 0; + + /*! + * \brief Remove all the registered callback functions. + */ + virtual void clearAllStateChangeCallbacks() = 0; + + /*! + * \brief retuns the latest state received. + */ + virtual StateEnum state() const = 0; +}; + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_ISTATECALLBACK_H diff --git b/src/lockguard.h a/src/lockguard.h new file mode 100644 index 0000000..7f3c2e2 --- /dev/null +++ a/src/lockguard.h @@ -0,0 +1,225 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#ifndef OSDEV_COMPONENTS_MQTT_LOCKGUARD_H +#define OSDEV_COMPONENTS_MQTT_LOCKGUARD_H + +// std +#include + +// osdev::components::mqtt::measurement +#include "measure.h" +#include "utils.h" + +/** + * @brief Enable the lock measurements when macro MEASURE_LOCKS is defined. + * If the macro is not defined the NOP versions of the measure macros are used. + */ +#ifdef MEASURE_LOCKS +#define OSDEV_COMPONENTS_MEASURELOCK OSDEV_COMPONENTS_MEASUREMENT_MEASURE +#define OSDEV_COMPONENTS_MEASURELOCK_SPECIFIC OSDEV_COMPONENTS_MEASUREMENT_MEASURE_SPECIFIC +#else +#define OSDEV_COMPONENTS_MEASURELOCK OSDEV_COMPONENTS_MEASUREMENT_MEASURE_NOP +#define OSDEV_COMPONENTS_MEASURELOCK_SPECIFIC OSDEV_COMPONENTS_MEASUREMENT_MEASURE_SPECIFIC_NOP +#endif + +/** + * @brief Create a lockguard for a given mutex in a specific context. + * @param mutexVariableName The name of the mutex. + * @param mutexObtainer The mutex to lock. This can also be a function that returns a mutex. + * If MEASURE_LOCKS is enabled this method uses a static histogram to record the timing of obtaining the lock for all instances. + */ +#define OSDEV_COMPONENTS_LOCKGUARD_OBTAINER(mutexVariableName, mutexObtainer) \ + OSDEV_COMPONENTS_MEASURELOCK(LOCKGUARD, std::lock_guard Lock__Guard__##mutexVariableName##__(mutexObtainer), osdev::components::mqtt::measure_locking_tag, osdev::components::mqtt::MeasureLockingValue, 100) \ + osdev::components::mqtt::apply_unused_parameters(Lock__Guard__##mutexVariableName##__); + +/** + * @brief Create a lockguard for a given mutex in a specific context. + * @param mutexVariableName The name of the mutex. + * @param mutexObtainer The mutex to lock. This can also be a function that returns a mutex. + * @param id The id that identifies this specific lock guard. Used for measuring timings. + * If MEASURE_LOCKS is enabled this method uses a specific histogram instance to record the timing of obtaining this lock. + */ +#define OSDEV_COMPONENTS_LOCKGUARD_SPECIFIC_OBTAINER(mutexVariableName, mutexObtainer, id) \ + OSDEV_COMPONENTS_MEASURELOCK_SPECIFIC(id, LOCKGUARD, std::lock_guard Lock__Guard__##mutexVariableName##__(mutexObtainer), osdev::components::mqtt::measure_locking_tag, osdev::components::mqtt::MeasureLockingValue, 100) \ + osdev::components::mqtt::apply_unused_parameters(Lock__Guard__##mutexVariableName##__); + +/** + * @brief Create a lockguard for a given recursive mutex in a specific context. + * @param mutexVariableName The name of the mutex. + * @param mutexObtainer The mutex to lock. This can also be a function that returns a mutex. + * If MEASURE_LOCKS is enabled this method uses a static histogram to record the timing of obtaining the lock for all instances. + */ +#define OSDEV_COMPONENTS_RECURSIVELOCKGUARD_OBTAINER(mutexVariableName, mutexObtainer) \ + OSDEV_COMPONENTS_MEASURELOCK(RECURSIVELOCKGUARD, std::lock_guard Lock__Guard__##mutexVariableName##__(mutexObtainer), osdev::components::mqtt::measure_locking_tag, osdev::components::mqtt::MeasureLockingValue, 100) \ + osdev::components::mqtt::apply_unused_parameters(Lock__Guard__##mutexVariableName##__); + +/** + * @brief Create a lockguard for a given recursive mutex in a specific context. + * @param mutexVariableName The name of the mutex. + * @param mutexObtainer The mutex to lock. This can also be a function that returns a mutex. + * @param id The id that identifies this specific lock guard. Used for measuring timings. + * If MEASURE_LOCKS is enabled this method uses a specific histogram instance to record the timing of obtaining this lock. + */ +#define OSDEV_COMPONENTS_RECURSIVELOCKGUARD_SPECIFIC_OBTAINER(mutexVariableName, mutexObtainer, id) \ + OSDEV_COMPONENTS_MEASURELOCK_SPECIFIC(id, RECURSIVELOCKGUARD, std::lock_guard Lock__Guard__##mutexVariableName##__(mutexVariableName), osdev::components::mqtt::measure_locking_tag, osdev::components::mqtt::MeasureLockingValue, 100) \ + osdev::components::mqtt::apply_unused_parameters(Lock__Guard__##mutexVariableName##__); + +/** + * @brief Create a lockguard for a given bare mutex in a specific context. + * @param mutexVariableName The name of the mutex. + * If MEASURE_LOCKS is enabled this method uses a static histogram to record the timing of obtaining the lock for all instances. + */ +#define OSDEV_COMPONENTS_LOCKGUARD(mutexVariableName) \ + OSDEV_COMPONENTS_LOCKGUARD_OBTAINER(mutexVariableName, mutexVariableName) + +/** + * @brief Create a lockguard for a given bare mutex in a specific context. + * @param mutexVariableName The name of the mutex. + * @param id The id that identifies this specific lock guard. Used for measuring timings. + * If MEASURE_LOCKS is enabled this method uses a specific histogram instance to record the timing of obtaining this lock. + */ +#define OSDEV_COMPONENTS_LOCKGUARD_SPECIFIC(mutexVariableName, id) \ + OSDEV_COMPONENTS_LOCKGUARD_SPECIFIC_OBTAINER(mutexVariableName, mutexVariableName, id) + +/** + * @brief Create a lockguard for a given bare recursive mutex in a specific context. + * @param mutexVariableName The name of the mutex. + * If MEASURE_LOCKS is enabled this method uses a static histogram to record the timing of obtaining the lock for all instances. + */ +#define OSDEV_COMPONENTS_RECURSIVELOCKGUARD(mutexVariableName) \ + OSDEV_COMPONENTS_RECURSIVELOCKGUARD_OBTAINER(mutexVariableName, mutexVariableName) + +/** + * @brief Create a lockguard for a given bare recursive mutex in a specific context. + * @param mutexVariableName The name of the mutex. + * @param id The id that identifies this specific lock guard. Used for measuring timings. + * If MEASURE_LOCKS is enabled this method uses a static histogram to record the timing of obtaining the lock for all instances. + */ +#define OSDEV_COMPONENTS_RECURSIVELOCKGUARD_SPECIFIC(mutexVariableName, id) \ + OSDEV_COMPONENTS_RECURSIVELOCKGUARD_SPECIFIC_OBTAINER(mutexVariableName, mutexVariableName, id) + +/** + * @brief Defines the lock name that is used by the OSDEV_COMPONENTS_UNIQUELOCK_* macros + */ +#define OSDEV_COMPONENTS_UNIQUELOCK(mutexVariableName) Unique__Lock__##mutexVariableName##__ + +/** + * @brief Create a uniqeue lock for a given mutex. + * @param mutexVariableName The name of the mutex. + * @param mutexObtainer The mutex to lock. This can also be a function that returns a mutex. + * If MEASURE_LOCKS is enabled this method uses a static histogram to record the timing of obtaining the lock for all instances. + */ +#define OSDEV_COMPONENTS_UNIQUELOCK_CREATE_OBTAINER(mutexVariableName, mutexObtainer) \ + OSDEV_COMPONENTS_MEASURELOCK(UNIQUELOCK_CREATE, std::unique_lock OSDEV_COMPONENTS_UNIQUELOCK(mutexVariableName)(mutexObtainer), osdev::components::mqtt::measure_locking_tag, osdev::components::mqtt::MeasureLockingValue, 100) \ + osdev::components::mqtt::apply_unused_parameters(OSDEV_COMPONENTS_UNIQUELOCK(mutexVariableName)); + +/** + * @brief Create a uniqeue lock for a given mutex. + * @param mutexVariableName The name of the mutex. + * @param mutexObtainer The mutex to lock. This can also be a function that returns mutex. + * @param id The id that identifies this specific lock guard. Used for measuring timings. + * If MEASURE_LOCKS is enabled this method uses a specific histogram instance to record the timing of obtaining this lock. + */ +#define OSDEV_COMPONENTS_UNIQUELOCK_CREATE_SPECIFIC_OBTAINER(mutexVariableName, mutexObtainer, id) \ + OSDEV_COMPONENTS_MEASURELOCK_SPECIFIC(id, UNIQUELOCK_CREATE, std::unique_lock OSDEV_COMPONENTS_UNIQUELOCK(mutexVariableName)(mutexObtainer), osdev::components::mqtt::measure_locking_tag, osdev::components::mqtt::MeasureLockingValue, 100) \ + osdev::components::mqtt::apply_unused_parameters(OSDEV_COMPONENTS_UNIQUELOCK(mutexVariableName)); + +/** + * @brief Create a uniqeue lock for a given bare mutex. + * @param mutexVariableName The name of the mutex. + * If MEASURE_LOCKS is enabled this method uses a static histogram to record the timing of obtaining the lock for all instances. + */ +#define OSDEV_COMPONENTS_UNIQUELOCK_CREATE(mutexVariableName) \ + OSDEV_COMPONENTS_UNIQUELOCK_CREATE_OBTAINER(mutexVariableName, mutexVariableName) + +/** + * @brief Create a uniqeue lock for a given bare mutex. + * @param mutexVariableName The name of the mutex. + * @param id The id that identifies this specific unique lock. Used for measuring timings. + * If MEASURE_LOCKS is enabled this method uses a specific histogram instance to record the timing of obtaining this lock. + */ +#define OSDEV_COMPONENTS_UNIQUELOCK_CREATE_SPECIFIC(mutexVariableName, id) \ + OSDEV_COMPONENTS_UNIQUELOCK_CREATE_SPECIFIC_OBTAINER(mutexVariableName, mutexVariableName, id) + +/** + * @brief Lock a given uniqeue lock. + * @param mutexVariableName The name of the mutex from which the lockname is derived. + * If MEASURE_LOCKS is enabled this method uses a static histogram to record the timing of obtaining the lock for all instances. + */ +#define OSDEV_COMPONENTS_UNIQUELOCK_LOCK(mutexVariableName) \ + OSDEV_COMPONENTS_MEASURELOCK(UNIQUELOCK_LOCK, OSDEV_COMPONENTS_UNIQUELOCK(mutexVariableName).lock(), osdev::components::mqtt::measure_locking_tag, osdev::components::mqtt::MeasureLockingValue, 100) + +/** + * @brief Lock a given uniqeue lock. + * @param mutexVariableName The name of the mutex from which the lockname is derived. + * @param id The id that identifies this specific unique lock guard. Used for measuring timings. + * If MEASURE_LOCKS is enabled this method uses a specific histogram instance to record the timing of obtaining this lock. + */ +#define OSDEV_COMPONENTS_UNIQUELOCK_LOCK_SPECIFIC(mutexVariableName, id) \ + OSDEV_COMPONENTS_MEASURELOCK_SPECIFIC(id, UNIQUELOCK_LOCK, OSDEV_COMPONENTS_UNIQUELOCK(mutexVariableName).lock(), osdev::components::mqtt::measure_locking_tag, osdev::components::mqtt::MeasureLockingValue, 100) + +/** + * @brief Unlock a given uniqeue lock. + * @param mutexVariableName The name of the mutex from which the lockname is derived. + */ +#define OSDEV_COMPONENTS_UNIQUELOCK_UNLOCK(mutexVariableName) \ + OSDEV_COMPONENTS_UNIQUELOCK(mutexVariableName).unlock(); + +/** + * @brief Unlock a given uniqeue lock. + * @param mutexVariableName The name of the mutex from which the lockname is derived. + * @param id The id that identifies this specific unique lock guard. Can be used for measuring timings. + */ +#define OSDEV_COMPONENTS_UNIQUELOCK_UNLOCK_SPECIFIC(mutexVariableName, id) \ + OSDEV_COMPONENTS_UNIQUELOCK(mutexVariableName).unlock(); + +namespace osdev { +namespace components { +namespace mqtt { + +/** + * @brief Context tag type. + */ +struct measure_locking_tag +{ +}; + +/** + * @brief Type for measuring lock timings + * The unit is microseconds and the values are expected between 0 and 100 microseconds. + * This type is used to construct a timing histogram. + */ +struct MeasureLockingValue +{ + /** + * @brief The value type of the timing value. + */ + using value_type = std::chrono::microseconds; + + const value_type minValue = value_type(0); ///< Constant mininum value. + const value_type maxValue = value_type(100); ///< Constant maximum value. + + const char* unit = "us"; ///< The value unit. +}; + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_LOCKGUARD_H diff --git b/src/macrodefs.h a/src/macrodefs.h new file mode 100644 index 0000000..890e673 --- /dev/null +++ a/src/macrodefs.h @@ -0,0 +1,50 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#ifndef OSDEV_COMPONENTS_MQTT_MACRODEFS_H +#define OSDEV_COMPONENTS_MQTT_MACRODEFS_H + +#include + +#include "compiletimestring.h" + +/// @brief Helper macro to stringify a symbol +#define OSDEV_COMPONENTS_STRINGIFY(x) #x + +/// @brief Use this macro to safely stringify a symbol +/// This will also work for nested macro's +#define OSDEV_COMPONENTS_TOSTRING(x) OSDEV_COMPONENTS_STRINGIFY(x) + +/// @brief Helper macro to combine two symbols +#define OSDEV_COMPONENTS_COMBINER(x, y) x##y + +/// @brief Use this macro to safely combine two symbols +/// This will also work for nested macro's +#define OSDEV_COMPONENTS_COMBINE(x, y) OSDEV_COMPONENTS_COMBINER(x, y) + +/// @brief Macro that reduces a path to the basename +#define OSDEV_COMPONENTS_MANAGEDBASEFILENAME \ + OSDEV_COMPONENTS_CSTRING_BOUNDED(__FILE__, osdev::components::rfind(__FILE__, '/') + 1, sizeof(__FILE__) - 1) + +/// @brief Compiletime test if an instance derives from a certain base class +#define OSDEV_COMPONENTS_DERIVESFROM(derived, baseType) \ + static_assert(std::is_base_of::type>::type>::value, \ + OSDEV_COMPONENTS_TOSTRING(derived) " must be derived from " OSDEV_COMPONENTS_TOSTRING(baseType)) + +#endif //OSDEV_COMPONENTS_MQTT_MACRODEFS_H diff --git b/src/measure.h a/src/measure.h new file mode 100644 index 0000000..14350fb --- /dev/null +++ a/src/measure.h @@ -0,0 +1,155 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#ifndef OSDEV_COMPONENTS_MQTT_MEASUREMENT_MEASURE_H +#define OSDEV_COMPONENTS_MQTT_MEASUREMENT_MEASURE_H + +#include "macrodefs.h" +#include "histogramprovider.h" +#include "timemeasurement.h" + +namespace osdev { +namespace components { +namespace mqtt { +namespace measurement { + +inline std::string createId(const std::string& operationName, const std::string& dynamicId) +{ + return operationName + (dynamicId.empty() ? std::string{} : std::string(" ") + dynamicId); +} + +} // End namespace measurement +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#define OSDEV_COMPONENTS_MEASUREMENT_LOCATION \ + (OSDEV_COMPONENTS_MANAGEDBASEFILENAME + OSDEV_COMPONENTS_CSTRING(":" OSDEV_COMPONENTS_TOSTRING(__LINE__))).chars + +/** + * @brief Macro helper that sets up a TimeMeasurement on an operation. + * The operation must have observable side effects otherwise the compiler might move the operation from in between the time measurements. + * @param operationName The name of the operation. This is part of the id under which the measurements are registered. + * @param dynamicId The dynamic identification of this measurement. This id makes different instances of the same operation unique. + * The dynamicId must be an expression that yields a string like object. The dynamic id is part of the id under which the + * measurements are registered. + * @param operation The operation to perform. + * @param context The context tag used to identify the HistogramProvider. + * @param valueType A datatype that can be used by HistogramProvider. + * @param numBuckets The number of buckets to use for the histogram. + * @param boolOnDestroy Boolean value that makes the measurement measure on destruction. + */ +#define OSDEV_COMPONENTS_MEASUREMENT_MEASURE_HELPER(operationName, dynamicId, operation, context, valueType, numBuckets, boolOnDestroy) \ + static const std::string OSDEV_COMPONENTS_COMBINE(OSDEV_COMPONENTS_COMBINE(ID__, __LINE__), __) = \ + std::string(OSDEV_COMPONENTS_CSTRING(#operationName).chars); \ + osdev::components::mqtt::measurement::TimeMeasurement OSDEV_COMPONENTS_COMBINE( \ + OSDEV_COMPONENTS_COMBINE(TM__, __LINE__), __)( \ + osdev::components::mqtt::measurement::createId(OSDEV_COMPONENTS_COMBINE(OSDEV_COMPONENTS_COMBINE(ID__, __LINE__), __), dynamicId), \ + [](const std::string& id, std::chrono::steady_clock::time_point, std::chrono::microseconds, std::chrono::microseconds sinceLast) { \ + osdev::components::mqtt::measurement::HistogramProvider::instance().addValue(id, sinceLast); \ + }, \ + boolOnDestroy); \ + operation; + +/** + * @brief Make a measurement before and after an operation. + * The operation must have observable side effects otherwise the compiler might move the operation from in between the time measurements. + * @param operationName The name under which the measurements are registered. + * @param operation The operation to perform. + * @param context The context tag used to identify the HistogramProvider. + * @param valueType A datatype that can be used by HistogramProvider. + * @param numBuckets The number of buckets to use for the histogram. + */ +#define OSDEV_COMPONENTS_MEASUREMENT_MEASURE(operationName, operation, context, valueType, numBuckets) \ + OSDEV_COMPONENTS_MEASUREMENT_MEASURE_HELPER(operationName, OSDEV_COMPONENTS_MEASUREMENT_LOCATION, operation, context, valueType, numBuckets, false) \ + OSDEV_COMPONENTS_COMBINE(OSDEV_COMPONENTS_COMBINE(TM__, __LINE__), __).measure(); + +/** + * @brief Make a measurement on a return operation. + * The operation must have observable side effects otherwise the compiler might move the operation from in between the time measurements. + * @param operationName The name under which the measurements are registered. + * @param operation The operation to perform. + * @param context The context tag used to identify the HistogramProvider. + * @param valueType A datatype that can be used by HistogramProvider. + * @param numBuckets The number of buckets to use for the histogram. + */ +#define OSDEV_COMPONENTS_MEASUREMENT_MEASURE_ONDESTROY(operationName, operation, context, valueType, numBuckets) \ + OSDEV_COMPONENTS_MEASUREMENT_MEASURE_HELPER(operationName, OSDEV_COMPONENTS_MEASUREMENT_LOCATION, operation, context, valueType, numBuckets, true) + +/** + * @brief Make a measurement on a return operation. + * The operation must have observable side effects otherwise the compiler might move the operation from in between the time measurements. + * @param operationName The name under which the measurements are registered. + * @param dynamicId An extra identification to discern between instances of the same operation. The dynamicId must be an expression that + * yields a string like object. + * @param operation The operation to perform. + * @param context The context tag used to identify the HistogramProvider. + * @param valueType A datatype that can be used by HistogramProvider. + * @param numBuckets The number of buckets to use for the histogram. + */ +#define OSDEV_COMPONENTS_MEASUREMENT_MEASURE_SPECIFIC_ONDESTROY(operationName, dynamicId, operation, context, valueType, numBuckets) \ + OSDEV_COMPONENTS_MEASUREMENT_MEASURE_HELPER(operationName, dynamicId, operation, context, valueType, numBuckets, true) + +/** + * @brief Nop version that only performs the operation. + */ +#define OSDEV_COMPONENTS_MEASUREMENT_MEASURE_ONDESTROY_NOP(operationName, operation, context, valueType, numBuckets) operation; + +/** + * @brief Nop version that only performs the operation. + */ +#define OSDEV_COMPONENTS_MEASUREMENT_MEASURE_NOP(operationName, operation, context, valueType, numBuckets) operation; + +/** + * @brief Nop version that only performs the operation. + */ +#define OSDEV_COMPONENTS_MEASUREMENT_MEASURE_SPECIFIC_ONDESTROY_NOP(operationName, dynamicId, operation, context, valueType, numBuckets) operation; + +/** + * @brief Make a measurement before and after an operation for a specific instance of the operation. + * The operation must have observable side effects otherwise the compiler might move the operation from in between the time measurements. + * @param dynamicId An extra identification to discern between instances of the same operation. + * @param operationName The name under which the measurements are registered. + * @param operation The operation to perform. + * @param context The context tag used to identify the HistogramProvider. + * @param valueType A datatype that can be used by HistogramProvider. + * @param numBuckets The number of buckets to use for the histogram. + */ +#define OSDEV_COMPONENTS_MEASUREMENT_MEASURE_SPECIFIC(dynamicId, operationName, operation, context, valueType, numBuckets) \ + auto OSDEV_COMPONENTS_COMBINE(OSDEV_COMPONENTS_COMBINE(IDPREFIX__, __LINE__), __) = \ + OSDEV_COMPONENTS_MANAGEDBASEFILENAME + OSDEV_COMPONENTS_CSTRING(":" OSDEV_COMPONENTS_TOSTRING(__LINE__) ", function "); \ + auto OSDEV_COMPONENTS_COMBINE(OSDEV_COMPONENTS_COMBINE(IDPOSTFIX__, __LINE__), __) = OSDEV_COMPONENTS_CSTRING(", " #operationName " "); \ + static const std::string OSDEV_COMPONENTS_COMBINE(OSDEV_COMPONENTS_COMBINE(ID__, __LINE__), __) = \ + std::string(OSDEV_COMPONENTS_COMBINE(OSDEV_COMPONENTS_COMBINE(IDPREFIX__, __LINE__), __).chars) + \ + std::string(__func__) + \ + OSDEV_COMPONENTS_COMBINE(OSDEV_COMPONENTS_COMBINE(IDPOSTFIX__, __LINE__), __).chars; \ + osdev::components::mqtt::measurement::TimeMeasurement OSDEV_COMPONENTS_COMBINE(OSDEV_COMPONENTS_COMBINE(TM__, __LINE__), __)( \ + OSDEV_COMPONENTS_COMBINE(OSDEV_COMPONENTS_COMBINE(ID__, __LINE__), __) + dynamicId, \ + [](const std::string& id, std::chrono::steady_clock::time_point, std::chrono::microseconds, std::chrono::microseconds sinceLast) { \ + osdev::components::mqtt::measurement::HistogramProvider::instance().addValue(id, sinceLast); \ + }, \ + false); \ + operation; \ + OSDEV_COMPONENTS_COMBINE(OSDEV_COMPONENTS_COMBINE(TM__, __LINE__), __).measure(); + +/** + * @brief Nop version that only performs the operation. + */ +#define OSDEV_COMPONENTS_MEASUREMENT_MEASURE_SPECIFIC_NOP(dynamicId, operationName, operation, context, valueType, numBuckets) operation; + +#endif // OSDEV_COMPONENTS_MEASUREMENT_MEASUREMENT_H diff --git b/src/metaprogrammingdefs.h a/src/metaprogrammingdefs.h new file mode 100644 index 0000000..c7a378c --- /dev/null +++ a/src/metaprogrammingdefs.h @@ -0,0 +1,183 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#ifndef OSDEV_COMPONENTS_MQTT_METAPROGRAMMINGDEFS_H +#define OSDEV_COMPONENTS_MQTT_METAPROGRAMMINGDEFS_H + +#include // for std::size_t +#include +#include + +#include "utils.h" + +/** + * @brief Create a type trait that checks for the existence of a member in a struct. + * @param memberName The member name + * + * A call such as OSDEV_COMPONENTS_HASMEMBER_TRAIT(onSuccess) leads to a type trait has_onSuccess. + */ +#define OSDEV_COMPONENTS_HASMEMBER_TRAIT(memberName) \ + template \ + struct has_##memberName : public std::false_type \ + { \ + }; \ + \ + template \ + struct has_##memberName().memberName)>> : public std::true_type \ + { \ + }; + +/** + * @brief Create a type trait that checks for the existence of a member method with no parameters. + * @param methodName The method name + * @param postfix An extra postfix that is added to the type trait struct. + * This makes it possible create type traits that check for + * overloaded methods. + * + * A call such as OSDEV_COMPONENTS_HASMETHOD_TRAIT(capacity,1) leads to a type trait has_capacity1. + */ +#define OSDEV_COMPONENTS_HASMETHOD_TRAIT(methodName, postfix) \ + template \ + struct has_##methodName##postfix : public std::false_type \ + { \ + }; \ + \ + template \ + struct has_##methodName##postfix().methodName())>> : public std::true_type \ + { \ + }; + +/** + * @brief Create a type trait that checks for the existence of a member method with 1 or more parameters. + * @param methodName The method name + * @param postfix An extra postfix that is added to the type trait struct. + * This makes it possible create type traits that check for + * overloaded methods. + * @param ... Variable number of arguments. These can be values, but also declval constructs such as std::declval(). + * + * A call such as OSDEV_COMPONENTS_HASMETHODWP_TRAIT(capacity,,"string") leads to a type trait has_capacity that + * checks for the existence of a method capacity that accepts a const char pointer. + */ +#define OSDEV_COMPONENTS_HASMETHODWP_TRAIT(methodName, postfix, ...) \ + template \ + struct has_##methodName##postfix : public std::false_type \ + { \ + }; \ + \ + template \ + struct has_##methodName##postfix().methodName(__VA_ARGS__))>> : public std::true_type \ + { \ + }; + +namespace osdev { +namespace components { +namespace mqtt { + +/** + * @brief Used to detect ill formed types in a SFINAE context. + * If the parameter pack Ts contains an invalid type, struct make_void cannot be expanded. + */ +template +struct make_void +{ + using type = void; +}; + +/** + * @brief Introduced in c++17 (but will also work in c++11) + */ +template +using void_t = typename make_void::type; + +/// @brief Build an index list that can be used to traverse another list (f.i. a char array). +/// Recursive definition. +/// Start with an empty indices list. Build the indices list recursively. +/// upper-1, indices... becomes the indices... in the next recursive call. +template class meta_functor, std::size_t... Is> +struct apply_bounded_range +{ + typedef typename apply_bounded_range::result result; +}; + +/// @brief Terminator of apply_bounded_range. +/// Terminates when the upper bound (which runs) is equal to the lower bound. +template class meta_functor, std::size_t... Is> +struct apply_bounded_range +{ + typedef typename meta_functor::result result; +}; + +/** + * @brief Helper method to perform static_assert on the expected count of a parameter pack. + */ +template +void static_assert_count() +{ + constexpr std::size_t actualCount = sizeof...(Args); + static_assert(expectedCount == actualCount, "Unexpected parameter count."); +} + +/** + * @brief Helper method to convert a type to std::string. + * @param d_first The output iterator to write the converted string to. + * @param arg The argument to convert to std::string. + */ +template +int apply_to_string_one(OutputIt d_first, const T& arg) +{ + std::ostringstream ss; + ss << arg; + *d_first++ = ss.str(); + return 0; +} + +/** + * @brief Converts all parameters to std::string. + * @param d_first The output iterator to write the converted strings to. + * @param args The arguments to convert to std::string. + */ +template +void apply_to_string(OutputIt d_first, Args&&... args) +{ + // d_first is not used when args parameter pack is empty. + apply_unused_parameters(d_first); + + // We need to use the expand_type trick in order to be able to use an initializer list + // so that we can call the method for every variadic argument + using expand_type = int[]; + // Array must be initialized with values. Add a single value so that the array can be initialized when the parameter pack is empty. + expand_type et{ 0, apply_to_string_one(d_first, std::forward(args))... }; + apply_unused_parameters(et); +} + +/** + * @brief Helper that removes const, volatile and reference from a type. + * @note Defined in std C++20. + */ +template +struct remove_cvref +{ + using type = typename std::remove_cv::type>::type; +}; + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_METAPROGRAMMINGDEFS_H diff --git b/src/mqttclient.cpp a/src/mqttclient.cpp new file mode 100644 index 0000000..0314e07 --- /dev/null +++ a/src/mqttclient.cpp @@ -0,0 +1,544 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "mqttclient.h" + +// osdev::components::mqtt +#include "clientpaho.h" +#include "mqttutil.h" +#include "mqttidgenerator.h" +#include "mqtttypeconverter.h" + +// mlogic::common +#include "lockguard.h" +#include "uriparser.h" + +// std +#include +#include + +using namespace osdev::components::mqtt; + +namespace { +/** + * @brief Generate a unique client id so that a new client does not steal the connection from an existing client. + * @param clientId The base client identifier. + * @param clientNumber The next client that is derived from the base client identifier. + * @return A unique client identifier string. + */ +std::string generateUniqueClientId(const std::string& clientId, std::size_t clientNumber) +{ + return clientId + "_" + std::to_string(clientNumber) + "_" + MqttTypeConverter::toStdString(MqttIdGenerator::generate()); +} + +} // namespace + +MqttClient::MqttClient(const std::string& _clientId, const std::function& deliveryCompleteCallback) + : m_interfaceMutex() + , m_internalMutex() + , m_endpoint() + , m_clientId(_clientId) + , m_activeTokens() + , m_activeTokensCV() + , m_deliveryCompleteCallback(deliveryCompleteCallback) + , m_serverState(this) + , m_principalClient() + , m_additionalClients() + , m_eventQueue(_clientId) + , m_workerThread(std::thread(&MqttClient::eventHandler, this)) +{ +} + +MqttClient::~MqttClient() +{ + // DebugLogToFIle ("MqttClient", "%1 - dtor", m_clientId); + { + // DebugLogToFIle ("MqttClient", "%1 - disconnect", m_clientId); + this->disconnect(); + decltype(m_principalClient) principalClient{}; + + OSDEV_COMPONENTS_LOCKGUARD(m_internalMutex); + // DebugLogToFIle ("MqttClient", "%1 - cleanup principal client", m_clientId); + m_principalClient.swap(principalClient); + } + // DebugLogToFIle ("MqttClient", "%1 - dtor stop queue", m_clientId); + m_eventQueue.stop(); + if (m_workerThread.joinable()) { + m_workerThread.join(); + } + // DebugLogToFIle ("MqttClient", "%1 - dtor ready", m_clientId); +} + +std::string MqttClient::clientId() const +{ + return m_clientId; +} + +StateChangeCallbackHandle MqttClient::registerStateChangeCallback(const SlotStateChange& cb) +{ + OSDEV_COMPONENTS_LOCKGUARD(m_interfaceMutex); + OSDEV_COMPONENTS_LOCKGUARD(m_internalMutex); + return m_serverState.registerStateChangeCallback(cb); +} + +void MqttClient::unregisterStateChangeCallback(StateChangeCallbackHandle handle) +{ + OSDEV_COMPONENTS_LOCKGUARD(m_interfaceMutex); + OSDEV_COMPONENTS_LOCKGUARD(m_internalMutex); + m_serverState.unregisterStateChangeCallback(handle); +} + +void MqttClient::clearAllStateChangeCallbacks() +{ + OSDEV_COMPONENTS_LOCKGUARD(m_interfaceMutex); + OSDEV_COMPONENTS_LOCKGUARD(m_internalMutex); + m_serverState.clearAllStateChangeCallbacks(); +} + +StateEnum MqttClient::state() const +{ + OSDEV_COMPONENTS_LOCKGUARD(m_interfaceMutex); + OSDEV_COMPONENTS_LOCKGUARD(m_internalMutex); + return m_serverState.state(); +} + +void MqttClient::connect(const std::string& host, int port, const Credentials& credentials) +{ + + osdev::components::mqtt::ParsedUri _endpoint = { + { "scheme", "tcp" }, + { "user", credentials.username() }, + { "password", credentials.password() }, + { "host", host }, + { "port", std::to_string(port) } + }; + this->connect(UriParser::toString(_endpoint)); +} + +void MqttClient::connect(const std::string& _endpoint) +{ + // InfoLogToFIle ("MqttClient", "%1 - Request connect", m_clientId); + OSDEV_COMPONENTS_LOCKGUARD(m_interfaceMutex); + IMqttClientImpl* client(nullptr); + { + OSDEV_COMPONENTS_LOCKGUARD(m_internalMutex); + if (m_principalClient && m_principalClient->connectionStatus() != ConnectionStatus::Disconnected) { + if (_endpoint == m_endpoint) + { + // idempotent + return; + } + else + { + // ErrorLogToFIle ("MqttClient", "%1 - Cannot connect to different endpoint. Disconnect first.", m_clientId); + // Normally a throw (Yuck!) (MqttException, "Cannot connect while already connected"); + } + } + m_endpoint = _endpoint; + if (!m_principalClient) { + std::string derivedClientId(generateUniqueClientId(m_clientId, 1)); + m_principalClient = std::make_unique( + m_endpoint, + derivedClientId, + [this](const std::string& id, ConnectionStatus cs) { this->connectionStatusChanged(id, cs); }, + [this](std::string clientId, std::int32_t token) { this->deliveryComplete(clientId, token); }); + } + client = m_principalClient.get(); + } + client->connect(true); +} + +void MqttClient::disconnect() +{ + // InfoLogToFIle ("MqttClient", "%1 - Request disconnect", m_clientId); + OSDEV_COMPONENTS_LOCKGUARD(m_interfaceMutex); + + decltype(m_additionalClients) additionalClients{}; + std::vector clients{}; + { + OSDEV_COMPONENTS_LOCKGUARD(m_internalMutex); + if (!m_principalClient || m_principalClient->connectionStatus() == ConnectionStatus::Disconnected || m_principalClient->connectionStatus() == ConnectionStatus::DisconnectInProgress) { + return; + } + m_additionalClients.swap(additionalClients); + + for (const auto& c : additionalClients) { + clients.push_back(c.get()); + } + clients.push_back(m_principalClient.get()); + } + + // DebugLogToFIle ("MqttClient", "%1 - Unsubscribe and disconnect clients", m_clientId); + for (auto& cl : clients) { + cl->unsubscribeAll(); + } + this->waitForCompletionInternal(clients, std::chrono::milliseconds(2000), std::set{}); + + for (auto& cl : clients) { + cl->disconnect(false, 2000); + } + this->waitForCompletionInternal(clients, std::chrono::milliseconds(3000), std::set{}); +} + +Token MqttClient::publish(const MqttMessage& message, int qos) +{ + if (hasWildcard(message.topic()) || !isValidTopic(message.topic())) { + return Token(m_clientId, -1); + } + OSDEV_COMPONENTS_LOCKGUARD(m_interfaceMutex); + IMqttClientImpl* client(nullptr); + { + OSDEV_COMPONENTS_LOCKGUARD(m_internalMutex); + if (!m_principalClient || m_principalClient->connectionStatus() == ConnectionStatus::Disconnected) + { + if( !m_principalClient ) + { + std::cout << "Principal client not initialized" << std::endl; + } + + if( m_principalClient->connectionStatus() == ConnectionStatus::Disconnected ) + { + std::cout << "Unable to publish, not connected.." << std::endl; + } + // ErrorLogToFIle ("MqttClient", "%1 - Unable to publish, not connected", m_clientId); + // Throw (MqttException, "Not connected"); + } + client = m_principalClient.get(); + } + return Token(client->clientId(), client->publish(message, qos)); +} + +Token MqttClient::subscribe(const std::string& topic, int qos, const std::function& cb) +{ + // DebugLogToFIle ("MqttClient", "%1 - Subscribe to topic %2 with qos %3", m_clientId, topic, qos); + // OSDEV_COMPONENTS_LOCKGUARD(m_interfaceMutex); + bool clientFound = false; + IMqttClientImpl* client(nullptr); + { + // OSDEV_COMPONENTS_LOCKGUARD(m_internalMutex); + if (!m_principalClient || m_principalClient->connectionStatus() == ConnectionStatus::Disconnected) + { + // ErrorLogToFIle ("MqttClient", "%1 - Unable to subscribe, not connected", m_clientId); + // throw (?)(MqttException, "Not connected"); + } + if (!m_principalClient->isOverlapping(topic)) { + client = m_principalClient.get(); + clientFound = true; + } + else { + for (const auto& c : m_additionalClients) { + if (!c->isOverlapping(topic)) { + client = c.get(); + clientFound = true; + break; + } + } + } + if (!clientFound) { + // DebugLogToFIle ("MqttClient", "%1 - Creating new ClientPaho instance for subscription on topic %2", m_clientId, topic); + std::string derivedClientId(generateUniqueClientId(m_clientId, m_additionalClients.size() + 2)); // principal client is nr 1. + m_additionalClients.emplace_back(std::make_unique( + m_endpoint, + derivedClientId, + [this](const std::string& id, ConnectionStatus cs) { this->connectionStatusChanged(id, cs); }, + std::function{})); + client = m_additionalClients.back().get(); + } + } + if (!clientFound) { + client->connect(true); + } + return Token{ client->clientId(), client->subscribe(topic, qos, cb) }; +} + +std::set MqttClient::unsubscribe(const std::string& topic, int qos) +{ + // DebugLogToFIle ("MqttClient", "%1 - Unsubscribe from topic %2 with qos %3", m_clientId, topic, qos); + OSDEV_COMPONENTS_LOCKGUARD(m_interfaceMutex); + std::vector clients{}; + { + OSDEV_COMPONENTS_LOCKGUARD(m_internalMutex); + if (!m_principalClient || m_principalClient->connectionStatus() == ConnectionStatus::Disconnected) { + // ErrorLogToFIle ("MqttClient", "%1 - Unable to unsubscribe, not connected", m_clientId); + // Throw (MqttException, "Not connected"); + } + clients.push_back(m_principalClient.get()); + for (const auto& c : m_additionalClients) { + clients.push_back(c.get()); + } + } + std::set tokens{}; + for (auto* c : clients) { + auto token = c->unsubscribe(topic, qos); + if (-1 != token) { + tokens.emplace(Token{ c->clientId(), token }); + } + } + return tokens; +} + +bool MqttClient::waitForCompletion(std::chrono::milliseconds waitFor) const +{ + return this->waitForCompletion(waitFor, std::set{}); +} + +bool MqttClient::waitForCompletion(std::chrono::milliseconds waitFor, const Token& token) const +{ + if (-1 == token.token()) { + return false; + } + return this->waitForCompletion(waitFor, std::set{ token }); +} + +bool MqttClient::waitForCompletion(std::chrono::milliseconds waitFor, const std::set& tokens) const +{ + OSDEV_COMPONENTS_LOCKGUARD(m_interfaceMutex); + std::vector clients{}; + { + OSDEV_COMPONENTS_LOCKGUARD(m_internalMutex); + if (m_principalClient) { + clients.push_back(m_principalClient.get()); + } + for (const auto& c : m_additionalClients) { + clients.push_back(c.get()); + } + } + return waitForCompletionInternal(clients, waitFor, tokens); +} + +boost::optional MqttClient::commandResult(const Token& token) const +{ + OSDEV_COMPONENTS_LOCKGUARD(m_interfaceMutex); + std::vector clients{}; + { + OSDEV_COMPONENTS_LOCKGUARD(m_internalMutex); + if (m_principalClient) { + clients.push_back(m_principalClient.get()); + } + for (const auto& c : m_additionalClients) { + clients.push_back(c.get()); + } + } + for (auto* c : clients) { + if (token.clientId() == c->clientId()) { + return c->operationResult(token.token()); + } + } + return boost::optional{}; +} + +std::string MqttClient::endpoint() const +{ + auto ep = UriParser::parse(m_endpoint); + if (ep.find("user") != ep.end()) { + ep["user"].clear(); + } + if (ep.find("password") != ep.end()) { + ep["password"].clear(); + } + return UriParser::toString(ep); +} + +void MqttClient::connectionStatusChanged(const std::string& id, ConnectionStatus cs) +{ + (void)id; + (void)cs; + // DebugLogToFIle ("MqttClient", "%1 - connection status of wrapped client %2 changed to %3", m_clientId, id, cs); + IMqttClientImpl* principalClient{ nullptr }; + std::vector clients{}; + std::vector connectionStates{}; + { + OSDEV_COMPONENTS_LOCKGUARD(m_internalMutex); + if (!m_principalClient) { + return; + } + if (m_principalClient) { + principalClient = m_principalClient.get(); + clients.push_back(principalClient); + connectionStates.push_back(m_principalClient->connectionStatus()); + } + for (const auto& c : m_additionalClients) { + clients.push_back(c.get()); + connectionStates.push_back(c->connectionStatus()); + } + } + auto newState = determineState(connectionStates); + bool resubscribe = (StateEnum::ConnectionFailure == m_serverState.state() && StateEnum::Good == newState); + if (resubscribe) { + { + OSDEV_COMPONENTS_LOCKGUARD(m_internalMutex); + m_activeTokens.clear(); + } + for (auto* cl : clients) { + try { + cl->resubscribe(); + } + catch (const std::exception& e) { + // ErrorLogToFIle ("MqttClient", "%1 - resubscribe on wrapped client %2 in context of connection status change in wrapped client %3 failed : %4", m_clientId, cl->clientId(), id, e.what()); + } + } + m_activeTokensCV.notify_all(); + } + + // The server state change and a possible resubscription are done in the context of the MqttClient worker thread + // The wrapper is free to pick up new work such as the acknowledment of the just recreated subscriptions. + this->pushEvent([this, resubscribe, clients, principalClient, newState]() { + if (resubscribe) { + // Just wait for the subscription commands to complete. We do not use waitForCompletionInternal because that call will always timeout when there are active tokens. + // Active tokens are removed typically by work done on the worker thread. The wait action is also performed on the worker thread. + auto waitFor = std::chrono::milliseconds(1000); + if (!waitForCompletionInternalClients(clients, waitFor, std::set{})) { + if (std::accumulate(clients.begin(), clients.end(), false, [](bool hasPending, IMqttClientImpl* client) { return hasPending || client->hasPendingSubscriptions(); })) { + // WarnLogToFIle ("MqttClient", "%1 - subscriptions are not recovered within timeout.", m_clientId) + } + } + if (principalClient) { + try { + principalClient->publishPending(); + } + catch (const std::exception& e) { + // ErrorLogToFIle ("MqttClient", "%1 - publishPending on wrapped client %2 failed : %3", m_clientId, principalClient->clientId(), e.what()); + } + } + } + m_serverState.emitStateChanged(newState); + }); +} + +void MqttClient::deliveryComplete(const std::string& _clientId, std::int32_t token) +{ + if (!m_deliveryCompleteCallback) { + return; + } + + Token t(_clientId, token); + { + OSDEV_COMPONENTS_LOCKGUARD(m_internalMutex); + if (!m_activeTokens.insert(t).second) { + // This should not happen. This means that some callback on the wrapper never came. + // ErrorLogToFIle ("MqttClient", "%1 -deliveryComplete, token %1 is already active", m_clientId, t); + } + } + this->pushEvent([this, t]() { + OSDEV_COMPONENTS_SCOPEGUARD(m_activeTokens, [this, &t]() { + { + OSDEV_COMPONENTS_LOCKGUARD(m_internalMutex); + m_activeTokens.erase(t); + } + m_activeTokensCV.notify_all(); + }); + m_deliveryCompleteCallback(t); + }); +} + +bool MqttClient::waitForCompletionInternal(const std::vector& clients, std::chrono::milliseconds waitFor, const std::set& tokens) const +{ + if (!waitForCompletionInternalClients(clients, waitFor, tokens)) { + return false; + } + std::unique_lock lck(m_internalMutex); + return m_activeTokensCV.wait_for(lck, waitFor, [this, &tokens]() { + if (tokens.empty()) { // wait for all operations to end + return m_activeTokens.empty(); + } + else if (tokens.size() == 1) { + return m_activeTokens.find(*tokens.cbegin()) == m_activeTokens.end(); + } + std::vector intersect{}; + std::set_intersection(m_activeTokens.begin(), m_activeTokens.end(), tokens.begin(), tokens.end(), std::back_inserter(intersect)); + return intersect.empty(); }); +} + +bool MqttClient::waitForCompletionInternalClients(const std::vector& clients, std::chrono::milliseconds& waitFor, const std::set& tokens) const +{ + for (auto* c : clients) { + std::set clientTokens{}; + for (const auto& token : tokens) { + if (c->clientId() == token.clientId()) { + clientTokens.insert(token.token()); + } + } + if (tokens.empty() || !clientTokens.empty()) { + waitFor -= c->waitForCompletion(waitFor, clientTokens); + } + } + if (waitFor > std::chrono::milliseconds(0)) { + return true; + } + waitFor = std::chrono::milliseconds(0); + return false; +} + +StateEnum MqttClient::determineState(const std::vector& connectionStates) +{ + std::size_t unknownStates = 0; + std::size_t goodStates = 0; + std::size_t reconnectStates = 0; + for (auto cst : connectionStates) { + switch (cst) { + case ConnectionStatus::Disconnected: + ++unknownStates; + break; + case ConnectionStatus::DisconnectInProgress: // count as unknown because we don't want resubscribes to trigger when a wrapper is in this state. + ++unknownStates; + break; + case ConnectionStatus::ConnectInProgress: // count as unknown because the wrapper is not connected yet. + ++unknownStates; + break; + case ConnectionStatus::ReconnectInProgress: + ++reconnectStates; + break; + case ConnectionStatus::Connected: + ++goodStates; + break; + } + } + auto newState = StateEnum::Unknown; + if (reconnectStates > 0) { + newState = StateEnum::ConnectionFailure; + } + else if (unknownStates > 0) { + newState = StateEnum::Unknown; + } + else if (connectionStates.size() == goodStates) { + newState = StateEnum::Good; + } + return newState; +} + +void MqttClient::pushEvent(std::function ev) +{ + m_eventQueue.push(ev); +} + +void MqttClient::eventHandler() +{ + // InfoLogToFIle ("MqttClient", "%1 - starting event handler", m_clientId); + for (;;) { + std::vector> events; + if (!m_eventQueue.pop(events)) + { + break; + } + for (const auto& ev : events) + { + ev(); + } + } + // InfoLogToFIle ("MqttClient", "%1 - leaving event handler", m_clientId); +} diff --git b/src/mqttclient.h a/src/mqttclient.h new file mode 100644 index 0000000..ac183c1 --- /dev/null +++ a/src/mqttclient.h @@ -0,0 +1,212 @@ +#ifndef OSDEV_COMPONENTS_MQTT_MQTTCLIENT_H +#define OSDEV_COMPONENTS_MQTT_MQTTCLIENT_H + +// std +#include +#include +#include +#include +#include +#include + +// osdev::components::mqtt +#include "synchronizedqueue.h" +#include "istatecallback.h" +#include "serverstate.h" + +#include "imqttclient.h" + +namespace osdev { +namespace components { +namespace mqtt { + +// Forward definition +class IMqttClientImpl; + +class MqttClient : public virtual IMqttClient +{ +public: + /*! + * \brief Construct an instance of the MqttClient. + * \param clientId The client identification used in the connection to the mqtt broker. + * \param deliveryCompleteCallback Optional callback used to signal completion of a publication. + */ + MqttClient( const std::string& clientId, const std::function& deliveryCompleteCallback = std::function{}); + virtual ~MqttClient() override; + + // Non copyable, non movable + MqttClient(const MqttClient&) = delete; + MqttClient& operator=(const MqttClient&) = delete; + MqttClient(MqttClient&&) = delete; + MqttClient& operator=(MqttClient&&) = delete; + + /** + * @see IStateCallback + */ + virtual std::string clientId() const override; + + /** + * @see IStateCallback + */ + virtual StateChangeCallbackHandle registerStateChangeCallback(const SlotStateChange& cb) override; + + /** + * @see IStateCallback + */ + virtual void unregisterStateChangeCallback(StateChangeCallbackHandle handle) override; + + /** + * @see IStateCallback + */ + virtual void clearAllStateChangeCallbacks() override; + + /** + * @see IStateCallback + */ + virtual StateEnum state() const override; + + // MqttClient interface + + /** + * @see IMqttClient + */ + virtual void connect(const std::string& host, int port, const Credentials& credentials) override; + + /** + * @see IMqttClient + */ + virtual void connect(const std::string& endpoint) override; + + /** + * @see IMqttClient + */ + virtual void disconnect() override; + + /** + * @see IMqttClient + */ + virtual Token publish(const MqttMessage& message, int qos) override; + + /** + * @see IMqttClient + * When an overlapping subscription is detected a new connection has to be created. This is a synchronous action + * and this method will wait for the connection to be up. + */ + virtual Token subscribe(const std::string& topic, int qos, const std::function& cb) override; + + /** + * @see IMqttClient + */ + virtual std::set unsubscribe(const std::string& topic, int qos) override; + + /** + * @see IMqttClient + */ + virtual bool waitForCompletion(std::chrono::milliseconds waitFor) const override; + + /** + * @see IMqttClient + */ + virtual bool waitForCompletion(std::chrono::milliseconds waitFor, const Token& token) const override; + + /** + * @see IMqttClient + */ + virtual bool waitForCompletion(std::chrono::milliseconds waitFor, const std::set& tokens) const override; + + /** + * @see IMqttClient + */ + virtual boost::optional commandResult(const Token& token) const override; + + /** + * @see IMqttClient + */ + virtual std::string endpoint() const override; + +private: + /*! + * \brief Callback used to pick up the connection status of the wrappers. + * \param id The client id. + * \param cs The connection status. + */ + void connectionStatusChanged( const std::string& id, ConnectionStatus cs ); + + /*! + * \brief Callback for handling delivery complete. + * \param clientId The identifier of the client on which the publish command is executed. + * \param token The token identifies the publish command. + */ + void deliveryComplete( const std::string& clientId, std::int32_t token ); + + /** + * \brief Wait for commands to complete including active tokens in this client. + * The interface mutex is not locked by this method. + * First wait for client commands to complete (use method waitForCompletionInternalClients) + * and then wait for publish delivery callbacks to complete. + * \param clients - Vector with client wrapper pointers that need to be waited on. + * \param[in,out] waitFor - The number of milliseconds to wait for completetion of all commands and delivery callbacks. + * \param tokens - The tokens to wait for. An empty set means to wait for all commands on all clients to complete + * including all publish delivery callbacks. + * \return True when commands have completed in time including delivery callbacks or false if not. + */ + bool waitForCompletionInternal(const std::vector& clients, std::chrono::milliseconds waitFor, const std::set& tokens) const; + + /** + * \brief Wait for commands on the wrapper clients to complete. + * The interface mutex is not locked by this method. + * \param clients - Vector with client wrapper pointers that need to be waited on. + * \param[in,out] waitFor - The number of milliseconds to wait for completetion of all commands. + * On return waitFor contains the time left. + * \param tokens - The tokens to wait for. An empty set means to wait for all commands + * on all clients to complete. + * \return True when all commands have completed in time or false if not. + */ + bool waitForCompletionInternalClients(const std::vector& clients, std::chrono::milliseconds& waitFor, const std::set& tokens) const; + + /** + * @brief Determine the state of this client based on the connection statusses of its client wrappers. + * The states this client can communicate are: + * Unknown : When at least one wrapper is in a different state then Connected or ReconnectInProgress or when no wrappers are available. + * Good : When all wrappers are connected. + * ConnectionFailure : When at least one wrapper attempts reconnection. + * Unregister : When the serverstate instance is destroyed. + * + * The other states are about the information providers to the mqtt broker (the publishers) and we cannot say anything about them here. + * The state "Good" is the exception. This state means in this case that this clients connection is ok and not that the underlying data + * source (publisher) is ok. + * + * @param connectionStates A vector with the connection statusses of all client wrappers. + */ + StateEnum determineState(const std::vector& connectionStates); + + /** + * @brief Add an event to the synchronized queue. + * @param ev A function object that performs work in the context of this class. + */ + void pushEvent(std::function ev); + + /** + * @brief Worker method that executes the events. + */ + void eventHandler(); + + mutable std::mutex m_interfaceMutex; ///< Makes the interface mutual exclusive + mutable std::mutex m_internalMutex; ///< Protect the internal state. + std::string m_endpoint; ///< The endpoint uri. + std::string m_clientId; ///< The main client identification. + std::set m_activeTokens; ///< Set with active command tokens. Callbacks still need to be made for these tokens. + mutable std::condition_variable m_activeTokensCV; ///< Wait on a condition to become true w.r.t. the active token set. + std::function m_deliveryCompleteCallback; ///< Optional callback for publish completion. + ServerState m_serverState; ///< Records the state of the connection to the broker that this client is connected to. + std::unique_ptr m_principalClient; ///< The main wrapper client. + std::vector> m_additionalClients; ///< A vector of additional wrapper clients. + SynchronizedQueue> m_eventQueue; ///< Synchronized queue for scheduling additional work. + std::thread m_workerThread; ///< A worker thread that is used to perform actions that cannot be done on the callback threads. +}; + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_MQTTCLIENT_H diff --git b/src/mqttfailure.cpp a/src/mqttfailure.cpp new file mode 100644 index 0000000..ed824f6 --- /dev/null +++ a/src/mqttfailure.cpp @@ -0,0 +1,51 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "mqttfailure.h" + +// osdev::components::mqtt +#include "errorcode.h" + +using namespace osdev::components::mqtt; + +MqttFailure::MqttFailure(const MQTTAsync_failureData* data) + : m_token(data ? data->token : 0) + , m_code(data ? data->code : MQTTASYNC_FAILURE) + , m_message() +{ + if (!data) { + m_message = "missing response data"; + } + else if (!data->message) { + m_message = "no message"; + } + else { + m_message = std::string(data->message); + } +} + +std::string MqttFailure::codeToString() const +{ + if (m_code < 0) { + return pahoAsyncErrorCodeToString(m_code); + } + else if (0x80 == m_code) { + return "Failure"; + } + return std::string("Unknown(") + std::to_string(m_code) + ")"; +} diff --git b/src/mqttfailure.h a/src/mqttfailure.h new file mode 100644 index 0000000..7bbd01a --- /dev/null +++ a/src/mqttfailure.h @@ -0,0 +1,58 @@ +#ifndef OSDEV_COMPONENTS_MQTT_MQTTFAILURE_H +#define OSDEV_COMPONENTS_MQTT_MQTTFAILURE_H + +// std +#include + +// paho +#include + +namespace osdev { +namespace components { +namespace mqtt { + +/*! + * \brief Class for paho mqtt failure response data. + */ +class MqttFailure +{ +public: + /*! + * \brief Construct MqttFailure instance by copying information from the paho failure struct. + * \param data Paho response failure data. + */ + explicit MqttFailure(const MQTTAsync_failureData* data); + + /*! + * \return The command token + */ + MQTTAsync_token token() const { return m_token; } + + /*! + * \return The failure code. + */ + int code() const { return m_code; } + + /*! + * \return The failure message. + * \retval "no message" when no message is available. + */ + const std::string& message() const { return m_message; } + + /*! + * \return string interpretation of the code. + * \note negative codes are interpreted as paho error codes. + */ + std::string codeToString() const; + +private: + MQTTAsync_token m_token; ///< Command token. + int m_code; ///< Failure code. + std::string m_message; ///< Optional message. Equal to "no message" when message is not available. +}; + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_MQTTFAILURE_H diff --git b/src/mqttidgenerator.cpp a/src/mqttidgenerator.cpp new file mode 100644 index 0000000..7d197f6 --- /dev/null +++ a/src/mqttidgenerator.cpp @@ -0,0 +1,86 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "mqttidgenerator.h" + +// std +#include +#include +#include +#include + +// boost +#include + +#include "lockguard.h" +#include "commondefs.h" + +namespace { + +boost::uuids::basic_random_generator createGenerator() +{ + static auto timeSeed = static_cast(std::chrono::duration_cast(std::chrono::high_resolution_clock::now().time_since_epoch()).count()); + static auto pidSeed = static_cast(getpid()); + static std::hash hasher; + static auto threadSeed = static_cast(hasher(std::this_thread::get_id())); + static boost::mt19937 randomNumberGenerator; + randomNumberGenerator.seed(timeSeed ^ pidSeed ^ threadSeed); + return boost::uuids::basic_random_generator(&randomNumberGenerator); +} + +} // namespace + +using namespace osdev::components::mqtt; + +const MqttId mqttNamespace = boost::lexical_cast("9c0f3730-cc4f-49eb-ab5b-079bc5b0e481"); + +// static +MqttId MqttIdGenerator::generate() +{ + // From the boost design notes + // Seeding is unqiue per random generator: + // The boost::uuids::basic_random_generator class' default constructor seeds the random number generator + // with a SHA-1 hash of a number of different values including std::time(0), std::clock(), uninitialized data, + // value return from new unsigned int, etc.. + // Functions are reentrant: + // All functions are re-entrant. Classes are as thread-safe as an int. That is an instance can not be shared + // between threads without proper synchronization. + static auto uuidGenerator = createGenerator(); + static std::mutex generatorMutex; + OSDEV_COMPONENTS_LOCKGUARD(generatorMutex); + return uuidGenerator(); +} + +//static +MqttId MqttIdGenerator::nullId() +{ + return boost::uuids::nil_uuid(); +} + +// static +MqttId MqttIdGenerator::nameId(MqttId namespaceUuid, const std::string& name) +{ + boost::uuids::name_generator gen(namespaceUuid); + return gen(name.c_str()); +} + +// static +MqttId MqttIdGenerator::nameId(const std::string& name) +{ + return MqttIdGenerator::nameId(mqttNamespace, name); +} diff --git b/src/mqttidgenerator.h a/src/mqttidgenerator.h new file mode 100644 index 0000000..1da27c2 --- /dev/null +++ a/src/mqttidgenerator.h @@ -0,0 +1,47 @@ +#ifndef OSDEV_COMPONENTS_MQTT_MLOGICIDGENERATOR_H +#define OSDEV_COMPONENTS_MQTT_MLOGICIDGENERATOR_H + +// std +#include + +// osdev::components::mqtt +#include "commondefs.h" + +namespace osdev { +namespace components { +namespace mqtt { + +class MqttIdGenerator +{ +public: + /** + * @brief Generates a new MqttId, which is guaranteed to be unique. + * @return A new unique MqttId. + */ + static MqttId generate(); + + /** + * @brief Returns an MqttId that represents null. + * @return An MqttId that represents null. + */ + static MqttId nullId(); + + /** + * @brief Returns an MqttId based on a namespace uuid and a given string. + * @param namespaceUuid The namespace in which the MqttId is generated. + * @param name The name for which an MqttId is generated. + */ + static MqttId nameId(MqttId namespaceUuid, const std::string& name); + + /** + * @brief Returns an MqttId in the MQTT namespace for a given string. + * @param name The name for which an MqttId is generated. + */ + static MqttId nameId(const std::string& name); +}; + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_MLOGICIDGENERATOR_H diff --git b/src/mqttmessage.cpp a/src/mqttmessage.cpp new file mode 100644 index 0000000..c69fda8 --- /dev/null +++ a/src/mqttmessage.cpp @@ -0,0 +1,56 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "mqttmessage.h" + +using namespace osdev::components::mqtt; + +MqttMessage::MqttMessage() + : m_retained() + , m_duplicate() + , m_topic() + , m_payload() +{} + +MqttMessage::MqttMessage(const std::string &_topic, const MQTTAsync_message &message) + : m_retained( 0 != message.retained ) + , m_duplicate( 0 != message.dup ) + , m_topic( _topic ) + , m_payload() +{ + const char *msg = reinterpret_cast(message.payload); + m_payload = std::string( msg, msg + message.payloadlen ); +} + +MqttMessage::MqttMessage( const std::string &_topic, bool retainedFlag, bool duplicateFlag, std::string thePayload ) + : m_retained( retainedFlag ) + , m_duplicate( duplicateFlag ) + , m_topic( _topic ) + , m_payload( thePayload ) +{} + +MQTTAsync_message MqttMessage::toAsyncMessage() const +{ + MQTTAsync_message msg = MQTTAsync_message_initializer; + msg.payload = reinterpret_cast( const_cast( m_payload.c_str() ) ); + msg.payloadlen = static_cast( m_payload.size() ); + msg.qos = -1; + msg.retained = static_cast( m_retained ); + + return msg; +} diff --git b/src/mqttmessage.h a/src/mqttmessage.h new file mode 100644 index 0000000..dcb89c0 --- /dev/null +++ a/src/mqttmessage.h @@ -0,0 +1,68 @@ +#ifndef OSDEV_COMPONENTS_MQTT_MQTTMESSAGE_H +#define OSDEV_COMPONENTS_MQTT_MQTTMESSAGE_H + +// std +#include + +// paho-c +#include + +namespace osdev { +namespace components { +namespace mqtt { + +/*! + * @brief Class for paho mqtt message data + */ +class MqttMessage +{ +public: + /*! + * @brief Construct empty MqttMessage instance + */ + MqttMessage(); + + /*! + * @brief Construct MqttMessage instance by copying information gfrom the paho message struct + * @param topic - Paho topic data (copied) + * @param msg - Paho message data (copied) + */ + MqttMessage( const std::string &topic, const MQTTAsync_message &msg ); + + /*! + * @brief Construct MqttMessage instance. + * @param topic - Topic String + * @param retainedFlag - Flag that indicates if message is retained + * @param duplicateFlag - Flag that indicates if message is duplicate. + * @param thePayload - The message itself. + */ + MqttMessage( const std::string &topic, bool retainedFlag, bool duplicateFlag, std::string thePayload ); + + /*! @return The retained flag value. */ + bool retained() const { return m_retained; } + + /*! @return The duplicate flag value */ + bool duplicate() const { return m_duplicate; } + + /*! @return The topic on which the message is received. */ + const std::string& topic() const { return m_topic; } + + /*! @return The message payload. */ + const std::string& payload() const { return m_payload; } + + /*! @return This instance as a paho message */ + MQTTAsync_message toAsyncMessage() const; + +private: + bool m_retained; ///< Retained flag. Not all brokers communicate this flag correctly. (emqx does not, mosquitto does.) + bool m_duplicate; ///< Duplicate flag ( for qos 1? ) + std::string m_topic; ///< The topic on which the message is recieved. + std::string m_payload; ///< The actual message data. +}; + + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_MQTTMESSAGE_H diff --git b/src/mqttstream.h a/src/mqttstream.h new file mode 100644 index 0000000..f1055b4 --- /dev/null +++ a/src/mqttstream.h @@ -0,0 +1,127 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#ifndef OSDEV_COMPONENTS_MQTT_MQTTSTREAM_H +#define OSDEV_COMPONENTS_MQTT_MQTTSTREAM_H + +// This header is used in conjunction with mlogic/commmon/stringify.h to get output streaming of stl container types. +// The streaming operators are not suitable for marshalling because type information is lost! + +// std +#include +#include +#include +#include +#include +#include +#include + +namespace osdev { +namespace components { +namespace mqtt { + +/** + * @brief Streams a container with content for which stream operators are available. + * @note This function is meant for printing and not for marshalling! + * @tparam Open The container opening character. + * @tparam Close The container closing character. + * @tparam T The container type. + * @param os The stream to use. + * @param rhs The container that is to be streamed. + * @param sep The field separator. Default is ", " + * @return reference to the stream object. + */ +template +std::ostream& streamContainer(std::ostream& os, const T& rhs, const std::string& sep = ", ") +{ + os << Open; + for (auto cit = rhs.cbegin(); rhs.cend() != cit; ++cit) { + os << *cit; + if (std::next(cit) != rhs.end()) { + os << sep; + } + } + os << Close; + + return os; +} + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +namespace std { + +/** + * @brief Streams a list that contains values for which an output stream operator is available. + */ +template +std::ostream& operator<<(std::ostream& os, const std::list& rhs) +{ + return osdev::components::mqtt::streamContainer<'<', '>'>(os, rhs); +} + +/** + * @brief Streams an array that contains values for which an output stream operator is available. + */ +template +std::ostream& operator<<(std::ostream& os, const std::array& rhs) +{ + return osdev::components::mqtt::streamContainer<'[', ']'>(os, rhs); +} + +/** + * @brief Streams a vector that contains values for which an output stream operator is available. + */ +template +std::ostream& operator<<(std::ostream& os, const std::vector& rhs) +{ + return osdev::components::mqtt::streamContainer<'[', ']'>(os, rhs); +} + +/** + * @brief Streams a set that contains values for which an output stream operator is available. + */ +template +std::ostream& operator<<(std::ostream& os, const std::set& rhs) +{ + return osdev::components::mqtt::streamContainer<'{', '}'>(os, rhs); +} + +/** + * @brief Streams a map that contains keys and values for which an output stream operator is available. + */ +template +std::ostream& operator<<(std::ostream& os, const std::map& rhs) +{ + return osdev::components::mqtt::streamContainer<'{', '}'>(os, rhs); +} + +/** + * @brief Streams a pair that contains values for which an output stream operator is available. + */ +template +std::ostream& operator<<(std::ostream& os, const std::pair& rhs) +{ + os << "{" << rhs.first << " : " << rhs.second << "}"; + return os; +} + +} // End namespace std + +#endif // OSDEV_COMPONENTS_MQTT_MQTTSTREAM_H diff --git b/src/mqttsuccess.cpp a/src/mqttsuccess.cpp new file mode 100644 index 0000000..e7bcc05 --- /dev/null +++ a/src/mqttsuccess.cpp @@ -0,0 +1,85 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "mqttsuccess.h" + +using namespace osdev::components::mqtt; + +ConnectionData::ConnectionData() + : m_serverUri() + , m_mqttVersion() + , m_sessionPresent() +{ +} + +ConnectionData::ConnectionData(char* _serverUri, int _mqttVersion, int _sessionPresent) + : m_serverUri(_serverUri ? _serverUri : "unknown") + , m_mqttVersion(_mqttVersion) + , m_sessionPresent(0 != _sessionPresent) +{ +} + +MqttSuccess::MqttSuccess(MQTTAsync_token _token) + : m_token(_token) + , m_data(Unspecified{}) +{ +} + +MqttSuccess::MqttSuccess(MQTTAsync_token _token, int _qos) + : m_token(_token) + , m_data(_qos) +{ +} + +MqttSuccess::MqttSuccess(MQTTAsync_token _token, const std::vector& _qosMany) + : m_token(_token) + , m_data(_qosMany) +{ +} + +MqttSuccess::MqttSuccess(MQTTAsync_token _token, const MqttMessage& pubMsg) + : m_token(_token) + , m_data(pubMsg) +{ +} + +MqttSuccess::MqttSuccess(MQTTAsync_token _token, const ConnectionData& connData) + : m_token(_token) + , m_data(connData) +{ +} + +int MqttSuccess::qos() const +{ + return boost::get(m_data); +} + +std::vector MqttSuccess::qosMany() const +{ + return boost::get>(m_data); +} + +MqttMessage MqttSuccess::publishData() const +{ + return boost::get(m_data); +} + +ConnectionData MqttSuccess::connectionData() const +{ + return boost::get(m_data); +} diff --git b/src/mqttsuccess.h a/src/mqttsuccess.h new file mode 100644 index 0000000..d582bd9 --- /dev/null +++ a/src/mqttsuccess.h @@ -0,0 +1,144 @@ +#ifndef OSDEV_COMPONENTS_MQTT_MQTTSUCCESS_H +#define OSDEV_COMPONENTS_MQTT_MQTTSUCCESS_H + +// std +#include +#include + +// boost +#include + +// paho +#include + +// osdev::components::mqtt +#include "mqttmessage.h" + +namespace osdev { +namespace components { +namespace mqtt { + +/** + * @brief Class that holds paho connection data which is returned in the connect response. + */ +class ConnectionData +{ +public: + /*! + * \brief Construct an empty ConnectData instance. + */ + ConnectionData(); + + /*! + * \brief Construct ConnectData based on incoming values from paho. + * \param serverUri - The serverUri to which the connection is made (needs to be copied). + * \param mqttVersion - The mqtt version used by the broker. + * \param sessionPresent - Flag that indicates if a session was present for the given clientId. + */ + ConnectionData(char* serverUri, int mqttVersion, int sessionPresent); + + /*! + * \return The server uri. + */ + const std::string& serverUri() const { return m_serverUri; } + + /*! + * \return The mqtt version. + */ + int mqttVersion() const { return m_mqttVersion; } + + /*! + * \return if a session was present for the given clientId. + */ + bool sessionPresent() const { return m_sessionPresent; } + +private: + std::string m_serverUri; ///< The broker server uri. + int m_mqttVersion; ///< The mqtt version used by the broker. + bool m_sessionPresent; ///< Flag that indicates whether a session was present for the client id used in the connect. +}; + +struct Unspecified +{ +}; + +/*! + * \brief Class for paho mqtt success response data. + * The paho success response data uses a union and can have different information depending on the command. + */ +class MqttSuccess +{ +public: + /*! + * \brief Response data for commands without specific data. + * \param token The token that identifies to which command this response belongs. + */ + explicit MqttSuccess(MQTTAsync_token token); + + /*! + * \brief Response data for a subscribe command. + * \param token The token that identifies to which command this response belongs. + * \param qos Actual quality of service of the subscription. + */ + MqttSuccess(MQTTAsync_token token, int qos); + + /*! + * \brief Response data for a subscribe many command. + * \param token The token that identifies to which command this response belongs. + * \param qosMany Actual quality of service of the subscription for each topic filter. + */ + MqttSuccess(MQTTAsync_token token, const std::vector& qosMany); + + /*! + * \brief Response data for a publish command. + * \param token The token that identifies to which command this response belongs. + * \param pubMsg The message that was published. + */ + MqttSuccess(MQTTAsync_token token, const MqttMessage& pubMsg); + + /*! + * \brief Response data for a connect command. + * \param token The token that identifies to which command this response belongs. + * \param connData The connection data. + */ + MqttSuccess(MQTTAsync_token token, const ConnectionData& connData); + + /*! + * \return the command token. + */ + MQTTAsync_token token() const { return m_token; } + + /*! + * \return the qos + * \throw exception when command is not a subscribe command. + */ + int qos() const; + + /*! + * \return a vector of qos values (matching the topics in the subscribe many command). + * \throw exception when command is not a subscribe many command. + */ + std::vector qosMany() const; + + /*! + * \return Message that has been published. + * \throw exception when command is not a publish command. + */ + MqttMessage publishData() const; + + /*! + * return Connection data. + * throw exception when command is not a connect command. + */ + ConnectionData connectionData() const; + +private: + MQTTAsync_token m_token; ///< Command token. + boost::variant, MqttMessage, ConnectionData, Unspecified> m_data; ///< Data for the various commands. +}; + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_MQTTSUCCESS_H diff --git b/src/mqtttypeconverter.cpp a/src/mqtttypeconverter.cpp new file mode 100644 index 0000000..54b5df6 --- /dev/null +++ a/src/mqtttypeconverter.cpp @@ -0,0 +1,82 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "mqtttypeconverter.h" + +// std +#include + +// boost +#include +#include + +// date +#include "date.h" + +#include "stringutils.h" + +using namespace osdev::components::mqtt; + +namespace osdev { +namespace components { +namespace mqtt { +namespace MqttTypeConverter { + +std::string toStdString(const MqttId& mqttId) +{ + return boost::lexical_cast(mqttId); +} + +MqttId toMlogicId(const std::string& mqttId) +{ + return boost::lexical_cast(mqttId); +} + +StdTime toStdTime(const time_t& posixTime) +{ + return std::chrono::system_clock::from_time_t(posixTime); +} + +time_t toPosixTime(const StdTime& stdTime) +{ + return std::chrono::system_clock::to_time_t(stdTime); +} + +OptionalTime toOptionalTime(const std::string& posixTimeString) +{ + static const OptionalTime nonExisting; + + try { + auto lotPosixTime = boost::lexical_cast(posixTimeString); + return toStdTime(lotPosixTime); + } + catch (const boost::bad_lexical_cast&) { + return nonExisting; + } +} + +std::string toShortGuidAppendedString(const std::string& str, const MqttId& mqttId) +{ + std::string idStr = toStdString(mqttId); + return str + "-" + idStr.substr(0, 4); +} + +} // End namespace MqttTypeConverter +} // End namespace mqtt +} // End namespace components +} // End namespace osdev diff --git b/src/mqtttypeconverter.h a/src/mqtttypeconverter.h new file mode 100644 index 0000000..fcff520 --- /dev/null +++ a/src/mqtttypeconverter.h @@ -0,0 +1,89 @@ +#ifndef OSDEV_COMPONENTS_MQTT_MLOGICTYPECONVERTER_H +#define OSDEV_COMPONENTS_MQTT_MLOGICTYPECONVERTER_H + +// std +#include +#include +#include "date.h" +#include + +// boost +#include + +#include "commondefs.h" + +namespace osdev { +namespace components { +namespace mqtt { + +/** + * @brief Utility namespace to convert between mqtt common types and other frequently used types. + */ +namespace MqttTypeConverter { + +/** + * @brief Converts from MqttId to std::string. + * @param mqttId The mqttId to convert. + * @return The std::string with contents of the provided mqttId. Format is 12345678-9abc-def0-1234-56789abcdef0. + */ +std::string toStdString(const MqttId& mqttId); + +/** + * @brief Converts from system clock timepoint to std::string. + * @tparam Duration std::chrono::duration instance. + * Duration::Period is used to determine the precision + * of the subsecond part of the returned ISO8601 string. + * Uses the duration of the StdTime type by default. + * @param tp The timepoint to converter. + * @return ISO8601 string representation of stdTime. + */ +template +std::string toStdString(const std::chrono::time_point& tp) +{ + return date::format("%FT%T%Ez", tp); +} + +/** + * @brief Converts from std::string to MqttId. + * @param mqttId The MqttId string to convert. + * @return the converted string to MqttId. + */ +MqttId toMqttId(const std::string& mqttId); + +/** + * @brief Creates a descriptive string based on the specified input parameters. + * @param str The prefix of the string. + * @param mqttId The id of which to use the first 4 characters. + * @return str + "-" + + * Example: "Unassigned-a2c4". + */ +std::string toShortGuidAppendedString(const std::string& str, const MqttId& mqttId); + +/** + * @brief Converts from PosixTime (time_t) to StdTime. + * @param posixTime The Posix Time (time_t) to convert. + * @return The StdTime with same value as the provided posixTime. + */ +StdTime toStdTime(const std::time_t& posixTime); + +/** + * @brief Converts from StdTime to PosixTime (time_t). + * @param stdTime The StdTime to convert. + * @return The PosixTime with the same value as the provided stdTime. + */ +time_t toPosixTime(const osdev::components::mqtt::StdTime& stdTime); + +/** + * @brief Converts the specified posixTimeString to an OptionalTime. + * @param posixTimeString A posix time as string. + * @return The converted posixTimeString. + * @retval boost::none if the specified posixTimeString could not be converted to a StdTime. + */ +osdev::components::mqtt::OptionalTime toOptionalTime(const std::string& posixTimeString); + +} // End namespace MqttTypeConverter +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_MQTTTYPECONVERTER_H diff --git b/src/mqttutil.cpp a/src/mqttutil.cpp new file mode 100644 index 0000000..70d75e8 --- /dev/null +++ a/src/mqttutil.cpp @@ -0,0 +1,132 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "mqttutil.h" + +// boost +#include +#include + +namespace osdev { +namespace components { +namespace mqtt { + +bool isValidTopic( const std::string &topic ) +{ + if (topic.empty() || topic.size() > 65535) { + return false; + } + + auto posHash = topic.find('#'); + if (std::string::npos != posHash && posHash < topic.size() - 1) { + return false; + } + + std::size_t pos = 0; + while ((pos = topic.find('+', pos)) != std::string::npos) { + if (pos > 0 && topic[pos - 1] != '/') { + return false; + } + if (pos < topic.size() - 1 && topic[pos + 1] != '/') { + return false; + } + ++pos; + } + return true; +} + +bool hasWildcard( const std::string &topic ) +{ + return ( topic.size() > 0 && (topic.find( '+' ) != std::string::npos || topic.size() - 1 == '#' ) ); +} + +bool testForOverlap( const std::string &existingTopic, const std::string &newTopic ) +{ + if (existingTopic == newTopic) { + return true; + } + + // A topic that starts with a $ can never overlap with topic that does not start with a $. + // Topics that start!!! with a $ are so called system reserved topics and not meant for users to publish to. + if ((existingTopic[0] == '$' && newTopic[0] != '$') || + (existingTopic[0] != '$' && newTopic[0] == '$')) { + return false; + } + + std::vector existingTopicList; + std::vector newTopicList; + boost::algorithm::split(existingTopicList, existingTopic, [](char ch) { return ch == '/'; }); + boost::algorithm::split(newTopicList, newTopic, [](char ch) { return ch == '/'; }); + + auto szExistingTopicList = existingTopicList.size(); + auto szNewTopicList = newTopicList.size(); + // Walk through the topic term by term until it is proved for certain that the topics either have no overlap + // or do have overlap. + for (std::size_t idx = 0; idx < std::minmax(szExistingTopicList, szNewTopicList).first; ++idx) { + if ("#" == existingTopicList[idx] || "#" == newTopicList[idx]) { + return true; // Match all wildcard found so there is always a possible overlap. + } + else if ("+" == existingTopicList[idx] || "+" == newTopicList[idx]) { + // these terms can match each other based on wildcard. + // Topics are still in the race for overlap, proceed to the next term + } + else if (existingTopicList[idx] != newTopicList[idx]) { + return false; // no match possible because terms are not wildcards and differ from each other. + } + else { + // term is an exact match. The topics are still in the race for overlap, proceed to the next term. + } + } + // Still no certain prove of overlap. If the number of terms for both topics are the same at this point then they overlap. + // If they are not the same than a match all wildcard on the longer topic can still make them match because of a special rule + // that states that a topic with a match all wildcard also matches a topic without that term. + // Example: topic /level/1 matches wildcard topic /level/1/# + // A match single wildcard at the end or at the term before a match all wildcard must also be taken into account. + // Example: /level/+ can overlap with /level/1/# + // /level/1 can overlap with /level/+/# + if (szNewTopicList != szExistingTopicList) { + if (szNewTopicList == szExistingTopicList + 1 && "#" == newTopicList[szNewTopicList - 1]) { + return (newTopicList[szNewTopicList - 2] == existingTopicList[szExistingTopicList - 1] || "+" == existingTopicList[szExistingTopicList - 1] || "+" == newTopicList[szNewTopicList - 2]); + } + if (szExistingTopicList == szNewTopicList + 1 && "#" == existingTopicList[szExistingTopicList - 1]) { + return (existingTopicList[szExistingTopicList - 2] == newTopicList[szNewTopicList - 1] || "+" == newTopicList[szNewTopicList - 1] || "+" == existingTopicList[szExistingTopicList - 2]); + } + return false; + } + + return true; +} + +std::string convertTopicToRegex(const std::string& topic) +{ + // escape the regex characters in msgTemplate + static const boost::regex esc("[.^$|()\\[\\]{}*+?\\\\]"); + static const std::string rep("\\\\$&"); // $&, refers to whatever is matched by the whole expression + + std::string out = boost::regex_replace(topic, esc, rep); + + static const boost::regex multiTopicRegex("#$"); + static const boost::regex singleTopicRegex(R"((/|^|(?<=/))\\\+(/|$))"); + + out = boost::regex_replace(out, multiTopicRegex, ".*"); + return boost::regex_replace(out, singleTopicRegex, "$1[^/]*?$2"); +} + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev diff --git b/src/mqttutil.h a/src/mqttutil.h new file mode 100644 index 0000000..6e859d1 --- /dev/null +++ a/src/mqttutil.h @@ -0,0 +1,46 @@ +#ifndef OSDEV_COMPONENTS_MQTT_MQTTUTIL_H +#define OSDEV_COMPONENTS_MQTT_MQTTUTIL_H + +// std +#include + +namespace osdev { +namespace components { +namespace mqtt { + +/*! + * \brief Determine if topic is a valid mqtt topic filter. + * \param topic - The topic to test. + * \return True when topic is valid, false otherwise. + */ +bool isValidTopic( const std::string &topic ); + +/*! + * \brief Test a topic against another topicfilter for overlap. + * \param existingTopic - The topic to test against + * \param newTopic - The topic to test. + * \return True when topics overlap, false otherwise + */ +bool testForOverlap( const std::string & existingTopic, const std::string &newTopic ); + +/*! + * \brief Test a topic for occurence of wildcards + * \param topic - The topic to test + * \return True if topics contains wildcards, false otherwise + */ +bool hasWildcard( const std::string &topic ); + +/*! + * \brief Create a regular expression string based on a topicfilter that can be used + * to match topic strings against topics with no wildcards. + * \pre The topic filter is valid. + * \return The regular expression string. If the topic filter is not valid then the + * returned string is also not valid. + */ +std::string convertTopicToRegex( const std::string &topic ); + +} // End namespace mqtt +} // End namespace components +} // osdev + +#endif // OSDEV_COMPONENTS_MQTT_MQTTUTIL_H diff --git b/src/scopeguard.cpp a/src/scopeguard.cpp new file mode 100644 index 0000000..d993160 --- /dev/null +++ a/src/scopeguard.cpp @@ -0,0 +1,42 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "scopeguard.h" + +using namespace osdev::components::mqtt; + +ScopeGuard::ScopeGuard() + : m_cleanupFunc() +{ +} + +ScopeGuard::ScopeGuard(const CleanUpFunction& cleanupFunc) + : m_cleanupFunc(cleanupFunc) +{ +} + +ScopeGuard::~ScopeGuard() noexcept +{ + try { + if (m_cleanupFunc) { + m_cleanupFunc(); + } + } + catch (...) { + } +} diff --git b/src/scopeguard.h a/src/scopeguard.h new file mode 100644 index 0000000..7e72e7c --- /dev/null +++ a/src/scopeguard.h @@ -0,0 +1,55 @@ +#ifndef OSDEV_COMPONENTS_MQTT_SCOPEGUARD_H +#define OSDEV_COMPONENTS_MQTT_SCOPEGUARD_H + +// std +#include + +#include "macrodefs.h" +#include "utils.h" + +#define OSDEV_COMPONENTS_SCOPEGUARD(variableName, ...) \ + osdev::components::mqtt::ScopeGuard OSDEV_COMPONENTS_COMBINE(Scope__Guard__##variableName##__, __LINE__)(__VA_ARGS__); \ + osdev::components::mqtt::apply_unused_parameters(OSDEV_COMPONENTS_COMBINE(Scope__Guard__##variableName##__, __LINE__)); + +namespace osdev { +namespace components { +namespace mqtt { + +using CleanUpFunction = std::function; + +/** + * @brief Ensures that a cleanup function is called at the end of the current scope. + */ +class ScopeGuard +{ +public: + /** + * @brief Constructs an empty scopeguard. + * The scopeguard can be set by moving another ScopeGuard into this one. + */ + ScopeGuard(); + + /** + * @brief Constructs a RAII instance that will call cleanupFunc in it's destructor. + * @param cleanupFunc The cleanup function to call at the end of the current scope. + * This cleanup function must not throw exceptions. If it does, the behavior is undefined. + */ + ScopeGuard(const CleanUpFunction& cleanupFunc); + + // Movable, not copyable + ScopeGuard(const ScopeGuard&) = delete; + ScopeGuard& operator=(const ScopeGuard&) = delete; + ScopeGuard(ScopeGuard&&) = default; + ScopeGuard& operator=(ScopeGuard&&) = default; + + ~ScopeGuard() noexcept; + +private: + CleanUpFunction m_cleanupFunc; +}; + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_SCOPEGUARD_H diff --git b/src/serverstate.cpp a/src/serverstate.cpp new file mode 100644 index 0000000..dd2ee78 --- /dev/null +++ a/src/serverstate.cpp @@ -0,0 +1,83 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "serverstate.h" + +using namespace osdev::components::mqtt; + +const std::string ServerState::s_identifier("ServerState"); + +std::atomic ServerState::s_nextServerStateCallbackHandle(1); + +ServerState::ServerState(const IStateCallback* stateCallbackIf) + : sig_serverStateChanged() + , m_stateCallbackIf(stateCallbackIf) + , m_serverStateCallbackMap() + , m_state(StateEnum::Unknown) +{ +} + +ServerState::~ServerState() +{ + try { + this->clearAllStateChangeCallbacks(); + } + catch (...) { + } +} + +StateChangeCallbackHandle ServerState::registerStateChangeCallback(const IStateCallback::SlotStateChange& cb) +{ + const auto handle = s_nextServerStateCallbackHandle++; + m_serverStateCallbackMap.emplace(std::piecewise_construct, + std::make_tuple(handle), + std::make_tuple(sig_serverStateChanged.connect(cb))); + + return handle; +} + +void ServerState::unregisterStateChangeCallback(StateChangeCallbackHandle handle) +{ + m_serverStateCallbackMap.erase(handle); +} + +void ServerState::clearAllStateChangeCallbacks() +{ + this->emitStateChanged(StateEnum::Unregister); + m_serverStateCallbackMap.clear(); +} + +void ServerState::emitStateChanged(StateEnum newState) +{ + m_state = newState; ///< store state, to allow clients to retrieve the last received state. + + if (nullptr != m_stateCallbackIf) { + sig_serverStateChanged(m_stateCallbackIf, newState); + } +} + +StateEnum ServerState::state() const +{ + return m_state; +} + +std::string ServerState::stateCallbackClientId() const +{ + static const std::string nullId = "null"; + return (nullptr == m_stateCallbackIf) ? nullId : m_stateCallbackIf->clientId(); +} diff --git b/src/serverstate.h a/src/serverstate.h new file mode 100644 index 0000000..6ac40cd --- /dev/null +++ a/src/serverstate.h @@ -0,0 +1,110 @@ +#ifndef OSDEV_COMPONENTS_MQTT_SERVERSTATE_H +#define OSDEV_COMPONENTS_MQTT_SERVERSTATE_H + +// std +#include +#include + +// boost +#include + +// osdev::components::mqtt +#include "istatecallback.h" + +namespace osdev { +namespace components { +namespace mqtt { + +/*! + * \brief Class for administrating ServerState callbacks. + * ServiceClientBase uses this object to notify the server state listeners + * of changes in the serverstate of the server that the client is connected to. + */ +class ServerState +{ +public: + /*! + * \brief Constructs a ServerState object. This object has a one to one relation with an IStateCallback object. + * \param stateCallbackIf identification of the interface that generates the signal. + */ + explicit ServerState(const IStateCallback* stateCallbackIf); + + /*! + * \brief Destroy the ServerState. + * Calls clearAllStateChangeCallbacks() to unregister itself from the listeners. + */ + virtual ~ServerState(); + + // non copyable, non movable + ServerState(const ServerState&) = delete; + ServerState& operator=(ServerState&) = delete; + ServerState(ServerState&&) = delete; + ServerState& operator=(ServerState&&) = delete; + + /*! + * \brief Registers a statechange callback method. + * \param cb - The callback method. + * \return handle that identifies the callback method. + */ + StateChangeCallbackHandle registerStateChangeCallback(const IStateCallback::SlotStateChange& cb); + + /*! + * \brief Unregisters a state change callback method. + * \param handle Handle that identifies the callback method. + */ + void unregisterStateChangeCallback(StateChangeCallbackHandle handle); + + /*! + * \brief Removes all callback methods. + * An Unregister state is signalled to the listeners. + */ + void clearAllStateChangeCallbacks(); + + /*! + * \brief Emit the State changed signal. + * \param newState - The new state. + */ + void emitStateChanged(StateEnum newState); + + /*! + * \brief Return the last state received from server. + * \return state of server + */ + StateEnum state() const; + + /*! + * \brief Returns the handle that will be given to the next callback that is registered. + */ + static StateChangeCallbackHandle nextHandle() + { + return s_nextServerStateCallbackHandle; + } + +private: + /*! + * Type for holding connections to server state callback functions. + */ + using ServerStateCallbackMap = std::map; + + IStateCallback::SigStateChange sig_serverStateChanged; ///< Signal emitted when server state has changed. + + const IStateCallback* m_stateCallbackIf; ///< Identification of the the interface that generates the signal (not owned) + ServerStateCallbackMap m_serverStateCallbackMap; ///< Map with serverstate callback connections. + + static std::atomic s_nextServerStateCallbackHandle; ///< Handle given to next serverstate callback registration (0 is not valid). + + StateEnum m_state; ///< Store the last state received from the server. + + /*! + * \return The clientId of the IStateCallback interface, or "null" if it is nullptr. + */ + std::string stateCallbackClientId() const; + + static const std::string s_identifier; +}; + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_SERVERSTATE_H diff --git b/src/sharedreaderlock.cpp a/src/sharedreaderlock.cpp new file mode 100644 index 0000000..aa3afe6 --- /dev/null +++ a/src/sharedreaderlock.cpp @@ -0,0 +1,203 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "sharedreaderlock.h" + + +// #include "mlogic/common/invalidoperationexception.h" +#include "lockguard.h" +#include "mqttstream.h" + + +// #include "mlogic/common/logger/loggerprovider.h" + +using namespace osdev::components::mqtt; + +// What happens if a read lock owner locks again +// - another read lock : map the threads owning read locks and update a count. +// - a write lock : upgrade the read lock to an exclusive write lock (including all recursive locks). +// +// The same for a write lock owner. +// - another write lock : map the threads owning a write lock and update the count. +// - a read lock : treat the lock as an exclusive write lock +// +// What happens if a thread that never locks does an unlock. +// - when no locks are active : nothing happens +// - during active read locks : nothing happens +// - during active write lock : nothing happens +// +// What happens when multiple threads want a write lock. +// - Only one writer can be active at any time. Other writers will have to wait until the active writer has unlocked. +// + +namespace { + +/** + * @param lockMap The map to check for active locks. + * @return true when the lockMap contains an active lock, false otherwise. + */ +bool hasActiveLock(const std::map& lockMap) +{ + for (const auto& lockPair : lockMap) { + if (lockPair.second.active()) { + return true; + } + } + return false; +} + +} // namespace +SharedReaderLock::SharedReaderLock() + : m_mutex() + , m_readLockMap() + , m_writeLockMap() + , m_readersCV() + , m_writersCV() +{ +} + +SharedReaderLock::~SharedReaderLock() +{ + OSDEV_COMPONENTS_LOCKGUARD(m_mutex); + if (!m_readLockMap.empty() || !m_writeLockMap.empty()) + { + // toLogFile ("SharedReaderLock", "Cannot destroy this lock because threads are still registered. Readers : %1, Writers : %2", m_readLockMap, m_writeLockMap); + // (InvalidOperationException, "Cannot destroy SharedReaderLock"); + } +} + +void SharedReaderLock::lockShared() +{ + OSDEV_COMPONENTS_UNIQUELOCK_CREATE(m_mutex); + + if (m_writeLockMap.end() != m_writeLockMap.find(std::this_thread::get_id())) + { // If the thread owns a write lock than update the write lock count + m_writeLockMap[std::this_thread::get_id()].increase(); + return; + } + + auto resultPair = m_readLockMap.insert(std::make_pair(std::this_thread::get_id(), LockData{})); + if (!resultPair.second) + { // thread has a read lock already... + resultPair.first->second.increase(); // add recursive reader lock + return; // Already owner of the shared lock, so proceed. + } + + m_readersCV.wait(OSDEV_COMPONENTS_UNIQUELOCK(m_mutex), [this] { return m_writeLockMap.empty(); }); + + // Reread the iterator because it might have been invalidated. + auto it = m_readLockMap.find(std::this_thread::get_id()); + if (m_readLockMap.end() == it) + { + // normally Throw(InvalidOperationException, "Thread id must be registered in the shared reader map at this point"); + } + it->second.increase(); + // the reader is now registered and is a shared owner of the SharedReaderLock +} + +void SharedReaderLock::lockExclusive() +{ + OSDEV_COMPONENTS_UNIQUELOCK_CREATE(m_mutex); + + // First check if the thread already owns a read lock. + auto readLockIter = m_readLockMap.find(std::this_thread::get_id()); + + if (m_readLockMap.end() != readLockIter) { + auto resultPair = m_writeLockMap.insert(std::make_pair(readLockIter->first, LockData::initialize(readLockIter->second))); + if (!resultPair.second) + { + // throw (InvalidOperationException, "Thread id cannot be registered in the write map at this point"); + } + // toLogFile ("SharedReaderLock", "Upgrade to exclusive lock"); + m_readLockMap.erase(readLockIter); + // This is an upgrade of the read lock to an exclusive write lock. + // proceed to the wait call on m_writersCV. + } + else + { + auto resultPair = m_writeLockMap.insert(std::make_pair(std::this_thread::get_id(), LockData{})); + if (!resultPair.second) + { // thread has a write lock already... + resultPair.first->second.increase(); // add recursive write lock + return; // Already owner of the exclusive lock, so proceed. + } + } + + m_writersCV.wait(OSDEV_COMPONENTS_UNIQUELOCK(m_mutex), [this] { return !hasActiveLock(m_readLockMap) && !hasActiveLock(m_writeLockMap); }); + + // Reread the iterator because it might have been invalidated. + auto it = m_writeLockMap.find(std::this_thread::get_id()); + if (m_writeLockMap.end() == it) + { + // throw (InvalidOperationException, "Thread id must be registered in the writer map at this point"); + } + it->second.increase(); + // the thread is now registered and is an exclusive owner of the SharedReaderLock +} + +void SharedReaderLock::unlock() +{ + OSDEV_COMPONENTS_UNIQUELOCK_CREATE(m_mutex); + + auto readLockIter = m_readLockMap.find(std::this_thread::get_id()); + auto writeLockIter = m_writeLockMap.find(std::this_thread::get_id()); + + if (m_writeLockMap.end() != writeLockIter && m_readLockMap.end() != readLockIter) + { + return; // thread does not have a lock. Do nothing. + } + + if (m_readLockMap.end() != readLockIter) + { + if (!readLockIter->second.decrease()) + { // The lock is not active anymore so remove it + m_readLockMap.erase(readLockIter); + + if (!hasActiveLock(m_readLockMap) && !m_writeLockMap.empty()) + { // no active read lock anymore and a writer that waits for an exclusive lock. + // toLogFileDebug("SharedReaderLock", "notify_one writersCV"); + OSDEV_COMPONENTS_UNIQUELOCK_UNLOCK(m_mutex); + m_writersCV.notify_one(); + } + } + return; + } + + if (m_writeLockMap.end() != writeLockIter) + { + if (!writeLockIter->second.decrease()) + { + // remove the exclusive write lock + m_writeLockMap.erase(writeLockIter); + + if (m_writeLockMap.empty()) + { + // toLogFile ("SharedReaderLock", "notify_all readersCV"); + OSDEV_COMPONENTS_UNIQUELOCK_UNLOCK(m_mutex); + m_readersCV.notify_all(); + } + else + { + // toLogFile ("SharedReaderLock", "notify_one writersCV"); + OSDEV_COMPONENTS_UNIQUELOCK_UNLOCK(m_mutex); + m_writersCV.notify_one(); + } + } + return; + } +} diff --git b/src/sharedreaderlock.h a/src/sharedreaderlock.h new file mode 100644 index 0000000..3a862be --- /dev/null +++ a/src/sharedreaderlock.h @@ -0,0 +1,183 @@ +#ifndef OSDEV_COMPONENTS_MQTT_SHAREDREADERLOCK_H +#define OSDEV_COMPONENTS_MQTT_SHAREDREADERLOCK_H + +// std +#include +#include +#include +#include + +// mlogic::common +#include "scopeguard.h" + +namespace osdev { +namespace components { +namespace mqtt { + +#define OSDEV_COMPONENTS_SHAREDLOCK_SCOPE(lockvar) \ + lockvar.lockShared(); \ + OSDEV_COMPONENTS_SCOPEGUARD(lockvar, [&]() { lockvar.unlock(); }); + +#define OSDEV_COMPONENTS_EXCLUSIVELOCK_SCOPE(lockvar) \ + lockvar.lockExclusive(); \ + OSDEV_COMPONENTS_SCOPEGUARD(lockvar, [&]() { lockvar.unlock(); }); + +/** + * @brief Class is used to administrate the lock data. + */ +class LockData +{ +public: + /** + * @brief Default constructable. Lock is not active and the count is 0. + */ + LockData() + : m_count(0) + , m_active(false) + { + } + + // Copyable, movable + LockData(const LockData&) = default; + LockData& operator=(const LockData&) = default; + LockData(LockData&&) = default; + LockData& operator=(LockData&&) = default; + + /** + * @return true when the lock is active, false otherwise. + * @note A lock becomes active the first time that increase() is called. + */ + inline bool active() const + { + return m_active; + } + + /** + * @brief Increases the lock count by one. + * An inactive lock becomes active by this call. + */ + inline void increase() + { + m_active = true; + ++m_count; + } + + /** + * @brief Decreases the lock count by one. + * The count is only decreased for active locks. When the lock count becomes 0 the lock + * is deactivated. + * @return true when the lock is still active after decrease and false when it is deactivated. + */ + inline bool decrease() + { + if (m_active) { + --m_count; + m_active = (0 != m_count); + } + return m_active; + } + + /** + * @brief Conversion operator that returns the lock count. + */ + inline operator std::size_t() const + { + return m_count; + } + + /** + * @brief Static method for initializing a lock data based on already existing lock data. + * The new lock data is not active. + * @note This is used to promote a shared lock to an exclusive lock. + */ + inline static LockData initialize(const LockData& other) + { + auto newLockData(other); + newLockData.m_active = false; + return newLockData; + } + +private: + std::size_t m_count; ///< The lock count. + + /** + * @brief Flag to indicate whether the lock is active. + * This flag is necessary because when the lock is promoted + * the lock count is not zero but the lock still should be activated again. + */ + bool m_active; +}; + +/** + * @brief Lock class that allows multiple readers to own the lock in a shared way. + * A writer will want exclusive ownership so that it can mutate the content that + * is protected by this lock. + * + * Reader and writer should be interpreted as to how threads interact with the content that this lock protects. It is up + * to the caller to enforce the correct behaviour. In other words don't take a shared lock and change the content! + * + * The administration of this class uses the std::thread::id to register which thread holds what kind of lock. + * This id is reused, so be really careful to pair each lock with an unlock, otherwise newly spawned threads might + * end up having a lock without taking one. + */ +class SharedReaderLock +{ +public: + /** + * Default constructable. + * The lock is not locked. + */ + SharedReaderLock(); + + /** + * Destructor will throw when there are threads registered. + */ + ~SharedReaderLock(); + + // Non copyable, non movable + SharedReaderLock(const SharedReaderLock&) = delete; + SharedReaderLock& operator=(const SharedReaderLock&) = delete; + SharedReaderLock(SharedReaderLock&&) = delete; + SharedReaderLock& operator=(SharedReaderLock&&) = delete; + + /** + * @brief Lock in a shared way. For read only operations. + * Multiple threads can have shared ownership on this lock. + * It is guaranteed that a call to lockExclusive will wait until all read locks are unlocked. + * When a call to lockExclusive is made and is waiting, no new reader locks are accepted. + * A thread that owns a shared lock can lock again. The lock will be unlocked for this thread when as many unlock calls are made. + * A thread that owns a shared lock can upgrade the lock to an exclusive lock by calling lockExclusive. The thread has to wait + * for exclusive ownership and has the exclusive lock until all unlocks are made (if it had done multiple shared locks before an exclusive lock). + */ + void lockShared(); + + /** + * @brief Lock in an exclusive way. For write operations. + * Only one thread can have exclusive ownership of this lock. + * While a thread waits for exlusive ownership shared locks are denied. This lock is unfair in the + * sense that it favours write locks. + * A thread that owns exclusive ownership can make another exclusive lock or a even a shared lock. Both are + * treated as an exclusive lock that updates the lock count. As many unlocks need to be called to unlock the exclusive lock. + */ + void lockExclusive(); + + /** + * @brief Unlock the lock. The thread id is used to determine which lock needs to be unlocked. + * If a thread does not own this lock at all then nothing happens. + */ + void unlock(); + +private: + std::mutex m_mutex; ///< Mutex that protects the lock administration. + std::map m_readLockMap; ///< Map with read lock data. + std::map m_writeLockMap; ///< Map with write lock data. + + std::condition_variable m_readersCV; ///< lockShared waits on this condition variable. + std::condition_variable m_writersCV; ///< lockExclusive waits on this condition variable. +}; + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_SHAREDREADERLOCK_H diff --git b/src/stringify.h a/src/stringify.h new file mode 100644 index 0000000..0d91bd4 --- /dev/null +++ a/src/stringify.h @@ -0,0 +1,51 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#ifndef OSDSEV_COMPONENTS_MQTT_STRINGIFY_H +#define OSDSEV_COMPONENTS_MQTT_STRINGIFY_H + +// std +#include + +// osdev::components::mqtt +#include "mqttstream.h" + +namespace osdev { +namespace components { +namespace mqtt { + +/** + * @brief Stringifies all objects for which an output stream operator is available. + * @note This method is meant to be used for printing and not for marshalling! + * @tparam T The object type. + * @param value The value to stringify. + * @return stringified value. + */ +template +std::string toString(const T& value) +{ + std::ostringstream oss; + oss << value; + return oss.str(); +} + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_STRINGIFY_H diff --git b/src/stringutils.cpp a/src/stringutils.cpp new file mode 100644 index 0000000..967b9e1 --- /dev/null +++ a/src/stringutils.cpp @@ -0,0 +1,42 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "stringutils.h" + +// std +#include + +namespace osdev { +namespace components { +namespace mqtt { + +void removeCharsFromStringInPlace(std::string& str, const std::string& charsToRemove) +{ + for (std::size_t i = 0; i < charsToRemove.length(); ++i) { + str.erase(std::remove(str.begin(), str.end(), charsToRemove[i]), str.end()); + } +} + +bool is_numeric(const std::string& str) +{ + return std::all_of(str.begin(), str.end(), [](const std::string::value_type& c) { return ::isdigit(c); }); +} + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev diff --git b/src/stringutils.h a/src/stringutils.h new file mode 100644 index 0000000..3fba4fd --- /dev/null +++ a/src/stringutils.h @@ -0,0 +1,51 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#ifndef OSDEV_COMPONENTS_MQTT_STRINGUTILS_H +#define OSDEV_COMPONENTS_MQTT_STRINGUTILS_H + +// See boost/algorithm/string.hpp for more string utility functions + +// std +#include +#include +#include + +namespace osdev { +namespace components { +namespace mqtt { + +/** + * @brief Removes characters from the specified string. Modifies the specified string in place. + * @param str The string to modify. + * @param charsToRemove The characters to remove from str. + */ +void removeCharsFromStringInPlace(std::string& str, const std::string& charsToRemove); + +/** + * @brief Determines whether the specified string is all digits. + * @param str The string for which to determine if it's numeric. + * @return True if the specified string is numeric; otherwise, false. + */ +bool is_numeric(const std::string& str); + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_STRINGUTILS_H diff --git b/src/synchronizedqueue.h a/src/synchronizedqueue.h new file mode 100644 index 0000000..a3f1dd3 --- /dev/null +++ a/src/synchronizedqueue.h @@ -0,0 +1,316 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#ifndef OSDEV_COMPONENTS_MQTT_SYNCHRONIZEDQUEUE_H +#define OSDEV_COMPONENTS_MQTT_SYNCHRONIZEDQUEUE_H + +// std +#include +#include +#include +#include +#include +#include +#include + +// osdev::components::mqtt +#include "lockguard.h" +#include "metaprogrammingdefs.h" + +namespace osdev { +namespace components { +namespace mqtt { + +OSDEV_COMPONENTS_HASMETHOD_TRAIT(capacity, ) + +/*! + * \brief Generic Queue template for defining a thin + * wrapper around std::queue to make overflow detection possible. + * This template has no definition and will lead to a compile + * time error when it is chosen. + */ +template +class Queue; + +/*! + * \brief A specialization when the underlying container has a capacity method. + * To detect overflow the capacity of the underlying container is needed. + * Not all containers have a capacity. + * When the capacity method is not available SFINAE will discard this template. + */ +template +class Queue::value>::type> : public std::queue +{ +public: + using size_type = typename std::queue::size_type; + + typename C::size_type capacity() const + { + return this->c.capacity(); + } +}; + +/*! + * \brief A specialization for when the underlying container does not support a capacity + * In this case max_size is returned which results in overflow not being detected. + */ +template +class Queue::value>::type> : public std::queue +{ +public: + using size_type = typename std::queue::size_type; + typename C::size_type capacity() const + { + return this->c.max_size(); + } +}; + +/*! + * \brief Represents a synchronized queue + * @tparam T The type of the items in the queue + * @tparam C The underlying character. The container must satisfy the + * requirements of a SequenceContainer. + * Addittionally container must supply a pop_front and a max_size method. + * + * The underlying container determines the overflow behaviour. + * A circular buffer leads to a lossy queue that drops items when the + * queue is full while a std::deque will never overflow. + * + * The queue has the following states: started, paused, stopped. + * In the started state the queue acceptsincoming items and it allows items + * to be popped when data is available. + * In the paused state incoming items are allowed. The pop method will + * block until the queue is unpaused or stopped. + * In the stopped state incoming items are not allowed and dropped. + * The pop method will return false. + */ +template > +class SynchronizedQueue +{ +public: + using QueueType = Queue; + + /*! + * \brief Constructs an empty queue + * \param id - Identification string for this queue ( used in logging ). + * \param paused - The state in which to setup the queue ( pause or active), + * ( Default is active ) + */ + explicit SynchronizedQueue( const std::string &id, bool paused = false ) + : m_id( id ) + , m_queueMutex() + , m_dataAvailableOrStopCV() + , m_queue() + , m_stop( false ) + , m_pause( paused ) + , m_numOverflows( 0 ) + {} + + /*! + * \brief Stops the queue on destruction + */ + ~SynchronizedQueue() + { + this->stop(); + } + + /*! + * @brief Pushes the item in the queue + * @tparam TItem - The cv qualified type of the value to push. + * In a stopped state the queue drops incoming data. + * In a paused / active state the queue accepts incoming data. + * @tparam item - The item to push to the queue. + */ + template + void push(TItem &&item) + { + OSDEV_COMPONENTS_UNIQUELOCK_CREATE_SPECIFIC( m_queueMutex, m_id ); + if( m_stop ) + { + return; + } + + if( m_queue.capacity() == m_queue.size() ) + { + if( m_numOverflows++ % 100 == 0 ) + { + // Log a warning that there is a number of overflows. + } + } + m_queue.push( std::forward(item) ); + OSDEV_COMPONENTS_UNIQUELOCK_UNLOCK_SPECIFIC(m_queueMutex, m_id); + m_dataAvailableOrStopCV.notify_one(); + } + + /*! + * @brief pop - Pops an item from the queue. This method blocks when te state is paused or when there is no data available. + * @param item - The item to which to copy the popped item. + * @return True if an item was popped: otherwise false + */ + bool pop(T& item) + { + OSDEV_COMPONENTS_UNIQUELOCK_CREATE_SPECIFIC( m_queueMutex, m_id ); + m_dataAvailableOrStopCV.wait(OSDEV_COMPONENTS_UNIQUELOCK(m_queueMutex), [this]() + { return ((this->m_queue.size() > 0 && !m_pause) || this->m_stop); }); + if( m_stop ) + { + return false; + } + item = std::move(m_queue.front()); + m_queue.pop(); + return true; + } + + bool pop(std::vector &items) + { + OSDEV_COMPONENTS_UNIQUELOCK_CREATE_SPECIFIC( m_queueMutex, m_id ); + m_dataAvailableOrStopCV.wait(OSDEV_COMPONENTS_UNIQUELOCK(m_queueMutex), [this]() + { return ((this->m_queue.size() > 0 && !m_pause) || this->m_stop); }); + if( m_stop ) + { + return false; + } + items.clear(); + items.reserve(m_queue.size()); + while( m_queue.size() > 0 ) + { + items.emplace_back(std::move(m_queue.front() ) ); + m_queue.pop(); + } + return true; + } + + /*! + * \return The current size of the queue + */ + typename QueueType::size_type size() const + { + OSDEV_COMPONENTS_LOCKGUARD_SPECIFIC(m_queueMutex, m_id); + return m_queue.size(); + } + + + /*! + * \brief Start the Queue + * The queue is only started when it is in a stopped state. + * \param paused - If true, the queue will be started in a paused + * state which means that no items will be popped. + */ + void start(bool paused) + { + // Reason that a lock is used: See documentation of std::condition_variable + // + // Even is the shared variable is atomic (m_stop in this case), it must be modified under the mutex + // in order to correctly publish the modification to the waiting thread. + // + OSDEV_COMPONENTS_UNIQUELOCK_CREATE_SPECIFIC(m_queueMutex, m_id); + if( !m_stop ) + { + // already started + return; + } + m_stop = false; + m_pause = paused; + OSDEV_COMPONENTS_UNIQUELOCK_UNLOCK_SPECIFIC(m_queueMutex, m_id); + if( !paused ) + { + m_dataAvailableOrStopCV.notify_all(); + } + } + + /*! + * \brief Pause or unpause the queue. + * When the queue is paused no items will be popped. + * The state is not altered when the queue is stopped. + * \param value - Flag that indicates whether the queue is paused or unpaused. + */ + void pause(bool value) + { + // Reason that a lock is used: see documentation of std::condition_variable + // + // Even if the shared variable is atomic (m_stop in this case), it must be modified under the mutex + // in order to correctly publish the modification to the waiting thread. + // + OSDEV_COMPONENTS_UNIQUELOCK_CREATE_SPECIFIC(m_queueMutex, m_id); + if (m_stop) { + return; + } + m_pause = value; + OSDEV_COMPONENTS_UNIQUELOCK_UNLOCK_SPECIFIC(m_queueMutex, m_id); + if (!value) { + m_dataAvailableOrStopCV.notify_all(); + } + } + + /*! + * \brief Stop the queue. + * The pop method will return a false after calling this method. + * The queue can be restarted with the start method. + */ + void stop() + { + // Reason that a lock is used: see documentation of std::condition_variable + // + // Even if the shared variable is atomic (m_stop in this case), it must be modified under the mutex + // in order to correctly publish the modification to the waiting thread. + // + OSDEV_COMPONENTS_UNIQUELOCK_CREATE_SPECIFIC(m_queueMutex, m_id); + m_stop = true; + m_pause = false; + OSDEV_COMPONENTS_UNIQUELOCK_UNLOCK_SPECIFIC(m_queueMutex, m_id); + m_dataAvailableOrStopCV.notify_all(); + } + + /*! + * \brief Clears the queue. + * This method also resets the overflow counter. + */ + void clear() + { + OSDEV_COMPONENTS_LOCKGUARD_SPECIFIC(m_queueMutex, m_id); + QueueType emptyQueue; + std::swap(m_queue, emptyQueue); + m_numOverflows = 0; + } + +private: + const std::string m_id; ///< Queue identification string + mutable std::mutex m_queueMutex; ///< Protects access to the queue + std::condition_variable m_dataAvailableOrStopCV; ///< Provides wait functionality for the queue becoming empty + QueueType m_queue; ///< Holds the items + std::atomic_bool m_stop; ///< Flag that indicates whether the queue needs to stop. + std::atomic_bool m_pause; ///< Flag that indicates whether the queue is paused. + + /*! + * \brief Counts the number of items that the buffer overflows. + * If the underlying buffer is a ring buffer an overflow + * means that an item will be overwritten. For a normal + * sequence container it means that the it is enlarged. + */ + std::uint32_t m_numOverflows; +}; + + + + + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_SYNCHRONIZEDQUEUE_H diff --git b/src/timemeasurement.cpp a/src/timemeasurement.cpp new file mode 100644 index 0000000..09a2044 --- /dev/null +++ a/src/timemeasurement.cpp @@ -0,0 +1,52 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "timemeasurement.h" + +using namespace osdev::components::mqtt::measurement; + +TimeMeasurement::TimeMeasurement(const std::string& id, const TimeMeasurementCallback& cb, bool measureOnDestruction) + : m_id(id) + , m_start(std::chrono::steady_clock::now()) + , m_last(m_start) + , m_callback(cb) + , m_measureOnDestruction(measureOnDestruction) +{ +} + +TimeMeasurement::~TimeMeasurement() +{ + if (m_measureOnDestruction) { + this->measure(); + } +} + +void TimeMeasurement::set() +{ + m_last = std::chrono::steady_clock::now(); +} + +void TimeMeasurement::measure() +{ + auto now = std::chrono::steady_clock::now(); + auto sinceLast = std::chrono::duration_cast(now - m_last); + auto sinceStart = std::chrono::duration_cast(now - m_start); + m_last = now; + + m_callback(m_id, m_start, sinceStart, sinceLast); +} diff --git b/src/timemeasurement.h a/src/timemeasurement.h new file mode 100644 index 0000000..c39c4b1 --- /dev/null +++ a/src/timemeasurement.h @@ -0,0 +1,51 @@ +#ifndef OSDEV_COMPONENTS_MQTT_MEASUREMENT_TIMEMEASUREMENT_H +#define OSDEV_COMPONENTS_MQTT_MEASUREMENT_TIMEMEASUREMENT_H + +#include +#include +#include +#include + +namespace osdev { +namespace components { +namespace mqtt { +namespace measurement { + +using TimeMeasurementCallback = std::function; + +class TimeMeasurement +{ +public: + TimeMeasurement(const std::string& id, const TimeMeasurementCallback& callback, bool measureOnDestruction = true); + ~TimeMeasurement(); + + TimeMeasurement(const TimeMeasurement&) = delete; + TimeMeasurement& operator=(const TimeMeasurement&) = delete; + TimeMeasurement(TimeMeasurement&&) = default; + TimeMeasurement& operator=(TimeMeasurement&&) = default; + + void set(); + void measure(); + +private: + std::string m_id; + + std::chrono::steady_clock::time_point m_start; + std::chrono::steady_clock::time_point m_last; + TimeMeasurementCallback m_callback; + bool m_measureOnDestruction; +}; + +template +std::ostream& operator<<(std::ostream& os, const std::chrono::duration& rhs) +{ + os << rhs.count(); + return os; +} + +} // End namespace measurement +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_MEASUREMENT_TIMEMEASUREMENT_H diff --git b/src/token.cpp a/src/token.cpp new file mode 100644 index 0000000..83e0d77 --- /dev/null +++ a/src/token.cpp @@ -0,0 +1,63 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "token.h" + +using namespace osdev::components::mqtt; + +Token::Token() + : m_clientId( "" ) + , m_token( -1 ) +{} + +Token::Token( const std::string &_clientId, std::int32_t _tokenNr ) + : m_clientId( _clientId ) + , m_token( _tokenNr ) +{} + +bool Token::equals( const Token &rhs ) const +{ + return ( rhs.m_clientId == m_clientId && rhs.m_token == m_token); +} + +bool Token::smallerThan( const Token &rhs ) const +{ + if( m_clientId < rhs.m_clientId ) + { + return true; + } + else if( rhs.m_clientId < m_clientId ) + { + return false; + } + + return m_token < rhs.m_token; +} + +namespace osdev { +namespace components { +namespace mqtt { + +std::ostream& operator<<(std::ostream &os, const Token &rhs ) +{ + return os << rhs.clientId() << '-' << rhs.token(); +} + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev diff --git b/src/token.h a/src/token.h new file mode 100644 index 0000000..79da130 --- /dev/null +++ a/src/token.h @@ -0,0 +1,106 @@ +#ifndef OSDEV_COMPONENTS_MQTT_TOKEN_H +#define OSDEV_COMPONENTS_MQTT_TOKEN_H + +// std +#include +#include + +// paho +#include + +namespace osdev { +namespace components { +namespace mqtt { + +/*! + * \brief The Token class defines an operation token + */ +class Token +{ +public: + /*! @brief Construct an invalid token. + * The token number is -1 in that case. The client is undefined, in this case empty. + */ + Token(); + + /*! @brief Constructs token for an operation originating from specific client wrapper. + * @param clientId - Identifies the client wrapper + * @param tokenNr - Identifies the operation done on that client. + */ + Token( const std::string &clientId, std::int32_t tokenNr ); + + /*! @return True when token has a valid token number, false otherwise. */ + bool isValid() const { return -1 == m_token; } + + /*! @return The operation token */ + const std::string& clientId() const { return m_clientId; } + + /*! @return The operation token */ + std::int32_t token() const { return m_token; } + + /*! @return True if Tokens have the same clientId and token number, false otherwise. */ + bool equals( const Token &rhs ) const; + + /*! + * @brief Token is ordered. + * First on lexical test of clientId and with same clientId on token number. + */ + bool smallerThan( const Token &rhs ) const; + +private: + std::string m_clientId; ///< Identified the client + std::int32_t m_token; ///< Identifies the operation on that client. +}; + +/** + * @return True if Tokens have the same clientId and token number, false otherwise. + */ +inline bool operator==( const Token &lhs, const Token &rhs ) +{ + return lhs.equals( rhs ); +} + +inline bool operator==( const Token &lhs, std::int32_t rhs ) +{ + return lhs.token() == rhs; +} + +inline bool operator==( std::int32_t lhs, const Token &rhs ) +{ + return lhs == rhs; +} + +template +inline bool operator!=( const TLeft &lhs, const TRight &rhs ) +{ + return !( lhs == rhs ); +} + +/*! + * @return True if Token lhs is smaller than token rhs + */ +inline bool operator<( const Token &lhs, std::int32_t rhs ) +{ + return lhs.token() < rhs; +} + +inline bool operator<( std::int32_t lhs, const Token &rhs ) +{ + return lhs < rhs.token(); +} + +inline bool operator<( const Token &lhs, const Token &rhs ) +{ + return lhs.smallerThan( rhs ); +} + +/*! + * @brief Stream operator for a Token + */ +std::ostream& operator<<( std::ostream &os, const Token &rhs ); + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_TOKEN_H diff --git b/src/uriparser.cpp a/src/uriparser.cpp new file mode 100644 index 0000000..3cf48d1 --- /dev/null +++ a/src/uriparser.cpp @@ -0,0 +1,374 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "uriparser.h" + +// std +#include +#include +#include + +// gnu-c +#include + +// boost +#include +#include +#include +#include + +// osdev::components::mqtt +#include "bimap.h" +#include "stringutils.h" +#include "uriutils.h" + +// mlogic::common::logger +// #include "mlogic/common/logger/loggerprovider.h" +// #include "mlogic/common/invalidargumentexception.h" +// #include "mlogic/common/nullptrexception.h" +// #include "mlogic/common/systemexception.h" + + +using namespace osdev::components::mqtt; + +namespace { + +/** + * @brief Copies an item from the from container to the to container. + * @tparam TFrom The type of the container from which to copy. + * @tparam TTo The type of the container to which to copy. + * @param itemName The name of the item to copy. + * @param from The container from which to copy. + * @param to The cointainer to which to copy. + * @param transformation Apply transformation function to the input. Default no transformation. + */ +template +void copyItem(const std::string& itemName, const TFrom& from, TTo& to, const std::function& transformation = std::function()) +{ + if (transformation) { + to.insert(std::make_pair(itemName, transformation(from[itemName]))); + } + else { + to.insert(std::make_pair(itemName, from[itemName])); + } +} + +const std::string& getItem(const std::map& source, const std::string& itemName) +{ + static const std::string s_empty; + const auto cit = source.find(itemName); + if (cit != source.end()) { + return cit->second; + } + return s_empty; +} + +static const boost::bimap& getReservedCharacterMap() +{ + static const auto s_lookupTable = makeBimap( + { { ':', percentEncode<':'>() }, + { '/', percentEncode<'/'>() }, + { '?', percentEncode<'?'>() }, + { '#', percentEncode<'#'>() }, + { '[', percentEncode<'['>() }, + { ']', percentEncode<']'>() }, + { '@', percentEncode<'@'>() }, + { '!', percentEncode<'!'>() }, + { '$', percentEncode<'$'>() }, + { '&', percentEncode<'&'>() }, + { '\'', percentEncode<'\''>() }, + { '(', percentEncode<'('>() }, + { ')', percentEncode<')'>() }, + { '*', percentEncode<'*'>() }, + { '+', percentEncode<'+'>() }, + { ',', percentEncode<','>() }, + { ';', percentEncode<';'>() }, + { '=', percentEncode<'='>() }, + + { '"', percentEncode<'"'>() }, + { '%', percentEncode<'%'>() }, + { '-', percentEncode<'-'>() }, + { '.', percentEncode<'.'>() }, + { '<', percentEncode<'<'>() }, + { '>', percentEncode<'>'>() }, + { '\\', percentEncode<'\\'>() }, + { '^', percentEncode<'^'>() }, + { '_', percentEncode<'_'>() }, + { '`', percentEncode<'`'>() }, + { '{', percentEncode<'{'>() }, + { '|', percentEncode<'|'>() }, + { '}', percentEncode<'}'>() }, + { '~', percentEncode<'~'>() } }); + + return s_lookupTable; +} + +std::string decode(const std::string& in) +{ + static constexpr size_t encodingTokenSize = 3; // example: %20 encodes a space character. + const auto& reservedLookup = getReservedCharacterMap(); + + std::string out = in; + std::size_t pos = 0; + while ((pos = out.find('%', pos)) != std::string::npos) { + if (pos + encodingTokenSize > out.size()) { + // MLOGIC_COMMON_THROW(InvalidArgumentException, "Invalid encoding at end of string"); + } + const auto cit = reservedLookup.right.find(out.substr(pos, 3)); + if (reservedLookup.right.end() != cit) { + // string& replace (size_t pos, size_t len, size_t n, char c) + // where n is the number of fill characters (1 in this case). + out.replace(pos, encodingTokenSize, 1, cit->second); + } + ++pos; + } + return out; +} + +std::string encode(const std::string& in) +{ + const auto& reservedLookup = getReservedCharacterMap(); + + std::string out = in; + for (size_t pos = 0; pos < out.size(); ++pos) { + const auto cit = reservedLookup.left.find(out[pos]); + if (reservedLookup.left.end() != cit) { + out.replace(pos, 1, cit->second); + pos += 2; + } + } + return out; +} + +} // anonymous + + +// static +ParsedUri UriParser::parse(const std::string& uri) +{ + // Before turning to regular expressions, the following ibraries were evaluated to achieve this functionality: + // Qt QUrlParser: http://doc.qt.io/qt-4.8/qurl.html + // cpp-netlib: https://github.com/cpp-netlib/uri + // uriparser: http://uriparser.sourceforge.net/ + // From the above, cpp-netlib was the most compelling because of its pending standardization for C++(17?). + // However, none of these libraries could handle strings for port service names, so a custom implementation seems necessary. + // As an additional validation step, one of the above libraries could be used after the port service name was replaced (see below). + // + // Split the uri in two stages. First break down the uri in its components: scheme (cannot be empty), authority (cannot be empty), path, query, fragment. + // The path, query and fragment parts are optional. Because the scheme and authority part cannot be empty only a subset of uri's is handled by this function. + // In the second stage the authority is parsed to get the hostname and the port. In order to use an ipv6 host address it must be + // wrapped in brackets. The addresses are not validated whether they are correct. When a open bracket is found the part that is between brackets + // must contain at least two colons. This is enough to discern something that resembles an ipv6 address from the other possibilities. + static const std::string regexUriString( + R"regex(^(?[^:/?#]+)://(?[^/?#]+)(?[^?#]*)(?\?(?[^#]*))?(?#(?.*))?)regex"); + + // This regex only checks for non occurrence of the @ symbol since the input is the result from the regexUri where it is already validated that characters "/>#" do not exist + // in the authority part. This regexAuthority uses the if then else construct "(?(?=regex)then|else)" to choose between ipv6 or something else. + static const std::string regexAuthorityString( + R"regex(^((?[^:@]+)(:(?[^@]+))?@)?(?(?=(?\[))\[(?=(.*:){2,}.*)(?[^@]+)\]|(?[^:@]+))(?:(?[^:@]+))?$)regex"); + + static boost::regex uriRegex(regexUriString); + boost::cmatch whatUri; + auto uriMatches = boost::regex_match( + uri.c_str(), + whatUri, + uriRegex); + if (!uriMatches) + { + // ErrorLogToFile("UriParser", "Invalid uri: '%1'", uri); + // throw (InvalidArgumentException, "No match for the specified uri."); + } + + static boost::regex authorityRegex(regexAuthorityString); + boost::cmatch whatAuthority; + std::string authority = whatUri["authority"]; + auto authorityMatches = boost::regex_match( + authority.c_str(), + whatAuthority, + authorityRegex); + if (!authorityMatches) + { + // ErrorToLogFile("UriParser", "Uri contains invalid authority part: %1", authority); + // Throw (InvalidArgumentException, "Uri contains invalid authority part."); + } + + static const auto toLower = [](const std::string& in) -> std::string { return boost::to_lower_copy(in); }; + + ParsedUri parsedUri; + copyItem("scheme", whatUri, parsedUri, toLower); + copyItem("user", whatAuthority, parsedUri, &decode); + copyItem("password", whatAuthority, parsedUri, &decode); + copyItem("ipv6", whatAuthority, parsedUri); // Acts as a flag. Empty means not ipv6, not empty means ipv6 (this is not validated in the parse however!) + copyItem("host", whatAuthority, parsedUri, toLower); + copyItem("port", whatAuthority, parsedUri, toLower); + copyItem("path", whatUri, parsedUri); + copyItem("query", whatUri, parsedUri); + copyItem("fragment", whatUri, parsedUri, &decode); + + return parsedUri; +} + +// static +ParsedQuery UriParser::parseQuery(const ParsedUri& parsedUri) +{ + const auto cit = parsedUri.find("query"); + if (parsedUri.end() == cit) { + return {}; + } + + const auto& queryString = cit->second; + + std::vector keyValues; + boost::algorithm::split(keyValues, queryString, [](char ch) { return ch == '&'; }); + + std::map retval; + + for (const auto& query : keyValues) { + auto pos = query.find('='); + if (std::string::npos != pos) { + retval[decode(query.substr(0, pos))] = pos + 1 < query.size() ? decode(query.substr(pos + 1)) : ""; + } + } + return retval; +} + +// static +ParsedPath UriParser::parsePath(const ParsedUri& parsedUri) +{ + const auto cit = parsedUri.find("path"); + if (parsedUri.end() == cit) { + return {}; + } + + const auto& pathString = cit->second; + + ParsedPath pathElements; + if (!pathString.empty()) { + const auto path = pathString.substr(1); + boost::algorithm::split(pathElements, path, [](char ch) { return ch == '/'; }); // empty string will lead to a single pathElement + std::transform(pathElements.begin(), pathElements.end(), pathElements.begin(), &decode); + } + + return pathElements; +} + +// static +std::string UriParser::normalize(const std::string& uri) +{ + auto parsedUri = parse(uri); + + auto& portString = parsedUri["port"]; + if (!portString.empty() && !is_numeric(portString)) { + auto portnumber = getPortnumber(portString); + portString = boost::lexical_cast(portnumber); + } + + return toString(parsedUri); +} + +// static +std::string UriParser::toString(const ParsedUri& parsedUri) +{ + const auto& schemeString = getItem(parsedUri, "scheme"); + const auto& hostString = getItem(parsedUri, "host"); + + if( schemeString.empty() ) + { + // Do something sensible + } + + if( hostString.empty() ) + { + // Do something sensible. + } + + const auto& user = getItem(parsedUri, "user"); + const auto& password = getItem(parsedUri, "password"); + const auto& portString = getItem(parsedUri, "port"); + const auto& pathString = getItem(parsedUri, "path"); + const auto& queryString = getItem(parsedUri, "query"); + const auto& fragmentString = getItem(parsedUri, "fragment"); + + // wrap hostname in brackets only if the incoming url uses them + bool ipv6 = !getItem(parsedUri, "ipv6").empty(); + + std::ostringstream oss; + oss << schemeString << "://"; + + if (!user.empty()) { + oss << encode(user); + if (!password.empty()) { + oss << ":" << encode(password); + } + oss << "@"; + } + + if (ipv6) { + oss << '['; + } + oss << hostString; + if (ipv6) { + oss << ']'; + } + + if (!portString.empty()) { + oss << ':' << portString; + } + + if (!pathString.empty()) { + auto pathElements = UriParser::parsePath(parsedUri); + for (const auto& element : pathElements) { + oss << "/" << encode(element); + } + } + if (!queryString.empty()) { + auto queryElements = UriParser::parseQuery(parsedUri); + oss << '?'; + for (auto cit = queryElements.cbegin(); queryElements.cend() != cit; ++cit) { + oss << encode(cit->first) << '=' << encode(cit->second); + if (next(cit) != queryElements.cend()) { + oss << '&'; + } + } + } + if (!fragmentString.empty()) { + oss << '#' << encode(fragmentString); + } + return oss.str(); +} + +// static +int UriParser::getPortnumber(const std::string& service, const std::string& protocolName) +{ + const unsigned int bufSize = 1024; + struct servent data; + struct servent* result; + char buf[bufSize]; // contains the strings that data points to + ::getservbyname_r(service.c_str(), protocolName.c_str(), &data, buf, bufSize, &result); + if (nullptr == result) + { + // ErrorLogToFile ("UriParser", "Could not determine the portnumber for the specified service: %1 for protocol: %2. Please check the portnumber configuration in /etc/services.", service, protocolName); + // throw (?) (InvalidArgumentException, "Could not determine the portnumber for the specified service and/or protocol."); + } + +// htonx functions need -Wold-style-cast disabled +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" + return static_cast(ntohs(static_cast(data.s_port))); +#pragma GCC diagnostic pop +} diff --git b/src/uriparser.h a/src/uriparser.h new file mode 100644 index 0000000..9e0be0f --- /dev/null +++ a/src/uriparser.h @@ -0,0 +1,87 @@ +#ifndef OSDEV_COMPONENTS_MQTT_URIPARSER_H +#define OSDEV_COMPONENTS_MQTT_URIPARSER_H + +// std +#include + +#include "commondefs.h" + +namespace osdev { +namespace components { +namespace mqtt { + +/** + * @brief Helper class to parse, normalize, and replace the port service name by portnumber in a uri. + * example: + * opc.tcp://user:secret\@processmodel:processmodel/path1/path2?query3=value4#fragment5 + * will result in + * opc.tcp://user:secret\@processmodel:12345/path1/path2?query3=value4#fragment5 + * @note This assumes that the port service name was registered in the etc/services file under protocol tcp. + * Lookup of a portnumber for protocols other than tcp are not supported. + * + * IPv6 addresses in a uri are only parsable when they are wrapped in brackets ([]) (RFC 3986, section 3.2.2) + * + * @note This class is designed to handle a subset of all possible uris. The scheme and the authority part are + * expected not to be empty. The authority part can contain user and password information. + * + * @note Only scheme, hostname and port are converted to lowercase (see RFC 3986 6.2.2.1. Case Normalization) + * + * @note Special characters are escaped with percent encoding. This applies to user, password, path, query and fragment parts. + * The path and query part is escaped on a per element basis. This means that ParsedUri contains the raw query and path strings. + * The decoding is done when the methods parseQuery and parsePath are called. + */ +class UriParser +{ +public: + /** + * @brief Parses the specified uri string. + * @param uri The uri string to parse. + * @return A map containing the parsed uri. + * @note The path and query parts can still contain percent encoded special characters. + */ + static ParsedUri parse(const std::string& uri); + + /** + * @brief Parse the query part of a parsed uri. Percent encoded special characters are decoded. + * @param parsedUri A ParsedUri object. + * @return key/value map. + */ + static ParsedQuery parseQuery(const ParsedUri& parsedUri); + + /** + * @brief Parse the path part of a parsed uri. Percent encoded special characters are decoded. + * @param parsedUri A ParsedUri object. + * @return vector of path elements. + */ + static ParsedPath parsePath(const ParsedUri& parsedUri); + + /** + * @brief Parses, normalizes, and replaces the port service name by portnumber in the specified uri string. + * @param uri The uri string to parse and normalize. + * @return The normalized uri string, with the port service name replaced by the portnumber. + */ + static std::string normalize(const std::string& uri); + + /** + * @brief Converts a parsed uri back to a string. + * @param parsedUri The parsed uri to convert to string. + * @return The uri as string. + */ + static std::string toString(const ParsedUri& parsedUri); + + /** + * @brief Get portnumber associated with a service. + * @param serviceName Name of the service for which a portnumber is searched. + * @param protocolName Name of the protocol for which the portname was registered. + * @return portnumber. + * @throws InvalidArgumentException when service is unknown. + * @throws SystemException when underlying systemcall fails. + */ + static int getPortnumber(const std::string& serviceName, const std::string& protocolName = "tcp"); +}; + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_URIPARSER_H diff --git b/src/uriutils.h a/src/uriutils.h new file mode 100644 index 0000000..591d4cd --- /dev/null +++ a/src/uriutils.h @@ -0,0 +1,54 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#ifndef OSDEV_COMPONENTS_MQTT_URIUTILS_H +#define OSDEV_COMPONENTS_MQTT_URIUTILS_H + +// std +#include + +#include "compiletimedigits.h" + +namespace osdev { +namespace components { +namespace mqtt { + +/** + * @brief Get the percent encoded string for a given character. + * @tparam N The character to encode. + * @return pointer to the encoded character string. + */ +template +inline const char* percentEncode() +{ + static const auto* s_code = + (compiletime_string<'%'>{} + typename apply_bounded_range< // + 0, // + numberOfDigits<16>(N), // + string_builder< // + adapt_for_string_builder< // + ProduceDigits>>::template produce>::result{}) + .chars; + return s_code; +} + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_URIUTILS_H diff --git b/src/utils.cpp a/src/utils.cpp new file mode 100644 index 0000000..7955f7b --- /dev/null +++ a/src/utils.cpp @@ -0,0 +1,52 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "utils.h" + +// std +#include +#include +#include + +#include + +using namespace osdev::components::mqtt; + +std::string getPathToBinary(int pid) +{ + static const int sMaxPath = PATH_MAX; + pid_t p = pid; + if (-1 == pid) + { + p = getpid(); + } + std::ostringstream oss; + oss << "/proc/" << p << "/exe"; + char buf[sMaxPath] = { 0 }; + ssize_t r = readlink(oss.str().c_str(), buf, sMaxPath); + if (-1 == r) + { + // "readlink failed" + } + if (r >= sMaxPath) + { + buf[sMaxPath - 1] = '\0'; // make the buffer 0 terminated + // "path is too long" + } + return dirname(buf); +} diff --git b/src/utils.h a/src/utils.h new file mode 100644 index 0000000..ce77820 --- /dev/null +++ a/src/utils.h @@ -0,0 +1,228 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#ifndef OSDEV_COMPONENTS_MQTT_UTILS_H +#define OSDEV_COMPONENTS_MQTT_UTILS_H + +// std +#include +#include +#include +#include + +namespace osdev { +namespace components { +namespace mqtt { + +/** + * @brief Does nothing. + * Utility template function to explicitly use parameters, that would otherwise be unused. + * This helps to prevent the -Wunused-parameter warning. + */ +template +void apply_unused_parameters(const Args&...) +{ +} + +/** + * @brief Converts (dynamic_cast) a std::unique_ptr from TFrom to TTo. + * @param from The std::unique_ptr to convert from TFrom to TTo. + * @return The converted std::unique_ptr. + */ +template +std::unique_ptr dynamic_unique_ptr_cast(std::unique_ptr&& from) +{ + auto to = dynamic_cast(from.get()); + if (nullptr == to) { + return std::unique_ptr(nullptr); + } + + from.release(); + return std::unique_ptr(to); +} + +/** + * @brief Converts (dynamic_cast) a std::unique_ptr from TFrom to TTo. + * @param from The std::unique_ptr to convert from TFrom to TTo. + * @return The converted std::unique_ptr. + */ +template +std::unique_ptr dynamic_unique_ptr_cast(std::unique_ptr&& from) +{ + auto to = dynamic_cast(from.get()); + if (nullptr == to) { + return std::unique_ptr(nullptr, from.get_deleter()); + } + + from.release(); + return std::unique_ptr(to, std::move(from.get_deleter())); +} + +/** + * @brief Helper class for iteration of keys of a map. + */ +template +class KeyIterator : public TMap::iterator +{ +public: + typedef typename TMap::iterator MapIterator; + typedef typename MapIterator::value_type::first_type KeyType; + + KeyIterator(const MapIterator& other) + : TMap::iterator(other) + { + } + + KeyType& operator*() + { + return TMap::iterator::operator*().first; + } +}; + +/** + * @brief Helper function to get the begin KeyIterator from a map. + */ +template +KeyIterator KeyBegin(MapType& map) +{ + return KeyIterator(map.begin()); +} + +/** + * @brief Helper function to get the end KeyIterator from a map. + */ +template +KeyIterator KeyEnd(MapType& map) +{ + return KeyIterator(map.end()); +} + +/** + * @brief Helper class for iteration of keys of a const map. + */ +template +class KeyConstIterator : public TMap::const_iterator +{ + typedef typename TMap::const_iterator TMapIterator; + typedef typename TMapIterator::value_type::first_type TKeyType; + +public: + KeyConstIterator(const TMapIterator& other) + : TMapIterator(other) + { + } + + const TKeyType& operator*() + { + return TMapIterator::operator*().first; + } +}; + +/** + * @brief Helper function to get the cbegin KeyConstIterator from a const map. + */ +template +KeyConstIterator KeyBegin(const TMap& map) +{ + return KeyConstIterator(map.cbegin()); +} + +/** + * @brief Helper function to get the cend KeyConstIterator from a const map. + */ +template +KeyConstIterator KeyEnd(const TMap& map) +{ + return KeyConstIterator(map.cend()); +} + +/** + * @brief Helper function to get the difference of the keys in two maps. + * @param map1 The first map for which to examine the keys. + * @param map2 The second map for which to examine the keys. + * @return Collection of keys present in map1, but not in map2. + * @note The types of the keys in the maps must be identical. + */ +template +std::vector keyDifference( + const TMap1& map1, + const TMap2& map2, + const std::function& keyCompare = std::less()) +{ + static_assert(std::is_same< + typename TMap1::key_type, + typename TMap2::key_type>::value, + "Inconsistent key types."); + + typedef typename TMap1::key_type Key; + std::vector onlyInMap1; + std::set_difference( + KeyBegin(map1), + KeyEnd(map1), + KeyBegin(map2), + KeyEnd(map2), + std::inserter(onlyInMap1, onlyInMap1.begin()), + keyCompare); + return onlyInMap1; +} + +/** + * @brief Helper function to get the intersection of the keys in two maps. + * @param map1 The first map for which to examine the keys. + * @param map2 The second map for which to examine the keys. + * @return Collection of keys present in both maps. + * @note The types of the keys in the maps must be identical. + */ +template +std::vector keyIntersection( + const TMap1& map1, + const TMap2& map2, + const std::function& keyCompare = std::less()) +{ + static_assert(std::is_same< + typename TMap1::key_type, + typename TMap2::key_type>::value, + "Inconsistent key types."); + + typedef typename TMap1::key_type Key; + std::vector inBothMaps; + std::set_intersection( + KeyBegin(map1), + KeyEnd(map1), + KeyBegin(map2), + KeyEnd(map2), + std::inserter(inBothMaps, inBothMaps.begin()), + keyCompare); + return inBothMaps; +} + +/** + * @brief Determine the absolute path of the binary that belongs to a process. + * @param pid Process Id ifor which to determine the path to the binary. If pid equals -1 then the pid of the current process is used. + * @return Absolute path to the binary. + * @throw SystemException if call to readlink fails. + * @throw PathException if path is to long. + * @note Works only for processes owned by the user that is calling this function. + */ +std::string getPathToBinary(int pid = -1); + +} // End namespace mqtt +} // End namespace components +} // End namespace osdev + +#endif // OSDEV_COMPONENTS_MQTT_UTILS_H diff --git b/tests/CMakeLists.txt a/tests/CMakeLists.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ a/tests/CMakeLists.txt diff --git b/tests/pub/CMakeLists.txt a/tests/pub/CMakeLists.txt new file mode 100644 index 0000000..0e3c279 --- /dev/null +++ a/tests/pub/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.0) +LIST(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../cmake) + +include(projectheader) +project_header(test_mqtt_pub) + +include_directories( SYSTEM + ${CMAKE_CURRENT_SOURCE_DIR}/../../src +) + +include(compiler) +set(SRC_LIST + ${CMAKE_CURRENT_SOURCE_DIR}/publisher.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/publisher.h + ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp +) + +add_executable( ${PROJECT_NAME} + ${SRC_LIST} +) + +target_link_libraries( + ${PROJECT_NAME} + mqtt +) + +set_target_properties( ${PROJECT_NAME} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib + ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/archive +) + +include(installation) +install_application() diff --git b/tests/pub/main.cpp a/tests/pub/main.cpp new file mode 100644 index 0000000..69bbfeb --- /dev/null +++ a/tests/pub/main.cpp @@ -0,0 +1,96 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ + +// std +#include +#include +#include + +#include "publisher.h" + +enum TIME_RES +{ + T_MICRO, + T_MILLI, + T_SECONDS +}; + +std::uint64_t getEpochUSecs() +{ + auto tsUSec =std::chrono::time_point_cast(std::chrono::system_clock::now()); + return static_cast(tsUSec.time_since_epoch().count()); +} + + +void sleepcp( int number, TIME_RES resolution = T_MILLI ) // Cross-platform sleep function +{ + int factor = 0; // Should not happen.. + + switch( resolution ) + { + case T_MICRO: + factor = 1; + break; + + case T_MILLI: + factor = 1000; + break; + + case T_SECONDS: + factor = 1000000; + break; + } + + usleep( number * factor ); +} + +int main( int argc, char* argv[] ) +{ + // We're not using the command parameters, so we just want to keep the compiler happy. + (void)argc; + (void)argv; + + uint64_t messageNumber = 0; + + // Create the publisher, run it and publish a message every 0.001 sec. + std::cout << "Create the publisher object : "; + Publisher *pPublisher = new Publisher(); + if( pPublisher != nullptr ) + { + std::cout << "{OK}" << std::endl; + std::cout << "Connecting to the broker : "; + pPublisher->connect( "localhost", 1883, "", "" ); + + // Assume we are connected now, start publishing. + while( 1 ) + { + std::string payload = "" ; + pPublisher->publish( std::string( "test/publisher/TestPublisher" ), payload ); + + sleepcp( 1, T_SECONDS ); + if( messageNumber > 2000000000 ) + messageNumber = -1; + + messageNumber++; + } + } + else + return -1; + +} diff --git b/tests/pub/publisher.cpp a/tests/pub/publisher.cpp new file mode 100644 index 0000000..e371503 --- /dev/null +++ a/tests/pub/publisher.cpp @@ -0,0 +1,43 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ + +// osdev::components::mqtt +#include "token.h" + +// mqtt_tests +#include "publisher.h" + +Publisher::Publisher() + : m_mqtt_client( "TestPublisher" ) +{ + +} + +void Publisher::connect( const std::string &hostname, int portnumber, const std::string &username, const std::string &password ) +{ + m_mqtt_client.connect( hostname, portnumber, osdev::components::mqtt::Credentials( username, password ) ); + std::cout << "Client state : " << m_mqtt_client.state() << std::endl; +} + +void Publisher::publish( const std::string &message_topic, const std::string &message_payload ) +{ + osdev::components::mqtt::MqttMessage message( message_topic, true, false, message_payload ); + std::cout << "[Publisher::publish] - Publising message : " << message_payload << " to topic : " << message_topic << std::endl; + osdev::components::mqtt::Token t_result = m_mqtt_client.publish( message, 0 ); +} diff --git b/tests/pub/publisher.h a/tests/pub/publisher.h new file mode 100644 index 0000000..786420f --- /dev/null +++ a/tests/pub/publisher.h @@ -0,0 +1,42 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#pragma once + +// std +#include +#include + +// osdev::components::mqtt +#include "mqttclient.h" +#include "compat-c++14.h" + +class Publisher +{ +public: + Publisher(); + + virtual ~Publisher() {} + + void connect( const std::string &hostname, int portnumber = 1883, const std::string &username = std::string(), const std::string &password = std::string() ); + + void publish( const std::string &message_topic, const std::string &message_payload ); + +private: + osdev::components::mqtt::MqttClient m_mqtt_client; +}; diff --git b/tests/sub/CMakeLists.txt a/tests/sub/CMakeLists.txt new file mode 100644 index 0000000..f118452 --- /dev/null +++ a/tests/sub/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.0) +LIST(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../cmake) + +include(projectheader) +project_header(test_mqtt_sub) + +include_directories( SYSTEM + ${CMAKE_CURRENT_SOURCE_DIR}/../../src +) + +include(compiler) +set(SRC_LIST + ${CMAKE_CURRENT_SOURCE_DIR}/subscriber.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/subscriber.h + ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp +) + +add_executable( ${PROJECT_NAME} + ${SRC_LIST} +) + +target_link_libraries( + ${PROJECT_NAME} + mqtt +) + +set_target_properties( ${PROJECT_NAME} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib + ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/archive +) + +include(installation) +install_application() diff --git b/tests/sub/main.cpp a/tests/sub/main.cpp new file mode 100644 index 0000000..ef9199e --- /dev/null +++ a/tests/sub/main.cpp @@ -0,0 +1,92 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ + +// std +#include +#include +#include + +#include "subscriber.h" + +enum TIME_RES +{ + T_MICRO, + T_MILLI, + T_SECONDS +}; + +std::uint64_t getEpochUSecs() +{ + auto tsUSec =std::chrono::time_point_cast(std::chrono::system_clock::now()); + return static_cast(tsUSec.time_since_epoch().count()); +} + + +void sleepcp( int number, TIME_RES resolution = T_MILLI ) // Cross-platform sleep function +{ + int factor = 0; // Should not happen.. + + switch( resolution ) + { + case T_MICRO: + factor = 1; + break; + + case T_MILLI: + factor = 1000; + break; + + case T_SECONDS: + factor = 1000000; + break; + } + + usleep( number * factor ); +} + +int main( int argc, char* argv[] ) +{ + // Satisfy the compiler + (void)argc; + (void)argv; + + std::cout << "Creating the subscriber : "; + // Create the subscriber + Subscriber *pSubscriber = new Subscriber(); + if( pSubscriber != nullptr ) + { + std::cout << "[OK]" << std::endl; + std::cout << "Connecting to the test-broker : " << std::endl; + pSubscriber->connect( "localhost", 1883, "", "" ); + std::cout << "Subscribing to the test-topic....." << std::endl; + pSubscriber->subscribe( "test/publisher/TestPublisher" ); + + // Start a loop to give the subscriber the possibility to do its work. + while( 1 ) + { + sleepcp( 1, T_SECONDS ); // Sleep 1 Sec to give the scheduler the change to interfene. + } + } + else + { + std::cout << "[FAILED]" << std::endl; + return -1; + } + return 0; +} diff --git b/tests/sub/subscriber.cpp a/tests/sub/subscriber.cpp new file mode 100644 index 0000000..68804e4 --- /dev/null +++ a/tests/sub/subscriber.cpp @@ -0,0 +1,46 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#include "subscriber.h" +#include "mqttmessage.h" +#include "credentials.h" + +Subscriber::Subscriber() + : m_mqtt_client( "TestSubscriber" ) +{ + +} + +void Subscriber::connect( const std::string &hostname, int portnumber, const std::string &username, const std::string &password ) +{ + m_mqtt_client.connect( hostname, portnumber, osdev::components::mqtt::Credentials( username, password ) ); + std::cout << "Client state : " << m_mqtt_client.state() << std::endl; +} + +void Subscriber::subscribe( const std::string &message_topic ) +{ + m_mqtt_client.subscribe( message_topic, 1, [this](const osdev::components::mqtt::MqttMessage &message) + { + this->receive_data(message.topic(), message.payload() ); + }); +} + +void Subscriber::receive_data( const std::string &message_topic, const std::string &message_payload ) +{ + std::cout << "[Subscriber::receive_data] - Received message : " << message_payload << " from topic : " << message_topic << std::endl; +} diff --git b/tests/sub/subscriber.h a/tests/sub/subscriber.h new file mode 100644 index 0000000..34b2525 --- /dev/null +++ a/tests/sub/subscriber.h @@ -0,0 +1,45 @@ +/* Copyright (C) 2019 + * + * This file is part of the osdev components suite + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ +#pragma once + +// std +#include +#include + +// osdev::components::mqtt +#include "mqttclient.h" +#include "compat-c++14.h" + +class Subscriber +{ +public: + Subscriber(); + + virtual ~Subscriber() {} + + void connect( const std::string &hostname, int portnumber, const std::string &username, const std::string &password ); + + void subscribe( const std::string &message_topic ); + +private: + void receive_data( const std::string &message_topic, const std::string &message_payload ); + +private: + osdev::components::mqtt::MqttClient m_mqtt_client; +};