diff --git a/CMakeLists.txt b/CMakeLists.txt index edc49b19..4f48190f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ set(AGENT_VERSION_MAJOR 2) set(AGENT_VERSION_MINOR 2) set(AGENT_VERSION_PATCH 0) set(AGENT_VERSION_BUILD 1) -set(AGENT_VERSION_RC "_RC3") +set(AGENT_VERSION_RC "_RC4") # This minimum version is to support Visual Studio 2017 and C++ feature checking and FetchContent cmake_minimum_required(VERSION 3.16 FATAL_ERROR) diff --git a/samples/dyn_load.xml b/samples/dyn_load.xml new file mode 100644 index 00000000..d8cd7a5f --- /dev/null +++ b/samples/dyn_load.xml @@ -0,0 +1,23 @@ + + +
+ + + + + + + + + + + + + + + + + + + + diff --git a/samples/empty.xml b/samples/empty.xml new file mode 100644 index 00000000..5e336730 --- /dev/null +++ b/samples/empty.xml @@ -0,0 +1,9 @@ + + +
+ + diff --git a/src/mtconnect/agent.cpp b/src/mtconnect/agent.cpp index 931afd9d..d962648f 100644 --- a/src/mtconnect/agent.cpp +++ b/src/mtconnect/agent.cpp @@ -73,7 +73,7 @@ namespace mtconnect { static const string g_available("AVAILABLE"); // Agent public methods - Agent::Agent(boost::asio::io_context &context, const string &deviceXmlPath, + Agent::Agent(config::AsyncContext &context, const string &deviceXmlPath, const ConfigOptions &options) : m_options(options), m_context(context), @@ -83,7 +83,7 @@ namespace mtconnect { m_deviceXmlPath(deviceXmlPath), m_circularBuffer(GetOption(options, config::BufferSize).value_or(17), GetOption(options, config::CheckpointFrequency).value_or(1000)), - m_pretty(GetOption(options, mtconnect::configuration::Pretty).value_or(false)) + m_pretty(IsOptionSet(options, mtconnect::configuration::Pretty)) { using namespace asset; @@ -97,8 +97,8 @@ namespace mtconnect { m_assetStorage = make_unique( GetOption(options, mtconnect::configuration::MaxAssets).value_or(1024)); - m_versionDeviceXml = - GetOption(options, mtconnect::configuration::VersionDeviceXmlUpdates).value_or(false); + m_versionDeviceXml = IsOptionSet(options, mtconnect::configuration::VersionDeviceXmlUpdates); + m_createUniqueIds = IsOptionSet(options, config::CreateUniqueIds); auto jsonVersion = uint32_t(GetOption(options, mtconnect::configuration::JsonVersion).value_or(2)); @@ -145,6 +145,9 @@ namespace mtconnect { for (auto device : devices) addDevice(device); + if (m_versionDeviceXml && m_createUniqueIds) + versionDeviceXml(); + loadCachedProbe(); m_initialized = true; @@ -360,7 +363,6 @@ namespace mtconnect { return false; } - // Fir the DeviceAdded event for each device bool changed = false; for (auto device : devices) { @@ -387,6 +389,46 @@ namespace mtconnect { } } + void Agent::loadDevice(const string &deviceXml, const optional source) + { + m_context.pause([=](config::AsyncContext &context) { + try + { + auto printer = dynamic_cast(m_printers["xml"].get()); + auto device = m_xmlParser->parseDevice(deviceXml, printer); + + if (device) + { + bool changed = receiveDevice(device, true); + if (changed) + loadCachedProbe(); + + if (source) + { + auto s = findSource(*source); + if (s) + { + const auto &name = device->getComponentName(); + s->setOptions({{config::Device, *name}}); + } + } + } + } + catch (runtime_error &e) + { + LOG(error) << "Error loading device: " + deviceXml; + LOG(error) << "Error detail: " << e.what(); + cerr << e.what() << endl; + } + catch (exception &f) + { + LOG(error) << "Error loading device: " + deviceXml; + LOG(error) << "Error detail: " << f.what(); + cerr << f.what() << endl; + } + }); + } + bool Agent::receiveDevice(device_model::DevicePtr device, bool version) { NAMED_SCOPE("Agent::receiveDevice"); @@ -442,7 +484,16 @@ namespace mtconnect { if (auto odi = oldDev->getAssetCount(), ndi = device->getAssetCount(); odi && !ndi) device->addDataItem(odi, errors); + if (errors.size() > 0) + { + LOG(error) << "Error adding device required data items for " << *device->getUuid() << ':'; + for (const auto &e : errors) + LOG(error) << " " << e->what(); + return false; + } + verifyDevice(device); + createUniqueIds(device); LOG(info) << "Checking if device " << *uuid << " has changed"; if (*device != *oldDev) @@ -508,26 +559,37 @@ namespace mtconnect { void Agent::versionDeviceXml() { + NAMED_SCOPE("Agent::versionDeviceXml"); + + using namespace std::chrono; + if (m_versionDeviceXml) { // update with a new version of the device.xml, saving the old one // with a date time stamp - auto ext = "."s + getCurrentTime(LOCAL); + auto ext = + date::format(".%Y-%m-%dT%H+%M+%SZ", date::floor(system_clock::now())); fs::path file(m_deviceXmlPath); fs::path backup(m_deviceXmlPath + ext); if (!fs::exists(backup)) fs::rename(file, backup); - printer::XmlPrinter printer(true); - - std::list list; - copy_if(m_deviceIndex.begin(), m_deviceIndex.end(), back_inserter(list), - [](DevicePtr d) { return dynamic_cast(d.get()) == nullptr; }); - auto probe = printer.printProbe(0, 0, 0, 0, 0, list); - - ofstream devices(file.string()); - devices << probe; - devices.close(); + auto printer = getPrinter("xml"); + if (printer != nullptr) + { + std::list list; + copy_if(m_deviceIndex.begin(), m_deviceIndex.end(), back_inserter(list), + [](DevicePtr d) { return dynamic_cast(d.get()) == nullptr; }); + auto probe = printer->printProbe(0, 0, 0, 0, 0, list, nullptr, true, true); + + ofstream devices(file.string()); + devices << probe; + devices.close(); + } + else + { + LOG(error) << "Cannot find xml printer"; + } } } @@ -661,7 +723,8 @@ namespace mtconnect { auto devices = m_xmlParser->parseFile( configXmlPath, dynamic_cast(m_printers["xml"].get())); - if (!m_schemaVersion && m_xmlParser->getSchemaVersion()) + if (!m_schemaVersion && m_xmlParser->getSchemaVersion() && + !m_xmlParser->getSchemaVersion()->empty()) { m_schemaVersion = m_xmlParser->getSchemaVersion(); m_intSchemaVersion = IntSchemaVersion(*m_schemaVersion); @@ -809,38 +872,32 @@ namespace mtconnect { } else { - // Check if we are already initialized. If so, the device will need to be - // verified, additional data items added, and initial values set. - // if (!m_initialized) - { - m_deviceIndex.push_back(device); + m_deviceIndex.push_back(device); - // TODO: Redo Resolve Reference with entity - // device->resolveReferences(); - verifyDevice(device); + // TODO: Redo Resolve Reference with entity + // device->resolveReferences(); + verifyDevice(device); + createUniqueIds(device); - if (m_observationsInitialized) - { - initializeDataItems(device); + if (m_observationsInitialized) + { + initializeDataItems(device); - // Check for single valued constrained data items. - if (m_agentDevice && device != m_agentDevice) + // Check for single valued constrained data items. + if (m_agentDevice && device != m_agentDevice) + { + entity::Properties props {{"VALUE", uuid}}; + if (m_intSchemaVersion >= SCHEMA_VERSION(2, 2)) { - entity::Properties props {{"VALUE", uuid}}; - if (m_intSchemaVersion >= SCHEMA_VERSION(2, 2)) - { - const auto &hash = device->getProperty("hash"); - if (hash.index() != EMPTY) - props.insert_or_assign("hash", hash); - } - - auto d = m_agentDevice->getDeviceDataItem("device_added"); - m_loopback->receive(d, props); + const auto &hash = device->getProperty("hash"); + if (hash.index() != EMPTY) + props.insert_or_assign("hash", hash); } + + auto d = m_agentDevice->getDeviceDataItem("device_added"); + m_loopback->receive(d, props); } } - // else - // LOG(warning) << "Adding device " << uuid << " after initialialization not supported yet"; } if (m_intSchemaVersion >= SCHEMA_VERSION(2, 2)) @@ -875,6 +932,7 @@ namespace mtconnect { if (changed) { + createUniqueIds(device); if (m_intSchemaVersion >= SCHEMA_VERSION(2, 2)) device->addHash(); @@ -910,6 +968,28 @@ namespace mtconnect { } } + void Agent::createUniqueIds(DevicePtr device) + { + if (m_createUniqueIds && !dynamic_pointer_cast(device)) + { + std::unordered_map idMap; + + device->createUniqueIds(idMap); + device->updateReferences(idMap); + + // Update the data item map. + for (auto &id : idMap) + { + auto di = device->getDeviceDataItem(id.second); + if (auto it = m_dataItemMap.find(id.first); it != m_dataItemMap.end()) + { + m_dataItemMap.erase(it); + m_dataItemMap.emplace(id.second, di); + } + } + } + } + void Agent::loadCachedProbe() { NAMED_SCOPE("Agent::loadCachedProbe"); @@ -1026,30 +1106,22 @@ namespace mtconnect { void AgentPipelineContract::deliverCommand(entity::EntityPtr entity) { - static auto pattern = regex("\\*[ ]*([^:]+):[ ]*(.+)"); + auto command = entity->get("command"); auto value = entity->getValue(); - smatch match; - if (std::regex_match(value, match, pattern)) - { - auto device = entity->maybeGet("device"); - auto command = boost::algorithm::to_lower_copy(match[1].str()); - auto param = match[2].str(); - auto source = entity->maybeGet("source"); + auto device = entity->maybeGet("device"); + auto source = entity->maybeGet("source"); - if (!device || !source) - { - LOG(error) << "Invalid command: " << command << ", device or source not specified"; - } - else - { - LOG(debug) << "Processing command: " << command << ": " << value; - m_agent->receiveCommand(*device, command, param, *source); - } + if ((command != "devicemodel" && !device) || !source) + { + LOG(error) << "Invalid command: " << command << ", device or source not specified"; } else { - LOG(warning) << "Cannot parse command: " << value; + LOG(debug) << "Processing command: " << command << ": " << value; + if (!device) + device.emplace(""); + m_agent->receiveCommand(*device, command, value, *source); } } @@ -1306,8 +1378,7 @@ namespace mtconnect { {"description", mem_fn(&Device::setDescriptionValue)}, {"nativename", [](DevicePtr device, const string &name) { device->setProperty("nativeName", name); }}, - {"calibration", - [](DevicePtr device, const string &value) { + {"calibration", [](DevicePtr device, const string &value) { istringstream line(value); // Look for name|factor|offset triples @@ -1328,8 +1399,7 @@ namespace mtconnect { di->setConverter(conv); } } - }}, - }; + }}}; static std::unordered_map adapterDataItems { {"adapterversion", "_adapter_software_version"}, @@ -1349,6 +1419,10 @@ namespace mtconnect { deviceChanged(device, oldUuid, oldName); } } + else if (command == "devicemodel") + { + loadDevice(value, source); + } else { auto action = deviceCommands.find(command); diff --git a/src/mtconnect/agent.hpp b/src/mtconnect/agent.hpp index 8483855c..4b4cbc6d 100644 --- a/src/mtconnect/agent.hpp +++ b/src/mtconnect/agent.hpp @@ -40,6 +40,7 @@ #include "mtconnect/buffer/checkpoint.hpp" #include "mtconnect/buffer/circular_buffer.hpp" #include "mtconnect/config.hpp" +#include "mtconnect/configuration/async_context.hpp" #include "mtconnect/configuration/hook_manager.hpp" #include "mtconnect/configuration/service.hpp" #include "mtconnect/device_model/agent_device.hpp" @@ -90,7 +91,7 @@ namespace mtconnect { /// - VersionDeviceXmlUpdates /// - JsonVersion /// - DisableAgentDevice - Agent(boost::asio::io_context &context, const std::string &deviceXmlPath, + Agent(configuration::AsyncContext &context, const std::string &deviceXmlPath, const ConfigOptions &options); /// Destructor for the Agent. @@ -167,14 +168,15 @@ namespace mtconnect { // Source and Sink /// @brief Find a source by name - /// @param[in] name the name to find + /// @param[in] name the identity to find /// @return A shared pointer to the source if found, otherwise nullptr source::SourcePtr findSource(const std::string &name) const { for (auto &s : m_sources) - if (s->getIdentity() == name) + { + if (s->getIdentity() == name || s->getName() == name) return s; - + } return nullptr; } /// @brief Find a sink by name @@ -257,10 +259,16 @@ namespace mtconnect { /// @param[in] oldName The old name void deviceChanged(DevicePtr device, const std::string &oldUuid, const std::string &oldName); /// @brief Reload the devices from a device file after updates - /// @param deviceFile The device file to load + /// @param[in] deviceFile The device file to load /// @return true if successful bool reloadDevices(const std::string &deviceFile); + /// @brief receive and parse a single device from a source + /// @param[in] deviceXml the device xml as a string + /// @param[in] source the source loading the device + void loadDevice(const std::string &deviceXml, + const std::optional source = std::nullopt); + /// @name Message when source has connected and disconnected ///@{ @@ -403,6 +411,14 @@ namespace mtconnect { std::string devicesAndPath(const std::optional &path, const DevicePtr device) const; + /// @brief Creates unique ids for the device model and maps to the originals + /// + /// Also updates the agents data item map by adding the new ids. Duplicate original + /// ids will be last in wins. + /// + /// @param[in] device device to modify + void createUniqueIds(DevicePtr device); + protected: friend class AgentPipelineContract; @@ -427,7 +443,7 @@ namespace mtconnect { protected: ConfigOptions m_options; - boost::asio::io_context &m_context; + configuration::AsyncContext &m_context; boost::asio::io_context::strand m_strand; std::shared_ptr m_loopback; @@ -493,7 +509,8 @@ namespace mtconnect { // Xml Config std::optional m_schemaVersion; std::string m_deviceXmlPath; - bool m_versionDeviceXml = false; + bool m_versionDeviceXml {false}; + bool m_createUniqueIds {false}; int32_t m_intSchemaVersion = IntDefaultSchemaVersion(); // Circular Buffer diff --git a/src/mtconnect/configuration/agent_config.cpp b/src/mtconnect/configuration/agent_config.cpp index 3b546fc1..c0f8e1d6 100644 --- a/src/mtconnect/configuration/agent_config.cpp +++ b/src/mtconnect/configuration/agent_config.cpp @@ -102,7 +102,7 @@ namespace mtconnect::configuration { boost::log::trivial::logger_type *gAgentLogger = nullptr; AgentConfiguration::AgentConfiguration() - : m_context {make_unique()}, m_monitorTimer(m_context->getContext()) + : m_context {make_unique()}, m_monitorTimer(m_context->get()) { NAMED_SCOPE("AgentConfiguration::AgentConfiguration"); using namespace source; @@ -645,6 +645,7 @@ namespace mtconnect::configuration { {configuration::MaxAssets, int(DEFAULT_MAX_ASSETS)}, {configuration::CheckpointFrequency, 1000}, {configuration::LegacyTimeout, 600s}, + {configuration::CreateUniqueIds, false}, {configuration::ReconnectInterval, 10000ms}, {configuration::IgnoreTimestamps, false}, {configuration::ConversionRequired, true}, @@ -896,10 +897,14 @@ namespace mtconnect::configuration { adapterOptions, ptree {}); m_agent->addSource(source, false); } - else + else if (m_agent->getDevices().size() > 1) { throw runtime_error("Adapters must be defined if more than one device is present"); } + else + { + LOG(warning) << "Starting with no devices or adapters"; + } } #ifdef WITH_PYTHON diff --git a/src/mtconnect/configuration/agent_config.hpp b/src/mtconnect/configuration/agent_config.hpp index 7ec330fa..32fcffba 100644 --- a/src/mtconnect/configuration/agent_config.hpp +++ b/src/mtconnect/configuration/agent_config.hpp @@ -112,9 +112,9 @@ namespace mtconnect { /// @param[in] agent the agent the configuration will take ownership of void setAgent(std::unique_ptr &agent) { m_agent = std::move(agent); } /// @brief get the agent associated with the configuration - const Agent *getAgent() const { return m_agent.get(); } + Agent *getAgent() const { return m_agent.get(); } /// @brief get the boost asio io context - auto &getContext() { return m_context->getContext(); } + auto &getContext() { return m_context->get(); } /// @brief get a pointer to the async io manager auto &getAsyncContext() { return *m_context.get(); } diff --git a/src/mtconnect/configuration/async_context.hpp b/src/mtconnect/configuration/async_context.hpp index 31f35c3f..0446685e 100644 --- a/src/mtconnect/configuration/async_context.hpp +++ b/src/mtconnect/configuration/async_context.hpp @@ -40,8 +40,12 @@ namespace mtconnect::configuration { AsyncContext(const AsyncContext &) = delete; ~AsyncContext() {} + /// @brief Testing only: method to remove the run guard from the context + void removeGuard() { m_guard.reset(); } + /// @brief get the boost asio context reference - auto &getContext() { return m_context; } + auto &get() { return m_context; } + /// @brief operator() returns a reference to the io context /// @return the io context operator boost::asio::io_context &() { return m_context; } @@ -127,6 +131,44 @@ namespace mtconnect::configuration { m_context.restart(); } + /// @name Cover methods for asio io_context + /// @{ + + /// @brief io_context::run_for + template + auto run_for(const std::chrono::duration &rel_time) + { + return m_context.run_for(rel_time); + } + + /// @brief io_context::run + auto run() { return m_context.run(); } + + /// @brief io_context::run_one + auto run_one() { return m_context.run_one(); } + + /// @brief io_context::run_one_for + template + auto run_one_for(const std::chrono::duration &rel_time) + { + return m_context.run_one_for(rel_time); + } + + /// @brief io_context::run_one_until + template + auto run_one_until(const std::chrono::time_point &abs_time) + { + return m_context.run_one_for(abs_time); + } + + /// @brief io_context::poll + auto poll() { return m_context.poll(); } + + /// @brief io_context::poll + auto get_executor() BOOST_ASIO_NOEXCEPT { return m_context.get_executor(); } + + /// @} + private: void operator=(const AsyncContext &) {} diff --git a/src/mtconnect/configuration/config_options.hpp b/src/mtconnect/configuration/config_options.hpp index 7116d858..a012d399 100644 --- a/src/mtconnect/configuration/config_options.hpp +++ b/src/mtconnect/configuration/config_options.hpp @@ -68,6 +68,7 @@ namespace mtconnect { DECLARE_CONFIGURATION(TlsOnly); DECLARE_CONFIGURATION(TlsPrivateKey); DECLARE_CONFIGURATION(TlsVerifyClientCertificate); + DECLARE_CONFIGURATION(CreateUniqueIds); DECLARE_CONFIGURATION(VersionDeviceXmlUpdates); DECLARE_CONFIGURATION(WorkerThreads); ///@} diff --git a/src/mtconnect/device_model/component.hpp b/src/mtconnect/device_model/component.hpp index fb91fce6..a554e4c3 100644 --- a/src/mtconnect/device_model/component.hpp +++ b/src/mtconnect/device_model/component.hpp @@ -43,6 +43,7 @@ namespace mtconnect { using DevicePtr = std::shared_ptr; using DataItemPtr = std::shared_ptr; + using WeakDataItemPtr = std::weak_ptr; /// @brief MTConnect Component Entity class AGENT_LIB_API Component : public entity::Entity @@ -245,6 +246,27 @@ namespace mtconnect { return nullptr; } + /// @brief Get the component topic path as a list + /// + /// Recurses to root and then appends getTopicName + /// @param[in,out] pth the path list to append to + void path(std::list &pth) + { + auto p = getParent(); + if (p) + p->path(pth); + + pth.push_back(getTopicName()); + } + + std::optional createUniqueId(std::unordered_map &idMap, + const boost::uuids::detail::sha1 &sha1) override + { + auto newId = Entity::createUniqueId(idMap, sha1); + m_id = *newId; + return newId; + } + protected: void setParent(ComponentPtr parent) { m_parent = parent; } void setDevice(DevicePtr device) { m_device = device; } diff --git a/src/mtconnect/device_model/data_item/data_item.cpp b/src/mtconnect/device_model/data_item/data_item.cpp index 89842b47..85ac2381 100644 --- a/src/mtconnect/device_model/data_item/data_item.cpp +++ b/src/mtconnect/device_model/data_item/data_item.cpp @@ -243,7 +243,8 @@ namespace mtconnect { bool DataItem::hasName(const string &name) const { - return m_id == name || (m_name && *m_name == name) || (m_source && *m_source == name); + return m_id == name || (m_name && *m_name == name) || (m_source && *m_source == name) || + (m_originalId && *m_originalId == name); } // Sort by: Device, Component, Category, DataItem @@ -297,21 +298,12 @@ namespace mtconnect { } } - inline void path(std::list &pth, ComponentPtr c) - { - auto p = c->getParent(); - if (p) - path(pth, p); - - pth.push_back(c->getTopicName()); - } - void DataItem::makeTopic() { std::list pth; auto comp = m_component.lock(); - path(pth, comp); + comp->path(pth); { auto cp = m_composition.lock(); if (cp) diff --git a/src/mtconnect/device_model/data_item/data_item.hpp b/src/mtconnect/device_model/data_item/data_item.hpp index 3138a8a9..35662d97 100644 --- a/src/mtconnect/device_model/data_item/data_item.hpp +++ b/src/mtconnect/device_model/data_item/data_item.hpp @@ -40,6 +40,7 @@ namespace mtconnect { } namespace device_model { class Composition; + struct UpdateDataItemId; /// @brief DataItem related entities namespace data_item { @@ -221,15 +222,44 @@ namespace mtconnect { bool operator<(const DataItem &another) const; bool operator==(const DataItem &another) const { return m_id == another.m_id; } + /// @brief Return the category as a char * const char *getCategoryText() const { return m_categoryText; } + std::optional createUniqueId( + std::unordered_map &idMap, + const boost::uuids::detail::sha1 &sha1) override + { + m_originalId.emplace(m_id); + auto pref = m_id == m_preferredName; + m_id = *Entity::createUniqueId(idMap, sha1); + if (pref) + m_preferredName = m_id; + m_observatonProperties.insert_or_assign("dataItemId", m_id); + return m_id; + } + + /// @brief Get a reference to the optional original id + /// @returns optional original id + const auto &getOriginalId() const { return m_originalId; } + + void updateReferences(const std::unordered_map idMap) override + { + Entity::updateReferences(idMap); + if (hasProperty("compositionId")) + m_observatonProperties.insert_or_assign("compositionId", + get("compositionId")); + } + protected: double simpleFactor(const std::string &units); std::map buildAttributes() const; + friend struct device_model::UpdateDataItemId; + protected: // Unique ID for each component std::string m_id; + std::optional m_originalId; // Name for itself std::optional m_name; diff --git a/src/mtconnect/device_model/device.cpp b/src/mtconnect/device_model/device.cpp index d2fbf3b7..1de61472 100644 --- a/src/mtconnect/device_model/device.cpp +++ b/src/mtconnect/device_model/device.cpp @@ -153,6 +153,10 @@ namespace mtconnect { if (auto it = m_dataItems.get().find(name); it != m_dataItems.get().end()) return it->lock(); + if (auto it = m_dataItems.get().find(name); + it != m_dataItems.get().end()) + return it->lock(); + if (auto it = m_dataItems.get().find(name); it != m_dataItems.get().end()) return it->lock(); @@ -161,5 +165,65 @@ namespace mtconnect { return nullptr; } + + struct UpdateDataItemId + { + const string &m_id; + UpdateDataItemId(const string &id) : m_id(id) {} + + void operator()(WeakDataItemPtr &ptr) + { + auto di = ptr.lock(); + di->m_id = m_id; + } + }; + + void Device::createUniqueIds(std::unordered_map &idMap) + { + boost::uuids::detail::sha1 sha; + sha.process_bytes(m_uuid->data(), m_uuid->size()); + + Component::createUniqueId(idMap, sha); + + for (auto it = m_dataItems.begin(); it != m_dataItems.end(); it++) + { + auto di = it->lock(); + const auto &oldId = di->getOriginalId(); + if (oldId) + { + auto id = idMap.find(*oldId); + if (id != idMap.end()) + { + m_dataItems.modify(it, UpdateDataItemId(id->second)); + } + else + { + LOG(error) << "Cannot find id " << *oldId << " in data item map"; + } + } + else + { + LOG(error) << "DataItem id for " << di->getId() << " was not made unique"; + } + } + + for (auto p : m_componentsById) + { + auto comp = p.second.lock(); + if (comp) + { + auto orig = comp->maybeGet("originalId"); + if (orig && m_componentsById.count(*orig) == 0) + { + m_componentsById.emplace(*orig, comp); + } + if (m_componentsById.count(comp->getId()) == 0) + { + m_componentsById.emplace(comp->getId(), comp); + } + } + } + } + } // namespace device_model } // namespace mtconnect diff --git a/src/mtconnect/device_model/device.hpp b/src/mtconnect/device_model/device.hpp index 97e62af9..1e61ec31 100644 --- a/src/mtconnect/device_model/device.hpp +++ b/src/mtconnect/device_model/device.hpp @@ -53,6 +53,9 @@ namespace mtconnect { /// @brief multi-index tag: Data items indexed by id struct ById {}; + /// @brief multi-index tag: Data items indexed by optional original id + struct ByOriginalId + {}; /// @brief multi-index tag: Data items index by Source struct BySource {}; @@ -66,6 +69,29 @@ namespace mtconnect { using result_type = std::string; const result_type &operator()(const WeakDataItemPtr d) const { return d.lock()->getId(); } }; + /// @brief multi-index data item id extractor + /// + /// falls back to id if orginal id is not given + + struct ExtractOriginalId + { + using result_type = std::string; + const result_type operator()(const WeakDataItemPtr &d) const + { + const static result_type none {}; + if (d.expired()) + return none; + else + { + auto di = d.lock(); + auto id = di->getOriginalId(); + if (id) + return *id; + else + return di->getId(); + } + } + }; /// @brief multi-index data item name extractor /// /// falls back to id if name is not given @@ -130,6 +156,7 @@ namespace mtconnect { using DataItemIndex = mic::multi_index_container< WeakDataItemPtr, mic::indexed_by, ExtractId>, + mic::hashed_unique, ExtractOriginalId>, mic::hashed_non_unique, ExtractSource>, mic::hashed_non_unique, ExtractName>, mic::ordered_non_unique, ExtractType>>>; @@ -165,7 +192,13 @@ namespace mtconnect { /// @brief Add a data item to the device /// @param[in] dataItem shared pointer to the data item void addDeviceDataItem(DataItemPtr dataItem); - /// @brief get a data item by source, name, and id + /// @brief get a data item by multiple indexes + /// + /// Looks for a data item using the following idexes in the following order: + /// 1. id + /// 2. original id (set when creating unique ids) + /// 3. name + /// 4. source /// @param[in] name the source, name, or id of the data item /// @return shared pointer to the data item if found DataItemPtr getDeviceDataItem(const std::string &name) const; @@ -236,6 +269,12 @@ namespace mtconnect { /// @return the uuid of the device const std::string getTopicName() const override { return *m_uuid; } + /// @brief Converts all the ids to unique ids by hasing the topics + /// + /// Converts the id attribute to a unique value and caches the original value + /// in case it is required later + void createUniqueIds(std::unordered_map &idMap); + protected: void cachePointers(DataItemPtr dataItem); diff --git a/src/mtconnect/entity/entity.cpp b/src/mtconnect/entity/entity.cpp index 557e7ac0..777eb306 100644 --- a/src/mtconnect/entity/entity.cpp +++ b/src/mtconnect/entity/entity.cpp @@ -15,6 +15,8 @@ // limitations under the License. // +#include + #include #include "factory.hpp" @@ -131,7 +133,7 @@ namespace mtconnect::entity { for (const auto &e : m_properties) { // Skip hash - if (!skip.contains(e.first)) + if (!skip.contains(e.first) && !isHidden(e.first)) { const auto &value = e.second; sha1.process_bytes(e.first.c_str(), e.first.size()); @@ -141,4 +143,105 @@ namespace mtconnect::entity { } } + struct UniqueIdVisitor + { + std::unordered_map &m_idMap; + const boost::uuids::detail::sha1 &m_sha1; + UniqueIdVisitor(std::unordered_map &idMap, + const boost::uuids::detail::sha1 &sha1) + : m_idMap(idMap), m_sha1(sha1) + {} + + void operator()(EntityPtr &p) { p->createUniqueId(m_idMap, m_sha1); } + + void operator()(EntityList &l) + { + for (auto &e : l) + e->createUniqueId(m_idMap, m_sha1); + } + + template + void operator()(const T &) + {} + }; + + std::optional Entity::createUniqueId( + std::unordered_map &idMap, const boost::uuids::detail::sha1 &sha1) + { + optional res; + + auto it = m_properties.find("id"); + if (it != m_properties.end()) + { + std::string newId, oldId; + auto origId = maybeGet("originalId"); + if (!origId) + { + oldId = std::get(it->second); + m_properties.emplace("originalId", oldId); + newId = makeUniqueId(sha1, oldId); + it->second = newId; + } + else + { + oldId = *origId; + newId = std::get(it->second); + } + idMap.emplace(oldId, newId); + res.emplace(newId); + } + + UniqueIdVisitor visitor(idMap, sha1); + + // Recurse properties + for (auto &p : m_properties) + { + std::visit(visitor, p.second); + } + + return res; + } + + struct ReferenceIdVisitor + { + const std::unordered_map &m_idMap; + ReferenceIdVisitor(const std::unordered_map &idMap) : m_idMap(idMap) {} + + void operator()(EntityPtr &p) { p->updateReferences(m_idMap); } + + void operator()(EntityList &l) + { + for (auto &e : l) + e->updateReferences(m_idMap); + } + + template + void operator()(T &) + {} + }; + + void Entity::updateReferences(std::unordered_map idMap) + { + using namespace boost::algorithm; + for (auto &prop : m_properties) + { + if (prop.first != "originalId" && (iends_with(prop.first, "idref") || + (prop.first.length() > 2 && iends_with(prop.first, "id")))) + { + auto it = idMap.find(std::get(prop.second)); + if (it != idMap.end()) + { + prop.second = it->second; + } + } + } + + ReferenceIdVisitor visitor(idMap); + + // Recurse all + for (auto &p : m_properties) + { + std::visit(visitor, p.second); + } + } } // namespace mtconnect::entity diff --git a/src/mtconnect/entity/entity.hpp b/src/mtconnect/entity/entity.hpp index f5eebb56..beca10ba 100644 --- a/src/mtconnect/entity/entity.hpp +++ b/src/mtconnect/entity/entity.hpp @@ -106,6 +106,18 @@ namespace mtconnect { /// @return the identity virtual const entity::Value &getIdentity() const { return getProperty("id"); } + /// @brief create unique ids recursively + /// @param[in,out] idMap old entity id to new entity id map + /// @param[in] sha the root sha1 + /// @returns optional string value of the new id + virtual std::optional createUniqueId( + std::unordered_map &idMap, + const boost::uuids::detail::sha1 &sha1); + + /// @brief update all id references to the new ids recursively + /// @param[in] idMap map of old ids to new ids + virtual void updateReferences(const std::unordered_map idMap); + /// @brief checks if there is entity is a list that has additional properties /// @return `true` if this a a list with additional attributes bool hasListWithAttribute() const @@ -122,6 +134,15 @@ namespace mtconnect { /// @brief checkis if this is an entity only containing a list /// @returns `true` if this entity contains a list bool isList() const { return m_properties.count("LIST") > 0; } + /// @brief check if the property is hidden for normal serialization + /// @param[in] name the name of the property + /// + /// Used for serializing properties for configuration purposes where additional attibutes + /// may be included in the document. The only property is `originalId` for now. + /// + /// This may be extended to include a property set in the future. + /// @returns `true` if the property is hidden + bool isHidden(const std::string &name) const { return name == "originalId"; } /// @brief get the name of the entity /// @return name const auto &getName() const { return m_name; } diff --git a/src/mtconnect/entity/factory.hpp b/src/mtconnect/entity/factory.hpp index db769f4c..e1d91bc3 100644 --- a/src/mtconnect/entity/factory.hpp +++ b/src/mtconnect/entity/factory.hpp @@ -359,6 +359,10 @@ namespace mtconnect { /// @brief scan requirements and register factories refrenced in the requirements void registerEntityRequirements() { + if (!m_simpleProperties.count("originalId")) + { + m_requirements.emplace_back("originalId", false); + } for (auto &r : m_requirements) { m_properties.emplace(r.getName()); diff --git a/src/mtconnect/entity/json_printer.hpp b/src/mtconnect/entity/json_printer.hpp index 795ba776..3cb57f16 100644 --- a/src/mtconnect/entity/json_printer.hpp +++ b/src/mtconnect/entity/json_printer.hpp @@ -106,7 +106,8 @@ namespace mtconnect::entity { /// @param version the supported MTConnect serialization version /// - Version 1 has a repreated objects in arrays for collections of objects /// - Version 2 combines arrays of objects by type - JsonPrinter(T &writer, uint32_t version) : m_version(version), m_writer(writer) {}; + JsonPrinter(T &writer, uint32_t version, bool includeHidden = false) + : m_version(version), m_writer(writer), m_includeHidden(includeHidden) {}; /// @brief create a json object from an entity /// @@ -134,8 +135,11 @@ namespace mtconnect::entity { for (auto &prop : entity->getProperties()) { - visitor.m_key = &prop.first; - visit(visitor, prop.second); + if (m_includeHidden || !entity->isHidden(prop.first)) + { + visitor.m_key = &prop.first; + visit(visitor, prop.second); + } } } @@ -268,6 +272,7 @@ namespace mtconnect::entity { protected: uint32_t m_version; T &m_writer; + bool m_includeHidden {false}; }; /// @brief Serialization wrapper to turn an entity into a json string. @@ -275,7 +280,8 @@ namespace mtconnect::entity { { public: /// @brief Create a printer for with a JSON vesion and flag to pretty print - JsonEntityPrinter(uint32_t version, bool pretty = false) : m_version(version), m_pretty(pretty) + JsonEntityPrinter(uint32_t version, bool pretty = false, bool includeHidden = false) + : m_version(version), m_pretty(pretty), m_includeHidden(includeHidden) {} /// @brief wrapper around the JsonPrinter print method that creates the correct printer @@ -287,7 +293,7 @@ namespace mtconnect::entity { using namespace rapidjson; StringBuffer output; RenderJson(output, m_pretty, [&](auto &writer) { - JsonPrinter printer(writer, m_version); + JsonPrinter printer(writer, m_version, m_includeHidden); printer.printEntity(entity); }); @@ -306,7 +312,7 @@ namespace mtconnect::entity { using namespace rapidjson; StringBuffer output; RenderJson(output, m_pretty, [&](auto &writer) { - JsonPrinter printer(writer, m_version); + JsonPrinter printer(writer, m_version, m_includeHidden); printer.print(entity); }); @@ -316,5 +322,6 @@ namespace mtconnect::entity { protected: uint32_t m_version; bool m_pretty; + bool m_includeHidden {false}; }; } // namespace mtconnect::entity diff --git a/src/mtconnect/entity/xml_parser.cpp b/src/mtconnect/entity/xml_parser.cpp index 1190994b..e027e362 100644 --- a/src/mtconnect/entity/xml_parser.cpp +++ b/src/mtconnect/entity/xml_parser.cpp @@ -234,8 +234,8 @@ namespace mtconnect::entity { return nullptr; } - EntityPtr XmlParser::parse(FactoryPtr factory, const string &document, const string &version, - ErrorList &errors, bool parseNamespaces) + EntityPtr XmlParser::parse(FactoryPtr factory, const string &document, ErrorList &errors, + bool parseNamespaces) { NAMED_SCOPE("entity.xml_parser"); EntityPtr entity; diff --git a/src/mtconnect/entity/xml_parser.hpp b/src/mtconnect/entity/xml_parser.hpp index 5c8c346f..8e66a5f3 100644 --- a/src/mtconnect/entity/xml_parser.hpp +++ b/src/mtconnect/entity/xml_parser.hpp @@ -49,12 +49,10 @@ namespace mtconnect { /// @brief Parse a string document to an entity /// @param factory The factory to use to create the top level entity /// @param document the document as a string - /// @param version version to parse /// @param errors errors that occurred during the parsing /// @param parseNamespaces `true` if namespaces should be parsed /// @return a shared pointer to an entity if successful - static EntityPtr parse(FactoryPtr factory, const std::string &document, - const std::string &version, ErrorList &errors, + static EntityPtr parse(FactoryPtr factory, const std::string &document, ErrorList &errors, bool parseNamespaces = true); }; } // namespace entity diff --git a/src/mtconnect/entity/xml_printer.cpp b/src/mtconnect/entity/xml_printer.cpp index 4eadbd19..1ba68e0c 100644 --- a/src/mtconnect/entity/xml_printer.cpp +++ b/src/mtconnect/entity/xml_printer.cpp @@ -207,10 +207,13 @@ namespace mtconnect { for (const auto &prop : properties) { auto &key = prop.first; - if (islower(key.getName()[0]) || attrs.count(key) > 0) - attributes.emplace_back(prop); - else - elements.emplace_back(prop); + if (m_includeHidden || !entity->isHidden(key)) + { + if (islower(key.getName()[0]) || attrs.count(key) > 0) + attributes.emplace_back(prop); + else + elements.emplace_back(prop); + } } // Reorder elements if they need to be specially ordered. diff --git a/src/mtconnect/entity/xml_printer.hpp b/src/mtconnect/entity/xml_printer.hpp index 959fc8ec..9d556bbf 100644 --- a/src/mtconnect/entity/xml_printer.hpp +++ b/src/mtconnect/entity/xml_printer.hpp @@ -34,7 +34,7 @@ namespace mtconnect { class AGENT_LIB_API XmlPrinter { public: - XmlPrinter() = default; + XmlPrinter(bool includeHidden = false) : m_includeHidden(includeHidden) {} /// @brief convert an entity to a XML document using `libxml2` /// @param writer libxml2 `xmlTextWriterPtr` @@ -42,6 +42,9 @@ namespace mtconnect { /// @param namespaces a set of namespaces to use in the document void print(xmlTextWriterPtr writer, const EntityPtr entity, const std::unordered_set &namespaces); + + protected: + bool m_includeHidden {false}; }; } // namespace entity } // namespace mtconnect diff --git a/src/mtconnect/parser/xml_parser.cpp b/src/mtconnect/parser/xml_parser.cpp index b45e60bd..14b4199c 100644 --- a/src/mtconnect/parser/xml_parser.cpp +++ b/src/mtconnect/parser/xml_parser.cpp @@ -190,30 +190,32 @@ namespace mtconnect::parser { devices = xmlXPathEval(BAD_CAST path.c_str(), xpathCtx); - if (!devices) - throw(string) xpathCtx->lastError.message; - - xmlNodeSetPtr nodeset = devices->nodesetval; - - if (!nodeset || !nodeset->nodeNr) - throw(string) "Could not find Device in XML configuration"; - - entity::ErrorList errors; - for (int i = 0; i != nodeset->nodeNr; ++i) + if (!devices || !devices->nodesetval || !devices->nodesetval->nodeNr) + { + LOG(warning) << "No devices in Devices.xml file – expecting dynamic configuration"; + } + else { - auto device = - entity::XmlParser::parseXmlNode(Device::getRoot(), nodeset->nodeTab[i], errors); - if (device) - deviceList.emplace_back(dynamic_pointer_cast(device)); + xmlNodeSetPtr nodeset = devices->nodesetval; - if (!errors.empty()) + entity::ErrorList errors; + for (int i = 0; i != nodeset->nodeNr; ++i) { - for (auto &e : errors) - LOG(warning) << "Error parsing device: " << e->what(); + auto device = + entity::XmlParser::parseXmlNode(Device::getRoot(), nodeset->nodeTab[i], errors); + if (device) + deviceList.emplace_back(dynamic_pointer_cast(device)); + + if (!errors.empty()) + { + for (auto &e : errors) + LOG(warning) << "Error parsing device: " << e->what(); + } } } - xmlXPathFreeObject(devices); + if (devices) + xmlXPathFreeObject(devices); xmlXPathFreeContext(xpathCtx); } catch (string e) @@ -241,6 +243,41 @@ namespace mtconnect::parser { return deviceList; } + DevicePtr XmlParser::parseDevice(const std::string &deviceXml, printer::XmlPrinter *aPrinter) + { + DevicePtr device; + + using namespace boost::adaptors; + using namespace boost::range; + + std::unique_lock lock(m_mutex); + + try + { + entity::ErrorList errors; + auto entity = entity::XmlParser::parse(Device::getRoot(), deviceXml, errors); + if (errors.size() > 0) + { + LOG(warning) << "Errors parsing Device: " << deviceXml; + for (auto &e : errors) + { + LOG(warning) << " " << e->what(); + } + } + else + { + device = dynamic_pointer_cast(entity); + } + } + catch (string e) + { + LOG(fatal) << "Cannot parse XML document: " << e; + throw; + } + + return device; + } + XmlParser::~XmlParser() { std::unique_lock lock(m_mutex); diff --git a/src/mtconnect/parser/xml_parser.hpp b/src/mtconnect/parser/xml_parser.hpp index 6590d708..eff7ac9c 100644 --- a/src/mtconnect/parser/xml_parser.hpp +++ b/src/mtconnect/parser/xml_parser.hpp @@ -49,8 +49,15 @@ namespace mtconnect::parser { /// @brief Parses a file and returns a list of devices /// @param[in] aPath to the file /// @param[in] aPrinter the printer to obtain and set namespaces + /// @returns a list of device pointers std::list parseFile(const std::string &aPath, printer::XmlPrinter *aPrinter); + /// @brief Parses a single device fragment + /// @param[in] deviceXml device xml of a single device + /// @param[in] aPrinter the printer to obtain and set namespaces + /// @returns a shared device pointer if successful + device_model::DevicePtr parseDevice(const std::string &deviceXml, + printer::XmlPrinter *aPrinter); /// @brief Just loads the document, assumed it has already been parsed before. /// @param aDoc the XML document to parse diff --git a/src/mtconnect/pipeline/guard.hpp b/src/mtconnect/pipeline/guard.hpp index 6b252605..5c66c935 100644 --- a/src/mtconnect/pipeline/guard.hpp +++ b/src/mtconnect/pipeline/guard.hpp @@ -170,7 +170,7 @@ namespace mtconnect { { return check(matches(entity), entity); } - + /// @brief set the alternative action if this guard does not match auto &operator||(Guard other) { @@ -194,7 +194,7 @@ namespace mtconnect { { return check(matches(entity), entity); } - + /// @brief set the alternative action if this guard does not match auto &operator||(Guard other) { @@ -244,7 +244,7 @@ namespace mtconnect { { return B::check(matches(entity), entity); } - + /// @brief set the alternative action if this guard does not match auto &operator||(Guard other) { diff --git a/src/mtconnect/pipeline/pipeline.hpp b/src/mtconnect/pipeline/pipeline.hpp index 20a0044f..1ee84923 100644 --- a/src/mtconnect/pipeline/pipeline.hpp +++ b/src/mtconnect/pipeline/pipeline.hpp @@ -155,8 +155,7 @@ namespace mtconnect { return true; } - - + /// @brief add a transform after the target. /// @param[in] target the named transforms to insert this transform after /// @param[in] transform the transform to add before @@ -183,7 +182,7 @@ namespace mtconnect { return true; } - + /// @brief splices the transform as the first option in targets next list. /// @param[in] target the named transforms to insert this transform after /// @param[in] transform the transform to add before @@ -284,10 +283,7 @@ namespace mtconnect { /// @brief Sends the entity through the pipeline /// @param[in] entity the entity to send through the pipeline /// @return the entity returned from the transform - entity::EntityPtr run(entity::EntityPtr &&entity) - { - return m_start->next(std::move(entity)); - } + entity::EntityPtr run(entity::EntityPtr &&entity) { return m_start->next(std::move(entity)); } /// @brief Bind the transform to the start /// @param[in] transform the transform to bind @@ -301,7 +297,7 @@ namespace mtconnect { /// @brief check if the pipeline has a pipeline context /// @returns `true` if there is a context bool hasContext() const { return bool(m_context); } - + /// @brief check if the pipeline has a pipeline contract /// @returns `true` if there is a contract bool hasContract() const { return bool(m_context) && bool(m_context->m_contract); } diff --git a/src/mtconnect/pipeline/pipeline_context.hpp b/src/mtconnect/pipeline/pipeline_context.hpp index 99d506a0..bd451f15 100644 --- a/src/mtconnect/pipeline/pipeline_context.hpp +++ b/src/mtconnect/pipeline/pipeline_context.hpp @@ -73,7 +73,7 @@ namespace mtconnect::pipeline { using SharedState = std::unordered_map; SharedState m_sharedState; }; - + /// @brief Alias for a shared pointer to the pipeline context using PipelineContextPtr = std::shared_ptr; } // namespace mtconnect::pipeline diff --git a/src/mtconnect/pipeline/shdr_token_mapper.cpp b/src/mtconnect/pipeline/shdr_token_mapper.cpp index 5c022236..188848c5 100644 --- a/src/mtconnect/pipeline/shdr_token_mapper.cpp +++ b/src/mtconnect/pipeline/shdr_token_mapper.cpp @@ -270,7 +270,7 @@ namespace mtconnect { auto body = *token++; XmlParser parser; - res = parser.parse(Asset::getRoot(), body, "2.0", errors); + res = parser.parse(Asset::getRoot(), body, errors); if (auto asset = dynamic_pointer_cast(res)) { asset->setAssetId(assetId); diff --git a/src/mtconnect/printer/json_printer.cpp b/src/mtconnect/printer/json_printer.cpp index 2e36fde6..9ab0afbf 100644 --- a/src/mtconnect/printer/json_printer.cpp +++ b/src/mtconnect/printer/json_printer.cpp @@ -124,12 +124,13 @@ namespace mtconnect::printer { } std::string JsonPrinter::printErrors(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const ProtoErrorList &list) const + const uint64_t nextSeq, const ProtoErrorList &list, + bool pretty) const { defaultSchemaVersion(); StringBuffer output; - RenderJson(output, m_pretty, [&](auto &writer) { + RenderJson(output, m_pretty || pretty, [&](auto &writer) { AutoJsonObject obj(writer); { AutoJsonObject obj(writer, "MTConnectError"); @@ -180,13 +181,14 @@ namespace mtconnect::printer { const uint64_t nextSeq, const unsigned int assetBufferSize, const unsigned int assetCount, const std::list &devices, - const std::map *count) const + const std::map *count, + bool includeHidden, bool pretty) const { defaultSchemaVersion(); StringBuffer output; - RenderJson(output, m_pretty, [&](auto &writer) { - entity::JsonPrinter printer(writer, m_jsonVersion); + RenderJson(output, m_pretty || pretty, [&](auto &writer) { + entity::JsonPrinter printer(writer, m_jsonVersion, includeHidden); AutoJsonObject top(writer); AutoJsonObject obj(writer, "MTConnectDevices"); @@ -206,13 +208,13 @@ namespace mtconnect::printer { } std::string JsonPrinter::printAssets(const uint64_t instanceId, const unsigned int bufferSize, - const unsigned int assetCount, - const asset::AssetList &asset) const + const unsigned int assetCount, const asset::AssetList &asset, + bool pretty) const { defaultSchemaVersion(); StringBuffer output; - RenderJson(output, m_pretty, [&](auto &writer) { + RenderJson(output, m_pretty || pretty, [&](auto &writer) { entity::JsonPrinter printer(writer, m_jsonVersion); AutoJsonObject top(writer); @@ -406,12 +408,13 @@ namespace mtconnect::printer { std::string JsonPrinter::printSample(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const uint64_t firstSeq, - const uint64_t lastSeq, ObservationList &observations) const + const uint64_t lastSeq, ObservationList &observations, + bool pretty) const { defaultSchemaVersion(); StringBuffer output; - RenderJson(output, m_pretty, [&](auto &writer) { + RenderJson(output, m_pretty || pretty, [&](auto &writer) { AutoJsonObject top(writer); AutoJsonObject obj(writer, "MTConnectStreams"); obj.AddPairs("jsonVersion", m_jsonVersion, "schemaVersion", *m_schemaVersion); diff --git a/src/mtconnect/printer/json_printer.hpp b/src/mtconnect/printer/json_printer.hpp index 8b417e09..e3045389 100644 --- a/src/mtconnect/printer/json_printer.hpp +++ b/src/mtconnect/printer/json_printer.hpp @@ -31,19 +31,22 @@ namespace mtconnect::printer { ~JsonPrinter() override = default; std::string printErrors(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const ProtoErrorList &list) const override; + const uint64_t nextSeq, const ProtoErrorList &list, + bool pretty = false) const override; std::string printProbe(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const unsigned int assetBufferSize, const unsigned int assetCount, const std::list &devices, - const std::map *count = nullptr) const override; + const std::map *count = nullptr, + bool includeHidden = false, bool pretty = false) const override; std::string printSample(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const uint64_t firstSeq, const uint64_t lastSeq, - observation::ObservationList &results) const override; + observation::ObservationList &results, + bool pretty = false) const override; std::string printAssets(const uint64_t anInstanceId, const unsigned int bufferSize, - const unsigned int assetCount, - const asset::AssetList &asset) const override; + const unsigned int assetCount, const asset::AssetList &asset, + bool pretty = false) const override; std::string mimeType() const override { return "application/mtconnect+json"; } uint32_t getJsonVersion() const { return m_jsonVersion; } diff --git a/src/mtconnect/printer/printer.hpp b/src/mtconnect/printer/printer.hpp index 56810ba6..14a856f5 100644 --- a/src/mtconnect/printer/printer.hpp +++ b/src/mtconnect/printer/printer.hpp @@ -61,7 +61,7 @@ namespace mtconnect { /// @return the error document virtual std::string printError(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const std::string &errorCode, - const std::string &errorText) const + const std::string &errorText, bool pretty = false) const { return printErrors(instanceId, bufferSize, nextSeq, {{errorCode, errorText}}); } @@ -72,7 +72,8 @@ namespace mtconnect { /// @param[in] list the list of errors /// @return the MTConnect Error document virtual std::string printErrors(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const ProtoErrorList &list) const = 0; + const uint64_t nextSeq, const ProtoErrorList &list, + bool pretty = false) const = 0; /// @brief Generate an MTConnect Devices document /// @param[in] instanceId the instance id /// @param[in] bufferSize the buffer size @@ -82,11 +83,12 @@ namespace mtconnect { /// @param[in] devices a list of devices /// @param[in] count optional asset count and type association /// @return the MTConnect Devices document - virtual std::string printProbe( - const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, - const unsigned int assetBufferSize, const unsigned int assetCount, - const std::list &devices, - const std::map *count = nullptr) const = 0; + virtual std::string printProbe(const uint64_t instanceId, const unsigned int bufferSize, + const uint64_t nextSeq, const unsigned int assetBufferSize, + const unsigned int assetCount, + const std::list &devices, + const std::map *count = nullptr, + bool includeHidden = false, bool pretty = false) const = 0; /// @brief Print a MTConnect Streams document /// @param[in] instanceId the instance id /// @param[in] bufferSize the buffer size @@ -97,8 +99,8 @@ namespace mtconnect { /// @return the MTConnect Streams document virtual std::string printSample(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const uint64_t firstSeq, - const uint64_t lastSeq, - observation::ObservationList &results) const = 0; + const uint64_t lastSeq, observation::ObservationList &results, + bool pretty = false) const = 0; /// @brief Generate an MTConnect Assets document /// @param[in] anInstanceId the instance id /// @param[in] bufferSize the buffer size @@ -106,8 +108,8 @@ namespace mtconnect { /// @param[in] asset the list of assets /// @return the MTConnect Assets document virtual std::string printAssets(const uint64_t anInstanceId, const unsigned int bufferSize, - const unsigned int assetCount, - asset::AssetList const &asset) const = 0; + const unsigned int assetCount, asset::AssetList const &asset, + bool pretty = false) const = 0; /// @brief get the mime type for the documents /// @return the mime type virtual std::string mimeType() const = 0; diff --git a/src/mtconnect/printer/xml_printer.cpp b/src/mtconnect/printer/xml_printer.cpp index ec0ffea5..887d59ba 100644 --- a/src/mtconnect/printer/xml_printer.cpp +++ b/src/mtconnect/printer/xml_printer.cpp @@ -340,13 +340,14 @@ namespace mtconnect::printer { } std::string XmlPrinter::printErrors(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const ProtoErrorList &list) const + const uint64_t nextSeq, const ProtoErrorList &list, + bool pretty) const { string ret; try { - XmlWriter writer(m_pretty); + XmlWriter writer(m_pretty || pretty); initXmlDoc(writer, eERROR, instanceId, bufferSize, 0, 0, nextSeq, nextSeq - 1); @@ -377,20 +378,21 @@ namespace mtconnect::printer { string XmlPrinter::printProbe(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const unsigned int assetBufferSize, const unsigned int assetCount, const list &deviceList, - const std::map *count) const + const std::map *count, bool includeHidden, + bool pretty) const { string ret; try { - XmlWriter writer(m_pretty); + XmlWriter writer(m_pretty || pretty); initXmlDoc(writer, eDEVICES, instanceId, bufferSize, assetBufferSize, assetCount, nextSeq, 0, nextSeq - 1, count); { AutoElement devices(writer, "Devices"); - entity::XmlPrinter printer; + entity::XmlPrinter printer(includeHidden); for (auto &device : deviceList) printer.print(writer, device, m_deviceNsSet); @@ -413,13 +415,14 @@ namespace mtconnect::printer { string XmlPrinter::printSample(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const uint64_t firstSeq, - const uint64_t lastSeq, ObservationList &observations) const + const uint64_t lastSeq, ObservationList &observations, + bool pretty) const { string ret; try { - XmlWriter writer(m_pretty); + XmlWriter writer(m_pretty || pretty); initXmlDoc(writer, eSTREAMS, instanceId, bufferSize, 0, 0, nextSeq, firstSeq, lastSeq); @@ -492,12 +495,13 @@ namespace mtconnect::printer { } string XmlPrinter::printAssets(const uint64_t instanceId, const unsigned int bufferSize, - const unsigned int assetCount, const AssetList &asset) const + const unsigned int assetCount, const AssetList &asset, + bool pretty) const { string ret; try { - XmlWriter writer(m_pretty); + XmlWriter writer(m_pretty || pretty); initXmlDoc(writer, eASSETS, instanceId, 0u, bufferSize, assetCount, 0ull); { @@ -578,6 +582,8 @@ namespace mtconnect::printer { } string rootName = "MTConnect" + xmlType; + if (!m_schemaVersion) + defaultSchemaVersion(); string xmlns = "urn:mtconnect.org:" + rootName + ":" + *m_schemaVersion; string location; diff --git a/src/mtconnect/printer/xml_printer.hpp b/src/mtconnect/printer/xml_printer.hpp index 955dcf90..bb4e0c2d 100644 --- a/src/mtconnect/printer/xml_printer.hpp +++ b/src/mtconnect/printer/xml_printer.hpp @@ -44,20 +44,22 @@ namespace mtconnect { ~XmlPrinter() override = default; std::string printErrors(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const ProtoErrorList &list) const override; + const uint64_t nextSeq, const ProtoErrorList &list, + bool pretty = false) const override; std::string printProbe(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const unsigned int assetBufferSize, const unsigned int assetCount, const std::list &devices, - const std::map *count = nullptr) const override; + const std::map *count = nullptr, + bool includeHidden = false, bool pretty = false) const override; std::string printSample(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const uint64_t firstSeq, - const uint64_t lastSeq, - observation::ObservationList &results) const override; + const uint64_t lastSeq, observation::ObservationList &results, + bool pretty = false) const override; std::string printAssets(const uint64_t anInstanceId, const unsigned int bufferSize, - const unsigned int assetCount, - const asset::AssetList &asset) const override; + const unsigned int assetCount, const asset::AssetList &asset, + bool pretty = false) const override; std::string mimeType() const override { return "text/xml"; } /// @brief Add a Devices XML device namespace diff --git a/src/mtconnect/sink/rest_sink/rest_service.cpp b/src/mtconnect/sink/rest_sink/rest_service.cpp index 66fe262d..9dfb7e6c 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.cpp +++ b/src/mtconnect/sink/rest_sink/rest_service.cpp @@ -105,6 +105,7 @@ namespace mtconnect { {"to", QUERY, "Sequence number at to stop reporting observations"}, {"from", QUERY, "Sequence number at to start reporting observations"}, {"interval", QUERY, "Time in ms between publishing data–starts streaming"}, + {"pretty", QUERY, "Instructs the result to be pretty printed"}, {"heartbeat", QUERY, "Time in ms between publishing a empty document when no data has changed"}}); @@ -400,31 +401,36 @@ namespace mtconnect { // Probe auto handler = [&](SessionPtr session, const RequestPtr request) -> bool { auto device = request->parameter("device"); + auto pretty = *request->parameter("pretty"); + auto printer = printerForAccepts(request->m_accepts); if (device && !ends_with(request->m_path, string("probe")) && m_sinkContract->findDeviceByUUIDorName(*device) == nullptr) return false; - respond(session, probeRequest(printer, device)); + respond(session, probeRequest(printer, device, pretty)); return true; }; - m_server->addRouting({boost::beast::http::verb::get, "/probe", handler}) + m_server->addRouting({boost::beast::http::verb::get, "/probe?pretty={bool:false}", handler}) .document("MTConnect probe request", "Provides metadata service for the MTConnect Devices information model for all " "devices."); - m_server->addRouting({boost::beast::http::verb::get, "/{device}/probe", handler}) + m_server + ->addRouting( + {boost::beast::http::verb::get, "/{device}/probe?pretty={bool:false}", handler}) .document("MTConnect probe request", "Provides metadata service for the MTConnect Devices information model for " "device identified by `device` matching `name` or `uuid`."); // Must be last - m_server->addRouting({boost::beast::http::verb::get, "/", handler}) + m_server->addRouting({boost::beast::http::verb::get, "/?pretty={bool:false}", handler}) .document("MTConnect probe request", "Provides metadata service for the MTConnect Devices information model for all " "devices."); - m_server->addRouting({boost::beast::http::verb::get, "/{device}", handler}) + m_server + ->addRouting({boost::beast::http::verb::get, "/{device}??pretty={bool:false}", handler}) .document("MTConnect probe request", "Provides metadata service for the MTConnect Devices information model for " "device identified by `device` matching `name` or `uuid`."); @@ -466,7 +472,9 @@ namespace mtconnect { return true; }; - string qp("type={string}&removed={bool:false}&count={integer:100}&device={string}"); + string qp( + "type={string}&removed={bool:false}&" + "count={integer:100}&device={string}&pretty={bool:false}"); m_server->addRouting({boost::beast::http::verb::get, "/assets?" + qp, handler}) .document("MTConnect assets request", "Returns up to `count` assets"); m_server->addRouting({boost::beast::http::verb::get, "/asset?" + qp, handler}) @@ -568,19 +576,23 @@ namespace mtconnect { { streamCurrentRequest(session, printerForAccepts(request->m_accepts), *interval, request->parameter("device"), - request->parameter("path")); + request->parameter("path"), + *request->parameter("pretty")); } else { respond(session, currentRequest(printerForAccepts(request->m_accepts), request->parameter("device"), request->parameter("at"), - request->parameter("path"))); + request->parameter("path"), + *request->parameter("pretty"))); } return true; }; - string qp("path={string}&at={unsigned_integer}&interval={integer}"); + string qp( + "path={string}&at={unsigned_integer}&" + "interval={integer}&pretty={bool:false}"); m_server->addRouting({boost::beast::http::verb::get, "/current?" + qp, handler}) .document("MTConnect current request", "Gets a stapshot of the state of all the observations for all devices " @@ -602,7 +614,7 @@ namespace mtconnect { session, printerForAccepts(request->m_accepts), *interval, *request->parameter("heartbeat"), *request->parameter("count"), request->parameter("device"), request->parameter("from"), - request->parameter("path")); + request->parameter("path"), *request->parameter("pretty")); } else { @@ -610,7 +622,8 @@ namespace mtconnect { sampleRequest( printerForAccepts(request->m_accepts), *request->parameter("count"), request->parameter("device"), request->parameter("from"), - request->parameter("to"), request->parameter("path"))); + request->parameter("to"), request->parameter("path"), + *request->parameter("pretty"))); } return true; }; @@ -618,7 +631,8 @@ namespace mtconnect { string qp( "path={string}&from={unsigned_integer}&" "interval={integer}&count={integer:100}&" - "heartbeat={integer:10000}&to={unsigned_integer}"); + "heartbeat={integer:10000}&to={unsigned_integer}&" + "pretty={bool:false}"); m_server->addRouting({boost::beast::http::verb::get, "/sample?" + qp, handler}) .document("MTConnect sample request", "Gets a time series of at maximum `count` observations for all devices " @@ -685,7 +699,7 @@ namespace mtconnect { // ------------------------------------------- ResponsePtr RestService::probeRequest(const Printer *printer, - const std::optional &device) + const std::optional &device, bool pretty) { NAMED_SCOPE("RestService::probeRequest"); @@ -709,14 +723,14 @@ namespace mtconnect { m_sinkContract->getCircularBuffer().getSequence(), uint32_t(m_sinkContract->getAssetStorage()->getMaxAssets()), uint32_t(m_sinkContract->getAssetStorage()->getCount()), deviceList, - &counts), + &counts, false, pretty), printer->mimeType()); } ResponsePtr RestService::currentRequest(const Printer *printer, const std::optional &device, const std::optional &at, - const std::optional &path) + const std::optional &path, bool pretty) { using namespace rest_sink; DevicePtr dev {nullptr}; @@ -732,7 +746,8 @@ namespace mtconnect { } // Check if there is a frequency to stream data or not - return make_unique(rest_sink::status::ok, fetchCurrentData(printer, filter, at), + return make_unique(rest_sink::status::ok, + fetchCurrentData(printer, filter, at, pretty), printer->mimeType()); } @@ -740,7 +755,7 @@ namespace mtconnect { const std::optional &device, const std::optional &from, const std::optional &to, - const std::optional &path) + const std::optional &path, bool pretty) { using namespace rest_sink; DevicePtr dev {nullptr}; @@ -761,7 +776,8 @@ namespace mtconnect { return make_unique( rest_sink::status::ok, - fetchSampleData(printer, filter, count, from, to, end, endOfBuffer), printer->mimeType()); + fetchSampleData(printer, filter, count, from, to, end, endOfBuffer, nullptr, pretty), + printer->mimeType()); } struct AsyncSampleResponse @@ -787,13 +803,14 @@ namespace mtconnect { ChangeObserver m_observer; chrono::system_clock::time_point m_last; boost::asio::steady_timer m_timer; + bool m_pretty {false}; }; void RestService::streamSampleRequest(rest_sink::SessionPtr session, const Printer *printer, const int interval, const int heartbeatIn, const int count, const std::optional &device, const std::optional &from, - const std::optional &path) + const std::optional &path, bool pretty) { NAMED_SCOPE("RestService::streamSampleRequest"); @@ -814,6 +831,7 @@ namespace mtconnect { asyncResponse->m_printer = printer; asyncResponse->m_heartbeat = std::chrono::milliseconds(heartbeatIn); asyncResponse->m_service = getptr(); + asyncResponse->m_pretty = pretty; checkPath(asyncResponse->m_printer, path, dev, asyncResponse->m_filter); @@ -968,7 +986,8 @@ namespace mtconnect { // sent. content = fetchSampleData(asyncResponse->m_printer, asyncResponse->m_filter, asyncResponse->m_count, asyncResponse->m_sequence, nullopt, end, - asyncResponse->m_endOfBuffer, &asyncResponse->m_observer); + asyncResponse->m_endOfBuffer, &asyncResponse->m_observer, + asyncResponse->m_pretty); // Even if we are at the end of the buffer, or within range. If we are filtering, // we will need to make sure we are not spinning when there are no valid events @@ -1003,12 +1022,13 @@ namespace mtconnect { const Printer *m_printer {nullptr}; FilterSetOpt m_filter; boost::asio::steady_timer m_timer; + bool m_pretty {false}; }; void RestService::streamCurrentRequest(SessionPtr session, const Printer *printer, const int interval, const std::optional &device, - const std::optional &path) + const std::optional &path, bool pretty) { checkRange(printer, interval, 0, numeric_limits().max(), "interval"); DevicePtr dev {nullptr}; @@ -1026,6 +1046,7 @@ namespace mtconnect { asyncResponse->m_interval = chrono::milliseconds {interval}; asyncResponse->m_printer = printer; asyncResponse->m_service = getptr(); + asyncResponse->m_pretty = pretty; asyncResponse->m_session->beginStreaming( printer->mimeType(), boost::asio::bind_executor(m_strand, [this, asyncResponse]() { @@ -1061,7 +1082,8 @@ namespace mtconnect { } asyncResponse->m_session->writeChunk( - fetchCurrentData(asyncResponse->m_printer, asyncResponse->m_filter, nullopt), + fetchCurrentData(asyncResponse->m_printer, asyncResponse->m_filter, nullopt, + asyncResponse->m_pretty), boost::asio::bind_executor(m_strand, [this, asyncResponse]() { asyncResponse->m_timer.expires_from_now(asyncResponse->m_interval); asyncResponse->m_timer.async_wait(boost::asio::bind_executor( @@ -1072,7 +1094,7 @@ namespace mtconnect { ResponsePtr RestService::assetRequest(const Printer *printer, const int32_t count, const bool removed, const std::optional &type, - const std::optional &device) + const std::optional &device, bool pretty) { using namespace rest_sink; @@ -1088,14 +1110,14 @@ namespace mtconnect { m_sinkContract->getAssetStorage()->getAssets(list, count, !removed, uuid, type); return make_unique( status::ok, - printer->printAssets(m_instanceId, - uint32_t(m_sinkContract->getAssetStorage()->getMaxAssets()), - uint32_t(m_sinkContract->getAssetStorage()->getCount()), list), + printer->printAssets( + m_instanceId, uint32_t(m_sinkContract->getAssetStorage()->getMaxAssets()), + uint32_t(m_sinkContract->getAssetStorage()->getCount()), list, pretty), printer->mimeType()); } ResponsePtr RestService::assetIdsRequest(const Printer *printer, - const std::list &ids) + const std::list &ids, bool pretty) { using namespace rest_sink; @@ -1109,16 +1131,16 @@ namespace mtconnect { auto message = str.str().substr(0, str.str().size() - 2); return make_unique(status::not_found, - printError(printer, "ASSET_NOT_FOUND", message), + printError(printer, "ASSET_NOT_FOUND", message, pretty), printer->mimeType()); } else { return make_unique( status::ok, - printer->printAssets(m_instanceId, - uint32_t(m_sinkContract->getAssetStorage()->getMaxAssets()), - uint32_t(m_sinkContract->getAssetStorage()->getCount()), list), + printer->printAssets( + m_instanceId, uint32_t(m_sinkContract->getAssetStorage()->getMaxAssets()), + uint32_t(m_sinkContract->getAssetStorage()->getCount()), list, pretty), printer->mimeType()); } } @@ -1304,13 +1326,13 @@ namespace mtconnect { } string RestService::printError(const Printer *printer, const string &errorCode, - const string &text) const + const string &text, bool pretty) const { LOG(debug) << "Returning error " << errorCode << ": " << text; if (printer) return printer->printError( m_instanceId, m_sinkContract->getCircularBuffer().getBufferSize(), - m_sinkContract->getCircularBuffer().getSequence(), errorCode, text); + m_sinkContract->getCircularBuffer().getSequence(), errorCode, text, pretty); else return errorCode + ": " + text; } @@ -1385,7 +1407,7 @@ namespace mtconnect { // ------------------------------------------- string RestService::fetchCurrentData(const Printer *printer, const FilterSetOpt &filterSet, - const optional &at) + const optional &at, bool pretty) { ObservationList observations; SequenceNumber_t firstSeq, seq; @@ -1409,14 +1431,14 @@ namespace mtconnect { } return printer->printSample(m_instanceId, m_sinkContract->getCircularBuffer().getBufferSize(), - seq, firstSeq, seq - 1, observations); + seq, firstSeq, seq - 1, observations, pretty); } string RestService::fetchSampleData(const Printer *printer, const FilterSetOpt &filterSet, int count, const std::optional &from, const std::optional &to, SequenceNumber_t &end, bool &endOfBuffer, - ChangeObserver *observer) + ChangeObserver *observer, bool pretty) { std::unique_ptr observations; SequenceNumber_t firstSeq, lastSeq; @@ -1449,7 +1471,7 @@ namespace mtconnect { } return printer->printSample(m_instanceId, m_sinkContract->getCircularBuffer().getBufferSize(), - end, firstSeq, lastSeq, *observations); + end, firstSeq, lastSeq, *observations, pretty); } } // namespace sink::rest_sink diff --git a/src/mtconnect/sink/rest_sink/rest_service.hpp b/src/mtconnect/sink/rest_sink/rest_service.hpp index 2368641a..83e27864 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.hpp +++ b/src/mtconnect/sink/rest_sink/rest_service.hpp @@ -91,65 +91,75 @@ namespace mtconnect { ///@{ /// @brief Handler for a probe request - /// @param p printer for doc generation - /// @param device optional device name or uuid + /// @param[in] p printer for doc generation + /// @param[in] device optional device name or uuid + /// @param[in] pretty `true` to ensure response is formatted /// @return MTConnect Devices response ResponsePtr probeRequest(const printer::Printer *p, - const std::optional &device = std::nullopt); + const std::optional &device = std::nullopt, + bool pretty = false); /// @brief Handler for a current request - /// @param p printer for doc generation - /// @param device optional device name or uuid - /// @param at optional sequence number to take the snapshot - /// @param path an xpath to filter + /// @param[in] p printer for doc generation + /// @param[in] device optional device name or uuid + /// @param[in] at optional sequence number to take the snapshot + /// @param[in] path an xpath to filter + /// @param[in] pretty `true` to ensure response is formatted /// @return MTConnect Streams response ResponsePtr currentRequest(const printer::Printer *p, const std::optional &device = std::nullopt, const std::optional &at = std::nullopt, - const std::optional &path = std::nullopt); + const std::optional &path = std::nullopt, + bool pretty = false); /// @brief Handler for a sample request - /// @param p printer for doc generation - /// @param count maximum number of observations - /// @param device optional device name or uuid - /// @param from optional starting sequence number - /// @param to optional ending sequence number - /// @param path an xpath for filtering + /// @param[in] p printer for doc generation + /// @param[in] count maximum number of observations + /// @param[in] device optional device name or uuid + /// @param[in] from optional starting sequence number + /// @param[in] to optional ending sequence number + /// @param[in] path an xpath for filtering + /// @param[in] pretty `true` to ensure response is formatted /// @return MTConnect Streams response ResponsePtr sampleRequest(const printer::Printer *p, const int count = 100, const std::optional &device = std::nullopt, const std::optional &from = std::nullopt, const std::optional &to = std::nullopt, - const std::optional &path = std::nullopt); + const std::optional &path = std::nullopt, + bool pretty = false); /// @brief Handler for a streaming sample - /// @param session session to stream data to - /// @param p printer for doc generation - /// @param interval the minimum interval between sending documents in ms - /// @param heartbeat how often to send an empty document if no activity in ms - /// @param count the maxumum number of observations - /// @param device optional device name or uuid - /// @param from optional starting sequence number - /// @param path optional path for filtering + /// @param[in] session session to stream data to + /// @param[in] p printer for doc generation + /// @param[in] interval the minimum interval between sending documents in ms + /// @param[in] heartbeat how often to send an empty document if no activity in ms + /// @param[in] count the maxumum number of observations + /// @param[in] device optional device name or uuid + /// @param[in] from optional starting sequence number + /// @param[in] path optional path for filtering + /// @param[in] pretty `true` to ensure response is formatted void streamSampleRequest(SessionPtr session, const printer::Printer *p, const int interval, const int heartbeat, const int count = 100, const std::optional &device = std::nullopt, const std::optional &from = std::nullopt, - const std::optional &path = std::nullopt); + const std::optional &path = std::nullopt, + bool pretty = false); /// @brief Handler for a streaming current - /// @param session session to stream data to - /// @param p printer for doc generation - /// @param interval the minimum interval between sending documents in ms - /// @param device optional device name or uuid - /// @param path optional path for filtering + /// @param[in] session session to stream data to + /// @param[in] p printer for doc generation + /// @param[in] interval the minimum interval between sending documents in ms + /// @param[in] device optional device name or uuid + /// @param[in] path optional path for filtering + /// @param[in] pretty `true` to ensure response is formatted void streamCurrentRequest(SessionPtr session, const printer::Printer *p, const int interval, const std::optional &device = std::nullopt, - const std::optional &path = std::nullopt); + const std::optional &path = std::nullopt, + bool pretty = false); /// @brief Handler for put/post observation - /// @param p printer for response generation - /// @param device device - /// @param observations key/value pairs for the observations - /// @param time optional timestamp + /// @param[in] p printer for response generation + /// @param[in] device device + /// @param[in] observations key/value pairs for the observations + /// @param[in] time optional timestamp /// @return `` if succeeds ResponsePtr putObservationRequest(const printer::Printer *p, const std::string &device, const QueryMap observations, @@ -181,21 +191,25 @@ namespace mtconnect { ///@{ /// @brief Asset request handler for assets by type or device - /// @param p printer for the response document - /// @param count maximum number of assets to return - /// @param removed `true` if response should include removed assets - /// @param type optional type of asset to filter - /// @param device optional device name or uuid + /// @param[in] p printer for the response document + /// @param[in] count maximum number of assets to return + /// @param[in] removed `true` if response should include removed assets + /// @param[in] type optional type of asset to filter + /// @param[in] device optional device name or uuid + /// @param[in] pretty `true` to ensure response is formatted /// @return MTConnect Assets response document ResponsePtr assetRequest(const printer::Printer *p, const int32_t count, const bool removed, const std::optional &type = std::nullopt, - const std::optional &device = std::nullopt); + const std::optional &device = std::nullopt, + bool pretty = false); /// @brief Asset request handler using a list of asset ids - /// @param p printer for the response document - /// @param ids list of asset ids + /// @param[in] p printer for the response document + /// @param[in] ids list of asset ids + /// @param[in] pretty `true` to ensure response is formatted /// @return MTConnect Assets response document - ResponsePtr assetIdsRequest(const printer::Printer *p, const std::list &ids); + ResponsePtr assetIdsRequest(const printer::Printer *p, const std::list &ids, + bool pretty = false); /// @brief Asset request handler to update an asset /// @param p printer for the response document @@ -242,7 +256,7 @@ namespace mtconnect { /// @param text descriptive error text /// @return MTConnect Error document std::string printError(const printer::Printer *printer, const std::string &errorCode, - const std::string &text) const; + const std::string &text, bool pretty = false) const; /// @name For testing only ///@{ @@ -281,14 +295,15 @@ namespace mtconnect { // Current Data Collection std::string fetchCurrentData(const printer::Printer *printer, const FilterSetOpt &filterSet, - const std::optional &at); + const std::optional &at, bool pretty = false); // Sample data collection std::string fetchSampleData(const printer::Printer *printer, const FilterSetOpt &filterSet, int count, const std::optional &from, const std::optional &to, SequenceNumber_t &end, bool &endOfBuffer, - observation::ChangeObserver *observer = nullptr); + observation::ChangeObserver *observer = nullptr, + bool pretty = false); // Verification methods template diff --git a/src/mtconnect/sink/rest_sink/server.cpp b/src/mtconnect/sink/rest_sink/server.cpp index a1728ade..cfafe518 100644 --- a/src/mtconnect/sink/rest_sink/server.cpp +++ b/src/mtconnect/sink/rest_sink/server.cpp @@ -272,7 +272,7 @@ namespace mtconnect::sink::rest_sink { visit( overloaded {[](const std::monostate &) {}, [&obj](const std::string &s) { obj.Add(s); }, [&obj](int32_t i) { obj.Add(i); }, [&obj](uint64_t i) { obj.Add(i); }, - [&obj](double d) { obj.Add(d); }}, + [&obj](double d) { obj.Add(d); }, [&obj](bool b) { obj.Add(b); }}, param.m_default); } } diff --git a/src/mtconnect/sink/rest_sink/server.hpp b/src/mtconnect/sink/rest_sink/server.hpp index c7c84557..07c5c4b5 100644 --- a/src/mtconnect/sink/rest_sink/server.hpp +++ b/src/mtconnect/sink/rest_sink/server.hpp @@ -245,6 +245,7 @@ namespace mtconnect::sink::rest_sink { /// @name Swagger Support /// @{ + /// /// @brief Add swagger routings to the Agent void addSwaggerRoutings(); /// @brief generate swagger API from routings diff --git a/src/mtconnect/source/adapter/adapter_pipeline.cpp b/src/mtconnect/source/adapter/adapter_pipeline.cpp index ce6f83c8..b902164f 100644 --- a/src/mtconnect/source/adapter/adapter_pipeline.cpp +++ b/src/mtconnect/source/adapter/adapter_pipeline.cpp @@ -69,9 +69,10 @@ namespace mtconnect { "Message", Properties {{"VALUE", data}, {"topic", topic}, {"source", source}}); run(std::move(entity)); }; - handler->m_command = [this](const std::string &data, const std::string &source) { - auto entity = - make_shared("Command", Properties {{"VALUE", data}, {"source", source}}); + handler->m_command = [this](const std::string &command, const std::string &value, + const std::string &source) { + auto entity = make_shared( + "Command", Properties {{"command", command}, {"VALUE", value}, {"source", source}}); run(std::move(entity)); }; diff --git a/src/mtconnect/source/adapter/adapter_pipeline.hpp b/src/mtconnect/source/adapter/adapter_pipeline.hpp index 3948c862..6f1660cf 100644 --- a/src/mtconnect/source/adapter/adapter_pipeline.hpp +++ b/src/mtconnect/source/adapter/adapter_pipeline.hpp @@ -26,6 +26,8 @@ namespace mtconnect::source::adapter { struct Handler { using ProcessData = std::function; + using ProcessCommand = std::function; using ProcessMessage = std::function; using Connect = std::function; @@ -33,7 +35,7 @@ namespace mtconnect::source::adapter { /// @brief Process Data Messages ProcessData m_processData; /// @brief Process an adapter command - ProcessData m_command; + ProcessCommand m_command; /// @brief Process a message with a topic ProcessMessage m_processMessage; diff --git a/src/mtconnect/source/adapter/shdr/connector.cpp b/src/mtconnect/source/adapter/shdr/connector.cpp index bb144eb1..f8570448 100644 --- a/src/mtconnect/source/adapter/shdr/connector.cpp +++ b/src/mtconnect/source/adapter/shdr/connector.cpp @@ -289,19 +289,12 @@ namespace mtconnect::source::adapter::shdr { LOG(trace) << "(" << m_server << ":" << m_port << ") Received line: " << line; // Check for heartbeats - if (line[0] == '*') + if (line[0] == '*' && !line.compare(0, 6, "* PONG")) { - if (!line.compare(0, 6, "* PONG")) - { - LOG(debug) << "(Port:" << m_localPort << ") Received a PONG for " << m_server << " on port " - << m_port; - if (!m_heartbeats) - startHeartbeats(line); - } - else - { - protocolCommand(line); - } + LOG(debug) << "(Port:" << m_localPort << ") Received a PONG for " << m_server << " on port " + << m_port; + if (!m_heartbeats) + startHeartbeats(line); } else { diff --git a/src/mtconnect/source/adapter/shdr/shdr_adapter.cpp b/src/mtconnect/source/adapter/shdr/shdr_adapter.cpp index 0496ff38..1b119b6d 100644 --- a/src/mtconnect/source/adapter/shdr/shdr_adapter.cpp +++ b/src/mtconnect/source/adapter/shdr/shdr_adapter.cpp @@ -19,6 +19,9 @@ #include "shdr_adapter.hpp" #include +#include +#include +#include #include #include @@ -112,29 +115,25 @@ namespace mtconnect::source::adapter::shdr { { if (data == *m_terminator) { - if (m_handler && m_handler->m_processData) - m_handler->m_processData(m_body.str(), getIdentity()); + forwardData(m_body.str()); m_terminator.reset(); m_body.str(""); } else { - m_body << endl << data; + m_body << std::endl << data; } - - return; } - - if (size_t multi = data.find("--multiline--"); multi != string::npos) + else if (size_t multi = data.find("--multiline--"); multi != std::string::npos) { m_body.str(""); m_body << data.substr(0, multi); m_terminator = data.substr(multi); - return; } - - if (m_handler && m_handler->m_processData) - m_handler->m_processData(data, getIdentity()); + else + { + forwardData(data); + } } catch (std::exception &e) { @@ -164,18 +163,30 @@ namespace mtconnect::source::adapter::shdr { { NAMED_SCOPE("ShdrAdapter::protocolCommand"); - static auto pattern = regex("\\*[ ]*([^:]+):[ ]*(.+)"); - smatch match; - using namespace boost::algorithm; + namespace qi = boost::spirit::qi; + namespace ascii = boost::spirit::ascii; + namespace phoenix = boost::phoenix; - if (std::regex_match(data, match, pattern)) - { - auto command = to_lower_copy(match[1].str()); - auto value = match[2].str(); + using ascii::space; + using qi::char_; + using qi::lexeme; + using qi::lit; + string command; + auto f = [&command](const auto &s) { command = string(s.begin(), s.end()); }; + + auto it = data.begin(); + bool res = + qi::phrase_parse(it, data.end(), (lit("*") >> lexeme[+(char_ - ':')][f] >> ':'), space); + + if (res) + { + string value(it, data.end()); ConfigOptions options; + boost::to_lower(command); + if (command == "conversionrequired") options[configuration::ConversionRequired] = is_true(value); else if (command == "relativetime") @@ -190,7 +201,11 @@ namespace mtconnect::source::adapter::shdr { if (options.size() > 0) setOptions(options); else if (m_handler && m_handler->m_command) - m_handler->m_command(data, getIdentity()); + m_handler->m_command(command, value, getIdentity()); + } + else + { + LOG(warning) << "protocolCommand: Cannot parse command: " << data; } } } // namespace mtconnect::source::adapter::shdr diff --git a/src/mtconnect/source/adapter/shdr/shdr_adapter.hpp b/src/mtconnect/source/adapter/shdr/shdr_adapter.hpp index 169d61fe..5187aa6c 100644 --- a/src/mtconnect/source/adapter/shdr/shdr_adapter.hpp +++ b/src/mtconnect/source/adapter/shdr/shdr_adapter.hpp @@ -118,15 +118,25 @@ namespace mtconnect { /// @brief Change the options for the adapter /// @param[in] options the set of options - void setOptions(const ConfigOptions &options) + void setOptions(const ConfigOptions &options) override { for (auto &o : options) m_options.insert_or_assign(o.first, o.second); + bool started = m_pipeline.started(); m_pipeline.build(m_options); - if (!m_pipeline.started()) + if (!m_pipeline.started() && started) m_pipeline.start(); } + protected: + void forwardData(const std::string &data) + { + if (data[0] == '*') + protocolCommand(data); + else if (m_handler && m_handler->m_processData) + m_handler->m_processData(data, getIdentity()); + } + protected: ShdrPipeline m_pipeline; diff --git a/src/mtconnect/source/loopback_source.cpp b/src/mtconnect/source/loopback_source.cpp index 05acd1de..9befdcf1 100644 --- a/src/mtconnect/source/loopback_source.cpp +++ b/src/mtconnect/source/loopback_source.cpp @@ -112,7 +112,7 @@ namespace mtconnect::source { entity::ErrorList &errors) { // Parse the asset - auto entity = entity::XmlParser::parse(asset::Asset::getRoot(), document, "1.7", errors); + auto entity = entity::XmlParser::parse(asset::Asset::getRoot(), document, errors); if (!entity) { LOG(warning) << "Asset could not be parsed"; diff --git a/src/mtconnect/source/source.hpp b/src/mtconnect/source/source.hpp index 38c6a71c..cdacfb9f 100644 --- a/src/mtconnect/source/source.hpp +++ b/src/mtconnect/source/source.hpp @@ -87,6 +87,10 @@ namespace mtconnect { /// @return the asio strand boost::asio::io_context::strand &getStrand(); + /// @brief changes the options in the source + /// @param[in] options the options to update + virtual void setOptions(const ConfigOptions &options) {} + protected: std::string m_name; boost::asio::io_context::strand m_strand; diff --git a/src/mtconnect/utilities.hpp b/src/mtconnect/utilities.hpp index 4ef4862a..07e48f10 100644 --- a/src/mtconnect/utilities.hpp +++ b/src/mtconnect/utilities.hpp @@ -21,9 +21,11 @@ #pragma once #include +#include #include #include #include +#include #include #include @@ -714,15 +716,67 @@ namespace mtconnect { /// @param s the version inline int32_t IntSchemaVersion(const std::string &s) { - int major, minor; + int major {0}, minor {0}; char c; std::stringstream vstr(s); vstr >> major >> c >> minor; - return SCHEMA_VERSION(major, minor); + if (major == 0) + { + return IntDefaultSchemaVersion(); + } + else + { + return SCHEMA_VERSION(major, minor); + } } /// @brief Retrieve the best Host IP address from the network interfaces. /// @param[in] context the boost asio io_context for resolving the address /// @param[in] onlyV4 only consider IPV4 addresses if `true` std::string GetBestHostAddress(boost::asio::io_context &context, bool onlyV4 = false); + + /// @brief Function to create a unique id given a sha1 namespace and an id. + /// + /// Creates a base 64 encoded version of the string and removes any illegal characters + /// for an ID. If the first character is not a legal start character, maps the first 2 characters + /// to the legal ID start char set. + /// + /// @param[in] sha the sha1 namespace to use as context + /// @param[in] id the id to use transform + /// @returns Returns the first 16 characters of the base 64 encoded sha1 + inline std::string makeUniqueId(const boost::uuids::detail::sha1 &sha, const std::string &id) + { + using namespace std; + + boost::uuids::detail::sha1 sha1(sha); + + constexpr string_view startc("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_"); + constexpr auto isIDStartChar = [](unsigned char c) -> bool { return isalpha(c) || c == '_'; }; + constexpr auto isIDChar = [isIDStartChar](unsigned char c) -> bool { + return isIDStartChar(c) || isdigit(c) || c == '.' || c == '-'; + }; + + sha1.process_bytes(id.data(), id.length()); + unsigned int digest[5]; + sha1.get_digest(digest); + + string s(32, ' '); + auto len = boost::beast::detail::base64::encode(s.data(), digest, sizeof(digest)); + + s.erase(len - 1); + std::remove_if(++(s.begin()), s.end(), not_fn(isIDChar)); + + // Check if the character is legal. + if (!isIDStartChar(s[0])) + { + // Change the start character to a legal character + uint32_t c = s[0] + s[1]; + s.erase(0, 1); + s[0] = startc[c % startc.size()]; + } + + s.erase(16); + + return s; + } } // namespace mtconnect diff --git a/test/adapter_test.cpp b/test/adapter_test.cpp index c30b4ea5..1ac8c8e8 100644 --- a/test/adapter_test.cpp +++ b/test/adapter_test.cpp @@ -75,3 +75,50 @@ Another Line... --multiline--ABC---)DOC"; EXPECT_EQ(exp, data); } + +TEST(AdapterTest, should_forward_multiline_command) +{ + asio::io_context ioc; + asio::io_context::strand strand(ioc); + ConfigOptions options {{configuration::Host, "localhost"s}, {configuration::Port, 7878}}; + boost::property_tree::ptree tree; + pipeline::PipelineContextPtr context = make_shared(); + auto adapter = make_unique(ioc, context, options, tree); + + auto handler = make_unique(); + string command, value; + handler->m_command = [&](const string &c, const string &v, const string &s) { + command = c; + value = v; + }; + adapter->setHandler(handler); + + adapter->processData("* deviceModel: --multiline--ABC1234"); + EXPECT_TRUE(adapter->getTerminator()); + EXPECT_EQ("--multiline--ABC1234", *adapter->getTerminator()); + adapter->processData(""); + adapter->processData(" "); + adapter->processData(""); + adapter->processData("--multiline--ABC1234"); + + const auto exp = R"DOC( + +)DOC"; + EXPECT_EQ("devicemodel", command); + EXPECT_EQ(exp, value); +} + +TEST(AdapterTest, should_set_options_from_commands) +{ + asio::io_context ioc; + asio::io_context::strand strand(ioc); + ConfigOptions options {{configuration::Host, "localhost"s}, {configuration::Port, 7878}}; + boost::property_tree::ptree tree; + pipeline::PipelineContextPtr context = make_shared(); + auto adapter = make_unique(ioc, context, options, tree); + + adapter->processData("* shdrVersion: 3"); + + auto v = GetOption(adapter->getOptions(), "ShdrVersion"); + ASSERT_EQ(int64_t(3), *v); +} diff --git a/test/agent_adapter_test.cpp b/test/agent_adapter_test.cpp index 386e2787..c5f2a7ae 100644 --- a/test/agent_adapter_test.cpp +++ b/test/agent_adapter_test.cpp @@ -121,7 +121,8 @@ class AgentAdapterTest : public testing::Test } m_agentTestHelper->getAgent()->stop(); - m_agentTestHelper->m_ioContext.run_for(100s); + m_agentTestHelper->m_ioContext.removeGuard(); + m_agentTestHelper->m_ioContext.run_for(10s); m_agentTestHelper.reset(); m_adapter.reset(); diff --git a/test/agent_test_helper.hpp b/test/agent_test_helper.hpp index bb83eac2..2f3291bc 100644 --- a/test/agent_test_helper.hpp +++ b/test/agent_test_helper.hpp @@ -127,6 +127,7 @@ class AgentTestHelper if (m_agent) m_agent->stop(); m_agent.reset(); + m_ioContext.stop(); } auto session() { return m_session; } @@ -309,7 +310,7 @@ class AgentTestHelper std::unique_ptr m_agent; std::stringstream m_out; mtconnect::sink::rest_sink::RequestPtr m_request; - boost::asio::io_context m_ioContext; + mtconnect::configuration::AsyncContext m_ioContext; boost::asio::io_context::strand m_strand; boost::asio::ip::tcp::socket m_socket; mtconnect::sink::rest_sink::Response m_response; diff --git a/test/asset_test.cpp b/test/asset_test.cpp index 82d3aebf..49e383f3 100644 --- a/test/asset_test.cpp +++ b/test/asset_test.cpp @@ -74,7 +74,7 @@ TEST_F(AssetTest, TestExtendedAsset) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "1.7", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); auto asset = dynamic_cast(entity.get()); ASSERT_NE(nullptr, asset); @@ -102,7 +102,7 @@ TEST_F(AssetTest, asset_should_parse_and_load_if_asset_id_is_missing_from_xml) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "1.7", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); auto asset = dynamic_cast(entity.get()); ASSERT_NE(nullptr, asset); diff --git a/test/component_parameters_test.cpp b/test/component_parameters_test.cpp index 61fc0c16..5b5c5294 100644 --- a/test/component_parameters_test.cpp +++ b/test/component_parameters_test.cpp @@ -80,7 +80,7 @@ TEST_F(ComponentParametersTest, should_parse_simple_parameter_set) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "2.2", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); auto asset = dynamic_cast(entity.get()); @@ -133,7 +133,7 @@ TEST_F(ComponentParametersTest, should_parse_simple_parameter_set) hdoc.insert(68, string(" hash=\"" + hash1 + "\"")); ASSERT_EQ(content, hdoc); - auto entity2 = parser.parse(Asset::getRoot(), content, "2.2", errors); + auto entity2 = parser.parse(Asset::getRoot(), content, errors); ASSERT_EQ(hash1, entity2->hash()); @@ -162,7 +162,7 @@ TEST_F(ComponentParametersTest, should_parse_two_parameter_sets) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "2.2", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); auto asset = dynamic_cast(entity.get()); diff --git a/test/config_test.cpp b/test/config_test.cpp index 00d7b690..86630978 100644 --- a/test/config_test.cpp +++ b/test/config_test.cpp @@ -75,8 +75,12 @@ namespace { fs::path createTempDirectory(const string &ext) { fs::path root {fs::path(TEST_BIN_ROOT_DIR) / ("config_test_" + ext)}; - if (!fs::exists(root)) - fs::create_directory(root); + if (fs::exists(root)) + { + fs::remove_all(root); + } + + fs::create_directory(root); chdir(root.string().c_str()); m_config->updateWorkingDirectory(); // m_config->setDebug(false); @@ -993,7 +997,7 @@ Port = 0 ASSERT_TRUE(dataItem); ASSERT_EQ("SPINDLE_SPEED", dataItem->getType()); - boost::asio::steady_timer timer1(context.getContext()); + boost::asio::steady_timer timer1(context.get()); timer1.expires_from_now(1s); timer1.async_wait([this, &devices, agent](boost::system::error_code ec) { if (ec) @@ -1012,7 +1016,7 @@ Port = 0 } }); - boost::asio::steady_timer timer2(context.getContext()); + boost::asio::steady_timer timer2(context.get()); timer2.expires_from_now(6s); timer2.async_wait([this, agent, &chg](boost::system::error_code ec) { if (!ec) @@ -1072,7 +1076,7 @@ Port = 0 ASSERT_TRUE(dataItem); ASSERT_EQ("SPINDLE_SPEED", dataItem->getType()); - boost::asio::steady_timer timer1(context.getContext()); + boost::asio::steady_timer timer1(context.get()); timer1.expires_from_now(1s); timer1.async_wait([this, &devices](boost::system::error_code ec) { if (ec) @@ -1085,7 +1089,7 @@ Port = 0 } }); - boost::asio::steady_timer timer2(context.getContext()); + boost::asio::steady_timer timer2(context.get()); timer2.expires_from_now(6s); timer2.async_wait([this, &chg](boost::system::error_code ec) { if (!ec) @@ -1138,7 +1142,7 @@ Port = 0 auto instance = rest->instanceId(); - boost::asio::steady_timer timer1(context.getContext()); + boost::asio::steady_timer timer1(context.get()); timer1.expires_from_now(1s); timer1.async_wait([this, &config](boost::system::error_code ec) { if (ec) @@ -1154,7 +1158,7 @@ Port = 0 auto th = thread([this, agent, instance, &context]() { this_thread::sleep_for(5s); - boost::asio::steady_timer timer1(context.getContext()); + boost::asio::steady_timer timer1(context.get()); timer1.expires_from_now(1s); timer1.async_wait([this, agent, instance](boost::system::error_code ec) { if (!ec) @@ -1215,7 +1219,7 @@ Port = 0 DataItemPtr di; - boost::asio::steady_timer timer1(context.getContext()); + boost::asio::steady_timer timer1(context.get()); timer1.expires_from_now(1s); timer1.async_wait([this, &devices](boost::system::error_code ec) { if (ec) @@ -1229,7 +1233,7 @@ Port = 0 } }); - boost::asio::steady_timer timer2(context.getContext()); + boost::asio::steady_timer timer2(context.get()); timer2.expires_from_now(6s); timer2.async_wait([this](boost::system::error_code ec) { if (!ec) @@ -1332,7 +1336,7 @@ Port = 0 ASSERT_NE(nullptr, printer); ASSERT_EQ("1.2", *printer->getSchemaVersion()); - boost::asio::steady_timer timer1(context.getContext()); + boost::asio::steady_timer timer1(context.get()); timer1.expires_from_now(1s); timer1.async_wait([this, &devices, agent](boost::system::error_code ec) { if (ec) @@ -1355,7 +1359,7 @@ Port = 0 auto th = thread([this, agent, instance, &context]() { this_thread::sleep_for(5s); - boost::asio::steady_timer timer1(context.getContext()); + boost::asio::steady_timer timer1(context.get()); timer1.expires_from_now(1s); timer1.async_wait([this, agent, instance](boost::system::error_code ec) { if (!ec) @@ -1386,4 +1390,423 @@ Port = 0 th.join(); } + TEST_F(ConfigTest, should_add_a_new_device_when_deviceModel_received_from_adapter) + { + using namespace mtconnect::source::adapter; + + fs::path root {createTempDirectory("6")}; + + fs::path devices(root / "Devices.xml"); + fs::path config {root / "agent.cfg"}; + { + ofstream cfg(config.string()); + cfg << R"DOC( +VersionDeviceXmlUpdates = true +Port = 0 + +Adapters { + Device { + } +} +)DOC"; + cfg << "Devices = " << devices << endl; + } + + copyFile("empty.xml", devices, 0min); + + boost::program_options::variables_map options; + boost::program_options::variable_value value(boost::optional(config.string()), false); + options.insert(make_pair("config-file"s, value)); + + m_config->initialize(options); + auto &asyncContext = m_config->getAsyncContext(); + + auto agent = m_config->getAgent(); + const auto &printer = agent->getPrinter("xml"); + ASSERT_NE(nullptr, printer); + + auto sp = agent->findSource("_localhost_7878"); + ASSERT_TRUE(sp); + + auto adapter = dynamic_pointer_cast(sp); + ASSERT_TRUE(adapter); + + auto validate = [&](boost::system::error_code ec) { + using namespace std::filesystem; + using namespace std::chrono; + using namespace boost::algorithm; + + if (!ec) + { + // Check for backup file + auto ext = date::format(".%Y-%m-%dT%H+", date::floor(system_clock::now())); + auto dit = directory_iterator("."); + std::list entries; + copy_if(dit, end(dit), back_inserter(entries), + [&ext](const auto &de) { return contains(de.path().string(), ext); }); + ASSERT_EQ(1, entries.size()); + + auto device = agent->getDeviceByName("LinuxCNC"); + ASSERT_TRUE(device) << "Cannot find LinuxCNC device"; + + const auto &components = device->getChildren(); + ASSERT_EQ(1, components->size()); + + auto cont = device->getComponentById("cont"); + ASSERT_TRUE(cont) << "Cannot find Component with id cont"; + + auto exec = device->getDeviceDataItem("exec"); + ASSERT_TRUE(exec) << "Cannot find DataItem with id exec"; + + auto pipeline = dynamic_cast(adapter->getPipeline()); + ASSERT_EQ("LinuxCNC", pipeline->getDevice()); + } + m_config->stop(); + }; + + boost::asio::steady_timer timer2(asyncContext.get()); + + auto send = [this, &adapter, &timer2, validate](boost::system::error_code ec) { + if (ec) + { + m_config->stop(); + } + else + { + adapter->processData("* deviceModel: --multiline--AAAAA"); + adapter->processData(R"( + + + + + + + + + + + + + + +)"); + adapter->processData("--multiline--AAAAA"); + + timer2.expires_from_now(500ms); + timer2.async_wait(validate); + } + }; + + boost::asio::steady_timer timer1(asyncContext.get()); + timer1.expires_from_now(100ms); + timer1.async_wait(send); + + m_config->start(); + } + + TEST_F(ConfigTest, should_update_a_device_when_received_from_adapter) + { + using namespace mtconnect::source::adapter; + + fs::path root {createTempDirectory("7")}; + + fs::path devices(root / "Devices.xml"); + fs::path config {root / "agent.cfg"}; + { + ofstream cfg(config.string()); + cfg << R"DOC( +VersionDeviceXmlUpdates = true +CreateUniqueIds = true +Port = 0 +)DOC"; + cfg << "Devices = " << devices << endl; + } + + copyFile("dyn_load.xml", devices, 0min); + + boost::program_options::variables_map options; + boost::program_options::variable_value value(boost::optional(config.string()), false); + options.insert(make_pair("config-file"s, value)); + + m_config->initialize(options); + auto &asyncContext = m_config->getAsyncContext(); + + auto agent = m_config->getAgent(); + auto device = agent->getDeviceByName("LinuxCNC"); + ASSERT_TRUE(device); + + const auto &printer = agent->getPrinter("xml"); + ASSERT_NE(nullptr, printer); + + auto sp = agent->findSource("_localhost_7878"); + ASSERT_TRUE(sp); + + auto adapter = dynamic_pointer_cast(sp); + ASSERT_TRUE(adapter); + + auto validate = [&](boost::system::error_code ec) { + using namespace std::filesystem; + using namespace std::chrono; + using namespace boost::algorithm; + + if (!ec) + { + // Check for backup file + auto ext = date::format(".%Y-%m-%dT%H+", date::floor(system_clock::now())); + auto dit = directory_iterator("."); + std::list entries; + copy_if(dit, end(dit), back_inserter(entries), + [&ext](const auto &de) { return contains(de.path().string(), ext); }); + ASSERT_EQ(2, entries.size()); + + auto device = agent->getDeviceByName("LinuxCNC"); + ASSERT_TRUE(device) << "Cannot find LinuxCNC device"; + + const auto &components = device->getChildren(); + ASSERT_EQ(1, components->size()); + + auto cont = device->getComponentById("cont"); + ASSERT_TRUE(cont) << "Cannot find Component with id cont"; + + auto devDIs = device->getDataItems(); + ASSERT_TRUE(devDIs); + ASSERT_EQ(5, devDIs->size()); + + auto dataItems = cont->getDataItems(); + ASSERT_TRUE(dataItems); + ASSERT_EQ(2, dataItems->size()); + + auto it = dataItems->begin(); + ASSERT_EQ("exc", (*it)->get("originalId")); + it++; + ASSERT_EQ("mode", (*it)->get("originalId")); + + auto estop = device->getDeviceDataItem("estop"); + ASSERT_TRUE(estop) << "Cannot find DataItem with id estop"; + + auto exec = device->getDeviceDataItem("exc"); + ASSERT_TRUE(exec) << "Cannot find DataItem with id exc"; + + auto pipeline = dynamic_cast(adapter->getPipeline()); + ASSERT_EQ("LinuxCNC", pipeline->getDevice()); + } + m_config->stop(); + }; + + boost::asio::steady_timer timer2(asyncContext.get()); + + auto send = [this, &adapter, &timer2, validate](boost::system::error_code ec) { + if (ec) + { + m_config->stop(); + } + else + { + adapter->processData("* deviceModel: --multiline--AAAAA"); + adapter->processData(R"( + + + + + + + + + + + + + + + +)"); + adapter->processData("--multiline--AAAAA"); + + timer2.expires_from_now(500ms); + timer2.async_wait(validate); + } + }; + + boost::asio::steady_timer timer1(asyncContext.get()); + timer1.expires_from_now(100ms); + timer1.async_wait(send); + + m_config->start(); + } + + TEST_F(ConfigTest, should_update_the_ids_of_all_entities) + { + fs::path root {createTempDirectory("8")}; + + fs::path devices(root / "Devices.xml"); + fs::path config {root / "agent.cfg"}; + { + ofstream cfg(config.string()); + cfg << R"DOC( +VersionDeviceXmlUpdates = true +CreateUniqueIds = true +Port = 0 +)DOC"; + cfg << "Devices = " << devices << endl; + } + + copyFile("dyn_load.xml", devices, 0min); + + boost::program_options::variables_map options; + boost::program_options::variable_value value(boost::optional(config.string()), false); + options.insert(make_pair("config-file"s, value)); + + m_config->initialize(options); + + auto agent = m_config->getAgent(); + auto device = agent->getDeviceByName("LinuxCNC"); + ASSERT_TRUE(device); + + auto deviceId = device->getId(); + + ASSERT_NE("d", deviceId); + ASSERT_EQ("d", device->get("originalId")); + + // Get the data item by its old id + auto exec = device->getDeviceDataItem("exec"); + ASSERT_TRUE(exec); + ASSERT_TRUE(exec->getOriginalId()); + ASSERT_EQ("exec", *exec->getOriginalId()); + + // Re-initialize the agent with the modified device.xml with the unique ids aready created + // This tests if the originalId in the device xml file does the ritght thing when mapping ids + m_config = std::make_unique(); + m_config->setDebug(true); + m_config->initialize(options); + + auto agent2 = m_config->getAgent(); + + auto device2 = agent2->getDeviceByName("LinuxCNC"); + ASSERT_TRUE(device2); + ASSERT_EQ(deviceId, device2->getId()); + + auto exec2 = device->getDeviceDataItem("exec"); + ASSERT_TRUE(exec2); + ASSERT_EQ(exec->getId(), exec2->getId()); + ASSERT_TRUE(exec2->getOriginalId()); + ASSERT_EQ("exec", *exec2->getOriginalId()); + } + + TEST_F(ConfigTest, should_add_a_new_device_with_duplicate_ids) + { + using namespace mtconnect::source::adapter; + + fs::path root {createTempDirectory("9")}; + + fs::path devices(root / "Devices.xml"); + fs::path config {root / "agent.cfg"}; + { + ofstream cfg(config.string()); + cfg << R"DOC( +VersionDeviceXmlUpdates = true +CreateUniqueIds = true +Port = 0 +)DOC"; + cfg << "Devices = " << devices << endl; + } + + copyFile("dyn_load.xml", devices, 0min); + + boost::program_options::variables_map options; + boost::program_options::variable_value value(boost::optional(config.string()), false); + options.insert(make_pair("config-file"s, value)); + + m_config->initialize(options); + auto &asyncContext = m_config->getAsyncContext(); + + auto agent = m_config->getAgent(); + auto device = agent->getDeviceByName("LinuxCNC"); + ASSERT_TRUE(device); + + const auto &printer = agent->getPrinter("xml"); + ASSERT_NE(nullptr, printer); + + auto sp = agent->findSource("_localhost_7878"); + ASSERT_TRUE(sp); + + auto adapter = dynamic_pointer_cast(sp); + ASSERT_TRUE(adapter); + + auto validate = [&](boost::system::error_code ec) { + using namespace std::filesystem; + using namespace std::chrono; + using namespace boost::algorithm; + + if (!ec) + { + // Check for backup file + auto ext = date::format(".%Y-%m-%dT%H+", date::floor(system_clock::now())); + auto dit = directory_iterator("."); + std::list entries; + copy_if(dit, end(dit), back_inserter(entries), + [&ext](const auto &de) { return contains(de.path().string(), ext); }); + ASSERT_EQ(2, entries.size()); + + ASSERT_EQ(3, agent->getDevices().size()); + + auto device1 = agent->getDeviceByName("LinuxCNC"); + ASSERT_TRUE(device1) << "Cannot find LinuxCNC device"; + + auto device2 = agent->getDeviceByName("AnotherCNC"); + ASSERT_TRUE(device2) << "Cannot find LinuxCNC device"; + + auto pipeline = dynamic_cast(adapter->getPipeline()); + ASSERT_EQ("AnotherCNC", pipeline->getDevice()); + } + m_config->stop(); + }; + + boost::asio::steady_timer timer2(asyncContext.get()); + + auto send = [this, &adapter, &timer2, validate](boost::system::error_code ec) { + if (ec) + { + m_config->stop(); + } + else + { + auto pipeline = dynamic_cast(adapter->getPipeline()); + ASSERT_EQ("LinuxCNC", pipeline->getDevice()); + + adapter->processData("* deviceModel: --multiline--AAAAA"); + adapter->processData(R"( + + + + + + + + + + + + + + + +)"); + adapter->processData("--multiline--AAAAA"); + + timer2.expires_from_now(500ms); + timer2.async_wait(validate); + } + }; + + boost::asio::steady_timer timer1(asyncContext.get()); + timer1.expires_from_now(100ms); + timer1.async_wait(send); + + m_config->start(); + } + + TEST_F(ConfigTest, should_not_reload_when_monitor_files_is_on) { GTEST_SKIP(); } + + TEST_F(ConfigTest, should_map_references_to_new_ids) { GTEST_SKIP(); } + } // namespace diff --git a/test/connector_test.cpp b/test/connector_test.cpp index d44fc62d..092cb202 100644 --- a/test/connector_test.cpp +++ b/test/connector_test.cpp @@ -67,8 +67,13 @@ class TestConnector : public Connector void processData(const std::string &data) override { - m_data = data; - m_list.emplace_back(m_data); + if (data[0] == '*') + protocolCommand(data); + else + { + m_data = data; + m_list.emplace_back(m_data); + } } void protocolCommand(const std::string &data) override { m_command = data; } diff --git a/test/cutting_tool_test.cpp b/test/cutting_tool_test.cpp index db06c5d0..85e7afe1 100644 --- a/test/cutting_tool_test.cpp +++ b/test/cutting_tool_test.cpp @@ -101,7 +101,7 @@ TEST_F(CuttingToolTest, TestMinmalArchetype) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "1.7", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); auto asset = dynamic_cast(entity.get()); @@ -160,7 +160,7 @@ TEST_F(CuttingToolTest, TestMeasurements) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "1.7", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); auto asset = dynamic_cast(entity.get()); @@ -235,7 +235,7 @@ TEST_F(CuttingToolTest, TestItems) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "1.7", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); auto asset = dynamic_cast(entity.get()); @@ -354,7 +354,7 @@ TEST_F(CuttingToolTest, TestMinmalTool) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "1.7", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); auto asset = dynamic_cast(entity.get()); @@ -410,7 +410,7 @@ TEST_F(CuttingToolTest, TestMinmalToolError) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "1.7", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(2, errors.size()); ASSERT_EQ( "CuttingToolLifeCycle(CutterStatus): Property CutterStatus is required and not provided", @@ -437,7 +437,7 @@ TEST_F(CuttingToolTest, TestMeasurementsError) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "1.7", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(6, errors.size()); auto it = errors.begin(); EXPECT_EQ("FunctionalLength(VALUE): Property VALUE is required and not provided", @@ -522,7 +522,7 @@ TEST_F(CuttingToolTest, test_extended_cutting_item) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "1.7", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); auto asset = dynamic_cast(entity.get()); @@ -663,7 +663,7 @@ TEST_F(CuttingToolTest, test_xmlns_with_top_element_alias) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "1.7", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); // Round trip test @@ -728,7 +728,7 @@ TEST_F(CuttingToolTest, element_order_should_place_cutter_status_before_locus) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "2.0", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); // Round trip test diff --git a/test/entity_parser_test.cpp b/test/entity_parser_test.cpp index 91a8f029..1c130b87 100644 --- a/test/entity_parser_test.cpp +++ b/test/entity_parser_test.cpp @@ -118,7 +118,7 @@ TEST_F(EntityParserTest, TestParseSimpleDocument) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(root, doc, "1.7", errors); + auto entity = parser.parse(root, doc, errors); ASSERT_EQ(0, errors.size()); ASSERT_EQ("FileArchetype", entity->getName()); @@ -163,7 +163,7 @@ TEST_F(EntityParserTest, TestRecursiveEntityLists) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(root, doc, "1.7", errors); + auto entity = parser.parse(root, doc, errors); ASSERT_EQ(0, errors.size()); ASSERT_EQ("Device", entity->getName()); @@ -212,7 +212,7 @@ TEST_F(EntityParserTest, TestRecursiveEntityListFailure) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(root, doc, "1.7", errors); + auto entity = parser.parse(root, doc, errors); ASSERT_EQ(1, errors.size()); ASSERT_FALSE(entity); ASSERT_EQ(string("Device(uuid): Property uuid is required and not provided"), @@ -236,7 +236,7 @@ TEST_F(EntityParserTest, TestRecursiveEntityListMissingComponents) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(root, doc, "1.7", errors); + auto entity = parser.parse(root, doc, errors); ASSERT_EQ(2, errors.size()); ASSERT_TRUE(entity); ASSERT_EQ(string("Components(Component): Entity list requirement Component must have at least 1 " @@ -280,7 +280,7 @@ TEST_F(EntityParserTest, TestRawContent) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(root, doc, "1.7", errors); + auto entity = parser.parse(root, doc, errors); auto expected = R"DOC( And some text @@ -310,7 +310,7 @@ TEST_F(EntityParserTest, check_proper_line_truncation) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(root, doc, "1.7", errors); + auto entity = parser.parse(root, doc, errors); ASSERT_EQ("Description", entity->getName()); ASSERT_EQ("And some text", entity->getValue()); } diff --git a/test/entity_printer_test.cpp b/test/entity_printer_test.cpp index d8b844b5..558f4e8e 100644 --- a/test/entity_printer_test.cpp +++ b/test/entity_printer_test.cpp @@ -103,7 +103,7 @@ TEST_F(EntityPrinterTest, TestParseSimpleDocument) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(root, doc, "1.7", errors); + auto entity = parser.parse(root, doc, errors); ASSERT_EQ(0, errors.size()); entity::XmlPrinter printer; @@ -129,7 +129,7 @@ TEST_F(EntityPrinterTest, TestFileArchetypeWithDescription) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(root, doc, "1.7", errors); + auto entity = parser.parse(root, doc, errors); ASSERT_EQ(0, errors.size()); entity::XmlPrinter printer; @@ -176,7 +176,7 @@ TEST_F(EntityPrinterTest, TestRecursiveEntityLists) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(root, doc, "1.7", errors); + auto entity = parser.parse(root, doc, errors); ASSERT_EQ(0, errors.size()); entity::XmlPrinter printer; @@ -222,7 +222,7 @@ TEST_F(EntityPrinterTest, TestEntityOrder) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(root, doc, "1.7", errors); + auto entity = parser.parse(root, doc, errors); ASSERT_EQ(0, errors.size()); entity::XmlPrinter printer; @@ -264,7 +264,7 @@ TEST_F(EntityPrinterTest, TestRawContent) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(root, doc, "1.7", errors); + auto entity = parser.parse(root, doc, errors); ASSERT_EQ(0, errors.size()); entity::XmlPrinter printer; @@ -280,6 +280,86 @@ TEST_F(EntityPrinterTest, TestRawContent) ASSERT_EQ(expected, m_writer->getContent()); } +TEST_F(EntityPrinterTest, should_honor_include_hidden_parameter) +{ + auto component = make_shared(Requirements { + Requirement("id", true), + Requirement("name", false), + Requirement("uuid", false), + }); + + auto components = make_shared( + Requirements({Requirement("Component", ENTITY, component, 1, Requirement::Infinite)})); + components->registerMatchers(); + components->registerFactory(regex(".+"), component); + + component->addRequirements({Requirement("Components", ENTITY_LIST, components, false)}); + + auto device = make_shared(*component); + device->addRequirements(Requirements { + Requirement("name", true), + Requirement("uuid", true), + }); + + auto root = make_shared(Requirements {Requirement("Device", ENTITY, device)}); + + auto doc = string { + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n"}; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(root, doc, errors); + ASSERT_EQ(0, errors.size()); + + boost::uuids::detail::sha1 sha1; + unordered_map idMap; + + entity->createUniqueId(idMap, sha1); + + entity::XmlPrinter printer(false); + printer.print(*m_writer, entity, {}); + + ASSERT_EQ(R"( + + + + + + + + + +)", + m_writer->getContent()); + + m_writer = make_unique(true); + entity::XmlPrinter printer2(true); + printer2.print(*m_writer, entity, {}); + + ASSERT_EQ(R"( + + + + + + + + + +)", + m_writer->getContent()); +} + class EntityPrinterNamespaceTest : public EntityPrinterTest { protected: diff --git a/test/file_asset_test.cpp b/test/file_asset_test.cpp index 5a8c0aba..a2d57c93 100644 --- a/test/file_asset_test.cpp +++ b/test/file_asset_test.cpp @@ -83,7 +83,7 @@ TEST_F(FileAssetTest, TestMinmalArchetype) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "1.7", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); auto asset = dynamic_cast(entity.get()); @@ -141,7 +141,7 @@ TEST_F(FileAssetTest, TestMinmalFile) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "1.7", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); auto asset = dynamic_cast(entity.get()); diff --git a/test/json_printer_asset_test.cpp b/test/json_printer_asset_test.cpp index 3ae3dfd8..b58be6e8 100644 --- a/test/json_printer_asset_test.cpp +++ b/test/json_printer_asset_test.cpp @@ -76,7 +76,7 @@ class JsonPrinterAssetTest : public testing::Test AssetPtr parseAsset(const std::string &xml, entity::ErrorList &errors) { - auto entity = m_parser->parse(Asset::getRoot(), xml, "1.7", errors); + auto entity = m_parser->parse(Asset::getRoot(), xml, errors); AssetPtr asset; for (auto &error : errors) { diff --git a/test/json_printer_test.cpp b/test/json_printer_test.cpp index f86a78ea..8aa813e8 100644 --- a/test/json_printer_test.cpp +++ b/test/json_printer_test.cpp @@ -167,7 +167,7 @@ TEST_F(JsonPrinterTest, Header) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(root, doc, "1.7", errors); + auto entity = parser.parse(root, doc, errors); ASSERT_EQ(0, errors.size()); entity::JsonEntityPrinter jprinter(1, true); @@ -190,7 +190,7 @@ TEST_F(JsonPrinterTest, Devices) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(root, doc, "1.7", errors); + auto entity = parser.parse(root, doc, errors); ASSERT_EQ(0, errors.size()); entity::JsonEntityPrinter jprinter(1, true); @@ -212,7 +212,7 @@ TEST_F(JsonPrinterTest, Components) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(root, doc, "1.7", errors); + auto entity = parser.parse(root, doc, errors); ASSERT_EQ(0, errors.size()); entity::JsonEntityPrinter jprinter(1, true); @@ -243,7 +243,7 @@ TEST_F(JsonPrinterTest, TopLevelDataItems) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(root, doc, "1.7", errors); + auto entity = parser.parse(root, doc, errors); ASSERT_EQ(0, errors.size()); entity::JsonEntityPrinter jprinter(1, true); @@ -265,7 +265,7 @@ TEST_F(JsonPrinterTest, data_items_using_version_2) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(root, doc, "1.7", errors); + auto entity = parser.parse(root, doc, errors); ASSERT_EQ(0, errors.size()); entity::JsonEntityPrinter jprinter(2, true); @@ -302,7 +302,7 @@ TEST_F(JsonPrinterTest, ElementListWithProperty) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(root, doc, "1.7", errors); + auto entity = parser.parse(root, doc, errors); ASSERT_EQ(0, errors.size()); entity::JsonEntityPrinter jprinter(1, true); @@ -340,7 +340,7 @@ TEST_F(JsonPrinterTest, elements_with_property_list_version_2) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(root, doc, "1.7", errors); + auto entity = parser.parse(root, doc, errors); ASSERT_EQ(0, errors.size()); entity::JsonEntityPrinter jprinter(2, true); @@ -354,3 +354,177 @@ TEST_F(JsonPrinterTest, elements_with_property_list_version_2) ASSERT_EQ("2", jdoc.at("/Root/CuttingItems/list/CuttingItem/1/itemId"_json_pointer).get()); } + +TEST_F(JsonPrinterTest, should_honor_include_hidden_parameter) +{ + auto root = createFileArchetypeFactory(); + auto doc = deviceModel(); + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(root, doc, errors); + ASSERT_EQ(0, errors.size()); + + boost::uuids::detail::sha1 sha1; + unordered_map idMap; + + entity->createUniqueId(idMap, sha1); + + entity::JsonEntityPrinter jprinter(1, true, false); + auto jdoc = jprinter.print(entity); + + ASSERT_EQ(R"({ + "MTConnectDevices": { + "Devices": [ + { + "Device": { + "Components": [ + { + "Systems": { + "Components": [ + { + "Electric": { + "id": "Pm2JhGKEeAYzVA8c" + } + }, + { + "Heating": { + "id": "culKrBObwYWb6x0g" + } + } + ], + "Description": { + "value": "Hey Will", + "model": "abc" + }, + "id": "_cNZEyq5kGkgppmh" + } + } + ], + "DataItems": [ + { + "DataItem": { + "category": "EVENT", + "id": "FFZeJQRwQvAdUJX4", + "name": "avail", + "type": "AVAILABILITY" + } + }, + { + "DataItem": { + "category": "EVENT", + "id": "T0qItk3igtyip1XX", + "type": "ASSET_CHANGED" + } + }, + { + "DataItem": { + "category": "EVENT", + "id": "LWOt9yZtpFPWjL7v", + "type": "ASSET_REMOVED" + } + } + ], + "id": "DFYX7ls4d4to2Lhb", + "name": "foo", + "uuid": "xxx" + } + } + ], + "Header": { + "assetBufferSize": 8096, + "assetCount": 60, + "bufferSize": 131072, + "creationTime": "2021-01-07T18:34:15Z", + "deviceModelChangeTime": "2021-01-07T18:34:15Z", + "instanceId": 1609418103, + "sender": "DMZ-MTCNCT", + "version": "1.6.0.6" + } + } +})", + jdoc); + + entity::JsonEntityPrinter jprinter2(1, true, true); + jdoc = jprinter2.print(entity); + + ASSERT_EQ(R"({ + "MTConnectDevices": { + "Devices": [ + { + "Device": { + "Components": [ + { + "Systems": { + "Components": [ + { + "Electric": { + "id": "Pm2JhGKEeAYzVA8c", + "originalId": "e1" + } + }, + { + "Heating": { + "id": "culKrBObwYWb6x0g", + "originalId": "h1" + } + } + ], + "Description": { + "value": "Hey Will", + "model": "abc" + }, + "id": "_cNZEyq5kGkgppmh", + "originalId": "s1" + } + } + ], + "DataItems": [ + { + "DataItem": { + "category": "EVENT", + "id": "FFZeJQRwQvAdUJX4", + "name": "avail", + "originalId": "avail", + "type": "AVAILABILITY" + } + }, + { + "DataItem": { + "category": "EVENT", + "id": "T0qItk3igtyip1XX", + "originalId": "d1_asset_chg", + "type": "ASSET_CHANGED" + } + }, + { + "DataItem": { + "category": "EVENT", + "id": "LWOt9yZtpFPWjL7v", + "originalId": "d1_asset_rem", + "type": "ASSET_REMOVED" + } + } + ], + "id": "DFYX7ls4d4to2Lhb", + "name": "foo", + "originalId": "d1", + "uuid": "xxx" + } + } + ], + "Header": { + "assetBufferSize": 8096, + "assetCount": 60, + "bufferSize": 131072, + "creationTime": "2021-01-07T18:34:15Z", + "deviceModelChangeTime": "2021-01-07T18:34:15Z", + "instanceId": 1609418103, + "sender": "DMZ-MTCNCT", + "version": "1.6.0.6" + } + } +})", + jdoc); +} diff --git a/test/mqtt_isolated_test.cpp b/test/mqtt_isolated_test.cpp index b5db41c4..3bfaf993 100644 --- a/test/mqtt_isolated_test.cpp +++ b/test/mqtt_isolated_test.cpp @@ -220,7 +220,7 @@ TEST_F(MqttIsolatedUnitTest, mqtt_tcp_client_should_receive_loopback_publication std::uint16_t pid_sub1; - auto client = mqtt::make_async_client(m_agentTestHelper->m_ioContext, "localhost", m_port); + auto client = mqtt::make_async_client(m_agentTestHelper->m_ioContext.get(), "localhost", m_port); client->set_client_id("cliendId1"); client->set_clean_session(true); @@ -320,7 +320,8 @@ TEST_F(MqttIsolatedUnitTest, mqtt_tcp_client_should_receive_loopback_publication client->async_connect(); - m_agentTestHelper->m_ioContext.run(); + m_agentTestHelper->m_ioContext.removeGuard(); + m_agentTestHelper->m_ioContext.run_for(2s); ASSERT_TRUE(received); } @@ -446,7 +447,7 @@ TEST_F(MqttIsolatedUnitTest, mqtt_tcp_client_authentication) std::uint16_t pid_sub1; - auto client = mqtt::make_async_client(m_agentTestHelper->m_ioContext, "localhost", m_port); + auto client = mqtt::make_async_client(m_agentTestHelper->m_ioContext.get(), "localhost", m_port); client->set_client_id("cliendId1"); client->set_clean_session(true); @@ -545,7 +546,8 @@ TEST_F(MqttIsolatedUnitTest, mqtt_tcp_client_authentication) client->async_connect(); - m_agentTestHelper->m_ioContext.run(); + m_agentTestHelper->m_ioContext.removeGuard(); + m_agentTestHelper->m_ioContext.run_for(2s); ASSERT_TRUE(received); } diff --git a/test/qif_document_test.cpp b/test/qif_document_test.cpp index 84af60bb..8955bd71 100644 --- a/test/qif_document_test.cpp +++ b/test/qif_document_test.cpp @@ -117,7 +117,7 @@ TEST_F(QIFDocumentTest, minimal_qif_definition) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "2.0", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); auto asset = dynamic_cast(entity.get()); @@ -183,7 +183,7 @@ TEST_F(QIFDocumentTest, test_qif_xml_round_trip) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "2.0", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); auto asset = dynamic_cast(entity.get()); @@ -227,7 +227,7 @@ TEST_F(QIFDocumentTest, should_generate_json) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "2.0", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); auto asset = dynamic_cast(entity.get()); diff --git a/test/raw_material_test.cpp b/test/raw_material_test.cpp index d6c600fe..558dd262 100644 --- a/test/raw_material_test.cpp +++ b/test/raw_material_test.cpp @@ -101,7 +101,7 @@ TEST_F(RawMaterialTest, minimal_raw_material_definition) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "2.0", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); auto asset = dynamic_cast(entity.get()); @@ -143,7 +143,7 @@ TEST_F(RawMaterialTest, should_parse_raw_material_and_material) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "2.0", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); auto asset = dynamic_cast(entity.get()); @@ -194,7 +194,7 @@ TEST_F(RawMaterialTest, should_round_trip_xml) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "2.0", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); entity::XmlPrinter printer; @@ -233,7 +233,7 @@ TEST_F(RawMaterialTest, should_generate_json) ErrorList errors; entity::XmlParser parser; - auto entity = parser.parse(Asset::getRoot(), doc, "2.0", errors); + auto entity = parser.parse(Asset::getRoot(), doc, errors); ASSERT_EQ(0, errors.size()); entity::JsonEntityPrinter jsonPrinter(1, true);