Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/mtconnect/configuration/agent_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1248,7 +1248,8 @@ namespace mtconnect::configuration {
}
catch (exception &e)
{
LOG(info) << "Cannot load plugin " << name << " from " << path << " Reason: " << e.what();
LOG(debug) << "Plugin " << name << " from " << path << " not found, Reason: " << e.what()
<< ", trying next path if available.";
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/mtconnect/configuration/agent_config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ namespace mtconnect {
else
{
LOG(debug) << "Cannot find file '" << file << "' "
<< " in path " << path;
<< " in path " << path << ", continuing...";
}
}

Expand All @@ -312,7 +312,7 @@ namespace mtconnect {
if (!ec)
paths.emplace_back(con);
else
LOG(debug) << "Cannot file path: " << path << ", " << ec.message();
LOG(debug) << "Cannot find path: " << path << ", " << ec.message() << ", skipping...";
}

void addPathFront(std::list<std::filesystem::path> &paths, std::filesystem::path path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ namespace mtconnect {
Requirement("Rotation", ValueType::VECTOR, 3, false),
Requirement("TranslationDataSet", ValueType::DATA_SET, false),
Requirement("RotationDataSet", ValueType::DATA_SET, false)});
transformation->setOrder({"Translation", "TranslationDataSet", "Rotation", "RotationDataSet"});
transformation->setOrder(
{"Translation", "TranslationDataSet", "Rotation", "RotationDataSet"});

auto coordinateSystem = make_shared<Factory>(Requirements {
Requirement("id", true), Requirement("name", false), Requirement("nativeName", false),
Expand Down
3 changes: 2 additions & 1 deletion src/mtconnect/device_model/configuration/motion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ namespace mtconnect {
Requirement("Rotation", ValueType::VECTOR, 3, false),
Requirement("TranslationDataSet", ValueType::DATA_SET, false),
Requirement("RotationDataSet", ValueType::DATA_SET, false)});
transformation->setOrder({"Translation", "TranslationDataSet", "Rotation", "RotationDataSet"});
transformation->setOrder(
{"Translation", "TranslationDataSet", "Rotation", "RotationDataSet"});

static auto motion = make_shared<Factory>(Requirements {
Requirement("id", true), Requirement("parentIdRef", false),
Expand Down
11 changes: 6 additions & 5 deletions src/mtconnect/device_model/configuration/solid_model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ namespace mtconnect {
if (!solidModel)
{
static auto transformation = make_shared<Factory>(
Requirements {Requirement("Translation", ValueType::VECTOR, 3, false),
Requirement("Rotation", ValueType::VECTOR, 3, false),
Requirement("TranslationDataSet", ValueType::DATA_SET, false),
Requirement("RotationDataSet", ValueType::DATA_SET, false)});
transformation->setOrder({"Translation", "TranslationDataSet", "Rotation", "RotationDataSet"});
Requirements {Requirement("Translation", ValueType::VECTOR, 3, false),
Requirement("Rotation", ValueType::VECTOR, 3, false),
Requirement("TranslationDataSet", ValueType::DATA_SET, false),
Requirement("RotationDataSet", ValueType::DATA_SET, false)});
transformation->setOrder(
{"Translation", "TranslationDataSet", "Rotation", "RotationDataSet"});

solidModel = make_shared<Factory>(
Requirements {{"id", true},
Expand Down
2 changes: 1 addition & 1 deletion src/mtconnect/entity/xml_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ namespace mtconnect::entity {
}
else
{
LOG(warning) << "Unexpected element: " << nodeQName(child);
// LOG(warning) << "Unexpected element: " << nodeQName(child);
errors.emplace_back(
new EntityError("Invalid element '" + nodeQName(child) + "'", qname));
}
Expand Down
16 changes: 15 additions & 1 deletion src/mtconnect/parser/xml_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -202,13 +202,27 @@ namespace mtconnect::parser {
{
auto device =
entity::XmlParser::parseXmlNode(Device::getRoot(), nodeset->nodeTab[i], errors);

if (device)
{
deviceList.emplace_back(dynamic_pointer_cast<Device>(device));
}
else
{
LOG(error) << "Failed to parse device, skipping";
}

if (!errors.empty())
{
for (auto &e : errors)
LOG(warning) << "Error parsing device: " << e->what();
{
if (device)
LOG(warning) << "When loading device " << device->get<string>("name")
<< ", A problem was skipped: "
<< e->what();
else
LOG(error) << "Failed to load device: " << e->what();
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/mtconnect/pipeline/shdr_token_mapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,8 @@ namespace mtconnect {
}
catch (entity::PropertyError &e)
{
LOG(warning) << "Cannot convert value for data item id '" << dataItem->getId()
<< "': " << *token << " - " << e.what();
LOG(debug) << "Cannot convert value for data item id '" << dataItem->getId()
<< "': " << *token << " - " << e.what();
if (schemaVersion >= SCHEMA_VERSION(2, 5) && validation)
{
props.insert_or_assign("quality", "INVALID"s);
Expand Down
3 changes: 3 additions & 0 deletions src/mtconnect/sink/rest_sink/parameter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ namespace mtconnect::sink::rest_sink {
Parameter(const std::string &n, ParameterType t = STRING, UrlPart p = PATH)
: m_name(n), m_type(t), m_part(p)
{}
Parameter(const std::string_view &n, ParameterType t = STRING, UrlPart p = PATH)
: m_name(n), m_type(t), m_part(p)
{}
Parameter(const Parameter &o) = default;

/// @brief to support std::set interface
Expand Down
4 changes: 4 additions & 0 deletions src/mtconnect/sink/rest_sink/response.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <boost/beast/http/status.hpp>

#include <filesystem>
#include <list>
#include <unordered_map>

#include "cached_file.hpp"
Expand Down Expand Up @@ -64,6 +65,9 @@ namespace mtconnect {
std::optional<std::string> m_requestId; ///< Request id from websocket sub

CachedFilePtr m_file; ///< Cached file if a file is being returned

/// @brief Additional per-response header fields (e.g. for CORS preflight)
std::list<std::pair<std::string, std::string>> m_fields;
};

using ResponsePtr = std::unique_ptr<Response>;
Expand Down
72 changes: 64 additions & 8 deletions src/mtconnect/sink/rest_sink/routing.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@

#pragma once

#include <boost/algorithm/string.hpp>
#include <boost/beast/http/verb.hpp>

#include <iostream>
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#include <iostream> appears unused in this header. Consider removing it to avoid unnecessary recompiles and header bloat.

Suggested change
#include <iostream>

Copilot uses AI. Check for mistakes.
#include <list>
#include <optional>
#include <regex>
Expand Down Expand Up @@ -84,7 +86,8 @@ namespace mtconnect::sink::rest_sink {
m_pattern(pattern),
m_command(request),
m_function(function),
m_swagger(swagger)
m_swagger(swagger),
m_catchAll(true)
{}

/// @brief Added summary and description to the routing
Expand Down Expand Up @@ -295,6 +298,15 @@ namespace mtconnect::sink::rest_sink {
return true;
}

/// @brief check if the routing's path pattern matches a given path (ignoring verb)
/// @param[in] path the request path to test
/// @return `true` if the path matches this routing's pattern
bool matchesPath(const std::string &path) const
{
std::smatch m;
return std::regex_match(path, m, m_pattern);
}

/// @brief check if this is related to a swagger API
/// @returns `true` if related to swagger
auto isSwagger() const { return m_swagger; }
Expand All @@ -304,6 +316,10 @@ namespace mtconnect::sink::rest_sink {
/// @brief Get the routing `verb`
const auto &getVerb() const { return m_verb; }

/// @brief Check if the route is a catch-all (every path segment is a parameter)
/// @returns `true` if all path segments are parameters (e.g. `/{device}`)
auto isCatchAll() const { return m_catchAll; }

/// @brief Get the optional command associated with the routing
/// @returns optional routing
const auto &getCommand() const { return m_command; }
Expand All @@ -319,21 +335,60 @@ namespace mtconnect::sink::rest_sink {
protected:
void pathParameters(std::string s)
{
std::regex reg("\\{([^}]+)\\}");
std::smatch match;
std::stringstream pat;

while (regex_search(s, match, reg))
using namespace boost::algorithm;
using SplitList = std::list<boost::iterator_range<std::string::iterator>>;

SplitList parts;
auto pos = s.find_first_not_of('/');
if (pos != std::string::npos)
{
pat << match.prefix() << "([^/]+)";
m_pathParameters.emplace_back(match[1]);
s = match.suffix().str();
auto range = boost::make_iterator_range(s.begin() + pos, s.end());
split(parts, range, [](char c) { return c == '/'; });
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pathParameters currently calls boost::algorithm::split(parts, range, [](char c) { return c == '/'; });, but Boost's split expects a finder (e.g., boost::is_any_of("/")), not a char -> bool predicate. As written this is likely to fail to compile; use boost::is_any_of("/") (and an appropriate container type, e.g. std::vector<iterator_range<...>>) for consistency with other code (see SessionImpl::parseQueries).

Suggested change
split(parts, range, [](char c) { return c == '/'; });
split(parts, range, boost::is_any_of("/"));

Copilot uses AI. Check for mistakes.
}

bool hasLiteral = false;
for (auto &p : parts)
{
auto start = p.begin();
auto end = p.end();

auto openBrace = std::find(start, end, '{');
decltype(openBrace) closeBrace {end};
if (openBrace != end && std::distance(openBrace, end) > 2)
closeBrace = std::find(openBrace + 1, end, '}');

pat << "/";
if (openBrace != end && closeBrace != end)
{
if (openBrace > start)
{
pat << std::string_view(start, openBrace);
hasLiteral = true;
}
std::string_view param(openBrace + 1, closeBrace);
pat << "([^/]+)";
if (closeBrace + 1 < end)
{
pat << std::string_view(closeBrace + 1, end);
hasLiteral = true;
}
m_pathParameters.emplace_back(param);
Comment on lines +365 to +377
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::string_view(start, openBrace) / std::string_view(openBrace + 1, closeBrace) / etc. are constructed from iterators, but std::string_view does not have iterator-pair constructors. This will not compile; create the view from a pointer+length (e.g. &*it + std::distance) or append via pat.write() / std::string(start, openBrace) instead.

Copilot uses AI. Check for mistakes.
}
else
{
pat << std::string_view(start, end);
hasLiteral = true;
}
}
pat << s;
pat << "/?";

m_patternText = pat.str();
m_pattern = std::regex(m_patternText);

// A route is catch-all if it has parameters but no literal path segments
m_catchAll = !m_pathParameters.empty() && !hasLiteral;
}

void queryParameters(std::string s)
Expand Down Expand Up @@ -513,5 +568,6 @@ namespace mtconnect::sink::rest_sink {
std::optional<std::string> m_description;

bool m_swagger = false;
bool m_catchAll = false;
};
} // namespace mtconnect::sink::rest_sink
42 changes: 42 additions & 0 deletions src/mtconnect/sink/rest_sink/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@

#include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/case_conv.hpp>
#include <boost/algorithm/string/join.hpp>
#include <boost/asio.hpp>
#include <boost/asio/coroutine.hpp>
#include <boost/beast.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/version.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <boost/tokenizer.hpp>

#include <thread>
Expand Down Expand Up @@ -413,4 +415,44 @@ namespace mtconnect::sink::rest_sink {
// addRouting({boost::beast::http::verb::get, "/swagger.yaml", handler, true});
}

bool Server::handleOptionsRequest(SessionPtr session, const RequestPtr request)
{
using namespace boost;
using namespace adaptors;
set<http::verb> specificVerbs;
set<http::verb> catchAllVerbs;
for (const auto &r : m_routings)
{
if (!r.isSwagger() && r.matchesPath(request->m_path))
{
if (r.isCatchAll())
catchAllVerbs.insert(r.getVerb());
else
specificVerbs.insert(r.getVerb());
}
}

// If any specific route matched, use only those; otherwise fall back to catch-alls
auto &verbs = specificVerbs.empty() ? catchAllVerbs : specificVerbs;

// OPTIONS is always allowed
verbs.insert(http::verb::options);

// Build the Allow / Access-Control-Allow-Methods header value
string methods = algorithm::join(
verbs | transformed([](http::verb v) { return string(http::to_string(v)); }), ", ");

auto response = std::make_unique<Response>(status::no_content, "", "text/plain");
response->m_close = false;
response->m_fields.emplace_back("Allow", methods);
response->m_fields.emplace_back("Access-Control-Allow-Methods", methods);
response->m_fields.emplace_back("Access-Control-Allow-Headers",
"Content-Type, Accept, Accept-Encoding");
response->m_fields.emplace_back("Access-Control-Max-Age", "86400");

session->writeResponse(std::move(response));

return true;
}
Comment on lines +422 to +456
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleOptionsRequest always inserts OPTIONS and returns true, even when no routing matched request->m_path. That means an OPTIONS to an unknown path will return 204 instead of falling through to the existing 404 handling (and it contradicts the header comment that says it can return false). Consider returning false when both specificVerbs and catchAllVerbs are empty (before inserting OPTIONS), and only generating the Allow/ACAM headers when something actually matched.

Copilot uses AI. Check for mistakes.

} // namespace mtconnect::sink::rest_sink
14 changes: 14 additions & 0 deletions src/mtconnect/sink/rest_sink/server.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ namespace mtconnect::sink::rest_sink {
else
message = "Command failed: " + *request->m_command;
}
else if (request->m_verb == boost::beast::http::verb::options)
{
success = handleOptionsRequest(session, request);
}
else
{
for (auto &r : m_routings)
Expand Down Expand Up @@ -298,6 +302,16 @@ namespace mtconnect::sink::rest_sink {
const void renderSwaggerResponse(T &format);
/// @}

/// @name CORS Support
/// @{
///
/// @brief Handle OPTIONS request for CORS preflight requests
/// @param[in] session the client session
/// @param[in] request the incoming request
/// @return `true` if the request was handled, otherwise `false` and a 404 will be returned
bool handleOptionsRequest(SessionPtr session, const RequestPtr request);
/// @}

protected:
boost::asio::io_context &m_context;

Expand Down
9 changes: 7 additions & 2 deletions src/mtconnect/sink/rest_sink/session_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,9 @@ namespace mtconnect::sink::rest_sink {
auto &msg = m_parser->get();
const auto &remote = m_remote;

// Check for put, post, or delete
if (msg.method() != http::verb::get)
// Check for put, post, or delete (allow OPTIONS for CORS preflight)
if (msg.method() == http::verb::put || msg.method() == http::verb::post ||
msg.method() == http::verb::delete_)
{
if (!m_allowPuts)
{
Expand Down Expand Up @@ -376,6 +377,10 @@ namespace mtconnect::sink::rest_sink {
{
res->set(http::field::location, *response.m_location);
}
for (const auto &f : response.m_fields)
{
res->set(f.first, f.second);
}
}

template <class Derived>
Expand Down
Loading
Loading