This is an automated email from the ASF dual-hosted git repository.

pnoltes pushed a commit to branch feature/685-properties-json-serialization
in repository https://gitbox.apache.org/repos/asf/celix.git

commit 8e03a828be1f707df23b3fe77b62d491a3adf527
Author: Pepijn Noltes <pnol...@apache.org>
AuthorDate: Wed Apr 10 23:27:59 2024 +0200

    gh-685: Add support for a flat and nested flag for prop encoding
---
 libs/utils/gtest/src/CelixUtilsTestSuite.cc        |  32 ++
 .../utils/gtest/src/PropertiesEncodingTestSuite.cc | 435 ++++++++++++++-------
 libs/utils/include/celix_properties.h              |  81 ++--
 libs/utils/include/celix_utils.h                   |  56 +++
 libs/utils/src/properties_encoding.c               | 208 +++++++---
 5 files changed, 588 insertions(+), 224 deletions(-)

diff --git a/libs/utils/gtest/src/CelixUtilsTestSuite.cc 
b/libs/utils/gtest/src/CelixUtilsTestSuite.cc
index 26bef64b..95e143d4 100644
--- a/libs/utils/gtest/src/CelixUtilsTestSuite.cc
+++ b/libs/utils/gtest/src/CelixUtilsTestSuite.cc
@@ -315,6 +315,38 @@ TEST_F(UtilsTestSuite, WriteOrCreateStringTest) {
     celix_utils_freeStringIfNotEqual(buffer2, out2);
 }
 
+TEST_F(UtilsTestSuite, WriteOrCreateStringGuardTest) {
+    // Given a small buffer
+    char buffer[16];
+
+    {
+        // When writing a string that fits in the buffer
+        char* str = celix_utils_writeOrCreateString(buffer, sizeof(buffer), 
"abc");
+
+        // Then the str is equal to the buffer (in this case no malloc was 
needed)
+        EXPECT_EQ(buffer, str);
+
+        // And using celix_auto with a string guard
+        celix_auto(celix_utils_string_guard_t) guard = 
celix_utils_stringGuard_init(buffer, str);
+
+        // Then the guard will not free the string when going out of scope
+    }
+
+    {
+        // When writing a string that does not fit in the buffer
+        char* str = celix_utils_writeOrCreateString(
+            buffer, sizeof(buffer), 
"abc123def456ghi789jkl012mno345pqr678stu901vwx234yz");
+
+        // Then the str is not equal to the buffer (in this case a malloc was 
needed)
+        EXPECT_NE(buffer, str);
+
+        // And using celix_auto with a string guard
+        celix_auto(celix_utils_string_guard_t) guard = 
celix_utils_stringGuard_init(buffer, str);
+
+        // Then the guard will free the string when going out of scope
+    }
+}
+
 TEST_F(UtilsTestSuite, StrDupAndStrLenTest) {
     celix_autofree char* str = celix_utils_strdup("abc");
     ASSERT_NE(nullptr, str);
diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc 
b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc
index d346f5f6..84ed5d0a 100644
--- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc
+++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc
@@ -32,7 +32,7 @@ class PropertiesSerializationTestSuite : public 
::testing::Test {
     PropertiesSerializationTestSuite() { celix_err_resetErrors(); }
 };
 
-TEST_F(PropertiesSerializationTestSuite, EncodeEmptyPropertiesTest) {
+TEST_F(PropertiesSerializationTestSuite, SaveEmptyPropertiesTest) {
     //Given an empty properties object
     celix_autoptr(celix_properties_t) props = celix_properties_create();
 
@@ -41,8 +41,8 @@ TEST_F(PropertiesSerializationTestSuite, 
EncodeEmptyPropertiesTest) {
     size_t bufLen = 0;
     FILE* stream = open_memstream(&buf, &bufLen);
 
-    //When encoding the properties to the stream
-    auto status = celix_properties_encodeToStream(props, stream, 0);
+    //When saving the properties to the stream
+    auto status = celix_properties_saveToStream(props, stream, 0);
     ASSERT_EQ(CELIX_SUCCESS, status);
 
     //Then the stream contains an empty JSON object
@@ -50,7 +50,7 @@ TEST_F(PropertiesSerializationTestSuite, 
EncodeEmptyPropertiesTest) {
     EXPECT_STREQ("{}", buf);
 }
 
-TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithSingleValuesTest) 
{
+TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithSingleValuesTest) {
         //Given a properties object with single values
         celix_autoptr(celix_properties_t) props = celix_properties_create();
         celix_properties_set(props, "key1", "value1");
@@ -65,8 +65,8 @@ TEST_F(PropertiesSerializationTestSuite, 
EncodePropertiesWithSingleValuesTest) {
         size_t bufLen = 0;
         FILE* stream = open_memstream(&buf, &bufLen);
 
-        //When encoding the properties to the stream
-        auto status = celix_properties_encodeToStream(props, stream, 0);
+        //When saving the properties to the stream
+        auto status = celix_properties_saveToStream(props, stream, 0);
         ASSERT_EQ(CELIX_SUCCESS, status);
 
         //Then the stream contains the JSON representation snippets of the 
properties
@@ -85,7 +85,7 @@ TEST_F(PropertiesSerializationTestSuite, 
EncodePropertiesWithSingleValuesTest) {
         json_decref(root);
 }
 
-TEST_F(PropertiesSerializationTestSuite, 
EncodePropertiesWithNaNAndInfValuesTest) {
+TEST_F(PropertiesSerializationTestSuite, 
SavePropertiesWithNaNAndInfValuesTest) {
     //Given a NAN, INF and -INF value
     auto keys = {"NAN", "INF", "-INF"};
     for (const auto& key : keys) {
@@ -102,7 +102,7 @@ TEST_F(PropertiesSerializationTestSuite, 
EncodePropertiesWithNaNAndInfValuesTest
 
         //Then saving the properties to the stream fails, because JSON does 
not support NAN, INF and -INF
         celix_err_resetErrors();
-        auto status = celix_properties_encodeToStream(props, stream, 0);
+        auto status = celix_properties_saveToStream(props, stream, 0);
         EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
 
         //And an error msg is added to celix_err
@@ -111,7 +111,7 @@ TEST_F(PropertiesSerializationTestSuite, 
EncodePropertiesWithNaNAndInfValuesTest
 }
 
 
-TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithArrayListsTest) {
+TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsTest) {
     // Given a properties object with array list values
     celix_autoptr(celix_properties_t) props = celix_properties_create();
     celix_array_list_t* list1 = celix_arrayList_createStringArray();
@@ -141,7 +141,7 @@ TEST_F(PropertiesSerializationTestSuite, 
EncodePropertiesWithArrayListsTest) {
     FILE* stream = open_memstream(&buf, &bufLen);
 
     // When saving the properties to the stream
-    auto status = celix_properties_encodeToStream(props, stream, 0);
+    auto status = celix_properties_saveToStream(props, stream, 0);
     ASSERT_EQ(CELIX_SUCCESS, status);
 
     // Then the stream contains the JSON representation snippets of the 
properties
@@ -161,7 +161,7 @@ TEST_F(PropertiesSerializationTestSuite, 
EncodePropertiesWithArrayListsTest) {
 }
 
 
-TEST_F(PropertiesSerializationTestSuite, EncodeEmptyArrayTest) {
+TEST_F(PropertiesSerializationTestSuite, SaveEmptyArrayTest) {
     //Given a properties object with an empty array list of with el types 
string, long, double, bool, version
     celix_autoptr(celix_properties_t) props = celix_properties_create();
     celix_properties_assignArrayList(props, "key1", 
celix_arrayList_createStringArray());
@@ -171,21 +171,28 @@ TEST_F(PropertiesSerializationTestSuite, 
EncodeEmptyArrayTest) {
     celix_properties_assignArrayList(props, "key5", 
celix_arrayList_createVersionArray());
     EXPECT_EQ(5, celix_properties_size(props));
 
-    //And an in-memory stream
-    celix_autofree char* buf = nullptr;
-    size_t bufLen = 0;
-    FILE* stream = open_memstream(&buf, &bufLen);
+    //When saving the properties to a string
+    char* output = nullptr;
+    auto status = celix_properties_saveToString(props, 0, &output);
 
-    //When encoding the properties to the stream
-    auto status = celix_properties_encodeToStream(props, stream, 0);
+    //Then the save went ok
     ASSERT_EQ(CELIX_SUCCESS, status);
 
-    //Then the stream contains an empty JSON object, because empty arrays are 
treated as unset
-    fclose(stream);
-    EXPECT_STREQ("{}", buf);
+    //And the output contains an empty JSON object, because empty arrays are 
treated as unset
+    EXPECT_STREQ("{}", output);
+
+    //When saving the properties to a string with an error on  empty array flag
+    status = celix_properties_saveToString(props, 
CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS, &output);
+
+    //Then the save fails, because the empty array generates an error
+    ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+    //And at least one error message is added to celix_err
+    EXPECT_GE(celix_err_getErrorCount(), 1);
+    celix_err_printErrors(stderr, "Error: ", "\n");
 }
 
-TEST_F(PropertiesSerializationTestSuite, EncodeJPathKeysTest) {
+TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysTest) {
     //Given a properties object with jpath keys
     celix_autoptr(celix_properties_t) props = celix_properties_create();
     celix_properties_set(props, "key1", "value1");
@@ -200,8 +207,8 @@ TEST_F(PropertiesSerializationTestSuite, 
EncodeJPathKeysTest) {
     size_t bufLen = 0;
     FILE* stream = open_memstream(&buf, &bufLen);
 
-    //When encoding the properties to the stream
-    auto status = celix_properties_encodeToStream(props, stream, 0);
+    //When saving the properties to the stream
+    auto status = celix_properties_saveToStream(props, stream, 
CELIX_PROPERTIES_ENCODE_NESTED);
     ASSERT_EQ(CELIX_SUCCESS, status);
 
     //Then the stream contains the JSON representation snippets of the 
properties
@@ -219,90 +226,185 @@ TEST_F(PropertiesSerializationTestSuite, 
EncodeJPathKeysTest) {
     json_decref(root);
 }
 
-TEST_F(PropertiesSerializationTestSuite, EncodeJPathKeysWithCollisionTest) {
+TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysWithCollisionTest) {
+    // note this tests depends on the key iteration order for properties and
+    // properties key order is based on hash order of the keys, so this test 
can change if the string hash map
+    // implementation changes.
+
     //Given a properties object with jpath keys that collide
     celix_autoptr(celix_properties_t) props = celix_properties_create();
     celix_properties_set(props, "key1/key2/key3", "value1");
-    celix_properties_set(props, "key1/key2", "value2"); //collision with 
object "key1/key2"
-    celix_properties_set(props, "key4/key5/key6", "value3");
-    celix_properties_set(props, "key4/key5/key6/key7", "value4"); //collision 
with field "key3/key4/key5"
+    celix_properties_set(props, "key1/key2", "value2"); //collision with 
object "key1/key2/key3" -> overwrite
+    celix_properties_set(props, "key4/key5/key6/key7", "value4");
+    celix_properties_set(props, "key4/key5/key6", "value3"); //collision with 
field "key4/key5/key6/key7" -> overwrite
 
-    //And an in-memory stream
-    celix_autofree char* buf = nullptr;
-    size_t bufLen = 0;
-    FILE* stream = open_memstream(&buf, &bufLen);
-
-    //When encoding the properties to the stream
-    auto status = celix_properties_encodeToStream(props, stream, 0);
+    //When saving the properties to a string
+    celix_autofree char* output = nullptr;
+    auto status = celix_properties_saveToString(props, 
CELIX_PROPERTIES_ENCODE_NESTED, &output);
     ASSERT_EQ(CELIX_SUCCESS, status);
 
-    //Then the stream contains the JSON representation snippets of the 
properties
-    fclose(stream);
-    EXPECT_NE(nullptr, strstr(buf, R"("key1":{"key2":{"key3":"value1"}})")) << 
"JSON: " << buf;
-    EXPECT_NE(nullptr, strstr(buf, R"("key1/key2":"value2")")) << "JSON: " << 
buf;
-    EXPECT_NE(nullptr, strstr(buf, R"("key4/key5/key6":"value3")")) << "JSON: 
" << buf;
-    EXPECT_NE(nullptr, strstr(buf, 
R"("key4":{"key5":{"key6":{"key7":"value4"}}})")) << "JSON: " << buf;
-    //Note whether "key1/key2/key3" or "key1/key2" is serializer first depends 
on the hash order of the keys,
-    //so this test can change if the string hash map implementation changes.
+    //Then the stream contains the JSON representation of the properties with 
the collisions resolved
+    EXPECT_NE(nullptr, strstr(output, 
R"({"key1":{"key2":"value2"},"key4":{"key5":{"key6":"value3"}}})"))
+        << "JSON: " << output;
 
     //And the buf is a valid JSON object
     json_error_t error;
-    json_t* root = json_loads(buf, 0, &error);
+    json_t* root = json_loads(output, 0, &error);
     EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text;
     json_decref(root);
 }
 
 
-//TODO check desired behaviour, currently every "/" leads to a new object 
(except if an collision occurs)
-//TEST_F(PropertiesSerializationTestSuite, 
EncodePropertiesWithSpecialKeyNamesTest) {
-//    //Given a properties set with special key names (slashes)
-//    celix_autoptr(celix_properties_t) props = celix_properties_create();
-//    celix_properties_set(props, "/", "value1");
-//    celix_properties_set(props, "keyThatEndsWithSlash/", "value2");
-//    celix_properties_set(props, "key//With//Double//Slash", "value3");
-//    celix_properties_set(props, "object/", "value5");
-//    celix_properties_set(props, "object//", "value4");
-//    celix_properties_set(props, "object/keyThatEndsWithSlash/", "value6");
-//    celix_properties_set(props, "object/key//With//Double//Slash", "value7");
-//
-//    //And an in-memory stream
-//    celix_autofree char* buf = nullptr;
-//    size_t bufLen = 0;
-//    FILE* stream = open_memstream(&buf, &bufLen);
-//
-//    //When encoding the properties to the stream
-//    auto status = celix_properties_encodeToStream(props, stream, 0);
-//    ASSERT_EQ(CELIX_SUCCESS, status);
-//
-//    std::cout << buf << std::endl;
-//
-//    //Then the stream contains the JSON representation snippets of the 
properties
-//    fclose(stream);
-//    EXPECT_NE(nullptr, strstr(buf, R"("/":"value1")")) << "JSON: " << buf;
-//    EXPECT_NE(nullptr, strstr(buf, R"("keyThatEndsWithSlash/":"value2")")) 
<< "JSON: " << buf;
-//    EXPECT_NE(nullptr, strstr(buf, 
R"("key//With//Double//Slash":"value3")")) << "JSON: " << buf;
-//    EXPECT_NE(nullptr, strstr(buf, R"("object/":"value5")")) << "JSON: " << 
buf;
-//    EXPECT_NE(nullptr, strstr(buf, R"("/":"value5")")) << "JSON: " << buf; 
//child of object
-//    EXPECT_NE(nullptr, strstr(buf, R"("keyThatEndsWithSlash/":"value6")")) 
<< "JSON: " << buf; //child of object
-//    EXPECT_NE(nullptr, strstr(buf, 
R"("key//With//Double//Slash":"value7")")) << "JSON: " << buf; //child of object
-//
-//
-//    //And the buf is a valid JSON object
-//    json_error_t error;
-//    json_t* root = json_loads(buf, 0, &error);
-//    EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text;
-//    json_decref(root);
-//}
-
-
-TEST_F(PropertiesSerializationTestSuite, DecodeEmptyPropertiesTest) {
+
+TEST_F(PropertiesSerializationTestSuite, 
SavePropertiesWithKeyNamesWithSlashesTest) {
+    //Given a properties set with key names with slashes
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    celix_properties_set(props, "a/key/name/with/slashes", "value1");
+    //TODO test separately celix_properties_set(props, "/", "value2");
+    celix_properties_set(props, "/keyThatStartsWithSlash", "value3");
+    //TODO test separately celix_properties_set(props, 
"//keyThatStartsWithDoubleSlashes", "value4");
+    celix_properties_set(props, "keyThatEndsWithSlash/", "value5");
+    celix_properties_set(props, "keyThatEndsWithDoubleSlashes//", "value6");
+    celix_properties_set(props, "key//With//Double//Slashes", "value7");
+    celix_properties_set(props, "object/keyThatEndsWithSlash/", "value8");
+    celix_properties_set(props, "object/keyThatEndsWithDoubleSlashes//", 
"value9");
+    celix_properties_set(props, "object/key//With//Double//Slashes", 
"value10");
+
+
+    //When saving the properties to a string
+    char* output = nullptr;
+    auto status = celix_properties_saveToString(props, 
CELIX_PROPERTIES_ENCODE_NESTED, &output);
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    //Then the out contains the JSON representation snippets of the properties
+    EXPECT_NE(nullptr, strstr(output, 
R"("a":{"key":{"name":{"with":{"slashes":"value1"}}}})")) << "JSON: " << output;
+    EXPECT_NE(nullptr, strstr(output, R"("keyThatStartsWithSlash":"value3")")) 
<< "JSON: " << output;
+    EXPECT_NE(nullptr, strstr(output, R"("":"value5")")) << "JSON: " << output;
+    EXPECT_NE(nullptr, strstr(output, R"("":"value6")")) << "JSON: " << output;
+    EXPECT_NE(nullptr, strstr(output, R"("Slashes":"value7")")) << "JSON: " << 
output;
+    EXPECT_NE(nullptr, strstr(output, R"("":"value8")")) << "JSON: " << output;
+    EXPECT_NE(nullptr, strstr(output, R"("":"value9")")) << "JSON: " << output;
+    EXPECT_NE(nullptr, strstr(output, R"("Slashes":"value10")")) << "JSON: " 
<< output;
+
+    //And the output is a valid JSON object
+    json_error_t error;
+    json_t* root = json_loads(output, 0, &error);
+    EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text;
+
+
+    //And the structure for (e.g.) value10 is correct
+    json_t* node = json_object_get(root, "object");
+    ASSERT_NE(nullptr, node);
+    ASSERT_TRUE(json_is_object(node));
+    node = json_object_get(node, "key");
+    ASSERT_NE(nullptr, node);
+    ASSERT_TRUE(json_is_object(node));
+    node = json_object_get(node, "");
+    ASSERT_NE(nullptr, node);
+    ASSERT_TRUE(json_is_object(node));
+    node = json_object_get(node, "With");
+    ASSERT_NE(nullptr, node);
+    ASSERT_TRUE(json_is_object(node));
+    node = json_object_get(node, "");
+    ASSERT_NE(nullptr, node);
+    ASSERT_TRUE(json_is_object(node));
+    node = json_object_get(node, "Double");
+    ASSERT_NE(nullptr, node);
+    ASSERT_TRUE(json_is_object(node));
+    node = json_object_get(node, "");
+    ASSERT_NE(nullptr, node);
+    ASSERT_TRUE(json_is_object(node));
+    node = json_object_get(node, "Slashes");
+    ASSERT_NE(nullptr, node);
+    ASSERT_TRUE(json_is_string(node));
+    EXPECT_STREQ("value10", json_string_value(node));
+
+    json_decref(root);
+}
+
+TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyCollision) {
+    // note this tests depends on the key iteration order for properties and
+    // properties key order is based on hash order of the keys, so this test 
can change if the string hash map
+    // implementation changes.
+
+    //Given a properties that contains keys that will collide with an existing 
JSON object
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    celix_properties_set(props, "key1/key2/key3", "value1");
+    celix_properties_set(props, "key1/key2", "value2"); //collision with 
object "key1/key2" -> overwrite
+
+    //When saving the properties to a string
+    char* output = nullptr;
+    auto status = celix_properties_saveToString(props, 
CELIX_PROPERTIES_ENCODE_NESTED, &output);
+
+    //Then the save succeeds
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    // And both keys are serialized (one as a flat key) (flat key name is 
whitebox knowledge)
+    EXPECT_NE(nullptr, strstr(output, R"({"key1":{"key2":"value2"}})")) << 
"JSON: " << output;
+
+    //When saving the properties to a string with the error on key collision 
flag
+    status = celix_properties_saveToString(
+        props, CELIX_PROPERTIES_ENCODE_NESTED | 
CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS, &output);
+
+    //Then the save fails, because the keys collide
+    ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+    //And at least one error message is added to celix_err
+    EXPECT_GE(celix_err_getErrorCount(), 1);
+    celix_err_printErrors(stderr, "Error: ", "\n");
+}
+
+TEST_F(PropertiesSerializationTestSuite, 
SavePropertiesWithAndWithoutStrictFlagTest) {
+    //Given a properties set with an empty array list
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    auto* list = celix_arrayList_createStringArray();
+    celix_properties_assignArrayList(props, "key1", list);
+
+    //When saving the properties to a string without the strict flag
+    char* output = nullptr;
+    auto status = celix_properties_saveToString(props, 0, &output);
+
+    //Then the save succeeds
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    //When saving the properties to a string with the strict flag
+    status = celix_properties_saveToString(props, 
CELIX_PROPERTIES_ENCODE_STRICT, &output);
+
+    //Then the save fails, because the empty array generates an error
+    ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+    //And at least one error message is added to celix_err
+    EXPECT_GE(celix_err_getErrorCount(), 1);
+    celix_err_printErrors(stderr, "Error: ", "\n");
+}
+
+TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithPrettyPrintTest) {
+    //Given a properties set with 2 keys
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    celix_properties_set(props, "key1", "value1");
+    celix_properties_set(props, "key2", "value2");
+
+    //When saving the properties to a string with pretty print
+    char* output = nullptr;
+    auto status = celix_properties_saveToString(props, 
CELIX_PROPERTIES_ENCODE_PRETTY, &output);
+
+    //Then the save succeeds
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    // And the output contains the JSON representation snippets of the 
properties with pretty print (2 indent spaces and
+    // newlines)
+    auto* expected = "{\n  \"key2\": \"value2\",\n  \"key1\": \"value1\"\n}";
+    EXPECT_STREQ(expected, output);
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadEmptyPropertiesTest) {
     //Given an empty JSON object
     const char* json = "{}";
     FILE* stream = fmemopen((void*)json, strlen(json), "r");
 
-    //When decoding the properties from the stream
+    //When loading the properties from the stream
     celix_autoptr(celix_properties_t) props = nullptr;
-    auto status = celix_properties_decodeFromStream(stream, 0, &props);
+    auto status = celix_properties_loadFromStream(stream, 0, &props);
     ASSERT_EQ(CELIX_SUCCESS, status);
 
     //Then the properties object is empty
@@ -311,7 +413,7 @@ TEST_F(PropertiesSerializationTestSuite, 
DecodeEmptyPropertiesTest) {
     fclose(stream);
 }
 
-TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithSingleValuesTest) 
{
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithSingleValuesTest) {
     //Given a JSON object with single values for types string, long, double, 
bool and version
     const char* jsonInput = R"({
         "strKey":"strValue",
@@ -324,9 +426,9 @@ TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithSingleValuesTest) {
     //And a stream with the JSON object
     FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r");
 
-    //When decoding the properties from the stream
+    //When loading the properties from the stream
     celix_autoptr(celix_properties_t) props = nullptr;
-    auto status = celix_properties_decodeFromStream(stream, 0, &props);
+    auto status = celix_properties_loadFromStream(stream, 0, &props);
     ASSERT_EQ(CELIX_SUCCESS, status);
 
     //Then the properties object contains the single values
@@ -341,7 +443,7 @@ TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithSingleValuesTest) {
     EXPECT_STREQ("1.2.3.qualifier", vStr);
 }
 
-TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithArrayListsTest) {
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithArrayListsTest) {
     //Given a JSON object with array values for types string, long, double, 
bool and version
     const char* jsonInput = R"({
         "strArr":["value1","value2"],
@@ -356,9 +458,9 @@ TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithArrayListsTest) {
     //And a stream with the JSON object
     FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r");
 
-    //When decoding the properties from the stream
+    //When loading the properties from the stream
     celix_autoptr(celix_properties_t) props = nullptr;
-    auto status = celix_properties_decodeFromStream(stream, 0, &props);
+    auto status = celix_properties_loadFromStream(stream, 0, &props);
     ASSERT_EQ(CELIX_SUCCESS, status);
 
     //Then the properties object contains the array values
@@ -430,7 +532,7 @@ TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithArrayListsTest) {
     EXPECT_DOUBLE_EQ(3.0, celix_arrayList_getDouble(mixedRealAndIntArr2, 3));
 }
 
-TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithInvalidInputTest) 
{
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithInvalidInputTest) {
     auto invalidInputs = {
         R"({)",                            // invalid JSON (caught by jansson)
         R"([])",                           // unsupported JSON (top level 
array not supported)
@@ -440,9 +542,9 @@ TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithInvalidInputTest) {
         //Given an invalid JSON object
         FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), 
"r");
 
-        //When decoding the properties from the stream
+        //When loading the properties from the stream
         celix_autoptr(celix_properties_t) props = nullptr;
-        auto status = celix_properties_decodeFromStream(stream, 0, &props);
+        auto status = celix_properties_loadFromStream(stream, 0, &props);
 
         //Then loading fails
         EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
@@ -455,25 +557,31 @@ TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithInvalidInputTest) {
     }
 }
 
-TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithEmptyArrayTest) {
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithEmptyArrayTest) {
     //Given a JSON object with an empty array
-    auto* emptyArrays = R"({"key1":[]})";
+    auto* inputJSON = R"({"key1":[]})";
 
-    //And a stream with the JSON object
-    FILE* stream = fmemopen((void*)emptyArrays, strlen(emptyArrays), "r");
-
-    //When decoding the properties from the stream
+    //When loading the properties from string
     celix_autoptr(celix_properties_t) props = nullptr;
-    auto status = celix_properties_decodeFromStream(stream, 0, &props);
+    auto status = celix_properties_loadFromString2(inputJSON, 0, &props);
 
     //Then loading succeeds
     ASSERT_EQ(CELIX_SUCCESS, status);
 
     //And the properties object is empty, because empty arrays are treated as 
unset
     EXPECT_EQ(0, celix_properties_size(props));
+
+    //When loading the properties from string with a strict flag
+    status = celix_properties_loadFromString2(inputJSON, 
CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS, &props);
+
+    //Then loading fails, because the empty array generates an error
+    ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+    //And at least one error message is added to celix_err
+    ASSERT_GE(celix_err_getErrorCount(), 1);
 }
 
-TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithNestedObjectsTest) {
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithNestedObjectsTest) {
     // Given a complex JSON object
     const char* jsonInput = R"({
         "key1":"value1",
@@ -495,9 +603,9 @@ TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithNestedObjectsTest)
     // And a stream with the JSON object
     FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r");
 
-    // When decoding the properties from the stream
+    // When loading the properties from the stream
     celix_autoptr(celix_properties_t) props = nullptr;
-    auto status = celix_properties_decodeFromStream(stream, 0, &props);
+    auto status = celix_properties_loadFromStream(stream, 0, &props);
     ASSERT_EQ(CELIX_SUCCESS, status);
 
     // Then the properties object contains the nested objects
@@ -510,40 +618,79 @@ TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithNestedObjectsTest)
     EXPECT_EQ(6, celix_properties_getLong(props, "object3/object4/key6", 0));
 }
 
-TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithNestedObjectsAndJPathCollisionTest) {
-    // Given a complex JSON object with jpath keys that collide
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithDuplicatesTest) {
+    // Given a complex JSON object with duplicate keys
+    const char* jsonInput = R"({
+        "key":2,
+        "key":3
+    })";
+
+    // When loading the properties from a string.
+    celix_autoptr(celix_properties_t) props = nullptr;
+    auto status = celix_properties_loadFromString2(jsonInput, 0, &props);
+
+    // Then loading succeeds
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    // And the properties object contains the last values of the jpath keys
+    EXPECT_EQ(1, celix_properties_size(props));
+    EXPECT_EQ(3, celix_properties_getLong(props, "key", 0));
+
+    // When decoding the properties from the stream using a flog that does not 
allow duplicates
+    celix_autoptr(celix_properties_t) props2 = nullptr;
+    status = celix_properties_loadFromString2(jsonInput, 
CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES, &props2);
+
+    // Then loading fails, because of a duplicate key
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+    // And at least one error message is added to celix_err
+    EXPECT_GE(celix_err_getErrorCount(), 1);
+    celix_err_printErrors(stderr, "Error: ", "\n");
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesEscapedSlashesTest) {
+    // Given a complex JSON object with collisions and duplicate keys
+    // Collisions:
+    // - object object1/object2 and value object1/object2
+    // - value key1 in object2 in object1 and value object2/key in object1
+    // - value object3/key4 and value key4 in object object3
+    // Duplicate JSON keys:
+    // - key3
     const char* jsonInput = R"({
         "object1": {
             "object2": {
-                "key1":true
-            }
+                "key1": "value1"
+            },
+            "object2/key2": "value2"
         },
-        "object1/object2/key1":6,
-        "key2":2,
-        "key2":3
+        "object1/object2" : "value3",
+        "key3": "value4",
+        "key3": "value5",
+        "object3/key4": "value6",
+        "object3": {
+            "key4": "value7"
+        }
     })";
 
-    // And a stream with the JSON object
-    FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r");
-
-    // When decoding the properties from the stream
+    // When loading the properties from a string.
     celix_autoptr(celix_properties_t) props = nullptr;
-    auto status = celix_properties_decodeFromStream(stream, 0, &props);
+    auto status = celix_properties_loadFromString2(jsonInput, 0, &props);
 
     // Then loading succeeds
     ASSERT_EQ(CELIX_SUCCESS, status);
 
-    // And the properties object contains the last values of the jpath keys
-    EXPECT_EQ(2, celix_properties_size(props));
-    EXPECT_EQ(6, celix_properties_getLong(props, "object1/object2/key1", 0));
-    EXPECT_EQ(3, celix_properties_getLong(props, "key2", 0));
-
-    // When the stream is reset
-    fseek(stream, 0, SEEK_SET);
+    // And the properties object all the values for the colliding keys and a 
single (latest) value for the duplicate
+    // keys
+    EXPECT_EQ(5, celix_properties_size(props));
+    EXPECT_STREQ("value1", celix_properties_getString(props, 
"object1/object2/key1"));
+    EXPECT_STREQ("value2", celix_properties_getString(props, 
"object1/object2/key2"));
+    EXPECT_STREQ("value3", celix_properties_getString(props, 
"object1/object2"));
+    EXPECT_STREQ("value5", celix_properties_getString(props, "key3"));
+    EXPECT_STREQ("value7", celix_properties_getString(props, "object3/key4"));
 
-    // And decoding the properties from the stream using a flog that does not 
allow collisions
+    // When decoding the properties from a string using a flag that allows 
duplicates
     celix_autoptr(celix_properties_t) props2 = nullptr;
-    status = celix_properties_decodeFromStream(stream, 
CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES, &props2);
+    status = celix_properties_loadFromString2(jsonInput, 
CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES, &props2);
 
     // Then loading fails, because of a duplicate key
     EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
@@ -551,9 +698,20 @@ TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithNestedObjectsAndJPa
     // And at least one error message is added to celix_err
     EXPECT_GE(celix_err_getErrorCount(), 1);
     celix_err_printErrors(stderr, "Error: ", "\n");
+
+    // When decoding the properties from a string using a flag that allows 
collisions
+    celix_autoptr(celix_properties_t) props3 = nullptr;
+    status = celix_properties_loadFromString2(jsonInput, 
CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS, &props3);
+
+    // Then loading fails, because of a collision
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+    // And at least one error message is added to celix_err
+    EXPECT_GE(celix_err_getErrorCount(), 1);
+    celix_err_printErrors(stderr, "Error: ", "\n");
 }
 
-TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithStrictEnabledDisabledTest) {
+TEST_F(PropertiesSerializationTestSuite, 
LoadPropertiesWithAndWithoutStrictFlagTest) {
     auto invalidInputs = {
         R"({"mixedArr":["string", true]})", // Mixed array gives error on 
strict
         R"({"key1":null})",                 // Null value gives error on strict
@@ -567,9 +725,9 @@ TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithStrictEnabledDisabl
         //Given an invalid JSON object
         FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), 
"r");
 
-        //When decoding the properties from the stream with an empty flags
+        //When loading the properties from the stream with an empty flags
         celix_autoptr(celix_properties_t) props = nullptr;
-        auto status = celix_properties_decodeFromStream(stream, 0, &props);
+        auto status = celix_properties_loadFromStream(stream, 0, &props);
         celix_err_printErrors(stderr, "Error: ", "\n");
 
         //Then decoding succeeds, because strict is disabled
@@ -587,9 +745,9 @@ TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithStrictEnabledDisabl
         //Given an invalid JSON object
         FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), 
"r");
 
-        //When decoding the properties from the stream with a strict flag
+        //When loading the properties from the stream with a strict flag
         celix_autoptr(celix_properties_t) props = nullptr;
-        auto status = celix_properties_decodeFromStream(stream, 
CELIX_PROPERTIES_DECODE_STRICT, &props);
+        auto status = celix_properties_loadFromStream(stream, 
CELIX_PROPERTIES_DECODE_STRICT, &props);
 
         //Then decoding fails
         EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
@@ -602,7 +760,7 @@ TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithStrictEnabledDisabl
     }
 }
 
-TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithSpecialKeyNamesTest) {
+TEST_F(PropertiesSerializationTestSuite, 
LoadPropertiesWithSlashesInTheKeysTest) {
     // Given a complex JSON object
     const char* jsonInput = R"({
         "/": "value1",
@@ -618,9 +776,9 @@ TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithSpecialKeyNamesTest
     // And a stream with the JSON object
     FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r");
 
-    // When decoding the properties from the stream
+    // When loading the properties from the stream
     celix_autoptr(celix_properties_t) props = nullptr;
-    auto status = celix_properties_decodeFromStream(stream, 0, &props);
+    auto status = celix_properties_loadFromStream(stream, 0, &props);
     celix_err_printErrors(stderr, "Error: ", "\n");
     ASSERT_EQ(CELIX_SUCCESS, status);
 
@@ -636,5 +794,6 @@ TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithSpecialKeyNamesTest
 
 //TODO test with invalid version string
 //TODO is there a strict option needed for version (e.g. not parseable as 
version handle as string)
-//TODO test encoding flags
-//TODO error injection tests and wrappers for jansson functions
\ No newline at end of file
+//TODO error injection tests and wrappers for jansson functions
+//TODO test load and save with filename
+//TODO check all the celix_err messages for consistency
diff --git a/libs/utils/include/celix_properties.h 
b/libs/utils/include/celix_properties.h
index b82ad9ad..04dd04f9 100644
--- a/libs/utils/include/celix_properties.h
+++ b/libs/utils/include/celix_properties.h
@@ -180,31 +180,6 @@ CELIX_UTILS_EXPORT celix_status_t 
celix_properties_store(celix_properties_t* pro
                                                          const char* file,
                                                          const char* header);
 
-//TODO document the encode flags
-#define CELIX_PROPERTIES_ENCODE_PRETTY                  0x01
-#define CELIX_PROPERTIES_ENCODE_SORT_KEYS               0x02
-
-//TODO doc. Not encode does not reset the stream position.
-CELIX_UTILS_EXPORT celix_status_t celix_properties_encodeToStream(const 
celix_properties_t* properties,
-                                                                  FILE* stream,
-                                                                  int 
encodeFlags);
-
-//TODO document the decode flags
-#define CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES     0x01
-#define CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES    0x02
-#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS   0x04
-#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS     0x04
-#define CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS   0x08
-#define CELIX_PROPERTIES_DECODE_STRICT                                         
                                        \
-    (CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES | 
CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES |                      \
-     CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS | 
CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS |                     \
-     CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS)
-
-//TODO doc. Note decode does not reset the stream position.
-CELIX_UTILS_EXPORT celix_status_t celix_properties_decodeFromStream(FILE* 
stream,
-                                                                    int 
decodeFlags,
-                                                                    
celix_properties_t** out);
-
 /**
  * @brief Get the entry for a given key in a property set.
  *
@@ -962,6 +937,62 @@ CELIX_UTILS_EXPORT bool 
celix_propertiesIterator_equals(const celix_properties_i
          !celix_propertiesIterator_isEnd(&(iterName));                         
                                        \
          celix_propertiesIterator_next(&(iterName)))
 
+
+
+//TODO document the encode flags
+#define CELIX_PROPERTIES_ENCODE_PRETTY                  0x01
+#define CELIX_PROPERTIES_ENCODE_FLAT                    0x02 //TODO doc, 
explain that decoding options ensures all properties entries are written, but 
only as a top level field entries.
+#define CELIX_PROPERTIES_ENCODE_NESTED                  0x04
+
+#define CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS     0x10
+#define CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS   0x20
+#define CELIX_PROPERTIES_ENCODE_STRICT                                         
                                        \
+    (CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS | 
CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS)
+
+//TODO doc
+CELIX_UTILS_EXPORT celix_status_t celix_properties_save(const 
celix_properties_t* properties,
+                                                        const char* filename,
+                                                        int encodeFlags);
+
+//TODO doc. Not encode does not reset or close the stream position.
+CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToStream(const 
celix_properties_t* properties,
+                                                                FILE* stream,
+                                                                int 
encodeFlags);
+
+//TODO doc
+CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToString(const 
celix_properties_t* properties,
+                                                                int 
encodeFlags,
+                                                                char** out);
+
+
+//TODO document the decode flags
+#define CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES     0x01
+#define CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS     0x02
+#define CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES    0x04
+#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS   0x08
+#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS     0x10
+#define CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS   0x20
+#define CELIX_PROPERTIES_DECODE_STRICT                                         
                                        \
+    (CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES | 
CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS |                       \
+     CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES | 
CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS |                    \
+     CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS | 
CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS)
+
+//TODO doc. Note load2, because load is currently already used. Will be 
updated in the future.
+CELIX_UTILS_EXPORT celix_status_t celix_properties_load2(const char* filename,
+                                                         int decodeFlags,
+                                                         celix_properties_t** 
out);
+
+//TODO doc. Note decode does not reset or close the stream position.
+CELIX_UTILS_EXPORT celix_status_t celix_properties_loadFromStream(FILE* stream,
+                                                                  int 
decodeFlags,
+                                                                  
celix_properties_t** out);
+
+//TODO doc. Note celix_properties_loadFromString2, because loadFromString is 
currently already used. Will be updated in the future.
+CELIX_UTILS_EXPORT celix_status_t celix_properties_loadFromString2(const char* 
input,
+                                                                   int 
decodeFlags,
+                                                                   
celix_properties_t** out);
+
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/libs/utils/include/celix_utils.h b/libs/utils/include/celix_utils.h
index 7933b4eb..6f58f2b7 100644
--- a/libs/utils/include/celix_utils.h
+++ b/libs/utils/include/celix_utils.h
@@ -29,6 +29,8 @@ extern "C" {
 #include <stdbool.h>
 
 #include "celix_utils_export.h"
+#include "celix_compiler.h"
+#include "celix_cleanup.h"
 
 #define CELIX_US_IN_SEC (1000000)
 #define CELIX_NS_IN_SEC ((CELIX_US_IN_SEC)*1000)
@@ -91,6 +93,60 @@ __attribute__((format(printf, 3, 0)));
  */
 CELIX_UTILS_EXPORT void celix_utils_freeStringIfNotEqual(const char* buffer, 
char* str);
 
+/**
+ * @brief Guard for a string created with celix_utils_writeOrCreateString, 
celix_utils_writeOrCreateVString.
+ *
+ * Can be used with celix_auto() to automatically and correctly free the 
string.
+ * If the string is pointing to the buffer, the string should be freed, 
otherwise the string should be freed.
+ *
+ *
+ */
+typedef struct celix_utils_string_guard {
+    const char* buffer;
+    char* string;
+} celix_utils_string_guard_t;
+
+/**
+ * @brief Initialize a guard for a string created with 
celix_utils_writeOrCreateString, celix_utils_writeOrCreateVString.
+ *
+ * De-initialize with celix_utils_stringGuard_deinit().
+ *
+ * No allocation is performed.
+ * This is intended to be used with celix_auto().
+ *
+ * * Example:
+* ```
+ * const char* possibleLongString = ...
+ * char buffer[64];
+ * char* str = celix_utils_writeOrCreateString(buffer, sizeof(buffer), "Hello 
%s", possibleLongString);
+ * celix_auto(celix_utils_string_guard_t) strGuard = 
celix_utils_stringGuard_init(buffer, str);
+ * ```
+ * If the strGuard goes out of scope, the string will be freed correctly.
+ *
+ * @param buffer A (local) buffer which was potentially used to create the 
string.
+ * @param string The string to guard.
+ * @return An initialized string guard to be used with celix_auto().
+ */
+static CELIX_UNUSED inline celix_utils_string_guard_t 
celix_utils_stringGuard_init(const char* buffer, char* string) {
+    celix_utils_string_guard_t guard;
+    guard.buffer = buffer;
+    guard.string = string;
+    return guard;
+}
+
+/**
+ * @brief De-initialize a string guard.
+ *
+ * This will free the string if it is not equal to the buffer.
+ * This is intended to be used with celix_auto().
+ *
+ * @param guard The guard to de-initialize.
+ */
+static CELIX_UNUSED inline void 
celix_utils_stringGuard_deinit(celix_utils_string_guard_t* guard) {
+    celix_utils_freeStringIfNotEqual(guard->buffer, guard->string);
+}
+
+CELIX_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(celix_utils_string_guard_t, 
celix_utils_stringGuard_deinit)
 
 /**
  * @brief Compares two strings and returns true if the strings are equal.
diff --git a/libs/utils/src/properties_encoding.c 
b/libs/utils/src/properties_encoding.c
index ba30b670..e1ad99b0 100644
--- a/libs/utils/src/properties_encoding.c
+++ b/libs/utils/src/properties_encoding.c
@@ -31,8 +31,6 @@
 static celix_status_t
 celix_properties_decodeValue(celix_properties_t* props, const char* key, 
json_t* jsonValue, int flags);
 
-// TODO make jansson wrapper header for auto cleanup, wrap json_object_set_new 
and json_dumpf for error injection
-
 static celix_status_t celix_properties_versionToJson(const celix_version_t* 
version, json_t** out) {
     celix_autofree char* versionStr = celix_version_toString(version);
     if (!versionStr) {
@@ -68,7 +66,7 @@ static celix_status_t 
celix_properties_arrayElementEntryValueToJson(celix_array_
         return celix_properties_versionToJson(entry.versionVal, out);
     default:
         // LCOV_EXCL_START
-        celix_err_pushf("Unexpected array list element type %d", elType);
+        celix_err_pushf("Invalid array list element type %d", elType);
         return CELIX_ILLEGAL_ARGUMENT;
         // LCOV_EXCL_STOP
     }
@@ -79,9 +77,16 @@ static celix_status_t 
celix_properties_arrayElementEntryValueToJson(celix_array_
     return CELIX_SUCCESS;
 }
 
-static celix_status_t celix_properties_arrayEntryValueToJson(const 
celix_properties_entry_t* entry, json_t** out) {
+static celix_status_t celix_properties_arrayEntryValueToJson(const char* key,
+                                                             const 
celix_properties_entry_t* entry,
+                                                             int flags,
+                                                             json_t** out) {
     *out = NULL;
     if (celix_arrayList_size(entry->typed.arrayValue) == 0) {
+        if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS) {
+            celix_err_pushf("Invalid empty array for key %s", key);
+            return CELIX_ILLEGAL_ARGUMENT;
+        }
         return CELIX_SUCCESS; // empty array -> treat as unset property
     }
 
@@ -115,7 +120,8 @@ static celix_status_t 
celix_properties_arrayEntryValueToJson(const celix_propert
     return CELIX_SUCCESS;
 }
 
-static celix_status_t celix_properties_entryValueToJson(const 
celix_properties_entry_t* entry, json_t** out) {
+static celix_status_t
+celix_properties_entryValueToJson(const char* key, const 
celix_properties_entry_t* entry, int flags, json_t** out) {
     *out = NULL;
     switch (entry->valueType) {
     case CELIX_PROPERTIES_VALUE_TYPE_STRING:
@@ -126,7 +132,7 @@ static celix_status_t 
celix_properties_entryValueToJson(const celix_properties_e
         break;
     case CELIX_PROPERTIES_VALUE_TYPE_DOUBLE:
         if (isnan(entry->typed.doubleValue) || 
isinf(entry->typed.doubleValue)) {
-            celix_err_pushf("Double NaN or Inf not supported in JSON.");
+            celix_err_pushf("Invalid NaN or Inf in key '%s'.", key);
             return CELIX_ILLEGAL_ARGUMENT;
         }
         *out = json_real(entry->typed.doubleValue);
@@ -137,34 +143,76 @@ static celix_status_t 
celix_properties_entryValueToJson(const celix_properties_e
     case CELIX_PROPERTIES_VALUE_TYPE_VERSION:
         return celix_properties_versionToJson(entry->typed.versionValue, out);
     case CELIX_PROPERTIES_VALUE_TYPE_ARRAY_LIST:
-        return celix_properties_arrayEntryValueToJson(entry, out);
+        return celix_properties_arrayEntryValueToJson(key, entry, flags, out);
     default:
         // LCOV_EXCL_START
-        celix_err_pushf("Unexpected properties entry type %d", 
entry->valueType);
+        celix_err_pushf("Invalid properties entry type %d.", entry->valueType);
         return CELIX_ILLEGAL_ARGUMENT;
         // LCOV_EXCL_STOP
     }
 
     if (!*out) {
-        celix_err_push("Failed to create json value");
+        celix_err_pushf("Failed to create json value for key '%s'.", key);
         return CELIX_ENOMEM;
     }
     return CELIX_SUCCESS;
 }
 
-static celix_status_t
-celix_properties_addEntryToJson(const celix_properties_entry_t* entry, const 
char* key, json_t* root) {
+static celix_status_t celix_properties_addJsonValueToJson(json_t* value, const 
char* key, json_t* obj, int flags) {
+    if (!value) {
+        // ignore unset values
+        return CELIX_SUCCESS;
+    }
+
+    json_t* field = json_object_get(obj, key);
+    if (field) {
+        if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS) {
+            celix_err_pushf("Invalid key collision. key '%s' already exists.", 
key);
+            return CELIX_ILLEGAL_ARGUMENT;
+        }
+    }
+
+    int rc = json_object_set_new(obj, key, value);
+    if (rc != 0) {
+        celix_err_push("Failed to set json object");
+        return CELIX_ENOMEM;
+    }
+    return CELIX_SUCCESS;
+}
+
+static celix_status_t celix_properties_addPropertiesEntryFlatToJson(const 
celix_properties_entry_t* entry,
+                                                                    const 
char* key,
+                                                                    json_t* 
root,
+                                                                    int flags) 
{
+    json_t* value;
+    celix_status_t status = celix_properties_entryValueToJson(key, entry, 
flags, &value);
+    if (status != CELIX_SUCCESS) {
+        return status;
+    }
+    return celix_properties_addJsonValueToJson(value, key, root, flags);
+}
+
+static celix_status_t celix_properties_addPropertiesEntryToJson(const 
celix_properties_entry_t* entry,
+                                                                const char* 
key,
+                                                                json_t* root,
+                                                                int flags) {
     json_t* jsonObj = root;
-    const char* subKey = key;
+    const char* fieldName = key;
     const char* slash = strstr(key, "/");
     while (slash) {
-        celix_autofree const char* name = strndup(subKey, slash - subKey);
+        char buf[64];
+        char* name = celix_utils_writeOrCreateString(buf, sizeof(buf), "%.*s", 
(int)(slash - fieldName), fieldName);
+        celix_auto(celix_utils_string_guard_t) strGuard = 
celix_utils_stringGuard_init(buf, name);
         if (!name) {
             celix_err_push("Failed to create name string");
             return CELIX_ENOMEM;
         }
         json_t* subObj = json_object_get(jsonObj, name);
-        if (!subObj) {
+        if (!subObj || !json_is_object(subObj)) {
+            if (!json_is_object(subObj) && flags & 
CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS) {
+                celix_err_pushf("Invalid key collision. Key '%s' already 
exists.", name);
+                return CELIX_ILLEGAL_ARGUMENT;
+            }
             subObj = json_object();
             if (!subObj) {
                 celix_err_push("Failed to create json object");
@@ -175,51 +223,40 @@ celix_properties_addEntryToJson(const 
celix_properties_entry_t* entry, const cha
                 celix_err_push("Failed to set json object");
                 return CELIX_ENOMEM;
             }
-        } else if (!json_is_object(subObj)) {
-            // subObj is not an object, so obj cannot be added -> adding obj 
flat
-            jsonObj = root;
-            subKey = key;
-            break;
         }
 
         jsonObj = subObj;
-        subKey = slash + 1;
-        slash = strstr(subKey, "/");
-
-        json_t* field = json_object_get(jsonObj, subKey);
-        if (field) {
-            // field already exists, so adding obj flat
-            jsonObj = root;
-            subKey = key;
-            break;
-        }
+        fieldName = slash + 1;
+        slash = strstr(fieldName, "/");
     }
 
     json_t* value;
-    celix_status_t status = celix_properties_entryValueToJson(entry, &value);
+    celix_status_t status = celix_properties_entryValueToJson(fieldName, 
entry, flags, &value);
     if (status != CELIX_SUCCESS) {
         return status;
-    } else if (!value) {
-        // ignore unset values
-    } else {
-        int rc = json_object_set_new(jsonObj, subKey, value);
-        if (rc != 0) {
-            celix_err_push("Failed to set json object");
-            return CELIX_ENOMEM;
-        }
     }
-
-    return CELIX_SUCCESS;
+    return celix_properties_addJsonValueToJson(value, fieldName, jsonObj, 
flags);
 }
 
-celix_status_t celix_properties_encodeToStream(const celix_properties_t* 
properties, FILE* stream, int encodeFlags) {
+celix_status_t celix_properties_saveToStream(const celix_properties_t* 
properties, FILE* stream, int encodeFlags) {
     json_t* root = json_object();
     if (!root) {
         celix_err_push("Failed to create json object");
     }
 
+    if (!(encodeFlags & CELIX_PROPERTIES_ENCODE_FLAT) && !(encodeFlags & 
CELIX_PROPERTIES_ENCODE_NESTED)) {
+        //no encoding flags set, default to flat
+        encodeFlags |= CELIX_PROPERTIES_ENCODE_FLAT;
+    }
+
     CELIX_PROPERTIES_ITERATE(properties, iter) {
-        celix_status_t status = celix_properties_addEntryToJson(&iter.entry, 
iter.key, root);
+        celix_status_t status;
+        if (encodeFlags & CELIX_PROPERTIES_ENCODE_FLAT) {
+            status = 
celix_properties_addPropertiesEntryFlatToJson(&iter.entry, iter.key, root, 
encodeFlags);
+        } else {
+            assert(encodeFlags & CELIX_PROPERTIES_ENCODE_NESTED);
+            status = celix_properties_addPropertiesEntryToJson(&iter.entry, 
iter.key, root, encodeFlags);
+        }
         if (status != CELIX_SUCCESS) {
             json_decref(root);
             return status;
@@ -230,9 +267,6 @@ celix_status_t celix_properties_encodeToStream(const 
celix_properties_t* propert
     if (encodeFlags & CELIX_PROPERTIES_ENCODE_PRETTY) {
         jsonFlags = JSON_INDENT(2);
     }
-    if (encodeFlags & CELIX_PROPERTIES_ENCODE_SORT_KEYS) {
-        jsonFlags |= JSON_SORT_KEYS;
-    }
 
     int rc = json_dumpf(root, stream, jsonFlags);
     json_decref(root);
@@ -243,6 +277,35 @@ celix_status_t celix_properties_encodeToStream(const 
celix_properties_t* propert
     return CELIX_SUCCESS;
 }
 
+celix_status_t celix_properties_save(const celix_properties_t* properties, 
const char* filename, int encodeFlags) {
+    FILE* stream = fopen(filename, "w");
+    if (!stream) {
+        celix_err_pushf("Failed to open file %s", filename);
+        return CELIX_FILE_IO_EXCEPTION;
+    }
+    celix_status_t status = celix_properties_saveToStream(properties, stream, 
encodeFlags);
+    fclose(stream);
+    return status;
+}
+
+celix_status_t celix_properties_saveToString(const celix_properties_t* 
properties, int encodeFlags, char** out) {
+    *out = NULL;
+    celix_autofree char* buffer = NULL;
+    size_t size = 0;
+    FILE* stream = open_memstream(&buffer, &size);
+    if (!stream) {
+        celix_err_push("Failed to open memstream");
+        return CELIX_FILE_IO_EXCEPTION;
+    }
+
+    celix_status_t status = celix_properties_saveToStream(properties, stream, 
encodeFlags);
+    fclose(stream);
+    if (status == CELIX_SUCCESS) {
+        *out = celix_steal_ptr(buffer);
+    }
+    return status;
+}
+
 static celix_version_t* celix_properties_parseVersion(const char* value) {
     // precondition: value is a valid version string (14 chars prefix and 1 
char suffix)
     celix_version_t* version = NULL;
@@ -326,7 +389,7 @@ static celix_status_t 
celix_properties_determineArrayType(const json_t* jsonArra
         break;
     case JSON_NULL:
     default:
-        return CELIX_ILLEGAL_ARGUMENT; // TODO Add test for this case and 
maybe return a different error code and log error
+        return CELIX_ILLEGAL_ARGUMENT;
     }
 
     return CELIX_SUCCESS;
@@ -338,7 +401,7 @@ celix_properties_decodeArray(celix_properties_t* props, 
const char* key, const j
     celix_status_t status = celix_properties_determineArrayType(jsonArray, 
&elType);
     if (status != CELIX_SUCCESS && (flags & 
CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS)) {
         celix_autofree char* arrStr = json_dumps(jsonArray, JSON_ENCODE_ANY);
-        celix_err_pushf("Unsupported mixed or null array for key '%s': %s", 
key, arrStr);
+        celix_err_pushf("Invalid mixed or null array for key '%s': %s", key, 
arrStr);
         return status;
     } else if (status != CELIX_SUCCESS) {
         //ignore mixed types
@@ -378,7 +441,7 @@ celix_properties_decodeArray(celix_properties_t* props, 
const char* key, const j
         }
         default:
             // LCOV_EXCL_START
-            celix_err_pushf("Unexpected array list element type %d for key 
%s", elType, key);
+            celix_err_pushf("Invalid array list element type %d for key %s", 
elType, key);
             return CELIX_ILLEGAL_ARGUMENT;
             // LCOV_EXCL_STOP
         }
@@ -399,8 +462,9 @@ celix_properties_decodeValue(celix_properties_t* props, 
const char* key, json_t*
         return CELIX_SUCCESS; // ignore empty keys.
     }
 
-    if (celix_properties_hasKey(props, key) && (flags & 
CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES)) {
-        celix_err_pushf("Key `%s` already exists.", key);
+    if (!json_is_object(jsonValue) && celix_properties_hasKey(props, key) &&
+        (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS)) {
+        celix_err_pushf("Invalid key collision. Key '%s' already exists.", 
key);
         return CELIX_ILLEGAL_ARGUMENT;
     }
 
@@ -423,13 +487,14 @@ celix_properties_decodeValue(celix_properties_t* props, 
const char* key, json_t*
         const char* fieldName;
         json_t* fieldValue;
         json_object_foreach(jsonValue, fieldName, fieldValue) {
-            celix_autofree char* subKey;
-            int rc = asprintf(&subKey, "%s/%s", key, fieldName);
-            if (rc < 0) {
-                    celix_err_push("Failed to create sub key");
-                    return CELIX_ENOMEM;
+            char buf[64];
+            char* combinedKey = celix_utils_writeOrCreateString(buf, 
sizeof(buf), "%s/%s", key, fieldName);
+            celix_auto(celix_utils_string_guard_t) strGuard = 
celix_utils_stringGuard_init(buf, combinedKey);
+            if (!combinedKey) {
+                celix_err_push("Failed to create sub key");
+                return CELIX_ENOMEM;
             }
-            status = celix_properties_decodeValue(props, subKey, fieldValue, 
flags);
+            status = celix_properties_decodeValue(props, combinedKey, 
fieldValue, flags);
             if (status != CELIX_SUCCESS) {
                 return status;
             }
@@ -437,7 +502,7 @@ celix_properties_decodeValue(celix_properties_t* props, 
const char* key, json_t*
         return CELIX_SUCCESS;
     } else if (json_is_array(jsonValue) && json_array_size(jsonValue) == 0) {
         if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS) {
-            celix_err_pushf("Unexpected empty array for key `%s`", key);
+            celix_err_pushf("Invalid empty array for key '%s'", key);
             return CELIX_ILLEGAL_ARGUMENT;
         }
         // ignore empty arrays
@@ -446,14 +511,14 @@ celix_properties_decodeValue(celix_properties_t* props, 
const char* key, json_t*
         status = celix_properties_decodeArray(props, key, jsonValue, flags);
     } else if (json_is_null(jsonValue)) {
         if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES) {
-            celix_err_pushf("Unexpected null value for key `%s`", key);
+            celix_err_pushf("Invalid null value for key '%s'", key);
             return CELIX_ILLEGAL_ARGUMENT;
         }
         // ignore null values
         return CELIX_SUCCESS;
     } else {
         // LCOV_EXCL_START
-        celix_err_pushf("Unexpected json value type for key `%s`", key);
+        celix_err_pushf("Invalid json value type for key '%s'", key);
         return CELIX_ILLEGAL_ARGUMENT;
         // LCOV_EXCL_STOP
     }
@@ -484,7 +549,7 @@ static celix_status_t 
celix_properties_decodeFromJson(json_t* obj, int flags, ce
     return CELIX_SUCCESS;
 }
 
-celix_status_t celix_properties_decodeFromStream(FILE* stream, int 
decodeFlags, celix_properties_t** out) {
+celix_status_t celix_properties_loadFromStream(FILE* stream, int decodeFlags, 
celix_properties_t** out) {
     json_error_t jsonError;
     size_t jsonFlags = 0;
     if (decodeFlags & CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES) {
@@ -495,6 +560,27 @@ celix_status_t celix_properties_decodeFromStream(FILE* 
stream, int decodeFlags,
         celix_err_pushf("Failed to parse json: %s", jsonError.text);
         return CELIX_ILLEGAL_ARGUMENT;
     }
-
     return celix_properties_decodeFromJson(root, decodeFlags, out);
 }
+
+celix_status_t celix_properties_load2(const char* filename, int decodeFlags, 
celix_properties_t** out) {
+    FILE* stream = fopen(filename, "r");
+    if (!stream) {
+        celix_err_pushf("Failed to open file %s", filename);
+        return CELIX_FILE_IO_EXCEPTION;
+    }
+    celix_status_t status = celix_properties_loadFromStream(stream, 
decodeFlags, out);
+    fclose(stream);
+    return status;
+}
+
+celix_status_t celix_properties_loadFromString2(const char* input, int 
decodeFlags, celix_properties_t** out) {
+    FILE* stream = fmemopen((void*)input, strlen(input), "r");
+    if (!stream) {
+        celix_err_push("Failed to open memstream");
+        return CELIX_FILE_IO_EXCEPTION;
+    }
+    celix_status_t status = celix_properties_loadFromStream(stream, 
decodeFlags, out);
+    fclose(stream);
+    return status;
+}

Reply via email to