icinga2/lib/base/json.cpp
Julian Brost 30f7d74c7c JsonEncoder: wrap writer for flushing
This commit introduces intruduces a small helper class that wraps any writer
and provides a flush operation that performs the corresponding action if the
writer is an AsyncJsonWriter and does nothing otherwise.
2025-07-11 13:33:02 +02:00

514 lines
15 KiB
C++

/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
#include "base/json.hpp"
#include "base/debug.hpp"
#include "base/dictionary.hpp"
#include "base/namespace.hpp"
#include "base/objectlock.hpp"
#include "base/utility.hpp"
#include <boost/numeric/conversion/cast.hpp>
#include <stack>
#include <utility>
#include <vector>
using namespace icinga;
JsonEncoder::JsonEncoder(std::string& output, bool prettify)
: JsonEncoder{nlohmann::detail::output_adapter<char>(output), prettify}
{
}
JsonEncoder::JsonEncoder(std::basic_ostream<char>& stream, bool prettify)
: JsonEncoder{nlohmann::detail::output_adapter<char>(stream), prettify}
{
}
JsonEncoder::JsonEncoder(nlohmann::detail::output_adapter_t<char> w, bool prettify)
: m_Flusher{w}, m_Pretty(prettify), m_Writer(std::move(w))
{
}
/**
* Encodes a single value into JSON and writes it to the underlying output stream.
*
* This method is the main entry point for encoding JSON data. It takes a value of any type that can
* be represented by our @c Value class recursively and encodes it into JSON in an efficient manner.
* If prettifying is enabled, the JSON output will be formatted with indentation and newlines for better
* readability, and the final JSON will also be terminated by a newline character.
*
* @note If the used output adapter performs asynchronous I/O operations (it's derived from @c AsyncJsonWriter),
* please provide a @c boost::asio::yield_context object to allow the encoder to flush the output stream in a
* safe manner. The encoder will try to regularly give the output stream a chance to flush its data when it is
* safe to do so, but for this to work, there must be a valid yield context provided. Otherwise, the encoder
* will not attempt to flush the output stream at all, which may lead to huge memory consumption when encoding
* large JSON objects or arrays.
*
* @param value The value to be JSON serialized.
* @param yc The optional yield context for asynchronous operations. If provided, it allows the encoder
* to flush the output stream safely when it has not acquired any object lock on the parent containers.
*/
void JsonEncoder::Encode(const Value& value, boost::asio::yield_context* yc)
{
switch (value.GetType()) {
case ValueEmpty:
Write("null");
break;
case ValueBoolean:
Write(value.ToBool() ? "true" : "false");
break;
case ValueString:
EncodeNlohmannJson(value.Get<String>());
break;
case ValueNumber:
EncodeNumber(value.Get<double>());
break;
case ValueObject: {
const auto& obj = value.Get<Object::Ptr>();
const auto& type = obj->GetReflectionType();
if (type == Namespace::TypeInstance) {
static constexpr auto extractor = [](const NamespaceValue& v) -> const Value& { return v.Val; };
EncodeObject(static_pointer_cast<Namespace>(obj), extractor, yc);
} else if (type == Dictionary::TypeInstance) {
static constexpr auto extractor = [](const Value& v) -> const Value& { return v; };
EncodeObject(static_pointer_cast<Dictionary>(obj), extractor, yc);
} else if (type == Array::TypeInstance) {
EncodeArray(static_pointer_cast<Array>(obj), yc);
} else if (auto gen(dynamic_pointer_cast<ValueGenerator>(obj)); gen) {
EncodeValueGenerator(gen, yc);
} else {
// Some other non-serializable object type!
EncodeNlohmannJson(obj->ToString());
}
break;
}
default:
VERIFY(!"Invalid variant type.");
}
// If we are at the top level of the JSON object and prettifying is enabled, we need to end
// the JSON with a newline character to ensure that the output is properly formatted.
if (m_Indent == 0 && m_Pretty) {
Write("\n");
}
}
/**
* Encodes an Array object into JSON and writes it to the output stream.
*
* @param array The Array object to be serialized into JSON.
* @param yc The optional yield context for asynchronous operations. If provided, it allows the encoder
* to flush the output stream safely when it has not acquired any object lock.
*/
void JsonEncoder::EncodeArray(const Array::Ptr& array, boost::asio::yield_context* yc)
{
BeginContainer('[');
auto olock = array->LockIfRequired();
if (olock) {
yc = nullptr; // We've acquired an object lock, never allow asynchronous operations.
}
bool isEmpty = true;
for (const auto& item : array) {
WriteSeparatorAndIndentStrIfNeeded(!isEmpty);
isEmpty = false;
Encode(item, yc);
m_Flusher.FlushIfSafe(yc);
}
EndContainer(']', isEmpty);
}
/**
* Encodes a ValueGenerator object into JSON and writes it to the output stream.
*
* This will iterate through the generator, encoding each value it produces until it is exhausted.
*
* @param generator The ValueGenerator object to be serialized into JSON.
* @param yc The optional yield context for asynchronous operations. If provided, it allows the encoder
* to flush the output stream safely when it has not acquired any object lock on the parent containers.
*/
void JsonEncoder::EncodeValueGenerator(const ValueGenerator::Ptr& generator, boost::asio::yield_context* yc)
{
BeginContainer('[');
bool isEmpty = true;
while (auto result = generator->Next()) {
WriteSeparatorAndIndentStrIfNeeded(!isEmpty);
isEmpty = false;
Encode(*result, yc);
m_Flusher.FlushIfSafe(yc);
}
EndContainer(']', isEmpty);
}
/**
* Encodes an Icinga 2 object (Namespace or Dictionary) into JSON and writes it to @c m_Writer.
*
* @tparam Iterable Type of the container (Namespace or Dictionary).
* @tparam ValExtractor Type of the value extractor function used to extract values from the container's iterator.
*
* @param container The container to JSON serialize.
* @param extractor The value extractor function used to extract values from the container's iterator.
* @param yc The optional yield context for asynchronous operations. It will only be set when the encoder
* has not acquired any object lock on the parent containers, allowing safe asynchronous operations.
*/
template<typename Iterable, typename ValExtractor>
void JsonEncoder::EncodeObject(const Iterable& container, const ValExtractor& extractor, boost::asio::yield_context* yc)
{
static_assert(std::is_same_v<Iterable, Namespace::Ptr> || std::is_same_v<Iterable, Dictionary::Ptr>,
"Container must be a Namespace or Dictionary");
BeginContainer('{');
auto olock = container->LockIfRequired();
if (olock) {
yc = nullptr; // We've acquired an object lock, never allow asynchronous operations.
}
bool isEmpty = true;
for (const auto& [key, val] : container) {
WriteSeparatorAndIndentStrIfNeeded(!isEmpty);
isEmpty = false;
EncodeNlohmannJson(key);
Write(m_Pretty ? ": " : ":");
Encode(extractor(val), yc);
m_Flusher.FlushIfSafe(yc);
}
EndContainer('}', isEmpty);
}
/**
* Dumps a nlohmann::json object to the output stream using the serializer.
*
* This function uses the @c nlohmann::detail::serializer to dump the provided @c nlohmann::json
* object to the output stream managed by the @c JsonEncoder. Strings will be properly escaped, and
* if any invalid UTF-8 sequences are encountered, it will replace them with the Unicode replacement
* character (U+FFFD).
*
* @param json The nlohmann::json object to encode.
*/
void JsonEncoder::EncodeNlohmannJson(const nlohmann::json& json) const
{
nlohmann::detail::serializer<nlohmann::json> s(m_Writer, ' ', nlohmann::json::error_handler_t::replace);
s.dump(json, m_Pretty, true, 0, 0);
}
/**
* Encodes a double value into JSON format and writes it to the output stream.
*
* This function checks if the double value can be safely cast to an integer or unsigned integer type
* without loss of precision. If it can, it will serialize it as such; otherwise, it will serialize
* it as a double. This is particularly useful for ensuring that values like 0.0 are serialized as 0,
* which can be important for compatibility with clients like Icinga DB that expect integers in such cases.
*
* @param value The double value to encode as JSON.
*/
void JsonEncoder::EncodeNumber(double value) const
{
try {
if (value < 0) {
if (auto ll(boost::numeric_cast<nlohmann::json::number_integer_t>(value)); ll == value) {
EncodeNlohmannJson(ll);
return;
}
} else if (auto ull(boost::numeric_cast<nlohmann::json::number_unsigned_t>(value)); ull == value) {
EncodeNlohmannJson(ull);
return;
}
// If we reach this point, the value cannot be safely cast to a signed or unsigned integer
// type because it would otherwise lose its precision. If the value was just too large to fit
// into the above types, then boost will throw an exception and end up in the below catch block.
// So, in either case, serialize the number as-is without any casting.
} catch (const boost::bad_numeric_cast&) {}
EncodeNlohmannJson(value);
}
/**
* Writes a string to the underlying output stream.
*
* This function writes the provided string view directly to the output stream without any additional formatting.
*
* @param sv The string view to write to the output stream.
*/
void JsonEncoder::Write(const std::string_view& sv) const
{
m_Writer->write_characters(sv.data(), sv.size());
}
/**
* Begins a JSON container (object or array) by writing the opening character and adjusting the
* indentation level if pretty-printing is enabled.
*
* @param openChar The character that opens the container (either '{' for objects or '[' for arrays).
*/
void JsonEncoder::BeginContainer(char openChar)
{
if (m_Pretty) {
m_Indent += m_IndentSize;
if (m_IndentStr.size() < m_Indent) {
m_IndentStr.resize(m_IndentStr.size() * 2, ' ');
}
}
m_Writer->write_character(openChar);
}
/**
* Ends a JSON container (object or array) by writing the closing character and adjusting the
* indentation level if pretty-printing is enabled.
*
* @param closeChar The character that closes the container (either '}' for objects or ']' for arrays).
* @param isContainerEmpty Whether the container is empty, used to determine if a newline should be written.
*/
void JsonEncoder::EndContainer(char closeChar, bool isContainerEmpty)
{
if (m_Pretty) {
ASSERT(m_Indent >= m_IndentSize); // Ensure we don't underflow the indent size.
m_Indent -= m_IndentSize;
if (!isContainerEmpty) {
Write("\n");
m_Writer->write_characters(m_IndentStr.c_str(), m_Indent);
}
}
m_Writer->write_character(closeChar);
}
/**
* Writes a separator (comma) and an indentation string if pretty-printing is enabled.
*
* This function is used to separate items in a JSON array or object and to maintain the correct indentation level.
*
* @param emitComma Whether to emit a comma. This is typically true for all but the first item in a container.
*/
void JsonEncoder::WriteSeparatorAndIndentStrIfNeeded(bool emitComma) const
{
if (emitComma) {
Write(",");
}
if (m_Pretty) {
Write("\n");
m_Writer->write_characters(m_IndentStr.c_str(), m_Indent);
}
}
/**
* Wraps any writer of type @c nlohmann::detail::output_adapter_t<char> into a Flusher
*
* @param w The writer to wrap.
*/
JsonEncoder::Flusher::Flusher(const nlohmann::detail::output_adapter_t<char>& w)
: m_AsyncWriter(dynamic_cast<AsyncJsonWriter*>(w.get()))
{
}
/**
* Flushes the underlying writer if it supports that operation and is safe to do so.
*
* Safe flushing means that it only performs the flush operation if the @c JsonEncoder has not acquired
* any object lock so far. This is to ensure that the stream can safely perform asynchronous operations
* without risking undefined behaviour due to coroutines being suspended while the stream is being flushed.
*
* When the @c yc parameter is provided, it indicates that it's safe to perform asynchronous operations,
* and the function will attempt to flush if the writer is an instance of @c AsyncJsonWriter. Otherwise,
* this function does nothing.
*
* @param yc The yield context to use for asynchronous operations.
*/
void JsonEncoder::Flusher::FlushIfSafe(boost::asio::yield_context* yc) const
{
if (yc && m_AsyncWriter) {
m_AsyncWriter->Flush(*yc);
}
}
class JsonSax : public nlohmann::json_sax<nlohmann::json>
{
public:
bool null() override;
bool boolean(bool val) override;
bool number_integer(number_integer_t val) override;
bool number_unsigned(number_unsigned_t val) override;
bool number_float(number_float_t val, const string_t& s) override;
bool string(string_t& val) override;
bool binary(binary_t& val) override;
bool start_object(std::size_t elements) override;
bool key(string_t& val) override;
bool end_object() override;
bool start_array(std::size_t elements) override;
bool end_array() override;
bool parse_error(std::size_t position, const std::string& last_token, const nlohmann::detail::exception& ex) override;
Value GetResult();
private:
Value m_Root;
std::stack<std::pair<Dictionary*, Array*>> m_CurrentSubtree;
String m_CurrentKey;
void FillCurrentTarget(Value value);
};
String icinga::JsonEncode(const Value& value, bool prettify)
{
std::string output;
JsonEncoder encoder(output, prettify);
encoder.Encode(value);
return String(std::move(output));
}
/**
* Serializes an Icinga Value into a JSON object and writes it to the given output stream.
*
* @param value The value to be JSON serialized.
* @param os The output stream to write the JSON data to.
* @param prettify Whether to pretty print the serialized JSON.
*/
void icinga::JsonEncode(const Value& value, std::ostream& os, bool prettify)
{
JsonEncoder encoder(os, prettify);
encoder.Encode(value);
}
Value icinga::JsonDecode(const String& data)
{
String sanitized (Utility::ValidateUTF8(data));
JsonSax stateMachine;
nlohmann::json::sax_parse(sanitized.Begin(), sanitized.End(), &stateMachine);
return stateMachine.GetResult();
}
inline
bool JsonSax::null()
{
FillCurrentTarget(Value());
return true;
}
inline
bool JsonSax::boolean(bool val)
{
FillCurrentTarget(val);
return true;
}
inline
bool JsonSax::number_integer(JsonSax::number_integer_t val)
{
FillCurrentTarget((double)val);
return true;
}
inline
bool JsonSax::number_unsigned(JsonSax::number_unsigned_t val)
{
FillCurrentTarget((double)val);
return true;
}
inline
bool JsonSax::number_float(JsonSax::number_float_t val, const JsonSax::string_t&)
{
FillCurrentTarget((double)val);
return true;
}
inline
bool JsonSax::string(JsonSax::string_t& val)
{
FillCurrentTarget(String(std::move(val)));
return true;
}
inline
bool JsonSax::binary(JsonSax::binary_t& val)
{
FillCurrentTarget(String(val.begin(), val.end()));
return true;
}
inline
bool JsonSax::start_object(std::size_t)
{
auto object (new Dictionary());
FillCurrentTarget(object);
m_CurrentSubtree.push({object, nullptr});
return true;
}
inline
bool JsonSax::key(JsonSax::string_t& val)
{
m_CurrentKey = String(std::move(val));
return true;
}
inline
bool JsonSax::end_object()
{
m_CurrentSubtree.pop();
m_CurrentKey = String();
return true;
}
inline
bool JsonSax::start_array(std::size_t)
{
auto array (new Array());
FillCurrentTarget(array);
m_CurrentSubtree.push({nullptr, array});
return true;
}
inline
bool JsonSax::end_array()
{
m_CurrentSubtree.pop();
return true;
}
inline
bool JsonSax::parse_error(std::size_t, const std::string&, const nlohmann::detail::exception& ex)
{
throw std::invalid_argument(ex.what());
}
inline
Value JsonSax::GetResult()
{
return m_Root;
}
inline
void JsonSax::FillCurrentTarget(Value value)
{
if (m_CurrentSubtree.empty()) {
m_Root = value;
} else {
auto& node (m_CurrentSubtree.top());
if (node.first) {
node.first->Set(m_CurrentKey, value);
} else {
node.second->Add(value);
}
}
}